ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Verilog异步SRAM建模与控制器设计:从仿真到上板实战

Verilog异步SRAM建模与控制器设计:从仿真到上板实战 简介SRAM.zip 是一份以 Verilog 语言实现静态存储器SRAM的完整设计资料包面向 FPGA 与数字 IC 方向的初学者和进阶开发者也适合备赛电子设计竞赛的读者。内容围绕 SRAM 的核心机制展开包括六晶体管存储单元、交叉耦合锁存器、地址译码以及读写控制时序并结合 ModelSim、Vivado、Quartus 等常见工具给出从 RTL 编码、功能仿真到综合实现的关键思路。压缩包内共 100 个文件包含 .v 与 .tdf 设计源码、.qsf/.qpf 工程设置、.cdb/.rdb/.rpt/.summary 等综合布线报告、.vwf/.cvwf 仿真波形以及 .sdo 门级仿真输出整体仅 361KB小巧但覆盖了较完整的 Quartus 工程链路。目前已有 172 人浏览学习尤其适合借助包内 sram_test 测试工程逐步对照各阶段产物梳理读写操作的正确建模方法、非阻塞赋值与阻塞赋值的应用区别以及时序验证中的常见错误与排除思路从而快速迁移到自己的 SRAM 或片上存储设计中。1. 拿到 SRAM.zip 之后先想清楚里面是什么如果你在某个项目交接里看到一个名为SRAM.zip的压缩包里面大概率是两类东西某颗异步 SRAM 芯片的 Verilog 行为模型或者一个用来驱动 SRAM 读写时序的控制器源码。这两者经常一起出现因为做 FPGA 缓存或者替换 FIFO 时工程师习惯把型号相关的 model 和控制器打包成 zip 直接丢到工程里。问题是zip 里的代码往往来自不同项目、不同时钟域和不同位宽直接add sources编译大概率会报一连串端口不匹配和时序违例。这篇博文就顺着这个场景讲清楚如何用 Verilog 正确复用 SRAM 模型怎么写一个参数可调的 SRAM 控制器以及用最小验证环境把读写跑通。适合正在做 FPGA 存储、搞数字 IC 验证、或者准备“手撕 Verilog 面试题”的从业者。2. 异步 SRAM 的接口时序与 Verilog 建模要点2.1 先弄清 SRAM 与 DRAM 的差别才能看懂模型里的信号很多人一看到SRAM就默认它像 DDR 一样有复杂的分层协议其实异步 SRAM 是数字接口里最直白的一种给定地址和控制信号数据在固定延迟后出现在总线上。它不需要刷新存储单元是锁存器阵列这也是它比 DRAM 快、接口简单的原因代价是容量小、成本高。模型代码里如果出现#100这类延时那是在模拟地址到数据输出的tAA地址访问时间如果出现posedge we_n那是在捕获写操作。搞清楚 SRAM 和 DRAM 的区别和联系不是面试背诵题而是读模型时判断信号方向的依据DRAM 模型里常见的 bank、row/column 复用逻辑在 SRAM 模型中根本不存在看到就应该意识到文件拿错了。2.2 从 zip 里读 Verilog 模型控制信号优先级决定一切异步 SRAM 模型的结构非常固定核心是一个二维数组加三态门。收到 zip 后先不要急着编译把顶层文件打开找到片选cs_n或ce_n、写使能we_n、输出使能oe_n这三个信号然后回答三个问题写操作在哪个沿触发读数据是组合逻辑还是时序逻辑三态门受哪个信号控制典型的单口 SRAM 模型长这样注意读数据是纯组合路径module async_sram #( parameter ADDR_WIDTH 10, parameter DATA_WIDTH 8, parameter DEPTH 1024 )( input wire cs_n, input wire we_n, input wire oe_n, input wire [ADDR_WIDTH-1:0] addr, inout wire [DATA_WIDTH-1:0] data ); reg [DATA_WIDTH-1:0] mem [0:DEPTH-1]; reg [DATA_WIDTH-1:0] dout; // 写cs_n 和 we_n 同时为低时数据写入 always (posedge we_n or posedge cs_n) begin if (!cs_n !we_n) mem[addr] data; end // 读组合逻辑oe_n 有效后数据直接透传 always (*) begin if (!cs_n !oe_n we_n) dout mem[addr]; else dout {DATA_WIDTH{1bz}}; end assign data (!cs_n !we_n) ? {DATA_WIDTH{1bz}} : dout; endmodule代码里的写逻辑用posedge we_n触发但实际数据在we_n低电平期间已经稳定这会导致仿真模型出现一个常见问题如果数据在we_n拉高之前就变化写入的内容可能不是你期望的值。更稳妥的建模手法是把写操作的敏感列表换成addr和we_n的组合或者干脆用时钟沿配合建立保持检查。但作为模型上述写法足够应对大多数仿真因为它反映的正是异步 SRAM 手册里的行为描述。2.3 SRAM 模型的 3 个参数不要乱调模型头部通常有ADDR_WIDTH、DATA_WIDTH、DEPTH三个参数直接对应你手上那颗 SRAM 芯片的容量。这里有一个常见的错误认知把DEPTH设置成 1024但ADDR_WIDTH填了 12仿真也能跑因为 Verilog 数组索引允许越界声明但综合后地址译码器会多出一大块空逻辑。另一个要小心的是数据位宽有些 zip 里的模型是 16 位或 32 位接入 8 位控制器时需要对sram_data做重组不能直接截断。最简单的验证方法是看mem[addr] data两边宽度是否一致不一致时仿真器只会给一个 Warning这个 Warning 在大型工程里很容易被刷掉。下表给出不同容量器件的最小参数组合zip 里的代码一般不会写错但你自己新建验证环境时可以参考器件容量地址宽度数据宽度地址数8K x 8138819264K x 161616655361M x 321732131072注意容量写法的第一项是数据深度第二项是位宽很多人在 64K x 16 上把ADDR_WIDTH写成 16 位地址数 65536完全正确但DEPTH参数若直接填 65536 就会分配双倍内存仿真速度明显变慢。3. 手写 SRAM 控制器状态机与可综合代码3.1 为什么需要控制器而不是直接把引脚接到用户逻辑SRAM 模型可以直接在 testbench 里读写但真实工程中 FPGA 主频往往高于 SRAM 手册给出的访问时间直接拉引脚会产生毛刺。控制器的本质是一个时序状态机把用户逻辑的总线请求翻译成cs_n、we_n、oe_n电平组合并在合理的时间点采样数据或撤销写使能。异步 SRAM 没有时钟控制器的任务完全靠内部时钟计数来模拟建立保持窗口因此参数设置正确与否直接决定上板后是“偶尔出错”还是“稳定运行”。设计控制器前先明确需求是单次读单次写还是需要做多字节连续访问如果只是验证模型单次访问足够如果是给 UART 或以太网 MAC 用连续读会让你重新考虑地址自增逻辑。大多数 zip 里的控制器是为了单次访问写的最小实现连续读需要自己改。3.2 两个必调参数读等待周期与写恢复时间控制器必须有参数化配置。我一般保留RD_WAIT和WR_REC两个整数参数分别代表 OEn 拉低后等待多少个内部时钟周期再采样数据以及 WEn 拉高后等待多久再结束事务。RD_WAIT不是拍脑袋定的它由 SRAM 的tOELQOEn 低到数据有效时间除以 FPGA 时钟周期向上取整得出。例如tOELQ 12ns时钟 50MHz周期 20nsRD_WAIT取 1 就能满足但如果时钟是 100MHz取 2。WR_REC对应写恢复时间tWR即 WEn 拉高后地址和数据的保持时间至少取 1保守取 2。注意参数过大会降低吞吐过小会产生亚稳态采样调试时优先增大WR_REC读路径先查RD_WAIT。3.3 可综合的 SRAM 控制器状态机代码下面这段控制器支持单次读写状态机分四拍IDLE 等请求SETUP 拉控制信号ACCESS 完成数据读写RECOVERY 保证恢复时间。为了让代码更容易移植地址和数据输入输出都用寄存器打一拍避免异步信号直接进入状态机逻辑。module sram_ctrl #( parameter A_WIDTH 10, parameter D_WIDTH 8, parameter RD_WAIT 2, parameter WR_REC 1 )( input wire clk, input wire rst_n, input wire req, input wire wr_n, // 0 写 1 读 input wire [A_WIDTH-1:0] addr, input wire [D_WIDTH-1:0] wdata, output reg [D_WIDTH-1:0] rdata, output reg done, // SRAM 引脚 output reg cs_n, output reg we_n, output reg oe_n, output reg [A_WIDTH-1:0] sram_addr, inout wire [D_WIDTH-1:0] sram_data ); localparam S_IDLE 3d0; localparam S_SETUP 3d1; localparam S_ACCESS 3d2; localparam S_RECOVERY 3d3; reg [2:0] state, next_state; reg [7:0] cnt; reg [D_WIDTH-1:0] wdata_r; assign sram_data (!cs_n !we_n) ? wdata_r : {D_WIDTH{1bz}}; always (posedge clk or negedge rst_n) begin if (!rst_n) state S_IDLE; else state next_state; end always (*) begin next_state state; case (state) S_IDLE: if (req) next_state S_SETUP; S_SETUP: next_state S_ACCESS; S_ACCESS: begin if (wr_n cnt RD_WAIT - 1) next_state S_ACCESS; else next_state S_RECOVERY; end S_RECOVERY: if (cnt WR_REC - 1) next_state S_RECOVERY; else next_state S_IDLE; endcase end always (posedge clk or negedge rst_n) begin if (!rst_n) begin cs_n 1b1; we_n 1b1; oe_n 1b1; sram_addr {A_WIDTH{1b0}}; wdata_r {D_WIDTH{1b0}}; rdata {D_WIDTH{1b0}}; done 1b0; cnt 0; end else begin done 1b0; case (state) S_IDLE: begin cs_n 1b1; we_n 1b1; oe_n 1b1; if (req) begin sram_addr addr; wdata_r wdata; done 1b0; end end S_SETUP: begin cs_n 1b0; we_n ~wr_n; oe_n ~wr_n; // 读时拉低写时拉高 end S_ACCESS: begin cnt cnt 1; if (wr_n) begin if (cnt RD_WAIT - 1) rdata sram_data; // 采样时刻 end end S_RECOVERY: begin cnt cnt 1; we_n 1b1; oe_n 1b1; if (cnt WR_REC - 2) cs_n 1b1; else if (cnt WR_REC - 1) begin done 1b1; cnt 0; end end endcase end end endmodule状态机的关键逻辑在S_ACCESS读路径用RD_WAIT计数决定何时把sram_data采进rdata写路径则靠we_n在S_SETUP阶段拉低到S_RECOVERY阶段拉高之间的拍数实现数据建立时间。这里有一个调试细节sram_data在写周期里是控制器驱动总线读周期里是 SRAM 模型驱动总线切换瞬间总线会出现高阻态导致仿真波形上看到毛刺这是正常现象只要采样点不在毛刺窗口内即可。3.4 连续读写如何扩展单次控制器只处理一个事务遇到需要连续访问 FIFO 的场景可以在S_RECOVERY结束时判断req是否仍保持高如果保持高则跳回S_SETUP而不是S_IDLE同时把sram_addr的值自动加一。注意要保留done信号在每拍数据有效时拉高否则外部逻辑无法确定当前读出的数是哪个地址的。使用 Verilog 多字节收发逻辑时这个扩展尤其常用先连续写一段突发数据到 SRAM再连续读出控制器内部的地址计数器是单独处理还是由外部模块控制取决于你的数据总线宽度。若总线宽度大于 SRAM 位宽宁可把控制器改成支持地址步进也不要让外部逻辑每个字节发一次请求。4. SRAM 仿真验证testbench 搭建与三种典型报错4.1 最小 testbench激励、比较和自动结束有了模型和控制器接下来要做的 SRAM 仿真验证必须包含三个部分初始化 SRAM 内容或先写入数据、发起读事务并比较结果、超时机制防止仿真卡死。用 Icarus Verilog 或 VCS 都可以跑命令行方式如下iverilog -o sram_tb.vvp sram_ctrl.v async_sram.v sram_tb.v vvp sram_tb.vvp如果你用 VSCode 写 Verilog可以把这两个命令配成任务每次保存后一键运行。下面这个 testbench 先写 8 个字节再逐个读回比对同时打印错误地址timescale 1ns/1ps module sram_tb; reg clk 0; reg rst_n 0; reg req 0; reg wr_n 1; reg [9:0] addr 0; reg [7:0] wdata 0; wire [7:0] rdata; wire done; always #10 clk ~clk; sram_ctrl #( .A_WIDTH(10), .D_WIDTH(8), .RD_WAIT(2), .WR_REC(1) ) u_ctrl ( .clk(clk), .rst_n(rst_n), .req(req), .wr_n(wr_n), .addr(addr), .wdata(wdata), .rdata(rdata), .done(done) ); async_sram #( .ADDR_WIDTH(10), .DATA_WIDTH(8), .DEPTH(1024) ) u_sram ( .cs_n(u_ctrl.cs_n), .we_n(u_ctrl.we_n), .oe_n(u_ctrl.oe_n), .addr(u_ctrl.sram_addr), .data(u_ctrl.sram_data) ); integer i; reg [7:0] expected [0:7]; task automatic write_word(input [9:0] a, input [7:0] d); begin (posedge clk); addr a; wdata d; wr_n 0; req 1; (posedge done); req 0; end endtask task automatic read_word(input [9:0] a); begin (posedge clk); addr a; wr_n 1; req 1; (posedge done); req 0; end endtask initial begin for (i 0; i 8; i i 1) expected[i] i * 8h11; #20 rst_n 1; // 写入 8 个字节 for (i 0; i 8; i i 1) write_word(i, expected[i]); // 再读回 for (i 0; i 8; i i 1) begin read_word(i); if (rdata ! expected[i]) begin $display(ERROR: addr%0d expected%02h got%02h, i, expected[i], rdata); $finish; end end $display(ALL TESTS PASSED); $finish; end initial begin #100000; $display(TIMEOUT); $finish; end endmodule这个 testbench 的关键点是(posedge done)同步等待控制器完成事务如果正在测试的控制器done信号抖动或者长时间不拉高仿真会卡在任务里。超时机制用第二个 initial 块挂#100000语句一旦主线程阻塞超过该时间就强制结束。rdata ! expected用了不全等比较当rdata为高阻或 X 态时也会触发报错避免把 X 态误判成正确数据。4.2 仿真波形里重点看四个时刻波形能直接暴露多数问题。打开 GTKWave 或 VCS 的波形窗口后不要看整体跑完的结果用光标定位到第一次写事务的S_SETUP和S_ACCESS阶段检查以下内容第一we_n拉低期间sram_data是否稳定等于wdata如果总线上出现高阻说明三态门赋值条件写错第二读事务中oe_n拉低到rdata更新之间相隔多少个周期这个间隔必须等于RD_WAIT第三地址在cs_n拉低前是否建立异步 SRAM 对地址建立时间有要求仿真模型不会自动检查需要你在 testbench 里人为对比时钟沿第四事务结束后cs_n是否恢复高电平如果一直拉低下一次请求时状态机会出现奇怪的毛刺行为。4.3 仿真跑不起来的三类报错处理最常遇到的是端口连接错误sram_data方向不匹配。如果模型里声明的是inout控制器连的是wire编译会报“cannot assign to net”或方向错误。解压 zip 后先统一检查文件里是否有inout端口所有挂总线的地方必须用wire连接不能接到reg。第二类错误是always块中赋值宽度不一致比如${D_WIDTH{1bz}}写成了8bz当 D_WIDTH 被改成 16 位时高 8 位会被自动补零这不影响仿真但会产生警告。第三类是初始化顺序错误testbench 在#20 rst_n 1之前就发起了第一笔写请求控制器的复位还没完成req信号被当成毛刺忽略。解决办法是在发出第一个请求前至少等 3 个时钟沿并把#20改成#25避开时钟上升沿与复位释放的竞争。5. 上板验证与模型替换时的 X 态排查技巧仿真通过只代表逻辑时序符合预期真正上板调试时 FPGA 里的 SRAM 模型不再有效你需要把async_sram模块换成真实芯片同时保留控制器不变。这类替换的第一步是用 ILA 抓内部信号抓sram_addr、sram_data和控制器的状态寄存器不要只抓最终读写结果否则数据出错时无法定位是时序窗口问题还是连接问题。上板后最常见到的现象是第 1 个地址读写正确、后续连续地址出错这通常是地址建立时间不足把S_SETUP额外插入一拍空操作即可。X 态排查是另一个高频问题。仿真时波形里出现x并不一定是代码错误可能是因为控制器在上电初始状态给sram_data赋值高阻、而 testbench 在复位释放前采样了总线。排查方法是把所有寄存器复位值显式写成确定态cs_n 1b1; we_n 1b1; oe_n 1b1;三个信号一个都不能漏。如果复位后仍有 X 态检查 SRAM 模型的mem数组是否被完整初始化用$readmemh加载初始化文件是最直接的手段否则mem[addr]读出来全是 X。另一种技巧是在 testbench 的检查环节加入容错允许前两次事务的rdata为 X但后续必须严格匹配这样先定位初始化问题再分析时序问题。最后给一个 zip 里经常藏着的进阶技巧控制器搭配异步 FIFO 封装成带缓存的读写接口。做法是在控制器外加一层fifo_wr和fifo_rd信号把突发连续读写成流水线形式——S_RECOVERY期间预取下一地址的数据这样等效读速率接近一个周期一个数据比裸控制器快接近一倍。实现时注意 FIFO 的空满信号要和done对齐否则仿真时序正确但上板因为 FIFO 读使能早了一个周期而丢数据。验证这种流水线设计时把RD_WAIT分别设成 1、2、3 各跑一轮确认仿真在不同延迟下都能通过这个极限参数测试能提前暴露 SRAM 时序裕量不足的隐患。本文还有配套的精品资源点击获取
返回列表