TS1000 Error Reports: Basic & Advanced

In this article, we will explore how error reports work on the TS1000/ZX81 computer, and furthermore, how to use “custom” error reports in your own programs.

We are all familiar with the TS error report system, even if all our programs are perfect from the outset (ri-i-ight!). The error report appears at the bottom of the screen, separated by a slash-from the line number at which it occurred. If the program runs perfectly, the report 0/line_no. is seen. If there is an error, a report code of 1 to F appears instead.

The error code is kept in memory location 16384 (4000 hex), the first byte in RAM memory, which is also the first system variable. This variable goes by the name ERR_NR. We can check on the value of ERR_NR by typing the immediate command:

PRINT PEEK 16384

The number 255 will be printed to the screen, 255?? The error report at the bottom is zero! Why 255?

You have just discovered the first characteristic of the error report system. The value in ERR_NR is INCREMENTED before being printed. A value of 255 (FF in hexadecimal notation) is the largest number that a single byte or eight-bit register can hold. If a byte or register holds 255 and is incremented, the value rolls over and becomes zero. This is like an automobile odometer when reaching 99,999.9. Since incrementing FFh results in zero, we can also consider FFh to represent a minus 1. This is the view that applies in this case, and explains the error report of zero and the printed ERR_NR value of 255.

Actually, a more precise explanation is that, after the value is incremented, an additional 28 is added so that the correct character code is produced for printing to the screen. Thus, our 255 eventually ends up as 28, the character code for “0.” With this information in hand, try

POKE 16384,34

as an immediate command, or run the following one-line program:

10 POKE 16384,243

The results are error codes of “Z” and inverse “G,” respectively. In fact, all the single-character codes from 0 through 63 and 128 through 191 can be used as error codes by this method. The multi-character commands cannot be so used.

At this point, it appears a simple matter to use error reports in our own BASIC programs: First, a line to test for the error condition (and branch around if false, or no error), followed by a line to make the POKE to location 16384. Well; almost but not quite. Run this simple two-line program:

10 POKE 16384,143
20 REM

The resulting error report is 0/20. So, what happened to the inverse G? However, if we change line 10 to read POKE 16384,34, the report is Z/10 as expected. What is the difference?

We have just found several more characteristics of the error reporting system. The error report is printed at the bottom of the screen:

  • (1) after an immediate command,
  • (2) after the last line of a program or STOP, or
  • (3) after an error is detected in executing a program line.

As each line is executed, a test is made of the value in ERR_NR to see if an error occurred. The test is whether Bit 7 (the most significant bit) of ERR_NR is set. The test is not, as you might think, whether ERR_NR is 255 (FFh). This means that any value from 128 through 255 will test as being not-an-error. Furthermore, as each line is executed, the value of ERR_NR is reset back to 255.

The above explanation accounts for the fact that the inverse G error report didn’t occur in the two-line program. Line 10 does indeed POKE 143 into ERR_NR. The test, however, shows that bit 7 is set, so no error is indicated. As execution of line 20 commences, the value in ERR_NR is set to 255. Thus we get the error report 0/20 when the program reaches the end.

If we are satisfied with a somewhat expanded, but incomplete, set of error reports, then a single line can be inserted at each place in the program where an error report is needed.

IF <error_test> THEN POKE 16384,<error_code>

The <error_test> is just some expression to evaluate if the error condition has occurred. If the test is true, then the POKE is performed, and the program stops. The value of <error_code> can be in the range from 0 through 34 or 99 through 127. This will give error report characters of 1 through Z and inverse space through inverse 0 (zero).

If we want the entire set of single characters for our error reports, then we must make use of the characteristic that an error report is printed after the last line of the program. The POKE must be in the last line of the program. This will force the error code to be printed even though bit 7 may be set (codes 128-255). The following piece of code is an example of how to do this:

1500 LET ERR=<error_code>
1510 IF <error_test> THEN GOTO 9999
.
.
9999 POKE 16384,ERR

The first two lines are inserted everywhere in the program where an error report might be needed. Line 9999 must be the last line in the program. To produce all of the single-character codes by this method, the value of <error_code> can range from 0 though 34, 99 through 162, and 227 through 255.

Now, for all you machine-code aficionados, I will first show and discuss the use of a brief machine-code routine in a BASIC listing that will also give a full range of error codes. From there, it’s easy to do the same thing in machine-code only. I’ll start with the “how-to,” then go on the (more important) explanation of how it works.

