Zachary Pitcher

Ti8x Z80 ASM Project Euler 00b

Printing Hexadecimal and Decimal Numbers

how bcalls work

Texas Instruments provided a lot of useful subroutines, dubbed bcalls, that perform some repetitive or complicated task such as interfacing with the LCD screen. These system routines are stored in ROM, so they can only by accessed by a “callback” of sorts and an interrupt from user memory. If we unpack the bcall() macro in the standard ti83plus.inc include file, we can see that it simply interrupts and places the address of the system routine in code memory right below the interrupting line. The interrupt vector reads off the last two bytes in the stack, which together form the 16-bit address of the line after the interrupt–where the system routine’s address is located. Then it simply dispatches to that routine. So bcalls are pretty powerful, because they mean I don’t have to write as much code and it’s a lot easier to interact with the hardware on the Ti8x because most of those functions are available in some form of bcall.

#define bcall(xxxx) 	rst 28h 	\ .dw xxxx

printing using PutS

One such bcall is _PutS, which prints the ASCII-encoded zero-terminated string stored at the location pointed to by the HL register on the Home Screen of the LCD. Here is an example of its usage:

main:
	bcall(_ClrLCDFull)
	ld hl, 0
	ld (PenCol), hl 	; set cursor to (0,0)
	ld (CurRow), hl 	; the top left corner of the LCD
	ld hl, yolo 			; load address of string into hl
	bcall(_PutS) 			; print the string
	ret

yolo:
.db "#YOLO", 0

printing hexadecimal

There a million places where printing something would be useful, and for the greater purpose of solving and debugging the Project Euler problems, my main use for _PutS will be for printing numbers. But everything in memory is stored as a bunch of bits, which only make sense when you apply some context or format to them. So the problem I need to solve now is a matter of juggling encodings.

When I’m doing calculations, the bits in memory represent numbers, specifically binary numbers stored in 8-bit segments. But the _PutS subroutine assumes that the address it’s reading from is ASCII-encoded. If I ignore this discrepancy and apply _PutS to a number stored in 8-bit binary bytes (assuming none of the bytes are zero, which would cause the printing to stop prematurely), I get a jumbled mess. Therefore, I have to format the string buffer passed to _PutS. But translating the binary directly to ASCII-encoding would be very wasteful because each byte turns into eight ASCII characters (2^8 = 256) instead of one. It’s also hard for humans to interpret binary directly at a glance, so a standard alternative is hexadecimal. With hexadecimal, each 8-bit byte would only expand into two ASCII characters (16^2 = 256), so the number won’t overload the LCD when I try to print large numbers. Furthermore, it’s much easier to get a sense of the scale of a hexadecimal number than a binary number.

Luckily, it’s simple to convert 8-bit binary bytes to hexadecimal by simply splitting the high and low nibbles apart. Each of these nibbles is 4-bits, which is exactly enough to represent a base-16 number (2^4 = 16). Since the distinction between a hexadecimal representation in memory and binary one is just a matter of perspective, we don’t actually need to operate on the bits, but this will help us write the code that makes the hexadecimal printable.

The possible values of a single hex nibble are (0-9,A-F), which should become (30-39,65-70). Therefore, to print one hex nibble, we can add 30 to it to put it into the lower range. If it’s above 39, then it must have been A-F, in which case, we can add 25 to skip the gap between the numeral and alphabetic characters. The following code prints a single hex nibble:

; prints a hex nibble
; @param: 	a 	contains nibble to be printed
; @reg:	a
prtnbl:	
	cp $0a
	jr c, pnbnum	; compare with $A to test if letter or number			
pnbltr:	add a, $07
pnbnum:	add a, $30		; convert to ascii for both cases
	bcall(_PutC)	; print the ascii char in a
	ret
; end prtnbl

Keep in mind, however, that there are two nibbles per location in memory, so these should be split up and translated separately, then placed consecutively in memory. Since each byte turns into two hex nibbles, which need to be printed separately, then a hex number of length n will require a 2n-length buffer for the ASCII string to be printed. The following code prints a length-n hex number whose length is stored in the memory location directly before the number.

; prints data as hex bytes (little-endian)
; @param: 	hl	contains pointer to data to be printed
; @reg:	a,b,c
prthex:	
	dec hl
	ld b, 0
	ld c, (hl)		; c <- length of data in bytes
	add hl, bc 		; jump to end of data
	ld b, c
phxfor:	ld a, (hl)		; a <- byte of data to be printed
	and a, $f0
	rrca
	rrca
	rrca
	rrca		; a <- high nibble
	call prtnbl
	ld a, (hl)
	and a, $0f		; a <- low nibble
	call prtnbl
	dec hl
	djnz phxfor
	ret
; end prthex
	

converting hexadecimal to decimal

I used the double dabble algorithm to convert hex values to binary coded decimal so that I could print the decimal value to the screen. Once it’s written in BCD form, you can read off the bytes and convert to characters to print using the prthex function since the decimal value of each place value in BCD (0-9) is the same as the hex character (0x0 - 0x9).

; converts a hex value to binary coded decimal
; uses double dabble algorithm
; len_h = floor(log16(n)) + 1
; len_d = floor(log10(n)) + 1
; 
; len_d_max = floor(len_h*log10(16)) + 1
; len_d < len_h*5/4 + 1
;
; @param:	hl
; @reg:	a,b,c
hex2bcd:
	dec hl
	ld a, (hl)		; load length of data in bytes
	ld c, a 		; number of iterations of h2dshhx
	
	add a, a
	add a, a
	add a, a		; multiply by 8 for number of bits
	ld b, a 		; number of iterations of h2dfor

	ld a, c 		; len_h
	add a, a 		
	add a, a
	add a, c 		; multiply by 5
	and $fc
	rrca
	rrca 		; divide by 4
	add a, $01
	ld (bcdlen), a
	inc hl

h2dfor:	push bc

	ld hl, valbuf
	ld b, c 		; number of iterations of h2dshhx
h2dshhx:	rl (hl)		; shift the whole thing left one
	inc hl
	djnz h2dshhx

	push af

	ld hl, bcdbuf
	ld b, d		
h2dadd3:	
lonibchk:	ld a, (hl)	
	and $0f
	cp $05
	jr c, hinibchk
	add a, $03		; add 3 to low nibble if > 4
hinibchk:	ld c, a
	ld a, (hl)
	and $f0
	cp $50
	jr c, endchk
	add a, $30		; add 3 to high nibble if > 4
endchk:	add a, c
	ld (hl), a 		; write back
	inc hl
	djnz h2dadd3

	pop af

	ld hl, bcdbuf
	ld b, d		; number of iterations of h2dshdc
h2dshdc:	rl (hl)
	inc hl
	djnz h2dshdc	; shift carry into bcd

	scf
	ccf
	pop bc
	djnz h2dfor
	ret

bcdlen:	.db $00		; number of bytes in bcd representation
bcdbuf:	.fill $100
; end prtdec

Finally, I could print the hexadecimal and decimal values that I was operating on! The next step is to expand the 8-bit and (limited) 16-bit math that the z80 allows to 24-bits and beyond so I can calculate the answer to problem 001.