1
0
mirror of https://github.com/pevans/erc-c.git synced 2024-11-27 20:51:17 +00:00

Add support for decimal mode ADC/SBC.

This also corrects a bug where SBC set carry incorrectly in binary mode.
This commit is contained in:
Peter Evans 2018-02-23 00:46:07 -06:00
parent 2669460c6d
commit 0d1e22a348
3 changed files with 151 additions and 1 deletions

View File

@ -175,6 +175,7 @@ DECL_ADDR_MODE(zpy);
* (excepting mos6502.addr.c).
*/
DECL_INST(adc);
DECL_INST(adc_dec);
DECL_INST(and);
DECL_INST(asl);
DECL_INST(bad);
@ -227,6 +228,7 @@ DECL_INST(ror);
DECL_INST(rti);
DECL_INST(rts);
DECL_INST(sbc);
DECL_INST(sbc_dec);
DECL_INST(sec);
DECL_INST(sed);
DECL_INST(sei);

View File

@ -15,6 +15,11 @@
*/
DEFINE_INST(adc)
{
if (cpu->P & MOS_DECIMAL) {
mos6502_handle_adc_dec(cpu, oper);
return;
}
MOS_CARRY_BIT();
SET_RESULT(cpu->A + oper + carry);
@ -23,6 +28,58 @@ DEFINE_INST(adc)
cpu->A = result & 0xff;
}
/*
* Add a number to the accumulator using Binary-Coded Decimal, or BCD.
* Some things still work the same--for instance, if carry is high, we
* will still add one to A+M. The V flag doesn't make any sense in BCD,
* and apparently ADC's effects on V in decimal mode is "undocumented"?
* The N flag is also a bit odd, and the general wisdom seems to be that
* you should use multiple bytes if you want to represent a negative,
* and just use the sign bit on the MSB.
*/
DEFINE_INST(adc_dec)
{
MOS_CARRY_BIT();
// Determine the most and least siginificant digits of A and oper.
int a_msd = cpu->A >> 4,
a_lsd = cpu->A & 0xf,
o_msd = oper >> 4,
o_lsd = oper & 0xf;
// If any of these are greater than 9, then they are invalid BCD
// values, and we give up.
if (a_msd > 9 || a_lsd > 9 || o_msd > 9 || o_lsd > 9) {
return;
}
// Sum is built using the decimal senses of the msd/lsd variables;
// carry is also a factor.
int sum =
((a_msd * 10) + a_lsd) +
((o_msd * 10) + o_lsd) + carry;
// But ultimately, one byte can only hold $00 - $99
int modsum = sum % 100;
// And the final result has to be ported back into a "hexadecimal"
// number; you see, BCD values are not just literally decimal, they
// are decimal in hexadecimal form.
SET_RESULT(((modsum / 10) << 4) | (modsum % 10));
// Because the rules for carry is a bit different, we'll handle it
// here.
cpu->P &= ~MOS_CARRY;
if (sum > 100) {
cpu->P |= MOS_CARRY;
}
// We'll check zero, but not negative or overflow.
mos6502_modify_status(cpu, MOS_ZERO, cpu->A, sum);
cpu->A = result;
}
/*
* The cmp instruction will consider the difference of the accumulator
* minus the operand. It will then set the zero, negative, or carry bits
@ -145,9 +202,74 @@ DEFINE_INST(iny)
*/
DEFINE_INST(sbc)
{
// Jump into the binary coded decimal world with us! It's... funky!
if (cpu->P & MOS_DECIMAL) {
mos6502_handle_sbc_dec(cpu, oper);
return;
}
MOS_CARRY_BIT();
SET_RESULT(cpu->A - oper - carry);
mos6502_modify_status(cpu, MOS_NVZC, cpu->A, result);
// Carry is handled slightly differently in SBC; it's set if the
// value is non-negative, and unset if negative. (It's essentially a
// mirror of the N flag in that sense.)
cpu->P |= MOS_CARRY;
if (result >= 0x80) {
cpu->P &= ~MOS_CARRY;
}
mos6502_modify_status(cpu, MOS_NVZ, cpu->A, result);
cpu->A = result & 0xff;
}
/*
* Pretty similar to the code we're doing in adc_dec; we are here
* performing a subtraction in binary coded decimal.
*
* Note: a lot of this code is lifted from adc_dec; it's probably the
* only other time we will need to use this specific code, so I'm doing
* the Martin Fowler thing and waiting for a third occasion to arise
* before refactoring this into its own function.
*/
DEFINE_INST(sbc_dec)
{
MOS_CARRY_BIT();
// Determine the most and least siginificant digits of A and oper.
int a_msd = cpu->A >> 4,
a_lsd = cpu->A & 0xf,
o_msd = oper >> 4,
o_lsd = oper & 0xf;
// If any of these are greater than 9, then they are invalid BCD
// values, and we give up.
if (a_msd > 9 || a_lsd > 9 || o_msd > 9 || o_lsd > 9) {
return;
}
// Sum is built using the decimal senses of the msd/lsd variables;
// carry is also a factor.
int diff =
((a_msd * 10) + a_lsd) -
((o_msd * 10) + o_lsd) - carry;
// Force C to high to begin with
cpu->P |= MOS_CARRY;
// If diff is negative, we need to "overflow" it back to a
// positive number by adding 100. We also need to unset the C flag.
if (diff < 0) {
diff += 100;
cpu->P &= ~MOS_CARRY;
}
// And the final result has to be ported back into a "hexadecimal"
// number; you see, BCD values are not just literally decimal, they
// are decimal in hexadecimal form.
SET_RESULT(((diff / 10) << 4) | (diff % 10));
mos6502_modify_status(cpu, MOS_ZERO, cpu->A, result);
cpu->A = result;
}

View File

@ -17,6 +17,19 @@ Test(mos6502_arith, adc)
cr_assert_eq(cpu->A, 73);
}
Test(mos6502_arith, adc_dec)
{
cpu->P &= ~MOS_CARRY;
cpu->A = 0x05;
mos6502_handle_adc_dec(cpu, 0x10);
cr_assert_eq(cpu->A, 0x15);
// Test that A + M + 1 works for carry
cpu->P |= MOS_CARRY;
mos6502_handle_adc_dec(cpu, 0x13);
cr_assert_eq(cpu->A, 0x29);
}
Test(mos6502_arith, cmp)
{
cpu->A = 5;
@ -123,3 +136,16 @@ Test(mos6502_arith, sbc)
mos6502_handle_sbc(cpu, 8);
cr_assert_eq(cpu->A, 8);
}
Test(mos6502_arith, sbc_dec)
{
cpu->P = 0;
cpu->A = 0x15;
mos6502_handle_sbc_dec(cpu, 0x6);
cr_assert_eq(cpu->A, 0x9);
cpu->P |= MOS_CARRY;
cpu->A = 0x12;
mos6502_handle_sbc_dec(cpu, 0x2);
cr_assert_eq(cpu->A, 0x9);
}