mirror of
https://github.com/pevans/erc-c.git
synced 2024-10-03 22:54:42 +00:00
8e810e724f
This makes testing _slightly_ easier, because now the handlers require a type of state in the cpu vs. a specific opcode state in the segment that we execute from. (The latter being just more complex to work with and require in testing.)
319 lines
7.8 KiB
C
319 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)
|
|
{
|
|
if (cpu->addr_mode == ACC) {
|
|
MOS_CHECK_NZ(cpu->A - 1);
|
|
cpu->A--;
|
|
return;
|
|
}
|
|
|
|
MOS_CHECK_NZ(oper - 1);
|
|
mos6502_set(cpu, cpu->eff_addr, oper - 1);
|
|
}
|
|
|
|
/*
|
|
* In contrast, this does directly decrement the X register.
|
|
*/
|
|
DEFINE_INST(dex)
|
|
{
|
|
MOS_CHECK_NZ(cpu->X - 1);
|
|
cpu->X--;
|
|
}
|
|
|
|
/*
|
|
* And, again, here we decrement the Y register.
|
|
*/
|
|
DEFINE_INST(dey)
|
|
{
|
|
MOS_CHECK_NZ(cpu->Y - 1);
|
|
cpu->Y--;
|
|
}
|
|
|
|
/*
|
|
* The INC instruction is basically the same as the DEC one.
|
|
*/
|
|
DEFINE_INST(inc)
|
|
{
|
|
if (cpu->addr_mode == ACC) {
|
|
MOS_CHECK_NZ(cpu->A + 1);
|
|
cpu->A++;
|
|
return;
|
|
}
|
|
|
|
MOS_CHECK_NZ(oper + 1);
|
|
mos6502_set(cpu, cpu->eff_addr, oper + 1);
|
|
}
|
|
|
|
/*
|
|
* See DEX.
|
|
*/
|
|
DEFINE_INST(inx)
|
|
{
|
|
MOS_CHECK_NZ(cpu->X + 1);
|
|
cpu->X++;
|
|
}
|
|
|
|
/*
|
|
* See DEY.
|
|
*/
|
|
DEFINE_INST(iny)
|
|
{
|
|
MOS_CHECK_NZ(cpu->Y + 1);
|
|
cpu->Y++;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
}
|