第二十五章 伪双端口RAM IP核实验
对应《正点原子 ZYNQ FPGA 开发指南》第二十五章(约 P580~P600),本章无外部硬件参与,实验结果通过 ILA 在线调试观察。
本章在讲什么?(先看这个)
上一章的单端口 RAM 有个天然限制:读和写共用一个端口,同一时刻只能干一件事。而很多实际场景(最典型的就是下一章的 FIFO)要求一边写入、一边读出,同时进行——这就要用双端口 RAM。
双端口 RAM 分两种:伪双端口(端口 A 只能写、端口 B 只能读)和真双端口(两个端口都能读能写)。本章以更常用的伪双端口为例,重点解决一个新问题:读写冲突——当读写同时打向同一个地址时,读出来的数据是不可靠的。本章用"错开读写地址"的方法来规避它。
实验内容与上一章同构:BMG 配置成伪双端口 RAM(深度 64、位宽 8bit),写模块循环写入,写完一半后用 rd_flag 启动读模块,仿真 + ILA 验证读写不冲突。
核心概念拆解
25.1 RAM IP 核简介
三种静态 RAM 的框图对比一目了然:
- 单端口 RAM:一组信号(DINA/ADDRA/WEA/DOUTA...),读写共用;
- 伪双端口 RAM:端口 A 只有写入侧(DINA/CLKA/WEA/ADDRA),端口 B 只有读出侧(DOUTB/CLKB/ADDRB),两组信号完全独立,可以用两个不同的时钟;
- 真双端口 RAM:相当于把单端口 RAM 的全部信号复制一份成 A/B 两组,两个端口都能读写。



伪双端口 RAM 的框图里还有几个 ECC 相关信号(INJECTSBITERR、SBITERR 等),它们只在启用纠错码(ECC,单 bit 纠错/双 bit 检错)时才有意义,一般不用管。
两个必须理解的冲突概念:
- 读写冲突:同一时刻读写同一地址。你刚写进去的数据其实还没落进存储体,读出来的可能是默认值或旧值——读优先模式下读到旧数据,写优先模式下读到未知数据 "XX"。

- 写写冲突:两个端口的写使能同时有效且地址相同,该地址最终写入的是未知数据 "XX"。任何双端口 RAM 都不支持写写冲突,必须从设计上杜绝(伪双端口因为 A 只写、B 只读,天然不会发生)。

规避读写冲突有两条路:
- 错开读写地址(本章采用):让读地址和写地址永不指向同一地址;
- 写穿通到读(write-through):把最新写入的数据寄存一拍同步到读侧,检测到读写同地址时直接把这份"旁路数据"给读端口。

25.2 实验任务
将 BMG IP 核配置成同步(Common Clock)伪双端口 RAM 并读写,仿真观察波形,下载后用 ILA 验证读写无冲突。
25.3 程序设计
整体模块设计
因为读写信号完全独立,本章把读写拆成两个子模块:ram_wr(写模块,额外输出读启动信号 rd_flag)和 ram_rd(读模块),顶层 ip_2port_ram 负责连线。RAM 仍是纯内部存储,顶层只有时钟和复位两个输入。

RAM IP 核配置
与上一章流程完全一致,只有两处不同:
- Memory Type 选 Simple Dual Port RAM;
- 勾选 Common Clock(同步时钟)——本次读写都用 50MHz 系统时钟。不勾的话 A/B 端口可以有各自独立的时钟,这正是伪双端口能做跨时钟域数据缓冲的原因(下一章 FIFO 的读写时钟就不同)。
ECC Options 保持 No ECC;Port A(写深度 64、写位宽 8)、Port B(读深度 64、读位宽 8)配置方法同上一章,记住规则:只有端口 A 的写位宽/写深度可任意配,端口 B 的读深度会自动确定。其余选项卡保持默认,OK → Generate。

