💾 第13课:ROM设计

📖 本课目标

设计并实现ROM(只读存储器)模块,用于存储固化程序——监控程序、BASIC解释器和系统引导代码。ROM是CPU上电后第一个访问的存储器,我们的CPU复位后从$8000开始执行,这正是ROM的起始地址。

🧠 ROM在8位电脑中的角色

在经典的8位电脑中,ROM扮演着至关重要的角色。Apple II的Integer BASIC在ROM中,Commodore 64的KERNAL在ROM中,NES的游戏卡带就是ROM。ROM中的程序上电即可运行,无需加载。

Retro8的ROM布局 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ $8000 ┌──────────────────────┐ │ 系统引导代码 │ 32字节 │ (初始化+跳转监控) │ $8020 ├──────────────────────┤ │ 监控程序 │ ~2KB │ (命令行Shell) │ $8800 ├──────────────────────┤ │ BASIC解释器 │ ~20KB │ (词法分析+执行引擎) │ │ │ $D800 ├──────────────────────┤ │ 子程序库 │ ~2KB │ (数学/字符串/IO) │ $E000 ├──────────────────────┤ │ 中断向量表 │ 6字节 │ $E000: NMI向量 │ │ $E002: BRK向量 │ │ $E004: IRQ向量 │ $E006 ├──────────────────────┤ │ 字符点阵ROM │ 2KB │ 256字符×8字节/字符 │ $E800 ├──────────────────────┤ │ 保留空间 │ $F9FF └──────────────────────┘

🔧 Verilog实现

// ========================================
// rom.v - 只读存储器模块
// Retro8 复古电脑 存储子系统
// ✅Verilator验证通过
// ========================================

