Part 7 of 11 -- Simple Assembly for Atari BASIC - Convert Integer to Hex String
Atari BASIC Mac/65 6502 Assembly Atari 8-bit
Convert Integer To Hex String
Part 1 - Introduction
Part 2 - Learn 82.7% of Assembly Language in About Three Pages
Part 3 - The World Inside a USR() Routine
Part 4 - Implement DPEEK()
Part 5 - Implement DPOKE
Part 6 - Various Bit Manipulations
Part 7 - Convert Integer to Hex String
Part 8 - Convert Integer to Bit String
Part 9 - Memory Copy
Part 10 - Binary File I/O Part 1 (XIO is Broken)
Part 11 - Binary File I/O Part 2 (XIO is Broken)
OSS BASIC XL includes the Hex$() function that converts an integer value to a string of hexadecimal digits. That would be a useful accessory to the bit manipulation test program, since byte values are easier to visualize in hexadecimal.
The integer to hexadecimal string conversion sounds more simple than it is. An integer made of low byte value, $12, and high byte value, $34, exists in memory in the order: $12, $34. However, the hexadecimal text representation is in reverse order with high byte first, “3412”. Furthermore, the text representation is four characters (bytes) as “3”, “4”, “1”, “2”. This means the two bytes of the integer must be separated into individual nybble values and reorganized in the string order.
Next, the actual binary values of each nybble (0 to 15) must be converted into a text character. The ASCII/ATASCII characters “0” through “9” are not contiguous with the characters “A” through “F”. A look-up table of 16 text characters would be the most simple and direct method, but that would not be relocatable. So, a comparison/computation method is used, and discussed below:
INT2HEX in Mac/65 Assembler Code
0100 ; INT2HEX.M65 0105 ; 0110 ; Convert 16-bit integer into 0115 ; hex string. 0120 ; 0125 ; INT2HEX USR 2 arguments: 0130 ; Value == Integer to convert. 0135 ; StrAdr == Address of string 0140 ; which must be able 0145 ; to hold 4 characters 0150 ; 0155 ; USR return value 0 means no 0160 ; conversion. Non-Zero means 0165 ; STRADR contains hex string. 0170 ; 0175 ; Use the FR0/FR1 FP register. 0180 ; The return value for BASIC 0185 ; goes in FR0. 0190 ; No FP is used so all of FR0 0195 ; (and more FP registers) can 0200 ; be considered available. 0205 ; 0210 ZRET = $D4 ; FR0 $D4/$D5 Return value 0215 ZARGS = $D5 ; $D6-1 for arg Pulldown loop 0220 ZSTR = $D6 ; FR0 $D6/$D7 STRADR 0225 ZVAL = $D8 ; FR0 $D8/$D9 Integer value 0230 ; 0235 .OPT OBJ 0240 ; 0245 ; Arbitrary. This is relocatable. 0250 ; 0255 *= $9400 0260 ; 0265 INIT 0270 LDA #$00 ; Make sure return 0275 STA ZRET ; value is cleared 0280 STA ZRET+1 ; by default. 0285 PLA ; Get argument count 0290 BEQ EXIT ; Shortcut for no args 0295 ASL A ; Now number of bytes 0300 TAY 0305 CMP #$04 ; Integer Value1, StrAdr 0310 BEQ PULLDOWN 0315 ; 0320 ; Bad args. Clean up for exit. 0325 ; 0330 DISPOSE ; any number of args 0335 PLA 0340 DEY 0345 BNE DISPOSE 0350 RTS ; Abandon ship 0355 ; 0360 ; This code works the same 0365 ; for 1, 4, 8 ... arguments. 0370 ; 0375 PULLDOWN 0380 PLA 0385 STA ZARGS,Y 0390 DEY 0395 BNE PULLDOWN 0400 ; 0405 ; Arg validation. 0410 ; StrAdr may not be null. 0415 ; 0420 LDA ZSTR 0425 ORA ZSTR+1 0430 BEQ EXIT ; zstr is null 0435 ; 0440 CLD 0445 ; 0450 ; Split Integer Value into 0455 ; nybbles and temporarily store 0460 ; in the String output... 0465 ; 0470 LDX #$01 ; X index bytes. 0475 ; Y is already 0 0480 ; Y index string. 0485 ; 0490 SPLIT_BYTES 0495 LDA ZVAL,X ; Byte 0500 ; 0505 ; Right shift 4 to keep 0510 ; the high nybble. 0515 LSR A 0520 LSR A 0525 LSR A 0530 LSR A 0535 STA (ZSTR),Y ; Save for later 0540 ; 0545 ; Now, do the low nybble 0550 LDA ZVAL,X ; Byte again 0555 AND #$0F ; low nybble 0560 INY ; Next char in string 0565 STA (ZSTR),Y ; Save for later 0570 ; 0575 INY ; Next char in string 0580 DEX ; Next integer byte 0585 BPL SPLIT_BYTES 0590 ; 0595 ; Next, convert the nybbles 0600 ; saved in the string into the 0605 ; final ASCII form. 0610 ; 0615 DEY ; Correct string index. 0620 BYTE2HEX 0625 LDA (ZSTR),Y 0630 CMP #$0A ; 10 or greater? 0635 BCC ADD_48 ; No, 0 to 9. 0640 ; 0645 ADC #$06 ; "A"-"0"-$0A-Carry 0650 ADD_48 0655 ADC #$30 0660 STA (ZSTR),Y 0665 DEY ; 3, 2, 1, 0 will continue 0670 BPL BYTE2HEX ; -1 ends loop 0675 ; 0680 INC ZRET ; Successful return 0685 EXIT 0690 RTS 0695 ; 0700 .ENDThere is a new bit of code in the initialization:
0405 ; Arg validation. 0410 ; StrAdr may not be null. 0415 ; 0420 LDA ZSTR 0425 ORA ZSTR+1 0430 BEQ EXIT ; zstr is null 0435 ; 0440 CLDThis shows a short way to compare an address (or integer) to NULL/zero. It simply OR's the low and high bytes together. Then if any bit is set it means the address is not NULL. Any non-zero value is considered legitimate (which is not a purely correct assumption, but reasonably good enough for this exercise.)
The long way to check the string address would be to load low byte, compare to zero. If it is not zero, then the value is valid and so branch to the code to continue the routine. If it is zero, then load the high byte, compare to zero, and take the successful branch for non-zero and exit the routine if it is zero. That would take six instructions, handily duplicated by the three here.
Full validation (not done here) would mean a lot more code making sure the value is a valid address for a BASIC string, or otherwise not anywhere near low memory and not in the ROM area.
Finally, this code turns off BCD mode (CLD) because it will use Add instructions on binary values later.
The conversion is separated into two activities: First is separating the bytes into nybbles and placing them in the correct order. The second phase is converting the nybbles into the corresponding ASCII/ATASCII characters. The code could be done in just one loop, but that would mean duplicating the nybble to text conversion code twice in the loop. (And that idea could be implemented more efficiently as a subroutine called by JSR, but then the code would not be relocatable.) The first part of the conversion:
0490 SPLIT_BYTES 0495 LDA ZVAL,X ; Byte 0500 ; 0505 ; Right shift 4 to keep 0510 ; the high nybble. 0515 LSR A 0520 LSR A 0525 LSR A 0530 LSR A 0535 STA (ZSTR),Y ; Save for later 0540 ; 0545 ; Now, do the low nybble 0550 LDA ZVAL,X ; Byte again 0555 AND #$0F ; low nybble 0560 INY ; Next char in string 0565 STA (ZSTR),Y ; Save for later 0570 ; 0575 INY ; Next char in string 0580 DEX ; Next integer byte 0585 BPL SPLIT_BYTES
There are two loops happening here. One loop, indexed by X, counts in reverse, 1 to 0, from the high byte to the low byte of the integer. Within that loop is the activity to separate the two nybbles in the byte. Mixed in this loop is another loop, indexed by Y, counting from 0 to 3 to index each character position of the string. After the routine liberates the nybbles from their byte it stores the nybbles in their respective positions in the string. The Y index is thus incremented twice (two characters) for each decrement (one byte) of the X loop.
The end result is a “string” containing the four, binary nybble values each in a separate “character”. The binary values are not human readable, so the routine must convert the binary values to corresponding ASCII/ATASCII characters:
0620 BYTE2HEX 0625 LDA (ZSTR),Y 0630 CMP #$0A ; 10 or greater? 0635 BCC ADD_48 ; No, 0 to 9. 0640 ; 0645 ADC #$06 ; "A"-"0"-$0A-Carry 0650 ADD_48 0655 ADC #$30 0660 STA (ZSTR),Y 0665 DEY ; 3, 2, 1, 0 will continue 0670 BPL BYTE2HEX ; -1 ends loop
This second part of the conversion code loops though the string in reverse converting each byte value into the corresponding ASCII/ATASCII value. The obvious method to do this would be a lookup table that translates the 16 possible values into the corresponding character. However, a lookup table is not easily relocatable, so this version does the conversion by value comparisons and math.
Here is the problem laid out in one table. The math must convert one series of values (“Values”) into another series of values (“Text Values”) that will print as text characters.
Values ASCII Text Text Values Difference $00 “0” $30 $30 $01 “1” $31 $30 $02 “2” $32 $30 $03 “3” $33 $30 $04 “4” $34 $30 $05 “5” $35 $30 $06 “6” $36 $30 $07 “7” $37 $30 $08 “8” $38 $30 $09 “9” $39 $30 $0A “A” $41 $37 $0B “B” $42 $37 $0C “C” $43 $37 $0D “D” $44 $37 $0E “E” $45 $37 $0F “F” $46 $37This tells us a few facts. The original series (“Values”) breaks down to two destination series with different offsets, $30 and $37, and that the value of the offset increases rather than decreases ($30 < $37) with each series. Since the $A to $F text values are after the $0 to $9 values, then only addition is needed to convert the binary “Values” series into the “Text Values” series. So, the code merely needs to determine whether the value is in the first series or the second series and then add the difference accordingly. However, to squeeze code size a small bit of cleverness is added that capitalizes on the relationship between the offsets for each series.
At each position the code tests to determine if the value is in the $0 to $9 range or the $A to $F range. Values $0 to $9 result in a branch to code that adds $30 to the value, converting binary values $0 to $9 to ASCII/ATASCII values “0” to “9” (or $30 to $39). That branch bypasses other code below for handling values $A to $F.
The conversion for values $A to $F is in two parts – first, the code adds an offset. On first glance at the table above this offset should be the difference of $37 - $30 or $7. The first two conditions below do result in the $07 offset. But, an easily overlooked condition in the code is the carry bit acquired by the comparison. So, the offset value is decremented to $6 to compensate for the carry bit:
1. the difference between the “A” and “0” characters ($41 - $30 = $11) and then
2. it subtracts the integer value of the start of the $A to $F range ($11 - $A == $7) and then
3. it removes the Carry bit that is acquired by the comparison operation ($7 - $1 == $6).
After adding the offset the execution path falls into the same section used for $0 to $9 which adds $30 to the value. So, for binary value $A the end result is $A + $6 + Carry + $30 which is $41 or ASCII/ATASCII “A”.
The entire working code is 15 bytes long. The actual code using a lookup table to convert binary to ASCII would be much shorter (and faster), but still require an additional 16 bytes of supporting data for the translation table.
Since BASIC can pass the address of the string data, not not the control information of the string, the machine language routine has no way of knowing the size of the string – either the DIM'ensioned size or the actual string length. Therefore the machine language code can neither limit itself to the maximum string length nor change the string's current length while populating the text value. A BASIC program using the results of the conversion must insure that it sets the real length of the string prior to calling the conversion function. This is simple – the BASIC program need only assign a value to the string that is four characters long prior to using the machine language routine. Such as:
130 DIM A$(4) 140 A$=" ":REM FOUR SPACES
The conversion routine overwrites the contents of the string replacing the blank spaces with the hexadecimal text string. The BASIC program can then safely refer to the string contents, like this:
150 RH=USR(INT2HEX,4660,ADR(A$)) 160 ? 4660;"(dec) = $";A$ 170 ? "Low Byte = $";A$(3,4) 180 ? "High Byte = $";A$(1,2)
which results in this output:
4660(dec) = $1234 Low Byte = $34 High Byte = $12
Below are the source files and examples of how to load the machine language routine into BASIC included in the disk image and archive:
INT2HEX File List:
INT2HEX.M65 Saved Mac/65 source
INT2HEX.L65 Mac/65 source listing
INT2HEX.T65 Mac/65 source listed to H6: (linux)
INT2HEX.ASM Mac/65 assembly listing
INT2HEX.TSM Mac/65 assembly listing to H6: (linux)
INT2HEX.OBJ Mac/65 assembled machine language program (with load segments)
INT2HEX.BIN Assembled machine language program without load segments
INT2HEX.LIS LISTed DATA statements for INT2HEX.BIN routine.
INT2HEX.TLS LISTed DATA statements for INT2HEX.BIN routine to H6: (linux)
MAKEI2H.BAS BASIC program to create the INT2HEX.BIN file. This also contains the INT2HEX routine in DATA statements.
MAKEI2H.LIS LISTed version of MAKEI2H.BAS
MAKEI2H.TLS LISTed version of MAKEI2H.BAS to H6: (linux)
TESTI2H.BAS BASIC program that tests the INT2HEX USR() routines.
TESTI2H.LIS LISTed version of TESTI2H.BAS.
TESTI2H.TLS LISTed version of TESTI2H.BAS to H6: (linux)
But, let's say that you're really so lazy that its still too much effort for you to visualize the bits in each hex value. The solution to that problem is the next routine that converts a 16-bit integer into a string of 16 characters where each character represents a bit value, 0 or 1.
ZIP archive of files:
Tar archive of files (remove the .zip after download)
Do not be anxious about anything, but in everything by prayer and supplication with thanksgiving let your requests be made known to God. And the peace of God, which surpasses all understanding, will guard your hearts and your minds in Christ Jesus.