? A Graphics Programming Language (GPL) Primer & Routine to Read/Edit Text from KSCAN by Mack McCormick I have always avoided delving into the study of GPL because I felt it was too difficult, cumbersome, executed too slowly, and had little to offer. Boy, was I wrong. It makes writing routines used by BASIC a snap in assembler. For example, I recently needed a routine to read text from the screen which would allow full editing including erase, insert, delete, quit, bonk tone at right margin, and enter/up arrow/down arrow. I also wanted the neat auto-repeat feature used by TI where there is a slight pause before the key takes off repeating. I began to consider writing the routine but then remembered that an identical routine resided in GPL in GROM (Graphics Read Only Memory) in the console. I first consided using the routine from GROM but then remembered that it added the screen offset of >60 to each character and I didn't need that. I could have done some fancy trick to make it work but decided to convert GPL to 9900 assembler code. You'll find two programs here. One to link you to the routine in console GROM from a CALL LOAD from E/A BASIC and the identical (almost) routine in 9900 assembler code ready to link into any program you may have that needs this utility. I've included in the 9900 routine the actual GPL code used by the Pre-Scan routine of the monitor so you may see what conversions were necessary. Try reading the GPL instructions (marked with three *) to get a flavor for GPL). If the GPL gets in the way if using the routine in your program you may delete the GPL statements though they will have no effect if they remain. It has really become obvious to me why TI invented GPL though I used to condem them for it. The major reason is that it saves about 41% more code than straight assembler. GROM as you may know is only used by TI and is a chip which supplies a byte at a time to a memory mapped address and auto-increments to the next byte (like VDP RAM) unless you change the address to be read from. It's a great way to save memory. TI calls it medium speed memory. It is 6K bytes big and resides on 8K boundaries. It is an ideal medium to hold the console BASIC routines because the TMS9900 CPU chip in the console can only directly address 64K. The GPL actually does not execute any code. GPL is interpreted in console ROM beginning at >0024 and extending to >D18. This interpreter is straight assembler code which acts as directed by the GPL bytes coming from the GROM. Hence you see one reason TI BASIC is slow. It is an interpreted by GPL and GPL is interpreted by assembler. Two interpretations! Instructions in GPL usually have two operands and most instructions can access RAM, GROM, or VDP RAM. Most instructions are single byte operands unless the operand is preceded by a D for double operand. GPL uses two stacks a data stack at >83A0 and a subroutine address stack at >8380 (this allows arbitrary nesting of subroutines). Here are a few types of instructions: DATA TRANSFER -Single/Double Byte -Block to Block -Formatted Block Moves ARITHMETIC -add, subtract, multiply, divide, negate, absolute val. LOGICAL -AND, XOR, Shifts. CONDITION -Arithmetic and Logical BRANCHING -Conditional/Uncond BIT MANIPULATION -Set, reset, test. SUBROUTINE -Call, Return STACK OPNS -Push and Pop MISC -Random Number, KSCAN, Coincidence Detection, Sound, I/O The closest language to GPL is assembler and any experienced assembler programmer should have little difficulty learning GPL. One major difference is the use of MACRO instructions by the assembler such as REPEAT....UNTIL and IF....THEN....ELSE. Very similar in this respect to 99000 assembly language. A few words about how memory is addressed. Here are a few of the most common ways and their syntax. 5 represents the decimal byte 5. >33 represents hex 33. &110011 represents binary 110011. #5506 represents the decimal number 5506. :A: is the ASCII equivalent >41. Well this has been a *very* general overview of GPL. Let's look at some actual GPL source code and my intrepretation of the 9900 assembler equivalent. This routine could have been shortened but I tried to keep it as close to GPL as possible. Hope you enjoy it. If you have questions just ask. My 6 GPL manuals cover thousands of pages and we have just skimmed the surface here. I plan to write a GPL disassembler and interpreter to convert GPL to 9900 object code within the next six months if my schedule permits. That should make the job easy! DEF START REF KSCAN,VSBW,VSBR,GPLLNK * JUST A LITTLE ROUTINE TO TEST SUBROUTINE * START LWPI WS MOVB @H00,@KEYVAL SCAN ENTIRE KEYBOARD LOOP BL @READLN DATA >002,>2FE START, END POSITONS JMP LOOP ******************************************************************************** * This is the console GPL READLN routine at (>2A42 in GROM 1) converted to 9900 * assembler. Interprets BACKSPACE, INSERT, DELETE, and FORWARD. Uses SCRATCH * PAD RAM. Total number of characters may be limited by changing the start * value of ARG+2 (upper limit) and entering at READL1. VARW is the start address * of the field. VARA is the current highest write address. * Entering at READL1 allows us to pre-specify the minimum number of characters * to be read for default creation. * Entering at READ00 allows specification of the initial cursor position. In * this case ARG+6 has to be set to the cursor position and ARG+4<>0. * Programmer responsibility to insure that VARW <= ARG+6 <= VARA <= ARG+2 * ARG+4 indicates if the line has been changed. If so, ARG+4=0. * This is a possible call: * BL @READLN * DATA >1DF,>35D LOWER,UPPER SCREEN LIMITS ******************************************************************************** * EQUATES * WS EQU >8300 MY WORKSPACE ARG EQU >835C VARW EQU >8320 ABS LOWER LIMIT VARA EQU >832A CURRENT END OF LINE TEMP EQU 0 R0 USED FOR TEMP STORAGE TEMP1 EQU 1 R1 USED FOR ADDL TEMP STORAGE R1LB EQU WS+3 TEMP2 EQU 2 TEMP3 EQU 3 TIMER EQU >8379 VDP TIMER INC EVERY 1/60 SEC. KEYVAL EQU >8374 KEYBOARD TO SCAN RKEY EQU >8375 KEY CODE STATUS EQU >837C GPL STATUS BYTE * CONSTANTS * (Should be EQU with byte values in code to save memory.) H00 BYTE 0 H01 BYTE 1 HFF BYTE >FF H508 DATA 508 H60 DATA 60 H14 BYTE 14 H766 DATA 766 BREAK BYTE >02 DLETE BYTE >03 INSRT BYTE >04 CLRLN BYTE >07 BACK BYTE >08 FORW BYTE >09 DOWN BYTE >0A MVUP BYTE >0B CHRTN BYTE >0D CURSOR BYTE >1E SPACE BYTE >20 VARV BYTE 0 (This is at >8301 in GPL but I use >8300 for workspace) VAR1 DATA 0 AUTO REPEAT COUNTER (This is 1 byte at >830D in GPL) EVEN READLN * The GPL code stores >35D at ARG+2 but to give more utility replaced with the * next two lines of code. *** DST >35D,@ARG+2 GPL DOUBLE STORE MOV *R11+,@VARW START ADDRESS OF THE FIELD MOV *R11+,@ARG+2 UPPER LIMIT *** DST @VARW,@VARA MOV @VARW,@VARA NOTHING ENTERED YET * VARA SHOULD POINT TO A SPACE LOCATION OR END OF FIELD READL1 *** ST 1,@ARG+4 STORE BYTE=1 TO ARG+4 MOVB @H01,@ARG+4 MEANS NO CHANGE IN LINE READL2 *** DST @VARW,@ARG+5 HAD TO USE ARG+6 BECAUSE OF WORD BOUNDARY PROBLEMS MOV @VARW,@ARG+6 POSITION CURSOR AT START OF FIELD READ00 *** CLR @VAR1 CLEAR BYTE. I HAD TO USE WORD BECAUSE 9900 IS SO MUCH *** FASTER CLR @VAR1 COUNTER FOR AUTO REPEAT * This is where we return to exit INSERT mode. READ01 *** CLR @ARG+7 USED ARG+8 BECAUSE HAD TO USE ARG+6 & ARG+7 ALREADY MOVB @H00,@ARG+8 NORMAL OPERATION MODE *** ST CURSOR,@VARV MOVB @CURSOR,@VARV VARV USED FOR CURSOR/CHARACTER READ$1 * Input 1 char and alternate cursor and character for blink *** EX @VARV,RAM(@ARG+5) EXCHANGE @VARG WITH WHATS AT LOCATION ARG+5 IN VDP MOV @ARG+6,TEMP EXCHANGE VARV,ARG+6 BLWP @VSBR SWPB TEMP1 MOVB @VARV,TEMP1 BLWP @VSBW MOVB @R1LB,@VARV *** CLR @TIMER MOVB @H00,@TIMER SET VDP TIMER TO ZERO *** $REPEAT MACRO. REPEAT CODE UNTIL $UNTIL IS TRUE L00001 LIMI 2 ENABLE INTERRUPTS SO THE VDP TIMER (>8379) CAN INC LIMI 0 DISABLE INTERRUPTS SO THE VDP WON'T GET MESSED UP *** SCAN SCAN THE KEYBOARD BLWP @KSCAN SCAN FOR A CHARACTER *** BS READ$2 BRANCH ON COND BIT (EQ) SET MOVB @STATUS,@STATUS EQUAL BIT SET? JNE READ$2 FOUND A NEW CHARACTER *** INC @VAR1 INCREMENT THE BYTE @VAR1 BY ONE INC @VAR1 INC AUTO-REPEAT COUNTER *** $IF @RKEY .NE. >FF THEN MACRO. IF RKEY NOT EQ >FF THEN EXECUTE THE *** FOLLOWING CODE OTHERWISE SKIP TO THE $END IF TERMINATOR CB @RKEY,@HFF OLD KEY? JEQ L00002 YEP *** $IF @VAR1 .HE. 254 THEN HIGHER OR EQUAL C @VAR1,@H508 HOLD OLD KEY FOR A WHILE * HAD TO DOUBLE 254 TO SLOW DOWN ASSEMBLY CODE JLT L00002 BEFORE STARTING REPEAT *** SUB 30,@VAR1 SUBTRACT BYTE S @H60,@VAR1 CONTROL REPEAT RATE *** B READ$3 UNCONDITIONAL BRANCH JMP READ$3 *** $END IF *** $END IF *** $UNTIL @TIMER .H. 14 TERMINATOR FOR REPEAT UNTIL HIGHER THAN 14 L00002 CB @TIMER,@H14 JLE L00001 TIME NEXT CHARACTER SWITCH *** BR READ$1 BRANCH COND BIT RESET. USED TO SAVE ONE BYTE OF MEMORY JMP READ$1 RESTART CHAR BLINK CYCLE READ$2 *** CLR @VAR1 CLR @VAR1 CLEAR AUTO REPEAT COUNTER READ$3 *** $IF @VARV .NE. CURSOR THEN CB @VARV,@CURSOR IF NE EXCHANGE AGAIN JEQ L00003 *** EX @VARV,RAM(@ARG+5) MOV @ARG+6,TEMP EXCHANGE VARV,ARG+6 BLWP @VSBR SWPB TEMP1 MOVB @VARV,TEMP1 BLWP @VSBW MOVB @R1LB,@VARV *** $END IF *** $IF @RKEY .L. : : THEN IF RKEY LESS THAN SPACE THEN EXECUTE CODE L00003 CB @RKEY,@SPACE IF .LT. SPACE THEN CONTROL CHAR JLT L00004 B @L0000C * THIS IS WHERE YOU WOULD TRAP ALL CONTROL CODES * * HANDLE BREAK CHAR FIRST * CB @RKEY,@BREAK * JNE LABLE * BACK ARROW - SPACE BACK ONE POSITION *** $END IF *** $IF @RKEY .EQ. BACK GOTO RBACK GOTO's DO NOT REQUIRE AN END IF TERM L00004 CB @RKEY,@BACK BACK ARROW? JNE B00002 TO FIX OUT OF RANGE ERROR B @RBACK * RIGHT ARROW - FORWARD SPACE *** $IF @RKEY .EQ. FORW GOTO FORW B00002 CB @RKEY,@FORW JNE B00003 TO FIX OUT OF RANGE ERROR B @RFORW * INSERT * *** $IF @RKEY .EQ. INSRT THEN B00003 CB @RKEY,@INSRT JNE L00005 *** ST 1,@ARG+8 MOVB @H01,@ARG+8 SET INSERT MODE FLAG *** $END IF * DELETE - DELETE THE CURRENT CHAR *** $IF @RKEY .EQ. DLETE THEN L00005 CB @RKEY,@DLETE JNE L00006 *** CLR @ARG+4 MOVB @H00,@ARG+4 INDICATE A CHANGE IN LINE *** $IF @VARA .DNE. @ARG+6 THEN THE D MEANS DOUBLE OR WORD OF MEMORY COMPARE C @VARA,@ARG+6 EMPTY LINE? JEQ L0001F YEP. *** DST @VARA,@ARG MOV @VARA,@ARG MOVE EVERYTHING FROM THE RIGHT *** DSUB @ARG+5,@ARG DOUBLE BYTE (WORD) SUBTRACT S @ARG+6,@ARG OF THE CURSOR TO THE LEFT *** MOVE @ARG FROM RAM(1(ARG+6)) TO RAM(@ARG+5) THIS IS A BLOCK MOVE OF @ARG *** BYTES OF VDP RAM FROM WHATS AT ADDR ARG+6 PLUS 1 TO WHATS AT ADDRESS *** ARG+6. IN SHORT MOVE EVERYTHING ON SCREEN ONE BYTE LOWER. MOV @ARG,TEMP2 COUNTER MOV @ARG+6,TEMP INC TEMP MOVE @ARG FROM RAM(1(ARG+6)) TO RAM(@ARG+6) L00008 BLWP @VSBR DEC TEMP BLWP @VSBW INCT TEMP DEC TEMP2 JNE L00008 *** DDEC @VARA DECREMENT THE WORD (DOUBLE) AT VARA DEC @VARA PRE-UPDATE END OF STRING *** $IF RAM(@VARA) .EQ. : :+OFFSET GOTO READ01 OFFSET IS SCREEN OFFSET >60 MOV @VARA,TEMP BLWP @VSBR CB @TEMP1,@SPACE JNE B00001 TO RESOLVE OUT OF RG ERROR B @READ01 *** DINC @VARA INCREMENT THE WORD OF MEMORY AT VARA B00001 INC @VARA *** ST : :+OFFSET,RAM(@VARA) L0001F MOV @VARA,TEMP LI TEMP1,>2000 BLWP @VSBW *** BR READ01 B @READ01 * CLEAR - Clear the entire input line *** $IF @RKEY .EQ. CLRLN THEN L00006 CB @RKEY,@CLRLN JNE L00009 *** $REPEAT *** ST : :+OFFSET,RAM(@VARA) MOVB @SPACE,TEMP1 CLRLIN MOV @VARA,TEMP SO WE CAN FIDDLE WITH VALUE BLWP @VSBW DEC @VARA PRE-UPDATE END OF LINE *** $UNTIL @VARA .DL. @VARW DOUBLE LESS THAN C @VARA,@VARW UP TO AND INCL FIRST POS JHE CLRLIN *** DINC @VARA INC @VARA UNDO LAST SUBTRACTION CLR @ARG+4 MOVB @H00,@ARG+4 INDICATE CHANGE *** BR READL2 B @READL2 RESTART EVERYTHING *** $END IF * GENERAL EXIT POINT *** $IF @RKEY .NE. CHRTN THEN L00009 CB @RKEY,@CHRTN ONLY REACT ON CR/UP/DOWN JEQ L0000A *** $IF @RKEY .NE. MVUP THEN CB @RKEY,@MVUP JEQ L0000A *** $IF @RKEY .NE. DOWN GOTO READ$1 CB @RKEY,@DOWN JEQ L0000A B @READ$1 *** $END IF *** $END IF *** $IF @VARA .DEQ. @ARG+2 THEN DOUBLE EQUAL L0000A C @VARA,@ARG+2 CHECK FOR BLOCK ON LAST POSITION JNE L0000B *** $IF RAM(@VARA) .NE. : :+OFFSET THEN MOV @VARA,TEMP BLWP @VSBR CB TEMP1,@SPACE BLOCKED? JEQ L0000B *** DINC @VARA INC @VARA POINT BEYOND LAST CHAR IN LINE *** $END IF *** $END IF L0000B RT ENTER THE CURRENT LINE *** $END IF (THIS IS FROM THE $IF THAT CHECKED FOR CTRL CODES) * INSERT ROUTINE * *** $IF @ARG+8 .NE. 0 THEN INSERT L0000C CB @ARG+8,@H00 INSERT MODE JEQ L0000D READ$4 *** DST @VARA,@ARG MOV @VARA,@ARG USE ARG AS TEMP FOR INSERT *** $WHILE @ARG .DH. @ARG+6 L0000F C @ARG,@ARG+6 MOVE EVERYTHING UP TO CURSOR LOCATION JLE L0000E *** DDEC @ARG DEC @ARG COPY LOWER LOCATION TO HIGHER ONE *** ST RAM(@ARG),RAM(1(ARG)) GO FROM HIGH TO LOW IN VDP RAM MOV @ARG,TEMP BLWP @VSBR INC TEMP BLWP @VSBW JMP L0000F *** $SEND WHILE TERMINATOR FOR WHILE *** $IF @VARA .DL. @ARG+2 THEN L0000E C @VARA,@ARG+2 ONLY UPDATE VARA AS UPPER JHE L0000D *** DINC @VARA INC @VARA HASN'T BEEN REACHED YET *** $END IF *** $END IF *** ST @RKEY,RAM(@ARG+6) L0000D MOVB @RKEY,TEMP1 DISPLAY THE CHARACTER MOV @ARG+6,TEMP BLWP @VSBW *** CLR @ARG+4 MOVB @H00,@ARG+4 INDICATE CHANGE IN LINE READ05 *** $IF @ARG+5 .DEQ. @ARG+2 THEN C @ARG+6,@ARG+2 HIT RIGHT MARGIN? JNE L0002F *** CALL TONE2 CALL ANOTHER GPL ROUTINE IN THIS CASE BONK MOVB @H00,@STATUS CLEAR THE STATUS BYTE BEFORE ACCESSING GPL BLWP @GPLLNK GIVE A BAD RESPONSE TONE DATA >0036 *** BR READ$1 B @READ$1 STAY IN CURRENT MODE *** $END IF *** DINC @ARG+6 L0002F INC @ARG+6 UPDATE CURRENT ADDRESS *** IF @ARG+6 .DH. @VARA THEN C @ARG+6,@VARA CHECK FOR LAST NEW HIGH LIMIT JLE L00010 *** DST @ARG+5,@VARA MOV @ARG+6,@VARA UPDATE NEW HIGH LIMIT *** $END IF *** $IF @VARA .DL. >2FE GOTO READ$1 L00010 C @VARA,@H766 JHE L00011 TO FIX OUT OF RANGE PROBLEM B @READ$1 STILL SOME SPACE TO GO L00011 * This is where we could scroll the screen if needed * UPDATE POINTERS IF YOU SCROLL * *** CALL SCROLL SCROLL THE SCREEN *** DSUB 28,@VARA * S @H28,@VARA BACK TO START OF LINE *** DSUB 32,@VARW * S @H32,@VARW BACKUP START LINE ADDRESS *** DSUB 32,@ARG+2 * S @H32,@ARG+2 ABSOLUTE HIGH LIMIT BACKS UP TOO *** DSUB 32,@ARG+6 * S @H32,@ARG+6 CURRENT CURSOR POSITION ALSO *** BR READ$1 B @READ$1 START WITH SOMETHING ELSE * FORWARD CURSOR MOVE RFORW *** CLR @ARG+8 MOVB @H00,@ARG+8 LEAVE INSERT MODE *** BR READ05 B @READ05 USE REST OF LOGIC * BACK CURSOR MOVE RBACK *** $IF @ARG+5 .DH. @VARW C @ARG+6,@VARW CHECK BOTTOM RANGE JLE L00012 *** DDEC @ARG+6 DEC @ARG+6 *** $END IF *** BR READ01 L00012 B @READ01 END **************************************** * THIS IS A ROUTINE TO DIRECTLY ACCESS * * THE GROM READLN ROUTINE. USE CALL * * LOAD("DSK1.FILENAME") AND CALL LINK * * ("DSK1.START") FROM E/A BASIC TO SEE * * IT BECAUSE OF SCREEN OFFSET. * **************************************** DEF START GPLWS EQU >83E0 ADDRESS FOR GPL WORK SPACE H00 BYTE 0 WS BSS >20 MY WORKSPACE EVEN START LWPI WS POINT TO MY WORKSPACE LI R0,>2 MOV R0,@>8320 START SCREEN ADDRESS FOR SCAN MOVB @H00,@>837C CLEAR THE STATUS BYTE SO WE DON'T GET AN ERROR BLWP @GPLLNK LINK TO THE ROUTINE IN GROM DATA >2A42 MOVB @H00,@>837C RETURN TOTHE CALLING PROGRAM ON ENTER LWPI GPLWS B @>0070 * YOU COULD HAVE PLACED AN END STATEMENT HERE AND REF'd GPLLNK INSTEAD * OF USING THIS ROUTINE. * GPLLNK ROUTINE * GPLLNK DATA UTILWS,XGPL VECTOR FOR THE GPLLNK BLWP UTILWS EQU >2094 SUBSTK EQU >8373 FLAG2 EQU >8349 SVGPRT EQU >2030 H20 BYTE >20 EVEN XGPL MOVB @SUBSTK,R1 SRL R1,8 MOV *R14+,@>8304(R1) SOCB @H20,@FLAG2 LWPI GPLWS MOV @SVGPRT,R11 RT END Download complete. Turn off Capture File.