6502 Emulation Proper Way to Implement ADC and SBC
Asked Answered
X

4

13

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.

Xena answered 22/3, 2015 at 10:33 Comment(1)
For those wondering about this (which also stumped me), here is an explanation and the V flag calculation that works: rogerngo.com/article/20190920_overflow_6502Urethrectomy
T
16

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;
Tola answered 22/3, 2015 at 11:46 Comment(4)
The link to the blog post is broken.Gamali
I've excised the broken link.Tola
@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.Tola
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.Basin
F
22

(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).

Fiftyfifty answered 24/3, 2015 at 3:45 Comment(8)
I like your SBC implementation, but I don't see anything in your ADC implementation to handle Decimal mode...Tola
@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.Fiftyfifty
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.Tola
@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: visual6502.org/images/RP2A/…Fiftyfifty
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.Tola
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.Fiftyfifty
Upvoted, mostly for the SBC code (which I think is very elegant), and for the disclaimer.Tola
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!Cohligan
T
16

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;
Tola answered 22/3, 2015 at 11:46 Comment(4)
The link to the blog post is broken.Gamali
I've excised the broken link.Tola
@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.Tola
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.Basin
V
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);}}

}

Vivian answered 6/11, 2015 at 7:59 Comment(0)
T
1

A belated answer because I'm not especially impressed with the existing here's-some-code-you-figure-it-out answers.


The binary mode ADC and SBC are straightforward. For ADC:

  1. calculate A + operand + carry flag;
  2. set negative and zero based on the outcome;
  3. set overflow if that produces an impossible sign result — if you added two positive numbers and got a negative, or added two negative numbers and got a positive. Neither makes logical sense but both are possibilities due to the finite range; and
  4. set carry if there was carry out of bit 7. The easiest suggestion is just to do the arithmetic in 16-bit form and check whether anything is in bit 8 at the end.

For SBC, just complement the operand before doing exactly as above.


Decimal-mode SBC is the easier to explain.

Stuff that doesn't require exposition:

  1. calculate the binary result. This is SBC so that's A + ~operand + carry.
  2. set all flags based on the binary result.

The range of values that can fit inside a binary nibble is 0–15; the range of values used in BCD is 0–9. BCD values are always unsigned. So you're subtracting one positive number from another.

If the value in A is greater than or equal to the value being subtracted from it, you'll get the result you already want — e.g. 0x8 - 0x2 = 0x6 just like 8 - 2 = 6.

Only if the value in A was less than that being subtracted will you get into difficulty — e.g. 0x2 - 0x8 = 0xA when constrained to the nibble.

That means that a nibble needs correction back to BCD only exactly when it generated borrow — in terms of A + ~operand that's when there wasn't carry.

So, on a regular 6502:

  1. if there was carry into bit 4, subtract a further 6 (or add 10 if you prefer) from the low nibble, not allowing any further carry to propagate*; and
  2. if there was carry out of bit 7, subtract a further 0x60, not further affecting the carry flag.

* because carry already propagated when you wrapped below 0 in the original 8-bit addition. So you'd be allowing the same carry to propagate twice.

A 65C02 slightly alters things by, effectively, adding the 8-bit value of 0xfa if it needs to fix up the low nibble. Which for valid BCD digits is the same as adding 0xa without propagation because the top nibble of 0xf undoes the extra carry you should get when adding 0xa to the low nibble. It's not exactly the same for invalid original BCD values, and the 65C02's SBC will produce different final results for invalid original values as a result.


Decimal-mode ADC is similar but the test for detecting digits that need to be fixed is slightly different, and the flags are set at a different time.

A fix up is required if either:

  • the final nibble isn't a valid BCD value, e.g. 0x5 + 0x5 = 0xA; or
  • there was carry out of the nibble, e.g. 0x9 + 0x9 = 0x2, with carry.

Neither test is sufficient in isolation as demonstrated by the examples. A fix up is achieved by just adding 6 to the result.

Other than that, ADC also differs from SBC in that it sets N and V based on the intermediate result after fixing the low nibble but before fixing the high one.


Addendum:

