ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于IS61LV25616AL的FPGA异步SRAM读写控制与Verilog实现

基于IS61LV25616AL的FPGA异步SRAM读写控制与Verilog实现

简介:一套在Xilinx Spartan-3S400开发板上验证过的FPGA读写IS61LV25616AL SRAM的Verilog接口程序,面向FPGA学习者和嵌入式开发者,用于解决片内存储容量不足、外部SRAM地址/数据总线交互以及读写时序匹配问题。IS61LV25616AL是16位宽256K容量的低电压CMOS静态RAM,支持高速读写,并具有掉电保护功能;程序以状态机方式控制片选、读写和输出使能信号,并在开发板上实测稳定。压缩包共15个文件、约57KB,以3个Verilog源文件与ISE工程文件为主,另含用户约束文件、布局布线结果、位流报告和仿真相关记录,打开工程即可复现整个读写流程。目前已有1037人学习,适合用于FPGA外部存储器接口入门和课程设计参考。通过源码可掌握SRAM时序约束、片选时序、三态数据总线控制及硬件验证细节,同时附带IS61LV25616AL数据手册,为自查引脚和参数提供便利。

1. 直接操作 IS61LV25616AL 之前:先回答三个看似简单的问题

第一次在 FPGA 开发板上把一片 256K×16 的异步 SRAM 当作图像缓存来用,我差点以为这颗芯片是个黑匣子:读回来的数据和写进去的对不上,把访问速度调慢一档还是不稳定。IS61LV25616AL 本身不带同步时钟,所有读写动作都靠 CE_N、OE_N、WE_N 这几根控制线的电平顺序来触发,对用户的等待周期、三态控制和建立保持时间有硬性要求。网上能找到的 Verilog 参考程序往往只贴了状态机,却不解释为什么读操作要插一拍等待,为什么写时要把 OE_N 拉高。这篇笔记把一份可以直接拿进工程改的 SRAM 读写控制程序拆开,涵盖端口声明、时序核算、testbench 仿真和上板排坑,适合要在 FPGA 里接外部 SRAM 做缓存或数据缓冲的开发者。

2. 把 IS61LV25616AL 接到 Verilog 端口:地址、数据与字节使能的分工

2.1 引脚层面的分工:为什么 A 有 18 位、DQ 有 16 位

IS61LV25616AL 是一颗容量为 4 Mbit 的异步静态存储芯片,组织方式是 256K 深、16 位宽。256K 地址深度需要 18 根地址线 A[17:0],每次传输 16 bit 数据对应 DQ[15:0]。有的初学者看到地址线只有 18 根就开始怀疑容量计算,其实 2^18 乘以 16 bit 正好等于 4 Mbit,地址线和数据线是分开计算的。

这颗器件没有时钟引脚,所有动作都依靠控制线的组合电平驱动。下表是每个引脚组的职责。

引脚组位宽方向作用常规接法
A[17:0]18输入访问地址接 FPGA 普通 IO
DQ[15:0]16双向数据总线接 FPGA 的 IO,配合三态寄存器
CE_N1输入片选使能,低有效平时拉低,复用时可引出
OE_N1输入输出使能,低有效读时拉低,写时拉高
WE_N1输入写使能,低有效写操作期间拉低
UB_N / LB_N2输入高字节 / 低字节使能可单独引出或接地

字节使能这块最容易偷懒。我最早在某控制项目里把 UB_N 和 LB_N 直接接地,后来上层要支持 8 位字节访问,整个控制器推倒重来,白白浪费一个晚上。数据手册写得很清楚,UB_N 管 DQ[15:8],LB_N 管 DQ[7:0]。如果只做整体 16 位访问,两个脚都拉低不影响功能;但把使能脚做成模块端口再引出,以后换一颗支持字节掩码的 SRAM 或者要做半字更新时,就不用再改模块边界。

2.2 模块端口与三态总线的 Verilog 声明

