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

Page Menu


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@!

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 varaiable "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.


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

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


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.

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 *