The 65C02 also differs from the original 6502 in setting N and Z at the complete end of the operation rather than earlier so that they reflect the final BCD result. It charges you an extra cycle for that.

For the sake of interpreting some of the other code already posted here:

(a ^ result) & (result ^ operand) & 0x80 is overflow as described herein; it is non-zero only if the result differs in sign from both the original accumulator and operand, which indirectly implies that both accumulator and operand had the same sign.

(a ^ operand ^ result) & 0x10 is a test for carry into bit 4 because it calculates the naive result in that bit if there wasn't carry:

  • if the original inputs were 0 and 0, or 1 and 1, expect the output to be 0;
  • otherwise expect the output to be 1

... and then checks whether that differs from the actual final result. It can only differ if there was also carry into that bit position.


Decimal-mode code I actually use, given that the Numeric:: templates referenced do what the names say and given that I just store the result that should be evaluated for N or Z supposing anybody ever wants them as a negligible form of lazy evaluation:

SBC:

operand_ = ~operand_;
uint8_t result = a_ + operand_ + flags_.carry;

// All flags are set based only on the decimal result.
flags_.zero_result = result;
flags_.carry = Numeric::carried_out<7>(a_, operand_, result);
flags_.negative_result = result;
flags_.overflow = (( (result ^ a_) & (result ^ operand_) ) & 0x80) >> 1;

// General SBC logic:
//
// Because the range of valid numbers starts at 0, any subtraction that should have
// caused decimal carry and which requires a digit fix up will definitely have caused
// binary carry: the subtraction will have crossed zero and gone into negative numbers.
//
// So just test for carry (well, actually borrow, which is !carry).

// The bottom nibble is adjusted if there was borrow into the top nibble;
// on a 6502 additional borrow isn't propagated but on a 65C02 it is.
// This difference affects invalid BCD numbers only — valid numbers will
// never be less than -9 so adding 10 will always generate carry.
if(!Numeric::carried_in<4>(a_, operand_, result)) {
    if constexpr (is_65c02(personality)) {
        result += 0xfa;
    } else {
        result = (result & 0xf0) | ((result + 0xfa) & 0xf);
    }
}

// The top nibble is adjusted only if there was borrow out of the whole byte.
if(!flags_.carry) {
    result += 0xa0;
}

a_ = result;

ADC:

uint8_t result = a_ + operand_ + flags_.carry;
flags_.zero_result = result;
flags_.carry = Numeric::carried_out<7>(a_, operand_, result);

// General ADC logic:
//
// Detecting decimal carry means finding occasions when two digits added together totalled
// more than 9. Within each four-bit window that means testing the digit itself and also
// testing for carry — e.g. 5 + 5 = 0xA, which is detectable only by the value of the final
// digit, but 9 + 9 = 0x18, which is detectable only by spotting the carry.

// Only a single bit of carry can flow from the bottom nibble to the top.
//
// So if that carry already happened, fix up the bottom without permitting another;
// otherwise permit the carry to happen (and check whether carry then rippled out of bit 7).
if(Numeric::carried_in<4>(a_, operand_, result)) {
    result = (result & 0xf0) | ((result + 0x06) & 0x0f);
} else if((result & 0xf) > 0x9) {
    flags_.carry |= result >= 0x100 - 0x6;
    result += 0x06;
}

// 6502 quirk: N and V are set before the full result is computed but
// after the low nibble has been corrected.
flags_.negative_result = result;
flags_.overflow = (( (result ^ a_) & (result ^ operand_) ) & 0x80) >> 1;

// i.e. fix high nibble if there was carry out of bit 7 already, or if the
// top nibble is too large (in which case there will be carry after the fix-up).
flags_.carry |= result >= 0xa0;
if(flags_.carry) {
    result += 0x60;
}

a_ = result;
Thence answered 3/9, 2023 at 18:12 Comment(1)
Thanks to all the answers in this topic, my emulator is very close to passing all the Klaus Dormann tests, but is failing somewhere in the decimal mode SBC logic. Can you elaborate on your Numeric::carried_out and Numeric::carried_in templates do please? I've not been able to properly grok them.Lucius

© 2022 - 2025 — McMap. All rights reserved.