Demystifying the GameBoy/SM83’s DAA Instruction
As an off-and-on side project, I’m working on a GameBoy emulator. I definitely still have a long way to go (still working on the CPU!) but I wanted to share a detail that I felt deserved a more in-depth explanation: the DAA (“Decimal Adjust Accumulator”) instruction. This instruction is notorious for being a bit difficult to wrap your head around, but there is seemingly very little written about it.
In principle, this instruction is quite simple; if we perform an arithmetic instruction with two binary coded decimal (BCD) numbers, we should be able to get an output that is also in BCD. Of course, this raises a natural question…
What the Heck is Binary Coded Decimal?
Binary Coded Decimal is quite literal once you learn what it is; it’s just a decimal number whose digits are encoded in binary. While that’s a bit of an obtuse explanation, it really is that literal. This is likely best illustrated through an example.
Let’s encode the number 42 in BCD. The GameBoy uses four bits to represent each
digit, so we will use that here. In a four-bit representation, the digit 2 can
be written as 0010
, and the digit 4 can be represented as 0100
. Put
together, this means 42 in BCD is simply 0100 0010
, or 0x42
in hexadecimal.
Of course, this can be extended to as many digits as you’d like, but we’re going
to stick to two-digit numbers here, as this is all we can fit in an eight bit
register on the GameBoy.
While this representation is certainly simple on paper, it is a bit limiting. Though a single byte can be used to represent any number from 0 through 255, a single byte of BCD can only represent 0 through 99, since we only have two four-bit “slots” to store our decimal digits (which can only be 0-9)1. Given this loss, one might wonder why anyone would use this. While inefficient, BCD is actually quite useful when displaying numbers, as you can display the digits themselves with no additional logic. You can even see it used for this if you go digging through a disassembly of Tetris.
Adding Binary Coded Decimal Numbers
We’re on our way to understanding DAA, but before we get there, we need to learn to do some arithmetic with BCD. Going forward, I’m going to use hexadecimal for BCD, but the math will work the same in binary, as well.
Let’s add the BCD numbers 0x42
and 0x35
. This is quite simple, we just need
to add the digits and don’t carry anything.
0x42
+ 0x35
------
0x77
Our result, in BCD, is 77, and this matches what we might expect. Things get a little more complicated if we add BCD numbers that force us stretch beyond our decimal limits, though.
0x42
+ 0x29
------
0x6B
Eek! Our output is not in BCD, one of the digits is more than 9! In general, the
output of these operations is relatively meaningless; adding the decimal numbers
42 and 29 nets 71, and adding the hex numbers 0x42
+ 0x29
is decimal 107
(0x6B), which is certainly not the same number. Instead, we must perform an
adjustment to correct it. It turns out, when the unit digit is greater than
nine, we can correct it by simply adding six to our result. Indeed, 0x6B
+
0x06
equals 0x71
, which is the BCD result we want! This conversion from
these “hex-ish” numbers to BCD is actually precisely what the DAA instruction
does to our A register (the “accumulator”).
This offset of six might seem a bit random, but it’s actually derived from the difference between the highest numbers representable in bases 10 and 16 (9 and 15/F). A hand-wavy way of looking at it is that we must “move” the excess over nine into the next digit by adding this difference.
Let’s try another example to demonstrate the point.
0x54
+ 0x28
------
0x7C
We have a digit greater than nine, so we add six…
0x7C
+ 0x06
------
0x82
and there you have it. 54 + 28 is indeed, 82, and we have a BCD representation.
Correcting an Error in the Tens Place
While this strategy works great in the ones place, it breaks down a bit if we
attempt to apply it to the tens place. Let’s add 0x54
and 0x60
.
0x54
+ 0x70
------
0xC4
If we add six to the tens place (i.e. add 0x60
), we will end up with 0x24
(plus a carry). This certainly seems right; decimal 54 plus 70 equals 124, and
since we’re limited to two digits, 24 is a correct result. However, the rule is
slightly more complicated than just “if the tens place is greater than nine,
add 0x60
”. Consider the following example:
0x98
+ 0x04
------
0x9C
If we follow our two rules above, we would simply add 0x06
, since the ones
place is greater than nine.
0x9C
+ 0x06
------
0xA2
That’s certainly not BCD! However, if we apply our second rule to this result, we will get a correct result!
0xA2
+ 0x60
------
0x02 (plus a carry)
98 + 4 is 102, which wraps around past 99 to 2. It would be a bit inconvenient
to have to check your result twice, though, so you’ll often see this written as
“if DAA’s argument (the A register) is greater than 0x99
, add 0x60
”. This
is equivalent to the above, though in perhaps a roundabout way. Really what
we’re doing is ensuring that the two-digit number fits within our BCD
representation, and adding 0x60
to if it doesn’t; anything greater than
0x99
can’t fit in BCD, but we can push the “extra data” to the next place by
adding the difference between the value of place in our two bases, 0x60
. If
you think about it, this actually neatly fits in line with our first rule; in
that rule, we’re checking if the single-digit result (i.e. ignoring the top
nibble) fits in BCD by checking if the value is greater than 0x09
. This is
exactly the same principle, just applied to both numeric places2.
Carry Bits
I have neglected one piece so far: carries. Consider the following BCD addition:
1
0x_90
+ 0x_80
-------
0x110
If we are simply performing eight-bit arithmetic (as we are on the GameBoy),
this will wrap to 0x10
, and we will have the carry flag set. We can use our
same “greater than 0x99
” rule as above, here. The presence of a carry bit
indicates that the result must be greater than 0x99
, as our third place
would be the hundreds place. As such, we should also add 0x60
here.
0x10 # ignoring the hundreds place, since our adding wrapped
+ 0x60
-------
0x70
This is correct! 90 + 80 is 170, and since we wrapped around, 0x70
is the BCD
result we expect.
For the exact same reason, the same applies to our weird friend, the half-carry flag.
1
0x09
+ 0x08
-------
0x11
We performed a half-carry, so we must add 0x06
0x11
+ 0x06
-------
0x17
This matches our expectation; 9 plus 8 is 17, which matches our BCD result.
Let’s codify what we have so far.
fn get_daa_value(registers: &Registers) -> u8 {
let mut offset = 0_u8;
let a_value = registers
.get_single_8bit_register(register::SingleEightBit::A);
let half_carry = registers.get_flag_bit(register::Flag::HalfCarry);
let carry = registers.get_flag_bit(register::Flag::Carry);
if a_value & 0xF > 0x09 || half_carry == 1 {
offset |= 0x06;
}
if a_value > 0x99 || carry == 1 {
offset |= 0x60;
}
a_value.wrapping_add(offset)
}
You’ll notice that nothing in the above function depends on the original operands; the results stored in our flags are enough to perform the conversion!
Subtracting Binary Coded Decimal Numbers
We’re well on our way to understanding the DAA instruction, but there is one
more thing we must consider: subtraction. Our rules regarding carries work when
subtracting as well, but two caveats apply. Firstly, we will subtract our
offsets instead of adding them. This makes (hand-wavy) sense; just as we wanted
to “move” our extra data up a numeric place with addition (by adding six), here
we want to “move” the extra data down into the lower numeric place (by
subtracting six). When running the DAA instruction, we can simply check the
subtraction flag (N) to determine if a subtraction has just been performed, and
negate our adjustment accordingly. Secondly, we apply these rules to borrows,
rather than carries (which the GameBoy’s H and C registers already store for
us). As an example, let’s subtract 0x13
from 0x20
.
1
0x20
- 0x13
------
0x0D
While performing this subtraction, we needed to borrow from the tens place, so we will apply our half-carry rule, yielding the expected result of 20 - 13, 7.
0x0D
- 0x06
------
0x07
As you might expect, we also apply this rule to our tens place if we borrow from the hundreds place.
1
0x05
- 0x21
------
0xE4
And applying our carry rule…
0xE4
- 0x60
-------
0x84
This matches what one might expect, 5 minus 21 (wrapping from 0 to 99) is 84.
“Wait, That’s Illegal”
While our carry rules apply neatly to borrowing, the idea of a digit being greater than nine is a little messy. Simply, you cannot subtract two BCD numbers and end with a digit greater than nine unless you perform a borrow. You may have to prove this to yourself, but we can’t “grow” a digit in size unless we perform a borrow.
Unfortunately, there are no restrictions on when the DAA instruction is run; we
may have not performed a BCD subtraction beforehand. Consider a state where
register A is 0xF0
, and only the subtraction flag is set, indicating a
subtraction has just been performed. If we blindly apply what we’ve already
learned, we’d subtract 0x60
from this value, and get 0x90
. However, this is
not what the GameBoy actually does. In these cases, it will actually leave the
value in register A alone. We can fix this pretty easily, by only applying our
“digit greater than nine” rule if the subtraction flag is zero.
Let’s codify everything we’ve learned so far. We will only follow our “greater
than 9” rules if the subtraction flag is not set, and will perform a
wrapping_sub
if it is.
fn get_daa_value(registers: &Registers) -> u8 {
let mut offset = 0_u8;
let a_value = registers
.get_single_8bit_register(register::SingleEightBit::A);
let half_carry = registers.get_flag_bit(register::Flag::HalfCarry);
let carry = registers.get_flag_bit(register::Flag::Carry);
let subtract = registers.get_flag_bit(register::Flag::Subtract);
if (subtract == 0 && a_value & 0xF > 0x09) || half_carry == 1 {
offset |= 0x06;
}
if (subtract == 0 && a_value > 0x99) || carry == 1 {
offset |= 0x60;
}
if subtract == 0 {
a_value.wrapping_add(offset)
} else {
a_value.wrapping_sub(offset)
}
}
DAA’s Flags
We’re so close to finishing our DAA implementation, all we need to do now is update our flags. Thankfully, most of this is pretty straight forward.
Flag | Value |
---|---|
Zero | 1 if final value is zero, 0 otherwise |
Subtract | Untouched |
Half-Carry | 0 |
Carry | Set if final BCD value is > 0x99 |
The only slightly confusing one here is the carry flag. This will be set under two conditions
- If a subtraction has not been performed, and the unconverted value is greater
than
0x99
. This indicates that our BCD value is larger than our maximum of 99, and we must indicate that. - If our carry bit was already set. This indicates that our input result was already greater than our maximum of 99, and thus our final BCD value will be as well.
With these in place, we can finish our implementation!
fn run_instruction(processor: &mut Processor, opcode: u8) {
match opcode {
// ...
0x27 => {
use register::SingleEightBit;
use register::Flag;
let registers = &mut processor.registers;
let (result, carry) = get_daa_value(registers);
registers
.set_single_8bit_register(SingleEightBit::A, result);
registers.set_flag_bit(Flag::HalfCarry, 0);
registers.set_flag_bit(Flag::Carry, carry.into());
registers.set_flag_bit(Flag::Zero, (result == 0).into());
}
}
}
fn get_daa_value(registers: &Registers) -> (u8, bool) {
let mut offset = 0_u8;
let mut should_carry = false;
let a_value = registers
.get_single_8bit_register(register::SingleEightBit::A);
let half_carry = registers.get_flag_bit(register::Flag::HalfCarry);
let carry = registers.get_flag_bit(register::Flag::Carry);
let subtract = registers.get_flag_bit(register::Flag::Subtract);
if (subtract == 0 && a_value & 0xF > 0x09) || half_carry == 1 {
offset |= 0x06;
}
if (subtract == 0 && a_value > 0x99) || carry == 1 {
offset |= 0x60;
should_carry = true;
}
let output = if subtract == 0 {
a_value.wrapping_add(offset)
} else {
a_value.wrapping_sub(offset)
};
(output, should_carry)
}
Wrapping it up
Once you can wrap your head around it, the DAA instruction is fairly straight forward. The only tricky bit is that unlike the examples I’ve given above, DAA has no knowledge of the operands given in the original operation. However, by applying a few simple rules, we can convert the result to BCD. To summarize the rules we’ve established above:
- If we’re not subtracting, and the unit digit is greater than nine, we will
add
0x06
to register A. - If we’re not subtracting, and the value is greater than
0x99
, we will add0x60
to register A. - If there was a half carry, we will add
0x06
to register A. - If there was a full carry, we will add
0x60
to register A.
Applying the these rules, and then setting the appropriate flags, you will have a working DAA implementation! This implementation passes the tests provided by the JSMoo project. I want to extend a huge thanks to the maintainers of that project for sharing these tests. Without them, I’d definitely still be scratching my head.
-
If you’re still not convinced, you may be falling into a similar trap as I did. My original implementation checked if
value & 0xF0 > 0x90
. The example I put in this post was carefully chosen. If we had0x9C
in our A register, we would perform0x9C & 0xF0 > 0x90
, which is false, and we would fail to perform a necessary correction! ↩︎