跳到主要内容FPGA 嵌入式块存储器 RAM:原理与实现指南 | 极客日志编程语言
FPGA 嵌入式块存储器 RAM:原理与实现指南
FPGA 嵌入式块存储器(RAM)是构建高效系统的关键组件。介绍 RAM 的核心特性(随机存取、非破坏性读取、覆盖写入)及典型应用场景(数据缓冲、图像帧缓冲)。详细对比 SRAM 与 DRAM,并深入讲解 Vivado 中 Block Memory Generator IP 核的配置,包括单端口、简单双端口和真双端口模式,以及写优先、读优先等关键参数。通过基于 RAM 的图像显示系统实战案例,分析资源需求与代码实现,最后提供仿真验证测试平台设计,涵盖顺序读写、随机访问及冲突测试。
编程诗人7 浏览 在实际的 FPGA 系统设计中,很多时候我们需要的是可读可写的存储器,这就是 RAM(Random Access Memory,随机存取存储器)。无论是用于数据缓存、帧缓冲还是实时数据存储,RAM 都是构建高效 FPGA 系统不可或缺的组成部分。
本文从 RAM 的基本原理出发,详细讲解嵌入式块存储器 RAM 的分类、特性、配置方法以及在实际项目中的应用,特别关注如何通过 Vivado 工具链高效地使用 RAM IP 核。
二、RAM 的核心特性与应用场景
1. RAM 的三大核心特性
- 随机存取:支持对任意地址的读写操作,访问顺序不受限制
- 非破坏性读取:读取操作不会清除存储内容,数据可多次重复读取
- 覆盖写入:新数据写入会直接覆盖旧数据,支持动态更新
2. 典型应用场景分析
场景一:数据速率匹配缓冲
这是 RAM 最常见的应用场景之一。考虑以下实际问题:某 ADC 以 1μs 的间隔产生 12 位数据(速率:1000 个/ms),而串口以 115200 波特率发送数据(每 6 位数据需要 69.4μs)。数据产生速率远快于发送速率。
// 使用 RAM 作为数据缓冲器
module data_buffer(
input clk,
input rst_n,
input [11:0] adc_data,
input adc_data_valid,
output reg [5:0] uart_data,
output reg uart_data_valid
);
// 双端口 RAM:端口 A 用于写入 ADC 数据,端口 B 用于读取串口数据
// 深度:1000,宽度:12 位(写入)和 6 位(读取)
// 写入逻辑
always @(posedge clk) begin
if(adc_data_valid) begin
ram_wea <= 1'b1;
ram_addra <= write_addr;
ram_dina <= adc_data;
write_addr <= write_addr + 1;
end
end
// 读取逻辑
always @(posedge clk) begin
if(uart_ready) begin
ram_addrb <= read_addr;
uart_data <= ram_doutb[5:0]; // 只取低 6 位
read_addr <= read_addr + 1;
end
end
endmodule
// TFT 显示屏图像缓冲
module tft_frame_buffer(
input clk_pixel, // 像素时钟
input clk_system, // 系统时钟
input [15:0] pixel_in,// RGB565 像素数据
input pixel_valid,
output [15:0] pixel_out
);
// 双端口 RAM 配置
// 端口 A:串口写入(系统时钟域)
// 端口 B:TFT 读取(像素时钟域)
// 存储需求计算:800×480 分辨率,16 位/像素 → 800×480×16 = 6,144,000 bits
// 需要约 170 个 36Kb 的 BRAM 块
endmodule
三、RAM 的类型:SRAM 与 DRAM 详解
// DDR3 控制器接口示例
module ddr3_controller(
input clk,
input rst_n,
// 用户接口
input [31:0] app_addr,
input [255:0] app_wdf_data,
input app_wdf_wren,
output [255:0] app_rd_data,
output app_rd_data_valid,
// DDR3 物理接口
output [13:0] ddr3_addr,
output [2:0] ddr3_ba,
output ddr3_cas_n,
output ddr3_ras_n,
output ddr3_we_n
);
// 使用 Xilinx MIG IP 核实现
endmodule
四、Vivado 中 RAM IP 核的详细配置指南
1. RAM IP 核类型选择
在 Vivado 的 IP Catalog 中搜索"Block Memory Generator",可以看到多种 RAM 类型:
(1)单端口 RAM
// 接口信号
module single_port_ram(
input clka, // 时钟
input ena, // 使能
input wea, // 写使能(1=写,0=读)
input [9:0] addra, // 地址(10 位,深度 1024)
input [15:0] dina, // 数据输入
output [15:0] douta // 数据输出
);
特点:所有操作共享同一组端口,读写不能同时进行,适用于简单的数据存储场景。
(2)简单双端口 RAM
// 接口信号
module simple_dual_port_ram(
// 端口 A:只写
input clka,
input ena,
input wea, // 始终为 1(只写)
input [9:0] addra,
input [15:0] dina,
// 端口 B:只读
input clkb,
input enb,
input [9:0] addrb,
output [15:0] doutb
);
特点:端口 A 专用于写,端口 B 专用于读,可同时进行读写操作,适用于生产者 - 消费者模型。
(3)真双端口 RAM
// 接口信号
module true_dual_port_ram(
// 端口 A:可读写
input clka,
input ena,
input wea,
input [9:0] addra,
input [15:0] dina,
output [15:0] douta,
// 端口 B:可读写
input clkb,
input enb,
input web,
input [9:0] addrb,
input [15:0] dinb,
output [15:0] doutb
);
特点:两个端口都可独立读写,需要处理读写冲突,适用于复杂的数据共享场景。
2. 关键配置参数详解
(1)存储容量计算
总存储容量 = 数据位宽 × 深度(单位:bits)
示例:数据位宽 16 bits,深度 1024,总容量 16 × 1024 = 16,384 bits = 16 Kb。
(2)BRAM 资源使用
Xilinx 7 系列 FPGA 的 BRAM 配置:每个 BRAM 块 36 Kb,可配置为 1 个 36Kb RAM 或 2 个独立的 18Kb RAM。
常见配置模式:32K × 1, 16K × 2, 8K × 4, 4K × 9, 2K × 18, 1K × 36, 512 × 72。
(3)工作模式选择
在"Port A Options"或"Port B Options"中选择:
- Write First Mode(写优先模式):当读写同一地址时,写入的数据会立即出现在输出。
always @(posedge clk)
begin
if(wea)
begin
mem[addr] <= din;
dout <= din; // 写优先
end
else
dout <= mem[addr];
end
- Read First Mode(读优先模式):当读写同一地址时,先读取旧数据,再写入新数据。
always @(posedge clk)
begin
dout <= mem[addr]; // 先读
if(wea)
begin
mem[addr] <= din; // 后写
end
end
- No Change Mode(无变化模式):当读写同一地址时,输出保持不变。
always @(posedge clk)
begin
if(wea)
mem[addr] <= din;
else
dout <= mem[addr];
end
3. 字节写使能功能
字节写使能允许按字节粒度控制数据写入,特别适用于处理不同位宽的数据:
// 24 位数据,按字节写入控制
module byte_write_ram(
input clk,
input ena,
input [2:0] wea, // 3 位写使能,控制 3 个字节
input [9:0] addra,
input [23:0] dina, // 24 位输入数据
output [23:0] douta // 24 位输出数据
);
// wea[2:0] 控制:
// wea = 3'b111: 写入全部 3 个字节
// wea = 3'b011: 只写入低 2 个字节(dina[15:0])
// wea = 3'b001: 只写入最低字节(dina[7:0])
// wea = 3'b000: 不写入任何字节
endmodule
五、实战案例:基于 RAM 的图像显示系统
1. 详细实现代码
(1)顶层模块设计
系统架构设计包含时钟域划分、串口接收模块、图像数据写入控制、双端口 RAM 实例及 TFT 显示控制器。
module image_display_system(
input clk_100m, // 100MHz 系统时钟
input clk_pixel, // 像素时钟(25MHz for 800x480@60Hz)
input rst_n, // 复位
// 串口接口
input uart_rx,
output uart_tx,
// TFT 显示接口
output [15:0] tft_data,
output tft_hsync,
output tft_vsync,
output tft_de
);
wire clk_sys = clk_100m;
wire clk_disp = clk_pixel;
// 串口接收模块
wire [7:0] uart_rx_data;
wire uart_rx_valid;
uart_receiver u_uart_rx(.clk(clk_sys), .rst_n(rst_n), .rx(uart_rx), .data(uart_rx_data), .valid(uart_rx_valid));
// 图像数据写入控制
wire [15:0] ram_wdata;
wire [16:0] ram_waddr; // 800*480=384000,需要 19 位地址
wire ram_wen;
image_writer u_writer(.clk(clk_sys), .rst_n(rst_n), .uart_data(uart_rx_data), .uart_valid(uart_rx_valid), .ram_wdata(ram_wdata), .ram_waddr(ram_waddr), .ram_wen(ram_wen));
// 双端口 RAM 实例
wire [15:0] ram_rdata;
wire [16:0] ram_raddr;
blk_mem_gen_0 u_ram(
// 端口 A:写端口(串口数据写入)
.clka(clk_sys), .ena(1'b1), .wea(ram_wen), .addra(ram_waddr[16:0]), .dina(ram_wdata), .douta(),
// 端口 A 不读取
// 端口 B:读端口(TFT 显示读取)
.clkb(clk_disp), .enb(1'b1), .addrb(ram_raddr[16:0]), .doutb(ram_rdata)
);
// TFT 显示控制器
tft_controller u_tft(.clk(clk_disp), .rst_n(rst_n), .pixel_data(ram_rdata), .pixel_addr(ram_raddr), .tft_data(tft_data), .tft_hsync(tft_hsync), .tft_vsync(tft_vsync), .tft_de(tft_de));
endmodule
(2)图像数据写入模块
module image_writer(
input clk,
input rst_n,
input [7:0] uart_data,
input uart_valid,
output reg [15:0] ram_wdata,
output reg [16:0] ram_waddr,
output reg ram_wen
);
reg [1:0] byte_cnt; // 字节计数器(0-1,两个字节组成一个 16 位像素)
reg [7:0] pixel_low; // 像素低字节
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
byte_cnt <= 2'd0;
pixel_low <= 8'd0;
ram_waddr <= 17'd0;
ram_wen <= 1'b0;
end
elseif(uart_valid)
begin
case(byte_cnt)
2'd0: begin
pixel_low <= uart_data; // 保存低字节
byte_cnt <= 2'd1;
ram_wen <= 1'b0;
end
2'd1: begin
ram_wdata <= {uart_data, pixel_low}; // 拼接高字节和低字节
ram_waddr <= ram_waddr + 1'b1;
ram_wen <= 1'b1;
byte_cnt <= 2'd0;
end
endcase
end
else
ram_wen <= 1'b0;
end
endmodule
(3)TFT 显示控制器
module tft_controller(
input clk, // 像素时钟
input rst_n,
input [15:0] pixel_data, // 从 RAM 读取的像素数据
output reg [16:0] pixel_addr, // 读取地址
output reg [15:0] tft_data,
output reg tft_hsync,
output reg tft_vsync,
output reg tft_de
);
// 显示时序参数(800x480@60Hz)
parameter H_ACTIVE = 800; // 水平有效像素
parameter H_FP = 40; // 水平前沿
parameter H_SYNC = 128; // 水平同步脉冲
parameter H_BP = 88; // 水平后沿
parameter H_TOTAL = H_ACTIVE + H_FP + H_SYNC + H_BP;
parameter V_ACTIVE = 480; // 垂直有效行
parameter V_FP = 13; // 垂直前沿
parameter V_SYNC = 2; // 垂直同步脉冲
parameter V_BP = 33; // 垂直后沿
parameter V_TOTAL = V_ACTIVE + V_FP + V_SYNC + V_BP;
reg [10:0] h_cnt; // 水平计数器(0-1047)
reg [9:0] v_cnt; // 垂直计数器(0-527)
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
h_cnt <= 11'd0;
v_cnt <= 10'd0;
pixel_addr <= 17'd0;
tft_hsync <= 1'b0;
tft_vsync <= 1'b0;
tft_de <= 1'b0;
tft_data <= 16'd0;
end
else
begin
// 水平计数器递增
if(h_cnt == H_TOTAL - 1)
begin
h_cnt <= 11'd0;
// 垂直计数器递增
if(v_cnt == V_TOTAL - 1)
begin
v_cnt <= 10'd0;
pixel_addr <= 17'd0; // 帧结束,复位地址
end
else
v_cnt <= v_cnt + 1'b1;
end
else
h_cnt <= h_cnt + 1'b1;
// 生成同步信号
tft_hsync <= (h_cnt >= H_ACTIVE + H_FP) && (h_cnt < H_ACTIVE + H_FP + H_SYNC);
tft_vsync <= (v_cnt >= V_ACTIVE + V_FP) && (v_cnt < V_ACTIVE + V_FP + V_SYNC);
// 生成数据使能信号
tft_de <= (h_cnt < H_ACTIVE) && (v_cnt < V_ACTIVE);
// 在有效显示区域内读取像素数据
if(tft_de)
begin
tft_data <= pixel_data;
pixel_addr <= pixel_addr + 1'b1;
end
else
tft_data <= 16'd0;
end
end
endmodule
2. RAM 资源需求分析
对于 800×480 分辨率的 TFT 显示,使用 RGB565 格式(16 位/像素):
- 总像素数:800 × 480 = 384,000 像素
- 每像素数据:16 bits
- 总存储需求:384,000 × 16 = 6,144,000 bits
- Xilinx XC7Z015 芯片资源:
- 每个 BRAM 块:36 Kb
- 总 BRAM 数量:95 个
- 总 BRAM 容量:95 × 36 Kb = 3,420 Kb
- 结论:无法存储完整一帧图像
六、仿真验证
测试平台设计
module ram_tb;
reg clk;
reg rst_n;
// 时钟生成(100MHz)
always #5 clk =~clk;
// 测试序列
initial
begin
// 初始化
clk = 0;
rst_n = 0;
// 复位释放
#100 rst_n = 1;
// 测试 1:顺序写入然后读取
test_sequential();
// 测试 2:随机地址访问
test_random();
// 测试 3:读写冲突测试
test_collision();
$finish;
end
task test_sequential;
integer i;
begin
$display("=== 顺序读写测试开始 ===");
// 写入 0-1023
for(i = 0; i < 1024; i = i + 1)
begin
@(posedge clk);
ram_wen = 1;
ram_addr = i;
ram_wdata = i * 2;
end
@(posedge clk);
ram_wen = 0;
// 验证读取
for(i = 0; i < 1024; i = i + 1)
begin
@(posedge clk);
ram_addr = i;
@(posedge clk);
if(ram_rdata !== i * 2)
begin
$display("错误:地址 %d,期望值 %d,实际值 %d", i, i*2, ram_rdata);
end
end
$display("=== 顺序读写测试完成 ===");
end
endtask
task test_random;
integer i, addr;
begin
$display("=== 随机访问测试开始 ===");
for(i = 0; i < 100; i = i + 1)
begin
addr = $random % 1024;
// 写入随机数据
@(posedge clk);
ram_wen = 1;
ram_addr = addr;
ram_wdata = $random;
test_data[addr] = ram_wdata;
// 等待写入完成
@(posedge clk);
ram_wen = 0;
// 延迟 2 个周期后读取验证
repeat(2) @(posedge clk);
ram_addr = addr;
@(posedge clk);
if(ram_rdata !== test_data[addr])
begin
$display("随机测试错误:地址 %d", addr);
end
end
$display("=== 随机访问测试完成 ===");
end
endtask
endmodule
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online