FPGA中的嵌入式块存储器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核的详细配置指南

  1. 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. 关键配置参数详解
    (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
  1. 字节写使能功能
    字节写使能允许按字节粒度控制数据写入,特别适用于处理不同位宽的数据:
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. 系统架构设计
  2. 详细实现代码
    (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
  1. 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
相关推荐
Flamingˢ4 小时前
FPGA中的存储器模型:从IP核到ROM的深度解析与应用实例
网络协议·tcp/ip·fpga开发
FPGA小c鸡1 天前
【FPGA深度学习加速】RNN与LSTM硬件加速完全指南:从算法原理到硬件实现
rnn·深度学习·fpga开发
Aaron15881 天前
通信灵敏度计算与雷达灵敏度计算对比分析
网络·人工智能·深度学习·算法·fpga开发·信息与通信·信号处理
博览鸿蒙1 天前
IC 和 FPGA,到底区别在哪?
fpga开发
思尔芯S2C1 天前
FPGA原型验证实战:如何应对外设连接问题
fpga开发·risc-v·soc设计·prototyping·原型验证
Flamingˢ1 天前
FPGA实战:VGA成像原理、时序详解与Verilog控制器设计与验证
fpga开发
FPGA_小田老师1 天前
xilinx原语:OSERDES2(并串转换器)原语详解
fpga开发·lvds·xilinx原语·oserdese·并串转换
Blossom.1181 天前
从数字大脑到物理实体:具身智能时代的大模型微调与部署实战
人工智能·python·深度学习·fpga开发·自然语言处理·矩阵·django
漂洋过海的鱼儿2 天前
HLS (High-Level Synthesis)对比PS运行速度
fpga开发