6502 仿真实现 ADC 和 SBC 的正确方法
6502 Emulation Proper Way to Implement ADC and SBC
我一直在为 MOS 6502 开发仿真器,但我似乎无法让 ADC 和 SBC 正常工作。我正在使用加载到模拟内存中 0x4000 处的 AllSuiteA program 测试我的模拟器,对于 test09,我当前的 ADC 和 SBC 实现只是没有获得正确的标志。我无数次尝试更改他们的算法,但每次,进位标志和溢出标志都足够重要,并导致测试进入 branch/not 分支。
Both of my functions are based off this.
memory[0x10000] 是累加器。它存储在内存范围之外,因此我可以有一个单独的寻址开关语句。
这是我对这些功能的实现之一:
case "ADC":
var t = memory[0x10000] + memory[address] + getFlag(flag_carry);
(memory[0x10000] & 0x80) != (t & 0x80) ? setFlag(flag_overflow) : clearFlag(flag_overflow);
signCalc(memory[0x10000]);
zeroCalc(t);
t > 255 ? setFlag(flag_carry) : clearFlag(flag_carry);
memory[0x10000] = t & 0xFF;
break;
case "SBC":
var t = memory[0x10000] - memory[address] - (!getFlag(flag_carry));
(t > 127 || t < -128) ? setFlag(flag_overflow) : clearFlag(flag_overflow);
t >= 0 ? setFlag(flag_carry) : clearFlag(flag_carry);
signCalc(t);
zeroCalc(t);
memory[0x10000] = t & 0xFF;
break;
此时我完全没有想法,but I did also run into the same problem with the data offered here. 所以这不仅仅是一个实施计划让我失望。
勇敢的冒险家,欢迎来到 6502 'add' 和 'subtract' 命令的奥术大厅!许多人已经走在你前面,尽管很少有人完成了等待你的所有考验。铁石心肠!
好了,剧情结束。简而言之,ADC 和 SBC 几乎是最难模拟的 6502 指令,主要是因为它们 难以置信地 复杂和复杂的小逻辑块。它们处理进位、溢出和十进制模式,当然实际上依赖于可以被认为是 'hidden' 伪寄存器工作存储的东西。
更糟糕的是,关于这些说明的文章很多,而且很大一部分文献都是错误的。我在 2008 年解决了这个问题,花了很多时间研究并将小麦与谷壳分开。结果是我在此处重现的一些 C# 代码:
case 105: // ADC Immediate
_memTemp = _mem[++_PC.Contents];
_TR.Contents = _AC.Contents + _memTemp + _SR[_BIT0_SR_CARRY];
if (_SR[_BIT3_SR_DECIMAL] == 1)
{
if (((_AC.Contents ^ _memTemp ^ _TR.Contents) & 0x10) == 0x10)
{
_TR.Contents += 0x06;
}
if ((_TR.Contents & 0xf0) > 0x90)
{
_TR.Contents += 0x60;
}
}
_SR[_BIT6_SR_OVERFLOW] = ((_AC.Contents ^ _TR.Contents) & (_memTemp ^ _TR.Contents) & 0x80) == 0x80 ? 1 : 0;
_SR[_BIT0_SR_CARRY] = (_TR.Contents & 0x100) == 0x100 ? 1 : 0;
_SR[_BIT1_SR_ZERO] = _TR.Contents == 0 ? 1 : 0;
_SR[_BIT7_SR_NEGATIVE] = _TR[_BIT7_SR_NEGATIVE];
_AC.Contents = _TR.Contents & 0xff;
break;
(我在写下面的答案时忘记了 NES 6502 所缺少的十进制模式。无论如何我都会保留它,因为它可能对编写 NES 模拟器的人有用。)
一旦您的模拟器具有 ADC
,SBC
就很容易实现。您需要做的就是反转参数的位并将其传递给 ADC
实现。要直观地了解为什么会这样,请注意反转 arg
的所有位会产生 -arg - 1
的二进制补码,并研究当 carry
标志为和不为时会发生什么设置。
这是我的模拟器中 SBC
的完整源代码。所有标志也将正确设置。
static void sbc(uint8_t arg) { adc(~arg); /* -arg - 1 */ }
实现ADC
最棘手的部分是溢出标志的计算。设置它的条件是结果具有 "wrong" 符号。由于范围的计算方式,事实证明这只会在两种情况下发生:
- 两个正数相加,结果为负数
- 两个负数相加,结果为正数
(1)和(2)可以简化为如下条件:
- 两个符号相同的数相加,结果符号不同。
通过一些 XOR 技巧,这允许 overflow
标志设置为以下代码(来自我的模拟器的完整 ADC
实现):
static void adc(uint8_t arg) {
unsigned const sum = a + arg + carry;
carry = sum > 0xFF;
// The overflow flag is set when the sign of the addends is the same and
// differs from the sign of the sum
overflow = ~(a ^ arg) & (a ^ sum) & 0x80;
zn = a /* (uint8_t) */ = sum;
}
如果 a
寄存器和 arg
的符号不同,(a ^ arg)
在符号位位置给出 0x80
。 ~
翻转位,如果 a
和 arg
具有相同的符号,您将得到 0x80
。用更通俗的英语来说,条件可以写成
overflow = <'a' and 'arg' have the same sign> &
<the sign of 'a' and 'sum' differs> &
<extract sign bit>
ADC
实现(以及许多其他指令)也使用一个技巧将 zero
和 negative
标志存储在一起。
我的 CPU 实现(来自 NES 模拟器)可以找到 here 顺便说一下。搜索 "Core instruction logic" 将为您提供所有指令(包括非官方指令)的简单实现。
我已经 运行 它通过了大量的测试 ROM 而没有失败(NES 仿真的一个好处是有很多很棒的测试 ROM 可用),我认为它应该没有错误在这一点上(除了一些非常模糊的东西,例如在某些情况下打开总线值)。
以下是我的 C64、VIC 20 和 Atari 2600 模拟器 (http://www.z64k.com) 的 6502 核心的代码片段,它实现了十进制模式并通过了所有 Lorenzes、bclark 和 asap 测试。如果您需要我解释其中的任何一个,请告诉我。我还有一些旧代码仍然通过了所有测试程序,但将指令和指令的十进制模式拆分为单独的 类。如果您更愿意破译它,那么理解我的旧代码可能会更简单。我所有的模拟器都使用附加代码来实现 ADC、SBC 和 ARR(不包括 ARR 的完整代码)指令。
public ALU ADC=new ALU(9,1,-1);
public ALU SBC=new ALU(15,-1,0);
public ALU ARR=new ALU(5,1,-1){
protected void setSB(){AC.ror.execute();SB=AC.value;}
protected void fixlo(){SB=(SB&0xf0)|((SB+c0)&0x0f);}
protected void setVC(){V.set(((AC.value^(SB>>1))&0x20)==0x20);C.set((SB&0x40)==0x40);if((P&8)==8){Dhi(hb);}}
};
public class ALU{
protected final int base,s,m,c0,c1,c2;
protected int lb,hb;
public ALU(int base,int s,int m){this.base=base;this.s=s;this.m=m;c0=6*s;c1=0x10*s;c2=c0<<4;}
public void execute(int c){// c= P&1 for ADC and ARR, c=(~P)&1 for SBC, P=status register
lb=(AC.value&0x0f)+(((DL.value&0x0f)+c)*s);
hb=(AC.value&0xf0)+((DL.value&0xf0)*s);
setSB();
if(((P&8)==8)&&(lb&0x1f)>base){fixlo();}//((P&8)==8)=Decimal mode
N.set((SB&0x80)==0x80);
setVC();
AC.value=SB&0xff;
}
protected void setSB(){SB=hb+lb;Z.set((SB&0xff)==0);}
protected void fixlo(){SB=(hb+c1)|((SB+c0)&0x0f);}
protected void Dhi(int a){if((a&0x1f0)>base<<4){SB+=c2;C.set(s==1);}}
protected void setVC(){V.set(((AC.value^SB)&(AC.value^DL.value^m)&0x80)==0x80);C.set(SB>=(0x100&m));if((P&8)==8){Dhi(SB);}}
}
我一直在为 MOS 6502 开发仿真器,但我似乎无法让 ADC 和 SBC 正常工作。我正在使用加载到模拟内存中 0x4000 处的 AllSuiteA program 测试我的模拟器,对于 test09,我当前的 ADC 和 SBC 实现只是没有获得正确的标志。我无数次尝试更改他们的算法,但每次,进位标志和溢出标志都足够重要,并导致测试进入 branch/not 分支。
Both of my functions are based off this.
memory[0x10000] 是累加器。它存储在内存范围之外,因此我可以有一个单独的寻址开关语句。
这是我对这些功能的实现之一:
case "ADC":
var t = memory[0x10000] + memory[address] + getFlag(flag_carry);
(memory[0x10000] & 0x80) != (t & 0x80) ? setFlag(flag_overflow) : clearFlag(flag_overflow);
signCalc(memory[0x10000]);
zeroCalc(t);
t > 255 ? setFlag(flag_carry) : clearFlag(flag_carry);
memory[0x10000] = t & 0xFF;
break;
case "SBC":
var t = memory[0x10000] - memory[address] - (!getFlag(flag_carry));
(t > 127 || t < -128) ? setFlag(flag_overflow) : clearFlag(flag_overflow);
t >= 0 ? setFlag(flag_carry) : clearFlag(flag_carry);
signCalc(t);
zeroCalc(t);
memory[0x10000] = t & 0xFF;
break;
此时我完全没有想法,but I did also run into the same problem with the data offered here. 所以这不仅仅是一个实施计划让我失望。
勇敢的冒险家,欢迎来到 6502 'add' 和 'subtract' 命令的奥术大厅!许多人已经走在你前面,尽管很少有人完成了等待你的所有考验。铁石心肠!
好了,剧情结束。简而言之,ADC 和 SBC 几乎是最难模拟的 6502 指令,主要是因为它们 难以置信地 复杂和复杂的小逻辑块。它们处理进位、溢出和十进制模式,当然实际上依赖于可以被认为是 'hidden' 伪寄存器工作存储的东西。
更糟糕的是,关于这些说明的文章很多,而且很大一部分文献都是错误的。我在 2008 年解决了这个问题,花了很多时间研究并将小麦与谷壳分开。结果是我在此处重现的一些 C# 代码:
case 105: // ADC Immediate
_memTemp = _mem[++_PC.Contents];
_TR.Contents = _AC.Contents + _memTemp + _SR[_BIT0_SR_CARRY];
if (_SR[_BIT3_SR_DECIMAL] == 1)
{
if (((_AC.Contents ^ _memTemp ^ _TR.Contents) & 0x10) == 0x10)
{
_TR.Contents += 0x06;
}
if ((_TR.Contents & 0xf0) > 0x90)
{
_TR.Contents += 0x60;
}
}
_SR[_BIT6_SR_OVERFLOW] = ((_AC.Contents ^ _TR.Contents) & (_memTemp ^ _TR.Contents) & 0x80) == 0x80 ? 1 : 0;
_SR[_BIT0_SR_CARRY] = (_TR.Contents & 0x100) == 0x100 ? 1 : 0;
_SR[_BIT1_SR_ZERO] = _TR.Contents == 0 ? 1 : 0;
_SR[_BIT7_SR_NEGATIVE] = _TR[_BIT7_SR_NEGATIVE];
_AC.Contents = _TR.Contents & 0xff;
break;
(我在写下面的答案时忘记了 NES 6502 所缺少的十进制模式。无论如何我都会保留它,因为它可能对编写 NES 模拟器的人有用。)
一旦您的模拟器具有ADC
,SBC
就很容易实现。您需要做的就是反转参数的位并将其传递给 ADC
实现。要直观地了解为什么会这样,请注意反转 arg
的所有位会产生 -arg - 1
的二进制补码,并研究当 carry
标志为和不为时会发生什么设置。
这是我的模拟器中 SBC
的完整源代码。所有标志也将正确设置。
static void sbc(uint8_t arg) { adc(~arg); /* -arg - 1 */ }
实现ADC
最棘手的部分是溢出标志的计算。设置它的条件是结果具有 "wrong" 符号。由于范围的计算方式,事实证明这只会在两种情况下发生:
- 两个正数相加,结果为负数
- 两个负数相加,结果为正数
(1)和(2)可以简化为如下条件:
- 两个符号相同的数相加,结果符号不同。
通过一些 XOR 技巧,这允许 overflow
标志设置为以下代码(来自我的模拟器的完整 ADC
实现):
static void adc(uint8_t arg) {
unsigned const sum = a + arg + carry;
carry = sum > 0xFF;
// The overflow flag is set when the sign of the addends is the same and
// differs from the sign of the sum
overflow = ~(a ^ arg) & (a ^ sum) & 0x80;
zn = a /* (uint8_t) */ = sum;
}
如果 a
寄存器和 arg
的符号不同,(a ^ arg)
在符号位位置给出 0x80
。 ~
翻转位,如果 a
和 arg
具有相同的符号,您将得到 0x80
。用更通俗的英语来说,条件可以写成
overflow = <'a' and 'arg' have the same sign> &
<the sign of 'a' and 'sum' differs> &
<extract sign bit>
ADC
实现(以及许多其他指令)也使用一个技巧将 zero
和 negative
标志存储在一起。
我的 CPU 实现(来自 NES 模拟器)可以找到 here 顺便说一下。搜索 "Core instruction logic" 将为您提供所有指令(包括非官方指令)的简单实现。
我已经 运行 它通过了大量的测试 ROM 而没有失败(NES 仿真的一个好处是有很多很棒的测试 ROM 可用),我认为它应该没有错误在这一点上(除了一些非常模糊的东西,例如在某些情况下打开总线值)。
以下是我的 C64、VIC 20 和 Atari 2600 模拟器 (http://www.z64k.com) 的 6502 核心的代码片段,它实现了十进制模式并通过了所有 Lorenzes、bclark 和 asap 测试。如果您需要我解释其中的任何一个,请告诉我。我还有一些旧代码仍然通过了所有测试程序,但将指令和指令的十进制模式拆分为单独的 类。如果您更愿意破译它,那么理解我的旧代码可能会更简单。我所有的模拟器都使用附加代码来实现 ADC、SBC 和 ARR(不包括 ARR 的完整代码)指令。
public ALU ADC=new ALU(9,1,-1);
public ALU SBC=new ALU(15,-1,0);
public ALU ARR=new ALU(5,1,-1){
protected void setSB(){AC.ror.execute();SB=AC.value;}
protected void fixlo(){SB=(SB&0xf0)|((SB+c0)&0x0f);}
protected void setVC(){V.set(((AC.value^(SB>>1))&0x20)==0x20);C.set((SB&0x40)==0x40);if((P&8)==8){Dhi(hb);}}
};
public class ALU{
protected final int base,s,m,c0,c1,c2;
protected int lb,hb;
public ALU(int base,int s,int m){this.base=base;this.s=s;this.m=m;c0=6*s;c1=0x10*s;c2=c0<<4;}
public void execute(int c){// c= P&1 for ADC and ARR, c=(~P)&1 for SBC, P=status register
lb=(AC.value&0x0f)+(((DL.value&0x0f)+c)*s);
hb=(AC.value&0xf0)+((DL.value&0xf0)*s);
setSB();
if(((P&8)==8)&&(lb&0x1f)>base){fixlo();}//((P&8)==8)=Decimal mode
N.set((SB&0x80)==0x80);
setVC();
AC.value=SB&0xff;
}
protected void setSB(){SB=hb+lb;Z.set((SB&0xff)==0);}
protected void fixlo(){SB=(hb+c1)|((SB+c0)&0x0f);}
protected void Dhi(int a){if((a&0x1f0)>base<<4){SB+=c2;C.set(s==1);}}
protected void setVC(){V.set(((AC.value^SB)&(AC.value^DL.value^m)&0x80)==0x80);C.set(SB>=(0x100&m));if((P&8)==8){Dhi(SB);}}
}