第二十四章 单端口RAM IP核实验

对应《正点原子 ZYNQ FPGA 开发指南》第二十四章(约 P560~P579),本章无外部硬件参与,实验结果通过 ILA 在线调试观察。

本章在讲什么?(先看这个)

前面的实验里,我们存数据用的都是寄存器(reg)。寄存器快,但数量有限——想存几千上万个数据,寄存器资源根本扛不住。FPGA 里专门为此准备了块存储资源 BRAM(Block RAM),是厂商在逻辑资源之外加入的专用 RAM 硬块,速度快、容量大,构成数字电路体系的四大件之一(查找表、寄存器、组合逻辑、静态RAM)。

本章学习 Vivado 里调用 BRAM 的入口:Block Memory Generator IP 核(BMG)。任务是把它配置成一个单端口 RAM(深度 32、位宽 8bit),写模块往里写 0~31 共 32 个数,再读出来比对,仿真确认后用 ILA 上板验证。

本章是存储器三部曲的第一部(单端口 RAM → 伪双端口 RAM → FIFO),BMG 的配置套路学一遍,后面两章就是换选项而已。

核心概念拆解

24.1 RAM 简介

先看存储器的家谱:存储器分随机存储器(RAM)和只读存储器(ROM)。RAM 又分静态 RAM(有电就保数据,不需要刷新)和动态 RAM(需要周期性刷新,典型如 SDRAM/DDR)。本章的 BRAM 属于静态 RAM。

图24.1.1 存储器分类

静态 RAM 按端口分三种,这个表格值得背下来:

RAM类型 特性 适用场景
单端口 RAM(SP RAM) 只有一组数据/地址总线,读写共用一个端口 上电配置一次的参数表
伪双端口 RAM(SDP RAM) A 端口只写、B 端口只读,读写可同时进行 FIFO
真双端口 RAM(TDP RAM) A/B 两个端口都能读、都能写 需要两个独立写入口的场合(如统计模块)

有个反直觉的知识点:7 系列器件内部的 BRAM 硬块本身全部是真双端口 RAM,BMG IP 核只是把它"裁剪"配置成单端口或伪双端口来用——下一章我们会看到这一点。

BMG 配置成单端口 RAM 后的接口框图如下。核心信号就六个:写数据 DINA、地址 ADDRA、写使能 WEA(高写低读)、端口使能 ENA、时钟 CLKA、读数据 DOUTA。

图24.1.2 单端口RAM框图

24.2 硬件设计

本章只用系统时钟和复位按键,没有新硬件。

24.3 实验任务

将 BMG IP 核配置成单端口 RAM 并读写,仿真观察波形,下载后用 ILA 验证读写结果。

24.4 程序设计

整体模块设计

系统由三部分组成:顶层 ip_1port_ram + RAM IP 核(blk_mem_gen_0)+ 读写控制模块 ram_rw。RAM 是纯内部存储,不需要对外输出引脚,所以顶层只有时钟和复位两个输入。

图24.4.1 单端口RAM读写实验系统框图

RAM IP 核配置

IP Catalog 搜 "Block Memory",双击 Block Memory Generator:

图24.4.3 搜索Block Memory关键字

Basic 选项卡

  • Interface Type:选 Native。AXI4 接口一般只在和处理器交互数据时(如 ZYNQ 的 PS/PL 交互)才用;
  • Memory Type:选 Single Port RAM(另有伪双端口/真双端口 RAM 和 ROM 可选);
  • Write Enable(字节写使能)、Algorithm Options 保持默认。

图24.4.4 Basic选项卡配置界面

Port A Options 选项卡(重点):

  • Write Width = 8Read Width = 8(读写位宽通常一致;若不同,只支持 1:1、1:2、1:4 等固定倍数比);
  • Write Depth = 32,地址范围 0~31(注意位宽×深度不要超过器件 BRAM 总量);
  • Operating Mode:选 No Change(不变模式)——三种模式的区别见下文时序图;
  • Enable Port Type 保持 Use ENA pin;
  • 建议取消勾选 Primitives Output Register(Primitives/Core Output Register 都是给输出加一级寄存器来改善时序的,代价是读数据多延迟一拍;取消后当前周期给地址、下一周期就能读到数据)。

