Vertical Blank Interrupts From c on the ST
by Eugene R. Gobby
The primary purpose of this article is to show how to install a C routine or function as an interrupt driver. The interrupt that we will use is our old friend, the vertical blank interrupt (VBI).
In the course of doing this, I will show how to link to an external module (developed in assembly language) from C and how to get into the supervisor mode of the 68000.
Many of you are aware that C has pointers which can be used to point to the memory locations of its variables; well, C also has pointers to functions. If we put the value of this pointer into an interrupt vector, then our function will be executed every time that the interrupt is called.
The ST VBI system has what it calls a queue, where users can put their own interrupt routines. In color systems this happens exactly 60 times a second (50 Hz in Europe); in monochrome, 70 times a second (I'm not sure if this is exact).
Although there are BIOS or XBIOS calls for installing interrupts in many of the ST's various systems, there is apparently none for the VBI. In this case, we must put the 68000 into the supervisor mode so that we can access the memory locations involved. Since initially I had some trouble getting the call ( Super() ) provided for doing this to work, I wrote and linked my own trap handler to do it. I have since had success with Super() but I have left the Trap1() routine in this program for educational purposes.
The Trap 1 handler (Listing 2) is modeled on the Hitchhiker's Guide trap 14 handler; however, this one works with C. The two changes needed are: 1) declare the__trap1 label as a globl (notice that the assembler leaves out the "a"), and 2) declare the storage allocaton by .ds.L 1, not .ds.l. The global declaration allows us to call the "function" from C by the label name. The storage must be allocated as a long because we will be saving the address of the user stack.
In the C program (Listing 1), we first declare the function, trap1(), then we call it. We pass the arguments 0x20 (i.e., 20 hex) and 0L. We thus called a 68000 trap #1 exception. The trap handler then calls the operating system routine indicated by the code 0x20. This routine puts the 68000 into supervisor mode.
The supervisor stack pointer is loaded with the current value of the user stack pointer when the second argument is 0L; L meaning a longword. (I use uppercase L because the lowercase is often confused with the numeral 1.) The trap handler then passes the old, supervisor stack-pointer value back to us in save__ssp. When we return to user mode, we will pass save__ssp in place of 0L, so as to return to where we left off. While in supervisor mode, you should be careful of using the BIOS or XBIOS routines because many of them put the 68000 into the supervisor mode during execution. Thus, calling these routines often hangs the system. (Exactly why is not clear to me, since many of the normal VBI functions must be executed from the supervisor mode. Perhaps the VBI is more careful in checking to see if the 68000 is already in that mode or not.)
Installing the VBI
To install our VBI, we must find the queue and then find an empty space in that queue—indicated by a longword of all 0s (see Figure 1). The queue has only enough space for eight interrupt vectors, so conservative programming demands that we test for the number of vectors in the table.
Figure 1The function vbiset() is a general routine for installing VBIs in the ST. It will install our routine on the bottom of the queue if we enter with an ASCII passed to the variable process. If we pass an "n" instead, it will disengage our routine by writing a 0L to the queue. Note that if you have put other routines under the one being disengaged, they must be moved up.
The interrupt routine vbiroutine(), must be declared in vbiset(). The pointer to vbiroutine() is then simply vbiroutine. I declared vbiroutine to be an integer because I am using integers in the routine but, since I am not actually returning any values out of vbiroutine(), I suppose that it could just as easily have have been declared as returning a char or a long. However, it must be declared as something.
Two other pointers are used in vbiset(): vbiqueue and vbiempty. Vbiqueue is set equal to 0x456L. This is the memory location of the pointer to the queue. Thus vbiempty = vbiqueue, makes vbiempty equal to the first vector in the table of interrupt vectors. Since it is declared as a pointer, the compiler sees to it that it's incremented as a pointer and not as, say, an integer when we increment it.
Note that since the 68000, unlike the 6502, changes the whole address in one operation, we never have to worry that another interrupt will occur while we are changing the address. Thus there is no need to turn off interrupt processing during this instruction. In fact, when I set the TOS variable vbisem (hex 452) to 0 and tried to install a routine, it hung the system. Vbisem is used as a flag by the TOS during the VBI process itself, and it should probably not be altered by the user.
Our little VBI routine simply increments points at the rate of 60 (or 70) times a second. The while loop in main() just prints out the value of points until it has reached 600 (when ten seconds have passed). Then the vbi queue is returned to normal and we end the program. Since I wanted to increment points on successive interrupt calls, I declared it to be an external (equivalent to a global) variable. Thus, it remains in existence between calls to vbiroutine() and is also accessible to our print loop in main().
The vbiset() routine can be made more general by putting the supervisor entry and exit within it and by eliminating the calls to printf() and getchar().
Typing it in
Now I'll explain some of the mechanics of writing the program. (I used Atari's Developer's Kit.) First type Listing 2 into the editor and save it. Then find the utilities disk and call up COMMAND.PRG. The assembler is on the compiler disk. If you have a single drive system, you will have to copy the "trap1()" source code to it. Then type "as68 – L trap1.s trap1.o." Single drivers: transfer the object file to the linker disk. Type in the new linker batch file (Listing 3). This may or may not be the approved order of linking but it works.
I had a lot of trouble with the compiler when my programs got above a certain size. Since I was compiling to a second double-sided drive with an almost empty disk, I was going crazy. It turns out that the assembler writes an intermediate file to its own disk, regardless of what drive your source code is on. The batch programs in Listing 4 and 5 will force the intermediate file to be written to either a floppy B (type C2 filename in the batch dialog widow) or drive C (C3 filename), a hard disk or a ramdisk. When I C3 with a ramdisk, the assembler just flies.
A final note: I find that when the compiler or linker fails, my source (.C or O) file often gets wiped out, so I always keep a backup of each file. By the way, pressing a key during the various operations (there are about four for the compiler), will call the abort query, allowing you to save some time. For instance, if the preprocessor has found some errors, you can exit without going through the assembly operation.