Buhe Blog - Microsoft Internet Explorer

4.nes 模拟器模拟总线


CPU 使用三种总线访问内存(包括内存映射空间):

  • 地址总线承载所需位置的地址
  • 控制总线通知它是读还是写访问
  • 数据总线承载正在读取或写入的数据字节 总线本身不是设备;它是平台组件之间的连线。因此,我们不需要将它作为一个独立的模块来实现,因为 Rust 允许我们直接“连接”组件。然而,它是一个方便的抽象,我们可以卸下相当多的责任来保持 CPU 代码的清洁。 在我们当前的代码中,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 的寻址轨道。 然而,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