Jump to content





Part 7 of 11 -- Simple Assembly for Atari BASIC - Convert Integer to Hex String

Posted by kenjennings, 31 July 2016 · 372 views

Atari BASIC Mac/65 6502 Assembly Atari 8-bit

Convert Integer To Hex String
 
==============================================================  
 
Part 1 - Introduction
http://atariage.com/...or-atari-basic/
 
Part 2 - Learn 82.7% of Assembly Language in About Three Pages
http://atariage.com/...or-atari-basic/
 
Part 3 - The World Inside a USR() Routine 
http://atariage.com/...or-atari-basic/
 
Part 4 - Implement DPEEK() 
http://atariage.com/...or-atari-basic/
 
Part 5 - Implement DPOKE  
http://atariage.com/...or-atari-basic/
 
Part 6 - Various Bit Manipulations
http://atariage.com/...or-atari-basic/
 
Part 7 - Convert Integer to Hex String 
http://atariage.com/...or-atari-basic/
 
Part 8 - Convert Integer to Bit String
http://atariage.com/...or-atari-basic/
 
Part 9 - Memory Copy 
http://atariage.com/...or-atari-basic/
 
Part 10 - Binary File I/O  Part 1 (XIO is Broken) 
http://atariage.com/...or-atari-basic/
 
Part 11 - Binary File I/O  Part 2 (XIO is Broken)
http://atariage.com/...-basic-the-end/
 
==============================================================
 
 
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     .END
There 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     CLD
This 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          $37
This 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:
 
Attached File  Convert_Disk.zip (32.66KB)
downloads: 32
 
 
Tar archive of files (remove the .zip after download)
 
Attached File  Convert_Disk.tgz.zip (19.05KB)
downloads: 29
 
 
 
 
 
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.   
Philippians 4:6-7