Multitasking Spectrum Interrupts Easy multitasking with Richard Taylor's After and Every One of the most advanced features of the Amstrad micro it its ability to handle interrupts directly from Basic. In other machines such as the Spectrum, interrupts are a tool available only to the machine-code programmer. Amstrad Basic releases the power of interrupts to all in a simple but comprehensive manner. Although Spectrum interrupts can be harnessed quite readily - at least on the 48K machine - from machine code, the Basic programmer is left out in the cold with no commands to support interrupt-driven programming. Presented here is a program which rectifies this shortcoming by equipping Spectrum Basic with a number of interrupt com- mands, as well as the On Error and On Break commands found in Microsoft Basic. Due to the problems associated in accessing interrupts, even from machine code, on a 16K machine, it will only operate on the larger model. The machine code resides above RAMtop, occupying addres- ses 63866 to 65367. [It should be loaded] with: CLEAR 63000: LOAD ""CODE The 10 new Basic commands provided by the program can be accessed by typing them in in REM statements. There's a limit of one command per REM and, as usual, a REM statement must be the last item on a line. To make the computer treat REMs in this new fashion it is necessary to initialise the machine code by using a RANDOMIZE USR 63866 as the first line of your program. Subsequently, REMs will be treated in the new manner with no further need for USR calls. The full command list is as follows: AFTER ON ERROR GOTO EVERY ON BREAK STOP DISABLE ON BREAK GOSUB ENABLE IGNORE BREAK DROP RESUME I'll now deal with each of the commands individually. The simplest command is IGNORE BREAK. As you would expect, this command forces a program to ignore the break key and there- fore prevents you from breaking into it. As an example, type in the following short program, but only if there's nothing important in memory. 10 RANDOMIZE USR 63866 20 REM IGNORE BREAK 30 GO TO 30 Pulling out the plug is the only way out of this program. The command in line 20 is typed in letter by letter, a little more laborious than single key entry but certainly a lot less confusing. You can type in either upper or lower case but upper case tends to look a bit neater. If the first character of a REM is an asterisk then the rest of the line is ignored. In this way you are still able to add comments to a program. The next command, ON BREAK STOP, restores normality to the break key: 10 RANDOMIZE USR 63866 20 REM *Press 'a' to get out of this 30 REM IGNORE BREAK 40 IF INKEY$="a" THEN REM ON BREAK STOP 50 GO TO 40 The last of the three commands concerned with the opera- tion of the break key is ON BREAK GOSUB. This is a much more powerful command than the previous two; allowing a full-blooded subroutine to be called when break is pressed. In order to specify which line should be jumped to this command must be followed by a valid line number. You have two options over this, you can either put a number in directly or the name of a variable. What you're not allowed to do is have a mathematical expression, so something like 10+1000 is invalid but 1010 is OK. The compexity of the break subroutine will obviously depend on the application it is being used for. In many cases it would simply consist of a RUN command to restart the program completely if break is pressed. In any case, the operation of the routine is entirely up to you. A break subroutine is written in exactly the same manner as a normal subroutine, terminated with a RETURN command. In order to prevent break subroutines nesting themselves by the user pressing break while the break-handling routine is actually running, the key is ignored while the routine is in progress, just as though you'd used IGNORE BREAK. When the terminating RETURN is reached, the original status of break handling is restored. It is possible to redefine the operation of the break key from within the break handler itself, although its effect won't be initialised until the break routine has finished. The following short example will call the break handler at line 1000 only the first time break is pressed. 10 RANDOMIZE USR 63866 20 REM ON BREAK GOSUB 1000 30 GO TO 30 1000 PRINT "You've pressed break" 1010 REM ON BREAK STOP 1020 IF INKEY$=" " THEN GO TO 1020: REM *Wait if user still pessing break 1030 RETURN When you write a break handler it is important not to use any variables utilised in the main program as altering them might upset the program when it is returned to. It's not really a good idea to print on the screen either, as your printing might corrupt the layout of the program's own output or even, in some circumstances, cause it to halt with an error. If your program is menu-driven then it is a good idea to terminate the break routine with a jump to the part of the program which prints the menu. If a user selects the wrong option at the menu then pressing break will immediately return control to the menu without harm. Disabling the break key might not completely protect a program as there are other places where you may be able to stop it, namely when the computer asks "scroll?", during any printer/cassette/Microdrive access, or by typing STOP in response to an input prompt. However, you can intercept these possibilities by detecting the error reports that they cause using the ON ERROR GOTO n command, where n is a line number. This command allows you to trap any sort of error that occurs in a program. When an error is detected the computer does a GO TO to the given line - not a GO SUB, although the computer does remember where the error occurred. The error handling routine should be able to competently handle any error that can occur in a program. In common with break subroutines, error routines may just consist of a RUN command to restart the program if an error appears. Some Basics automatically list the line at which an error occurred to give you the opportunity of modifying it if, indeed, it is where the mistake lies. Using the ON ERROR command it is possible to add this facility to ZX Basic. It is necessary to know the line at which the error was found. To be able to do this you need to insert a DEF FN command in the first line of the program along with the USR call: 10 DEF FN v(a)=USR 63872: RANDOMIZE USR 63866 Inside the error routine, FN v(8) returns the line at which the error occurred. The self-listing routine is as follows; you might find it a very time-saving debugging aid: 10 DEF FN v(a)=USR 63972: RANDOMIZE USR 63866 20 REM ON ERROR GOTO 9990 9990 PRINT "Error ";CHR$ FN v(10);" at line "; FN v(8);":";FN v(9) 9991 PRINT 9992 LIST FN v(8) FN v(9) returns the statement number of the erroneous command and CHR$ FN v(10) returns the alphanumeric code for the error. The RESUME command can be used optionally at the end of an error-handling routine to continue execution of a pro- gram from the statement where the error was detected. If you follow RESUME by a line number then execution continues from that line instead of from the point of the error. You should always terminate error handlers with a RESUME if you intend to re-enter the program, never use a GO TO. To prevent error handlers from becoming nested, any errors detected within the error routine itself are reported in the usual manner. The following program repeats forever - or at least until you answer no to the "scroll?" prompt. It illustrates the danger of resuming from the point of the error without actually correcting the error's cause. 10 DEF FN v(a)=USR 63972: RANDOMIZE USR 63866 20 REM ON ERROR GOTO 1000 30 LET a=b: REM *What b? 1000 PRINT "Oh dear, there's been an error" 1010 REM RESUME The use of VERIFY or LOAD commands from within a program often causes problems if there is a tape error, since you are left in command mode once the error has been reported. If the program is going to be used by people other than its author, then it is necessary to print instructions on the screen of what to do in the event of a tape error to re- enter the program. This works, but is hardly state of the art in user-friendiness. The ON ERROR command provides a useful solution to this problem by allowing an error handler to be called if a tape error is detected which can invite the user to try again. There are many instances in a program where it is useful to use an ON ERROR command, most notably when checking the validity of user input. Rather than undergoing complicated checking routines it is much easier to assume that it is right but set up an error routine as a safety net while the input is being processed so that any errors caused by invalid data can be solved by looping back to the input statement to ask the user for the input again. The major function of the program is of course to provide interrupt handling. This is implemented in the form of the commands: AFTER, EVERY, DISABLE, ENABLE and DROP. The scheme of things is basically this: there is a timer avail- able counting at a rate of 50 units per second - the Frames rate. Using the AFTER command you can set the timer to a specific value. Immediately, the timer starts counting down towards zero. Your program can happily continue, not having to worry about the timer any further. When the counter reaches zero, the flow of your main program is temporarily diverted to a certain subroutine, just as if a GO SUB had been magically inserted in the right position in the pro- gram. The subroutine is not terminated by a RETURN command but by the normally innocuous CONTINUE. The syntax of the AFTER command is AFTER x, GOSUB y. The GOSUB has to be typed out in full. It is necessary, I'm afraid, to type in the seemingly redundant comma just before the GOSUB bit. The ability for a program to be freely interrupted in this way is often loosely termed Multitasking or parallel processing. Here's a silly example - a listing for a computerised egg timer. 10 RANDOMIZE USR 63866 20 REM AFTER 9000,GOSUB 1000 30 PRINT "Start boiling the egg. I'll just brush up on my mental arithmetic." 40 LET a=INT(RND*1000) 50 LET b=INT(RND*1000) 60 PRINT a;" + ";b;" = ";a+b 70 FOR c=1 TO 100: NEXT c 80 POKE 23692,255: REM *Allow automatic scrolling 90 GO TO 40 1000 PRINT "The egg is done" 1010 STOP Line 20 sets up the time delay of 9000 1/50ths of a second - three minutes. Lines 30 to 90 just waste time by doing something completely unrelated to eggs: adding numbers together. Lines 1000 and 1010 are the subroutine called when the three minutes are up. The AFTER command is a "single shot" command in that the subroutine is called only once, after which the timer becomes inactive. In most applications you would want a certain routine to be called at regular intervals. You could do this with the AFTER command if you re-initialised the timer with AFTER at the start of the subroutine. How- ever, a much better way is to use the EVERY command. EVERY is used in exactly the same manner as the AFTER command. When the "interrupt service routine" - the subroutine called when the timer reaches zero - is reached, the timer is automatically re-armed to its starting value, ready for the next time. This program will constantly update the time at the top left-hand side of the screen even while another program is running: 10 RANDOMIZE USR 63866 20 LET min=0: LET sec=0 30 REM EVERY 50,GOSUB 9900 40 LET a=0 50 PRINT AT 21,0;a: LET a=a+1 60 GO TO 50 9900 LET sec=sec+1 9910 IF sec>59 THEN LET min=min+1: LET sec=0 9920 PRINT AT 0,0;("0" AND min<10);min;":"; ("0" AND sec<10);sec 9930 CONTINUE The delay time in an AFTER or EVERY command can be up to 65,535 units, about 21 minutes 51 seconds. If you need longer delays than this for some reason then you could use the following method. For instance a delay of one hour could be produced by setting up an interrupt service routine - or ISR if you like abbreviations - called every 10 minutes. At the start of the program you would initia- lise a variable - a, say - to zero. Each time the ISR is called the value in a is incremented and when it reaches six, an hour has passed. Bear in mind, however, that the Spectrum's clock isn't very accurate and you could end up with a quite drastic error with such a long delay. So far I've talked about the "timer" in a singular sense. There are, in fact, eight timers. All eight are completely independent of one another in all respects. You can direct information to specific timers in an EVERY or AFTER command by tapping another number in after the delay time; e.g., AFTER 100,7,GOSUB 9000 uses timer 7. The timers are numbered 0 to 7. If you miss the timer number out, as in previous examples, then it defaults to timer 0. The DISABLE command enables - no put intended - you to suspend the operation of one or all of the timers. DISABLE used on its own disables all eight timers whereas DISABLE followed by a number disables only that particular timer. The inverse command is ENABLE, which is used in a similar fashion to re-enable previously disabled timers. 10 RANDOMIZE USR 63866 20 REM AFTER 100,GOSUB 1000 30 REM DISABLE 0 40 GO TO 40 1000 PRINT "Time up!" 1010 STOP does nothing unless you remove the DISABLE statement at line 30 or put in an ENABLE command at line 35. You'd usually use the DISABLE/ENABLE commands to protect certain parts of a program from being interrupted, possibly because that part manipulates variables used by the ISR(s) and could leave the variables in temporary states that may upset the ISR(s). The last command connected with interrupts is DROP. In an analogous way to normal GO SUBs, before an ISR is called the current line and statement numbers are stored away on a stack so that normal program execution can continue quite happily when the ISR is finished. In some circumstances, however, you might not want to ever return from the ISR. For example, in a game you might wish to set a time limit to complete a certain task, say 10 seconds. You could use AFTER 500,GOSUB 8000 so that when the time is up a jump to line 8000 will be made. Obviously you wouldn't want to return from the ISR to continue that particular part of the game. To save leaving the stack in an unbalanced state you would use the DROP command. This command simply makes the computer take the top item of its stack and throw it in its electronic dustbin. In an application such as this, you can disarm the timer with an AFTER 0,GOSUB x as soon as the task has been completed, otherwise you could find the com- puter calling the ISR at a rather inappropriate time. When a timer counts down to zero, the computer remembers that a certain line is to be called by placing its line number on yet another stack. When the statement currently being executed is finished it looks at the number it remem- bered and calls the appropriate ISR. Because ISR calls are not processed until the current statement is completed, INPUT, PAUSE, LOAD, SAVE, MERGE and BEEP may hold things up. The computer, being a meticulous beast by nature, care- fully piles up all the numbers of the ISRs it's got to call in preparation for such a time when it is able to process them. Eventually the computer will run out of room and will no longer bother to store the lines. The amount of room the computer has got for stack storage is determined by how much memory you leave free between RAMtop and the start of the program at 63866. A good value for RAMtop is 63000, which leaves room for all but the most complex applica- tions. Using the user-defined function introduced under the ON ERROR command, it is possble to interrogate any of the timers and find their current status. Use FN v(x), where x is the timer you wish to look at, numbered 0 to 7. 10 DEF FN v(a)=USR 63872: RANDOMIZE USR 63866 20 REM AFTER 1000,5,GOSUB 1000 30 PRINT AT 0,0;FN v(5);" " 40 GO TO 30 1000 STOP This program displays the time remaining on timer 5 as it plunges towards zero. The function will return a zero if the timer is either inactive or disabled. Unlike error and break handler routines, ISRs will nest to as many levels as you like, although in practice the number of levels is determined by the amount of free memory available above RAMtop for the stack. One thing to avoid is to define an EVERY command with a time interval smaller than the time needed to execute the associated ISR. The routine will "interrupt itself" in such a circumstance, as the next interrupt will have occurred before the ISR to handle the previous interrupt is finish- ed. Eventually the computer will get clogged up with an enormous stack of return lines which it never quite gets round to using. If this sort of situation is a possibility in your program then disable the timer right at the start of the ISR and enable it again right at the end. When writing ISRs, as with break and error handlers, it is important to remember that the routine should not do anything to unduly upset the main program by changing the value of one of the variables it is using, for instance. The computer looks after you a bit by automatically storing the print and plot positions at the start of the ISR and restoring them to those values when the ISR is terminated. Print and plot positions are therefore "local" to the ISR. If you print and draw from within an ISR, then the computer will automatically tidy any damage you might have done by changing the print/plot position. There are a couple of points of interest to Interface 1 users. First, you should not use OPEN # and CLOSE # commands in conjunction with this program. More precisely, programs using the interrupt facilities; ON ERROR and ON BREAK etc. don't matter. Secondly, any other Microdrive/ Interface 1 commands should be immediately followed by RANDOMIZE USR 63866: POKE 23728,0 sequences. REM statements typed in as direct commands are treated in the normal way and cannot, therefore, contain any new commands. The CONTINUE statement cannot be used in the normal way, as when an error is actually reported (in con- trast to an error handler being called) then all the new stacks are cleared and the timers are all forced inactive. CONTINUE will not, then, resume a program without actually clearing or changing anything.