The following piece of code will give all the single-character error codes:

0010 REM INT x
.
.
1500 IF <error_test> THEN GOTO 1530
1510 POKE 16515,<error_code>
1520 RAND USR 16514
.

The REM line is set up to reserve two spaces, e.g. 10 REM xx, and then doing a POKE 16514,207 as an immediate command. The three lines of BASIC are inserted anywhere that an error report code is needed. The <error_test> and <error_code> labels have the same meaning as before.

There is a more memory-efficient way of accomplishing the same result, as shown below:

0010 REM INT X INT X INT X ....
0020 LET BASE=16514
.
.
1500 IF <error_test1> THEN RAND USR BASE
.
.
IF <error_test2> THEN RAND USR (BASE+2)

The REM line is constructed similarly to the first example. Two spaces are reserved for each error report desired, and each even location (16514, 16516 etc.) is POKEd with 207. The odd locations are POKEd with the various error codes needed. Single lines of BASIC are inserted anywhere that an error report is wanted, with the machine-code jump after the THEN going to the selected error routine.

How can a simple two-byte machine-code routine, in which the second byte is used solely as data, produce an error report? The answer lies in the somewhat special properties of a set of Z80 machine-code instructions known as “restarts,” and in how the Sinclair ROM designers used these capabilities.

The Z80 instruction set is provided with eight restart instructions. A restart is essentially an unconditional CALL to a specific memory address. The eight restarts are RST 00, RST 08, RST 10, etc. up to RST 38 (in hex numbers). It is up to system designers to determine the use of this capability, which is generally to call often-used routines from many places within the ROM or other operating system. Restarts are one-byte instructions (as opposed to three bytes for CALL), so they make for efficient use of ROM memory locations.

The designers of the ZX81 ROM chose to use the restarts for such routines as “collect a character,” “print a character,” “collect next character,” “turn on floating-point calculator,” etc., and of course, “error restart.” The error restart is RST 08h, whose hexcode is CFh, i.e. the 207 that we POKEd into the REM line.

An important point to remember is that restarts are calls (like GOSUBs), not jumps (like GOTOs). A call in machine-code has the same meaning as in higher level languages; the location or address from where the call was made is stored, so that a return to the calling routing can be made when the call is complete. Thus, when we do a RAND USR 16514, the first instruction encountered is a RST 08h. This immediately pushes the address of the next byte (i.e. 16515) onto the stack, and then jumps to the error restart routine at address 0008.

When any of the other restart routines are called, the stacked return address is eventually used to get back to the place where the restart was called from. The error restart is the exception. No return is ever made to the calling routine! Rather, the return address is used as a pointer to the data byte that immediately follows the RST 08h instruction (i.e., the desired error code). Furthermore, by going directly to the error routine, there is no test of bit 7, so that any single-character error report can be used.

This is precisely how the ROM does the fifteen error reports that the ZX81 operation system contains. There is simply a RST 08 instruction at many places in the ROM, each followed by the appropriate data byte to produce the error report. This is, of course, preceded by the necessary code to test for the error condition and branch around the restart and data byte if no error is detected.

If you are programming entirely in machine code, it should be obvious by now how to produce error reports; do what the ROM does. Insert your code to test for the error and branch if no error, add a RST 08, and follow it with the code for the desired error report. Remember, however, that you cannot get back to the same point in your routine, since the return address is popped off the stack, used to load the data byte, and destroyed in the process.

Now the good news: The entire stack is cleared (reset), so it doesn’t matter how deep the stack is when you call RST 08h. You therefore don’t have to bother popping back whatever values you have on the stack before going to the RST 08. The BASIC GOSUB stack is NOT cleared, however, because of the clever way in which the machine stack and the GOSUB stack are controlled by the operating system. A discussion of this is beyond the scope of this article, but if you’re interested in experimenting with this see the “Fun with Hot Z” tyd-byt in a previous issue.

Error reports can be useful to the pure machine-code programmer as a means for tracing a program that crashes at some unknown point. Provide two NOPs (00h) at critical junctures in your program. These can then be turned into error traps by poking in a CF followed by data byte. If the program makes it to that point, an error report will be generated and the routine will return to a nice, safe BASIC warm boot, regardless of what might be on the stack.

Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.