ALIST1 MACLALIST2 MACO ALIST3 MAC[:EXAMPLESLOG<NOTICE SAVLOG COMMTRICKS22DOCg{4.PO 3 ; This is the FINDEM program that will identify some specific memory ; locations needed for listing 2. This code will assemble directly ; using Microsoft's M80. Other assemblers may use DEFB and DEFW storage ; declarations instead of DB and DW. ; ; If it was necessary to use MOVCPM and SYSGEN to open up 2K space above ; the BIOS for the LOGICAL NAME TRANSLATOR, then this program should be ; run with the smaller system size. ; ; This program will scan the BIOS looking for references to the CCP and ; notify you what values should be used for some EQU statements at the ; start of the LNT program in listing 2. ; .Z80 ASEG ORG 100H ; good place to start CR EQU 0DH ; carriage return LF EQU 0AH ; linefeed ; START: LD DE,CTMSG ; point to "CTARG IS " msg CALL PRLINE ; and send to console LD HL,(0001) ; get location of BIOS+3 LD DE,1603H-01A2H ; calculate offset to expected CTARG XOR A ; insure carry not set SBC HL,DE ; put expected CTARG address in HL LD A,5 ; should be a 05 there CP (HL) ; is it? JR Z,CCPOK ; if so, all is well, continue LD DE,NFMSG ; else point to "NOT FOUND" msg JP PRLINE ; send it and exit ;==================================================================== ;NOTE: If the NOT FOUND msg appears, then the CCP has been modified ; or is not where it should be at 1600h bytes below the BIOS. ; We are looking for a subroutine call ( CD 05 00 ) at location ; CCP+01A1 in order to pick up the destination at CCP+01A2 and ; CCP+01A3 which is needed by the LNT. ;==================================================================== ; CCPOK: LD A,H ; put the high byte of CTARG in A CALL SENDAX ; and send it to the console LD A,L ; put the low byte of CTARG in A CALL SENDAX ; and send it to the console. LD BC,0800H ; search counter set to 2K bytes. LD HL,(0001) ; get location of BIOS+3 LD DE,1603H ; get offset to CCP XOR A ; insure carry not set PUSH HL ; save the BIOS+3 location SBC HL,DE ; HL will contain address of CCP EX DE,HL ; so will DE. POP HL ; retrieve location of BIOS+3 ;==================================================================== ; Start of BIOS scan loops. We are looking for BIOS references to ; the CCP in LOOP1, and references to CCP+3 in LOOP2. ;==================================================================== ; LOOP1: LD A,E ; put low byte of CCP address in A CP (HL) ; match in the BIOS? JR NZ,GOON ; if not, go on INC HL ; else check the next byte LD A,D ; to see if the high byte also matches CP (HL) ; does it? JR NZ,GOON ; if not, go on DEC HL ; else we found a BTARG...point the low byte DEC HL ; Point to previous byte. CALL ISJMP ; Check to see if this is a jump. INC HL ; Look at the low byte again. JR NZ,GOON ; If not type of jump, forget this one. PUSH BC ; save the counter PUSH DE ; and the CCP address LD DE,GOTBT ; get ready to identify the BTARG. CALL PRLINE ; send the "BTARG" msg, LD A,(COUNT) ; put 1,2,3 or 4 in A INC A ; increment the BTARG counter, LD (COUNT),A ; and store it away. CALL SENDA ; and send the counter to console LD DE,AT ; point to " AT " string CALL PRLINE ; and send that, LD A,H ; point to high byte of this BTARG CALL SENDAX ; and tell us what it is, LD A,L ; point to low byte, CALL SENDAX ; and tell us what it is. POP DE ; retrieve CCP address. POP BC ; and the scan counter, GOON: INC HL ; move on along to next BIOS byte, DEC BC ; have we scanned 2K yet? LD A,C ; if not, OR B ; then JR NZ,LOOP1 ; continue scanning for CCP matches. LD BC,0800 ; done with first scan. Reset counter. LD A,'0' ; and the target id counter. LD (COUNT),A ; store it. LD HL,(0001) ; now point to BIOS+3 once again INC DE ; this time we look for INC DE ; reference to CCP+3, so INC DE ; DE in incremented by 3 LOOP2: LD A,E ; put low byte of CCP+3 in A CP (HL) ; match in BIOS? JR NZ,CONT ; if not, continue INC HL ; looks good so far, LD A,D ; check the high byte. CP (HL) ; found CCP+3 reference? JR NZ,CONT ; if not, continue DEC HL ; found a WTARG..back up to 1st byte. DEC HL ; Point to previous byte. CALL ISJMP ; Check to see if this is a jump. INC HL ; Look at the low byte again. JR NZ,CONT ; If not type of jump, forget this one. PUSH BC ; save the scan counter PUSH DE ; and CCP+3 location LD DE,GOTWT ; point to " WTARG " string CALL PRLINE ; and send that, LD A,(COUNT) ; put 1,2,3 or 4 in A INC A ; bump it for next time LD (COUNT),A ; and store it away CALL SENDA ; send which WTARG number this is. LD DE,AT ; point to " AT " string CALL PRLINE ; and send the string LD A,H ; put high byte of this WTARG in A CALL SENDAX ; and send it to console LD A,L ; ditto for the low byte CALL SENDAX ; to tell us where this WTARG is. POP DE ; retrieve CCP+3 location POP BC ; and the scan counter. CONT: INC HL ; point to next BIOS byte. DEC BC ; have we checked 2K bytes yet? LD A,C ; if not OR B ; then JR NZ,LOOP2 ; keep scanning. JP Z,0000 ; if so, then all done. ; ;==================================================================== ; NOTE: It may very well be the case that no BTARG msg is sent, or no ; WTARG msg is sent, but at least ONE msg of either type should ; appear. As many as 4 msgs of each type may be sent. If more than ; four jumps to the CCP or CCP+3 occur in your BIOS, you better ; write me a letter. ;==================================================================== ; ; Subroutine to determine if the byte pointed to by HL is a type of JUMP. ; ISJMP: LD A,(HL) ; Load the byte at HL. CP 0C3H ; If its an unconditional jump, RET Z ; then return. CP 0DAH ; Else, is it jump on carry? RET Z ; If so, then return. CP 0D2H ; Is it jump on no carry? RET Z ; If so return. CP 0CAH ; Is it jump on zero? RET Z ; If so return. CP 0C2H ; Is it jump on non-zero? RET ; If so return zero, else non-zero. ; ; ; Subroutine to send a string to the console. ; PRLINE: PUSH HL LD C,9 CALL 0005 POP HL RET ; ; Subroutine to print character in A on console. ; SENDA: PUSH HL LD E,A LD C,2 CALL 0005 POP HL RET ; ; Subroutine to send the two character ASCII hex of value in A ; SENDAX: PUSH AF ; save A RRCA ; shift the high nibble RRCA ; down to RRCA ; the RRCA ; low nibble CALL ZIP ; go the low nibble POP AF ; retrieve the original value ; ; we will fall through to the ZIP routine for the second nibble ; ; This little piece of code takes a value from 0 to F and produces the ; correct ASCII code to print '0' to 'F'. I love it. ; ZIP: AND 0FH ; mask off the high nibble ADD A,90H ; perform magic DAA ; once, ADC A,40H ; perform magic DAA ; twice, CALL SENDA ; and send it. RET ; ; CTMSG: DB CR,LF,'CTARG IS $' NFMSG: DB 'NOT FOUND!$' GOTBT: DB CR,LF,'BTARG$' GOTWT: DB CR,LF,'WTARG$' AT: DB ' AT $' COUNT: DB '0' END .PO 3 ; This is the LOGICAL NAME TRANSLATOR program. Care has been taken to avoid ; any uncommon Z80 pseudo ops, except perhaps the one on the first line. ; The only Z80 specific instructions are the block moves and relative jumps ; so that adapting to 8080 machines requires minimal effort. This code will ; assemble directly using Microsoft's M80. Other assemblers may use DEFB ; and DEFW storage declarations instead of DB and DW. ; .Z80 ASEG ORG 100H ; ;======================================================================= ; THE VALUE OF DTOUR MUST BE CHANGED TO BE THE START OF THE 2K FREE RAM ; ABOVE THE BIOS. IT IS CURRENTLY SET TO 58K. ;======================================================================= ; DTOUR EQU 0E800H ; Points to 2K RAM space above BIOS ; OFF EQU DTOUR - SCODL ; offset value used everywhere LASTAD EQU DTOUR + 2046 ; end of the logical name table CPMIO EQU 5 ; ;======================================================================== ; SOME OF THE FOLLOWING EQU VALUES MUST BE CHANGED IF IDENTIFIED BY THE ; FINDEM PROGRAM IN LISTING 1. THOSE NOT SPECIFIED BY THAT PROGRAM SHOULD ; BE LEFT ALONE. ;======================================================================= ; CTARG EQU 0BEA2H ; this one will be changed for sure. BTARG1 EQU 0D3C8H ; modify if identified by FINDEM program. BTARG2 EQU KEEP+OFF ; modify if identified by FINDEM program. BTARG3 EQU KEEP+OFF ; modify if identified by FINDEM program. BTARG4 EQU KEEP+OFF ; modify if identified by FINDEM program. WTARG1 EQU 0D3CBH ; modify if identified by FINDEM program. WTARG2 EQU KEEP+OFF ; modify if identified by FINDEM program. WTARG3 EQU KEEP+OFF ; modify if identified by FINDEM program. WTARG4 EQU KEEP+OFF ; modify if identified by FINDEM program. ; START: LD HL,(BTARG1) ; sorry, but a little self modifying code LD (FIXX1+1),HL ; to deal with unknowns is a must... LD HL,(WTARG1) ; but only LD (FIXX2+1),HL ; twice. LD HL,(CTARG) ; pick up the subroutine call in the CCP LD DE,DOOR+OFF ; point to our own destination instead, XOR A ; insure carry not set SBC HL,DE ; and see if the LNT is already active. JR Z,READY ; If so, then we can skip the next part, LD HL,SCODL ; else get ready to move the code LD DE,DTOUR ; to the 2K space above the BIOS. LD BC,ECODL-SCODL ; Get length of the code LDIR ; and do the block move. LD HL,DTOUR ; Point to the entry address BIO1: LD (BTARG1),HL ; and modify the BIOS to intercept warm LD (BTARG2),HL ; starts to the CCP in all the right LD (BTARG3),HL ; places found by FINDEM program. LD (BTARG4),HL ; Should never be this many of them. LD HL,DTOUR+3 ; All BIOS jumps to CCP+3 will now go to BIO2: LD (WTARG1),HL ; DTOUR+3, too, in case your BIOS does LD (WTARG2),HL ; any fancy stuff. There really shouldn't LD (WTARG3),HL ; be more than one or two, but you never LD (WTARG4),HL ; know. READY: LD A,(80H) ; Get length of command line extras, OR A ; if not zero JR NZ,RDLOG ; then go read the file in RBYE: LD (LNT+OFF),A ; else no file given, put a zero in LNT. JP GCLIP ; and go say goodbye. RDLOG: LD A,(0065H) ; See if there was a extention on the CP ' ' ; file name. Is extention blank? JR NZ,GOODN ; If not, never mind, whole name given. LD A,'L' ; Else fill it in LD (0065H),A ; with an L LD A,'O' ; and LD (0066H),A ; with an O LD A,'G' ; and LD (0067H),A ; with a G. GOODN: LD DE,005CH ; Ready to read it, point to FCB, LD C,0FH ; set up the OPEN function, CALL CPMIO ; and open the file. CP 0FFH ; a problem (like not there)? JR NZ,DORD ; If not, then ready to load it in, LD DE,NOTF ; else point to NOT FOUND, LD C,9 ; set up to PRINT STRING, CALL CPMIO ; send to console, JR RBYE ; and abort the program. DORD: LD DE,LDMA ; We want to load the file at LDMA RDLP: PUSH DE ; save that address LD C,1AH ; set up the SET DMA function CALL CPMIO ; and tell CPM where it goes. LD DE,005CH ; Point to the FCB, LD C,14H ; set up the READ function, CALL CPMIO ; and pull in a sector from disk. OR A ; All done? JR NZ,RDONE ; Yes! then get out of the read loop POP HL ; else retrieve the DMA address LD DE,0080H ; bump it by 128 ADD HL,DE ; and now we have a new DMA address. EX DE,HL ; Give it to DE JR RDLP ; and go read another sector. RDONE: POP HL ; Retrieve the last DMA address LD A,1AH ; and put a CPM end-of-file mark LD (HL),A ; there just to be safe. LD HL,LDMA ; Point to start of file in memory, SYNTH: LD DE,FAKE+2 ; point to our CCP look-alike buffer, LD BC,000DH ; store a carriage return in C, 0 in B, MLOOP: LD A,(HL) ; look at a byte from the file. CP C ; Is it carriage return? JR Z,MDONE ; If so, stop filling the buffer, INC B ; else count how many bytes we've moved LD (DE),A ; and put a byte from the file into buffer. INC DE ; Bump the buffer pointer, INC HL ; bump the file pointer, JR MLOOP ; and go look at the next byte. MDONE: INC HL ; Should be a linefeed after the return INC HL ; and the next byte is start of new line. PUSH HL ; Save the file pointer, LD HL,FAKE+1 ; point to byte count location in buffer, LD (HL),B ; and put the length of the line there. DEC HL ; Back up to very start of buffer LD (BUF0+OFF),HL ; pretend we are calling from the CCP ! CALL BDOOR+OFF ; and take a shortcut into the translator. POP HL ; LNT updated his table, now back to the file LD A,(HL) ; Get the first byte from the next line. CP 1AH ; All done by any chance? JR NZ,SYNTH ; No, go process another line. JP GCLIP ; All done. Go say goodbye. FAKE: DB 127 ; Start of CCP buffer look-alike, DS 127 ; with same length. GCLIP: LD DE,OKMSG ; Point to OK message. LD C,9 ; Send the message at DE, CALL CPMIO ; and AU REVOIR ! JP 0000 OKMSG: DB ' OK$' ; ; main segment to be relocated to high memory begins here.. ; SCODL: JP $MAIN+OFF ; Entry from BIOS (JMP CCP) PLUS3: JP $MAIN2+OFF ; Entry from BIOS (JMP CCP+3) BUF0: DW 0 ; Storage for CCP's buffer address KEEP: DW 0 ; Storage for general use FIN: DW 0 ; Storage for end of buffer address NOTF: DB 13,10,'NOT FOUND',13,10,'$' NOWAY: DB 13,10,'NO ROOM',13,10,'$' IERR: DB 13,10,'NO TRANSLATION..RE-ENTER.',13,10,'$' $LOG: DB 4,'#LOG' $DROP: DB 5,'#DROP' DS 24 ; Local stack space SAVE: DW 0 ; Storage for CP/M's stack pointer $MAIN: LD HL,DOOR+OFF ; Initialize the CCP on warm boot LD (CTARG),HL ; by altering the BDOS call FIXX1: JP 0000 ; This will be a JMP to CCP ; $MAIN2: LD HL,DOOR+OFF ; Initialize CCP on fancy boot LD (CTARG),HL ; by altering the BDOS call FIXX2: JP 0000H ; This will be a JMP to CCP+3 ; ; THE ADDRESS OF THE TABLE IS PUT AT DOOR-2 SO THAT SAVLOG CAN FIND IT BY ; CHECKING THE CALL ADDRESS FROM THE CCP AND SUBTRACTING 2. ; DW LNT+OFF ; Keep start of table for SAVLOG ; ; HERE IS WHERE THE CCP WILL CALL WHEN HE WANTS AN INPUT LINE ; DOOR: LD (BUF0+OFF),DE ; Save CCP's buffer address CALL CPMIO ; then go get the input line. BDOOR: LD HL,(BUF0+OFF) ; HL gets the start of buffer INC HL ; plus one (length of line). LD A,(HL) ; Did you just type ? OR A ; If so, then theres nothing to RET Z ; to look at..return to CCP. LD D,H ; else let DE point to line length, LD E,L ; then add the length to DE so CALL ADE+OFF ; that it points to end of line, INC DE ; plus one, then XOR A ; put a 00 byte there to LD (DE),A ; terminate the input line, and LD (FIN+OFF),DE ; store the end-of-line address. PUSH HL ; Save HL (buffer start+1). LCLP: INC HL ; Point to next byte of line, LD A,(HL) ; and check to see OR A ; if its the 00 terminator, JR Z,CHKOV ; If so, the check is over, jump. CP 'a' ; Is the character lower case? JR C,LCLP ; If not, continue CP '{' ; else this code will convert to JR NC,LCLP ; uppercase. (The CCP does the AND 0DFH ; same thing later, but we need LD (HL),A ; to do it NOW). JR LCLP ; Continue converting to upper. CHKOV: POP HL ; Retrieve start of bufer+1. INC HL ; point to first byte of line. LD A,(HL) ; Is this an INSTRUCTION line? CP '#' ; If so, hey..wait we better LD (SAVE+OFF),SP ; set up our own stack in either LD SP,SAVE+OFF ; case. Save his Stack pointer. JP Z,ANLIZ+OFF ;...If so, then go process line. ; ; THIS IS NOT A LNT INSTRUCTION ; EX DE,HL ; DE gets start of buffer SCNLP: CALL GETWD+OFF ; get the piece of input at DE JR Z,DONE ; If no more pieces, get out. LD A,(DE) ; else look at last char scanned. CP ':' ; Was the seperation char a : ? JR NZ,NOTD ; If not, never mind, jump NOTD LD A,1 ; else see if piece was only CP B ; one character (maybe drive spec). JR Z,SCNLP ; if so, ignore this piece, loop. NOTD: PUSH DE ; Got something. Save line ptr. CALL FIND+OFF ; go see if its in table. CALL NZ,XLATE+OFF ; If it is, do translation. POP DE ; If not, remember where we were, JR SCNLP ; and continue scannin the line. DONE: LD HL,(BUF0+OFF) ; Done scanning, point HL to buffer INC HL ; plus one, and INC HL ; finally to line length byte. LD BC,273BH ; Load a ; and ' into B and C. CLEAN: LD A,(HL) ; Put byte from line into A. OR A ; Are we at end-of-line? JP Z,BYE+OFF ; If so, then go wind down, CP B ; else check for ; JR Z,CHOP1 ; If ; found, go eliminate it. CP C ; else check for ' JR Z,CHOP1 ; If ' found, go eliminate it. INC HL ; point to next byte in line, JR CLEAN ; and continue cleaning up. CHOP1: CALL ERA1+OFF ; A ; or ' found, delete it. JR CLEAN ; and continue cleaning up. ; ; ERA1. DELETE ONE CHARACTER POINTED TO BY HL, AND AJUST LINE LEN. ; ERA1: PUSH HL ; Save PUSH DE ; every- PUSH BC ; body. LD DE,(FIN+OFF) ; DE looks at end-of-line. EX DE,HL ; Now HL does, DE to target byte. XOR A ; Insure carry not set, SBC HL,DE ; and find out how far from end. LD B,H ; Byte count to end goes into LD C,L ; register BC. PUSH DE ; Copy the address of byte to POP HL ; delete into HL, INC HL ; point to byte following, LDIR ; and shift the whole line down. LD HL,(FIN+OFF) ; Get the old end-of-line address, DEC HL ; subtract one, LD (FIN+OFF),HL ; to update it. LD HL,(BUF0+OFF) ; Look back at the start of buffer INC HL ; plus one (line length), DEC (HL) ; and update the length. POP BC ; Bring POP DE ; everybody POP HL ; back, and RET ; return. ; ; XLATE. Replaces the logical name with its string equivalent. ; XLATE: LD A,(DE) ; A gets length of logical name LD B,A ; from table, then copy to B. DELLP: CALL ERA1+OFF ; Delete the logical name from the DJNZ DELLP ; input line, char by char. LD A,(DE) ; A gets length once again, CALL ADE+OFF ; add to DE to point to the string INC DE ; definition length in the table. PUSH HL ; Save pointer into input line. PUSH DE ; Save pointer to string in table. LD A,(DE) ; A gets replacement string length, LD B,A ; and so does B ( loop counter ). ADDLP: CALL ADD1+OFF ; Insert extra bytes into the input DJNZ ADDLP ; line to make room for string. POP HL ; Retrieve pointer to string length LD C,(HL) ; from table and copy into C so LD B,0 ; that BC is byte counter of move. INC HL ; Now point to the string itself, POP DE ; get the pointer into input line. PUSH DE ; Keep a copy on the stack. LDIR ; Block move the string into line. POP DE ; Revive the pointer into the line, POP BC ; This is the return address!! POP HL ; We are replacing the input line PUSH DE ; pointer deep on the stack. PUSH BC ; Restore the return address, RET ; and the translation is done. ; ; ADD1 This routine duplicates the byte at HL on the input line and ; slides the rest of the line down to make room. ; ADD1: PUSH HL ; Save PUSH DE ; every- PUSH BC ; body. LD DE,(FIN+OFF) ; DE points to 00 at end of line. EX DE,HL ; Now HL does. DE points into line. XOR A ; insure carry not set. SBC HL,DE ; How many bytes must be shifted? LD B,H ; BC will contain LD C,L ; the byte count. INC BC ; (Be sure to move the 00 too). LD HL,(FIN+OFF) ; HL points to 00 at end of line. PUSH HL ; Save this spot while we INC HL ; update this pointer LD (FIN+OFF),HL ; by one location. POP DE ; Now end of line goes to DE, EX DE,HL ; swapped with HL, and LDDR ; the whole line is shifted by one. ADONE: LD A,(DE) ; If the last byte moved was not OR A ; the 00 (we were at end of line) JR NZ,ABYE ; then jump to ABYE. LD A,'&' ; Else we have 2 00 bytes. LD (DE),A ; Generate a non-zero dummy byte. ABYE: LD HL,(BUF0+OFF) ; Point to input line length INC HL ; and update it by adding one INC (HL) ; for the byte we just added. POP BC ; Restore POP DE ; every- POP HL ; body. RET ; and return. ; ; THIS WAS A LNT COMMAND...Sentinel character detected in column 1. ; ANLIZ: EX DE,HL ; Point DE to start of input line. CALL GETWD+OFF ; Pick up the first word of input, PUSH DE ; and save address of terninator. LD DE,$LOG+OFF ; Point to "#LOG" string. CALL TEST+OFF ; Does that match the input? JR NZ,DCHCK ; If not go check for DROP. ; ; COMMAND WAS #LOG ; CALL CRLF+OFF ; Send CR-LF to console. LD DE,LNT+OFF ; point to start of table. LOGLP: LD A,(DE) ; get next entry offset byte. OR A ; Is it zero (end of table)? JP Z,IGNOR+OFF ; If so we are done. Jump out. INC DE ; Else point to length of name, CALL SSTR+OFF ; and go send the name to console. LD A,':' ; Now send a colon to CALL SEND1+OFF ; follow the name, and LD A,'=' ; then an = to make it CALL SEND1+OFF ; look nice on the screen. INC DE ; Point to length of definition CALL SSTR+OFF ; string and go send the string. CALL CRLF+OFF ; Send a CR-LF INC DE ; and we should be at start of next JR LOGLP ; table entry. Loop until at end. ; DCHCK: LD DE,$DROP+OFF ; Point to "#DROP" string. CALL TEST+OFF ; Is that the instruction? JP NZ,NEWID+OFF ; If not, go test for new name. ; ;COMMAND WAS #DROP ; POP DE ; Retrieve pointer to next word on CALL GETWD+OFF ; input line and go get it. JR Z,NFMSG ; If nothing there, goto error. CALL FIND+OFF ; Else hunt the name in the table. JR Z,NFMSG ; If not in table, goto error. CALL KILL1+OFF ; Otherwise eliminate table entry. JP IGNOR+OFF ; We are done. Get out. IMSG: LD DE,IERR+OFF ; Point to "COMMAND ERROR" JR SPILL ; Go print it. NFMSG: LD DE,NOTF+OFF ; Point to "NOT FOUND" SPILL: LD C,9 ; Print the message CALL CPMIO ; and JP IGNOR+OFF ; get out. ; ; TEST FOR NEW IDENTIFIER. ; NEWID: POP DE ; Retrieve pointer to word term- LD A,(DE) ; inator from input line. CP ':' ; Was it a colon? JR NZ,IMSG ; If not, ?????, go tell him ?? ; WAS NEW IDENTIFIER.. PUSH DE ; Save pointer to : INC HL ; Point to byte after # char. PUSH HL ; and save that location. DEC B ; Adjust length of new name, PUSH BC ; and save it. CALL FIND+OFF ; Hunt the name in the table. CALL NZ,KILL1+OFF ; and eliminate it if found. BUILD: LD DE,LNT+OFF ; Point to start of table, POP BC ; and retrieve the name length. ENDLP: LD A,(DE) ; Look at this byte from table. OR A ; Is it 0 (at end of table)? JR Z,GEND ; If so, stop looking for end. INC DE ; Else keep looking JR ENDLP ; for end of table. GEND: LD (FIN+OFF),DE ; FIN can be used for temp storage. INC DE ; Point to byte after end of table. CALL FULLCK+OFF ; See if we have room for this entry. LD A,B ; A gets length of new name. LD (DE),A ; Store into table, INC DE ; point to next table byte. POP HL ; Retrieve pointer to name in line. CALL CPYLP+OFF ; and put a copy into the table. LD (KEEP+OFF),DE ; Store where we are in the table. POP DE ; Retrieve pointer to : in line. CALL GETWD+OFF ; Pick up the definition string. JR Z,IMSG ; If not any, send error and out. LD DE,(KEEP+OFF) ; Where were we in the table.. CALL FULLCK+OFF ; See if we have room in the table. LD A,B ; A gets length of string def. LD (DE),A ; Store into the table, INC DE ; and follow the length with a CALL CPYLP+OFF ; copy of the string definition. XOR A ; A is 0. LD (DE),A ; Make a new end of table marker. LD HL,(FIN+OFF) ; Retrieve old marker (still 0), EX DE,HL ; Swap these addresses. SBC HL,DE ; How many bytes did we add? LD A,L ; Put answer in A (less than 255). LD (DE),A ; and udate the old marker. JP IGNOR+OFF ; We are done. Get out. ; ; GENERAL PURPOSE SENDSTRING ROUTINE. DE POINTS TO LEN+STRING BUFFER ; SSTR: LD A,(DE) ; A gets length of string to send. PUSH BC ; Save whatever is in BC. LD B,A ; B will be byte counter. SSLP: INC DE ; Point to next byte to print. LD A,(DE) ; Put it into A, CALL SEND1+OFF ; and send it to console. DJNZ SSLP ; Loop until done. POP BC ; Restore BC. RET ; Bye. ; ; CPYLP ..BLOCK MOVE STRING AT HL TO LOCATION DE. B HAS THE LENGTH. ; CPYLP: LD A,(HL) ; Get a byte. LD (DE),A ; Copy it. INC HL ; Increment source and INC DE ; destination addresses DJNZ CPYLP ; and continue. RET ; ; KILL1. DROP AN ENTRY. DE POINTS TO IDENTIFIER LEN IN THE TABLE. ; KILL1: DEC DE ; Back up to start of entry, PUSH DE ; and copy this address POP HL ; into HL. LD A,(DE) ; A is length of this entry. CALL ADE+OFF ; Find the start of next entry, PUSH HL ; Save the start of entry to delete. LD HL,LASTAD ; Point to end of table space. SBC HL,DE ; Compute number of bytes to move. PUSH HL ; and copy into POP BC ; BC. POP HL ; Retrieve start of entry to delete. EX DE,HL ; Swap with next entry address. LDIR ; and shift the entire table down. RET ; ; SEND CHARACTER IN A TO CONSOLE ; SEND1: PUSH DE ; Save PUSH HL ; every- PUSH BC ; body. LD C,2 ; Function number for conout. LD E,A ; CP/M likes it in register E. CALL CPMIO ; Send it. POP BC ; Restore POP HL ; every- POP DE ; body, and RET ; return. ; ; SEND CR-LF TO CONSOLE ; CRLF: LD A,13 ; 13 is a carriage return. CALL SEND1+OFF ; Send it. LD A,10 ; 10 is a line feed. CALL SEND1+OFF ; Send it. RET ; ; GETWD ROUTINE. DE POINTS TO STRING AT ENTRY, AT TERMINATOR ON EXIT. ; HL POINTS TO START OF WORD, B IS LENGTH, C IS 1 IF ; A QUOTED STRING, ELSE 0. ZERO SET IF END OF BUFFER. ; GETWD: LD H,D ; Put a copy of DE LD L,E ; into HL, and LD BC,0000 ; zeroes in B and C. DEC HL ; Syncronize the loop. LEAD: INC HL ; Point to next byte of line, LD A,(HL) ; and see if at OR A ; end of line (00 byte). RET Z ; If so, goodbye. CALL PUNCK+OFF ; Else look for punctuation, and JR Z,LEAD ; skip over any you find. CP 39 ; Check for single quote. JR NZ,GSTRT ; If not quoted, never mind, jump. INC C ; Else set the quoted string flag. GSTRT: LD D,H ; DE points to first byte of word, LD E,L ; just like HL. GCONT: INC B ; bump the word length counter. INC HL ; Point to next byte on the line. LD A,(HL) ; and see if we are OR A ; at end of line (00 byte). JR Z,OUT ; If so, stop looking, jump. LD A,C ; Else check quoted string flag. OR A ; If quotes present, then JR NZ,QCK ; don't bother checking, jump. LD A,(HL) ; Otherwise, look for terminating CALL PUNCK+OFF ; punctuation and JR Z,OUT ; jump if thats the case. QCK: CP 39 ; Check for terminating quote. JR NZ,GCONT ; If not found, keep looking. Loop. INC B ; Got terminating quote. INC HL ; Point to byte following. OUT: EX DE,HL ; HL gets start. DE gets end of word. INC A ; Insure zero not set, RET ; and return. ; ; PUNCK. CHECKS FOR LEADING OR TRAILING PUNCTUATION. ; PUNCK: CP ' ' RET Z CP ',' RET Z CP ':' RET Z CP '=' RET Z CP ';' RET Z CP '+' RET Z CP '-' RET Z CP '[' RET Z CP ']' RET Z CP '/' RET Z RET ; ; FIND. SCANS LNT UNTIL A MATCH IS FOUND FOR STRING AT (HL). RETURNS ; ZERO IF NO MATCH, ELSE DE POINTS TO LEN BYTE OF ID STRING. ; FIND: LD DE,LNT+OFF ; Point to start of table. FLOOP: LD A,(DE) ; Get first byte of entry. OR A ; If at end of table, RET Z ; return with zero set. INC DE ; Else point to name length byte. CALL TEST+OFF ; Compare with string at HL. JR Z,GOTIT ; If a match, jump to GOTIT. FNXT: DEC DE ; Point to first byte of entry, LD A,(DE) ; pick up the entry length, CALL ADE+OFF ; and move the pointer to next one. JR FLOOP ; Continue checking for matches. GOTIT: INC A ; Match! Insure zero not set,and RET ; return. ; ; TEST. GENERAL PURPOSE STRING COMPARISON ROUTINE. DE+1 AND HL POINT TO ; STRINGS. B IS LENGTH. RETURNS ZERO SET IF TRUE MATCH. ; TEST: PUSH HL ; Save PUSH DE ; every- PUSH BC ; body. LD A,(DE) ; Get string length in table. CP B ; Is it same as string in line? JR NZ,TOV ; If not, the test is over. INC DE ; Point to first byte of string. TLOOP: LD A,(DE) ; Get a byte from table. CP (HL) ; Does it match so far? JR NZ,TOV ; If not, the test is over. INC HL ; Else look at next bytes to INC DE ; compare, and DJNZ TLOOP ; continue comparison. TOV: POP BC ; TRUE comparison. Zero is set. POP DE ; Restore POP HL ; everybody, and RET ; return. ; ; ADD VALUE IN A TO DE REGISTER PAIR ; ADE: ADD A,E LD E,A LD A,0 ADC A,D LD D,A RET ; ; FULL CHECK...IF NO MORE SPACE, SEND MSG AND ABORT. ; FULLCK: PUSH HL ; Save HL LD HL,LASTAD ; Point to last possible address. LD C,B ; BC will contain the length of LD B,0 ; characters we want to add. SBC HL,BC ; Adjust HL by that length. LD B,C ; Must preserve B register. SBC HL,DE ; If space comp is not positive, JR C,FULLUP ; then it won't fit. Blow it off. POP HL ; Else OK, restore HL and RET ; return. FULLUP: LD DE,NOWAY+OFF ; Point to NO ROOM msg, and JP SPILL+OFF ; bail out of here. ; IGNOR: LD HL,(BUF0+OFF) ; Point to start of input buffer, INC HL ; then to length of buffer. XOR A ; Load a zero there so the CCP LD (HL),A ; wont see the input at all. BYE: LD SP,(SAVE+OFF) ; Give the CCP his stack back, RET ; and return control to the CCP. ; LNT: DB 0 ; Start of NAME TABLE. ECODL: ; End of code, for computing offset. ORG 01000H ; A safe place to read in .LOG file. LDMA: DB 1AH ; A CP/M end-of-file mark. END .PO 3 ; This is the SAVLOG program that is used to create an ASCII text file ; of all LOGICAL NAME DEFINITIONS that are currently active. The output ; file can be read by the LNT program to onload the entire set of defin- ; itions at once or modified using standard editors. The output file- ; name is required on the command line. An extention type of .LOG is ; recommanded but not mandatory. This program will assemble directly ; using Microsoft's M80. Other assemblers may use DEFB and DEFW storage ; declarations instead and DB and DW. ; .Z80 ASEG ORG 100H CPMIO EQU 0005 ; CP/M entry point. FCB EQU 005CH ; Default file control block. CR EQU 13 ; Carriage return. LF EQU 10 ; Line feed. START: LD A,(0080H) ; See how long rest of line is. OR A ; If not zero, then filename JR NZ,CONT ; was specified. Go on.. LD DE,CERR ; Else you forgot. Point to msg, JP BYE ; print it and abort. CONT: LD DE,FCB ; Point to file control block. LD C,19 ; Set up delete function, and CALL CPMIO ; zap the file if it exists. LD DE,FCB ; Point to file control block. LD C,22 ; Set up MAKEFILE function, and CALL CPMIO ; try to create it. CP 255 ; If not bad return code, then JR NZ,READY ; continue. Jump to READY. LD DE,DFULL ; Else can't create file, notify JP BYE ; and abort. READY: LD HL,(0001) ; Get location of BIOS+3. LD DE,1603H-1A2H ; Calculate where CALL DOOR is XOR A ; Ensure carry not set, and SBC HL,DE ; HL should point to DOOR address. LD A,5 ; If a 05 is not found there, then CP (HL) ; assume the LNT is active, JR NZ,FOUND ; and jump to FOUND. LD DE,NOTUP ; If it was 05, notify that LNT is JP BYE ; not active and abort. FOUND: LD E,(HL) ; Pick up DOOR location found at INC HL ; HL and save in LD D,(HL) ; DE register. DEC DE ; Just before DOOR is strorage for LD A,(DE) ; address of table. Pick up high LD H,A ; byte and put into H and DEC DE ; pick up low byte and LD A,(DE) ; put into L, so that LD L,A ; HL now points to table. LD DE,OBUF ; Point DE to start of output buffer. LOOP: LD A,(HL) ; Get a byte from the table. OR A ; If you find a zero there, JR Z,DONE ; then jump to DONE. LD A,'#' ; Else not done, put a # LD (DE),A ; into the output buffer, INC DE ; bump the output buffer pointer, INC HL ; look a name length byte in table. LD C,(HL) ; put name length in C and LD B,0 ; zero B. BC contains name length. INC HL ; Point to start of name. LDIR ; Copy name to output buffer. LD A,':' ; We need a colon to follow LD (DE),A ; the name and INC DE ; after that LD A,'=' ; an equal sign LD (DE),A ; in the output buffer. INC DE ; Bump the output buffer pointer. LD C,(HL) ; HL will point to string length, LD B,0 ; so put that into BC like before. INC HL ; HL points to start of string. LDIR ; Move string definition to buffer. LD A,CR ; Next put a carriage return LD (DE),A ; into output buffer, INC DE ; followed by LD A,LF ; a line feed LD (DE),A ; and one whole output line is done. INC DE ; Bump the output buffer pointer, JR LOOP ; and continue until table ends. DONE: LD A,01AH ; Seal off the buffer with an LD (DE),A ; end-of-file character. EX DE,HL ; HL points to the EOF mark. LD DE,OBUF ; DE points to start of buffer. XOR A ; Ensure carry not set, SBC HL,DE ; and calculate length of buffer. RLC L ; Simple means to divide the RL H ; length by 128 to see how many INC H ; sectors to write to disk. LD B,H ; Put number of sectors in B. WLOOP: LD C,26 ; Set up DMA address. PUSH BC ; Save B PUSH DE ; Save where we are in buffer. CALL CPMIO ; Establish DMA address. LD DE,FCB ; Point to file control block. LD C,21 ; Set up WRITE SEQUENTIAL function, CALL CPMIO ; and write 128 bytes to disk. POP DE ; Retrieve our DMA address. POP BC ; Retrieve our sector counter. OR A ; Was write sucessful? JR NZ,WIND ; If not, close file, and notify. LD HL,80H ; Else update the DMA address ADD HL,DE ; to point to next 128 byte block, EX DE,HL ; which goes in DE, DJNZ WLOOP ; and loop until sector count=0. WIND: LD DE,FCB ; Point to file control block. LD C,16 ; Set up CLOSE function. JR Z,FIN ; and if all is well, close and end. CALL CPMIO ; else close and LD DE,DFULL ; point to error message, BYE: LD C,9 ; set up print function, FIN: CALL CPMIO ; perform function, JP 0000 ; and exit. CERR: DB 'FILENAME NOT SPECIFIED$' NOTUP: DB 'LNT NOT ACTIVE$' DFULL: DB 'DISK FULL OR R/O$' OBUF: DB 1AH END #TY:=TYPE any line that doesn't have the sentinal character in column one, like these, are treated as comments. #STAT:='A:STAT' the example above will let you see status without worrying about which drive it comes from. #SHOW:=STAT #D:=DIR #Q:=QED #X:=XSID #DEL:=ERA #DB:='DIR B:' #SHO:=SHOW #!:='*.*' #COPY:=NOTPIP #T:=TIME #ALL:='*.*' #U0:='USER 0' #U1:='USER 1' #U2:='USER 2' #U3:='USER 3' #U10:='USER 10' s! 9!9DM\ ͐)~#fo`is#r\͐! s#r͐ô!9+-----------------------------------------------------------------+ | ****** NOTICE ****** | | Copyright 1985 by Micro/Systems Journal | | PO Box 1192, Mountainside, NJ 07092 : | All rights reserved, reproduction prohibited without permission : +-----------------------------------------------------------------+:  \\  *aR>  ^#Vgo~(#>##N#>:>=N#> > >R$D\!\FILENAME NOT SPECIFIED$LNT NOT ACTIVE$DISK FULL OR R/O$:>=N#> > >+-----------------------------------------------------------------+ | ****** NOTICE ****** | | Copyright 1985 by Micro/Systems Journal | | PO Box 1192, Mountainside, NJ 07092 : | All rights reserved, reproduction prohibited without permission : +-----------------------------------------------------------------+ Abstracted from Micro/Systems Journal, March/April 1985 "New Tricks for CP/M 2.2" by David Brewer, PO Box 902306, Dallas, TX 75390 NEW TRICKS FOR CP/M 2.2: LOGICAL NAME TRANSLATION One of the most powerful features found in larger computer operating systems has somehow escaped the attention of the CP/M community entirely. Adding LOGICAL NAME TRANSLATION to enhance the CP/M man-machine interface is not difficult to implement or to understand; it is simply long overdue. The purpose of logical name translation is to simplify the dialogue between you and your operating system by "teaching" the system to recognize your own brand of shorthand . Having CP/M "know what you mean" can free you from the standard syntax and command usage rules you have been putting up with all this time. After all, who's in charge here, you or your microcomputer? By making this feature resident in the operating system, translating "what you say" into "what you mean" takes only a few milliseconds and in no way affects any applications software that you run on your machine. This article presents a technique for implementing the translation feature on Z-80 based CP/M 2.2 systems. The source listings are "low key" Z80 code (no use of index registers) so that adaptation for 8080 systems is not too difficult. WHAT IS A LOGICAL NAME ? Logical names are string type variables that can be defined and used at the operating system command line level. Wouldn't it be nice to define DB to represent the string 'DIR B:', and then be able to simply enter DB to produce a directory of disk B:? Logical names like DB are nothing more than a user-defined "shorthand" substitution for their string definitions. The beauty of the enhancement is that no disk access or any perceptable delay is introduced since the translation tables are maintained in memory. No renaming of files or commands is involved, and standard command usage is still valid. Logical names can be defined to represent ANY string, whether it be an entire command such as DB, a portion of a command, a filename, or any series of printable characters. In essence, by making logical name defini tions you are teaching CP/M to recognize a wider variety of command syntax. The logical names themselves are your own fabrication. On my system, ! means *.*, DEL and ERA mean the same thing, TY works the same as TYPE, and the list goes on. In 2K of memory, 50 to 100 definitions can be maintained, depending on string lengths. With a little imagination, you can see how CP/M could recognize commands in French, or execute lengthy PIP commands via a single keystroke. Handicapped users and Europeans take note! HOW DOES IT WORK? The CCP portion of CP/M is responsible for analyzing command lines, processing built-in commands, loading user programs, and handling SUBMIT procedures. Collecting a line of input however is done by calling a subroutine in the BDOS. The translator inter cepts this call so that the collected input line can be examined before the line is passed back to the CCP. If any logical names are detected on the input line, the translator replaces them with their string equivalents before returning control to the CCP. The translation is totally transparent to the CCP who thinks you have entered a standard command line. The catch to the enhancement is that the logical name trans lator must reside somewhere in memory at all times and not get trampled by applications programs. The only protected RAM space is that ABOVE the BIOS. MAKING ROOM IN THE ATTIC The logical name translator is going to cost 2K of memory, but it doesn't require any permanent changes to the operating system code itself since it is installed on the fly. If you don't want to use the feature, you don't have to bring it into the environment. Finding a place to hide the translator is the only problem to solve. Many systems already have RAM collecting dust in the "attic" above the BIOS due to disk controller or monitor ROMs in the 56K to 62K range. Phantom options on memory boards, or even an old fashioned 4K RAM board could be employed to provide the 2K space. In the worst case, the MOVCPM and SYSGEN programs must be used to generate a new CP/M system that is 2K smaller in size. Before you wince at the thought, consider how many programs you have that won't run in 2K less space and how often you will probably use the translation feature. HOW DO YOU USE IT? Once the translator is loaded, logical names may be used freely on any CP/M command line. Communicating with the transla tor to define new logical names, delete old ones, or produce a list of currently active names is quite simple. If the first character of a command line, immediately following the CP/M prompt, is the sentinel character # then the remainder of the line will be treated as an INSTRUCTION to the translator. After the instruction is processed, an empty line will be forwarded to the CCP who wouldn't know what to do with the instruction anyway. The translator does no disk access. The instruction to define (or redefine) a logical name SC might look like this: A>#SC:='STAT *.COM' In this case, SC has been chosen to represent an entire command to produce a status display of all .COM files on the disk. Notice that the logical name is followed by a colon and equal sign, followed by its string definition enclosed in single quotes. From this point forward, any appearance of the name SC will cause the definition string to be substituted. The simple command: A>SC will produce the same status display as the standard command. If no blanks or special characters are included in the string, the single quote delimiters are not required as in: A>#DEL:=ERA or A>#KILL:=DEL The second example shows that logical names can be defined in terms of other logical names. Now, ERA, DEL, and KILL can all be used interchangably to do the same thing. If you like a little class, try these: A>#SHOW:=STAT A>#ALL:='*.*' A>#COMS:='*.COM' Now the status utility can be invoked with some finesse: A>SHOW ALL and A>SHOW COMS My fingers never manage hit the W key on this one, so I gave in: A>#SHOE:=SHOW and SHOE, SHOW, and STAT all look identical to my system. If you are hacking away at a FORTRAN program called B:HALSTEAD.FOR you might set up some definitions like these: A>#F:='F80 B:HALSTEAD,=B:HALSTEAD.FOR' A>#L:='L80 B:HALSTEAD,B:HALSTEAD/N/E' Now, compiling is a one keystroke affair: A>F and so is linking: A>L You could, of course, put these commands in a submit procedure, and just to make it fun: A>#GO:='SUBMIT B:HALSJOB' then crank up the procedure by simply typing: A>GO Since extra spaces are generally acceptable on command lines, you can be creative with the FORTRAN compiler command: A>#Z:='B:HALSTEAD' A>#COMPILE:='F80 =' and actually make the command readable (imagine that!) A>COMPILE Z SOME SPECIAL EFFECTS An interesting side effect is that semicolons and single quote characters ( normally invalid on command lines ) are always filtered out by the translator but can be used as separaters. Single quotes can be used to designate portions of a command to be excluded from translation, and semicolons can be used for concatenation of logical names to adjacent characters or other logical names. Consider: A>#T:='TYPE' A>#XX:='B:ANIMAL' Then listing the BASIC source file B:ANIMAL.BAS on the console becomes: A>T XX;.BAS The exact substitution done by the translator would be: A>TYPE B:ANIMAL;.BAS but the semicolon is eliminated before the line is passed on to the CCP. Logical names must always be separated from surrounding text by spaces, or the usual punctuation. The translator will never see a logical name imbedded in a filename for instance, unless the semicolons or single quotes were used. The translator is smart enough to know the difference between a logical name like B and the drive specification B:, and doesn't attempt to translate any single characters that immediately precede a colon. It is easy to get carried away with the translator, and you can get yourself into trouble if you redefine symbols like = or : or * to be logical names. If you make use of the USER feature of CP/M (you really should), then you might be tempted to do this: A>#2:='USER 2' which will do just what you expect, but consider what will happen the next time you try: A>SAVE 2 TEST.COM The exact translation would be: A>SAVE USER 2 TEST.COM and will be rejected by the CCP. The following definition is nearly as short, and works a little better. A>#U2:='USER 2' Avoid defining a logical name in terms of itself; your system will make a quick trip to Pluto. The instruction to eliminate a logical name looks like: A>#DROP KILL At this point, KILL is no longer defined. The instruction to list all currently defined logical names and their string equivalences looks like: A>#LOG which will display all the active names on the console. LOADING AND SAVING LOGICAL NAMES It would not be very useful to require the user to retype all his definitions each time he powers up, so a means to batch load an entire set of definitions stored on disk is provided. This function is handled by the same program that activates the translator itself that appears in listing 2. Assuming that the program has been named LNT.COM, the translator is loaded by: A>LNT and will remain active until a power down or reset occurs. An "OK" message is printed indicating that the translator has been loaded. In this case, the current table of logical names will be initialized to empty. An optional filename may appear on the command line as in: A>LNT MYNAMES.LOG In this case, the program will read the specified file, expecting to find ASCII text lines of logical name definitions exactly as you would enter them from your keyboard (including the leading # character). The table of logical name definitions will then be initialized to include the definitions from the file. If no file type is given with the filename, an extention type of .LOG will be assumed. This command can be re-issued at any time to load a new set of logical names or zero out the current table. A second program (listing 3) is used to save the currently active logical name definitions to a disk file. Assuming that the program has been named SAVLOG.COM, this function is accomplished by: A>SAVLOG FILENAME.TYP The filename parameter is required and designates the file to receive the definitions. This file will be ASCII text compatable with the LNT.COM program, and may be edited, etc. using standard utilities. An extension type of .LOG is recommended . The program in listing 1 is will help identify the exact memory locations that the translator program needs to assemble correctly. Run this program first to discover the locations of CTARG, BTARG1 to BTARG4, and WTARG1 to WTARG4 required in equate statements at the head of the main program in listing 2. If you are the least bit curious about this enhancement, I encourage you to try it out; it can save you a great deal of time and frustration, and its fun!