12

I've been working on an emulator for the MOS 6502, but I just can't seem to get ADC and SBC working right. I'm testing my emulator with the AllSuiteA program loaded at 0x4000 in emulated memory, and for test09, my current ADC and SBC implementations just aren't getting the right flags. I've tried changing their algorithms countless times, but every time, the carry flag and overflow flag are just enough off to matter, and cause the test to branch/not branch.

Both of my functions are based off this.

memory[0x10000] is the Accumulator. It's stored outside of the memory range so I can have a separate addressing switch statement.

This is one of my implementations of these functions:

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;

I'm all out of ideas at this point, but I did also run into the same problem with the data offered here. So it isn't just one implementation plan failing me here.

z0rberg's
  • 674
  • 5
  • 10
NAME__
  • 625
  • 1
  • 7
  • 17
  • For those wondering about this (which also stumped me), here is an explanation and the V flag calculation that works: https://rogerngo.com/article/20190920_overflow_6502/ – Johncl Jan 21 '23 at 20:53

3 Answers3

21

(I forgot about Decimal mode -- which the NES's 6502 lacks -- when writing the answer below. I'll leave it anyway as it may be useful to people writing NES emulators.)

SBC is easy to implement once your emulator has ADC. All you need to do is to invert the bits of the argument and pass that to the ADC implementation. To get an intuitive idea of why this works, note that inverting all the bits of arg produces -arg - 1 in two's complement, and work through what happens when the carry flag is and isn't set.

Here's the complete source for SBC in my emulator. All flags will be set correctly too.

static void sbc(uint8_t arg) { adc(~arg); /* -arg - 1 */ }

The trickiest part when implementing ADC is the calculation of the overflow flag. The condition for it to get set is that the result has the "wrong" sign. Due to how the ranges work out, it turns out that this can only happen in two circumstances:

  1. Two positive numbers are added, and the result is a negative number.
  2. Two negative numbers are added, and the result is a positive number.

(1) and (2) can be simplified into the following condition:

  • Two numbers that have the same sign are added, and the result has a different sign.

With some XOR trickery, this allows the overflow flag to be set as in the following code (the complete ADC implementation from my emulator):

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) gives 0x80 in the sign bit position if the a register and arg differ in sign. ~ flips the bits so that you get 0x80 if a and arg have the same sign. In plainer English, the condition can be written

overflow = <'a' and 'arg' have the same sign> &  
           <the sign of 'a' and 'sum' differs> &  
           <extract sign bit>

The ADC implementation (as well as many other instructions) also uses a trick to store the zero and negative flags together.

My CPU implementation (from an NES emulator) can be found here by the way. Searching for "Core instruction logic" will give you simple implementations for all instructions (including unofficial instructions).

I've run it through plenty of test ROMs without failures (one of the upsides of NES emulation is that there's plenty of great test ROMs available), and I think it should be pretty much bug free at this point (save for some extremely obscure stuff involving e.g. open bus values in some circumstances).

Ulfalizer
  • 4,664
  • 1
  • 21
  • 30
  • I like your SBC implementation, but I don't see anything in your ADC implementation to handle Decimal mode... – Eight-Bit Guru Apr 09 '15 at 09:41
  • @Eight-BitGuru: Sorry, had forgotten about that. The 6502 in the NES lacks Decimal mode (perhaps due to a patent issue. Internally it's just a single trace that has been cut). Not sure how well the approach above would lend itself to decimal mode. – Ulfalizer Apr 09 '15 at 10:04
  • Hmm. The NES CPU is actually a Ricoh 2A03, containing a second source MOS Technology 6502 core lacking the 6502's binary-coded decimal mode, with 22 memory-mapped I/O registers that control an APU, rudimentary DMA, and game controller polling. It's not actually a 6502, so arguably this answer is not relevant to the question. – Eight-Bit Guru Apr 09 '15 at 15:25
  • 2
    @Eight-BitGuru: It uses a 6502 core. The 2A03 just happens to contain some other functionality too. Even the undocumented instructions work identically to standalone MOS 6502s (except perhaps for some very minor details due to the removal of BCD). You can see the 6502 core in the bottom right in this die shot: http://www.visual6502.org/images/RP2A/Nintendo_RP2A03G_die_shot_1a_1600w.jpg – Ulfalizer Apr 09 '15 at 15:47
  • All true. However, the core *lacks* some functionality of the 6502, in that Decimal mode isn't there. Consequently your emulation is of the 2A03, not the 6502, and therefore your answer is not correct for questions about emulating ADC/SBC on the 6502. – Eight-Bit Guru Apr 09 '15 at 15:57
  • 1
    Yeah, that's a fair point. I guess I could add a disclaimer to say that it ignores Decimal mode. Many people writing 6502/2A03 emulators probably do so for NES emulators, so it might be useful still. – Ulfalizer Apr 09 '15 at 16:00
  • Upvoted, mostly for the SBC code (which I think is very elegant), and for the disclaimer. – Eight-Bit Guru Apr 12 '15 at 11:25
  • My SBC kept failing Klaus' tests because I was calculating twos complement (by adding carry) first, before passing it on to ADC (with carry-add disabled). That caused me much pain. Your simple code of SBC(x) => ADC(~x) is amazing and worked! Thank you! – Ignis Incendio Oct 21 '21 at 13:59
15

Welcome, brave adventurer, to the arcane halls of the 6502 'add' and 'subtract' commands! Many have walked these steps ahead of you, though few have completed the gamut of trials that await you. Stout heart!

OK, dramatics over. In a nutshell, ADC and SBC are pretty-much the toughest 6502 instructions to emulate, mainly because they're incredibly complex and sophisticated little nuggets of logic. They handle carry, overflow, and decimal mode, and of course actually rely on what could be thought of as 'hidden' pseudo-register working storage.

Making things worse is that a lot has been written about these instructions, and a good percentage of the literature out there is wrong. I tackled this in 2008, spending many hours researching and separating the wheat from the chaff. The result is some C# code I reproduce here:

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;
Eight-Bit Guru
  • 9,756
  • 2
  • 48
  • 62
  • The link to the blog post is broken. – Solra Bizna Feb 20 '17 at 09:49
  • 1
    I've excised the broken link. – Eight-Bit Guru Feb 20 '17 at 12:05
  • 1
    @bartlomiej.n It's a Temporary Register (TR) used to hold the intermediate value which determines the SR flag settings before committing the final result to the Accumulator. – Eight-Bit Guru Aug 19 '19 at 12:21
  • 2
    I may be missing something, but I'm not sure how the decimal mode handling in the C# code snippet can work. The conditional that leads to adding 0x06 is only regarding bit 4 (mask 0x10) of the values. So, for example, if A=0x09, the immediate mode operand is 0x01, and carry is clear, then TR will initially be 0x0A, the (0x09 ^ 0x01 ^ 0x0A) & 0x10 == 0x10 condition will never be true, and TR will not be corrected. – David Simmons Oct 13 '20 at 22:16
2

The following is the snippet of code of the 6502 core of my C64, VIC 20 and Atari 2600 emulators (http://www.z64k.com) which implements decimal mode and passes all of Lorenzes, bclark and asap tests. Let me know if you need me to explain any of it. I also have older code that still passes all the test programs but splits the instructions and the decimal modes of the instructions into separate classes. It might be simpler to understand my older code if you prefer to decipher that. All my emulators use the attached code to implement the ADC, SBC and ARR(Full Code of ARR not included) instructions.

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);}}

}