图24.4.5 Port A Options选项卡配置界面

Other Options(初始化 .coe 文件等)和 Summary 保持默认,OK → Generate。之后 Design Runs 里同样能看到 blk_mem_gen_0_synth_1 在做 OOC 独立综合。

三种操作模式的时序图

这是本章最重要的原理知识。WEA=1 时既在写、也涉及读数据线 DOUT 上输出什么,三种模式的答案不同:

写优先(Write First):写入新数据后,把刚写入的数据送到 DOUT 上。即写地址 bb 时,DOUT 输出的就是新写入的 "1111"。

图24.4.10 写优先模式时序图

读优先(Read First):写操作时先把该地址的旧数据送到 DOUT,再写入新数据。即写地址 bb 时,DOUT 输出的是写入前的旧值。

图24.4.11 读优先模式时序图

不变(No Change):写操作期间 DOUT 保持不变(维持最后一次读操作的数据),读写彻底分开。

图24.4.12 不变模式时序图

不管哪种模式,读数据都比地址晚一个时钟周期输出——这是同步 RAM 的基本时序,写读出逻辑时时刻要记着。

RAM 读写模块设计

ram_rw 模块负责产生 RAM 需要的全部控制信号。设计思路:计数器 rw_cnt 循环计 0~63,0~31 时拉高 ram_we 写数据(写数据和地址都从 0 累加到 31),32~63 时拉低 ram_we 读数据(地址继续 0~31 循环)

图24.4.13 RAM读写模块框图与端口

先画波形图确认设计:ram_en 复位后恒为高;读写地址在 0~31 循环;读数据比地址晚一拍出现,读切写的瞬间保持最后一次读出的值。

图24.4.14 读写模块波形图

module ram_rw(
    input               clk         ,  //时钟信号
    input               rst_n       ,  //复位信号,低电平有效

    output  reg         ram_en      ,  //ram 使能信号
    output              ram_we      ,  //ram 读写选择
    output  reg  [4:0]  ram_addr    ,  //ram 读写地址
    output  reg  [7:0]  ram_wr_data ,  //ram 写数据
    input        [7:0]  ram_rd_data    //ram 读数据
);

//reg define
reg    [5:0]  rw_cnt ;                //读写控制计数器

//------------------------------------------------
//rw_cnt 计数范围在0~31,写入数据;32~63 时,读出数据
assign ram_we = (rw_cnt <= 6'd31 && ram_en == 1'b1) ? 1'b1 : 1'b0;

//控制RAM 使能信号
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        ram_en <= 1'b0;
    else
        ram_en <= 1'b1;
end

