Machine Code and Your TV Screen

See all articles from SYNC v3 n4

One of the ways that Basic is slow is in putting things on the screen. Have you ever played a game in Basic and you wanted things to zip across the screen, but, instead, they crawled? It is frustrating, but Basic is simply very slow in this department. In the 8K Basic SLOW mode, even something as simple as

10 FOR A=1 TO 10
20 PRINT A
30 NEXT A

takes a long time. (Try it and you will see.)

You can speed up the process of putting something on the screen by using machine code, but, to do that, you must have some understanding of how the computer puts things on the TV. This is a little technical, but do not panic—it will not last long.

TV Guide

Somewhere in RAM there is a block of memory called the display file. This is where the computer stores what will go on your TV screen. Depending on how long your program is, the display file could be almost anywhere in memory. For example, when there is no program in the computer, the display file starts at location 16509—near the bottom of RAM. With a 16K RAM pack and a long program, the display file might start way up around memory location 32000—almost at the top of memory.

To put something on the TV screen, the computer has to know where the display file starts. It keeps track of that starting point as one of the “system variables.” The people at Sinclair call this particular system variable “D-FILE” (for “display file”), and it is at the same place for 4K and 8K Basic—memory locations 16396 and 16397.

Any time you want to know where the display file begins, just look at those two memory locations. In Basic, you can do it like this:

PRINT PEEK (16396)+256*PEEK (16397)

Notice that you must multiply what is in the second memory location by 256, and add it to what is in the first memory location. This is similar to the way you load a number into a register pair in machine code—first comes the remainder, then comes the quotient.

Short and Sweet

One other thing you need to know about the display file is that it is only as long as it needs to be. The display file takes up space that otherwise could be used by a Basic program or by variables. Since some Sinclair computers come with only a small amount of RAM, it is important to use memory efficiently—and that means keeping the display file short.

In the display file, each line of the TV screen shows up as a string of character codes (numbers between 0 and 63, or between 128 and 191 for reversed characters—white on a black background). The last character code in each line is an ENTER code, 118. (The complete list of character codes is in the appendix to your manual titled “The Character Set.”)

If a line on the TV screen is empty, then the only thing in that line will be the ENTER code. This is about as short as you can get.

Taking a Look

Let’s take a look at the display file. Type in and run the program in Figure 1.

Figure 1.

10 PRINT "HELLO"
20 PRINT
30 PRINT
40 PRINT "OK"
50 PRINT
60 FOR A=0 TO 12
70 LET DFILE=PEEK (16396)+256*
PEEK (16397)
80 PRINT PEEK (DFILE+A)
90 NEXT A

Your screen should look like this:

HELLO

OK

118
45
42
49
49
52
118
118
118
52
48
118

The numbers in the bottom part of the screen are the first 13 numbers in the display file. If you check them against the appendix of your manual, you will find that they match up exactly to the first dozen characters on the screen, including the ENTER code at the end of each line.

But notice: the first byte in the display file is always an ENTER code.

Quick-change Artist

When something is added to a line in the display, everything in the display file has to move—shifting down one position to make room. That takes time, and it can get pretty complicated. The easiest way to get something on the screen is to start out with something there, then change it.

For example, try running the Basic program in Figure 2.

Figure 2.

10 PRINT "X"
20 LET DFILE=0
30 FOR A=0 TO 63
40 LET DFILE=PEEK (16396)+256*
PEEK (16397)
50 POKE DFILE+1,A
60 NEXT A

(Line 20 is necessary only for running the program in 4K Basic, in which the value of “D-FILE” changes every time you create a new variable.)

If your computer can run in SLOW mode, you will see the character in the upper left-hand corner of your screen change quickly, running through the regular character set.

It runs pretty quickly, but, as you would probably guess, it runs much, much faster in machine code. How can we translate this routine into machine code form?

The first thing we have to do is rewrite the “FOR/NEXT” loop into a simpler form. Remember, Z80 machine code has nothing exactly like FOR and NEXT. The results are shown in Figure 3.

Figure 3.

10 PRINT "X"
20 LET DFILE=0
30 LET A=0
40 LET DFILE=PEEK (16396)+256*
PEEK (16397)
50 POKE DFILE+1,A
60 LET A=A+1
70 IF A<64 THEN GOTO 30

Now we have solved the problem of the loop; we can use load, increment, and jump instructions to replace lines 20, 40, and 50.

But we will also need to use two new kinds of load instructions—instructions that will work the way PEEK and POKE work in Basic.

To do a machine code PEEK at location 16396, use this instruction:

LD A,(16396)

The equivalent in Basic is

LET A=PEEK (16396)

However, you can use this instruction only to load register A.

Remember, there is a big difference between

LD A,5

and

LD A,(5)

The first one puts the number 5 in register A. The second one puts whatever number is in memory location 5 into register A. This is the difference between

LET A=5

and

