Difference between revisions of "Add 8-bit to 16-bit"

From GbdevWiki
Jump to: navigation, search
(Adapted from Sik's tutorial at https://plutiedev.com/z80-add-8bit-to-16bit, with permission)
 
m (Signed subtraction: rewrite unfinished sentence in comment)
Line 80: Line 80:
 
     ; Values $00-$7F mean subtract $0000-$007F or add $0000-$FF81
 
     ; Values $00-$7F mean subtract $0000-$007F or add $0000-$FF81
 
     ; Values $80-$FF mean subtract $FF80-$FFFF or add $0080-$0001
 
     ; Values $80-$FF mean subtract $FF80-$FFFF or add $0080-$0001
     ; First negate A (since HL - A is the same as HL + -A)
+
     ; First begin to negate A (since HL - A is the same as HL + -A)
     ; But don't increment A yet because we want to handle
+
     ; A will be incremented later.
 
     cpl
 
     cpl
 
      
 
      

Revision as of 11:03, 21 September 2019

Sometimes you need to add an 8-bit value (in A) to a 16-bit one (in either HL, BC or DE). You can extend the value to 16-bit, but it can get cumbersome as you may need to clobber registers you're using and possibly move values around (since only HL can be a destination).

Here we show how to do this without resorting to using 16-bit operations (which can avoid all those issues). Note that here "unsigned" and "signed" refers to the 8-bit value in A.

Some notes:

  • In all these cases, only A gets clobbered.
  • In all these cases, you can replace HL with BC or DE.
  • You really should consider wrapping these into macros.

Unsigned addition

Adding A to HL is kind of tricky (you'll probably understand it better from the comments in the code snippet below). We first add the low byte. Then we add the high byte with carry and substract the low byte to get the high byte's result (which is high byte + carry).

We'll build on the code below for the other methods.

   add   a, l    ; A = A+L
   ld    l, a    ; L = A+L
   adc   a, h    ; A = A+L+H+carry
   sub   l       ; A = H+carry
   ld    h, a    ; H = H+carry

Signed addition

Signed addition is similar, except we "extend" A to 16-bit. The "high byte" of A is always either 0 (if positive) or -1 (if negative), so we check A's sign and adjust H accordingly. After this we add the lower byte as with unsigned addition.

   ; If A is negative, we need to subtract $100 from HL
   ; (since A's "upper byte" is $FF00)
   cp    $80
   jr    c, .positive
   dec   h
.positive:
   
   ; Then do addition as usual
   ; (to handle the "lower byte")
   add   a, l
   ld    l, a
   adc   a, h
   sub   l
   ld    h, a

This is longer than the traditional way to do this (but saves a register pair):

   ld   e, a
   rla          ; Bit 7 into carry
   sbc  a, a    ; $00 if no carry, $FF if carry
   ld   d, a
   add  hl, de

Unsigned subtraction

The idea behind this is that HL - A is the same thing as HL + (-A), so we flip A's sign then perform an addition. The catch is that A's "upper byte" becomes -1, so we need to subtract 1 from HL's upper byte to account for this. Think of this as a specialized variant of signed addition.

The exception is when A is 0 (which when negated is still 0). Substracting 0 is the same as doing nothing, so we just skip the whole ordeal in that case.

The CPL instruction calculates $FF - A, and INC to add 1 afterward makes it $100 - A.

   ; Flip A's sign.  If A is zero, do nothing.
   cpl
   inc   a
   jr    z, .skip
   
   ; Subtracting $0001 through $00FF is like adding
   ; $FFFF through $FF01 to A.  So subtract 1 from H.
   dec   h
   
   ; Now add the low byte as usual.  Two's complement
   ; takes care of ensuring the result is correct.
   add   a, l
   ld    l, a
   adc   a, h
   sub   l
   ld    h, a
.skip:

Signed subtraction

Signed subtraction is trivial to build on top of signed addition: flip A's sign then do the addition. The only catch is that we need to handle that -$80 should become +$80 (thankfully, the overflow flag gets set when trying to flip its sign, so we can handle that particular case easily).

   ; Values $00-$7F mean subtract $0000-$007F or add $0000-$FF81
   ; Values $80-$FF mean subtract $FF80-$FFFF or add $0080-$0001
   ; First begin to negate A (since HL - A is the same as HL + -A)
   ; A will be incremented later.
   cpl
   
   ; Values $00-$7F mean add $0001-$0080
   ; Values $80-$FF mean add $FF81-$0000
   ; So if value at least $80, add $FF00
   cp    $80
   jr    c, .positive
   dec   h
   scf
.positive:
   
   ; Now add A + 1 to HL
   adc   a, l
   ld    l, a
   adc   a, h
   sub   l
   ld    h, a

With 16-bit operations

For the sake of completeness, since if the stars align right somehow (i.e. values happen to be in the right registers) these may be more useful. Also they're more compact, if space usage matters more than speed in a particular case. Some are derived from how to extend 8-bit values to 16-bit.

Sik finds these not ideal for several reasons:

  • Destination must be HL, which is more important on 8080/SM83 than on Z80 because of its use to step through an array.
  • You must clobber BC or DE.
  • 8080 and SM83 lack 16-bit subtraction, having only addition. (Even though Z80 adds sbc hl, there is no dedicated opcode for sub hl.)

Unsigned addition:

   ld    e, a    ; DE = A
   ld    d, 0
   add   hl, de  ; HL = HL+DE

Signed addition:

   ld    e, a    ; DE = A
   add   a, a    ; Bit 7 into carry
   sbc   a, a    ; $00 if no carry, $FF if carry
   ld    d, a
   add   hl, de  ; HL = HL+DE

Unsigned subtraction: To be written

Signed substraction: To be written

External links