FPGA中的嵌入式块存储器RAM:从原理到实现的完整指南
文章目录
一、引言:为什么需要RAM?
在前一篇文章中,我们深入探讨了FPGA中ROM的原理与应用。然而,在实际的FPGA系统设计中,很多时候我们需要的是可读可写的存储器,这就是RAM(Random Access Memory,随机存取存储器)。无论是用于数据缓存、帧缓冲还是实时数据存储,RAM都是构建高效FPGA系统不可或缺的组成部分。
本文将从RAM的基本原理出发,详细讲解嵌入式块存储器RAM的分类、特性、配置方法以及在实际项目中的应用,特别关注如何通过Vivado工具链高效地使用RAM IP核。
二、RAM的核心特性与应用场景
1.RAM的三大核心特性
与ROM相比,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 场景二:图像帧缓冲
在视频处理系统中,RAM常被用作帧缓冲器:
// 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核的详细配置指南
- 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 );特点:
两个端口都可独立读写
需要处理读写冲突
适用于复杂的数据共享场景
- 关键配置参数详解
(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 begin dout <= mem[addr]; end 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) begin mem[addr]<= din; end else begin dout <= mem[addr]; end end - 字节写使能功能
字节写使能允许按字节粒度控制数据写入,特别适用于处理不同位宽的数据:
// 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)顶层模块设计
系统架构设计

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 begin ram_wen <=1'b0; end 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 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位/像素):
总像素数: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-1023for(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