11 March 2016 - Forum Rules

Main Menu

Emulate 16-bit ADC on a modern system

Started by Sinthet, December 27, 2012, 01:27:38 AM

Previous topic - Next topic


So, I was wondering if anyone knew a good method of emulating the behaviour the SNES CPU instruction ADC. The concept is simple enough, add 1 to compensate for overflows. However, the details of implementing this are escaping me.

Basically, I'm adding a ton of 2 byte words together and need a method to emulate ADC. I'm using two short ints (One to store the answer, one to read in the value to add). I'm doing this in C/C++, but any general info, code, etc is also much appreciated.

Its one of those "its 1 in the morning and this is driving me freaking mad" moments.  :banghead:


ADC means "add with carry." If the carry flag is set, you add 1 to the result and reset the carry flag.
In the event of a firestorm, the salad bar will remain open.


The human-readable version is on page 327 (or for those of us who prefer to search instead of finding the page: "Add the data located at the effective address"):

This is implemented (for 16-bit words) in bsnes v091 (bsnes/processor/r65816/algorithms.cpp) like so:
inline void R65816::op_adc_w() {
  int result;

  if(!regs.p.d) {
    result = regs.a.w + rd.w + regs.p.c;
  } else {
    result = (regs.a.w & 0x000f) + (rd.w & 0x000f) + (regs.p.c <<  0);
    if(result > 0x0009) result += 0x0006;
    regs.p.c = result > 0x000f;
    result = (regs.a.w & 0x00f0) + (rd.w & 0x00f0) + (regs.p.c <<  4) + (result & 0x000f);
    if(result > 0x009f) result += 0x0060;
    regs.p.c = result > 0x00ff;
    result = (regs.a.w & 0x0f00) + (rd.w & 0x0f00) + (regs.p.c <<  8) + (result & 0x00ff);
    if(result > 0x09ff) result += 0x0600;
    regs.p.c = result > 0x0fff;
    result = (regs.a.w & 0xf000) + (rd.w & 0xf000) + (regs.p.c << 12) + (result & 0x0fff);

  regs.p.v = ~(regs.a.w ^ rd.w) & (regs.a.w ^ result) & 0x8000;
  if(regs.p.d && result > 0x9fff) result += 0x6000;
  regs.p.c = result > 0xffff;
  regs.p.n = result & 0x8000;
  regs.p.z = (uint16_t)result == 0;

  regs.a.w = result;
we are in a horrible and deadly danger


The stuff in the else branch is executed when the CPU's decimal flag is set, which I've never seen used by SNES games. So unless you're actually working on a real emulator you can probably ignore it and the three most important lines are

result = regs.a.w + rd.w + regs.p.c;
regs.p.c = result > 0xffff;
regs.a.w = result; // implicit cast to 16-bit integer here


So, I've tried to simply re-implement what bsnes is doing, and I'm getting the same, incorrect values that I was getting with my original approach (Before I started throwing in a few hackish partial fixes which didn't actually work in the long run...).

Here is my relevant code.

  int checksum;
  int16_t aw;
  int16_t rw;
  bool carry = false;
  for (int i=0;i < 300; i++) {
    checksum = aw + rw + carry;
    std::cout << "Checksum: " << std::hex << checksum << "\n";
    carry = checksum > 0xffff;
    aw = checksum;

carry = checksum > 0xffff
Never seems to return true, however. If anyone is wondering, I'm revisiting my toy Final Fantasy 5 SRAM checksum fixer that I posted a question about almost a year ago (!). Here's a bit of a relevant trace.

$C2/F59B 7D 00 00    ADC $0000,x[$30:6028]   A:F290 X:6028 Y:05D8 P:envmxdIzc
$C2/F59E E8          INX                     A:0BAC X:6028 Y:05D8 P:envmxdIzC
$C2/F59F E8          INX                     A:0BAC X:6029 Y:05D8 P:envmxdIzC
$C2/F5A0 88          DEY                     A:0BAC X:602A Y:05D8 P:envmxdIzC
$C2/F5A1 88          DEY                     A:0BAC X:602A Y:05D7 P:envmxdIzC
$C2/F5A2 D0 F7       BNE $F7    [$F59B]      A:0BAC X:602A Y:05D6 P:envmxdIzC
$C2/F59B 7D 00 00    ADC $0000,x[$30:602A]   A:0BAC X:602A Y:05D6 P:envmxdIzC
$C2/F59E E8          INX                     A:24C8 X:602A Y:05D6 P:envmxdIzc
$C2/F59F E8          INX                     A:24C8 X:602B Y:05D6 P:envmxdIzc
$C2/F5A0 88          DEY                     A:24C8 X:602C Y:05D6 P:envmxdIzc
$C2/F5A1 88          DEY                     A:24C8 X:602C Y:05D5 P:envmxdIzc
$C2/F5A2 D0 F7       BNE $F7    [$F59B]      A:24C8 X:602C Y:05D4 P:envmxdIzc

My for loop runs 300 times because "Y" starts at 600, and counts down to 0, with "Y" being decremented twice every time. Each time I read 2 bytes, and everything seems to be fine there. LostTemplar seems to be correct that I can ignore all the code in the "else" clause (in the bsnes code), since the only Processor flag ever raised here is Carry.

I'm guessing I'm messing up a type somewhere, or else overlooking a small detail. Anyone have any ideas?

Just btw...
uname -p -i -o -v
#55-Ubuntu SMP Wed Dec 5 17:42:16 UTC 2012 x86_64 x86_64 GNU/Linux


Edit:  actually, I misread the code there.


It will never be true because aw and rw are signed 16-bit values, so the maximum value would be 0x7fff+0x7fff=0xfffe (everything greater than 0x8000 will be treated as negative). Try using uint16_t instead.


Utterly irrelevant I know but I had a "you know you have been pondering electronics too much" moment when I clicked on the topic and thought it might be a discussion about analogue to digital conversion.


Quote from: LostTemplar on December 27, 2012, 12:54:03 PM
It will never be true because aw and rw are signed 16-bit values, so the maximum value would be 0x7fff+0x7fff=0xfffe (everything greater than 0x8000 will be treated as negative). Try using uint16_t instead.
Wouldn't checksum have to be a long int (at least 17 bits) for checksum > 0xFFFF to be true?
"My watch says 30 chickens" Google, 2018


we are in a horrible and deadly danger


So... turns out, I did miss a small detail. We count down from 600 to 0 in hex, not decimal. So, my loop should actually run 767 times, not just 300 times.
I also decided to explicitly define stuff instead of relying on int/short, since they can indeed vary between systems and compilers.

Thanks for the help and suggestions everyone!  Much appreciated  :beer: