👒

4.nes 模拟器模拟总线

notion imagenotion image
CPU 使用三种总线访问内存(包括内存映射空间):
  • 地址总线承载所需位置的地址
  • 控制总线通知它是读还是写访问
  • 数据总线承载正在读取或写入的数据字节
总线本身不是设备;它是平台组件之间的连线。因此,我们不需要将它作为一个独立的模块来实现,因为 Rust 允许我们直接“连接”组件。然而,它是一个方便的抽象,我们可以卸下相当多的责任来保持 CPU 代码的清洁。
notion imagenotion image
在我们当前的代码中,CPU 可以直接访问 RAM 空间,并且忽略了内存映射区域。
通过引入 Bus 模块,我们可以有一个地方用于:
  • 设备内通信:
    • 数据读/写
    • 将硬件中断路由到 CPU(稍后会详细介绍)
  • 处理内存映射
  • 协调 PPU 和 CPU 时钟周期(稍后会详细介绍)
好消息是我们不需要编写数据、控制和地址总线的全面仿真。因为它不是硬件芯片,所以没有逻辑期望来自 BUS 的任何特定行为。所以我们可以只编写协调和信号路由。
现在,我们可以实现它的基本框架:
  • 访问 CPU RAM
  • 镜像
镜像是 NES 试图保持尽可能便宜的副作用。它可以看作是一个地址空间被映射到另一个地址空间。
例如,在 CPU 内存映射 RAM 地址空间[0x000 .. 0x0800] (2 KiB) 被镜像 3 次:
  • [0x800 .. 0x1000]
  • [0x1000 .. 0x1800]
  • [0x1800 .. 0x2000]
这意味着在读取或写入时访问 0x0000 或 0x0800 或 0x1000 或 0x1800 的内存地址没有区别。
镜像的原因是 CPU RAM 只有 2 KiB 的 ram 空间,并且只有 11 位足以寻址 RAM 空间。自然地,NES 主板只有 11 个从 CPU 到 RAM 的寻址轨道。
notion imagenotion image
然而,CPU 有[0x0000 - 0x2000]为 RAM 空间保留的寻址空间 - 这是 13 位。因此,在访问 RAM 时,最高 2 位无效。换一种说法,当 CPU 请求地址0b0001_1111_1111_1111(13 位)时,RAM 芯片将通过地址总线仅接收0b111_1111_1111(11 位)。
因此,尽管镜像看起来很浪费,但它是布线的副作用,在真正的硬件上它没有任何成本。另一方面,模拟器必须做额外的工作才能提供相同的行为。
长话短说,如果 BUS 收到[0x0000 ... 0x2000]范围内的请求,则需要将最高 2 位清零
类似地,地址空间[0x2008 .. 0x4000]反映了 PPU 寄存器[0x2000 .. 0x2008]的内存映射这些是 BUS 将负责的仅有的两个镜像。让我们立即对其进行编码,即使我们还没有 PPU 的任何东西。
因此,让我们介绍一个新模块 Bus,它可以直接访问 RAM。
pub struct Bus { cpu_vram: [u8; 2048] } impl Bus { pub fn new() -> Self{ Bus { cpu_vram: [0; 2048] } } }
总线还将提供读/写访问:
const RAM: u16 = 0x0000; const RAM_MIRRORS_END: u16 = 0x1FFF; const PPU_REGISTERS: u16 = 0x2000; const PPU_REGISTERS_MIRRORS_END: u16 = 0x3FFF; impl Mem for Bus { fn mem_read(&self, addr: u16) -> u8 { match addr { RAM ..= RAM_MIRRORS_END => {let mirror_down_addr = addr & 0b00000111_11111111; self.cpu_vram[mirror_down_addr as usize] } PPU_REGISTERS ..= PPU_REGISTERS_MIRRORS_END => {let _mirror_down_addr = addr & 0b00100000_00000111; todo!("PPU is not supported yet") } _ => {println!("Ignoring mem access at {}", addr); 0 } } } fn mem_write(&mut self, addr: u16, data: u8) { match addr { RAM ..= RAM_MIRRORS_END => {let mirror_down_addr = addr & 0b11111111111; self.cpu_vram[mirror_down_addr as usize] = data; } PPU_REGISTERS ..= PPU_REGISTERS_MIRRORS_END => {let _mirror_down_addr = addr & 0b00100000_00000111; todo!("PPU is not supported yet"); } _ => {println!("Ignoring mem write-access at {}", addr); } } } }
最后一步是将 CPU 对 RAM 的直接访问替换为通过 BUS 访问
pub struct CPU { pub register_a: u8, pub register_x: u8, pub register_y: u8, pub status: CpuFlags, pub program_counter: u16, pub stack_pointer: u8, pub bus: Bus, } impl Mem for CPU { fn mem_read(&self, addr: u16) -> u8 { self.bus.mem_read(addr) } fn mem_write(&mut self, addr: u16, data: u8) { self.bus.mem_write(addr, data) } fn mem_read_u16(&self, pos: u16) -> u16 { self.bus.mem_read_u16(pos) } fn mem_write_u16(&mut self, pos: u16, data: u16) { self.bus.mem_write_u16(pos, data) } } impl CPU { pub fn new() -> Self { CPU { register_a: 0, register_x: 0, register_y: 0, stack_pointer: STACK_RESET, program_counter: 0, status: CpuFlags::from_bits_truncate(0b100100), bus: Bus::new(), } } // ... }
现在就差不多了。不难,对吧?

本章完整源代码:GitHub