小鱼塘--自说自话的地方

  • 小玩意
  • 小想法
记录自己技术和想法地方
  1. 首页
  2. nes
  3. 正文

nes 模拟器开发教程(二):cpu 1.1

27 10 月, 2026 1点热度 0人点赞 0条评论
内容目录

作用

这个必须模拟,否则完全无法运行,我们可以在没有实现界面时候,通过打印来测试各种指令

模拟过程

  1. 重要数据结构:各种寄存器
  2. 各种指令,读取地址,读取寄存器,堆栈操作

寄存器

用于各种运算,计算机的本质就是运算

    struct Register {
        uint8_t ACC;
        uint8_t X;
        uint8_t Y;
        uint8_t SP;
        uint8_t P;
        uint8_t PC;
    };

ACC:常用于累加
X,Y:常用于循环
SP:堆栈指针,这个跟我们x86 esp/rsp 一样
P:状态寄存器,用于各种运算中,如果这个状态错误,会导致各种异常
PC:当前执行的地址,每个指令执行都会被修改掉,用于执行下一条指令

比x86 寄存器少了很多,因为现代CPU 发展多少年进步非常多了,如果模拟现代CPU要管理寄存器多了很多,同时指令多了很多倍。

寄存器对应文档wiki
https://www.nesdev.org/wiki/CPU_registers

指令

作用:进行运算和读写数据
大概官方指令有56个,我们按照文档加上AI 说明来实现指令,这里最终要是寻址方式,这里寻址方式是通用,我们只要把寻址方式封装,那么指令实现就方便很多,同时错误也会减少很多,常见错误就是寻址写错了

过程

我们找到一个比较简单但又包含几种寻址的指令 ADC, 这个也是nesdev cpu 文档的第一个
这个就是简单加法运算
https://www.nesdev.org/wiki/Instruction_reference 找到ADC 这个指令

  1. 查看文档说明
  2. A = A + memory + C 这个公式,这个A 代表上面寄存器ACC,memory 内存值 ,当前进位标识的值
  3. 文档核心说明:ADC adds the carry flag and a memory value to the accumulator,这个是文字说明
  4. 辅助:如果你还没有明白可以用AI浏览器,豆包或者tabbit 打开这个网页问ADC 指令说明
  5. 如果你还是不理解,你看要用代码说明ADC实现,反复几次你就实现类似的功能
  6. 按照文档实现ADC 对应中几个不同寻址模式实现,这里一个指令多个寻址,那么对应的字节码就会多个,因为Nes 指令集合只有一个字节,这个非常用于区分,现代指令集合就复杂多了,可能1~x多个字节

寻址模式

本质计算是读写地址从而读写数据,同时包括单纯数据赋值,不用来读写,但一般情况肯定用读写

文档介绍:https://www.nesdev.org/wiki/CPU_addressing_modes
分类:

  1. Indexed addressing
  2. Other addressing

这个区分规则是否x,y 参与运算,否则就是其他运算

按照文档对应公式计算就可以了,如果不明白可以要AI给出具体例子,这样子方便我们理解

具体寻址模式计算

其他寻址模式

1、Implicit
隐式寻址,没有数字用来运算,比喻CLC 、NOP 指令等等

2、Accumulator
这个只是 Accumulator 寄存器进行操作,这个比较简单,只是单独分类出来感觉

3、Immediate
数字用于运算,不能当成地址读取,一般nes代码用# 代替立即数字

4、Zero page
数字就是地址,只是这个地址0~255返回(1个字节),这个地址就是所谓的零页,因为是一个字节,访问速度快很多,2个字节的指令要短,那么速度更快

5、Absolute
16位作为地址,读写数据

6、Relative
这个根据当前pc 地址+ 数字获取地址用来读取,一般用来条件跳转
目标地址=PC+2+偏移量,这个是 -128~127

常见的指令:BCC, BCS,BEQ

7、Indirect
这个就地址就是纯间接,这个只有jmp

