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
:
- calculate A + operand + carry flag;
- set negative and zero based on the outcome;
- 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
- 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:
- calculate the binary result. This is SBC so that's A + ~operand + carry.
- 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:
- 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
- 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;