Buhe Blog - Microsoft Internet Explorer

3.2.模拟CPU内存寻址模式


在我们最初的实现中,CPU 接收指令作为单独的输入流,这不是实际 NES 中的工作方式。 NES 实现了典型的冯诺依曼架构:数据和指令都存储在内存中。从 CPU 的角度来看,执行的代码是数据,任何数据都可能被解释为可执行代码。CPU 无法区分。CPU 拥有的唯一机制是 **program_counter **寄存器,用于跟踪指令流中的位置。 让我们在我们的 CPU 代码中勾勒出这一点:


pub struct CPU {
   pub register_a: u8,
   pub register_x: u8,
   pub status: u8,
   pub program_counter: u16,
   memory: [u8; 0xFFFF]
}

impl CPU {

		fn mem_read(&self, addr: u16) -> u8 {
        self.memory[addr as usize]
    }
		fn mem_write(&mut self, addr: u16, data: u8) {
        self.memory[addr as usize] = data;
    }
		pub fn load_and_run(&mut self, program: Vec<u8>) {
        self.load(program);
        self.run()
    }
		pub fn load(&mut self, program: Vec<u8>) {
        self.memory[0x8000 .. (0x8000 + program.len())].copy_from_slice(&program[..]);
        self.program_counter = 0x8000;
    }
		pub fn run(&mut self) {
// note: we move intialization of program_counter from here to load function
			loop {
            let opscode = self.mem_read(self.program_counter);
            self.program_counter += 1;
						match opscode {
								//..
			      }
	    }
   }
}

我们刚刚为整个 64 KiB 的地址空间创建了一个数组。正如在下一章中讨论的那样,CPU 只有 2 KiB 的 RAM,其他所有内容都保留用于内存映射。 我们将程序代码加载到内存中,从 0x8000 地址开始。我们已经讨论过 [0x8000 .. 0xFFFF] 是为程序 ROM 保留的,我们可以假设指令流应该在这个空间的某个地方开始(不一定在 0x8000)。 NES 平台有一个特殊的机制来标记 CPU 应该从哪里开始执行。插入新卡带后,CPU 会收到一个称为“重置中断”的特殊信号,指示 CPU:

  • 重置状态(寄存器和标志)
  • 将 **program_counter **设置为存储在 0xFFFC 的 16 位地址 在实现之前,我应该简单提一下 NES CPU 可以寻址 65536 个内存单元。存储一个地址需要 2 个字节。NES CPU 使用 Little-Endian 寻址而不是 Big-Endian。这意味着地址的 8 个最低有效位将存储在 8 个最高有效位之前。 为了说明区别: 例如,将数据从存储单元 0x8000 读取到 A 寄存器的指令如下所示:

LDA $8000      <=>    ad 00 80

我们可以使用 Rust 的一些位运算来实现这种行为:


fn mem_read_u16(&mut self, pos: u16) -> u16 {
       let lo = self.mem_read(pos) as u16;
       let hi = self.mem_read(pos + 1) as u16;
       (hi << 8) | (lo as u16)
}

fn mem_write_u16(&mut self, pos: u16, data: u16) {
       let hi = (data >> 8) as u8;
       let lo = (data & 0xff) as u8;
       self.mem_write(pos, lo);
       self.mem_write(pos + 1, hi);
}

或者通过使用 Rust对原始类型的字节序支持。 现在我们可以正确实现重置功能了。我们将不得不调整load和load_and_run功能:

  • **load **方法应该将程序加载到 PRG ROM 空间并将对代码的引用保存到 0xFFFC 存储单元中
  • **reset **方法应该恢复所有寄存器的状态,并通过存储在 0xFFFC 的 2 字节值初始化 program_counter

pub fn reset(&mut self) {
       self.register_a = 0;
       self.register_x = 0;
       self.status = 0;
			 self.program_counter = self.mem_read_u16(0xFFFC);
   }

pub fn load(&mut self, program: Vec<u8>) {
       self.memory[0x8000 .. (0x8000 + program.len())].copy_from_slice(&program[..]);
       self.mem_write_u16(0xFFFC, 0x8000);
   }

pub fn load_and_run(&mut self, program: Vec<u8>) {
       self.load(program);
       self.reset();
       self.run()
   }

不要忘记现在修复失败的测试。 好吧,这是最容易的部分。 还记得我们上一章实现的 LDA 操作码吗?该单个助记符 (LDA) 实际上可以根据参数的类型翻译成 8 种不同的机器指令: 您可以阅读有关寻址模式的信息:

  • 这里
  • 在这里 简而言之,寻址模式是指令的一个属性,它定义了 CPU 应该如何解释指令流中接下来的 1 或 2 个字节。 不同的寻址方式有不同的指令大小,例如:
  • 零页版本 ($A5) 的大小为 2 个字节,一个用于操作码本身,一个用于参数。这就是为什么零页寻址不能引用前 255 个字节以上的内存。
  • 绝对版本注意:2字节的参数将根据 little-endian规则传递 ($AD) 有 3 个字节,地址占用 2 个字节,因此可以引用所有 65536 个存储单元。( ) 没有占用超过 3 个字节的操作码。CPU 指令大小可以是 1、2 或 3 个字节。 大多数 CPU 指令提供不止一种寻址方式。理想情况下,我们不想为每条 CPU 指令重新实现相同的寻址模式逻辑。 让我们尝试编写 CPU 应该如何解释不同的寻址模式:

