Spectrum Interrupts Adding Commands to Spectrum Basic (Sinclair User, November 1984) Keith Williams presents a detailed explanation on how to use interrupts on the Spectrum and provides a useful trace function for printing program line numbers [The article title and preamble are misleading, as the code does not add] [any new BASIC commands; it adds a trace function initialised via a USR ] [call and activated by setting a 'trace' variable within a program. JimG] ONE OF THE problems of adding new commands and routines to the Spectrum Basic is that it is held in ROM. That means that all the computer operations are under the control of an immutable program which cannot be stopped. Although it is easy to write new routines, they can only be called by means of a very user unfriendly RAND USR call. Passing of information into and out of those routines is also difficult without a whole chain of pokes. Even then it may be impossible to get the routine to work as it may be needed at the execution of every line. There is, however, one very simple way of beating the ROM and that is to use interrupts. While exploring this we can also develop a new command for the the Spectrum. If you are not interested in the detailed explanation you can still follow the instructions and load up the final program which gives a very useful trace function, an essential tool when debugging along programs. A trace function is a machine code routine which prints on screen, while a program is running, the number of the line which is currently being executed. The Spectrum is based on the popular Z-80A microprocessor. On that microprocessor are two electrical connections which are involved with interrupts. The first is the NMI which is of little use to our present purposes and so we will ignore it. When the voltage applied to the other pin changes we say that an interrupt occurs. That causes the processor to stop what it is doing, make a note of where it has reached and then jump to follow another program called the interrupt service routine. When that is finished it returns to its place in the original program. There are three interrupt modes available on the microprocessor. IM0 is designed for use with peripherals such as printers and the like. When the peripheral needs servicing, such as requiring more information, then it sends the interrupt message and then an address. The computer, in IM0, will run the routine starting at that address before returning from the interrupt. IM1 is the mode normally used by the Spectrum. When an interrupt occurs it will carry out the routine at address 56. That routine in the ROM does various housekeeping tasks such as reading the keyboard. The Spectrum is wired in such a way that an interrupt comes with every fluctuation in the mains voltage, in Britain that occurs 50 times a second. IM2 is much more useful. In that mode the processor jumps to a service routine the address of which must calculate from two numbers. The first of those numbers comes from the thing causing the interrupt. In a Spectrum this number 255 - or FF in Hex - is supplied by the hardware. The other number is programmed in. It is kept in a special store known as the I register. The processor jumps to the address 256*I+255. Just to make things more complicated this computed address is not the start address but is the address of a store holding the start address. To get our program on the way we must first poke its start address into another address - whose Hex value is xxFF. Then we must put its vector address - the xx from before - into I. That sounds complicated but really it is simple. There is, however, one problem. The screen chip in the Spectrum works in such a way that putting the vector in the first 16K of RAM causes difficulties for the machine. If you want to use interrupt routines on a 16K machine you must be very devious. If you put 40 into the I register then the processor will look at 40*256+255=10495 for a start address. That is in ROM and it contains the start address 32348 or 7E5C in Hex. On the 48K Spectrum you have more leeway and can either use the 16K trick or use the number 128 (80 Hex) in I. And so to the routines. A word first about getting them into memory. If you have an assembler then it is easy. If you do not then you will need a Hexloader program. One is provided in listing 1. [Not included here. See TRACE.ASM for commented assembler listing. JimG] You must first type it in and then save it. It enables you to type in the machine code and then save that. When entering the machine code routines in listing 2 type in only the digits and letters in the column 'hexcode'. Type in a whole line at a time and the address and code will appear on the screen. If you make a mistake then typing in 'r' will enable you to retype the previous line. When you have typed the last line of hex then an 'S' will lead to a save routine to keep the hex on tape. To minimise errors and make it easier to explain the program as a whole, it has been divided into its various sub-routines. Type those in one at a time; later they can be put together, using the Collator program in listing 3. The first pair of routines, TRON and TROFF, are the ones which switch the interrupt vectors around. TRON loads the register I with 0FEH, that is, 254. That means that the vector holding the start address of our routine must be placed at address 0FEFFH, that is, 65279. The register I cannot be loaded. directly and so we must first load the value into the A register. IM2 is selected for the reasons already stated and the remaining instructions DI and EI switch interrupts off and on. Strictly speaking they are not essential here but it might cause confusion for the machine if it was interrupted during this short routine at the wrong place. TRON is really quite separate from the rest of the program. Its job is to set the machine up so that we can switch the trace function on or off at will. It must therefore be called early on by a USR call. More about that later when the whole program is in. TROFF performs the opposite function in that it resets the machine to normal interrupts. You will notice that I is loaded with 3FH or 63. That is the address (3FFF) that the ROM sets up as an interrupt vector on power up. In interrupt mode 2 that would cause a jump to address 60 which is part way through the normal interrupt routine. It is important, therefore, when resetting to put the machine into IM1. The only USR call that is necessary anywhere in this program is the initial one to TRON. TROFF is called by means of a normal Basic variable. Type in the machine code and save it. The start address is FE7E. The pair of routines can be saved together under the name 'tron'. Once the whole program is in and enabled it is necessary to be able to switch the trace on and off. That can be done from Basic by inserting the line LET trace=x, where x can be one of three values: '-1' will cause a call to TROFF thereby disabling trace; '0' will switch trace off but leave it enabled. '1' will switch trace on. So if in your program you wish to trace through lines 300 to 700, say, insert in your program at line 299 LET trace=1. Line 701 LET trace=0 - stops the trace. Then run the program. The next routine TRQ searches the variables area to see if the variable trace exists. If it does not then it assumes a value of 0. If it does exist then the value is picked up. The method of finding the variable is to search through the variables area for the code 0B4H which is the code for 't' adjusted to show that it is the first letter of a long name variable (see page 167 of the manual, letter code 96 + 160). When the code is found each of the codes in turn is checked against the letters 'r', 'a' and 'c', finally against 0E5H which is the code for 'e' + 128, showing that it is the last letter of the name. The value of a number is stored in five bytes. The first is 0, the second the sign byte holding 0 for a positive number and -1 for a negative. The routine first picks that byte and adds one to it. If the answer is 0 then the A register is loaded with FF (-1) and a return to the main control routine is effected. If it is not negative then the next byte is looked at. That is the least significant byte of the value, i.e. x-256*INT (x/256). The byte is picked up and decreased by one. If the answer is 0 then it must have held 1 and so A is loaded with 1 and a return made. The only possible remaining value is 0 and so 0 is put into A and then the program returns to the main control. There are, probably, easier methods of doing this, for example using the LOOK-VARS routine in ROM at address 28B2H, but it is important not to alter any of the system variables or the alternate register set because the processor is in the course of running a Basic program. Consequently no ROM routines at all have been used but only simulations of them. Now type in TRQ and save it under that name. The start address is FE90. The Print routine is the one that prints out the line number. Again there would be easier ways of doing it, using RST 10H for example, but again a simulation has been produced. The routine is entered with 'C' holding the value of the digit and 'A' its position - 1 for thousands, 2 for hundreds, etc. Every character printed on the screen consists of eight rows of eight pixels. Each row can be represented as one byte. As the screen is 32 bytes wide it would seem that the easy way to print a character is to poke the first byte in and then the next byte 32 bytes further on and so on. The problem is that the screen is not laid out in memory in that simple and obvious way. In fact, each row of pixels is 256 bytes away from the last one. The algorithm therefore requires that each byte is poked into an address 256 bytes on from the previous. The pixel bytes are stored at an address pointed to by CHARS + 256 and the numbers start 128 bytes further on. As there are eight bytes for each character then the value in C must be multiplied by eight and then added to that base address. The routine may now be typed in and saved using the name "print". The start address is FED3. The next two routines are involved in sorting the current line number. Count divides the number in HL by the number in DE and returns the result in C and the remainder in HL. Line picks up the line number and uses Count to manipulate it digit by digit and then prints those out by calls to Print. The current line number is held in PPC so that is first picked up in HL. DE is loaded with 1000 and then Count is called to see how many thousands there are. The number is printed using A to say the print position i.e. the thousands column. Then that is repeated for the hundreds and so on. Count overlaps the interrupt vector at FEFFH and there must be a jump just before it so that the processor does not interpret the vector as instructions. The start address for this pair of routines is FEF3. They can be typed in and saved. The final routine is the control routine called MAIN and it controls all the others and performs the necessary housekeeping tasks. The start address is FF2D; the one held in the interrupt vector FEFF. It is the routine called when an interrupt occurs. The first task is a call to ROM address 56. That is the normal interrupt routine and it updates FRAMES, reads the keyboard and so on. Next the A register and the Flags register are saved so that normal service can be resumed on return. If a program is not running it will hold FF(-1) and then a return is made after restoring A and the flags register. If a program is running then the registers are saved and a call made to TRQ to see if the trace is to be switched on, off or disabled. TRQ returns that information in the A register. Depending on the value in that register a call is made to LINE or TROFF or none of those. Finally, all the registers are restored and control is returned to the ROM. All that remains is to collate all those routines. Type in the second Basic program in listing 3, rewind the tape and then run it, not forgetting to save MAIN first. The third Basic program - listing 4 - will load the trace utility above RAMTOP. It, or something like it, must be used on switching on and before you load or type in the program under test. To enable the trace, i.e. to call TRON, you need to give the command RAND USR 65150 after any start or NEW. Further sophistications can be added, such as a delay loop at the end of MAIN which will make it easier to follow the trace. [I didn't bother with the following, as all routines are in ] [contiguous memory so I just saved them as one block to start with.] [Also, the assembler listing was only 210 bytes long, not 214. JimG] Listing 3. Collator 10 CLEAR 65149 20 FOR n=0 TO 4: LOAD "" CODE: NEXT n 30 CLS: PRINT "Prepare tape for saving Press ENTER" 40 PAUSE 4e4 50 SAVE "trace" CODE 65150,214 60 STOP Listing 4: Trace 10 CLEAR 65149 20 LOAD "trace" CODE 30 NEW