1 前言在FPGA设计中双端口Block RAM是一种宝贵资源它允许两个端口独立地对同一存储空间进行读写操作广泛应用于数据缓存、跨时钟域交互、乒乓缓冲等场景。本文解析一个极简但实用性强、可扩展性好的双端口存储器操作模块它能通过A端口写入有符号数据同时通过B端口独立读出并且包含一个预留状态机框架。我们将结合Testbench进行仿真分析帮助读者理解硬件行为和设计思想。2 功能描述该模块对外呈现一个类似“存储器测试/访问接口”的形式A端口写每个时钟上升沿将输入数据i_din写入地址i_addra写使能始终有效。B端口读每个时钟上升沿从地址i_addrb读出一个数据经过一级寄存器锁存后输出o_dout。支持同步复位复位时状态机回到IDLE输出清零释放复位后正常工作。所有数据均为10位有符号数补码表示范围 -512 ~ 511。3 接口说明端口名方向位宽说明i_clk输入1系统时钟i_rst输入1同步复位高有效i_din输入10 (signed)A端口待写入数据i_addra输入10A端口写地址i_addrb输入10B端口读地址o_dout输出10 (signed)B端口读出的数据打拍后4 内部结构与关键设计4.1 Block Memory IP 配置要求需生成一个真双端口True Dual-PortBlock Memory配置如下A口写使能无读数据输出或未使用读B口仅读带复位rstb数据宽度10位深度至少满足10位地址0~1023。4.2 代码分段解析4.2.1 状态机框架预留扩展点parameter P_ST_IDLE 1b1; // 独热码 IDLE reg state_c, state_n; always (*) begin case (state_c) default: state_n P_ST_IDLE; endcase end always (posedge i_clk or posedge i_rst) begin if (i_rst) state_c d0; else state_c state_n; end当前设计中状态机只有一个IDLE状态且不会跳转似乎“多余”但实际上这是一个精心留出的扩展位。当未来需要加入写入保护、地址仲裁或突发读/写等控制逻辑时可以直接修改组合逻辑分支无需重构代码骨架。4.2.2 双端口存储器实例化blk_mem_gen_0 blk_mem_gen_0u ( .clka (i_clk ), .wea (1b1 ), // A口始终写使能 .addra (i_addra ), .dina (i_din ), .clkb (i_clk ), .rstb (i_rst ), // B口可复位清零输出 .addrb (i_addrb ), .doutb (w_doutb ), .rsta_busy ( ), .rstb_busy ( ) );A端口固定wea1意味着任何时钟沿都会将i_din写入i_addra如果不需要写入应在外部控制i_din和i_addra保持不变或拉低写使能可扩展。B端口使用rstb复位当i_rst有效时B口输出数据线被清零配合后续输出寄存器复位确保读出数据可靠归零。4.2.3 输出流水线寄存器reg signed [9:0] ro_o_dout; assign o_dout ro_o_dout; always (posedge i_clk or posedge i_rst) begin if (i_rst) ro_o_dout d0; else ro_o_dout w_doutb; end从Block RAM读出的w_doutb会存在组合逻辑延迟如果直接输出到模块外部可能引入毛刺并拉长时序路径。这里用一级寄存器锁存使o_dout完全来自触发器时序干净稳定系统频率可大幅提升。4.3 时序行为小结复位期间B口输出强制为0o_dout0。释放复位后第N个时钟沿B口地址i_addrb被采样Block RAM在内部开始读取第N1个时钟沿w_doutb更新为该地址存储的数据第N1个时钟沿后ro_o_dout锁存w_doutbo_dout对外可见。因此读操作存在2级延迟Block RAM读延迟 输出寄存器这是同步读RAM的典型表现。5 仿真测试与验证Testbench模拟了一个简单却完整的“写后读”测试场景时间操作0~100ns复位有效输出为0100ns释放复位开始写地址1数据120地址2数据130地址3数据140地址4数据150每10ns换一个地址和数据200ns开始读地址1~4每10ns切换一次5.1 预期输出波形时钟: _|¯|_|¯|_|¯|_|¯|_|¯|_|¯|_|¯|_|¯|_ 复位: ~~~~~~~~~|_________________________ i_addra: 0 1 2 3 4 4 ... i_din: 0 120 130 140 150 ... i_addrb: 0 0 ...(200ns) 1 2 3 4 ... w_doutb: 0 0 ... D1 D2 D3 D4 ... o_dout: 0 0 ... 0 D1 D2 D3 D4 ...读取地址切换后经过两个时钟周期输出端出现正确数据。写入的同时如果读取相同地址会因流水线特性读到旧值符合同步写读的Block RAM行为。5.2 验证结果仿真中o_dout在地址1~4依次输出120、130、140、150证明存储器写入和读取功能正常流水线输出逻辑正确。6 创新点与设计亮点双端口独立并行操作写端口和读端口使用独立地址总线可同时进行写入与读取互不干扰。实际应用中可用来构造FIFO、乒乓缓冲、参数表动态更新等结构。输出流水线打拍刻意在B端口数据输出后增加一级ro_o_dout寄存器将组合逻辑路径截断优化时序。这是成熟工程中的惯用做法值得初学者仔细体会。可扩展的状态机骨架虽然当前仅有IDLE状态且无跳转但其one-hot编码风格、组合逻辑加时序寄存器的结构为日后加入写保护、多页切换、错误检测等复杂控制提供了标准的插入点无需推倒重来。干净统一的复位管理A口无复位多数Block Memory IP不支持A口复位B口带复位并将复位信号同时作用于输出寄存器和IP的rstb保证所有输出在复位时彻底归零系统上电状态一致。有符号数原生支持模块端口均声明signed内部存储和读取自动保持补码解释便于直接连接DSP模块或算法处理单元。7 总结与拓展建议本文分析的双端口存储器读写模块以其精简的代码实现了完整的“写-存-读”链路并通过流水线输出、预留状态机等设计提升了时序性能和可扩展性。该模块非常适合作为IP评估测试、算法数据缓冲、参数表在线更新等场景的基础构建块。读者可在此基础上自行尝试将写使能wea改为外部输入实现可控写入增加读写地址生成逻辑构建一个真正的FIFO或RAM测试器扩展状态机实现读写冲突时的仲裁或等待。8 完整代码8.1 设计模块tops.vtimescale 1ns / 1ps module tops ( input i_clk, input i_rst, input signed [9:0] i_din, input [9:0] i_addra, input [9:0] i_addrb, output signed [9:0] o_dout ); // param parameter P_ST_IDLE 1b1; // one-hot idle // reg reg state_c; reg state_n; reg signed [9:0] ro_o_dout; // wire wire signed [9:0] w_doutb; // assign assign o_dout ro_o_dout; // FSM // (no state transition conditions in this design) // inst blk_mem_gen_0 blk_mem_gen_0u ( .clka (i_clk ), .wea (1b1 ), .addra (i_addra ), .dina (i_din ), .clkb (i_clk ), .rstb (i_rst ), .addrb (i_addrb ), .doutb (w_doutb ), .rsta_busy ( ), .rstb_busy ( ) ); // combine_Logic always (*) begin case (state_c) default: state_n P_ST_IDLE; endcase end // always always (posedge i_clk or posedge i_rst) begin if (i_rst) begin state_c d0; end else begin state_c state_n; end end always (posedge i_clk or posedge i_rst) begin if (i_rst) begin ro_o_dout d0; end else begin ro_o_dout w_doutb; end end endmodule8.2 测试平台test_tops.vtimescale 1ns / 1ps module test_tops; reg i_clk; reg i_rst; reg signed[9:0]i_din; reg signed[9:0]i_addra; reg signed[9:0]i_addrb; wire signed[9:0]o_dout; tops tops_u( .i_clk (i_clk), .i_rst (i_rst), .i_din (i_din), .i_addra(i_addra), .i_addrb(i_addrb), .o_dout (o_dout) ); initial begin i_clk 1b1; i_rst 1b1; #100 i_rst 1b0; end initial begin i_din 10d0; i_addra 10d0; #100 i_din 10d120; i_addra 10d1; #10 i_din 10d130; i_addra 10d2; #10 i_din 10d140; i_addra 10d3; #10 i_din 10d150; i_addra 10d4; end initial begin i_addrb 10d0; #200 i_addrb 10d1; #10 i_addrb 10d2; #10 i_addrb 10d3; #10 i_addrb 10d4; end always #5 i_clk ~i_clk; endmodule提示使用前请在Vivado中生成一个真双端口Block Memory配置数据宽度10位、深度≥1024并使能B端口的复位选项例化名称需与blk_mem_gen_0一致。
FPGA实战(06):基于双端口Block Memory的独立读写模块设计(Verilog)
发布时间:2026/6/13 1:19:02
1 前言在FPGA设计中双端口Block RAM是一种宝贵资源它允许两个端口独立地对同一存储空间进行读写操作广泛应用于数据缓存、跨时钟域交互、乒乓缓冲等场景。本文解析一个极简但实用性强、可扩展性好的双端口存储器操作模块它能通过A端口写入有符号数据同时通过B端口独立读出并且包含一个预留状态机框架。我们将结合Testbench进行仿真分析帮助读者理解硬件行为和设计思想。2 功能描述该模块对外呈现一个类似“存储器测试/访问接口”的形式A端口写每个时钟上升沿将输入数据i_din写入地址i_addra写使能始终有效。B端口读每个时钟上升沿从地址i_addrb读出一个数据经过一级寄存器锁存后输出o_dout。支持同步复位复位时状态机回到IDLE输出清零释放复位后正常工作。所有数据均为10位有符号数补码表示范围 -512 ~ 511。3 接口说明端口名方向位宽说明i_clk输入1系统时钟i_rst输入1同步复位高有效i_din输入10 (signed)A端口待写入数据i_addra输入10A端口写地址i_addrb输入10B端口读地址o_dout输出10 (signed)B端口读出的数据打拍后4 内部结构与关键设计4.1 Block Memory IP 配置要求需生成一个真双端口True Dual-PortBlock Memory配置如下A口写使能无读数据输出或未使用读B口仅读带复位rstb数据宽度10位深度至少满足10位地址0~1023。4.2 代码分段解析4.2.1 状态机框架预留扩展点parameter P_ST_IDLE 1b1; // 独热码 IDLE reg state_c, state_n; always (*) begin case (state_c) default: state_n P_ST_IDLE; endcase end always (posedge i_clk or posedge i_rst) begin if (i_rst) state_c d0; else state_c state_n; end当前设计中状态机只有一个IDLE状态且不会跳转似乎“多余”但实际上这是一个精心留出的扩展位。当未来需要加入写入保护、地址仲裁或突发读/写等控制逻辑时可以直接修改组合逻辑分支无需重构代码骨架。4.2.2 双端口存储器实例化blk_mem_gen_0 blk_mem_gen_0u ( .clka (i_clk ), .wea (1b1 ), // A口始终写使能 .addra (i_addra ), .dina (i_din ), .clkb (i_clk ), .rstb (i_rst ), // B口可复位清零输出 .addrb (i_addrb ), .doutb (w_doutb ), .rsta_busy ( ), .rstb_busy ( ) );A端口固定wea1意味着任何时钟沿都会将i_din写入i_addra如果不需要写入应在外部控制i_din和i_addra保持不变或拉低写使能可扩展。B端口使用rstb复位当i_rst有效时B口输出数据线被清零配合后续输出寄存器复位确保读出数据可靠归零。4.2.3 输出流水线寄存器reg signed [9:0] ro_o_dout; assign o_dout ro_o_dout; always (posedge i_clk or posedge i_rst) begin if (i_rst) ro_o_dout d0; else ro_o_dout w_doutb; end从Block RAM读出的w_doutb会存在组合逻辑延迟如果直接输出到模块外部可能引入毛刺并拉长时序路径。这里用一级寄存器锁存使o_dout完全来自触发器时序干净稳定系统频率可大幅提升。4.3 时序行为小结复位期间B口输出强制为0o_dout0。释放复位后第N个时钟沿B口地址i_addrb被采样Block RAM在内部开始读取第N1个时钟沿w_doutb更新为该地址存储的数据第N1个时钟沿后ro_o_dout锁存w_doutbo_dout对外可见。因此读操作存在2级延迟Block RAM读延迟 输出寄存器这是同步读RAM的典型表现。5 仿真测试与验证Testbench模拟了一个简单却完整的“写后读”测试场景时间操作0~100ns复位有效输出为0100ns释放复位开始写地址1数据120地址2数据130地址3数据140地址4数据150每10ns换一个地址和数据200ns开始读地址1~4每10ns切换一次5.1 预期输出波形时钟: _|¯|_|¯|_|¯|_|¯|_|¯|_|¯|_|¯|_|¯|_ 复位: ~~~~~~~~~|_________________________ i_addra: 0 1 2 3 4 4 ... i_din: 0 120 130 140 150 ... i_addrb: 0 0 ...(200ns) 1 2 3 4 ... w_doutb: 0 0 ... D1 D2 D3 D4 ... o_dout: 0 0 ... 0 D1 D2 D3 D4 ...读取地址切换后经过两个时钟周期输出端出现正确数据。写入的同时如果读取相同地址会因流水线特性读到旧值符合同步写读的Block RAM行为。5.2 验证结果仿真中o_dout在地址1~4依次输出120、130、140、150证明存储器写入和读取功能正常流水线输出逻辑正确。6 创新点与设计亮点双端口独立并行操作写端口和读端口使用独立地址总线可同时进行写入与读取互不干扰。实际应用中可用来构造FIFO、乒乓缓冲、参数表动态更新等结构。输出流水线打拍刻意在B端口数据输出后增加一级ro_o_dout寄存器将组合逻辑路径截断优化时序。这是成熟工程中的惯用做法值得初学者仔细体会。可扩展的状态机骨架虽然当前仅有IDLE状态且无跳转但其one-hot编码风格、组合逻辑加时序寄存器的结构为日后加入写保护、多页切换、错误检测等复杂控制提供了标准的插入点无需推倒重来。干净统一的复位管理A口无复位多数Block Memory IP不支持A口复位B口带复位并将复位信号同时作用于输出寄存器和IP的rstb保证所有输出在复位时彻底归零系统上电状态一致。有符号数原生支持模块端口均声明signed内部存储和读取自动保持补码解释便于直接连接DSP模块或算法处理单元。7 总结与拓展建议本文分析的双端口存储器读写模块以其精简的代码实现了完整的“写-存-读”链路并通过流水线输出、预留状态机等设计提升了时序性能和可扩展性。该模块非常适合作为IP评估测试、算法数据缓冲、参数表在线更新等场景的基础构建块。读者可在此基础上自行尝试将写使能wea改为外部输入实现可控写入增加读写地址生成逻辑构建一个真正的FIFO或RAM测试器扩展状态机实现读写冲突时的仲裁或等待。8 完整代码8.1 设计模块tops.vtimescale 1ns / 1ps module tops ( input i_clk, input i_rst, input signed [9:0] i_din, input [9:0] i_addra, input [9:0] i_addrb, output signed [9:0] o_dout ); // param parameter P_ST_IDLE 1b1; // one-hot idle // reg reg state_c; reg state_n; reg signed [9:0] ro_o_dout; // wire wire signed [9:0] w_doutb; // assign assign o_dout ro_o_dout; // FSM // (no state transition conditions in this design) // inst blk_mem_gen_0 blk_mem_gen_0u ( .clka (i_clk ), .wea (1b1 ), .addra (i_addra ), .dina (i_din ), .clkb (i_clk ), .rstb (i_rst ), .addrb (i_addrb ), .doutb (w_doutb ), .rsta_busy ( ), .rstb_busy ( ) ); // combine_Logic always (*) begin case (state_c) default: state_n P_ST_IDLE; endcase end // always always (posedge i_clk or posedge i_rst) begin if (i_rst) begin state_c d0; end else begin state_c state_n; end end always (posedge i_clk or posedge i_rst) begin if (i_rst) begin ro_o_dout d0; end else begin ro_o_dout w_doutb; end end endmodule8.2 测试平台test_tops.vtimescale 1ns / 1ps module test_tops; reg i_clk; reg i_rst; reg signed[9:0]i_din; reg signed[9:0]i_addra; reg signed[9:0]i_addrb; wire signed[9:0]o_dout; tops tops_u( .i_clk (i_clk), .i_rst (i_rst), .i_din (i_din), .i_addra(i_addra), .i_addrb(i_addrb), .o_dout (o_dout) ); initial begin i_clk 1b1; i_rst 1b1; #100 i_rst 1b0; end initial begin i_din 10d0; i_addra 10d0; #100 i_din 10d120; i_addra 10d1; #10 i_din 10d130; i_addra 10d2; #10 i_din 10d140; i_addra 10d3; #10 i_din 10d150; i_addra 10d4; end initial begin i_addrb 10d0; #200 i_addrb 10d1; #10 i_addrb 10d2; #10 i_addrb 10d3; #10 i_addrb 10d4; end always #5 i_clk ~i_clk; endmodule提示使用前请在Vivado中生成一个真双端口Block Memory配置数据宽度10位、深度≥1024并使能B端口的复位选项例化名称需与blk_mem_gen_0一致。