TRS-80 Tips and Tricks – String Packing and USR() Routines

General Framework and Explanation

Machine language routines in BASIC all follow the same general steps. You can either POKE it into RAM or pack it into a STRING. For the latter, you would set up a string of the length of the number of bytes in your machine language routine, find the LSB and MSB of that string, load the USR routine start address with that LSB and MSB, and then call the routine.

“Graphic packing” uses the same concepts as string packing, except graphics are packed into a string, using 1 byte instead of a lot more.

In both cases, there are a number of ways to set up the string. Let’s say you want 10 characters. You can set it:

  1. As a straight declaration like A$=”XXXXXXXXXX” or A$(1)=”XXXXXXXXXX”
  2. You can create the string by using the STRING$ command like A$=STRING$(10,”X”)
  3. You can build the string character by character by something like
    FOR X = 1 TO 10: READ D : A$=A$+CHR$(D) : NEXT X.

These all work equally well for loading code into a string, although you can only use #1 if you want to do the packing just once and embed it into your program as an already packed string. #3 is usable only if you are generating the string at run time, but you need to recalculate the VARPTR just before you set the jump points because automated garbage collection at the system level might move the string during or after build.

Anyway, to pack a string, you tell the computer to replace each byte in the string (in this case, A$) with a different byte. You then need to figure out where the starting address for that string is, and that is to use the VARPTR instruction.

In general, the LSB of the address is found at address is found at PEEK(VARPTR(A$)+ 1) and the MSB of the address is found at PEEK(VARPTR(A$)+ 2). The MSB and LSB combined into one number would be found at PEEK(VARPTR(A$)+ 2)*256 + PEEK(VARPTR(A$)+ 1).

