设计并实现ROM(只读存储器)模块,用于存储固化程序——监控程序、BASIC解释器和系统引导代码。ROM是CPU上电后第一个访问的存储器,我们的CPU复位后从$8000开始执行,这正是ROM的起始地址。
在经典的8位电脑中,ROM扮演着至关重要的角色。Apple II的Integer BASIC在ROM中,Commodore 64的KERNAL在ROM中,NES的游戏卡带就是ROM。ROM中的程序上电即可运行,无需加载。
// ========================================
// 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(组合逻辑读出),地址变化后数据立即输出。在FPGA实现中,这会被映射为分布式ROM或块ROM。如果需要更高的时钟频率,可以改用同步ROM(在时钟沿锁存输出):
// 同步ROM(FPGA优化)
always @(posedge clk) begin
if (rd && valid)
data <= rom_data[addr - ROM_BASE];
end
同步ROM增加1个周期延迟,但可以跑到更高的时钟频率。在Verilator仿真中两者无差异。
Verilog的initial块用于仿真初始化。在FPGA综合中,ROM内容通过以下方式初始化:
$readmemh("rom.hex", rom_data);我们的Verilator仿真使用initial块就足够了。
// 中断向量表 ($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;
}
写一个Python脚本,从汇编代码生成ROM初始化数据(hex格式)。脚本应该支持标签解析和指令编码。
添加一个功能:当CPU读取特定地址时产生断点信号。这在调试时非常有用——可以设断点在某个函数的入口地址。
修改ROM模块,使其同时映射到$0000-$77FF和$8000-$F9FF。这样上电时从$0000或$8000都能执行,方便不同的启动模式。
达成条件:
奖励:你的CPU有了"记忆"——上电后就知道该做什么!ROM是计算机灵魂的居所。
在真实的8位电脑中,ROM有多种物理实现:
| 类型 | 写入方式 | 擦除方式 | 用途 |
|---|---|---|---|
| Mask ROM | 工厂光刻 | 不可擦除 | 大批量固定程序 |
| PROM | 一次性编程 | 不可擦除 | 小批量定制 |
| EPROM | 电编程 | 紫外线擦除 | 开发阶段 |
| EEPROM | 电编程 | 电擦除 | 在系统可编程 |
| Flash | 电编程 | 电擦除(扇区) | 现代固件存储 |
在我们的Verilator仿真中,ROM就是一个简单的reg数组。但在FPGA实现中,ROM会被映射为块RAM或分布式ROM,内容通过.bit文件初始化。
商业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