module rom (
    input  wire [15:0]  addr,
    input  wire          rd,       // 读使能
    output reg  [7:0]   data,     // 读出数据
    output wire          valid     // 地址有效标志
);

    // ROM地址范围: $8000 - $F9FF (30KB)
    localparam [15:0] ROM_BASE = 16'h8000;
    localparam [15:0] ROM_SIZE = 16'h7A00;  // 30720字节

    // 地址有效性检查
    assign valid = (addr >= ROM_BASE) && (addr < ROM_BASE + ROM_SIZE);

    // ROM数据存储(初始化为简单引导程序)
    reg [7:0] rom_data [0:ROM_SIZE-1];

    // 初始化ROM内容
    initial begin
        // 默认填充NOP(0x00)
        integer i;
        for (i = 0; i < ROM_SIZE; i = i + 1)
            rom_data[i] = 8'h00;  // NOP

        // 引导代码 ($8000)
        rom_data[16'h0000] = 8'h84;  // SEI (关中断)
        rom_data[16'h0001] = 8'h04;  // LDI R1, #0x7F
        rom_data[16'h0002] = 8'h7F;  // 操作数 0x7F
        rom_data[16'h0003] = 8'h03;  // STA R1, $FF0D (系统控制)
        rom_data[16'h0004] = 8'h0D;  // 端口地址低
        rom_data[16'h0005] = 8'hFF;  // 端口地址高
        rom_data[16'h0006] = 8'h88;  // CLI (开中断)

        // 跳转到监控程序 ($8020)
        rom_data[16'h0007] = 8'h60;  // JMP $8020
        rom_data[16'h0008] = 8'h20;  // 地址低
        rom_data[16'h0009] = 8'h80;  // 地址高

        // 监控程序入口 ($8020)
        rom_data[16'h0020] = 8'hA8;  // HLT (简单测试)
    end

    // 同步读出
    always @(*) begin
        if (rd && valid)
            data = rom_data[addr - ROM_BASE];
        else
            data = 8'hFF;  // 未选中时输出0xFF
    end

endmodule

🔍 ROM设计要点

同步ROM vs 异步ROM

我们使用的是异步ROM(组合逻辑读出),地址变化后数据立即输出。在FPGA实现中,这会被映射为分布式ROM或块ROM。如果需要更高的时钟频率,可以改用同步ROM(在时钟沿锁存输出):

// 同步ROM(FPGA优化)
always @(posedge clk) begin
    if (rd && valid)
        data <= rom_data[addr - ROM_BASE];
end

同步ROM增加1个周期延迟,但可以跑到更高的时钟频率。在Verilator仿真中两者无差异。

ROM初始化方法

Verilog的initial块用于仿真初始化。在FPGA综合中,ROM内容通过以下方式初始化:

我们的Verilator仿真使用initial块就足够了。

📐 ROM中的中断向量表

// 中断向量表 ($E000-$E005)
// 每个向量是2字节地址

// $E000-$E001: NMI向量(不可屏蔽中断)
rom_data[16'h6000] = 8'h00;  // NMI处理程序低地址
rom_data[16'h6001] = 8'hE1;  // NMI处理程序高地址 → $E100

// $E002-$E003: BRK向量(软件中断)
rom_data[16'h6002] = 8'h00;  // → $E200
rom_data[16'h6003] = 8'hE2;

// $E004-$E005: IRQ向量(硬件中断)
rom_data[16'h6004] = 8'h00;  // → $E300
rom_data[16'h6005] = 8'hE3;

🧪 仿真验证

// tb_rom.cpp - ROM测试台
#include "Vrom.h"
#include "verilated.h"
#include <cstdio>

int main(int argc, char** argv) {
    Verilated::commandArgs(argc, argv);
    Vrom* rom = new Vrom;
    int errors = 0;

    // 测试地址有效性
    rom->addr = 0x8000; rom->rd = 1; rom->eval();
    if (!rom->valid) { printf("FAIL: $8000 not valid\n"); errors++; }
    else printf("PASS: $8000 valid\n");

    rom->addr = 0x7FFF; rom->rd = 1; rom->eval();
    if (rom->valid) { printf("FAIL: $7FFF should not be valid\n"); errors++; }
    else printf("PASS: $7FFF not valid\n");

    // 测试引导代码
    rom->addr = 0x8000; rom->rd = 1; rom->eval();
    if (rom->data != 0x84) { printf("FAIL: boot[0]=%02X\n", rom->data); errors++; }
    else printf("PASS: boot[0]=0x84 (SEI)\n");

    rom->addr = 0x8001; rom->eval();
    if (rom->data != 0x04) { printf("FAIL: boot[1]=%02X\n", rom->data); errors++; }
    else printf("PASS: boot[1]=0x04 (LDI)\n");

    // 测试未读时输出0xFF
    rom->rd = 0; rom->eval();
    if (rom->data != 0xFF) { printf("FAIL: not read\n"); errors++; }
    else printf("PASS: not read = 0xFF\n");

    printf("\nErrors: %d\n", errors);
    if (errors == 0) printf("ALL TESTS PASSED! ✅\n");
    rom->final(); delete rom;
    return errors ? 1 : 0;
}

📝 练习

练习1:ROM内容生成器

写一个Python脚本,从汇编代码生成ROM初始化数据(hex格式)。脚本应该支持标签解析和指令编码。

练习2:ROM断点检测

添加一个功能:当CPU读取特定地址时产生断点信号。这在调试时非常有用——可以设断点在某个函数的入口地址。

练习3:双映射ROM

修改ROM模块,使其同时映射到$0000-$77FF和$8000-$F9FF。这样上电时从$0000或$8000都能执行,方便不同的启动模式。

🏆 成就解锁

💾 ROM守护者

达成条件:

奖励:你的CPU有了"记忆"——上电后就知道该做什么!ROM是计算机灵魂的居所。

🔬 ROM的物理实现

在真实的8位电脑中,ROM有多种物理实现:

ROM技术对比

类型写入方式擦除方式用途
Mask ROM工厂光刻不可擦除大批量固定程序
PROM一次性编程不可擦除小批量定制
EPROM电编程紫外线擦除开发阶段
EEPROM电编程电擦除在系统可编程
Flash电编程电擦除(扇区)现代固件存储

在我们的Verilator仿真中,ROM就是一个简单的reg数组。但在FPGA实现中,ROM会被映射为块RAM或分布式ROM,内容通过.bit文件初始化。

ROM内容的安全保护

商业8位电脑的ROM包含专有软件,需要防止非法复制。常见的保护方法:

// ROM校验和验证
module rom_checksum (
    input wire clk, input wire rst_n,
    input wire start,
    output reg valid,
    output reg error
);
    reg [15:0] addr;
    reg [7:0]  checksum;
    reg        running;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            running <= 0; addr <= 0; checksum <= 0;
        end else if (start && !running) begin
            running <= 1; addr <= 0; checksum <= 0;
        end else if (running) begin
            checksum <= checksum ^ rom_data[addr];
            addr <= addr + 1;
            if (addr == ROM_SIZE - 1) begin
                running <= 0;
                valid <= (checksum == 0);  // XOR校验和应该为0
            end
        end
    end
endmodule