Before moving on, three items of caution:

  1. There are certain values which cannot be packed into a string. You can’t pack a string with CHR$(34), which is the value for a because that will result in an error due to your string having a close quote in the middle. The other value you cannot pack into a string IF YOU WANT TO SAVE THE PACKED STRING is CHR$(0) which will end the line and confuse BASIC. If you need to pack a CHR$(0), you will need to do it at run time, and build your string (example #3 above).
  2. You cannot save a packed program in ASCII. If you save it in ASCII the packed string will turn into the keywords they represent and it will not work.
  3. You cannot edit the packed string.

Graphics Packing

In graphics packing, you would write a separate program to pack the string, and then you would delete that program, except for the packed string, for further use. Because of this, you can’t build the string using example #3 above – you need it to be physically in your program. You also won’t ever need to know the location in memory of the string, so VARPTR isn’t needed at run time.

A sample program to pack graphics would start off like the following:

10 A$=”XXXXXXXXXX”
20 X = PEEK(VARPTR(A$)+ 2)*256 + PEEK(VARPTR(A$)+ 1)
30 FOR I= 1 to 10
40 READ J
50 POKE X+I-1,J
60 NEXT I
70 DATA 191, 191 , 191 , 198, 185, 184, 183, 182, 181, 180

If you run this program and then list it, you will note that line 10 has changed to

10 A$=”USINGUSINGUSINGPOINTCLOADCLEARAUTODELETELLISTLIST”

That’s 49 characters long … BUT … if type ?LEN(A$) you will get 10 bytes, because the graphics have been packed into those 10 bytes

and if you type ?A$ you will now see graphics!

Once you have line 10 set up this way, you can delete lines 20-70 and get on with your program. But note, you CANNOT edit line 10. If you need to modify line 10 you are going to need to run the program again, with your fixes in the DATA line.

So an example of keeping the string you packed, delete lines 20-70 leaving only line 10, and then writing a program around it would be:

5 CLS
20 FORX = 0 TO 47
30 PRINT @X,” “;A$
40 NEXT X

This will take that shape and move it across the top of the screen. No set/reset, no complex graphics controls … just a simple PRINT@!

Complex Graphics Example

Before moving onto machine language packing, let’s do a more complicated example. The following program will load 5 variables with 5 different 4 byte graphics of the familiar “Flagship” from the Big 5 games. I used an array to make the programming much shorter for purposes of this example.

10 A$(1)=”XXXX”
20 A$(2)=”XXXX”
30 A$(3)=”XXXX”
40 A$(4)=”XXXX”
50 A$(5)=”XXXX”
60 FOR I = 1 TO 5 : P(I) = PEEK(VARPTR(A$(I))+ 2)*256 + PEEK(VARPTR(A$(I))+ 1) : NEXT I
70 FOR I = 1 TO 5 : FOR J = 1 TO 4 : READ D : POKE P(I)+J-1,D : NEXT J : NEXT I
80 DATA 166,179,179,153 : ‘Flagship with no eye
90 DATA 166,183,179,153 : ‘Flagship with the eye in space 1
100 DATA 166,187,179,153 : ‘Flagship with the eye in space 2
110 DATA 166,179,183,153 : ‘Flagship with the eye in space 3
120 DATA 166,179,187,153 : ‘Flagship with the eye in space 4

If I used A$, B$, C$, D$, and E$ to match the above example instead of A$(1)…A$(5), I would need 5 different VARPTR lines with 5 different variables, and then would need 5 different loops to load the 4 bytes into the strings. Nothing really wrong with that, but it would make this page really long. You might say “why didn’t you use a loop to load the variables with the “XXXX”, instead of typing the same thing over and over from lines 10 to 50?”. That’s because we need those to be physically present in the program to do graphic packing, so they need to be individually defined.

So while we can’t use a FOR … NEXT loop to load the strings, we CAN us them everywhere else. With this, both lines 60 and 70 start with a FOR … NEXT loop for 5 elements, because there are 5 strings. Everything else on the line, matches the standard steps of using the VARPTR to find the POKE address, and then POKEing in the data.

If you run that and then were to enter FOR X = 1 TO 5 : PRINT A$(X); ” “;: NEXT X from the command line, you would see all 5 strings next to each other:

To get the flagship to move across the screen, you would then enter.

60 CLS
70 FOR I = 0 TO 55 STEP 5
80 FOR J = 1 TO 5
90 PRINT @ (I+J-1),” “;A$(J);
100 FOR K = 1 TO 10 : NEXT K
110 NEXT J
120 NEXT I

And you would wind up with this (NOTE: This is a repeating animated GIF, the flagship only goes from one side to the other in this program)

Machine Language / USR Packing

Machine language packing is the same theory as graphic packing, except that you need to know the variables location in memory to set up the USR call. It is also a good idea to implement some safety protocols into your program to check for cases of too much, or too little, data.

To point the USR to the string, DOS and non-DOS BASIC have different ways of implementation.

  • In non-DOS BASIC, you need to poke the LSB of the string into 16526 and the MSB into 16527. Once you do that, X=USR(0) will run the program in the string.
  • In DOS BASIC, you would use the DEFUSR0 command to set the call address for USR(0), but some DOS’s, like TRSDOS, won’t let you set a number higher than 32767. To get around this, you need to use a formula which calculates the entry point in the -32767 to 32767 range.

So, with this background, for machine language packing with a USR call, the routine generally would look like:

10 X$=”/////////////////////” :’ NUMBER OF SLASHES SHOULD MATCH THE NUMBER OF BYTES
20 A = PEEK(VARPTR(X$)+1) : B = PEEK(VARPTR(X$)+2) : C = A + 256 * B :’ STRING ADDRESS
30 X = 32768 :’ THIS IS NEEDED FOR THE CONVERSION TO -32767 TO 32767
40 E = C : ‘WE MAKE A COPY OF “C”, AS WE WILL NEED “C” FOR LATER.
The next line walks through the length of the string, reads the data, and pokes it into the string while keeping the variable “C” from being lost.
The complicated poke address is just fancy math for IF E=>32768 THEN POKE E-65536,D ELSE POKE E,D
50 FOR I = 1 TO LEN(X$): READ D : POKE E+2*X*(E>=X), D : E=E+1 : NEXT I : ‘ FILL THE STRING
The next line, together with 999, is just a safety check
60 READ D : IF D <> -1 THEN PRINT “String too short.” : END : ‘ JUST TO BE SAFE
Choose the next line depending on whether you are in Level II BASIC or in DOS BASIC …
70 POKE 16526, A: POKE 16527, B :’ LEVEL II BASIC
70 DEFUSR0=C+2*X*(C>=X): ‘ DOS BASIC
The above line is just a strictly math way of saying
IF C => 32768 THEN DEFUSR0 = C-65536 ELSE DEFUSR = C
99 END
100 DATA …..
999 DATA -1

Once run, the routine is called by a simple X=USR(0) command.

Ready-to-Use Machine Language Routines

Rising Saucer Sound

10 X$=”////////////////////////////”: ‘USE 28 SLASHES
100 DATA 205, 127, 10, 77, 68, 62, 1, 105, 211, 255
110 DATA 45, 32, 253, 60, 105, 211, 255, 45
120 DATA 32, 253, 13, 16, 238, 175, 211, 244, 201, -1

Fill the Screen with Character XXXX

10 X$=”//////////////////////”: ‘USE 22 SLASHES
100 DATA 229, 1, 16, 64, 33, 1, 60, 45, 62, XXXX: ‘REPLACE XXX WITH THE ASCII VALUE
110 DATA 119, 35, 16, 252, 6, 64, 13, 32, 247
120 DATA 225, 201, -1

Reverse All Graphic Characters

10 X$=”///////////////////////”: ‘USE 23 SLASHES
100 DATA 217, 33, 255, 63, 6, 60, 126, 254, 128
110 DATA 56, 4, 47, 246, 128, 119, 43, 124
120 DATA 184, 48, 242, 217, 201, -1

Move One Block To The Right

10 X$=”//////////////////////////////////////////////////”: ‘USE 50 SLASHES
100 DATA 33, 254, 63, 17, 255, 63, 1, 255, 3
110 DATA 237, 184, 14, 16, 17, 0, 60, 33, 63
120 DATA 0, 25, 237, 160, 229, 209, 121, 183, 32
130 DATA 244, 62, 16, 33, 0, 60, 229, 209, 19
140 DATA 1, 4, 0, 237, 176, 17, 60, 0, 25
150 DATA 61, 183, 32, 240, 201, -1

Save and Restore the Screen

To Save the Screen use GOSUB 11000

To Restore the Screen use GOSUB 12000

10000 REMEMBER TO SET THE MEMORY SIZE TO 31699
10010 FOR ZZ = 31700 TO 31723 : READ D : POKE ZZ, D: NEXT ZZ : END
10020 DATA 33, 0, 60, 17, 254, 123, 1, 0, 4, 237, 176, 201
10030 DATA 33, 254, 123, 17, 0, 60, 1, 0, 4, 237, 176, 201
11000 POKE 16526, 212 : POKE 16527, 123 : Q = USR(0) : RETURN
12000 POKE 16526, 224: POKE 16527, 123 : Q = USR(0) : RETURN

Rising Saucer Sound Routine

1 M$ = ” ” :’ 30 SPACES
2 I = VARPTR(M$) : J = PEEK (I+1) + 256*PEEK(I+2)
3 FOR K = J TO J+26 : READ X : POKE K,X : NEXT K
4 POKE 16526, PEEK(I+1) : POKE 16527, PEEK(I+2)
5 DATA 205, 127, 10, 77, 68, 62, 1, 105, 211, 255, 45, 32, 253
6 DATA 60, 105, 211, 255, 45, 32, 253, 13, 16, 238, 175, 211, 255, 201
7 A=USR(0)
8 FOR T= 1027 TO 755 STEP -1 : G = USR(T) : NEXT T

White Out Routines

To trigger white out use X=USR(0)

1 FOR I = 1 TO 14 : READ P : WR$ = WR$ + CHR$(P) : NEXT
2 POKE 16526, PEEK(VARPTR(WR$)+1)
3 POKE 16527, PEEK(VARPTR(WR$)+2)
4 DATA 33, 0, 60, 54, 255, 17, 1, 60, 1, 255, 3, 237, 176, 201

1 FOR I = 1 TO 13 : READ P : WR$ = WR$ + CHR$(P) : NEXT
2 POKE 16526, PEEK(VARPTR(WR$)+1)
3 POKE 16527, PEEK(VARPTR(WR$)+2)
4 DATA 33, 0, 60, 62, 191, 119, 35, 124, 254, 64, 32, 247, 201

1 DIM A(30) : FOR X = 1 TO 27 : READ A(X) : NEXT X
2 FOR X = 32512 TO 32538 : POKE X,A(X-32511): NEXT X
3 POKE 16526, 0: POKE 16527, 127
4 DATA 33, 0, 60, 17, 1, 60, 1, 255, 3, 54, 191, 237, 176, 6, 5
5 DATA 33, 255, 255, 43, 124, 181, 194, 18, 127, 16, 245, 201

Debounce and Break Disable Routine

This routine will provide keyboard debounce with break disabled. Remember to reserve 32742 as the MEMORY SIZE to protect the routine and to CSAVE it, as it will NEW when done:

10 CLS : FOR X = 32743 TO 32767 : READ A : POKE X, A : NEXT X
20 POKE 16526, 231 : POKE 16527, 127 : C=USR(0)
30 PRINT @ 512, “OK”;
40 FOR D = 1 TO 1000 : NEXT D
50 NEW
60 DATA 33, 238, 127, 34, 22, 64, 201, 205, 227
70 DATA 3, 103, 1, 50, 0, 205, 96, 0, 124, 254
80 DATA 1, 192, 62, 0, 201, 0, 0

NOTE: 16526 is 408E in Hex. 408EH is the 2-byte address of USR routine, so poking 231 and 127 is the LSB/MSB of 32743, the entry point of the routine.

Multiple USR() Calls

The following heading in the machine program permits as many USR calls as memory allows. A call for USR(0) goes to program 0, a call for USR(1) goes to program 1, etc. As it stands it can be used for graphics. If data needs to be passed to the machine program then Memory Size should be set to the appropriate number of bytes below the origin of the heading, the data poked into this “scratchpad” before calling USR(n), and program (n) then loads from the “scratchpad” as desired.

The first line of each program should bear the appropriate label – PRGO, PRG1, etc. It is, of course, imperative that no changes be made between CALL 2687 and JP PRGn.

Assembly language heading for multiple USR calls:

ORG nnnn
(LABELS) EQU (as desired)
” “
CALL 2687H
LD B, H
LD C, L
ADD HL,HL ; HL = HL * 2
ADD HL,BC ; HL + HL * 3
LD BC, TABLE
ADD HL, BC
JP (HL)
TABLE JP PRG0

Another ML String Packing Example

Found on the old yahoo group. Attribution unknown.

To use machine-language subroutines in BASIC programs the idea is to construct a string variable by using the CHR$ function to embed machine-language values in the string. Then the location of the string is found by the VARPTR, the location is POKEd into locations 16526 and 16527, and a USR call is executed to perform the subroutine. The advantage of this method is that no separate set of POKEs or loading of a SYSTEM tape has to be done.

First, we must assemble the subroutine. This particular subroutine contains two subroutines, one to write the contents of the video display to cassette tape and the second to read the cassette tape back and restore the display. The subroutines may be called from any BASIC program that contains them, so it is possible to save the appearance of the screen for game displays, business reports, or error conditions and restore the display at any later time. The subroutines use the machine-language cassette routines in Level II BASIC, so the entire process is very fast.

The machine-language code is presented below. The ROM routines CALLed are 212H (212 hexadecimal) (Define Cassette), 296H (Find Leader and Sync Byte), 235H (Read Byte), 1F8H (Turn Off Cassette), 287H (Write Leader and Sync Byte), and 264H (Write Byte). “IN” reads 1024 bytes from cassette to restore the display, while “OUT” writes out 1024 bytes from display memory. We can’t go into detail on the cassette functions in this chapter, but we’ll cover some of the code in the next chapter, “POKEing Around in Memory.”

Address
Bytes
Line
Label
Opcode
Operand
Comment
0000
AF
00100
IN
XOR
A
;0 FOR CASSETTE 1
0001
CD1202
00110
CALL
212H
;START CASSETTE
0004
CD9602
00120
CALL
296H
;FIND LEADER, SYNC BYTE
0007
21003C
00130
LD
HL,3C00H
;START OF SCREEN
000A
010004
00140
LD
BC, 1024
;1K BYTES IN SCREEN
000D
CD3502
00150
IN10
CALL
235H
;READ ONE BYTE
0010
77
00160
LD
(HL),A
;STORE IN SCREEN MEMORY
0011
23
00170
INC
HL
;POINT TO NEXT LOCATION
0012
0B
00180
DEC
BC
;DECREMENT BYTE COUNT
0013
78
00190
LD
A,B
;GET MS BYTE OF COUNT
0014
B1
00200
OR
C
;MERGE LB BYTE
0015
20F6
00210
JR
NZ,IN10
;GO IF NOT 1K
0017
CDF801
00220
CALL
1F8H
;TURN OFF CASSETTE
001A
C9
00230
RET
;RETURN
001B
AF
00240
OUT
XOR
A
;0 TO A
001C
CD1202
00250
CALL
212H
;START CASSETTE
001F
CD8702
00260
CALL
287H
;WRITE LEADER ON TAPE
0022
21003C
00270
LD
HL,3C00H
;START OF SCREEN MEMORY
0025
010004
00280
LD
BC,1024
;BYTE CNT = 1K
0028
7E
00290
OUT10
LD
A,(HL)
;GET BYTE
0029
CD6402
00300
CALL
264H
;WRITE ONE BYTE
002C
23
00310
INC
HL
;POINT TO NEXT
002D
0B
00320
DEC
BC
;DECREMENT BYTE COUNT
002E
78
00330
LD
A,B
;GET MS BYTE OF COUNT
002F
B1
00340
OR
C
;MERGE LS BYTE
0030
20F6
00350
JR
NZ,OUT10
;GO IF NOT 1K
0032
CDF801
00360
CALL
1F8H
;TURN OFF CASSETTE
0035
C9
00370
RET
;RETURN
0000
00380
END

00000 TOTAL ERRORS
34334 TEXT AREA BYTES LEFT

Label
Address
Line 1
Line 2
IN
0000
00100
IN10
000D
00150
00210
OUT
001B
00240
OUT10
0028
00290
00350

The machine code for the subroutines is presented below in decimal form. It’s relocatable and can be used anywhere in RAM memory. The “IN” portion starts at the first byte, and the OUT portion starts at the 28th byte.

175,205,18,2,205,150,2,33,0,60,1,0,4,
205,53,2,119,35,11,120,177,32,246,205,
248,1,201,175,205,18,2,205,135,2,33,0,
60,1,0,4,126,205,100,2,35,11,120,377,32,246,
205,248,1,201,255

The machine code above is converted to a string by assembling a string by moving DATA values into a dummy string, similar to the graphics method:

100 ZA$=”THIS IS A DUMMY STRING THAT WILL BE FILLED WITH CHARACT”
200 ZA=VARPTR(ZA$) ‘find address of block
300 ZB=PEEK(ZA+1)+PEEK(ZA+2)*256 ‘find string address
400 ZC=ZB+27 ‘find 2nd routine address
500 FOR ZI=ZB TO (ZB+LEN(ZA$)-1) ‘setup loop for pokeing
600 READ ZZ ‘set byte of ml
650 POKE ZI,ZZ ‘poke ml byte
700 NEXT ZI ‘loop til done
800 DATA 175,205,182,205,150,2,33,0,60,1,0,4,205,53
900 DATA 2,119,35,11,120,177,32,246,205,248,1,201,175
1000 DATA 205,18,2,205,135,2,33,0,60,1,0,4,126,205,100
1100 DATA 2,35,11,120,177,32,246,205,248,1,201,255

The above code initializes the string to the machine-language values from the DATA statement. To call the OUT machine-language subroutine, use the following code:

1900 POKE 16526,(ZC-(INT(ZC/256)) + 256
2000 POKE 16527,(INT(ZC/256))
2300 A=USR(0)

To call the IN machine-language subroutine, use the following code:

2400 POKE 16526,ZB-(INT(ZB/256))*256
2500 POKE 16527,(INT(ZB/256))
2600 A=USR(0)

3 thoughts to “TRS-80 Tips and Tricks – String Packing and USR() Routines”

  1. The White Out Routines do not use the “declare the string variable ahead of time” method, but seem to work fine.

    What is the purpose of declaring a string variable of an awkwardly-specific length if you can just create a concatenated string on-the-fly?

    Puzzled…

    1. TRS-80 BASIC does not require that strings be declared in advance, so very few programs do it.

  2. The point is that, you pre-pack the “awkwardly-specific-length string” one time, using the program at the top. This embeds the machine-language/graphics directly into the BASIC source code. Then you can delete all the DATA statements, and the pack FOR loop, and your program will consume a lot less memory. And, it starts up faster. Restrictions are that the DATA cannot contain 0 or 34 decimal, and there’s a limit on the length of each packed string (about 235 bytes max).

Leave a Reply to Guido Cancel reply

Your email address will not be published. Required fields are marked *