控制器设计成总线桥结构,上层只需要给地址和读写请求,模块内部负责把所有引脚波形转换成 SRAM 能认的时序。

module sram_ctl #( parameter ADDR_W = 18, parameter DATA_W = 16 )( input wire clk, input wire rst_n, // 用户侧接口 input wire wr_req, input wire rd_req, input wire [ADDR_W-1:0] req_addr, input wire [DATA_W-1:0] wr_data, output reg [DATA_W-1:0] rd_data, output reg rd_valid, // SRAM 物理接口 output reg [ADDR_W-1:0] sram_addr, inout wire [DATA_W-1:0] sram_data, output reg sram_ce_n, output reg sram_oe_n, output reg sram_we_n, output reg sram_ub_n, output reg sram_lb_n );

逻辑说明:这个模块对外提供了一套同步读写握手信号,对内则产生异步 SRAM 需要的控制波形。sram_data 是 inout 型,不能在 always 块里直接赋值,必须通过三态门间接驱动,所以模块里要另存一根数据输出寄存器和一根输出使能信号。

参数说明:ADDR_W 和 DATA_W 是专门为 IS61LV25616AL 的 18 位地址 / 16 位数据准备的。如果换成一颗 1M×16 的 SRAM,把 ADDR_W 改成 20、地址按位宽展开即可,数据路径几乎不用动。rd_valid 和 rd_data 组合成读返回通道,上层看到 rd_valid 为高再去取 rd_data,就不用猜数据到底什么时候有效。

三态总线的写法是双向总线设计里最容易翻车的地方:

