A tiny guide by Excellence in Art, written to remind himself how all this stuff fits together in the context of demo/game development.
If you’re the kind of person that’s very much at home reading technical docs, whitepapers and datasheets, you will probably be better off reading those. But if like me, you prefer someone holding your hand a little, hopefully this doc could be for you.
The MFP (Multi-Function Peripheral) in the ST is a pretty nice little chip. This document does not intend to document it fully, instead it focuses on the most “fun” bit: Timer B, which is used for graphical effects like raster interrupts (that allow us to use more colors on-screen). Once you feel you understand the stuff in this doc about Timer B, I hope you’ll find that knowledge transfers easily to the other timers!
Next we’re going to have a quick look at the registers we use to manipulate Timer B. You don’t have to memorize them, but I’m putting the information here so you’ll know where to find it once I start referring back to it later in the document.
Timer B registers
$120: The vector for Timer B/HBL $fffffa07, bit 0: Interrupt Enable - 1=Enabled, 0=Disabled $fffffa13, bit 0: Interrupt Mask - 1=Unmasked (will fire), 0=Masked (won't) $fffffa1b: Timer B Control - 0=Timer off 1-7=delay mode; divide master timer by: 1=4 2=10 3=16 4=50 5=64 6=100 7=200 8=Event Count mode (triggered by HBL, horizontal blank) 9-15=Pulse Extension mode, used to measure pulse length. Divides master timer by: 9=4 10=10 11=16 12=50 13=64 14=100 15=200 bit 4: Reset timer $fffffa21: Timer B Data - A counter that ticks down by 1 with every pulse. When it reaches zero, the interrupt fires and jumps to the vector at $120. $fffffa17, bit 3: 1=Software End-Interrupt mode, 0=Automatic End-Interrupt mode $fffffa0b, bit 0: Interrupt Pending - 1=Another interrupt in queue, 0=No interrupt in queue $fffffa0f, bit 0: Interrupt In-Service - 1=Interrupt in the middle of running, 0=Not running
How does this all this stuff work?
Most of the time, the MFP sits quietly in the back without disturbing anything or anyone. However, certain events can cause it to suddenly take control over the computer and force it to do something else. We call this an interrupt.
What happens when an interrupt fires depends on a number of vectors (sometimes called hooks, depending on who you ask). A vector is just a longword representing an address in memory, and when the interrupt fires, the computer jumps to the address in that vector.
For Timer B that address is $120. So every time a Timer B interrupt fires, the ST puts all other work on hold and calls the routine pointed to by the value at address $120.
In order to make Timer B help us do pretty raster effects, the kind souls at Atari connected a signal from the Shifter (which is ST’s graphics chip) to the MFP that sends a pulse every time the Shifter starts drawing a new scanline.
By setting Timer B to Event Count mode, it will count down the Timer B Data register every time an HBL (Horizontal Blank Line) pulse is generated. The Timer B Data counter lets us choose how often we want an HBL to actually fire an interrupt: By setting Timer B Data to 8, Timer B will only fire every 8th scanline, for example.
Short version: If both Interrupt Enable and Interrupt Mask bits aren’t set, your interrupt won’t fire.
Long version: Looking at the registers above we see there is a bit to enable Timer B, but also a bit to “mask” it. This may seem redundant, but it’s there for a reason; If we have Timer B set to fire every 8th scanline but mask turned off, Timer B won’t fire - but it will still update things in the background, like the Timer B Data counter and the End-Interrupt mode stuff (described later in this document).
Short version: If you can avoid interrupts overlapping, use Automatic End-Interrupt mode.
Long version: In Automatic End-Interrupt mode, when an interrupt fires the code pointed to by the vector at address $120 executes until an RTE - and that’s it. The programmer doesn’t have to do anything, really.
If the code triggered by the interrupt isn’t done executing when the next interrupt (of the same kind, obviously) triggers, the programmer will be in trouble; a crash would be highly likely.
Using the Software End-Interrupt mode, the programmer has a little insurance against that scenario.
In Software End-Interrupt mode, when an interrupt is triggered, the Interrupt In-Service bit is set high to let the system know that there’s already an interrupt in execution. It is up to the programmer to clear the Interrupt In-Service bit just before doing the RTE.
If another interrupt triggers while the Interrupt In-Service bit is set, the system sets the Interrupt Pending bit as a reminder for itself that there’s a new interrupt waiting, and as soon as the currently executing routine RTE’s the one “waiting in line” will be executed. However, if a third interrupt should try to fire, it simply won’t be honored - it will be “lost”.
Please note that the default mode is Software End-Interrupt mode.
As an assembler programmer, you are probably no stranger to crashes and those cute little comic book style bombs. If not, once you get to playing with interrupts you should be able to fill that gap in your education. Fast.
Not only must we take care to save and restore all registers our interrupt code use, we also need to temporarily shut down all interrupts before setting up and modifying interrupts so an interrupt isn’t called mid-way into writing settings. Luckily this is fairly easy to do, using the SR (Status Register).
Obviously the safest way to do this would be to only modify the bits of the SR that we need to change: In reality this is one of those cases where we can safely just clobber the register, because we know exactly what the consequences of doing that will be.
To shut down all interrupts:
To re-enable the ones we normally need in a demo or game:
So, what are the consequences of this? First of all, we’re clearing the lower byte of the SR, the CCR (Condition Code Register). This means no flags being set by previous instructions will remain set. Just keep that in mind.
As for the upper byte of the word, the upper nybble (the “2”) just means “trace mode off, supervisor mode on”. The lower nybble (“7”) sets the 68000 CPU’s internal interrupt level to max - meaning no interrupts are allowed.
Note: In order to keep this code short, I’m not doing any kind of saving and restoring of registers. This code WILL NOT EXIT, and even if it did you would be forced to reboot your ST afterwards.
; Timer B using HBL example ; Based in part on code by Evil/DHS - Thank him, not me! start: pea demo move.w #$26,-(sp) trap #14 addq.l #6,sp demo: ; Set up timers move.w #$2700,sr ; Shut down all interrupts ; Change vectors move.l #my_vbl,$70 ; 68000's VBL interrupt move.l #plain_rte,$68 ; 68000's HBL interrupt move.l #plain_rte,$134 ; MFP's Timer A move.l #plain_rte,$114 ; MFP's Timer C move.l #plain_rte,$110 ; MFP's Timer D move.l #plain_rte,$118 ; ACIA move.l #my_timer_b,$120 ; MFP's Timer B ; Disable and mask out all MFP Timers clr.b $fffffa07 ; Clear Interrupt Enable for MFP's Timer A and Timer B clr.b $fffffa13 ; Clear Interrupt Mask for MFP's Timer A and Timer B clr.b $fffffa09 ; Clear Interrupt Enable for MFP's Timer C and Timer D clr.b $fffffa15 ; Clear Interrupt Mask for MFP's Timer C and Timer D clr.b $fffffa1b ; Stop Timer B bset #0,$fffffa07 ; Interrupt Enable for Timer B (1=Enable, 0=Disable) bset #0,$fffffa13 ; Interrupt Mask for Timer B (1=Unmask, 0=Mask) move.b #1,$fffffa21 ; Timer B Data (counter will reach zero and fire every scanline) bclr #3,$fffffa17 ; Set Automatic End-Interrupt move.b #8,$fffffa1b ; Set Timer B to Event Count mode move.w #$2300,sr ; Turn all interrupts back on (well, the ones we need anyway) move.b #$12,$fffffc02 ; Turn off mouse reporting (for stability) endless_loop: bra endless_loop my_vbl: move #0,$ffff8240 ; Set background color to black rte my_timer_b: add.w #1,$ffff8240 ; Change background color rte plain_rte: rte
Appendix 1: MFP interrupts and 68000 interrupts
Looking at the legendary “hardware.txt”, which lists hardware addresses for the Atari 16⁄32-bit series of computers (at the time of writing available at: http://ftp.lip6.fr/pub/atari/Docs/hardware.txt ), we find:
$000068|Level 2 Int Autovector (HBL) |SD
…and a little later:
$000120|ST-MFP-8 - Timer B (HBL) |SD
What’s that about? Well, I’ve been simplifying things a bit: The MFP is not the ONLY device in the ST that can generate interrupts. The 68000 has 7 interrupt level on its own, and in Atari’s 16⁄32-bit machines, they’re mapped like this:
Motorola 68000 interrupt levels Level Assignment 7 NMI 6 MK68901 MFP 5 SCC (TT only) 4 VBLANK (Sync) 3 VME Interrupter (MegaSTE and TT only) 2 HBLANK (Sync) 1 Unused
The MFP is at level 6, VBLANK at level 4 and HBLANK at level 2. The Shifter’s HBL is actually connected to BOTH the MFP and the 68000 itself, so there are in fact two methods for catching HBL’s: One is using Timer B in Event Count mode, the other is the 68000 interrupt vector at $68. The VBL however is connected only to the 68000, so to catch it we need to use the vector at $70.
So are there any differences between catching the HBL using the MFP or the 68000’s interrupts? There are indeed: internally, different clocks are responsible for passing the signals along, and this explains the first important difference: The MFP’s Timer B HBL triggers quite a bit earlier, even before the visible area of the border, which is why the example code here has stable colors.
The $68 HBL triggers a bit later, well into the visible part of the screen, and it is less stable. The other main difference is that the MFP only fires on lines where the Shifter is displaying graphics (so if no borders are opened, only the normal 200 lines), but the $68 fires every time the Shifter starts a new scanline, regardless of whether that line is in the border area or not.
In the example code in this document we set SR to $2700 to shut down all interrupts. The “7” is the interrupt level, meaning only interrupts with a level higher than 7 are allowed to fire - and there are no higher levels of interrupt in the 68000.
When we are done setting up timers and vectors, we set SR to $2300 - the “3” indicating only interrupt levels 4 and up are allowed to fire. On an ST, that means the only 68000 interrupts allowed are VBLANK, the MFP and NMI (non-maskable interrupt) - not the HBLANK.
Oh, and in case it’s not obvious: In the example code in this document, I’m shutting down Timers A, C and D as well as the ACIA in order to save CPU time (an interrupt takes a number of cycles) and to prevent the raster colors to “go wobbly” (technical term).