LET A=PEEK (5)

The number in parentheses is sometimes called a pointer, and that is a good way to remember how it works. It points to where the computer will find the number it is going to load.

As you might expect, the machine code version of POKE uses the same kind of pointer notation. The instruction

LD (16396),A

works like

POKE 16396,A

The number in register A goes into memory location 16396.

Again, you can use this instruction only to load register A.

Doing the Two-step

You can also do a machine code PEEK or POKE with the register pairs BC, DE, and HL. One way is to load each register individually. For example,

LD A,(16396)
LD L,A
LD A,(16397)
LD H,A

would load the system variable “D-FILE” into register pair HL.

But you can do the same thing with a single instruction:

LD HL,(16396)

This automatically loads the number at location 16396 into register L, and loads the number at location 16397 into register H. It does two steps at once. This is faster for the Z80 processor, and easier for you.

You can also use this instruction with the other two register pairs, BC and DE. And you have probably guessed already that, if you can PEEK with any register pair, you can simply reverse the process to do the machine code equivalent of a POKE. You are right—

LD (16396),HL

is equivalent in Basic to

POKE 16396,L
POKE 16397,H

Pointers by the Pair

So far, we have covered the ways that you can PEEK or POKE to a specific numbered location. The number in parentheses in the machine code instruction always points to the memory location that is involved.

But in Basic, you can also use PEEK and POKE with variables, e.g., commands such as

LET A=PEEK (HL)

or

POKE HL,A

Can you put parentheses around the name of a register in machine code, and use it as a pointer?

Yes, but only sometimes.

A memory location is a number between 0 and 65535. That means it will take two registers—a register pair—to handle the number. As a result, you cannot say

LD A,(B)

but you can say

LD A,(BC)

In addition, register pairs BC and DE can be used only to PEEK or POKE with register A. Remember, register A is special. It is the register you can add to or subtract from, and it has many of its own special instructions we have not seen yet.

Here are the only machine code PEEK and POKE instructions that use register pairs BC and DE:

LD A,(BC)
LD A,(DE)
LD (BC),A
LD (DE),A

That is not much, certainly. What about register pair HL?

Pay close attention here—this is important:

In almost any instruction in which you can use one of the seven regular registers—A, B, C, D, E, H, and L—you can also use register pair HL as a pointer.

Here are a few examples:

LD B,(HL)
LD (HL),E
ADD A, (HL)
CP (HL)

If you stop to think about it, you will realize that is pretty impressive. This means you can use almost any memory location just like a register, so long as register pair HL is pointing to that memory location.

Whew

By now you are probably ready for a rest. But we are so close to being done with the machine code version of our quick-changing number program, so let’s finish it up fast. Figure 4 shows the program so far.

Figure 4.

10 PRINT "X"
20 LET DFILE=0
30 LET A=0
40 LET DFILE=PEEK (16396)+256*
PEEK (16397)
50 POKE DFILE+1,A
60 LET A=A+1
70 IF A<64 THEN GOTO 30

If we use register A for A and register pair HL for DFILE, we can translate it just about line by line. Starting with line 30, Figure 5 shows what we have so far.

Figure 5.

       LD A,0          62,0,
LD HL,(16396) 42,12,64,
INC HL 35,
LOOP: LD (HL),A 119,
INC A 60,
CP 64 254,64,
JR C,-6 56,-6,
RET 201

Now let’s poke this routine in and run it using the loader program in Figure 6. (Notice that PRINT “X” is part of the Basic program.)

First reserve space at the top of memory like this:

4K ROM: Type

PRINT USR(620)

Wait until the screen clears. Then type again

PRINT USR(620)

8K ROM: Enter this program:

10 POKE 16388,-20
20 POKE 16389,PEEK 16389-1
30 NEW

The program in Figure 6 is designed for a ZX80 or ZX81 with 1K of RAM. For a 2K TS1000, change line 10 to

10 LET Q=18414

If you are using a 16K RAM pack, change line 10 to

10 LET Q=32750

Now, for either ROM, type in the program in Figure 6.

Figure 6.

 10 LET Q=17390
20 FOR A=0 TO 12
30 PRINT A;"########";
40 INPUT B
50 POKE A+Q,B
60 PRINT PEEK (A+Q),
70 NEXT A
80 CLS
90 PRINT "X"
100 LET B=USR (Q)

Run the program and enter the numbers in the right-hand column of Figure 5 in exactly the order they appear.

Now you will see the character change even faster than the TV changes the screen—you may find it hard to believe anything happened at all. That is fast.

Doing It Yourself

You can also use the PEEK function to find out what is in a particular location on the screen. Figure 7 gives a short program that turns your display into a counter.

Figure 7.

 10 PRINT "00000000000000000000