在 6502/NES 里,"间接寻址"指的是指令的操作数不是一个直接的目标地址,而是一个"存放着目标地址的指针"​。CPU 先根据操作数找到指针位置,再从指针里读出 16 位地址,最后才访问真正的目标。严格意义上的 Indirect 是 JMP 指令专用的 (a) 模式,

全局数据是8字节,uint8_t

标志

溢出

这个算是所有的标志最难理解
ADC :加法的溢出

简单说,V 标志跟踪的是有符号溢出:把 8 位值当作 −128-128−128 到 +127+127+127 的数来看,当运算结果超出了这个范围(符号位“翻车”)时 V 被置 1。它和无符号溢出(C 标志)是两回事。

这个是加法:比喻2个正数相加,结果是负数,那么就是溢出了

正数溢出
两个正数相加,结果却成了负数(正溢出),例如 \7F + \01 = \80, 127 + 1 = -128;

负溢出
一个负数相加,结果变成正数(负溢出),-128+(-128) =0

正数+辅助不会溢出
这个不会溢出,溢出本质结果超出范围,符号位翻转(2个操作符号一定相同,但结果不一样),这个是加法的溢出,减法的溢出稍微不一样。

公式的逻辑

(result ^ A) & (result ^ memory) & $80

拆开看:先分别找出“结果与 A 不同的位”和“结果与 memory 不同的位”,两者按位与,最后只保留 bit 7(符号位)。等价的说法就是——只有当结果的符号位同时不同于 A 的符号位、也不同于 memory 的符号位时,V 才为 1

^:异或 ,两个位相同为0,相异为1

reuslt^A :进行异或,作用是用来判断符号是否相等,因为后面相与 & 0x80
result^memory:进行位于操作,用来判断符号相等,因为后面相与 & 0x80

总结:本质结果符号与运算符号都不相等,就是溢出,这个公式就是作用

资料

https://masswerk.at/6502/6502_instruction_set.html#ADC

在线运行
https://wavespeed.ai/zh-CN/free-tools/6502-emulator

代码

项目工程:后面补充

核心代码点

//测试代码入口
void Nes::Test()
{
    //测试代码 adc 0xa 
    uint8_t code[] = {
        0x69,0xA
    };

    InitNes();

    memcpy_s(m_memory, 65536, code, sizeof(code));

    m_cpu.SetAccRegister(0x2);
    m_cpu.Clock();              //执行单条指令
    m_cpu.PrintfRegister();     //打印寄存器
}

//核心代码运行接口
uint16_t CPU::Clock()
{
    uint16_t cycle = 0;
    uint8_t OPCODE = ReadByte(m_register.PC++);
    uint8_t data = 0;
    switch (OPCODE)
    {
    case 0x69:
        data = Immediate();
        ADC(data);
        cycle = 2;
        break;
    default:
        break;
    }

    return cycle;
}

代码说明

  1. 这个cpu::Clock 执行,进行读取指令
  2. 执行对应指令
  3. 整个流程基本这样子

指令读取

指令组成部分:opcode(操作码) + 操作数


//读取opcode 
uint8_t OPCODE = ReadByte(m_register.PC++);

//根据opcode 确定寻址模式,用于读取操作数
//比喻前面加0x69指令,z后续跟的立即数
data = Immediate();

//执行具体实现
ADC(data)

//cycle ,暂时可以不考虑,这个以后给ppu和apu用的,用于同步,暂时可以完全不考虑
cycle = 2;

这个指令基本执行过程

测试代码

  1. 对应机器码可以用 自己看 指令对应代码
  2. 用在线指令网站使用,点击汇编运行可以看到内存的对应指令

过程

  1. 打开网站 https://wavespeed.ai/zh-CN/free-tools/6502-emulator
  2. 写入代码 ADC 0xA ,全部替换
  3. 点击汇编,可以在反汇编机器码
  4. 可以填写到测试代码
标签: 暂无
最后更新:27 10 月, 2026

小鱼儿

爱研究技术,爱玩LOL

点赞
< 上一篇

COPYRIGHT © 2022 小鱼塘. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

湘ICP备18005349号