#[derive(Debug)]
#[allow(non_camel_case_types)]
pub enum AddressingMode {
   Immediate,
   ZeroPage,
   ZeroPage_X,
   ZeroPage_Y,
   Absolute,
   Absolute_X,
   Absolute_Y,
   Indirect_X,
   Indirect_Y,
   NoneAddressing,
}impl CPU {
// ...
fn get_operand_address(&self, mode: &AddressingMode) -> u16 {
	match mode {
           AddressingMode::Immediate => self.program_counter,
           AddressingMode::ZeroPage  => self.mem_read(self.program_counter) as u16,
           AddressingMode::Absolute => self.mem_read_u16(self.program_counter),
           AddressingMode::ZeroPage_X => {let pos = self.mem_read(self.program_counter);
               let addr = pos.wrapping_add(self.register_x) as u16;
               addr
           }
           AddressingMode::ZeroPage_Y => {let pos = self.mem_read(self.program_counter);
               let addr = pos.wrapping_add(self.register_y) as u16;
               addr
           }
           AddressingMode::Absolute_X => {let base = self.mem_read_u16(self.program_counter);
               let addr = base.wrapping_add(self.register_x as u16);
               addr
           }
           AddressingMode::Absolute_Y => {let base = self.mem_read_u16(self.program_counter);
               let addr = base.wrapping_add(self.register_y as u16);
               addr
           }
           AddressingMode::Indirect_X => {let base = self.mem_read(self.program_counter);let ptr: u8 = (base as u8).wrapping_add(self.register_x);
               let lo = self.mem_read(ptr as u16);
               let hi = self.mem_read(ptr.wrapping_add(1) as u16);
               (hi as u16) << 8 | (lo as u16)
           }
           AddressingMode::Indirect_Y => {let base = self.mem_read(self.program_counter);let lo = self.mem_read(base as u16);
               let hi = self.mem_read((base as u8).wrapping_add(1) as u16);
               let deref_base = (hi as u16) << 8 | (lo as u16);
               let deref = deref_base.wrapping_add(self.register_y as u16);
               deref
           }
           AddressingMode::NoneAddressing => {panic!("mode {:?} is not supported", mode);
           }
       }
   }

这样,我们就可以改变我们最初的LDA实现。


fn lda(&mut self, mode: &AddressingMode) {
       let addr = self.get_operand_address(mode);
       let value = self.mem_read(addr);
			 self.register_a = value;
       self.update_zero_and_negative_flags(self.register_a);
   }

pub fn run(&mut self) {
       loop {
           let code = self.mem_read(self.program_counter);
           self.program_counter += 1;match code {
               0xA9 => {
                   self.lda(&AddressingMode::Immediate);
                   self.program_counter += 1;
               }0xA5 => {
                   self.lda(&AddressingMode::ZeroPage);
                   self.program_counter += 1;
               }0xAD => {
                   self.lda(&AddressingMode::Absolute);
                   self.program_counter += 2;
               }//....
           }
       }
   }

注意:绝对有必要在从指令流中读取每个字节后递增 program_counter 。 不要忘记你的测试。


#[test]
   fn test_lda_from_memory() {
       let mut cpu = CPU::new();
       cpu.mem_write(0x10, 0x55);
       cpu.load_and_run(vec![0xa5, 0x10, 0x00]);assert_eq!(cpu.register_a, 0x55);
   }

使用相同的基础,我们可以快速实现 **STA **指令,将寄存器 A 的值复制到内存中。


fn sta(&mut self, mode: &AddressingMode) {
        let addr = self.get_operand_address(mode);
        self.mem_write(addr, self.register_a);
    }pub fn run(&mut self) {
//...
match code {
//..
/* STA */0x85 => {
                self.sta(AddressingMode::ZeroPage);
                self.program_counter += 1;
            }0x95 => {
                self.sta(AddressingMode::ZeroPage_X);
                self.program_counter += 1;
            }
				//..
        }
    }

在我们结束之前,我想提一下当前的运行方法由于几个原因有些不确定。首先,在某些操作之后将 program_counter 增加 1(或 2)的要求很容易出错。如果我们犯了一个错误,就很难发现它。 其次,如果我们可以将所有“LDA”操作归为一条 match 语句,岂不是更易读、更方便? 最后,我们所做的就是将指令规范硬编码到 Rust 代码中。翻译有点难以比较。以某种表格形式保存代码看起来是一种更易于管理的方法。 我留给你弄清楚如何达到这一点。

本章完整源代码:GitHub