"
20 LET DE=PEEK (16396)+256*PEE
K (16397)
30 LET DE=DE+20
40 LET HL=DE
50 LET A=PEEK (HL)
60 LET A=A+1
70 IF A<38 THEN GOTO 110
80 POKE HL,28
90 LET HL=HL-1
100 GOTO 50
110 POKE HL,A
120 GOTO 30

Can you work out the machine code program that will do the same thing? If you try running it, watch out—the lowest digits will probably flash by so fast, you will not be able to see them at all.

Variables—in Machine Code?

Now we know how to move a number between a memory location and a register. We can move the number from a register to a memory location (like the POKE command in Basic), or move it from the memory location to a register (like the PEEK function in Basic).

Besides looking at and putting things on the TV screen, there is something else we can use the machine code PEEK and POKE for—something even more useful and important. With these instructions, we can create our own machine code program variables—as many of them as we like.

Here is how it works:

Suppose we designate a specific byte of memory as a variable. (The best way is to choose a byte of memory that is near the machine code program. That way, it will be in protected memory, and will not be accidentally changed by the Basic system.) We can store a number at that memory location, and put the number into a register only when we need it.

For example, if we have a variable at location 17000, we can add 5 to it with these instructions:

LD A,(17000)
ADD A,5
LD (17000),A

A one-byte variable can hold any number from 0 to 255. To hold a larger number, we can use a variable that takes up two bytes. It can hold any number from 0 to 65535. We could add 5 to that kind of variable like this:

LD HL,(17000)
LD DE,5
ADD HL,DE
LD (17000),HL

Think for a moment about what this means. Up till now, we have only had a few registers to work with. As a result, we could keep track of only a few numbers at once—one number in each register. But by using variables in our machine code programs, we can keep track of many different numbers. There is really no limit to the number of variables we can use in a machine code program except the amount of available memory.

However, we must keep in mind that using variables takes more instructions than keeping all the numbers in registers. It also makes your machine code programs run a bit slower since it takes time to get variables from memory and load them into registers, and then to move them back from registers into memory after you are finished working with them.

It takes time—but not much time. For example, on your computer it takes an extra 8 one-millionths of a second to get a one-byte variable and then put it back.

That kind of time, we can usually afford.

There is a big advantage to using variables, though, that really makes it worthwhile. With variables, it is easier than ever to translate a program from Basic to machine code. A few more instructions are required than before, but with variables you can translate lines of Basic more directly.

Up until now, for example, we have had trouble with a line like

LET Q=R+S

First we had to break it down to

LET Q=R
LET Q=Q+S

and then decide what registers to put the numbers Q, R, and S in.

But with variables we can translate the single line of Basic into a series of machine code instructions. If Q is the variable at memory location 17001, R is at 17002, and S is at 17003, then

LET Q=R+S

becomes as in Figure 8.

Figure 8.

LD A,(17002)     get R
LD B,A
LD A,(17003) get S
ADD A,B add them
LD (17001),A put result in Q

Get the idea? It may seem a little more complicated, but here is the key: you can always use exactly the same routine to add two variables together and put the result in a third variable. To do the same thing with three different variables, all you have to do is plug different variable locations into this same set of instructions.

Remember that machine code has no error messages, and a mistake will probably make your computer crash. Then you have to unplug the computer and start all over again from scratch. Keeping track of what you are doing on paper or tape may save redoing a lot of work.

The more you can use “standard” chunks of program—like this routine for adding two variables together, the easier it is to write machine code routines that will not crash.

What is more, in Basic, you do have error messages, and, if there is a mistake in your program, it usually means you have to change just a line to fix the problem, then run it again. One easy way to write a machine code program is to start out by writing the program in Basic, making sure it runs properly, then translating the program into machine code, to get all the advantages of machine code speed.

It may not quite be all the best of both worlds, but it comes a little closer.

Coming Attractions

There is another big advantage to using variables in our machine code programs: it makes it easy to send a number into a machine code routine.

We already know how to get an answer out of a machine code program. When the machine code routine hits the “return” instruction, the value in one of the register pairs will be returned to Basic. This is the HL pair on the 4K ROM, or the BC pair on the 8K ROM.

But suppose we have a machine code routine that can draw a line on the screen from one point to another. How can we tell the routine what two points to draw the line between?

If we have made the coordinates of the two points variables in our machine code routine, it is easy. Since we know the memory locations of the variables, we can POKE our coordinates into the variables from our Basic program, before it reaches the line where the USR function sets the computer to work on the machine language routine.

We can also get as many answers as we like from a machine code routine, by PEEKing at the variable locations after the computer has returned to Basic. Usually we will not need to do that, but it is nice to know we can.

Next time, we will look at a machine code line-drawing routine that is not only much faster than Basic, but much smaller, too. The Basic version barely fits in a computer with 2K RAM, while the machine code takes up less than 300 bytes.

If you have comments or questions about machine code programming, or if something is not quite clear, let me hear from you. Be sure to send along a stamped, self-addressed envelope if you need a reply.