RAM 写模块设计
写模块的设计要点:复位后写端口使能和写使能一直为高,写地址在 0~63 循环;当写地址第一次计数到 31 时拉高 rd_flag 并保持,用这个"晚半程启动"把读地址和写地址错开。

module ram_wr(
input clk , //时钟信号
input rst_n , //复位信号,低电平有效
//RAM 写端口操作
output ram_wr_we , //ram 写使能
output reg ram_wr_en , //端口使能
output reg rd_flag , //读启动信号
output reg [5:0] ram_wr_addr, //ram 写地址
output [7:0] ram_wr_data //ram 写数据
);
//------------------------------------------------
//ram_wr_we 为高电平表示写数据
assign ram_wr_we = ram_wr_en ;
//写数据与写地址相同,因位宽不等,所以高位补0
assign ram_wr_data = {2'b0,ram_wr_addr} ;
//控制RAM 使能信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
ram_wr_en <= 1'b0;
else
ram_wr_en <= 1'b1;
end
//写地址信号 范围:0~63
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
ram_wr_addr <= 6'd0;
else if(ram_wr_addr < 6'd63 && ram_wr_we)
ram_wr_addr <= ram_wr_addr + 6'd1;
else
ram_wr_addr <= 6'd0;
end
//当写入32 个数据(0~31)后,拉高读启动信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rd_flag <= 1'b0;
else if(ram_wr_addr == 6'd31)
rd_flag <= 1'b1;
else
rd_flag <= rd_flag;
end
endmodule
写数据直接复用写地址({2'b0, ram_wr_addr} 高位补零),这样读出来的数据就能和地址一一对账,方便验证。
RAM 读模块设计
读模块更简单:rd_flag 拉高即读端口使能,读地址 0~63 循环。注意端口 B 只能读、没有写使能信号,使能后一直处于读状态。
module ram_rd(
input clk , //时钟信号
input rst_n , //复位信号,低电平有效
//RAM 读端口操作
input rd_flag , //读启动标志
input [7:0] ram_rd_data, //ram 读数据
output ram_rd_en , //端口使能
output reg [5:0] ram_rd_addr //ram 读地址
);
//------------------------------------------------
//控制RAM 使能信号
assign ram_rd_en = rd_flag;
//读地址信号 范围:0~63
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
ram_rd_addr <= 6'd0;
else if(ram_rd_addr < 6'd63 && ram_rd_en)
ram_rd_addr <= ram_rd_addr + 6'd1;
else
ram_rd_addr <= 6'd0;
end
endmodule
顶层模块设计
顶层把写模块、RAM IP、读模块依次拉通,ILA 例化在顶层观察全部关键信号:
module ip_2port_ram(
input sys_clk , //系统时钟
input sys_rst_n //系统复位,低电平有效
);
//wire define
wire ram_wr_en ; //RAM 端口A 使能
wire ram_wr_we ; //RAM 端口A 写使能
wire ram_rd_en ; //RAM 端口B 使能
wire rd_flag ; //读启动标志
wire [5:0] ram_wr_addr; //RAM 写地址
wire [7:0] ram_wr_data; //RAM 写数据
wire [5:0] ram_rd_addr; //RAM 读地址
wire [7:0] ram_rd_data; //RAM 读数据
//------------------------------------------------
//RAM 写模块
ram_wr u_ram_wr(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
.rd_flag (rd_flag ),
.ram_wr_en (ram_wr_en ),
.ram_wr_we (ram_wr_we ),
.ram_wr_addr (ram_wr_addr),
.ram_wr_data (ram_wr_data)
);
//简单双端口RAM
blk_mem_gen_0 u_blk_mem_gen_0 (
.clka (sys_clk ), // input wire clka
.ena (ram_wr_en ), // input wire ena
.wea (ram_wr_we ), // input wire [0 : 0] wea
.addra (ram_wr_addr), // input wire [5 : 0] addra
.dina (ram_wr_data), // input wire [7 : 0] dina
.clkb (sys_clk ), // input wire clkb
.enb (ram_rd_en ), // input wire enb
.addrb (ram_rd_addr), // input wire [5 : 0] addrb
.doutb (ram_rd_data) // output wire [7 : 0] doutb
);
//RAM 读模块
ram_rd u_ram_rd(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
.rd_flag (rd_flag ),
.ram_rd_en (ram_rd_en ),
.ram_rd_addr (ram_rd_addr),
.ram_rd_data (ram_rd_data)
);
//ILA 在线调试
ila_0 u_ila_0 (
.clk (sys_clk ),
.probe0 (ram_wr_en ),
.probe1 (ram_wr_we ),
.probe2 (ram_rd_en ),
.probe3 (rd_flag ),
.probe4 (ram_wr_addr),
.probe5 (ram_wr_data),
.probe6 (ram_rd_addr),
.probe7 (ram_rd_data)
);
endmodule
顶层波形图能看清整个时序编排:端口 A 先启动、一直写;写到一半 rd_flag 拉高启动端口 B;此后读写同时进行,但地址恒定相差 32,互不干涉。

25.4 仿真验证与下载验证
TB 仍只需时钟和复位激励。仿真波形有三个关键观察点:
- rd_flag 拉高后 ram_rd_en 才拉高,读模块随后开始工作;
- 一个时序逻辑细节:代码里是"写地址==31 时给 rd_flag 赋 1",但波形上 rd_flag 在写地址为 32 时才拉高——因为时序逻辑的赋值发生在下一个时钟上升沿,不是当拍生效。看波形时要有这个"延迟一拍"的意识;
- 读启动后写端口和读端口同时对 RAM 操作,读写地址恒定相差 32,没有发生读写冲突。


下载验证:引脚仍只需 sys_clk(U18)、sys_rst_n(N16),LVCMOS33,外加 create_clock 时序约束。ILA 波形与仿真一致:写端口和读端口同时工作、无读写冲突,实验验证成功。
初学者容易踩的坑
- 读写冲突不会报错,只会给你错数据。综合、实现、仿真(除非碰巧覆盖)都可能发现不了,上板后表现为"偶发读到旧值/未知值"。做双端口设计时先在纸上画出读写地址随时间的变化,确认永不相交。
- 错开一个地址就够,不必错开 32 个。只要读写时钟同频,地址错开 1 个就不会撞上。本章错开 32 是为了在 ILA 上看得更清楚。
- "地址==31 时 rd_flag 拉高"在波形上显示为地址 32 时拉高。这是时序逻辑一拍延迟的正常现象,不是代码 bug。分析波形永远要把寄存器输出比条件判断晚一拍算进去。
- 伪双端口的端口 A 没有读功能,所以配置界面里端口 A 不存在 Operating Mode(写优先/读优先)这些选项——它们是"写的时候读数据线输出什么"的规则,只在单端口/真双端口上有意义。
- 端口 B 使能后一直在读,没有"读使能"可关。如果需要"只读特定几个地址",要靠控制 ENB 的高低来实现,而不是去找一个不存在的读使能信号。
- Common Clock 勾不勾要想清楚。勾了表示 A/B 共用一个时钟(本章场景);不勾则读写可以各自给时钟,做跨时钟域缓冲时才需要——但也随之引入跨时钟域约束问题,初学阶段先用同步时钟。
小结 & 下一步
本章完成了存储器三部曲的第二部:理解了伪双端口 RAM"写读分家、可双时钟"的结构,掌握了读写冲突/写写冲突的概念及"错开地址"这一最简单的规避手段,rd_flag 这种"写侧通知读侧开工"的握手思路也是 FPGA 设计里的常见手法。真双端口 RAM 的用法作为拓展训练(两个读写模块各管一个端口、分区读写规避两种冲突),有兴趣可以自行挑战。
下一步进入第二十六章:FIFO IP 核实验。FIFO 的内部存储用的正是伪双端口 RAM,而读写时钟可以不同——届时还会引入跨时钟域、空满标志同步等更进阶的概念。