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
相关推荐
仲南音4 小时前
Xilinx FPGA——时钟IP核MMCM、PLL
fpga开发
I'm a winner18 小时前
基于Xilinx FPGA的LVDS高速串行通信系统(三)--数据训练及握手机制【文末源码】
fpga开发
upper202020 小时前
coremark移植坑之-nostartfiles -nostdlib参数
学习·fpga开发·coremark移植·riscv设计
Eloudy1 天前
Vivado 纯命令行构建 FPGA 项目教程
fpga开发
Rambo.xia1 天前
AXI跨时钟域与数据一致性——CDC FIFO深度不够、写后读冒险、多Master仲裁、乱序返回,出了Bug根本查不出来
fpga开发·bug
upper20201 天前
FPGA部署卷积神经网络识别MINST图片
fpga开发·卷积神经网络·minst·minst识别·部署神经网络·神经网络识别图片
你是我的解忧王子2 天前
嵌入式 ADC采样 原始数据分析上层(平均值 波动率 正态分布 线性补偿 系统误差)
fpga开发·数据挖掘·数据分析
9527华安2 天前
FPGA纯verilog代码ISP图像处理培训教程,基于IMX214,提供工程源码+视频教程+FPGA开发板
图像处理·fpga开发·isp·imx214·mipi
千寻xun3 天前
二、实战篇-NVME SSD控制之ZYNQ实现(六)-读写NVME SSD硬盘数据
fpga开发·nvme·nvme ssd
千寻xun3 天前
二、实战篇-NVME SSD控制之ZYNQ实现(三)
fpga开发·nvme ssd·nvme协议