
MULTITASKING BASIC EXTENSION
by Richard Taylor for Sinclair Spectrum
from Your Computer vol.4,#11 (Nov.1984)


SPECTRUM INTERRUPTS

Easy multitasking with Richard Taylor's After and Every.


ONE OF THE most advanced features of the Amstrad micro is 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 commands, 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 addresses 63866 to
65367. Type in listing 1 and Run it. The program automatically lowers
RAMtop and proceeds to Poke the code into memory. Each of the 23 data
lines holding the code in a hexadecimal form has an associated checksum.
If at any time the code in a line doesn't tally with its checksum then
the computer stops, displaying the line at which the discrepancy was
found.

When the code is in a form that the computer will readily accept - ie.
correct - you can save the code using the command below. Don't worry
about the long delay while the computer Pokes the code; it takes a
couple of minutes and don't be concerned by the blank screen, the
computer hasn't crashed - hopefully.

SAVE "MULTI-TASK" CODE 63866,1413

You can reload at any time with:

CLEAR 63000: LOAD "" CODE

The 10 new Basic commands provided by the program can be accessed by
typing them 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 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 therefore 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 GOTO 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 operation 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 complexity 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 GOTO 1020:
     REM *Wait if user still pressing 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 GOTO to the given
line - not a GOSUB, 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 63872: RANDOMIZE USR 63866
  20 REM ON ERROR GOTO 9990
     <Rest of program>
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(l0) 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 program 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 GOTO. 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 63872: 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-friendliness.

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 available 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 GOSUB had
been magically inserted in the right position in the program.

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 type [sic] by doing something
completely unrelated with 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.

However, 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 initialise 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, eg.

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 pun 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 GOSUBs, 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's been completed, otherwise you could find the computer 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 remembered 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, carefully 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 applications. Using the
user-defined function introduced under the ON ERROR command it is
possible 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 4000
  30 PRINT AT 0,0;FN v(5);"
  40 GO TO 30
1000 STOP

The 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 finished.

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 63869: 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 contrast 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.


--
Another Fine Product transcribed by:
Jim Grimwood, Weardale, England
http://www.users.globalnet.co.uk/~jimg/
--