reg [DATA_W-1:0] sram_dout; reg sram_dq_oe; assign sram_data = sram_dq_oe ? sram_dout : {DATA_W{1'bz}};

逻辑说明:sram_dq_oe 为 1 时,FPGA 主动驱动数据总线;为 0 时,总线释放成高阻,由外部 SRAM 通过 OE_N 把数据输出上来。读操作期间,sram_dq_oe 必须比 OE_N 的下降沿更早拉低,否则两个驱动源会在 DQ 上打架。这里有个隐蔽点:sram_dq_oe 和 sram_oe_n 是两根独立信号,SRAM 只有在 CE_N 和 OE_N 同时为低且 WE_N 为高时才输出数据,FPGA 侧必须先释放自己的总线,再去拉低 OE_N。

参数说明:{DATA_W{1'bz}}是 Verilog 里把高阻态扩展成 16 位宽的标准写法。不要图省事写16'bz,那只是 1 bit 的高阻值再做符号扩展,仿真波形里看不出问题,综合结果却可能变成未知态。我第一次调这段代码时在这行上盯了半个小时的 wave 窗口,最后才发现是位宽失配。

2.3 为什么用异步 SRAM 而不是同步 SRAM

同步 SRAM 或流水线 SRAM 带时钟引脚,理论上可以连续流水读,但控制逻辑更重:地址预加载一拍,数据输出再等一拍,中间时序全受时钟约束,接口协议比异步 SRAM 复杂一个量级。IS61LV25616AL 这类异步器件胜在接口直观,适合中小容量、突发性不强的缓冲场景,例如图像帧缓存、CPU 和 FPGA 之间的握手缓冲、低速采集数据的暂存。

如果要做几百兆字节每秒的大带宽缓冲,异步 SRAM 会卡在 tAA 和最小写周期上,这时候该选别的存储。反过来,在 50MHz 或 100MHz 总线下做数据缓冲,异步 SRAM 的控制逻辑一晚上就能调通,而且用 ILA 或示波器都容易观察,不像 DDR 接口那样需要复杂的培训序列和物理层校准。选型时要权衡的是需求带宽和调试便利,而不是单纯比芯片标称频率。

3. 用状态机写出一位不差的时序:读周期和写周期的参数核算

3.1 写周期状态机与 WE 的先后次序

很多示例代码把写操作压缩成两拍:第一拍给地址和 CE,第二拍给 WE 和写入数据。看似省资源,但这里忽略了异步 SRAM 的一个特性:WE_N 拉低之前,地址和数据必须已经在总线上稳定。如果地址和 WE 在同一拍翻转,SRAM 内部可能先识别成一次读周期,再切回写周期,写入地址和数据都会错位。

我的做法是把写操作拆成 IDLE、WR_SETUP、WR_ACTIVE、WR_RELEASE 四个状态,用多一个周期来换时序余量。

localparam IDLE = 4'd0; localparam WR_SETUP = 4'd1; localparam WR_ACTIVE = 4'd2; localparam WR_RELEASE = 4'd3; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; sram_we_n <= 1'b1; sram_ce_n <= 1'b1; sram_oe_n <= 1'b1; sram_dq_oe <= 1'b0; sram_addr <= 18'd0; sram_dout <= 16'd0; end else begin case (state) IDLE: begin if (wr_req && !rd_req) begin sram_ce_n <= 1'b0; sram_addr <= req_addr; sram_dout <= wr_data; sram_dq_oe <= 1'b1; // 写周期里 DQ 由 FPGA 驱动 sram_oe_n <= 1'b1; // 写时强制 OE 为高,避免总线竞争 state <= WR_SETUP; end end WR_SETUP: begin sram_we_n <= 1'b0; // 地址和 TE 就绪后拉低 WE state <= WR_ACTIVE; end WR_ACTIVE: begin sram_we_n <= 1'b1; // 保持低电平时间至少满足 tWP state <= WR_RELEASE; end WR_RELEASE: begin sram_ce_n <= 1'b1; sram_dq_oe <= 1'b0; // 释放数据总线,恢复读状态 state <= IDLE; end endcase end end

逻辑说明:状态机里最关键的步骤是 IDLE 状态先把地址和数据放在总线上,下一拍才把 WE_N 拉低。这样从地址稳定到 WE 下降沿至少隔了一个时钟周期,地址建立时间天然满足。WR_ACTIVE 是一拍,这一拍要保证 WE 低电平持续时间达到数据手册的 tWP;如果主时钟周期小于 tWP,就必须在中间插入额外状态。

参数说明:sram_oe_n 在写周期里必须保持为高。当 WE 和 OE 同时有效时,SRAM 内部读逻辑会把存储阵列里的旧数据往 DQ 上推,外部又把新数据往里灌,两个驱动源撞在一起,写操作会变得极不稳定。数据手册里写周期部分也明确给了一种典型的 OE 拉高时序,这里不是因为强迫症,而是因为正确的总线分层本来就是这么设计。

3.2 读周期状态机:插入等待拍才是对时序表的尊重

读操作比写操作更讲究采样点。从地址有效到数据出现在 DQ 上,需要等 tAA(地址到输出延迟)和 tOE(OE 到输出延迟)两个时间常数。如果 FPGA 在给出地址的同一拍就去锁存数据,几乎必然读回上一笔残留值。

localparam RD_SETUP = 4'd4; localparam RD_ACCESS = 4'd5; IDLE: begin if (rd_req && !wr_req) begin sram_ce_n <= 1'b0; sram_oe_n <= 1'b0; sram_addr <= req_addr; sram_dq_oe <= 1'b0; // 读前必须先释放 DQ state <= RD_SETUP; end end RD_SETUP: begin state <= RD_ACCESS; end RD_ACCESS: begin rd_data <= sram_data; // 访问周期末端完成采样 rd_valid <= 1'b1; state <= IDLE; end

逻辑说明:IDLE 状态发起地址并拉低 CE_N 和 OE_N。RD_SETUP 让 SRAM 内部地址译码和读出放大器稳定一拍,RD_ACCESS 里锁存数据。RD_ACCESS 结束后 rd_valid 拉高,告诉上层数据已经有效。这里故意把 rd_valid 设在数据锁存的同一拍,而不是在 IDLE 状态输出,避免上层在下一个周期误取到未更新的数据。

参数说明:RD_SETUP 和 RD_ACCESS 的停留拍数由主时钟周期决定。如果时钟是 50MHz,一个周期 20ns,而 SRAM 的 tAA 约 10ns 量级,各停一拍已经很宽裕;如果时钟升到 100MHz,一个周期只有 10ns,rd_data 的锁存点就非常接近 SRAM 的访问时间极限。稳妥做法是让 RD_SETUP 和 RD_ACCESS 都占两个周期,用一点延迟换取确定性的数据采样。

3.3 时钟频率和等待周期的换算:用表格把参数用起来

我习惯把每个用到的速度等级下的时序参数整理成一张表,放在模块头部注释里,调频率时直接照着算。下表以常见的 -10 速等级为例,具体数值要翻开你手上那批料的规格书确认。

参数含义参考最小值设计折算
tAA地址到数据输出约 10ns读等待周期 >= ceil(tAA / Tclk)
tWPWE 低电平脉宽约 9ns至少占 2 个时钟周期
tDS数据建立时间几 nsWE 拉高前至少一拍把数据放稳
tDH数据保持时间几 nsWE 拉高后继续保护一拍

折算时还要把 FPGA 引脚到内部逻辑的路径延迟算进去,一般两端各加 1~2ns。多插几拍等待状态本质上是用时间换可靠性;异步 SRAM 本来就没有突发模式,多两三拍时钟带来的性能损失,远小于上板后拿着逻辑分析仪猜数据错位的代价。时序余量这东西,在手边没有示波器之前,每个人的直觉都是玄学,只有把计算表列出来,心里才有底。

4. 在 testbench 里还原异步 SRAM 行为:仿真模型与控制器的联调

4.1 一个够用的 SRAM 行为模型

仿真异步 SRAM 不需要把晶体管级行为搬过来,但要准确模拟两件关键的事:写周期里 DQ 会被外部驱动、读周期里 DQ 由内部数据驱动。下面的模型用always块描述写透明现象,用条件赋值描述读输出。

module is61lv25616al_model #( parameter ADDR_WIDTH = 18, parameter DATA_WIDTH = 16 )( input wire [ADDR_WIDTH-1:0] A, inout wire [DATA_WIDTH-1:0] DQ, input wire CE_N, OE_N, WE_N, UB_N, LB_N ); reg [DATA_WIDTH-1:0] mem [0:(1<<ADDR_WIDTH)-1]; reg [DATA_WIDTH-1:0] dout; wire valid_read = !CE_N && !OE_N && WE_N; assign DQ = valid_read ? dout : {DATA_WIDTH{1'bz}}; // 写使能有效期间持续写入 always @(*) begin if (!CE_N && !WE_N) begin mem[A] = DQ; end end // 读输出随地址和使能变化 always @(*) begin dout = mem[A]; end endmodule

逻辑说明:valid_read 信号描述了读条件:CE_N 和 OE_N 都为低,且 WE_N 为高时才把存储内容驱动到 DQ 上。写条件的判断是 CE_N 和 WE_N 同为低,此时数据总线上的电平持续写入存储单元,这和异步 SRAM 的透明写特性一致。UB_N 和 LB_N 在这个简化模型里没有单独处理,如果需要仿真字节操作,可以把两个使能信号加进写条件,分别控制高 8 位和低 8 位是否写入。

参数说明:mem 的深度用(1<<ADDR_WIDTH)-1表示,地址位宽变化时模型自动跟随。这比直接写数字深度的通用性好,下次复制到别的 SRAM 工程不用改内部代码。

4.2 用任务测读写一致性

控制器和模型都准备好了,testbench 里要做的就是把两个模块连接起来,再写几个简单的测试任务。下面这段代码是自检式测试的一部分,重点在设计 watchdog 逻辑,而不是打印波形。

module tb_sram_ctl; reg clk = 0; reg rst_n = 0; always #10 clk = ~clk; // 50MHz 时钟 reg wr_req; reg rd_req; reg [17:0] req_addr; reg [15:0] wr_data; wire [15:0] rd_data; wire [17:0] sram_addr; wire sram_ce_n, sram_oe_n, sram_we_n; wire sram_ub_n, sram_lb_n; sram_ctl dut ( .clk(clk), .rst_n(rst_n), .wr_req(wr_req), .rd_req(rd_req), .req_addr(req_addr), .wr_data(wr_data), .rd_data(rd_data), .rd_valid(rd_valid), .sram_addr(sram_addr), .sram_data(sram_data), .sram_ce_n(sram_ce_n), .sram_oe_n(sram_oe_n), .sram_we_n(sram_we_n), .sram_ub_n(sram_ub_n), .sram_lb_n(sram_lb_n) ); is61lv25616al_model u_sram ( .A(sram_addr), .DQ(sram_data), .CE_N(sram_ce_n), .OE_N(sram_oe_n), .WE_N(sram_we_n), .UB_N(sram_ub_n), .LB_N(sram_lb_n) ); initial begin rst_n = 0; wr_req = 0; rd_req = 0; #100; rst_n = 1; // 写地址 0x00001,数据 0xA5A5 @(posedge clk); req_addr = 18'h00001; wr_data = 16'hA5A5; wr_req = 1; @(posedge clk); wr_req = 0; // 等待控制模块完成,再发起读 repeat (10) @(posedge clk); req_addr = 18'h00001; rd_req = 1; @(posedge clk); rd_req = 0; // 等 rd_valid 拉高后比较 wait (rd_valid); if (rd_data !== 16'hA5A5) begin $display("ERROR: addr 0x00001 readback = %04x", rd_data); $finish; end else begin $display("PASS: addr 0x00001 readback = %04x", rd_data); end $finish; end initial begin $dumpfile("tb_sram_ctl.vcd"); $dumpvars(0, tb_sram_ctl); end endmodule

逻辑说明:测试流程是先执行一次写请求,等控制模块内部状态机跑完,再发读请求,最后通过 rd_valid 握手信号确认数据有效。比较语句用了!==而不是!=,目的是把 x 态和 z 态也纳入比较,确保三态总线没有残留高阻值。

参数说明:repeat(10) 是一个保守的等待时间,因为控制模块在写请求后还要经过 WR_SETUP、WR_ACTIVE、WR_RELEASE 三个状态才回到 IDLE,至少需要 3 个时钟周期;等待 10 拍是为了给后续改动留余量。如果你把写状态机改成更多等待周期,这个数字要跟着调整。

5. 上板后的避坑与排查:四类翻车现场的定位与修复

5.1 现象一:读回的数据整段清零

现象:写进去 0xAAAA,读出来 0x0000;换地址再试依旧如此。

原因:写周期内 FPGA 没有正确驱动数据总线,或者 WE_N 拉低时数据还没有建立起来。常见原因是 IDLE 状态里只给了地址,忘拉 sram_dq_oe,SRAM 在 WE_N 有效期间读到的数据总线是高阻态;另一个原因是写期间 OE_N 没拉高,内部读路径和外部写路径在 DQ 上形成了总线竞争,把写入电平钳在了低电平。

解决:检查 IDLE 状态里 sram_dq_oe 是否在进入 WR_SETUP 前置 1,且写请求对应的 sram_dout 要先于或等于 WE_N 下降沿稳定。再检查 sram_oe_n 在整个写周期里是否为高。两者都正确后,写数据大概率能落进存储单元。

5.2 现象二:数据在高位和低位之间错位

现象:写入{16'h1234},读回来是倒过来的16'h3412,字节顺序反了。

原因:UB_N 和 LB_N 接反了,或者数据总线的高低位排列与你的打印函数不一致。IS61LV25616AL 的 DQ[15:8] 对应高字节,DQ[7:0] 对应低字节,如果 FPGA 侧把数据线的中间两位交叉,或者字节使能反接,就会读出这种对称性错位。

解决:最先检查顶层实例化的端口连接,sram_data[15:0] 和 sram_addr[17:0] 的位序必须一一对应。不要在代码里做端序反转来掩盖硬件错误,那等于给后面每块板子埋雷。如果确信端口没有交叉,再看打印函数用的是大端显示还是小端显示。

5.3 现象三:仿真全对,上板随机错数

现象:连续写 100 个地址,总有一两个地址的数据被相邻地址覆盖,且位置不固定。

原因:仿真模型里的地址跳变是理想的,但真实 PCB 走线存在延迟,地址线之间可能产生几纳秒的时差。如果 WE_N 刚好在这几纳秒的低电平窗口内,SRAM 抓到的地址就是中间态,写入位置会落到邻居地址。这属于典型的地线噪声和建立时间不足问题。

解决:在写请求到来前让地址提前一个周期在总线上稳定,把 WE_N 拉低的位置尽量靠近时钟沿末端,给地址线留出余量。另外检查 SRAM 引脚附近有没有加 0.1uF 去耦电容,地址线有没有过长或串连电阻。这类问题示波器上看最直接,没有示波器就用 ILA 抓连续写入时的地址总线毛刺。

5.4 现象四:降频才能跑通,升频就挂

现象:50MHz 下连续读写完全正常,改成 100MHz 后读操作隔几个周期就出错一次。

原因:访问时间余量不够。100MHz 下读写状态机的等待周期如果只按 tAA 的整数倍取整,片内路径延迟和 PCB 走线延迟会把余量吃掉。另一个可能是综合工具没有对 sram_addr 和 sram_data 的 IO 路径设置时序约束,默认走线摆放很差。

解决:先给 SRAM 接口加上明确的输入输出延迟约束,地址和数据总线约束在同一组引脚组里。然后回到第 3 章的折算表,把读等待和写等待各增加一拍,让实际窗口大于 SRAM 手册值 3ns 以上。如果加一拍后稳定了,说明问题在时序余量;如果还出错,就要检查器件速度和 PCB 布线质量。

6. 进阶:用 ILA 抓真实总线波形,把最后一点不确定从系统里拿掉

6.1 ILA 的触发条件怎么设才有价值

仿真是理想世界,上板后的信号质量才是真实世界。ILA 的触发条件设计好比照妖镜,要把最容易出错的时刻锁定下来。我会在状态机的 state 信号、wr_req、rd_valid 几根信号上做触发,采样深度设成 1024,采样时钟直接使用控制器 clk。

(* keep = "true" *) reg [2:0] debug_state; assign debug_state = state;

逻辑说明:加 keep 约束是为了防止综合器把 state 寄存器优化掉,ILA 采不到关键状态位。如果当前 FPGA 工具会在综合后改变寄存器命名,就去综合网表里抓一个保留信号,再映射到 ILA 的 probe 端口。

6.2 一段采样记录怎么看

抓到的波形里,最值得关注的是 SRAM 的 OE_N 下降沿和 rd_data 采样点的相对位置。正常情况下,OE_N 下降沿到 rd_data 锁存之间至少要隔开一个完整周期;如果波形里两者几乎重叠,说明你的状态机里读等待周期正好踩在访问时间边缘上。这种问题在仿真里看不出来,因为仿真模型不包含 IO pad 延迟。我就曾经因为忽略这一步,在一批板子上偶发错数,最后加了一拍等待周期才稳定。从那以后我每次做异步 SRAM 接口都会强制走一遍:先看时序参数表,再写状态机,最后用 ILA 核对真实波形,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进