Mastering Sideways ROM & RAM - Module 09 - Numerical arguments -------------------------------------------------------------- In modules 6 and 7 you were shown how to use the MOS to read numerical arguments by defining new Osbyte and Osword routines. In this module I will show you one way of reading numerical arguments and converting them into binary. There is no one correct way of reading numerical arguments and, as with most programming problems, there are as many solutions as there are people to write them. The method I will demonstrate is quite useful for sideways RAM programs because it uses the minimum amount of user memory and restores that memory after use. The method demonstrated in this module will read a hexadecimal argument in the range from &0 to &FFFF and convert the argument into a two byte binary number. The routine includes range and error checking to exclude invalid characters. The binary representation of the argument is stored in two bytes of zero page memory ready to be used by the rest of the routine. These two bytes can be restored before returning from the ROM. When you design your SWR programs you should anticipate how they will be used. If the instructions for using your program tell the user to enter, for example, the hexadecimal argument one-two-three, your program should recognise &123 &0123 123 and 0123 as all the same, correct, argument. Your program should not insist on using leading zeros or an ampersand (&) prefix if a hexadecimal number is expected but it should accept them without generating an error. It is not a good idea to get your SWR program running with a * command, then prompt for a number and use Osrdch to read it byte after byte. That style of programming works but it looks very unprofessional and writing a program which allows the user to edit the number with the Delete key is quite complicated. The argument(s) should follow the command so that if, for example, you have written a program which prints the contents of a memory location and you want to use it to print the contents of location &900 your SWR interpreter should accept any of the commands in figure 9.1 to call the routine and print the answer. >*PEEK &900 >*PEEK &0900 >*peek "900" >*pe. 0900 Figure 9.1 Valid commands and arguments ---------------------------------------- Up to four ASCII characters can be used to represent a two byte hexadecimal number. The nybble represented by the first ASCII character depends on how many characters are used to represent the number. For example, the A in &AB represents the most significant nybble of the least significant byte but the A in &ABC represents the least significant nybble of the most significant byte. If you read the ASCII characters from right to left, instead of the more natural left to right, the first byte you read will always be the least significant nybble of the least significant byte. If you read the characters from left to right you either have to know in advance how many characters are used so that, if necessary, you can pad out the number with leading zeros or you have to type the leading zeros to establish the place value of each character in the argument. To convert the ASCII representation of a hexadecimal argument into binary you should start with the least significant nybble of the least significant byte unless you use an elaborate conversion algorithm or leading zeros. This presents a problem because the Gsread routine used to read the ASCII characters from the argument reads from left to right. To get round this problem you can read each nybble of the argument, convert it into binary and push it on the stack until you read the termination character. Then pull each nybble off the stack and they will be in reverse order, as if they had been read from right to left. If you keep a count of the number of nybbles read from the argument it is a simple matter to pull the nybbles off the stack and convert each nybble, or pair of nybbles, into a byte. This technique is illustrated in figure 9.2. .found CLC \ terminate with space, return or " JSR &FFC2 \ initialise argument with Gsinit LDX #0 \ initial result = &0000 STX &A8 \ least sig. byte of result STX &A9 \ most sig. byte of result DEX \ X will count number of nybbles - 1 .argloop JSR &FFC5 \ read character from argument with Gsread BCS endarg \ branch if termination character CMP #ASC("&") \ is it "&"? BEQ argloop \ ignore "&" JSR convert \ convert nybble into binary PHA \ push nybble on stack INX \ count nybble in X register BPL argloop \ branch for next character .endarg CPX #4 \ more than 4 characters? BCS error \ branch if too many PLA \ pull LS nybble of LS byte STA &A8 \ store as LS byte DEX \ is that the only nybble? BMI finish \ branch if it is PLA \ pull MS nybble of LS byte ASL A \ shift ASL A \ left ASL A \ four ASL A \ bits ORA &A8 \ OR with &A8 STA &A8 \ and store LS byte DEX \ was that the last nybble? BMI finish \ branch if it was PLA \ pull LS nybble of MS byte STA &A9 \ store as MS byte DEX \ was that the last nybble? BMI finish \ branch if it was PLA \ pull MS nybble of MS byte ASL A \ shift ASL A \ left ASL A \ four ASL A \ bits ORA &A9 \ OR with &A9 STA &A9 \ and store MS byte .finish Figure 9.2 Converting from ASCII to binary ----------------------------------------- After recognising the * command the interpreter will pass control to the label ".found". The carry flag should be cleared before initialising the argument so that either a second quotation mark, a carriage return or a space will be read as the termination character of the argument. In figure 9.2 the X register is loaded with zero and then the content of X is stored in the two consecutive memory locations to be used for the result. The X register will be used to count the number of ASCII characters read from the argument minus one. X is decremented to start with X = &FF when no characters have been read. The argument is read one character at a time, ignoring any leading ampersand. Each character is converted from an ASCII character into a binary nybble and pushed on the stack. The X register is incremented with every binary nybble pushed on the stack. The routine will keep reading until the termination character is found. After reading the termination character the number stored in the X register should be between 0 and 3 inclusive. If it is outside that range either too many characters have been included in the argument or the argument has been omitted. In either case a branch to an error routine will be made. When all the characters have been read from the argument the routine starts pulling the binary nybbles off the stack. The X register is decremented and tested after each nybble has been processed to see if that nybble was the first one read from the argument. There must be at least one nybble to pull off the stack and not more than four. The first nybble pulled off the stack can be stored in the memory location reserved for the least significant byte. If a second nybble is available it has all its bits shifted left four times and ORed with the first nybble to form the least significant byte. A third nybble can be stored in the memory location reserved for the most significant byte and a forth nybble should have all its bits shifted left four times and ORed with the third nybble to form the most significant byte. The result is then available from the two consecutive zero page memory locations. No other part of user memory or SWR has been used by the conversion. The error routine and the ASCII character to binary nybble conversion subroutine are only refered to by name in figure 9.2 but suitable actual routines can be found in the program PEEK. The program PEEK uses the conversion routine outlined in figure 9.2 to implement the new command *PEEK . To use the program load the object code it generates into SWR and press the Break key. You can use the new command to print the hexadecimal value of the number stored in any byte of the I/O processor's memory. For example, *PEEK &900, *PEEK 0900 and *PEEK "900" are all equivalent and will all print the number stored in &900 in the I/O processor's memory. The program uses a one-command interpreter (lines 300-570) similar to the ones used in the earlier modules of the course. It uses the routine in figure 9.2 to read the argument and store the binary conversion in two zero page bytes (lines 580-1050). The read and convert routine has been modified slightly to store the two zero page bytes by pushing them on the stack before making the conversion (lines 610-640). The two bytes can then be restored before returning to the MOS (lines 1100-1130). The two zero page bytes are used with post-indexed indirect addressing to load the accumulator with the byte specified by the argument (lines 1060-1070). The accumulator content is printed using the two subroutines in lines 1190-1340. After printing the result the zero page locations are restored (lines 1100-1130). The registers pushed on the stack by the interpreter are pulled off to balance the stack (lines 1140-1160). The accumulator is reset to zero (line 1170) to inform the other ROMs that the request has been recognised, and control is returned to the MOS (line 1180). The ASCII to binary conversion subroutine is in lines 1350 to 1480. As well as converting the ASCII character into a binary nybble the subroutine also checks for valid characters. If an invalid character is found a branch is made to an error routine (lines 1490-1660) similar to the one introduced in Module 8. Because errors can be detected at any stage of the conversion, restoring the zero page bytes when an error is found would add extra complications to the error routine. All the information you need to restore the zero page bytes from the error routine is available in the X register. If you need the exercise you could modify the error routine to restore the zero page bytes before printing the error message. If you do there is no need to balance the stack as well because it will be reset by BASIC when control passes to the BRK instruction at &100. 10 REM: PEEK 20 MODE7 30 HIMEM=&3C00 40 DIM save 50 50 diff=&8000-HIMEM 60 address=&A8 70 errstack=&100 80 comvec=&F2 90 gsinit=&FFC2 100 gsread=&FFC5 110 osasci=&FFE3 120 osnewl=&FFE7 130 oscli=&FFF7 140 FOR pass = 0 TO 2 STEP 2 150 P%=HIMEM 160 [ OPT pass 170 BRK 180 BRK 190 BRK 200 JMP service+diff 210 OPT FNequb(&82) 220 OPT FNequb((copyright+diff) MOD 256) 230 BRK 240 .title 250 OPT FNequs("PEEK") 260 .copyright 270 BRK 280 OPT FNequs("(C)1987 Gordon Horsington") 290 BRK 300 .service 310 CMP #4 320 BEQ unrecognised 330 RTS 340 .unrecognised 350 PHA 360 TXA 370 PHA 380 TYA 390 PHA 400 LDX #&FF 410 .comloop 420 INX 430 LDA title+diff,X 440 BEQ found 450 LDA (comvec),Y 460 INY 470 CMP #ASC(".") 480 BEQ found 490 AND #&DF 500 CMP title+diff,X 510 BEQ comloop 520 PLA 530 TAY 540 PLA 550 TAX 560 PLA 570 RTS 580 .found 590 CLC 600 JSR gsinit 610 LDA address 620 PHA 630 LDA address+1 640 PHA 650 LDX #0 660 STX address 670 STX address+1 680 DEX 690 .argloop 700 JSR gsread 710 BCS endarg 720 CMP #ASC("&") 730 BEQ argloop 740 JSR convert+diff 750 PHA \ Push hex nybbles 760 INX 770 BPL argloop 780 .endarg 790 CPX #4 800 BCS error 810 PLA \ LS nybble low byte 820 STA address 830 DEX 840 BMI finish 850 PLA \ MS nybble low byte 860 ASL A 870 ASL A 880 ASL A 890 ASL A 900 ORA address 910 STA address \ LS byte 920 DEX 930 BMI finish 940 PLA \ LS nybble high byte 950 STA address+1 960 DEX 970 BMI finish 980 PLA \ MS nybble high byte 990 ASL A 1000 ASL A 1010 ASL A 1020 ASL A 1030 ORA address+1 1040 STA address+1 \ MS byte 1050 .finish 1060 LDY #0 1070 LDA (address),Y 1080 JSR hexbyte+diff 1090 JSR osnewl 1100 PLA 1110 STA address+1 1120 PLA 1130 STA address 1140 PLA 1150 PLA 1160 PLA 1170 LDA #0 1180 RTS 1190 .hexbyte 1200 PHA 1210 LSR A 1220 LSR A 1230 LSR A 1240 LSR A 1250 JSR nybble+diff 1260 PLA 1270 .nybble 1280 AND #&F 1290 SED 1300 CLC 1310 ADC #&90 1320 ADC #&40 1330 CLD 1340 JMP osasci 1350 .convert 1360 CMP #ASC("9")+1 1370 BCS letters 1380 CMP #ASC("0") 1390 BMI error 1400 AND #&F 1410 RTS 1420 .letters 1430 SBC #&37 1440 CMP #&A 1450 BMI error 1460 CMP #&10 1470 BCS error 1480 RTS 1490 .error 1500 LDA #(errormsg+diff) MOD 256 1510 STA address 1520 LDA #(errormsg+diff) DIV 256 1530 STA address+1 1540 LDY #&FF 1550 .errorloop 1560 INY 1570 LDA (address),Y 1580 STA errstack,Y 1590 BPL errorloop 1600 JMP errstack 1610 .errormsg 1620 BRK 1630 OPT FNequb(&FE) 1640 OPT FNequs("Out of range") 1650 BRK 1660 OPT FNequb(&FF) 1670 .lastbyte 1680 ] 1690 NEXT 1700 INPUT'"Save filename = "filename$ 1710 IF filename$="" END 1720 $save="SAVE "+filename$+" "+STR$~(HIMEM)+" "+STR$~(las tbyte)+" FFFF8000 FFFF8000" 1730 X%=save 1740 Y%=X% DIV 256 1750 *OPT1,2 1760 CALL oscli 1770 *OPT1,0 1780 END 1790 DEFFNequb(byte) 1800 ?P%=byte 1810 P%=P%+1 1820 =pass 1830 DEFFNequw(word) 1840 ?P%=word 1850 P%?1=word DIV 256 1860 P%=P%+2 1870 =pass 1880 DEFFNequd(double) 1890 !P%=double 1900 P%=P%+4 1910 =pass 1920 DEFFNequs(string$) 1930 $P%=string$ 1940 P%=P%+LEN(string$) 1950 =pass