资讯动态

FPGA读写IS61LV25616AL异步SRAM:Verilog时序控制与状态机实现

发布时间:2026/10/14 16:21:07 来源:尧图企业网站定制
简介面向FPGA开发者与数字逻辑学习者的IS61LV25616AL读写Verilog程序已在Xilinx Spartan-3S400开发板上完成实测验证适用于外部SRAM读写控制、存储扩展、接口时序学习等教学与工程场景。压缩包共15个文件约57KB以Verilog源码SRAM.v、command.v、key.v为核心另含Xilinx工程文件xise/projectmgr、UCF引脚约束、综合实现报告xreport及位流/网表xwbt/ncd等覆盖设计输入、综合、实现到下载的完整流程且目录内文件命名清晰便于按阶段查阅。IS61LV25616AL是一颗16位宽、256K容量的低电压CMOS SRAM支持高速读写并带掉电保护程序通过地址、数据总线及读写控制信号完成对存储器的有效访问帮助读者理解FPGA与外部SRAM之间的接口时序与状态控制开发板实测通过也表明其引脚分配与控制逻辑具有实际参考价值。程序包已有1037人学习结合源码、工程与报告可快速掌握SRAM控制器设计、引脚约束与硬件验证方法也可作为FPGA存储扩展项目的起步模板。1. FPGA读写IS61LV25616AL从时序手册到可综合Verilog的一次翻译一块256K×16的异步SRAM芯片没有时钟、没有刷新、没有突发模式只有一堆控制引脚和一张时序图。很多开发者第一次在FPGA上调试IS61LV25616AL时都觉得这玩意比SDRAM简单太多结果上板之后读出来的数据要么全F、要么随机跳写进去的数据再读回来对不上。标题里的Verilog程序本质上就是把这颗芯片数据手册里的读周期和写周期翻译成一段用系统时钟驱动的状态机逻辑。这篇文章给的是我常用的最小可运行方案适合两类人一类是板子已经画好、就差控制器逻辑的硬件工程师另一类是刚接触FPGA、想把异步SRAM接口吃透的学生。2. IS61LV25616AL的引脚与读写时序控制器设计的第一张图纸2.1 引脚功能与总线特性IS61LV25616AL是一颗异步SRAM容量256K×16bit也就是512KB地址线18根数据线16根。它和同步SRAM最大的区别是没有时钟引脚。所有读写操作都靠CE#、OE#、WE#、UB#、LB#这几个控制信号的电平组合来触发数据手册上给的是一张张建立时间和保持时间表格而不是时钟边沿约束。写控制器之前先把引脚功能理清楚。下面是一颗标准异步SRAM的引脚清单引脚方向功能说明A[17:0]输入地址总线18根寻址262144个16bit单元IO[15:0]双向数据总线读时输出数据写时输入数据CE#输入片选低有效未选中时数据总线呈高阻OE#输入输出使能低有效控制读操作时数据输出WE#输入写使能低有效控制写操作UB#输入高字节选通低有效控制IO[15:8]LB#输入低字节选通低有效控制IO[7:0]这里有两个容易忽略的点。第一UB#和LB#是独立的字节选通如果只接其中一根另一根悬空或者拉高对应字节的数据线会一直处于高阻状态这在调试时非常容易误判成芯片坏了。第二这是一颗3.3V LVTTL电平的芯片FPGA侧的bank电压必须配到3.3V如果你的FPGA bank是1.8V或者2.5V即使逻辑对了电平也不达标读回来的数据会随机翻转。2.2 读时序的关键参数与等待周期估算读操作看起来很简单把地址放上去拉低CE#和OE#等一段时间数据线上就有数据了。但“等一段时间”到底等多少个FPGA时钟周期这是控制器设计的关键。数据手册上读周期相关的参数主要有这么几个tAA是地址访问时间从地址有效到数据有效tCO是从CE#拉低到数据有效tOEL是从OE#拉低到数据有效tRC是读周期时间。常见速度等级的tAA在十几纳秒到二十几纳秒之间具体数值以你手里芯片丝印上的速度等级为准。在100MHz系统时钟下一个周期是10ns地址访问时间假设是15ns那么从地址稳定到数据稳定至少需要2个时钟周期。但实际工程里我不建议卡着最小值算因为FPGA引脚到内部逻辑还有走线延迟和组合逻辑延迟地址信号到达芯片引脚的时间比寄存器输出晚几纳秒很正常。我一般会在满足时序参数的基础上多留1拍裕量也就是说tAA按15ns算控制器里至少等3拍再锁存数据。读操作的时序约束按下述顺序展开先给地址再拉低CE#和OE#然后等待足够的周期数最后在数据有效窗口内把总线上的值锁存到内部寄存器。锁存动作必须发生在SRAM输出稳定的区间不能等到OE#已经拉高、总线已经开始释放的时候才去采样否则读到的就是高阻态转换过程中的垃圾值。2.3 写时序与字节选通的关系写时序比读时序多几个参数tSA是地址建立时间从地址有效到WE#拉低之间的间隔tSD是数据建立时间从数据有效到WE#拉低之间的间隔tWP是WE#低电平脉冲宽度tWR是写恢复时间从WE#拉高到地址释放之间的间隔。写操作的正确顺序是先放地址、放数据然后把CE#拉低保持WE#为高等待一段时间让地址和数据都稳定再把WE#拉低维持足够宽的低电平脉冲最后把WE#拉高保持地址和CE#一小段时间后再释放。很多初写Verilog控制器的人容易犯一个错误在同一个always块里同时翻转地址、数据和WE#。这样WE#拉低时地址和数据可能还在建立过程中不满足tSA和tSD结果就是写进去的地址或数据是错的读回来自然对不上。字节选通的问题也要在这里一并考虑。IS61LV25616AL是16bit位宽的芯片但UB#和LB#允许只操作其中一个字节。如果你的系统只用了低8位数据线正确做法是把LB#拉低、UB#拉高这样每次写操作只影响低字节。反过来如果两组字节选通都拉低就是一个16bit的双字节读写。把字节选通接到FPGA的普通IO口上而不是直接接地可以在后续扩展8bit/16bit混合访问时不用改板子。3. 状态机架构与模块划分把时序图变成可综合逻辑3.1 用状态机还是计数器为什么我选三段式状态机异步SRAM控制器本质是一个“慢速外设适配器”系统时钟很快、SRAM很慢中间需要状态机来产生等待周期。有的人喜欢用计数器硬延时拉低一个信号然后数多少个周期再拉高。这种做法在小规模电路里能工作但代码可读性和复用性都很差时序调整要改多处计数器的初值。我常用的做法是一个标准的三段式状态机第一段做状态跳变第二段产生SRAM控制信号第三段锁存读写数据。状态机的每个状态对应时序图里的一段操作比如读状态对应地址建立、OE#拉低、等待数据稳定写状态对应地址数据建立、WE#脉冲、写恢复。选择状态机而不是计数器的另一个原因是调试友好。在ILA逻辑分析仪里看波形时状态寄存器的值直接告诉你控制器现在处于哪个阶段如果波形停在某个状态不动了大概率是外部应答信号或者总线释放逻辑出了问题。计数器方案一旦卡住只能看到计数器在数数没法直接定位是哪个阶段的问题。3.2 模块划分与对外接口定义控制器模块的对外接口我一般分成两组一组是面向FPGA内部逻辑的请求接口一组是面向芯片引脚的外部接口。做到内外隔离上层逻辑不需要关心外部SRAM的时序只需要发请求、等应答、取数据。请求接口包含这几个信号req_valid表示有读写请求req_write表示当前请求是写还是读req_addr是18位地址req_wdata是写数据req_ready表示当前请求已经完成req_rdata是读回来的数据。外部接口则直接对应芯片引脚sram_addr、sram_data、sram_ce_n、sram_oe_n、sram_we_n、sram_ub_n、sram_lb_n。这种接口划分的好处是上层逻辑可以完全独立开发。比如上层是一个FIFO缓存模块只需要在FIFO非空时拉高req_valid写地址递增等req_ready后再判断下一个请求也可以封装成一个简单的AXI-Lite从机接口让片上处理器通过寄存器读写来访问SRAM。控制器内部的变化不会影响上层逻辑。3.3 时钟频率与时序裕量100MHz下的计算实例以100MHz系统时钟为例一个时钟周期10ns。假设芯片tAA典型值15ns、tSD典型值12ns、tWP典型值15ns做一下预算读操作地址在第一个时钟沿之后输出到引脚第三个时钟沿锁存数据中间间隔大约3个周期30ns扣除引脚输出延迟、板级走线延迟、芯片内部延迟实际裕量还有10ns以上。写操作地址和数据的建立时间预算为2个周期20nsWE#低电平保持2个周期20ns写恢复预留1个周期10ns均大于手册要求。如果系统时钟是200MHz一个周期5ns那就要重新算账。tAA按15ns算光地址到数据稳定就需要3个周期15ns加上组合逻辑和走线延迟读等待至少4拍。这种时候我建议用参数化设计把每个阶段的等待拍数做成parameter换时钟频率时只改参数不用改状态机逻辑。还要注意一个细节FPGA综合工具会把跨时钟域或者异步接口的路径当作普通时序路径去分析。像SRAM这类异步器件综合工具不知道芯片内部的实际延迟需要手动添加时序约束把SRAM接口的输入输出延迟约束到合理范围或者直接设置为false path否则布局布线后时序报告会误导你让你以为时序不收敛实际上问题根本不在FPGA内部。4. 读写控制Verilog代码最小可用实现与参数说明4.1 读状态机代码与等待拍数说明下面的代码是一个精简但完整的异步SRAM控制器默认按16bit全字读写字节选通在事务开始时同时拉低。先看读路径的状态机// 异步SRAM控制器读路径双字节访问固定等待2拍 module sram_async_ctrl #( parameter ADDR_WIDTH 18, parameter DATA_WIDTH 16 )( input wire clk, input wire rst_n, // 内部请求接口 input wire req_valid, input wire req_write, input wire [ADDR_WIDTH-1:0] req_addr, input wire [DATA_WIDTH-1:0] req_wdata, output reg req_ready, output reg [DATA_WIDTH-1:0] req_rdata, // SRAM外部引脚 output reg [ADDR_WIDTH-1:0] sram_addr, inout wire [DATA_WIDTH-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 ); localparam IDLE 4d0; localparam R_ACCESS 4d1; localparam R_LATCH 4d2; localparam W_SETUP 4d3; localparam W_PULSE 4d4; localparam W_RECOVERY 4d5; reg [3:0] state; reg [1:0] wait_cnt; reg [DATA_WIDTH-1:0] data_out; // 双向总线写阶段由FPGA驱动其余阶段释放为高阻 assign sram_data (state W_SETUP || state W_PULSE) ? data_out : {DATA_WIDTH{1bz}}; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; wait_cnt 2d0; req_ready 1b0; req_rdata {DATA_WIDTH{1b0}}; data_out {DATA_WIDTH{1b0}}; sram_addr {ADDR_WIDTH{1b0}}; sram_ce_n 1b1; sram_oe_n 1b1; sram_we_n 1b1; sram_ub_n 1b1; sram_lb_n 1b1; end else begin req_ready 1b0; // 单周期应答默认拉低 case (state) IDLE: begin if (req_valid) begin sram_addr req_addr; sram_ce_n 1b0; // 先选通片选 sram_ub_n 1b0; // 双字节模式 sram_lb_n 1b0; data_out req_wdata; if (req_write) begin state W_SETUP; end else begin sram_oe_n 1b0; // 读使能拉低 wait_cnt 2d2; // 等待2拍满足tAA state R_ACCESS; end end end R_ACCESS: begin if (wait_cnt 2d0) begin req_rdata sram_data; // 锁存有效数据 state R_LATCH; end else begin wait_cnt wait_cnt - 2d1; end end R_LATCH: begin sram_oe_n 1b1; // 释放读使能 sram_ce_n 1b1; sram_ub_n 1b1; sram_lb_n 1b1; req_ready 1b1; // 通知上层读完成 state IDLE; end读路径的逻辑说明请求在IDLE状态被接收地址和CE#在同一拍更新OE#拉低。wait_cnt初值设为2意味着寄存器在地址输出之后第3个时钟沿锁存数据。如果芯片tAA比较长、或者时钟频率高于100MHz把这里的2改成3或4。4.2 写状态机代码与脉冲宽度控制写路径的核心是控制WE#脉冲的宽度并且确保WE#拉低之前地址和数据都已经稳定。代码里W_SETUP状态专门用来建立地址和数据W_PULSE状态维持WE#低电平W_RECOVERY状态完成写恢复。W_SETUP: begin // 等待2拍保证地址和数据建立时间tSA/tSD if (wait_cnt 2d0) begin sram_we_n 1b0; // 拉低WE#写脉冲开始 wait_cnt 2d2; // 脉冲宽度2拍 state W_PULSE; end else begin wait_cnt wait_cnt - 2d1; end end W_PULSE: begin if (wait_cnt 2d0) begin sram_we_n 1b1; // 拉高WE#写脉冲结束 state W_RECOVERY; end else begin wait_cnt wait_cnt - 2d1; end end W_RECOVERY: begin // 地址和CE#继续保持1拍满足写恢复时间tWR sram_ce_n 1b1; sram_ub_n 1b1; sram_lb_n 1b1; req_ready 1b1; // 写完成应答 state IDLE; end default: state IDLE; endcase end end endmodule写路径逻辑说明从IDLE进入W_SETUP时地址、数据、CE#都已在上一拍建立W_SETUP再等2拍后WE#才拉低相当于WE#拉低之前有3个周期的地址稳定时间。W_PULSE维持2拍低电平W_RECOVERY释放信号前再保持1拍。整体写周期长度是1拍IDLE接收、3拍建立、2拍脉冲、1拍恢复共7个时钟周期。参数调整建议如果时钟只有50MHz一个周期20ns地址建立时间变成3拍60ns此时可以适当减少W_SETUP的等待拍数把wait_cnt从2改成1总周期缩短到6拍。但要注意改小等待拍数前必须确认芯片手册上tSA、tSD、tWP的最大值尤其是用低速芯片的时候脉冲宽度不够会直接导致写失败。4.3 双向总线的三态控制与字节选通代码里最关键的一行是三态赋值sram_data在W_SETUP和W_PULSE两个状态下输出数据其他状态全部是高阻。如果三态控制写反把数据输出留在了读状态那么读操作时FPGA驱动的数据会和SRAM输出的数据打架轻则读回来全错重则损坏芯片IO。字节选通的逻辑在代码里是固定拉低也就是双字节访问。如果你需要支持8bit访问一种常见做法是在请求接口加一个byte_en信号byte_en[1:0]值为2b10时只操作高字节、2b01时只操作低字节、2b11时双字节。然后在IDLE状态里依据byte_en的值分别控制sram_ub_n和sram_lb_nbyte_en[1]对应UB#byte_en[0]对应LB#。数据总线也要相应处理8bit访问时只使用对应的8根数据线。使用这个模块时我习惯在例化顶层加一个小的地址译码逻辑把SRAM映射到FPGA内部地址空间。比如系统总线地址最高位为1时访问SRAM为0时访问内部寄存器。这样调试时可以统一用同一个读写宏来访问所有外设不需要单独写SRAM测试逻辑。5. IS61LV25616AL读写调试避坑5个真翻车场景5.1 读数据全F或者不变OE#时序前于地址建立现象用ILA抓取读写波形地址信号已经变化OE#也已经拉低但读回来的数据始终是0xFFFF或者固定值不随地址变化。原因控制器把地址和OE#放在了同一个时钟沿输出地址到达SRAM引脚有延迟而OE#拉低后SRAM开始准备输出但此时地址输入端还是旧地址于是输出了旧地址的数据。如果旧地址处恰好没写过数据读回的就是全F。解决把OE#的拉低延后一拍。在IDLE状态先输出地址、拉低CE#下一个状态再拉低OE#。这样地址先建立OE#后有效保证SRAM看到的是稳定地址。修改代码时把sram_oe_n的拉低动作从IDLE移到R_ACCESS状态的第一拍。5.2 写进去再读出来是旧数据WE#脉冲宽度不够现象写操作后紧接着读同一地址读回来的数据不是刚写进去的值而是之前的值。在ILA上看WE#确实拉低了但低电平只持续了1个时钟周期。原因100MHz时钟下1个周期10ns如果芯片tWP最小值要求大于10ns那么1拍的写脉冲宽度不够SRAM内部没有完成数据写入。寄存器传输仿真可能不报错因为仿真模型往往放宽了时序要求上板才会暴露。解决把W_PULSE状态的wait_cnt从2改成3让WE#低电平持续3拍。如果时钟频率更高比如200MHz改成5拍以上。这个坑在低速芯片上特别常见不要只看数据手册的典型值用最小值和你的时钟周期做比较。5.3 仿真通过但上板读错综合工具乱优化异步接口现象ModelSim仿真一切正常读写时序完全符合预期烧到板子上随机出错有时连续读一个地址两次结果不同。原因综合工具默认把SRAM接口当作普通同步接口处理优化掉了看似冗余的等待拍数或者把sram_data总线上的三态逻辑做了不正确的等价变换。异步SRAM接口本来是靠延迟凑时序的工具优化后延迟变了时序裕量就没了。解决在综合约束文件里对sram_data、sram_addr以及所有sram控制信号设置为false path或者max delay约束。另外在代码里给三态赋值加上综合属性防止工具修改结构。Vivado里可以在信号声明处加(* IOB TRUE)、(KEEP TRUE *)这类属性告诉工具别动这些IO逻辑。5.4 只写了低字节高字节数据也变字节选通没正确复位现象用8bit模式只写低字节写完后读16bit数据发现高字节也变了像是被同时写入了。原因UB#和LB#在上一次16bit操作后被拉低再一次8bit操作时只把LB#拉低UB#没恢复成高电平于是同一次写操作同时选中了高字节和低字节。解决在W_RECOVERY和R_LATCH状态里把sram_ub_n和sram_lb_n显式复位为1不要只靠复位信号。上电后的初始值只保证第一次操作是对的之后每次操作结束都必须恢复两个字节选通信号。5.5 连续读写吞吐率上不去每笔事务都回IDLE握手现象上层逻辑连续发读写请求每笔请求之间能看到明显的空闲周期实际吞吐率远低于理论值。原因每次事务结束都要经过一个IDLE状态请求应答req_ready和下一笔req_valid之间存在握手间隔。当上层模块在req_ready拉高后立刻拉高req_valid控制器要等下一个时钟沿才能回到IDLE并接收新请求白白浪费了一拍。解决去掉IDLE状态让状态机在R_LATCH/W_RECOVERY结束后直接判断req_valid如果请求已经准备好就直接进入下一笔事务。或者把握手改成流水线方式在最后一拍就预取下一笔的地址和数据。这个优化在做大数据块搬运时收益明显能提升接近30%的吞吐率。6. 从能读写到用得稳上板验证方法与接口封装进阶控制器写完、综合通过只是第一步。真正验证IS61LV25616AL读写正确性我习惯在板子上跑三类测试。第一类是地址扫描测试从地址0开始往每一个地址写入一个由地址本身计算出来的16bit数据比如低8位写地址低8位、高8位写地址取反然后从头再读一遍比对每个地址的数据。这个测试能快速发现地址线焊接问题、字节选通问题、以及高位地址线没接对导致的两个地址映射到同一物理单元。第二类是随机读写测试用一个线性反馈移位寄存器产生伪随机地址和伪随机数据连续执行写读操作读回来的数据和写入值比对同时记录错误地址和错误数据。随机测试能覆盖连续读写的交叉场景发现间隔太短导致的写失败问题。第三类是总线方向测试把sram_data的上游驱动能力单独测一遍。在FPGA内部把数据总线回环到自己读数据寄存器先写一个值再读同一个地址ILA里同时抓sram_data的实际电平变化。如果数据总线三态切换有问题这个测试会直接卡住。接口封装方面如果这颗SRAM是给片上处理器用的我会把控制器进一步封装成AXI-Lite从机接口每次处理器读写操作翻译成一次SRAM请求事务寄存器地址的低18位直接对应SRAM地址。如果是给DMA或高速数据通路用建议做成AXI-Stream接口读方向把SRAM数据打包成连续的流数据写方向把流数据拆包写入递增地址。我自己做这类异步SRAM控制器总结的经验是等待拍数宁多勿少字节选通宁显勿隐。多等一两拍只会让吞吐率低几个百分点但少等一拍就是数据错误字节选通每个事务结束都要显式恢复不要依赖上电初值。希望这篇踩坑记录能帮你少走点弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