One thing it was obvious I would want to do with the Zolatron 64 6502 homebrew computer is have a routine to display the contents of sections of memory – à la Wozmon.
There’s a snag, though. The content of any given byte in memory is a number, in the range 0-255. But to display this on the screen, this would need to be converted to a string of ASCII codes representing that number. I also thought it might be handy to be able to go in the other direction, too – to turn a string of characters coming in via the serial port, for example, into their numeric counterpart.
This sort of thing is trivially easy in high-level languages. In 6502 assembler, you’re on your own.
To keep things simple, I decided to stick to a hexdecimal string representation of numbers. This would always be two characters, in the range from ’00’ to ‘FF’.
To illustrate what I mean, let’s take the hexadecimal number B6 (or 0xB6, or $B6, depending on how you like to denote hex numbers). That’s 182 in decimal, but we don’t care about that.
To turn this into a string, I need to convert it to the two characters ‘B’ and ‘6’. In ASCII, these are code values $42 and $36 (or 66 and 54 in decimal).
First, a bit of preparation. Elsewhere in the code, I’ve declared a memory location as TMP_TEXT_BUF – the start of a 64-byte buffer for just this kind of operation. (I’ll be using only three bytes here, though.)
And I created a table of the hex characters ‘0’–’F’. You’ll see how this works in a moment.
These lines are not in the same place in the code, but I’ve put them together below just so you can see how they look.
TMP_TEXT_BUF = $07A0 ; general-purpose buffer/scratchpad .hex_chr_tbl ; hex character table equs "0123456789ABCDEF"
And here’s the subroutine to convert from a one-byte numeric value to a string.
; convert 1-byte value to 2-char hex string .byte_to_hex_str ; assumes that number to be converted is in A tax ; keep a copy of A in X for later lsr A ; logical shift right 4 bits lsr A lsr A lsr A ; A now contains upper nibble of value tay ; put in Y to act as offset lda hex_chr_tbl,Y ; load A with appropriate char from lookup table sta TMP_TEXT_BUF ; and stash that in the text buffer txa ; recover original value of A and #%00001111 ; mask to get lower nibble value tay ; again, put in Y to act as offset lda hex_chr_tbl,Y ; load A with appropriate char from lookup table sta TMP_TEXT_BUF+1 ; and stash that in the next byte of the buffer lda #0 ; and end with a null byte sta TMP_TEXT_BUF+2 rts
We assume that the byte to be converted has already been loaded into A before calling this subroutine. This value is copied into the X register so that we can use it again later.
We’re going to deal with the high nibble first. For the number $B6, this would be the B part.
We perform a logical shift right (lsr) four times. This means the lower nibble has been thrown away and the upper nibble moved into the lower nibble position. B6 has become 0B. Now we shove this value into the Y register where it acts as an offset for the next line: lda hex_chr_tbl,Y.
The value 0B is 11 in decimal. So, in effect we’re loading the 11th element of the table that starts in memory at location ‘hex_chr_tbl’. We defined a string of characters at this location, the 11th element of which is the character ‘B’. The actual value that will be loaded into A at this point is actually $42 – the hex numeric code for the ASCII symbol ‘B’.
We stash this value at the start of the temporary text buffer.
Next we reload the original value (B6) into A by transferring it from X where we’d stored it. We AND this value with the binary value 00001111. That has the effect of masking out the high nibble. B6 has become 06. Again, we move this into Y to act as an offset to look up the correct character in our table, like we did before. The ASCII code for ‘6’ is $36 (54 in decimal). We store this value in the second position in the buffer.
Finally, we put the null value 0 in the third byte of the buffer, to act as a terminator.
Eh, voilà! We now have the string we need in the buffer, to be used however we want.
Going the other way
The reverse process is slightly more involved, but not too much.
I’ve elswhere declared some one-byte, zero-page addresses:
FUNC_RESULT = $60 ; to hold the result of a subroutine BYTE_CONV_L = $63 ; scratch space for converting bytes between num & string BYTE_CONV_H = BYTE_CONV_L+1
We assume that the calling code has put the character for the high nibble into BYTE_CONV_H and the character for the low nibble into BYTE_CONV_L. The FUNC_RESULT byte is a general purpose location for holding the results of subroutines.
Now here’s the code.
.hex_str_to_byte ; assumes text is in BYTE_CONV_H and BYTE_CONV_L lda BYTE_CONV_H ; load the high nibble character jsr asc_hex_to_bin ; convert to number - result is in A asl A ; shift to high nibble asl A asl A asl A sta FUNC_RESULT ; and store lda BYTE_CONV_L ; get the low nibble character jsr asc_hex_to_bin ; convert to number - result is in A ora FUNC_RESULT ; OR with previous result sta FUNC_RESULT ; and store final result rts .asc_hex_to_bin ; assumes ASCII char val is in A sec sbc #$30 ; subtract $30 - this is good for 0-9 cmp #10 ; is value more than 10? bcc asc_hex_to_bin_end ; if not, we're okay sbc #$07 ; otherwise subtract another $07 for A-F .asc_hex_to_bin_end rts ; value is returned in A
First we load the character for the high nibble and run this through the asc_hex_to_bin subroutine. This takes the code and subtracts $30 (decimal 48) from it. Why? Well, the ASCII code for the character ‘0’ is $30. If you subtract $30 from this, you get the numeric value 0. Similarly, the ASCII code for ‘9’ is $39. Again, subtract $30 and you get 9. So this conversion is easy. For values 0–9.
The hex symbol for 10 is ‘A’ and, alas, that letter does not come right after ‘9’ in the ASCII table. The letters ‘A’–’F’ have the codes $41–$46. So we run a test to see if, after subtracting $30, we are left with a value of more than 10. If so, we know we’re dealing with the values A–F and need to subtract a further 7 from the number to get the value we’re looking for.
Having got the value for the high nibble, we do an arithmetic shift left (asl) four times to shift the bits into the high nibble position, and store this in FUNC_RESULT. Then we grab the character for the low nibble and run it through the asc_hex_to_bin subroutine as before. As that subroutine leaves the result of its calculations in A, we simply OR this result with the one we had for the high nibble, and we have our result. This is left in A.