文章目录
-
- 一、引言:为什么需要RAM?
- 二、RAM的核心特性与应用场景
- 三、RAM的类型:SRAM与DRAM详解
- [四、Vivado中RAM IP核的详细配置指南](#四、Vivado中RAM IP核的详细配置指南)
- 五、实战案例:基于RAM的图像显示系统
- 六、仿真验证
一、引言:为什么需要RAM?
在前一篇文章中,我们深入探讨了FPGA中ROM的原理与应用。然而,在实际的FPGA系统设计中,很多时候我们需要的是可读可写的存储器,这就是RAM(Random Access Memory,随机存取存储器)。无论是用于数据缓存、帧缓冲还是实时数据存储,RAM都是构建高效FPGA系统不可或缺的组成部分。
本文将从RAM的基本原理出发,详细讲解嵌入式块存储器RAM的分类、特性、配置方法以及在实际项目中的应用,特别关注如何通过Vivado工具链高效地使用RAM IP核。
二、RAM的核心特性与应用场景
1.RAM的三大核心特性
与ROM相比,RAM具有以下显著特性:
- 随机存取:支持对任意地址的读写操作,访问顺序不受限制
- 非破坏性读取:读取操作不会清除存储内容,数据可多次重复读取
- 覆盖写入:新数据写入会直接覆盖旧数据,支持动态更新
2.典型应用场景分析
场景一:数据速率匹配缓冲
这是RAM最常见的应用场景之一。考虑以下实际问题:
bash
某ADC以1μs的间隔产生12位数据(速率:1000个/ms),而串口以115200波特率发送数据(每6位数据需要69.4μs)。数据产生速率远快于发送速率。
解决方案:
c
// 使用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
场景二:图像帧缓冲
在视频处理系统中,RAM常被用作帧缓冲器:
c
// 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详解

c
// 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核的详细配置指南
- RAM IP核类型选择
在Vivado的IP Catalog中搜索"Block Memory Generator",可以看到多种RAM类型:
(1)单端口RAM
c
// 接口信号
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
c
// 接口信号
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
c
// 接口信号
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
);
特点:
两个端口都可独立读写
需要处理读写冲突
适用于复杂的数据共享场景
- 关键配置参数详解
(1)存储容量计算
c
总存储容量 = 数据位宽 × 深度
单位: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(写优先模式):
c
// 当读写同一地址时,写入的数据会立即出现在输出
always @(posedge clk) begin
if (wea) begin
mem[addr] <= din;
dout <= din; // 写优先
end else begin
dout <= mem[addr];
end
end
Read First Mode(读优先模式):
c
// 当读写同一地址时,先读取旧数据,再写入新数据
always @(posedge clk) begin
dout <= mem[addr]; // 先读
if (wea) begin
mem[addr] <= din; // 后写
end
end
No Change Mode(无变化模式):
c
// 当读写同一地址时,输出保持不变
always @(posedge clk) begin
if (wea) begin
mem[addr] <= din;
end else begin
dout <= mem[addr];
end
end
- 字节写使能功能
字节写使能允许按字节粒度控制数据写入,特别适用于处理不同位宽的数据:
c
// 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: 不写入任何字节
五、实战案例:基于RAM的图像显示系统
- 系统架构设计

- 详细实现代码
(1)顶层模块设计
c
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)图像数据写入模块
c
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 else if (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 begin
ram_wen <= 1'b0;
end
end
endmodule
(3)TFT显示控制器
c
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 begin
v_cnt <= v_cnt + 1'b1;
end
end else begin
h_cnt <= h_cnt + 1'b1;
end
// 生成同步信号
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 begin
tft_data <= 16'd0;
end
end
end
endmodule
- RAM资源需求分析
对于800×480分辨率的TFT显示,使用RGB565格式(16位/像素):
c
总像素数: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
结论:无法存储完整一帧图像
六、仿真验证
测试平台设计
c
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