FPGA驱动异步SRAM IS61LV25616AL:Verilog控制器设计与时序避坑

发布时间:2026/10/23 12:16:57
FPGA驱动异步SRAM IS61LV25616AL:Verilog控制器设计与时序避坑
简介面向FPGA初学者与嵌入式系统开发者这份资源提供了一套经XSC3S400开发板实际验证的IS61LV25616AL读写接口Verilog程序。IS61LV25616AL是16位宽、256K容量的低电压CMOS SRAM常用来扩展FPGA有限的内置存储程序中地址总线、数据总线、读写控制与片选信号的时序设计均清晰可读完整展示了从端口声明、读写状态跳转到数据读回的实现通路。压缩包共15个文件、约57KB包含.v源代码、.ucf引脚约束文件、ISE工程配置.xise/.gise以及综合报告和HTML说明页面便于对照项目结构快速重建并移植。目前已有1038人学习。借助源码与工程文件读者可深入理解FPGA与外部SRAM的交互时序掌握读写状态机设计方法并能迁移到其他存储芯片或开发平台是FPGA存储接口入门、课程设计与竞赛准备的实用参考。1. 为什么说 IS61LV25616AL 没有看起来那么省心第一次把 IS61LV25616AL 挂到 FPGA 上时我一度以为这不过是个“地址拉上去、数据自动出来”的简单外设。真去读数据手册才发现这颗 256K×16 的异步 SRAM 虽然不需要时钟但每一次读写都得卡住 CE#、OE#、WE# 的先后顺序连字节使能 UB#/LB# 也不能随便放飞。网上一搜“FPGA verilog 读写 SRAM”资料很多但能直接拿来用、状态机写得清楚、仿真能跑通的不多。这个项目的价值就是把这种“看似简单实则到处是时序陷阱”的芯片封装成一个参数可调、握手清晰、便于二次开发的 Verilog 控制器。无论你是做图像缓存、软核 CPU 的局部内存还是纯粹想在 FPGA 上体验并行总线它都很合适它也是一份很好的“异步器件被同步状态机驱动”的活教材。2. 硬件接口与工程组织先把引脚和时序握在手里2.1 IS61LV25616AL 的关键信号与电气参数IS61LV25616AL 是一颗 3.3V 供电的异步 CMOS SRAM容量是 256K × 16bit也就是 512KB。没有内部时钟所有动作都由外部控制信号的电平组合触发。FPGA 这边必须自己产生一套符合时序要求的地址、读写控制信号并把双向数据总线管好。这颗芯片的引脚分成四组18 根地址线、16 根数据线、5 根控制线和电源地。控制线里最容易搞混的是 CE#、OE#、WE# 和 UB#/LB#。CE# 是片选整颗芯片是否工作OE# 只管读操作时数据能不能输出到 DQWE# 只管写操作UB#/LB# 决定 D[15:8] 和 D[7:0] 哪一半参与当前访问。信号方向作用A[17:0]输入地址总线18 位对应 256K 个 16bit 单元D[15:0]双向数据总线读时由 SRAM 驱动写时由 FPGA 驱动CE#输入片选低有效拉高后芯片处于休眠OE#输入输出使能低有效影响读时序WE#输入写使能低有效影响写时序UB#输入高字节选择低有效控制 D[15:8]LB#输入低字节选择低有效控制 D[7:0]数据手册上最重要的参数是 tAA地址到数据有效时间、tDOEOE# 下降沿到数据有效时间和 tPWEWE# 低电平脉冲宽度。常见的访问时间档位有 10ns、12ns、15ns具体取决于后缀。FPGA 的时钟周期必须留够余量。如果系统时钟 50MHz一个周期 20ns不加等待也够如果是 100MHz 系统时钟一个周期才 10ns就需要状态机里至少插入 24 个时钟周期来等待 SRAM 响应。2.2 工程目录与 RTL 文件划分拿到这个工程后先看目录结构。一个合格的异步 SRAM 读写工程通常不会把所有代码堆在一个顶层文件里。我一般习惯这样组织sram_demo/ ├── rtl/ │ ├── sram_controller.v // 核心状态机只负责一次读写 │ ├── top.v // 顶层绑定物理引脚 │ └── uart_debug.v // 可选串口命令解析/数据回显 ├── sim/ │ ├── sram_tb.v // 测试平台验证读写流程 │ └── is61lv25616al_model.v // SRAM行为模拟模型 ├── constraints/ │ └── pins.xdc // 引脚分配和时序约束 └── scripts/ └── run_sim.tcl // Vivado/Quartus仿真脚本sram_controller.v 是核心模块它对外暴露的接口很像一个内存控制器start、rw、addr、wdata、rdata、busy。顶层模块不直接碰 SRAM 信号只调用这个控制器。这样的好处是如果以后把 IS61LV25616AL 换成别的异步 SRAM只需要改控制器内部实现顶层的逻辑不用动。控制器内部采用同步状态机设计。所有 SRAM 控制信号都在系统时钟的上升沿改变而不是用组合逻辑直接产生。异步 SRAM 本身没有时钟但我们用同步设计去驱动它就能避免毛刺、抖动和综合后时序难收敛的问题。每次访问由 start 发起busy 拉高等到状态机走完一个完整读写周期后busy 拉低表示一次访问结束。工程里如果只做读写测试可以加一个简单的串口模块在 PC 上发命令来写某个地址、读某个地址如果要看波形也可以直接用 ILA 抓 SRAM 引脚。下面第三章先把最核心的控制器讲清楚。3. 核心模块拆解一个带状态机的 SRAM 访问控制器3.1 读写状态机IDLE → ACCESS → DONE这个控制器只有三个状态IDLE、ACCESS、DONE。IDLE 等待外部发起请求ACCESS 中保持地址、使能信号并等待足够的时钟周期DONE 负责把总线释放、恢复片选并通知上层一次访问结束。先看完整的 RTL 代码这是整个工程里最值得抄走的一段// sram_controller.v // 参数 CLK_CYCLES 控制一次访问经历多少个系统时钟周期 module sram_controller #( parameter CLK_CYCLES 4 )( input wire clk, input wire rst_n, input wire start, // 发起一次读写 input wire rw, // 1: 写, 0: 读 input wire [17:0] addr, // 读写地址 input wire [15:0] wdata, // 写数据 output reg [15:0] rdata, // 读数据 output reg busy, // 忙信号 // 接物理 SRAM 的信号 output reg [17:0] sram_addr, inout wire [15: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 ); localparam IDLE 2d0; localparam ACCESS 2d1; localparam DONE 2d2; reg [1:0] state; reg [4:0] cnt; reg [15:0] dout; reg driving; // 1: FPGA驱动sram_data; 0: 释放总线 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; cnt 0; busy 1b0; driving 1b0; sram_ce_n 1b1; sram_oe_n 1b1; sram_we_n 1b1; sram_ub_n 1b1; sram_lb_n 1b1; sram_addr 18d0; end else begin case (state) IDLE: begin if (start !busy) begin busy 1b1; sram_ce_n 1b0; sram_ub_n 1b0; sram_lb_n 1b0; sram_addr addr; if (rw) begin // 写访问拉低 WE把数据放到总线上 sram_we_n 1b0; sram_oe_n 1b1; dout wdata; driving 1b1; end else begin // 读访问拉低 OE数据由 SRAM 驱动 sram_we_n 1b1; sram_oe_n 1b0; driving 1b0; end cnt 0; state ACCESS; end end ACCESS: begin cnt cnt 1b1; if (cnt CLK_CYCLES - 1) begin // 最后一个等待周期 if (!rw !driving) begin // 读且数据总线由SRAM驱动 rdata sram_data; end // 结束访问抬高 WE/OE下一步释放数据总线 sram_we_n 1b1; sram_oe_n 1b1; state DONE; end end DONE: begin sram_ce_n 1b1; sram_ub_n 1b1; sram_lb_n 1b1; driving 1b0; busy 1b0; state IDLE; end endcase end end // 双向数据总线的三态控制 assign sram_data driving ? dout : 16hzzzz; endmodule这段代码里有几个关键点需要展开讲。第一三态总线。sram_data 声明为 inout wire不能直接赋值。我单独用driving寄存器控制是让 FPGA 驱动数据还是把总线释放给 SRAM。写操作时driving1sram_data 被赋上 dout读操作时driving0sram_data 变成高阻zzzz此时 SRAM 的 OE# 为低数据自然从 SRAM 芯片输出到总线上。仿真里最常见的翻车就是把inout口当普通 output 用导致多驱动源冲突。第二CLK_CYCLES 参数。这个参数是一次访问从发起信号生效到采到数据或写完要跨多少个系统时钟周期。默认取 4在 50MHz 时钟下就是 80ns对于 10ns 访问时间的 SRAM 来说非常保守在 100MHz 下就是 40ns也够用。你完全可以根据自己的时钟频率和芯片速度等级把它改小到 2 或 3。需要注意最小不要设为 1否则状态机中的判断条件会和计数器更新产生奇怪的竞争数据可能早一个周期被采样。第三字节使能。代码里在每次访问时把 UB# 和 LB# 都拉低也就是默认 16 位全读写。这是最简单的用法。如果你只想写一个字节就需要多引入一个字节选择信号这会在后面第六章改造时展开。3.2 顶层集成与三态总线的处理顶层模块的作用是把控制器和物理引脚绑起来。这部分代码量少但容易踩坑尤其是 inout 总线在顶层如何连线。// top.v 片段 module top ( input wire clk, input wire rst_n, // 另一侧来自用户逻辑 input wire start, input wire rw, input wire [17:0] addr, input wire [15:0] wdata, output wire [15:0] rdata, output wire busy, // 物理 SRAM 引脚 output wire [17:0] sram_addr_pin, inout wire [15:0] sram_data_pin, output wire sram_ce_n_pin, output wire sram_oe_n_pin, output wire sram_we_n_pin, output wire sram_ub_n_pin, output wire sram_lb_n_pin ); sram_controller #( .CLK_CYCLES(4) ) u_sram ( .clk (clk), .rst_n (rst_n), .start (start), .rw (rw), .addr (addr), .wdata (wdata), .rdata (rdata), .busy (busy), .sram_addr (sram_addr_pin), .sram_data (sram_data_pin), .sram_ce_n (sram_ce_n_pin), .sram_oe_n (sram_oe_n_pin), .sram_we_n (sram_we_n_pin), .sram_ub_n (sram_ub_n_pin), .sram_lb_n (sram_lb_n_pin) ); endmodule在约束文件里这些线对应 FPGA 的物理封装脚。数据总线 sram_data_pin 必须是支持三态的引脚绝大多数 FPGA bank 都满足。关键约束是不能让综合工具给 SRAM 控制信号做跨时钟域处理因为它们本身就是和系统时钟同源的。需要在 XDC 里对输入时钟做create_clock其余信号通过set_input_delay/set_output_delay约束到芯片的 tAA、tDOE 等参数上。初学者为了跑通可以直接只做时钟约束和引脚分配WR、OE、CE 这些信号由于状态机同步产生物理时序一般也能稳定但想要跨到高频 100MHz 以上必须把延迟补进约束里。3.3 仿真模型testbench 里模拟 SRAM 行为控制器写完后不要急着上板。先写一个 SRAM 行为模型在 testbench 里把读写验证一遍。IS61LV25616AL 没有时钟行为模型的核心就是用一个内存数组存数据再按照 OE# 和 CE# 的电平把数据放到总线上。下面是一个足够用的行为模型片段// is61lv25616al_model.v 仿真专用 module is61lv25616al_model ( input wire [17:0] A, inout wire [15:0] DQ, input wire CE_n, input wire OE_n, input wire WE_n, input wire UB_n, input wire LB_n ); reg [15:0] mem [0:(118)-1]; // 读操作只有片选、输出使能同时有效时才驱动总线 assign DQ (!CE_n !OE_n WE_n) ? mem[A] : 16hzzzz; // 写操作在 WE_n 上升沿锁存数据 reg [17:0] addr_latch; reg [15:0] data_latch; reg we_prev; always (*) begin if (!CE_n !WE_n) begin addr_latch A; data_latch DQ; end end always (posedge WE_n) begin if (!CE_n) begin // 写入时同时考虑字节使能 if (!UB_n) mem[addr_latch][15:8] data_latch[15:8]; if (!LB_n) mem[addr_latch][7:0] data_latch[7:0]; end end endmodule这个模型里读操作是组合逻辑行为OE# 一拉低数据立即出现在总线上没有模拟实际 SRAM 几纳秒的访问延迟。仿真时如果你关心 tAA 延迟可以在 model 里加#10ns的延迟赋值。不过对于验证状态机流程组合行为已经足够看到读写时序是否正确。上面的写操作模型用posedge WE_n锁存数据这是模拟异步 SRAM 内部锁存行为的关键点。实际芯片是在 WE# 上升沿那一刻把数据总线上的值抓进去因此我们测试写入时必须在 WE# 拉高之前让数据保持稳定否则仿真中会出现 X 态或写入错误值。4. 避坑指南从仿真到上板最容易翻车的五件事4.1 启动与复位类的坑现象 1上板后读取一直返回 0xFFFF 或 0x0000仿真却一切正常。这是最常见的“仿真过了一上板就翻车”场景。仿真时 testbench 把地址线、数据线全部拉得干干净净但真实 FPGA 上SRAM 的引脚可能没接好、约束没做对或者 OE# 和 WE# 的电平在初始化阶段不确定。原因主要有两个一是复位后 sram_ce_n 没有立刻拉高SRAM 在上电瞬间可能进入非法状态二是 XDC 里数据总线引脚漏分配综合工具默认把未用引脚当成普通 IO导致物理上读不到数据。解决方法是先把复位逻辑拉长至少保持几十微秒确保 FPGA 配置完成后 SRAM 电源稳定、控制线确实输出确定电平。再检查 XDC 里是否给数据总线、地址总线分配了正确的 bank 电压和引脚。最后用 ILA 抓 sram_ce_n、sram_oe_n 和 sram_data_pin看读周期里数据到底有没有出现在总线上。现象 2复位刚释放就开始连续读写状态机忙信号处理不对。如果 controller 之外的上层逻辑在 rst_n 拉高后的第一个周期就发起 start此时 busy 可能还没有完全释放导致 start 被漏掉。原因是 IDLE 状态下只判断start !busy但 busy 是由 DONE 状态拉低后一拍才变为 0。如果上层在 busy 为 1 时继续发访问请求请求会被忽略。解决办法是上层握手协议必须跟上。一般做法是每次发起访问前检测 busy 是否为低检测到低后再打一拍 start。或者更简单的做法把 start 当成“请求边沿”只在 busy 拉低后的第二个周期才检测。我习惯在顶层加一个简单的 FIFO把访问请求缓存起来控制器空闲时从 FIFO 里取任务这样就避免了 start/busy 的时序竞态。4.2 读写时序类的坑现象 3写操作后马上读同一个地址读回来的是旧值或不定值。这是异步 SRAM 最经典的时序坑。SRAM 的写周期要求在 WE# 下降沿之前地址有效、CE# 有效在 WE# 上升沿附近数据必须满足建立时间和保持时间。很多初学者写的状态机在 WE# 拉高的同时就把 sram_data 总线释放掉相当于数据和写结束边沿同时变化SRAM 内部大概率锁存到高阻态或前一拍的数据。解决方法是让数据总线多保持一拍。我上面代码里特意把 driving 信号放到 DONE 状态再拉低而不是在 ACCESS 最后一个周期就释放。这样 WE# 拉高后的下一个时钟沿数据仍然稳定满足常见 SRAM 至少 0ns 的保持时间要求。如果你要加 margin可以在 DONE 状态里再多等一个周期再释放。现象 4多个逻辑模块共享数据总线读操作时出现冲突甚至能感觉到芯片发烫。IS61LV25616AL 的数据总线是双向的。当控制器处于读状态时FPGA 一侧必须把总线释放掉输出高阻如果用户代码里不小心在同一个 inout 线上用了两个assign或者顶层例化了两个 controller 都接在同一组引脚读写时就会产生两个驱动源轻则波形异常重则短路发烫。解决办法是数据总线在 FPGA 内部必须只经过一个三态驱动器。所有模块若要访问 SRAM应当通过一个仲裁器或一个 controller 实例统一管理。我一般不会在多个 RTL 模块里直接引用 sram_data而是把所有读写请求汇聚到一个 memory access 层再由这一层去操作 controller。现象 5只修改了一个字节另一个字节被意外覆盖。因为 IS61LV25616AL 有 UB#/LB# 两个字节使能引脚如果一直拉低16 位访问和 8 位访问就无法区分。写入 8bit 数据时若 LB# 为低低字节会被写同时如果 UB# 也是低高字节也会被写入——哪怕高字节数据总线是高阻SRAM 也会把高阻值锁存进去变成不确定数据。解决办法是控制器增加一个byte_en[1:0]输入写操作时根据它控制 UB#/LB#。例如只写低字节时UB_n1, LB_n0只写高字节时UB_n0, LB_n1。读操作也可以按字节读但读的时候未选中的字节数据不会进总线rdata 对应位会变成 0 或保持之前的值上层逻辑需要自行掩码。5. 运行与验证在 FPGA 上把数据写进去读出来5.1 用你的综合工具跑仿真拿到这个工程第一步不是上板而是跑仿真。在 sim 目录下打开 testbench里面有完整的读写激励先写地址 0x00000再读回来比对然后写几个随机地址验证不同地址的读写是否正确。如果你用的是开源仿真工具可以直接用 iverilog 跑iverilog -g2012 -o sram_tb sim/sram_tb.v rtl/sram_controller.v sim/is61lv25616al_model.v vvp sram_tb如果用的是 Vivado 或 Quartus 自带的仿真器直接在工程里把 sram_controller.v 和 is61lv25616al_model.v 加入仿真文件集然后运行 behavioral simulation。仿真脚本一般长这样Vivado XSim 命令launch_simulation add_wave /sram_tb/* run -all仿真波形里重点看几个信号cnt递增时sram_we_n和sram_oe_n是否按访问类型变化写操作时sram_data是否在 WE# 上升沿前后保持稳定读操作时 rdata 采到的值和写进去的值是否一致。testbench 里还有一个check_result任务它会自动比较写入和读出的数据并在终端打印 PASS/FAIL 信息。仿真通过后再进行上板实测。5.2 通过 ILA 或逻辑分析仪进行板级验证上板验证有两种常见做法。第一种是直接用 FPGA 内部逻辑分析仪ILA在综合工具里把 sram_ce_n、sram_we_n、sram_oe_n、sram_addr、sram_data 这些信号加入 debug 核设置触发条件为start 1或busy 1跑一段时间后拉取波形。ILA 的好处是不需要额外外部仪器缺点是它会占用 FPGA 内部 BRAM 和布线资源而且触发深度有限。第二种做法是在工程里加一个 UART 串口调试模块PC 通过串口发送“写地址数据”或“读地址”命令FPGA 解析后调用 sram_controller再把结果通过串口发回。这个方式适合持续读写测试尤其适合验证 256K 地址范围内的随机读写稳定性。我建议第一次上板先做固定地址的回环测试写 0xAAAA、0x5555、0x1234再读回来把结果通过 UART 输出。如果这三个地址都通过再遍历整片内存写一遍递增数据再读一遍校验。这个流程能快速暴露地址线虚焊、数据线接反、字节使能错误等物理层问题。5.3 约束文件中的关键设置引脚约束和时序约束是上板前不能省的一步。在 constraints 文件里至少要做三件事。第一分配物理引脚。地址线 A[17:0]、数据线 D[15:0]、控制线都要映射到 FPGA 封装的某个 bank 脚位并且确保 bank 电压是 3.3V与 SRAM 电源一致。引脚分配错了仿真再漂亮也没用。第二创建时钟约束。如果你的系统时钟是外部 50MHz 晶振就在 XDC 里写create_clock -name sys_clk -period 20.000 [get_ports clk]如果系统时钟进来后经过 PLL 倍频到 100MHz需要把 PLL 输出引脚作为主时钟源让综合工具正确计算所有同步逻辑的时序路径。第三对 SRAM 接口做一张输入输出延迟约束。常见做法是设置set_output_delay和set_input_delay把 SRAM 的 tAA、tDOE 换算成 FPGA 时钟的延迟窗口。对于第一次跑通可以先只做时钟约束因为控制器状态机极低的时钟频率比如 50MHz 下访问周期 80ns给足了时序余量但如果你想跑接近 SRAM 极限的访问速度delay 约束必须补上否则综合工具会把 I/O 路径优化得过于乐观上板后才发现采样边沿不对。验证完成的标准是整片内存能连续写 0x0000 到 0xFFFF 再回读校验通过过程中没有出现一例数据翻转。如果出现偶发性错误优先把 CLK_CYCLES 参数调大一个周期再把示波器或 ILA 波形拉出来对比 SRAM 数据手册上的时序参数看是哪一步没满足。6. 二次开发从“能读写”到“好用”这个控制器最值得改的是它的参数化和握手方式。第一个简单改造是加字节写使能。给 sram_controller 增加一个 input[1:0] byte_en在 IDLE 状态时把 UB#/LB# 的赋值从固定拉低改为受 byte_en 控制。写低字节时byte_en[0]1LB# 拉低写高字节时byte_en[1]1UB# 拉低。这样上一层把 16bit 数据总线和 8bit 数据单元访问统一起来不需要为 8bit 访问单开一个控制器。第二个改造是加简单 FIFO 缓冲。如果你要连续读写一段数据比如图像缓存每写一个数据都要等待busy信号效率是极低的。正确做法是在 controller 外面套一个 FIFO写路径上写数据先入 FIFOcontroller 空闲时自动弹出并发送到 SRAM读路径上controller 读回来的数据写入另一个 FIFO上层逻辑只从 FIFO 取。这样上层逻辑和 SRAM 访问并行不互相等。常见代码是把 controller 的start和busy变成一个简单的状态仲裁器只要有数据就发起访问。第三个改造是读突发模式。IS61LV25616AL 是异步 SRAM每访问一次都要重新等待地址到数据时间但如果把地址线在外设时钟下周期性递增就可以把读访问效率逼近“连续地址、流水线采样”。做法是把 CLK_CYCLES 参数减小到 2在状态机里增加一个burst_len计数器每次访问完成后如果还有下一笔地址就不回 IDLE而是直接进入下一个 ACCESS只保持 CE# 为低。这个设计能显著提高图像行缓存场景下的吞吐量。我亲身经历的一次教训是第一版控制器把读数据采样放在 DONE 状态而不是 ACCESS 最后一个周期导致上板后总是读到上一拍地址的数据。后来我在仿真里把 sram_data 波形逐拍拉出来才意识到采样时刻晚了一个周期。从那以后我每设计一个外部器件控制器都会在 testbench 里加一条“地址变化后第 N 拍数据变化”的断言把采样窗口卡得死死的。希望这篇文章能帮你少走一次我走过的弯路让 IS61LV25616AL 在 FPGA 上真正听话起来。本文还有配套的精品资源点击获取