Mastering Sideways ROM & RAM - Module 15 - Multi command interpreter -------------------------------------------------------------------- Up until now, all the modules of the course that have used an unrecognised * command interpreter to demonstrate a programming technique have used a one-command interpreter. In this module I will introduce a multiple command interpreter which can be used to implement one or more new * commands per SWR bank. The multiple command interpreter is much more complicated than the simple one-command version. It includes an optional prefix character for the commands as well as command shortening with a dot. Prefixing commands with an optional character is quite a useful feature to include in your interpreters. Not only does it make your work look more professional but it is very useful if the command names you choose for your ROM clash with those in a higher priority ROM. If, for example, your ROM uses the command *DUMP and it is in a lower priority ROM socket than the DFS ROM then the command will always be intercepted by the DFS before your interpreter has a chance to recognise it. If you use an optional "X" prefix your ROM will get the opportunity to intercept the command as long as the equivalent *XDUMP command is ignored by the DFS ROM. Because the interpreter is used to implement more than one command you can not use the title string in the header as a command name, although the title string should still be included in the header to identify the ROM image. The single command name has to be replaced with a multiple command name table. Each name in the table is followed by a two byte address. This is the address of the routine associated with the command name. Each address is stored in the table with the high byte of the address followed by the low byte. This breaks with the 6502 convention of storing the low byte first but it is done so that the high byte, which must have the most significant bit set, can be used as an end of command name marker as well as part of the address. The byte &FF is used as a termination character in the command table. The command table used in the example program is shown in BASIC 2 Assembler in figure 15.1. .commtable EQUS "SIREN" \ command string EQUB siren DIV 256 \ end of command marker & ms byte EQUB siren MOD 256 \ ls byte EQUS "HEHE" \ command string EQUB hehe DIV 256 \ end of command marker & ms byte EQUB hehe MOD 256 \ ls byte EQUS "UFO" \ command string EQUB ufo DIV 256 \ end of command marker & ms byte EQUB ufo MOD 256 \ ls byte EQUB &FF \ termination character Figure 15.1 The new command table. ----------- ---------------------- The interpreter intercepts service call 4 and then compares the unrecognised command with each command in the command table. If a match is found the command address is taken from the command table and stored in two consecutive zero page bytes which are used to make an indirect jump to the new routine. The order of the commands in the table is very important. The longest command name must be at the head of the table and the shortest at the end. The reason for this is that if two commands such as *RED and *REDUCE are included in the table and the unrecognised command is compared with RED before REDUCE then *REDUCE will be recognised as *RED with the argument "UCE". The commands in the table must all be upper case alphabetic characters and must not start with the optional character, in this example "X". Numbers and punctuation characters must not be used because the unrecognised command is forced to upper case by ANDing it with #&DF. The three new routines in the example program SOUNDS are called with *SIREN, *HEHE, and *UFO. All three routines are the machine code equivalent of the ENVELOPE and SOUND commands suggested by their names. Load the object code generated by SOUNDS into SWR and press the Break key. The command *HEHE can be shortened to three characters (*HEH.) but if it is shortened further (*HE.) the command clashes with the *HELP command. The optional prefix has to be used (*XH.) if you want to shorten the command to two characters. The multiple command interpreter intercepts service call 4 (lines 280-310) and pushes the registers (lines 320-360) and two zero page bytes (lines 370-400) on the stack. The X register is used as an offset on the command table address and it is initialised with the value of &FE (line 410). The reason why X is initialised to &FE, and not zero as you might expect, is that it is incremented in the outer loop of the comparison routine (line 450) to point to the byte before the first byte of each command in the table and again in the inner loop (line 550) to point to every byte of each command starting with the first byte. The Y register is the offset on the unrecognised command text pointer and needs to be restored to its initial value at the begining of each comparison. It is pushed on the stack as a temporary store (lines 410-420). The outer loop of the comparison routine is in lines 450 to 700. The outer loop points to each command in the command table. The X register is incremented to point to the byte preceding the first byte of each command (line 450) and the Y register is restored to its initial value (lines 460-480). The first character of the unrecognised command is read (line 490) and forced to upper case (line 500). It is compared with the optional first character (line 510) and if it matches the Y register is incremented to point to the next character (line 530). If it does not match the Y register is not incremented so that the first character will be read again and compared with the contents of the command table. The inner loop of the comparison routine is in lines 550 to 640. This loop compares every byte of each new command with every byte of the unrecognised command and recognises a command if either all the pairs of bytes match or if all pairs of bytes match until a dot is read at the end of the unrecognised command. The X register is incremented (line 550) to point to the first byte of a command in the command table. The byte pointed to by the offset in X is read (line 560) and tested (line 570) to see if it is the end of command marker (the most significant byte of the address bytes). When the command name and unrecognised command match the end of command marker will be read and control is passed to the label ".found". Control is also passed to ".found" when the termination character (&FF) is read. If the end of command marker or termination character is not read the next byte in the unrecognised command is compared with "." (lines 580-600). If the next character is a dot control is passed to the label ".foundot". The inner loop in lines 550 to 640 exits when a mismatch in the pairs of bytes is found. When a mismatch occurs the X register is pointed to the end of command marker (lines 660-680) and the marker is compared with &FF to see if it is the command table termination character (line 690). If the termination character is found the zero page bytes and registers are restored and control is passed back to the MOS (lines 720-820). If it is not the routine branches back to the label ".firstchar" (line 440) to compare the unrecognised command with the next command in the table. When a command shortened with a dot is recognised control is passed to the label ".foundot" (line 830). The X register is then incremented until it points to the end of command marker (lines 840-860). An unshortened command passes control to the label ".found" (line 870). The accumulator is compared with &FF to see if the termination character has been read. If it has then none of the commands match the unrecognised command and so the registers are restored and control passes back to the MOS (lines 710-820). If the accumulator does not contain the termination character it must contain the most significant byte of the new routine. This is stored in the most significant of the two consecutive zero page bytes (line 900). The X register is incremented to point to the byte storing the least significant byte of the new routine's address (line 910). This byte is loaded into the accumulator (line 920) and stored in the least significant of the two zero page bytes (line 930). The Y register was pushed on the stack as a temporary store and is pulled off and discarded (line 940). The argument is initialised (lines 950-960) and an indirect jump is made to the new routine (line 970). After the new routine has been executed the two zero page bytes used for the indirect jump are restored (lines 1190-1220), the stack is balanced (lines 1230-1250), the accumulator is loaded with zero to indicate that the command has been recognised (line 1260) and control is passed back to the MOS (line 1270). Understanding and explaining how the interpreter works is much more difficult than using it. In order to use this interpreter with your own programs you only need to add the command names to the command table and, of course, the new routines. You don't need to alter the coding of the interpreter at all which makes it very easy to adapt for your own software. When you adapt your own programs to run in SWR with this interpreter you must not forget to use a routine, such as the one in lines 1180 to 1270, which restores the two zero page bytes and balances the stack before returning control to the MOS. Restoring the memory used by the interpreter, balancing the stack and loading the accumulator with zero before returning is absolutely vital. This multiple command interpreter will not work properly if you leave out this essential last routine. This interpreter will be developed further in the next module to include a *HELP service. 10 REM: SOUNDS 20 MODE7 30 HIMEM=&3C00 40 DIM save 50 50 diff=&8000-HIMEM 60 address=&A8 70 comvec=&F2 80 gsinit=&FFC2 90 gsread=&FFC5 100 osword=&FFF1 110 oscli=&FFF7 120 FOR pass = 0 TO 2 STEP 2 130 P%=HIMEM 140 [ OPT pass 150 BRK 160 BRK 170 BRK 180 JMP service+diff 190 OPT FNequb(&82) 200 OPT FNequb((copyright+diff) MOD 256) 210 BRK 220 OPT FNequs("SOUNDS") 230 .copyright 240 BRK 250 OPT FNequs("(C)1987 Gordon Horsington") 260 BRK 270 .service 280 CMP #4 290 BEQ unrecognised 300 RTS 310 .unrecognised 320 PHA 330 TXA 340 PHA 350 TYA 360 PHA 370 LDA address 380 PHA 390 LDA address+1 400 PHA 410 LDX #&FE 420 TYA 430 PHA 440 .firstchar 450 INX 460 PLA 470 TAY 480 PHA 490 LDA (comvec),Y 500 AND #&DF 510 CMP #ASC("X") 520 BNE interpret 530 INY 540 .interpret 550 INX 560 LDA commtable+diff,X 570 BMI found 580 LDA (comvec),Y 590 INY 600 CMP #ASC(".") 610 BEQ founddot 620 AND #&DF 630 CMP commtable+diff,X 640 BEQ interpret 650 .another 660 INX 670 LDA commtable+diff,X 680 BPL another 690 CMP #&FF 700 BNE firstchar 710 .exit 720 PLA 730 PLA 740 STA address+1 750 PLA 760 STA address 770 PLA 780 TAY 790 PLA 800 TAX 810 PLA 820 RTS 830 .founddot 840 INX 850 LDA commtable+diff,X 860 BPL founddot 870 .found 880 CMP #&FF 890 BEQ exit 900 STA address+1 910 INX 920 LDA commtable+diff,X 930 STA address 940 PLA 950 SEC 960 JSR gsinit 970 JMP (address) 980 .commtable 990 OPT FNequs("SIREN") 1000 OPT FNequb((siren+diff) DIV 256) 1010 OPT FNequb((siren+diff) MOD 256) 1020 OPT FNequs("HEHE") 1030 OPT FNequb((hehe+diff) DIV 256) 1040 OPT FNequb((hehe+diff) MOD 256) 1050 OPT FNequs("UFO") 1060 OPT FNequb((ufo+diff) DIV 256) 1070 OPT FNequb((ufo+diff) MOD 256) 1080 OPT FNequb(&FF) 1090 .hehe 1100 LDA #8 1110 LDX #(ehehe+diff) MOD 256 1120 LDY #(ehehe+diff) DIV 256 1130 JSR osword \ Envelope 1140 LDA #7 1150 LDX #(shehe+diff) MOD 256 1160 LDY #(shehe+diff) DIV 256 1170 JSR osword \ Sound 1180 .pullout 1190 PLA 1200 STA address+1 1210 PLA 1220 STA address 1230 PLA 1240 PLA 1250 PLA 1260 LDA #0 1270 RTS 1280 .siren 1290 LDA #8 1300 LDX #(esiren+diff) MOD 256 1310 LDY #(esiren+diff) DIV 256 1320 JSR osword \ Envelope 1330 LDA #7 1340 LDX #(ssiren+diff) MOD 256 1350 LDY #(ssiren+diff) DIV 256 1360 JSR osword \ Sound 1370 JMP pullout+diff 1380 .ufo 1390 LDA #8 1400 LDX #(eufo+diff) MOD 256 1410 LDY #(eufo+diff) DIV 256 1420 JSR osword \ Envelope 1430 LDA #7 1440 LDX #(sufo+diff) MOD 256 1450 LDY #(sufo+diff) DIV 256 1460 JSR osword \ Sound 1470 JMP pullout+diff 1480 .ehehe 1490 OPT FNequd(&02FF0301) 1500 OPT FNequd(&461E1E32) 1510 OPT FNequd(&FFFFFF7F) 1520 OPT FNequw(&7E7E) 1530 .shehe 1540 OPT FNequd(&00010011) 1550 OPT FNequd(&00320075) 1560 .esiren 1570 OPT FNequd(&07F90102) 1580 OPT FNequd(&000A0A00) 1590 OPT FNequd(&8200007E) 1600 OPT FNequw(&7E7E) 1610 .ssiren 1620 OPT FNequd(&00020012) 1630 OPT FNequd(&00FF0088) 1640 .eufo 1650 OPT FNequd(&FF010103) 1660 OPT FNequd(&01080700) 1670 OPT FNequd(&FF00FF03) 1680 OPT FNequw(&7E7E) 1690 .sufo 1700 OPT FNequd(&00030013) 1710 OPT FNequd(&003C0082) 1720 .lastbyte 1730 ] 1740 NEXT 1750 INPUT'"Save filename = "filename$ 1760 IF filename$="" END 1770 $save="SAVE "+filename$+" "+STR$~(HIMEM)+" "+STR$~(las tbyte)+" FFFF8000 FFFF8000" 1780 X%=save 1790 Y%=X% DIV 256 1800 *OPT1,2 1810 CALL oscli 1820 *OPT1,0 1830 END 1840 DEFFNequb(byte) 1850 ?P%=byte 1860 P%=P%+1 1870 =pass 1880 DEFFNequw(word) 1890 ?P%=word 1900 P%?1=word DIV 256 1910 P%=P%+2 1920 =pass 1930 DEFFNequd(double) 1940 !P%=double 1950 P%=P%+4 1960 =pass 1970 DEFFNequs(string$) 1980 $P%=string$ 1990 P%=P%+LEN(string$) 2000 =pass