erc-c/src/mos6502/arith.c

325 lines
7.8 KiB
C

/*
* mos6502.arith.c
*
* We define here the logic for arithmetic instructions for the MOS 6502
* processor.
*/
#include "mos6502/mos6502.h"
#include "mos6502/enums.h"
/*
* The adc instruction will add a number to the accumulator, "with
* carry". If the carry bit is set, we will add 1 to the accumulator as
* an after-effect.
*/
DEFINE_INST(adc)
{
if (cpu->P & MOS_DECIMAL) {
mos6502_handle_adc_dec(cpu, oper);
return;
}
MOS_CARRY_BIT();
vm_8bit result = cpu->A + oper + carry;
MOS_CHECK_NZ(result);
cpu->P &= ~MOS_OVERFLOW;
if (((cpu->A ^ oper) & 0x80) &&
((cpu->A ^ result) & 0x80)
) {
cpu->P |= MOS_OVERFLOW;
}
// Carry has different meanings in different contexts... in ADC,
// carry is set if the result requires a ninth bit (the carry bit!)
// to be set.
cpu->P &= ~MOS_CARRY;
if ((cpu->A + oper + carry) > 0xff) {
cpu->P |= MOS_CARRY;
}
cpu->A = result;
}
/*
* 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.
vm_8bit result = ((modsum / 10) << 4) | (modsum % 10);
cpu->P &= ~MOS_OVERFLOW;
if (((cpu->A ^ sum) & 0x80) &&
((cpu->A ^ result) & 0x80)
) {
cpu->P |= MOS_OVERFLOW;
}
// As you can see, decimal comports a different meaning for the
// carry bit than its binary version
cpu->P &= ~MOS_CARRY;
if (sum > 100) {
cpu->P |= MOS_CARRY;
}
MOS_CHECK_Z(result);
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
* based upon that difference. _The accumulator is neither modified nor
* harmed by this operation._ (We have trained experts on the set to
* monitor the health of the accumulator, whom we've named George.)
*/
DEFINE_INST(cmp)
{
MOS_CHECK_NZ(cpu->A - oper);
// With CMP, carry is set if the difference between A and oper is
// not zero.
cpu->P &= ~MOS_CARRY;
if (cpu->A >= oper) {
cpu->P |= MOS_CARRY;
}
}
/*
* This instruction is functionally identical to CMP, with the exception
* that it considers the X register rather than the accumulator.
*/
DEFINE_INST(cpx)
{
MOS_CHECK_NZ(cpu->X - oper);
cpu->P &= ~MOS_CARRY;
if (cpu->X >= oper) {
cpu->P |= MOS_CARRY;
}
}
/*
* Again, this is a variant of the CMP instruction, except that it works
* with the Y register.
*/
DEFINE_INST(cpy)
{
MOS_CHECK_NZ(cpu->Y - oper);
cpu->P &= ~MOS_CARRY;
if (cpu->Y >= oper) {
cpu->P |= MOS_CARRY;
}
}
/*
* Here we will decrement the value at the effective address in memory
* by 1.
*/
DEFINE_INST(dec)
{
oper--;
MOS_CHECK_NZ(oper);
if (cpu->addr_mode == ACC) {
cpu->A = oper;
return;
}
mos6502_set(cpu, cpu->eff_addr, oper);
}
/*
* In contrast, this does directly decrement the X register.
*/
DEFINE_INST(dex)
{
oper--;
MOS_CHECK_NZ(oper);
cpu->X = oper;
}
/*
* And, again, here we decrement the Y register.
*/
DEFINE_INST(dey)
{
oper--;
MOS_CHECK_NZ(oper);
cpu->Y = oper;
}
/*
* The INC instruction is basically the same as the DEC one.
*/
DEFINE_INST(inc)
{
oper++;
MOS_CHECK_NZ(oper);
if (cpu->addr_mode == ACC) {
cpu->A = oper;
return;
}
mos6502_set(cpu, cpu->eff_addr, oper);
}
/*
* See DEX.
*/
DEFINE_INST(inx)
{
oper++;
MOS_CHECK_NZ(oper);
cpu->X = oper;
}
/*
* See DEY.
*/
DEFINE_INST(iny)
{
oper++;
MOS_CHECK_NZ(oper);
cpu->Y = oper;
}
/*
* This instruction will subtract the operand from the accumulator,
* again, "with carry". In this context, that means that if the carry
* bit is set, then we will subtract 1 again from the A register after
* the operand is subtracted. The result is stored in the accumulator.
*/
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();
int result32 = cpu->A - oper - (carry ? 0 : 1);
vm_8bit result8 = cpu->A - oper - (carry ? 0 : 1);
MOS_CHECK_Z(result8);
// 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;
cpu->P &= ~MOS_NEGATIVE;
if (result32 < 0) {
cpu->P &= ~MOS_CARRY;
cpu->P |= MOS_NEGATIVE;
}
cpu->P &= ~MOS_OVERFLOW;
if (((cpu->A ^ oper) & 0x80) &&
((cpu->A ^ result8) & 0x80)
) {
cpu->P |= MOS_OVERFLOW;
}
cpu->A = result8;
}
/*
* 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 ? 0 : 1);
// 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.
vm_8bit result = ((diff / 10) << 4) | (diff % 10);
MOS_CHECK_Z(result);
// This is a bit of an odd sequence... but overflow in decimal is a
// bit odd to begin with. I ended up replicating the behavior of
// AppleWin here.
cpu->P &= ~MOS_OVERFLOW;
if (((cpu->A ^ diff) & 0x80) &&
((cpu->A ^ result) & 0x80)
) {
cpu->P |= MOS_OVERFLOW;
}
cpu->A = result;
}