笔记根据正点原子官方教学视频第19讲
时间:2025/4/2
【第一期】手把手教你学领航者&启明星ZYNQ之FPGA开发篇【真人出镜】FPGA教学视频教程_哔哩哔哩_bilibili
有错误的地方希望可以告诉博主
前言
RAM 介绍
RAM(Random Access Memory ):即随机访问存储器,它可以随时把数据写入任一指定地址的存储单元,也可以随时从任一指定地址中读出数据,其读写速度是由时钟频率决定的。RAM主要用来存放程序及程序执行过程中产生的中间数据、运算结果等。
存储器分类
特点 | RAM | ROM |
---|---|---|
可写性 | 可以随时写入数据 | 通常只读,某些类型可以写入(如 EEPROM) |
易失性 | 易失性,断电后数据丢失 | 非易失性,断电后数据不丢失 |
访问速度 | 读写速度快 | 读取速度较慢 |
用途 | 用于存储临时数据和正在运行的程序 | 用于存储固化的程序代码和配置数据 |
静态 RAM 和 动态 RAM 的区别如下:
特点 | DRAM | SRAM |
---|---|---|
刷新需求 | 需要定期刷新(Refresh) | 不需要刷新 |
访问速度 | 较慢(需要刷新) | 较快(不需要刷新) |
存储密度 | 高(单位面积存储容量大) | 低(单位面积存储容量小) |
成本 | 低(单位存储成本低) | 高(单位存储成本高) |
功耗 | 较高(刷新操作增加功耗) | 较低(不需要刷新) |
应用场景 | 主存储器(内存条)、显存等大容量存储需求 | 高速缓存(Cache)、寄存器堆等高速存储需求 |
静态 RAM
单端口 RAM 在同一个端口进行读写操作。适用场景:没有同时读写需求。
简单双端口 RAM 虽然有两个端口,但端口只能进行读操作,或者进行写操作。适用场景:有同时读写需求。
真双端口 RAM 的两个端口都支持读写操作。适用场景:有多个同时读写需求。
单端口 RAM IP 核端口
端口 | 方向 | 作用 | 备注 |
DINA | INPUT | 数据写入端口 | A 是端口序号,如果还有一个 DIN 端口,就叫DINB 了,以下端口同理。 |
ADDRA | INPUT | 地址输入端口 | 用于指定要访问的 RAM 地址。 |
WEA | INPUT | 读写控制信号端口 | 高电平 1 时,写;低电平 0 时,读。 |
ENA | INPUT | 使能端口 | 高电平 1 时,RAM 可用。 |
RSTA | INPUT | 复位端口 | 为高电平时,RAM 的内容被清零。 |
REGCEA | INPUT | 寄存器使能端口 | 用于控制输出寄存器的使能。低电平 0 时,DOUTA 输出固定值;高电平 1 时,输出输出寄存器中的数据。 |
CLKA | INPUT | 时钟输入端口 | 用于同步 RAM 的操作。 |
DOUTA | OUTPUT | 数据输出端口 | 用于从指定地址的存储单元读取数据。 |
简单(伪)双端口 RAM IP 核端口
其中 INJECTSBITERR 和 INJECTDBITERR 以及 SBITERR 和 DBITERR 和ECC(纠错码)相关。
端口 | 方向 | 作用 | 备注 |
DINA | INPUT | 数据写入端口 | A 是端口序号,如果还有一个 DIN 端口,就叫DINB 了,以下端口同理。 |
ADDRA | INPUT | 地址输入端口 | 用于指定要访问的 RAM 地址。 |
WEA | INPUT | 读写控制信号端口 | 高电平 1 时,写;低电平 0 时,不写。 |
ENA | INPUT | 使能端口 | 高电平 1 时,RAM 可用。 |
CLKA | INPUT | 时钟输入端口 | 用于同步 RAM 的操作。 |
ADDRB | INPUT | 地址输入端口 | 用于指定要访问的 RAM 地址。 |
ENB | INPUT | 使能端口 | 高电平 1 时,RAM 可用。 |
RSTB | INPUT | ||
REGCEB | INPUT | 寄存器使能端口 | 用于控制输出寄存器的使能。低电平 0 时,DOUTA 输出固定值;高电平 1 时,输出输出寄存器中的数据。 |
CLKB | INPUT | 时钟输入端口 | 用于同步 RAM 的操作。 |
真双端口 RAM IP 核端口
参考单端口 RAM IP 核端口。
动态 RAM
后续补上。
RAM IP核
Xilinx使用 “Block Memory Generator” IP 核来产生各种各样的 RAM 或 ROM。
基础设置
Interface Type(接口类型):
可选项 | 作用 |
Native(常规接口) | |
AXI4 | 一般用于软核开发,通过软核来控制 RAM;此接口符合 AXI 协议,用于与 AXI 总线连接 |
Memory Type(存储器类型):
可选项 |
Single Port RAM(单端口 RAM) |
Simple Dual Port RAM(简单双端口 RAM) |
True Dual Port RAM(真正双端口 RAM) |
Single Port ROM(单端口 ROM) |
Dual Port ROM(双端口 ROM) |
Generate address interface with 32 bits(生成 32 位地址接口):
默认情况下,Block Memory Generator 生成的地址接口宽度是根据存储器的深度自动计算的。例如,如果存储器的深度为 64,地址宽度为 6 位(因为 2^6=64)。选中这个选项后,地址接口的宽度将固定为 32 位,无论存储器的实际深度是多少。
Common Clock(公共时钟):这个选项用于配置存储器的时钟信号。选中:所有端口(例如读端口和写端口)共享同一个时钟信号。未选中:每个端口可以使用独立的时钟信号(适用于双端口存储器)。
ECC Options(错误校正码选项):
可选项 | 作用 |
Soft ECC(软件 ECC) | 使用软核逻辑实现 ECC 功能。 |
BuiltIn ECC(内置 ECC) | 使用 FPGA 内置的 ECC 功能。 |
No ECC(无 ECC) | 不启用任何错误校正功能。 |
Byte Write Enable:启用字节写使能。通过图示讲解:
可以看到,这里固定在地址0000来写入数据,写入24位数据 DIN,其中 WE 是字节写使能,当 WE 为011时,可以看到 FF EE DD 只有低2字节被写入(通过 RAM Contents 查看)。后续同理。需要注意的是:启用该使能后 WE 的位宽会发生变化,具体变化后的位宽取决于一次写入数据的字节数(若是16位,则是2字节,WE 的位宽变为2)。具体来说,WE 控制的字节是从低位开始计算的,如18位写入数据,[7:0]为第一字节,[8:15]为第二字节。
Algorithm Options(算法选项):
Algorithm:选择存储器的算法:
可选项 | 作用 |
Minimum Area | 优化存储器的面积。 |
Maximum Performance | 优化存储器的性能。 |
Fixed Primitives(固定原语) | 使用固定大小的原语(Primitive)来实现存储器。 |
Primitive:选择存储器的原语。例如:8kx2的意思就是每个原语具有8192个存储单元,每个存储单元的宽度为2位(换句话说,可以用于存储 8k 个 2 位的数据)。
端口设置
Memory Size(存储器大小)
可选项 | 作用 |
Write Width(写宽度) | 定义存储器的写数据位宽 |
Read Width(读宽度) | 定义存储器的读数据位宽 |
Write Depth(写深度) | 存储器的总容量 = 数据位宽 × 地址深度 |
Read Depth(读深度) |
Operating Mode(操作模式)
在做写操作的时候,读端口 DOUT 的数据如何变化。
可选项 |
Write First(先写模式) |
Read First(先读模式) |
No Change (不改变模式) |
写优先模式:输入数据同时写入内存并驱动数据输出。
读优先模式:之前存储在写地址的数据会出现在数据输出端,同时输入数据被存储在内存中。
不改变模式:输出锁存器在写操作期间保持不变。如图3-11所示,数据输出仍然是之前的读数据,并且不受同一端口写操作的影响。
Enable Port Type(使能端口类型)
可选项 | 作用 |
Use ENA Pin | 启动 EN 引脚来控制 RAM 使能 |
Always Enabled | RAM 持续使能 |
Port A Optional Output Registers(端口A可选输出寄存器)
可选项 | 作用 |
Primitives Output Register(原语输出寄存器) | |
Core Output Register(核心输出寄存器) | |
SoftECC Input Register(软ECC输入寄存器) | |
REGCEA Pin(REGCEA引脚) |
Port A Output Reset Options(端口A输出复位选项)
注意:这里的复位并非重置了 RAM 中所有地址的值。
可选项 | 作用 |
RSTA Pin(复位引脚) | 是否使用复位引脚 |
Output Reset Value(Hex)(输出复位值) | 定义复位后输出引脚输出的值 |
Reset Memory Latch(复位存储锁存器) | 确定是否在复位相应端口的嵌入式基本输出寄存器的同时,也复位内存锁存 |
Reset Priority(复位优先级) | 定时钟使能和复位的优先级关系,即相应端口的时钟使能是否优先于复位,或者复位是否优先于时钟使能 |
READ Address Change A(读地址变化A)
当读取地址变化时,输出数据会立即更新为新地址对应的数据(无额外延迟)。若启用输出寄存器,地址变化后需等待时钟沿才能看到新数据。
具体 IP 核参数使用请参照手册(Block Memory Generator v8.4 Product Guide (PG058) • 查看器 • AMD 技术信息门户网站) ,查看手册是必要的能力,这里不再对更多具体参数进行详细解释。
简单项目练手
单端口 RAM 实验任务
本节的实验任务是使用Vivado软件生成一个单端口的RAM并对其进行读写操作,然后通过仿真观察波形是否正确,最后通过在线调试工具对实验结果进行验证。如:将RAM设置的深度和宽度分别为32和8进行读写测试。
程序框图
从上述单端口 RAM 的图示中可以看到(这个图示在手册中同样存在),单端口 RAM 一共有8个引脚,但并不是每一个引脚都是我们需要的。
时序
ram_we:高电平写,低电平表示读。这里计数器的作用是方便向指定地址写入指定数据,如计数器为 1 时,向地址 1 写入数据 1.
代码
RAM_RW 模块的代码如下:
module ram_rw(
input clk,
input rst_n,
input [7:0] ram_rd_data, // RAM 读出的数据
output reg ram_en, // RAM 使能信号,高电平有效
output ram_we, // RAM 写使能 1:写;0:读
output reg [4:0] ram_addr, // RAM 读写地址
output reg [7:0] ram_wr_data // RAM 写入的数据
);
reg [5:0] rw_cnt;
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)
ram_en <= 1'b0;
else
ram_en <= 1'b1;
end
// 对 rw_cnt 进行赋值
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
rw_cnt <= 6'b0;
else if (ram_en)
rw_cnt <= rw_cnt + 6'b1;
else if (ram_en && rw_cnt == 6'd63)
rw_cnt <= 6'b0;
else if (!ram_en)
rw_cnt <= 6'b0;
else
rw_cnt <= rw_cnt;
end
// 对 RAM 地址进行赋值,变化范围是0-31
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
ram_addr <= 5'b0;
else if (ram_en && ram_addr == 5'd31)
ram_addr <= 5'b0;
else if (ram_en)
ram_addr <= ram_addr + 5'b1;
else if (!ram_en)
ram_addr <= 5'b0;
else
ram_addr <= ram_addr;
end
// 对 RAM 写数据进行赋值,变化范围是0-31
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
ram_wr_data <= 8'b0;
else if (ram_we && ram_wr_data < 8'd31)
ram_wr_data <= ram_wr_data +8'b1;
else
ram_wr_data <= 8'b0;
end
endmodule
顶层模块的代码如下:
module ip_1port_ram(
input sys_clk,
input sys_rst_n
);
wire [7:0] ram_rd_data ;
wire ram_en ;
wire ram_we ;
wire [4:0] ram_addr ;
wire [7:0] ram_wr_data ;
ram_rw u_ram_rw(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
.ram_rd_data (ram_rd_data),
.ram_en (ram_en),
.ram_we (ram_we),
.ram_addr (ram_addr),
.ram_wr_data (ram_wr_data)
);
blk_mem_gen_0 your_instance_name (
.clka (sys_clk),
.ena (ram_en),
.wea (ram_we),
.addra (ram_addr),
.dina (ram_wr_data),
.douta (ram_rd_data)
);
endmodule
双端口 RAM 实验任务
本节的实验任务是使用Vivado软件生成一个简单双端口的RAM并对其进行读写操作,然后通过仿真观察波形是否正确,最后通过在线调试工具对实验结果进行验证。如:将RAM设置的深度和宽度分别为64和8进行读写测试。
程序框图
时序
写模块时序如下:
这里加入 rd_flag 是为了保证读模块在写入操作完成后才开始读数据,以确保读出有效数据。
读模块时序如下:
代码
读模块代码如下:
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 + 1'b1;
else
ram_rd_addr <= 6'd0;
end
endmodule
写模块代码如下:
module ip_2port_ram(
input sys_clk , //系统时钟
input sys_rst_n //系统复位,低电平有效
);
//wire define
wire ram_wr_en ; //端口A使能
wire ram_wr_we ; //ram端口A写使能
wire ram_rd_en ; //端口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_0 u_ila_0 (
.clk (sys_clk ), // input wire clk
.probe0 (ram_wr_en ), // input wire [0:0] probe0
.probe1 (ram_wr_we ), // input wire [0:0] probe1
.probe2 (ram_rd_en ), // input wire [0:0] probe2
.probe3 (rd_flag ), // input wire [0:0] probe3
.probe4 (ram_wr_addr), // input wire [5:0] probe4
.probe5 (ram_wr_data), // input wire [7:0] probe5
.probe6 (ram_rd_addr), // input wire [5:0] probe6
.probe7 (ram_rd_data) // input wire [7:0] probe7
);
endmodule
顶层模块代码如下:
module ip_2port_ram(
input sys_clk , //系统时钟
input sys_rst_n //系统复位,低电平有效
);
//wire define
wire ram_wr_en ; //端口A使能
wire ram_wr_we ; //ram端口A写使能
wire ram_rd_en ; //端口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_0 u_ila_0 (
.clk (sys_clk ), // input wire clk
.probe0 (ram_wr_en ), // input wire [0:0] probe0
.probe1 (ram_wr_we ), // input wire [0:0] probe1
.probe2 (ram_rd_en ), // input wire [0:0] probe2
.probe3 (rd_flag ), // input wire [0:0] probe3
.probe4 (ram_wr_addr), // input wire [5:0] probe4
.probe5 (ram_wr_data), // input wire [7:0] probe5
.probe6 (ram_rd_addr), // input wire [5:0] probe6
.probe7 (ram_rd_data) // input wire [7:0] probe7
);
endmodule