//读写控制计数器,计数器范围0~63
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        rw_cnt <= 6'b0;
    else if(rw_cnt == 6'd63  && ram_en)
        rw_cnt <= 6'b0;
    else if(ram_en)
        rw_cnt <= rw_cnt + 6'd1;
    else
        rw_cnt <= 6'b0;
end

//读写地址信号 范围:0~31
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        ram_addr <= 5'b0;
    else if(ram_addr == 5'd31 && ram_en)
        ram_addr <= 5'b0;
    else if (ram_en)
        ram_addr <= ram_addr + 5'd1;
    else
        ram_addr <= 5'b0;
end

//在WE 拉高期间产生RAM 写数据,变化范围是0~31
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        ram_wr_data <= 8'b0;
    else if(ram_wr_data < 8'd31 && ram_we)
        ram_wr_data <= ram_wr_data + 8'd1;
    else
        ram_wr_data <= 8'b0 ;
end

//ILA 在线调试
ila_0 u_ila_0 (
    .clk(clk),             // input wire clk

    .probe0(ram_en),       // input wire [0:0]  probe0
    .probe1(ram_we),       // input wire [0:0]  probe1
    .probe2(ram_addr),     // input wire [4:0]  probe2
    .probe3(ram_wr_data),  // input wire [7:0]  probe3
    .probe4(ram_rd_data)   // input wire [7:0]  probe4
);

endmodule

顶层模块设计

顶层就是把 IP 核和读写模块拉通连线:

图24.4.15 顶层模块框图与端口

module ip_1port_ram(
    input         sys_clk ,  //系统时钟
    input         sys_rst_n  //系统复位,低电平有效
);

//wire define
wire             ram_en      ;  //RAM 使能
wire             ram_we      ;  //RAM 读写使能信号,高电平写入,低电平读出
wire    [4:0]    ram_addr    ;  //RAM 读写地址
wire    [7:0]    ram_wr_data ;  //RAM 写数据
wire    [7:0]    ram_rd_data ;  //RAM 读数据

//------------------------------------------------
//ram 读写模块
ram_rw  u_ram_rw(
    .clk            (sys_clk     ),
    .rst_n          (sys_rst_n   ),
    //RAM
    .ram_en         (ram_en      ),
    .ram_we         (ram_we      ),
    .ram_addr       (ram_addr    ),
    .ram_wr_data    (ram_wr_data ),
    .ram_rd_data    (ram_rd_data )
);

//ram ip 核
blk_mem_gen_0  blk_mem_gen_0 (
    .clka  (sys_clk      ),  // input wire clka
    .ena   (ram_en       ),  // input wire ena
    .wea   (ram_we       ),  // input wire [0 : 0] wea
    .addra (ram_addr     ),  // input wire [4 : 0] addra
    .dina  (ram_wr_data  ),  // input wire [7 : 0] dina
    .douta (ram_rd_data  )   // output wire [7 : 0] douta
);

endmodule

24.5 仿真验证与下载验证

TB 只需时钟和复位两个激励(与流水灯实验相同套路)。仿真波形验证了不变模式的特性:写操作期间读数据线保持不变;写阶段地址和数据同步从 0 累加到 31;读阶段给出地址的下一拍,ram_rd_data 输出 0、1、2……31,与写入值完全一致。

图24.4.17 写操作仿真波形图

下载验证:本章顶层没有对外输出,引脚只需约束时钟和复位(sys_clk=U18,sys_rst_n=N16,LVCMOS33,外加 create_clock -period 20.000 时序约束)。ILA 已实例化在 ram_rw 模块中,上板后在 Hardware Manager 里触发采集:写操作波形显示地址/数据同步累加,读操作波形显示读出数据滞后地址一拍且数值 0~31 全部正确——与仿真结果一致,实验验证成功。

初学者容易踩的坑

  • 读数据晚一拍是常态不是 bug。同步 RAM 给出地址后要等下一个时钟上升沿数据才出现在 DOUT 上。很多人在 ILA 里看到"地址 0 读出的数据是上一个值"就以为 IP 核坏了,其实把波形对齐一拍看就完全正确。
  • Output Register 的取舍要想清楚。勾选 Primitives/Core Output Register 能改善时序、提高频率,但读延迟变成两拍。如果你照抄"下一拍出数据"的经验去写状态机,时序就错位了。学习阶段建议不勾。
  • Operating Mode 不是随便选的。写优先模式写操作会把新数据冒到读数据线上,读优先模式会把旧数据冒出来——如果你的下游逻辑假设"写的时候读数据不动",就必须选 No Change,否则会读到意外数据。
  • 深度和位宽量力而行。Write Depth × Write Width 超过器件 BRAM 总量会直接报错;地址位宽要根据深度算好(深度 32 → 地址 5bit),位宽给小了地址会截断、给大了浪费资源。
  • 读写位宽不同时只有固定倍数比可用(1:1、1:2、1:4、1:8、2:1、4:1、8:1、16:1),其它比例 Vivado 不接受。
  • RAM 没有输出引脚不代表不用做引脚约束检查。顶层只有时钟和复位两个 IO,XDC 里仍要写 create_clock 时序约束——少了它,时序分析没有基准,综合结果不可信。

小结 & 下一步

本章完成了存储器三部曲的第一部:弄清了静态 RAM 三种端口形态的区别,掌握了 BMG IP 核配置单端口 RAM 的关键选项(Native 接口、Memory Type、位宽/深度、Operating Mode、输出寄存器),并通过仿真和 ILA 验证了"写 0~31 再读出 0~31"的完整流程。ram_rw 这种"计数器切换读写状态"的测试结构还会在后面两章反复出现

下一步进入第二十五章:伪双端口 RAM 实验——写端口和读端口分开、读写可以同时进行,还会引出读写冲突等重要概念。