作用
这个必须模拟,否则完全无法运行,我们可以在没有实现界面时候,通过打印来测试各种指令
模拟过程
- 重要数据结构:各种寄存器
- 各种指令,读取地址,读取寄存器,堆栈操作
寄存器
用于各种运算,计算机的本质就是运算
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 这个指令
- 查看文档说明
- A = A + memory + C 这个公式,这个A 代表上面寄存器ACC,memory 内存值 ,当前进位标识的值
- 文档核心说明:ADC adds the carry flag and a memory value to the accumulator,这个是文字说明
- 辅助:如果你还没有明白可以用AI浏览器,豆包或者tabbit 打开这个网页问ADC 指令说明
- 如果你还是不理解,你看要用代码说明ADC实现,反复几次你就实现类似的功能
- 按照文档实现ADC 对应中几个不同寻址模式实现,这里一个指令多个寻址,那么对应的字节码就会多个,因为Nes 指令集合只有一个字节,这个非常用于区分,现代指令集合就复杂多了,可能1~x多个字节
寻址模式
本质计算是读写地址从而读写数据,同时包括单纯数据赋值,不用来读写,但一般情况肯定用读写
文档介绍:https://www.nesdev.org/wiki/CPU_addressing_modes
分类:
- Indexed addressing
- 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;
}
代码说明
- 这个cpu::Clock 执行,进行读取指令
- 执行对应指令
- 整个流程基本这样子
指令读取
指令组成部分:opcode(操作码) + 操作数
//读取opcode
uint8_t OPCODE = ReadByte(m_register.PC++);
//根据opcode 确定寻址模式,用于读取操作数
//比喻前面加0x69指令,z后续跟的立即数
data = Immediate();
//执行具体实现
ADC(data)
//cycle ,暂时可以不考虑,这个以后给ppu和apu用的,用于同步,暂时可以完全不考虑
cycle = 2;
这个指令基本执行过程
测试代码
- 对应机器码可以用 自己看 指令对应代码
- 用在线指令网站使用,点击汇编运行可以看到内存的对应指令
过程
- 打开网站 https://wavespeed.ai/zh-CN/free-tools/6502-emulator
- 写入代码 ADC 0xA ,全部替换
- 点击汇编,可以在反汇编机器码
- 可以填写到测试代码