
简介这是一份基于Verilog的SRAM设计与仿真工程包面向数字电路初学者、FPGA开发者及存储器设计爱好者用于理解静态存储器SRAM的基本单元结构、地址映射与读写控制逻辑。压缩包共100个文件约361KB主体为Quartus工程文件包含Verilog源码.v、编译综合报告.rpt/.summary、数据库文件.cdb/.rdb、仿真波形.vwf以及辅助脚本与说明文档可覆盖从RTL编码、逻辑综合到门级仿真的完整设计流程。已有172人学习下载特别适合希望对照完整工程案例快速上手的读者。通过研读工程可以掌握SRAM模块的Verilog建模方法理解读操作采用非阻塞赋值、写操作采用阻塞赋值的时序意义学会利用ModelSim或Vivado进行读写功能仿真并验证连续读写、随机访问等场景下的正确性。工程附带综合报告和波形文件便于排查时序问题、优化访问性能整体目录结构清晰是一份兼具教学与工程参考价值的存储设计样例。1. SRAM 的 Verilog 实现为什么没你想的那么简单在 FPGA 或 ASIC 里写一个 SRAM 的 Verilog 模型看起来就是一组二维数组加一个时钟但真正放进 Quartus 综合再上板你会发现同一段代码有时候被拆成几百个 LUT有时候被塞进专用 Block RAM时序结果差出三倍。SRAM 虽然叫静态存储器只要不断电数据就不丢但它的读通路、写使能策略、初始化方式每一项都会影响最终 fmax 和面积。这篇文章就以一个常见的 SRAM.zip 工程包为背景从六管存储单元原理讲起给出可复用的参数化 Verilog 模块、自动比对的 testbench以及 Quartus 综合后的排错思路。适合正在做数字设计、准备手撕 verilog 面试题或者刚接手 sram 相关 RTL 的工程师。2. 六管存储单元与 Verilog 建模先搞清你写的是什么2.1 单元内部结构与行为级差异SRAM 的一个 bit 由两个交叉耦合反相器和两个传输门组成即标准 6T 单元。反相器构成正反馈锁存器让数据保持字线选通后内部节点把电平送到差分位线上灵敏放大器再根据压差输出。DRAM 靠电容电荷保存信息需要周期性刷新SRAM 靠正反馈保存不需要刷新但 6T 结构面积更大。这也是用 Verilog 建模时最容易忽略的点你写的reg[7:0] mem[0:255]只是行为级模型不是真实版图在 FPGA 里它可能被综合成触发器阵列、分布式 LUTRAM 或专用 RAM 块在 ASIC 里通常要交给 memory compiler 生成单元。能不能推断成硬核完全取决于 RTL 的写法而不是你声明了什么类型。2.2 参数化模块地址、数据、容量与读模式写 SRAM 的第一步是确定端口参数。工程里最常见的基础参数如下参数名默认值说明ADDR_WIDTH8地址线宽度存储深度为 2 的 ADDR_WIDTH 次方DATA_WIDTH8数据线宽度决定一个地址能存多少 bitREAD_MODESYNCSYNC读输出寄存一拍ASYNC为组合读INIT_FILE十六进制初始化文件路径空串表示不上电加载注意地址宽度和深度要一致。很多资源里只给ADDR_WIDTH不写DEPTH实际容量是1ADDR_WIDTH乘以DATA_WIDTH。如果做多字节收发比如数据位宽 16 bit却要按字节独立写就不能简单地把地址左移一位而是要在 RTL 里加字节使能。这个坑后面最后一张会专门讲。module sram_param #( parameter ADDR_WIDTH 8, parameter DATA_WIDTH 8, parameter READ_MODE SYNC, parameter INIT_FILE )( input wire clk, input wire we, input wire re, input wire [ADDR_WIDTH-1:0] addr, input wire [DATA_WIDTH-1:0] wdata, output reg [DATA_WIDTH-1:0] rdata ); reg [DATA_WIDTH-1:0] mem [0:(1ADDR_WIDTH)-1]; integer i; initial begin if (INIT_FILE ! ) begin $readmemh(INIT_FILE, mem); end else begin for (i 0; i (1ADDR_WIDTH); i i 1) mem[i] {DATA_WIDTH{1b0}}; end end always (posedge clk) begin if (we) mem[addr] wdata; end generate if (READ_MODE SYNC) begin : sync_read always (posedge clk) begin if (re) rdata mem[addr]; end end else begin : async_read always (*) begin if (re) rdata mem[addr]; else rdata {DATA_WIDTH{1b0}}; end end endgenerate endmodule写通路只有一段always (posedge clk)且内部统一用非阻塞赋值。非阻塞赋值的调度语义是时钟沿对输入采样在事件队列末尾再更新 reg这样同一时钟沿上的多位写入不会互相覆盖。如果你在这个 always 里混用阻塞赋值仿真时经常出现写地址刚更新、数据还没落进 memory 的竞争效应综合结果虽然一般没错但验证阶段很难排查。读通路用generate区分同步读和异步读。同步读输出比地址晚一拍适合高频时序收紧的场景异步读的地址到数据是组合路径时延不可控通常只用于小容量寄存器堆。2.3 常见误区读用阻塞、写用非阻塞很多教程会告诉你读操作使用非阻塞赋值写操作使用阻塞赋值这在硬 IP 的固定模板里偶然成立但不是通用规则。正确的约束是所有描述时序逻辑的 always 块统一使用非阻塞赋值只有组合逻辑比如地址译码、异步读输出才用阻塞赋值。如果你把上面的rdata在同步读分支里改成阻塞赋值仿真波形会看到输出比正常时序提前一个时间片而且 Quartus 的 RAM 推断可能失败。判断标准只有一个这个 always 块是不是在时钟沿触发、是否在电路里生成寄存器是就必须用。3. 搭一个可综合的 SRAM 读写通路与 Quartus 工程整理3.1 为什么内部逻辑不用 inout 数据总线下载包里如果是模拟外部 SRAM 芯片芯片引脚通常是地址总线和双向数据线data。但在 FPGA 内部例化 SRAM 模块我一般不会在纯逻辑里写inout。原因有两层第一内部网络没有三态引脚的物理结构综合器最终会把它映射成三态缓冲 MUX消耗更多逻辑且驱动强度不均匀第二inout在仿真里必须同时盯住方向控制信号一不留神就会出现两个驱动源把总线打到x。所以上面的参数化模块把wdata和rdata分开只有当模块要接到 FPGA 的 IO 或模拟外部 SRAM 颗粒时才在顶层把两个独立总线和一个data三态引脚对接。这样的分离让读写互不抢占也方便加写后读回校验。3.2 下载包里的 Quartus 工程文件怎么用解压 SRAM.zip 后你会看到一排后缀很长的文件例如sram_test.rtlv_sg.cdb、sram_test.map.cdb、sram_test.cmp.cdb它们不是源码而是 Quartus 在编译流程里生成的中间数据库。具体对应关系如下文件后缀来源阶段你能拿来做什么.vpr.ammdb工程编译后的数据库恢复部分分析与综合结果.map.cdb / .cmp.cdbAnalysis Synthesis 和 Fitter 的数据库查看映射后的逻辑占用.rtlv_sg.cdb / .root_partition.map.reg_db.cdb寄存器级网表和分区数据库检查寄存器与 RAM 是否被推断必要时打开 RTL Viewer.cdfChain Description File器件配置链信息下载硬件时使用.sim.cvwfModelSim/Questa 波形配置文件直接复用信号和窗口布局如果你手上只有这个工程包没有原始 .qpf/.qsf常见做法是把sram_test.v.bak改名为sram_test.v然后在 Quartus 里新建工程并指定它为顶层模块。.v.bak只比当前版本早一版通常里面已经包含可综合的读写逻辑。.cdf不需要手动打开Quartus 的 Programmer 会自动加载.sim.cvwf则要在 ModelSim 中通过 File - Open - Waveform 打开注意它记录的是仿真路径需要按实际目录重新映射一下vsim work.tb_sram。3.3 RAM 推断的边界Chisel 生成与手写 Verilog 的底层一致FPGA 综合器识别 RAM 的模板非常固定写操作必须是if (we) mem[addr] din;读操作可以是组合或同步不能对地址位做任意运算不能在写使能上叠加复杂逻辑。只要模式匹配工具才允许把数组推断成硬核 RAM一旦你在地址上加了addr[7:1]这类位选择或者用for循环对 mem 做双写综合器立刻把它打散成普通触发器和 LUT面积与时序双双失控。这也是为什么用 Chisel 方式生成 RTL和原生 Verilog 开发相比编码效率确实高但最终能不能用上 Block RAM仍然取决于生成的 RTL 是否落入同样的模板。生成器解决的是写代码的重复劳动解决不了物理综合工具的模式匹配在 Chisel 里用Mem也突破不了 6T 单元的上限。因此在做 SRAM 选型时不要迷信生成工具打开综合报告里的 RAM Summary 看一眼才是唯一标准。4. SRAM 仿真验证读写测试、波形分析与常见陷阱4.1 Testbench 用计数器驱动地址自动比对读回数据写仿真最好的方式不是手动敲地址和期望值而是用一个 Verilog 计数器把整个地址空间走一遍。计数器的每一拍都会改变地址主时钟边沿采样写入或读出的数据最后自动统计错误次数。这样可以一次性覆盖地址译码、字节线和写使能的端点行为。下面这个 testbench 是对 2.2 节模块的完整验证。module tb_sram(); parameter DEPTH 256; parameter ADDR_WIDTH 8; parameter DATA_WIDTH 8; reg clk 0; reg we, re; reg [ADDR_WIDTH-1:0] addr; reg [DATA_WIDTH-1:0] wdata; wire [DATA_WIDTH-1:0] rdata; always #5 clk ~clk; sram_param #( .ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(DATA_WIDTH), .READ_MODE(SYNC) ) dut ( .clk(clk), .we(we), .re(re), .addr(addr), .wdata(wdata), .rdata(rdata) ); integer i; integer err_cnt 0; initial begin we 0; re 0; addr 0; wdata 0; (posedge clk); for (i 0; i DEPTH; i i 1) begin (posedge clk); we 1; addr i; wdata i[7:0]; end (posedge clk); we 0; for (i 0; i DEPTH; i i 1) begin (posedge clk); re 1; addr i; (posedge clk); if (rdata ! i[7:0]) begin err_cnt err_cnt 1; $display(Mismatch at addr 0x%0h: got 0x%0h, i, rdata); end end (posedge clk); re 0; if (err_cnt 0) $display(TEST PASSED); else $display(TEST FAILED with %0d errors, err_cnt); $finish; end initial begin $dumpfile(sram.vcd); $dumpvars(0, tb_sram); end endmodule写循环里在(posedge clk)之后才把we/addr/wdata置为有效因此第一个时钟沿采样的是初始无效信号数据真正落进 memory 从第二个时钟沿开始。写循环结束后多打一拍用于关写使能避免最后一个地址被后续读操作误写入。读循环每个地址需要两个时钟沿第一个沿采样re和addr第二个沿得到rdata的寄存输出使用!全等比较一旦读到x或z都会判定为错误。提示wdata i[7:0]把 integer 的地址值截断成 8 bit 写进去比较时也按i[7:0]比避免不同位宽符号扩展带来的不一致。4.2 大容量存储体初始化$readmemh与文件路径当容量超过 8KB还用手工赋值的方式初始化不现实。常见做法是把数据写成十六进制文本用$readmemh加载。文件每行一个数据可以用地址跳过指定位置//支持注释。例如// sram_init.hex 0000 30 31 32 FF // 这是第4个数据在 2.2 节模块里只要把INIT_FILE参数设为sram_init.hex仿真器和综合器都会在初始化阶段把数据写入 mem。注意路径是工程运行目录不是模块所在目录如果在 ModelSim 里用绝对路径最省事。如果目标 ASIC 流程initial块本身不能综合成硬件这类初始化只对 FPGA 上电有效ASIC 必须靠rst_n复位逻辑或 memory compiler 的初始化选项实现。4.3 从.sim.cvwf波形里快速定位读写违例Quartus 仿真结束后你会在 simulation 目录下看到.sim.cvwf文件它不是波形数据本身而是记录显示布局和信号路径的配置。打开后重点看三个地方第一写阶段we高电平期间地址和数据是否在时钟上升沿前建立第二同步读输出比地址晚一拍所以rdata的跳变沿应严格对齐时钟而不是跟着地址马上变第三如果波形里出现红色x优先怀疑 memory 未初始化或地址越界。常见现象可以对照下表波形现象可能原因处理方向出现红色 x未初始化 / 地址越界检查$readmemh路径和 addr 边界rdata 与 addr 同拍变化异步读被错误综合把 READ_MODE 设为 SYNC 再综合连续写同一地址数据丢失读写时序或字节使能冲突加写后读检查确认每次写都成功另一个常见问题是写读循环中地址重合在同一地址的写时钟沿之后立刻发读会因为读写共用物理端口造成先读旧值、后写新值的顺序问题这在需同日读写的设计中要改成双端口或伪双端口。5. 综合后检查与两个排错技巧RAM Style 和多字节使能5.1 先查 RAM 有没有落入你预期的 Block RAM综合完成后打开 Compilation Report 里的 RAM Summary它会列出每个内存阵列被推断成什么类型、用了几块 hard block。如果你写的是大容量 SRAM却发现 RAM Summary 空白、能看到的都是 ALUT/FF说明 RTL 没被工具识别成 RAM。一个最直接的干预是给 mem 数组加综合属性(* ramstyle M9K, MLAB *) reg [DATA_WIDTH-1:0] mem [0:(1ADDR_WIDTH)-1];在 Vivado 里对应写法是(* ram_style block *) reg [DATA_WIDTH-1:0] mem [0:(1ADDR_WIDTH)-1];这里的M9K是 Intel FPGA 中的专用存储块名称MLAB是自适应逻辑块把ramstyle设为M9K是强制使用大块 RAM设为MLAB则优先分布式。属性加完之后必须重新跑全流程再到 RAM Summary 确认综合器偶尔仍会因为读写端口时序不匹配而放弃推断此时把异步读改成同步读通常能解决问题。5.2 32 位数据按字节写入的 SRAM 手撕写法最后一个实际项目里反复出现的需求是数据位宽 32 bit但每次只写其中一两个字节。如果直接把mem[addr]当作 32 bit 整体写会把没选中的字节一起覆盖。排错技巧是用字节使能byte_we[3:0]分段赋值而不是把 mem 拆成四个 8 bit 小模块always (posedge clk) begin if (byte_we[0]) mem[addr][7:0] wdata[7:0]; if (byte_we[1]) mem[addr][15:8] wdata[15:8]; if (byte_we[2]) mem[addr][23:16] wdata[23:16]; if (byte_we[3]) mem[addr][31:24] wdata[31:24]; end这种写法在主流综合器里可以保留 RAM 推断同时让每个字节独立更新。如果某个工具认不出这种部分位写模式退回的替代方案是先把目标地址读出、修改对应字节后整写回代价是多一拍读延迟和潜在的写后读风险。手写 verilog 面试题里常以给一个字节使能的 SRAM出现考点就是这一小段代码对 Block RAM 推断的影响。本文还有配套的精品资源点击获取