Jim Butterfield, Associate Editor
Outputting strings from machine language is no problem. The programmer takes the characters from memory and sends them out. Numbers need more work: the binary values must be changed to ASCII characters which must be sent out one at a time.
An added complexity is format: numbers often need to be carefully formed into a specific number of characters, so that they will print neatly in columns. Zero suppression is often desirable, so that a number such as 00204 will print as 204. Some of these jobs are fairly straightforward mechanical tasks; the hardest part is often the math routine which is needed to break up a binary number into several digits.
Binary values of zero to nine are easy. All we need to do is to change them to ASCII before sending them out.
We've mentioned before that ASCII represents the character zero, for example, as hexadecimal 30, decimal 48. PRINT CHR$(0) will not print a zero character - indeed, it won't print anything - so that we must do the job with PRINT CHR$(48). So, to print a binary zero, we must change it to hex 30, binary one must be changed to hex 31, and so forth, up to binary 9 changing to hex 39. Binary 10 is a different matter: we must make two digits out of it, one and zero. The easiest way to convert a single digit is with an ORA command: ORA #$30 will insert the desired high bits.
When we move on to more complex numbers, we'll need to remember that each digit, as we generate it, must be converted to ASCII before output.
Let's write a simple program to print several single numeric digits. We'll use $FFD2 for PRINT; this will work on all PET/CBM machines, VIC, and Commodore 64. Our coding goes:
LDX #$00 (start at zero) LOOP TXA (move number to A) ORA #$30 (convert to ASCII) JSR $FFD2 (print it) (INX) (go to next number) CPX #$0A (less than ten?) BCC LOOP (yes, print it) RTS
The output looks like a large number - the digits are printed side by side - but, in fact, it's ten independent digits.
As an exercise, let's convert the above program to BASIC POKEs and run it. Our BASIC equivalent goes:
100 DATA 162, 0, 138, 9, 48 110 DATA 32, 210, 255, 232, 224, 10 120 DATA 144, 245, 96 200 FOR J = 848 TO 861 : READ X 210 POKE J, X: NEXT J 300 FOR J = 1 TO 10:SYS 848: NEXT J
The first three lines give the machine language program in decimal. The individual instructions have been separated by spaces to make them more visible. Lines 200 and 210 POKE the program into the cassette area. Finally, line 300 invokes the machine language program ten times; you'll get a hundred digits printed.
Hex output, like input, is fairly easy. Hexadecimal might be viewed as a compact way of representing binary, and since the computer has binary, the conversion must be easy. It is. All we need to do is grab four bits at a time. Each group of four bits is a hex digit value, which can be converted to ASCII and then output. For example, a decimal value of 225 (hex E1) can be converted this way: take the high four bits, binary 1110, and convert and print as a hex character. That works out to a letter E. Now take the low four bits, binary 0001, and do the same, giving us the digit 1. We've printed E1, the hex value.
Let's get technical. How do we get the four high bits? By giving four shift-right instructions. The bits obligingly move over to the low order side, and zeros are left in the vacated space. Later, how do we get the four low bits? By taking the original value and performing an AND #$0F, which wipes out the high bits.
When the four-bit group is extracted, how do we change to ASCII? If the four-bit value is zero to nine, we can use the simple ORA #$30 as mentioned before. For the six high values, ten to fifteen (A to F), we would need to use arithmetic, usually the ADC command. Of course, we could bypass the whole question by setting up a table of digits and looking up each digit. Most programmers go for the arithmetic.
Multiple bytes are no problem for hex. We just convert them starting at the high order end: each byte generates two hex digits. Let's write a program to convert some memory bytes into hex and display them. First, a subroutine to convert and output a four-bit value in the A register as two hex digits:
HEXDIG CMP #$0A (alphabetic digit?) BCC SKIP (no, skip next part) ADC #$06 (add seven) SKIP ADC #$30 (convert to ASCII) JMP $FFD2 (print it)
There are a couple of curious coding quirks above. We need to add seven to the alphabetics: why does the coding say ADC #$06? Because the carry bit is set, that's why. Adding six plus a carry makes a total increase of seven. Another oddity: the subroutine doesn't return with RTS. Instead, it goes to another subroutine; when the other subroutine (FFD2) returns, it will return directly to the caller.
Now an outer subroutine. This one breaks a byte in the A register into two four-bit numbers and prints the two digits. It uses HEXDIG, above:
HEXOUT PHA (save the byte) LSR A LSR A (extract four..) LSR A (.. high bits) LSR A JSR HEXDIG (print hex char) PLA (bring back byte) AND #$0F (extract low four) JMP HEXDIG (restore ASCII)
Again, we save an RTS by doing a JMP direct to a subroutine.
Now we can do the main job: displaying a number of memory locations:
JOB LDX #$00 (counter) JLOOP LDA $FFC0, X (get a byte) JSR HEXOUT (print it) LDA #$20 (space char) JSR $FFD2 (print it) INX CPX #$0A (ten bytes yet?) BCC JLOOP (no, do another) LDA #$0D (RETURN char) JMP $FFD2 (print it)
We've written the program to display a specific range of addresses. You may change it to display what you wish.
The four LSR instructions may be considered the equivalent of dividing by 16. That's what the word "hexadecimal" means, of course: hex for six and decimal for ten, giving a total of 16.
You may have decided that hexadecimal output is quite easy. It is, compared to decimal, and that gives us an interesting possibility.
Could we write hex numbers that looked like decimal numbers? In other words, could we print decimal 22 by somehow converting it to look like hex 22, and then printing it? It sounds complex: decimal 22 would be written as hex 16, and hex 22 has a decimal value of 34. Not much in common there. But there's a gimmick.
The 6502 processor has an arithmetic feature called "decimal mode." When we invoke it (with the SED, Set Decimal, command), decimal arithmetic takes place using numbers that look like hex. In other words, the decimal value of 22 is stored as hex 22. The proper name for this kind of number is not hexadecimal, of course. This numbering system is called "binary coded decimal."
We can't go into the inner mysteries of BCD at this time, but a few facts can be noted. Decimal mode affects only the ADC (add with carry) and SBC (subtract) instructions; all other instructions still deal with binary numbers. If you're going to play with decimal mode, kill the interrupt for the moment; your interrupt routines may not be able to cope with "new math." And remember to put everything back (clear decimal mode, restore the interrupt) when you've finished doing the task at hand.
Decimal mode arithmetic is great for things like keeping score in video games. The scores can be easily translated and delivered to the screen. But decimal mode is not too good for serious mathematics: multiplication, division, square roots and such become much harder to handle. For most applications, stick with binary.
We'll be talking about how to convert binary numbers to decimal in the next session.