Arithmetics & boolean algebra
Adding, larger adding, and more adding
Let's talk a bit more about
add, shall we? It actually comes in two flavors: the
add a, X we just saw, which does 8-bit addition, but rumors speak of a mythical 16-bit variant:
add hl, X. This one has two major differences: one, only the Z flag is updated; two, unlike the 8-bit flavor, X cannot be an immediate value!
There exists an instruction that is basically
add but with an added twist:
adc. This instruction adds with carry. What does it mean? Well, where
add a, X was doing
a = a + X,
adc a, X does
a = a + X + carry (either 0 or 1). If you're wondering how that's useful, we're going to use it to emulate
add hl, $XXXX!
; add hl, $1337 ; Note: destroys A ld a, l add a, $37 ; Process the low 8 bits... ld l, a ; Store back ld a, h adc a, $13 ; Process the upper 8 bits, counting the carry that may have stemmed from the low 8 bits ld h, a
Let's run though this again, but supposing HL = $CAFE.
; hl = $CAFE ld a, l add a, $37 ; A = $37 + $FE = $35, and Carry set ld l, a ; hl = $CA35 ld a, h adc a, $13 ; A = $13 + $CA + 1 (carry set) = $DE ld h, a ; hl = $DE35
If you take your calculator out, you'll notice that this is the correct result! Using
add instead of
adc would have yielded $DD35 instead, which is wrong. Basically, the purpose of
adc is to simplify carrying out multi-byte additions. You saw an application above, here is another.
; Guess what this does? ld a, c add a, [hl] ; Remember, [hl] is basically an 8-bit register, so you CAN do this! ld c, a inc hl ld a, b adc a, [hl] ld b, a dec hl ; That's right -- this is basically `add bc, [hl]`. Neat, huh?
If you remember what I said about endianness, this is basically the reason why little-endian is more practical: you have to process (and therefore read and/or write) the low byte(s) first to be able to ripple the carry to the upper byte(s). Thus, we store the lower byte first. It may be a little awkward at first, but I promise you get used to it.
Oh, and by the way,
adc hl, X doesn't exist. Get used to it -- there's basically just
add hl, X as far as 16-bit operations are concerned. I miss my Z80 extended opcodes... ;-;
The other way around
Okay, great, I can add, but what if I wanted to subtract? Well, as the bold characters may have made you guess,
sub is right there for you! It has the same syntax as
add a, X, and - what a surprise! -
sub hl, X doesn't exist. It also affects the Z and C flags, although it's worth mentioning how C works there.
ld a, 3 ld b, 2 sub b ; A = $01, Z reset, C reset sub b ; A = $FF, Z reset, C set sub $FF ; A = $00, Z set, C reset sub 0 ; A = $00, Z set, C reset
As you can see, C is set when a borrow/underflow occurs. Note that other CPUs, such as the 6502 family, use an inverted meaning - C is reset when a borrow occurs. It's less intuitive (cue a swath of "oops"-level bugs), but it's more practical in some cases.
Oh, and by the way, there's also
sbc a, X. It works the same way as
adc a, X (
a = a - X - carry), and there's no
sbc hl, X, PLEASE STOP INSISTING
When you want to compare two values, you use
cp a, X.
cp is extremely straightforward -- it's
sub, but it doesn't write the result back to
a. If you're wondering what it's useful for aside for wasting time, you didn't notice that it still affects flags! As such, it's the preferred operator for, well, comparisons.
The cool part about
cp is that you can test both equality cases (==, !=) and two inequality cases (<, >=) using only one
cp. Don't believe me?
ld a, 42 cp 10 ; 42 - 10 = 32, C reset (42 >= 10), Z reset (42 != 10) ; Notice how you can chain comparisons? That comes in handy at times cp 57 ; 42 - 57 = 241, C set (42 < 57) , Z reset (42 != 57) cp 42 ; 42 - 42 = 0, C reset (42 >= 42), Z set (42 == 42)
Tough luck, the GBz80 does not have any multiplication or division capabilities. You can be clever about multiplication, though, using one simple trick:
add a, a basically multiplies
a by two! (This makes multiplying by powers of two much more efficient, btw). You could also multiply by 6 using this:
ld b, a add a, a ; *2 add a, b ; *3 add a, a ; *6
We will program, in a section further up, a function that performs multiplication. Don't worry.
Now, we can add and subtract. But we're dealing with bits, too, and maybe I'll want to manipulate them as well? Well, boolean algebra has got you covered. There are three operations that you need to know about:
Let's begin by what they have in common. They all operate on the
a register, basically like
add a, X. Each of these operations work bit by bit -- they're binary operations, not logical operations. All three of them reset the C flag, always, and modify the Z flag as you'd expect.
This operation takes each bit of both operands, and sets the corresponding bit of the result if and only if both bits are set.
ld a, %01010011 ld b, %00110101 and a, b ; A %0101 0011 ; B %0011 0101 ; ---------- ; A and B %0001 0001 ; Now, a = $11
This is pretty useful for "masking out" (resetting) some flags/bits, and to test if some bits are zero (since the Z flag will be set accordingly).
This operation takes each bit of both operands, and sets the corresponding bit of the result if and only if any of both bits is set.
ld a, %01010011 ld b, %00110101 or a, b ; A %0101 0011 ; B %0011 0101 ; ---------- ; A or B %0111 0111 ; Now, a = $77
This is pretty useful for setting some flags/bits. This is used less frequently than
and, but still.
This operation takes each bit of both operands, and sets the corresponding bit of the result if and only if both bits differ.
ld a, %01010011 ld b, %00110101 xor a, b ; A %0101 0011 ; B %0011 0101 ; ---------- ; A xor B %0110 0110 ; Now, a = $66
This is pretty useful to invert some flags/bits. This is heavily used instead of counters for "flip-flop" states (now you know why a lot of games have two-frame animations!).
Remember how I quickly skimmed over the H and N flags? Let's talk about them. But first, we need to talk about
parallel universes what BCD is. When you store a number, say 42, you'd usually store it raw -- 42, 0x2A, %00101010. BCD is storing each digit in each nibble -- $42, %01000010. It is less efficient, but much more practical for decimal displaying (instead of dividing by 10, you
and a, $0F). Therefore, BCD is better for displaying, but not for calculating: $59 + $01 = $60 and not $5A.
Luckily, there's an instruction that does just that:
daa performs decimal adjustment on the accumulator (the
a) register. It acts depending on the H, N and C flags, and the accumulator's contents. On a surface level, it basically adjusts the contents of the
a register to match the result of the BCD operation.
ld a, $39 add a, $01 ; A = $3A daa ; Perform M.A.G.I.C ; A = $40
Let it be known that
daa may set the C flag (for example if A was $99 in the example above instead of $39), and alters Z and H. If you don't care about how it works, you can safely skip the rest of this section. But if you want the nitty-gritty, then here we go.
First, let's describe how the H and N flags work. H is the half-carry, and it works exactly the same way as the carry (even depending on the instruction!), but taking only the lower 4 bits into account. So, for example, when adding $08 to $39, H is set, because there's is an overflow in the low 4 bits (they go from $9 to $1). N tells whether the previous operation was "positive" (flag reset) or "negative" (flag set). For example,
add a, X resets that flag, and
sub a, X sets it. This is because the adjusting depends on the last operation that occurred.
If H is set or the lower 4 bits of A contain a non-BCD digit ($A-$F), then an implicit
add a, $06 is performed (this includes modifying the C flag). Then if C is set or the upper 4 bits of A contain a non-BCD digit, an implicit
add a, $60 is performed, including modifying the C flag. (Note: I believe the H flag isn't modified by this second
add, but I'm not sure.) But, if the N flag was set, instead of
add, the operations performed are instead
subs. (Note: I think that's how it's done, but I'm unsure.)