Personal Computer News
1st September 1984
Published in Personal Computer News #076
Hang onto your BBC's variables and avoid disaster with Ian Copestake's routine.
BBC Valuable Variables
Hang onto your BBC's variables and avoid disaster with Ian Copestake's routine
If you've ever accidentally hit BREAK while running a Basic program on your BBC you'll know the anguish losing all your variables can cause. If the program has only a few important variables this is, at the least, merely frustrating. But if you're storing a lot of data, such as games scores or school marks, that's the sort of disaster you could well do without.
The routine presented here gets round the pain by storing the variables so that they can restart automatically, and all intact, when you press BREAK.
It's just as effective if CTRL-BREAK is pressed, and SHIFT-BREAK is also safe so long as there is no !BOOT file on line. It could alternatively be used for program protection as well.
The method uses some unofficial addresses in the BBC Basic workspace where no Operating System calls will do the job. However, the addresses used have not changed so far since the BBC Micro was launched.
When BREAK is pressed, the computer won't wipe clean all its memory unless you have used *FX200. The program and its variables are still present, but a few pointers are reset so that the computer ignores them.
You probably know that you can recover a program to some extent by including in it the command:
*KEY 10 OLD|MRUN|M
But this will not restore the variables, nor will it survive CTRL-BREAK. We cannot make use of function keys at all for present purposes, since CTRL-BREAK resets the buffer where their definitions are kept too effectively.
Basic keeps track of its variables in a 120-byte table starting at &0482. The first pair of bytes point to the first variable starting with A, the next pair to the first variable starting with B, and so on. After Z, a gap of 12 bytes is followed by the pointers to variables starting with A to Z. The gap corresponds to the one between Z and A in the table of ASCII codes. Finally, there are pointers to the first function and the first procedure. Another pointer kept at &0002,3 holds the address where the next new variable should be stored.
A Few Pointers
These pointers are reset when BREAK is pressed, but if we can manage to copy them to somewhere safe first and copy them back afterwards we will have solved the problem. Basic will then be able to find the variables it would normally think had vanished.
Fortunately, there is a BREAK intercept facility in the Operating System. As part of its normal BREAK procedure the computer looks at a particular memory location to see if it contains the value &4C, which is the 6502 machine code for an absolute jump. If it finds one, it jumps to the address contained in the next two bytes. We can put a machine code routine of our own at this address.
The memory location is checked twice at different stages of the BREAK procedure. The carry flag is 0 on the first visit, and 1 on the second, so that our routine can take a different action each time.
We can use *FX247,76 to put the code in the right place, and *FX248,LO and *FX249,HI will put the low and high bytes of our routine's address in the next two locations.
Reserved RAM
The first time it's called, our routine copies the variable pointers to an area of RAM previously set aside. This area could have been reserved in various ways, but I have chosen to use a byte array and let Basic worry about where to put it. The only disadvantage of this is that it pinches about half a page of your RAM; your program could also use the array for things that do not need to be preserved during BREAK.
Although we needn't know where the byte array is planted the routine must. I have attached it to the resident variable Z%. The resident variables are not destroyed by BREAK, and are always stored in the same place. They occupy four bytes each with @% at &0400, A% at &0404, and Z% at &0468. &0468,9 will always contain the address where our byte array starts; we can ignore the next two bytes because it is only a 16-bit address.
After copying the variable pointers into the byte array our routine returns control to the normal BREAK routine and waits to be called again. When this happens we want to arrange for the variable pointers to be copied back. We cannot do that until after we have executed OLD, otherwise they would all be reset again. To perform OLD in the middle of a machine code routine, we could delve into the Basic ROM to see where Acorn has put the OLD routine and then call it as a subroutine. But this would be far too naughty, even if we could be certain we had found the right address.
What we can best do is use OSBYTE &8A (the equivalent of *FX138) to insert some instructions from machine code into the keyboard buffer.
The instructions, in Basic, will be acted upon as soon as the BREAK routine has finished, just as if they had been typed at the keyboard.
So the second time our BREAK intercept routine is called it puts the following three commands into keyboard buffer:
OLD (RETURN)
CALL &address (RETURN)
GOTO line number (RETURN)
If then returns control to the normal BREAK routine.
You should use the run-time address of the machine code to copy the variable pointers back to their original positions: that is, the code from .START2 onwards in Listing 1. I have tacked it onto the end of the BREAK intercept code, but you could assemble it separately if required.
The line number is the line at which your program will restart after BREAK has been pressed. Anything in your program which need not or must not be repeated, such as a DIM statement, should be at an earlier line number. Function key definitions, which cannot be preserved against CTRL-BREAK, should be placed after the restart line. We have to use GOTO rather than RUN, otherwise the whole point of the exercise would be lost.
We can save a few bytes in our code by using Basic tokens. Here &D6 represents CALL all by itself, and &E5 stands for GOTO. When you run the example program, the bytes will appear on the screen as ordinary characters, but the Basic interpreter treats them as keywords. For some reason this dodge will not work with OLD, so I have used the ASCII codes for O., which saves a whole byte.
Now let's move on to operating the program. Type in listing 1. Don't worry about the blank lines, they're just for clarity. There should be no need to change any addresses if you are using MODE 7. Then save the program for future use. When you run the program, if all is well the machine code will be assembled without error and the message OFFSET = &72 will appear.
If this doesn't happen, check your listing carefully.
Check the start and finish addresses of your machine code. These are printed out as it is assembled, and on a normal BBC Micro should be &7B00 and &7B86. Add 1 to the finish address and execute:
*SAVE VARPRES 7B00 7B87
Type in the example program in listing 2 and save a copy in case of errors. Have your cassette or disk ready so that VARPRES can be loaded, and then run the example program. Finally, you'll have to convince yourself that it works.
A Matter of Source
As for modifying the addresses, the machine code generated by Source is relocatable: you can assemble it at one address and use it at another. To alter the assembly address, assign your own value to ASSADR at line 40 of Source. Use the appropriate addresses when you *SAVE the code.
VARPRES uses two consecutive bytes in zero page. These can be anywhere not used by Basic, but must be in zero page. To change them, give your own value to ZPGADR at line 50 of Source.
In the example program, VARPRES is loaded at &7B00. In your own program you can load it to any address which will never be overwritten, but a few changes are necessary. Obviously, change the *LOAD address (example line 60). Then change *FX248 and *FX249 to refer to the low and high bytes of your *LOAD address (example lines 90, 100).
Alter the data at Source line 450, and reassemble the VARPRES code. The four data items represent the run-time address of the code from START2 onwards, which is equal to your *LOAD address plus the OFFSET value printed at the end of assembly. You can do a dummy assembly to check OFFSET, but it should still be &72.
Get the computer to print the run-time address in hex; add a leading zero if necessary to make it four digits; look up the ASCII codes for these four digits, and put them in the data statement. The DATA statement can be in decimal if you prefer, but it must represent a hexadecimal address.
The example program is re-entered at line 500 after break. To change this, place your equivalent of the example lines 60-120 before your re-entry line. Then change the data at Source line 480 to represent the ASCII codes for the decimal digits of your line number, and re-assemble the VARPRES code.
As it stands, Source caters only for a three-digit line number: use leading zeros if yours is less than this.
Program Notes on Source (Listing 1)
ASSADR and ZPGADR are explained in the section on modifying the addresses
RESVAR is the address of the resident variable Z%. If you wish to use a different resident variable, calculate a new RESVAR as explained earlier
VARPNT is the address of the Basic variable pointers table
VARTOP is the address of the next-new-variable pointer
Lines 150-610 form the break intercept routine
Line 150. When the break intercept routine is called for the second time the carry flag is set. The program jumps to the code at .SECOND
Lines 170-200. The address of the byte array is copied from Z% to zero page to suit 6502 machine code indexing methods
Lines 220-340. The Basic variable pointers table and the next-new-variable pointer, 122 bytes in all, are copied into the byte array
Lines 400-410. These prepare for OSBYTE &8A and specify the keyboard buffer
Lines 430-580. A mixture of Basic and assembler is used to produce the code for 15 consecutive OSBYTE calls. Line 520 is equivalent to assembling LDY#, and line 530 puts the appropriate value in the next byte. Line 540 increments the program counter to allow for these two bytes, and line 550 re-enters the assembler for line 560. There are slightly neater ways of doing this with the Basic II assembler, but the system used here works with either Basic. The code produced by this method takes up more space than if we used a loop and a data block within the code itself, but the routine would then need to know the address of the data block and relocatability would suffer.
The data represents:
O,.,RETURN,CALL,&,7,B,7,2,RETURN,GOTO,5,0,0,RETURN
The RESTORE statement at line 500 is a subtle but vital necessity. When I first tried to assemble the code without this line, I was perplexed to see an out of data message appear. After counting the data items several times I realised that the assembler was READing the data on its first pass and had none left for the second.
Lines 630-800. This code is called when the instructions in the keyboard buffer are processed, and simply reverses the effect of lines 220-340
Line 840 gives the position of the last section of code relative to the first