Zolatron 64 6502 homebrew – converting between text and numbers in assembly

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.

KISS

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.