⚡ 第14课:RAM控制器

📖 本课目标

设计并实现RAM控制器模块,管理30KB读写存储器的访问。RAM是程序运行时的数据区——变量、堆栈、缓冲区都在这里。我们将实现读写控制、地址解码和等待状态管理。

🧠 RAM控制器架构

RAM控制器结构 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ CPU侧 SRAM侧 ┌─────────┐ ┌─────────┐ │ addr │──┐ │ A[14:0] │ │ [15:0] │ │ ┌───────┐ │ │ │ │ ├──▶│地址 │──▶│ │ │ data │ │ │解码 │ │ │ │ [7:0] │◀─┤ └───────┘ │ D[7:0] │ │ │ │ │ │ │ rd/wr │──┤ ┌───────┐ │ /CS │ │ │ └──▶│控制 │──▶│ /OE │ │ │ │逻辑 │──▶│ /WE │ │ │ └───────┘ │ │ └─────────┘ └─────────┘ 地址映射: $0000 - $77FF: 30KB SRAM (用户数据区) $7800 - $7FFF: 2KB VGA缓冲 (由VGA控制器管理) 访问时序: 读: 地址有效 → rd=1 → 数据可用 (1周期) 写: 地址+数据有效 → wr=1 → 数据锁存 (1周期)

🔧 Verilog实现

// ========================================
// ram_ctrl.v - RAM控制器
// Retro8 复古电脑 存储子系统
// ✅Verilator验证通过
// ========================================

module ram_ctrl (
    input  wire          clk,
    input  wire          rst_n,
    input  wire [15:0]  addr,
    input  wire [7:0]   data_in,   // 写入数据
    input  wire          rd,        // 读使能
    input  wire          wr,        // 写使能
    output reg  [7:0]   data_out,  // 读出数据
    output wire          valid,     // 地址在RAM范围内
    output wire          ready      // 数据就绪
);

    // RAM地址范围
    localparam [15:0] RAM_BASE = 16'h0000;
    localparam [15:0] RAM_SIZE = 16'h7800;  // 30KB

    // 地址有效性
    assign valid = (addr >= RAM_BASE) && (addr < RAM_BASE + RAM_SIZE);
    assign ready = valid;  // 单周期SRAM,立即可用

    // RAM存储阵列
    reg [7:0] ram_data [0:RAM_SIZE-1];

    // 写操作
    always @(posedge clk) begin
        if (wr && valid)
            ram_data[addr - RAM_BASE] <= data_in;
    end

    // 读操作(组合逻辑)
    always @(*) begin
        if (rd && valid)
            data_out = ram_data[addr - RAM_BASE];
        else
            data_out = 8'hFF;
    end

endmodule

🔍 RAM控制器深度分析

写前读(Read-Before-Write)

我们的RAM控制器是同步写、异步读。这意味着:

在同一个地址同时读写时,读出的是旧值,新值在下一个周期才可见。这与我们在寄存器文件中讨论的行为一致。

内存保护

在简单的8位系统中,任何程序都可以读写任何内存地址。但我们可以添加简单的保护机制:

// 简单的内存保护:保护VGA缓冲区
wire vga_area = (addr >= 16'h7800) && (addr < 16'h8000);
wire io_area  = (addr >= 16'hFF00);

// VGA缓冲区只允许VGA控制器写入
wire wr_allowed = valid && !vga_area;
// 或者:允许CPU写入VGA缓冲,但通过MMIO控制

📐 RAM初始化与测试模式

// RAM上电测试模式
// 检测RAM大小和可靠性

module ram_test (
    input  wire          clk,
    input  wire          rst_n,
    input  wire          start,
    output reg  [15:0]  test_addr,
    output reg  [7:0]   test_data,
    output reg          test_wr,
    output reg          test_rd,
    output reg          pass,
    output reg          fail
);

    localparam IDLE   = 3'd0;
    localparam WRITE  = 3'd1;
    localparam READ   = 3'd2;
    localparam CHECK  = 3'd3;
    localparam DONE   = 3'd4;

    reg [2:0] state;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= IDLE; test_addr <= 16'd0;
        end else begin
            case (state)
                IDLE: if (start) state <= WRITE;
                WRITE: begin
                    test_wr <= 1'b1;
                    test_data <= test_addr[7:0];  // 写入地址低8位
                    state <= READ;
                end
                READ: begin
                    test_wr <= 1'b0;
                    test_rd <= 1'b1;
                    state <= CHECK;
                end
                CHECK: begin
                    test_rd <= 1'b0;
                    if (test_data !== test_addr[7:0])
                        fail <= 1'b1;
                    if (test_addr < 16'h77FF) begin
                        test_addr <= test_addr + 16'd1;
                        state <= WRITE;
                    end else begin
                        pass <= 1'b1;
                        state <= DONE;
                    end
                end
            endcase
        end
    end

endmodule

📝 练习

练习1:多端口RAM

CPU和VGA控制器需要同时访问VGA缓冲区。设计一个双端口RAM控制器,支持一个写端口和两个读端口。

练习2:RAM刷新模拟

如果使用DRAM而不是SRAM,需要定期刷新。设计一个简单的刷新控制器,每隔一定周期自动刷新一行。

练习3:内存分页

实现4个16KB内存页(bank),通过I/O端口选择当前活跃的页。这样可以在8位地址空间中使用超过64KB的RAM。

🏆 成就解锁

⚡ 内存管理者

达成条件:

奖励:你的CPU现在有了"工作台"——可以在RAM中创建变量、缓冲区和堆栈。程序不再是只读的,数据可以自由流动。

🔬 SRAM时序深入分析

理解SRAM的时序对设计可靠的内存控制器至关重要:

SRAM读操作时序

读操作时序(典型异步SRAM) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ┌─────┐ ┌─────┐ ┌─────┐ clk │ │ │ │ │ │ ──┘ └─────┘ └─────┘ └── addr ──┤ 有效地址 ├────────────────────── ┌──────────┐ /CS ───┤ ├──────────────────── ──┘ └────────────────── ┌──────────┐ /OE ───┤ ├──────────────────── ──┘ └────────────────── data ────┤ 无效数据 ├─────┤ 有效数据 ├──── ↑ t_AA(地址访问时间) 通常10-20ns 关键参数: t_AA: 地址到数据有效时间 t_OE: /OE到数据有效时间 t_RC: 读周期时间

SRAM写操作时序

写操作时序 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ┌─────┐ ┌─────┐ clk │ │ │ │ ──┘ └─────┘ └────── addr ──┤ 有效地址 ├────────────── ┌──────────────┐ /CS ───┤ ├────────── ──┘ └────────── ┌─────┐ /WE ──────────┤ ├──────────── ─────────┘ └──────────── data ──────────┤有效数据├────────── 关键参数: t_SA: 地址建立到/WE下降 t_SD: 数据建立到/WE上升 t_HD: /WE后数据保持时间 t_WC: 写周期时间

FPGA中的RAM实现

FPGA提供两种RAM实现方式:

综合工具会根据RAM大小自动选择实现方式。我们的30KB RAM会使用块RAM。