
In the assembly course's S26 your game kept time by asking. The main loop read the status register over and over until the frame flag rose, and the machine did nothing else while it waited. In S28 you took over the CTC's interrupt, but it ticks to its own clock, not the screen's. The 256 can do better: its video chip can interrupt the Z80 once every frame. Your program is told, instead of having to ask.
On the TC-01, the only interrupt a program meets is MOS's own: the CTC's tick, about once a second. The video chip raises its frame flag, but the signal never reaches the Z80. That is why S26 polled.
On the 256, the signal does reach it. Bit 5 of register 1 switches it on, and from then on the Z80 is interrupted once a frame. You met this in S3, where writing 224 to register 1 threw a TC-01 program back to MOS. Here is why.
MOS runs the Z80 in IM 2 with I = FBh, so its interrupt table is at FB00h. The video chip's interrupt arrives with vector FFh, so the Z80 takes the handler's address from FBFFh and FC00h. MOS has no entry there: FC00h is part of MOS's own code. The Z80 jumps to whatever those two bytes happen to make, and the program is gone.
So to use the interrupt you give the Z80 an interrupt table of your own, with a proper entry for vector FFh.
MOS uses its table too. Its once-a-second tick comes through the entry at FB06h, which holds FC65h, MOS's tick handler, and that handler keeps MOS's clock. Build a table from scratch and the tick goes nowhere useful. The clock stops for as long as your program runs, and the lost time is never made up.
The answer is to copy MOS's table, all of it, and add your one entry. Every vector MOS uses then still leads where MOS expects, and vector FFh leads to you. Never fill in a table from TC-01 listings: the 256's MOS keeps its handlers at different addresses.
The table has two rules. It must start on a 256-byte boundary, because I supplies the high byte of every entry's address. And it takes 257 bytes: vector FFh's entry starts at the table's last byte, so its second byte is the first byte past it. A table at 9000h uses 9000h to 9100h.
The code that does all this lives in one file, frame.asm, which the rest of the primer includes. Type it in once, save it next to your programs, and put INCLUDE "frame.asm" at the end of any program that wants the frame interrupt. Here it is, a part at a time.
TABLE: EQU 9000h ; our IM 2 table, 9000h-9100h (257 bytes)
ENTRY: EQU 9110h ; where the interrupt really enters: above 8000h
TICKS: EQU 9200h ; frames counted since irqon (16 bits)
FLAG: EQU 9202h ; set each frame, cleared by waitf
STATUS: EQU 9203h ; the status byte S#0 as read this frame: bit 7
; frame, bit 6 ninth/fifth sprite, bit 5 collision
TICKS counts frames from the moment the interrupt goes on: your game's clock, in fiftieths of a second at 50 Hz. FLAG is how the interrupt tells the main loop that a new frame has begun. ENTRY is explained below.
setreg: OUT (9), A
LD A, B
OR 128
OUT (9), A
RET
Writes A to video register B, the usual two bytes to port 9. Later sections use it too.
irqon: DI
LD HL, 0FB00h ; MOS's table: its clock tick entry stays as it is
LD DE, TABLE
LD BC, 256
LDIR
Interrupts off first: no interrupt may arrive while the table is half built. Then the copy of MOS's 256 bytes, FB00h to FBFFh, to 9000h.
LD HL, stub ; the entry stub must live above 8000h:
LD DE, ENTRY ; a MOS call has the ROM over 0000h-7FFFh, and an
LD BC, stubend-stub ; interrupt during one must not land in ROM
LDIR
LD HL, ENTRY ; vector FFh takes its address from 90FFh and 9100h
LD (TABLE+255), HL
A small piece of code, the stub, is copied up to 9110h, and the FFh entry, at 90FFh and 9100h, is pointed at it. Why the stub has to be up there is the next part.
LD HL, 0
LD (TICKS), HL
XOR A
LD (FLAG), A
LD A, 90h ; I = the table's page
LD I, A
RET
The count and flag start at zero, and I is pointed at the new table. MOS already has the Z80 in IM 2, so there's no IM 2 to write. irqon returns with interrupts still off. You then set register 1 with bit 5 on and EI yourself.
When your program calls MOS with RST 8, MOS pages its ROM in over the bottom 32K of memory while the call runs. A frame interrupt can arrive at any moment, including in the middle of a MOS call. If the Z80 then jumps to a handler down near 0100h, it finds ROM there instead of your program, and the machine restarts.
So the interrupt enters at 9110h, above 8000h, where the ROM never reaches:
stub: PUSH AF
IN A, (26h)
BIT 1, A ; 1 = the ROM is in
JR Z, st1
OUT (24h), A ; ROM out
CALL frame
OUT (24h), A ; ROM back in, as MOS had it
POP AF
EI
RETI
st1: CALL frame
POP AF
EI
RETI
stubend:
Bit 1 of port 26h says whether the ROM is in: 1 means it is. Port 24h is the switch: each write to it swaps the bottom of memory between ROM and RAM, as it does on the TC-01. If the ROM is in, the stub pages it out, calls frame down in your program, and pages it back in before returning, so MOS carries on as if nothing had happened. If it isn't, the stub just calls frame. Either way it ends as S28's handler did: EI, then RETI.
The stub uses only relative jumps, so it works at whatever address it's copied to. Its CALL frame names an absolute address, which is what you want: frame stays where it was assembled.
This gives your every-frame code one rule: it must not call MOS. It may have interrupted a MOS call, with the ROM paged out from under it. Print, read the keyboard and call MOS from the main loop, never from the interrupt.
frame: PUSH AF
PUSH BC
PUSH DE
PUSH HL
IN A, (9) ; read the status: this clears the frame flag
LD (STATUS), A ; keep it: reading cleared the sprite flags too
LD HL, (TICKS)
INC HL
LD (TICKS), HL
LD A, 1
LD (FLAG), A
CALL onframe ; the program's every-frame work
POP HL
POP DE
POP BC
POP AF
RET
S28's discipline: push what you touch, pop it back. The IN A, (9) matters most. Reading the status register clears the frame flag, and that is what stops the interrupt. Leave it out and the interrupt comes straight back the moment the handler returns, again and again, and the program never runs.
That one read also clears the sprite flags: collision (bit 5) and too many sprites on a line (bit 6). If the handler threw the byte away, your game would never see them, so it keeps a copy in STATUS. Under the interrupt, test STATUS for the chip's collision bit (the assembly course's S15), never port 9 itself.
Then the count goes up, the flag is set for the main loop, and onframe, which your program supplies, does whatever must happen every frame. Palette changes, like S6's fade, belong there. onframe may use AF, BC, DE and HL.
That read of port 9 brings a second rule. The 256's chip has ten status registers, and register 15 says which one port 9 reads. If an interrupt arrives while your main loop has register 15 set to anything but 0, the handler reads the wrong status register, the flag is never cleared, and the interrupt comes back without end. Whenever you set register 15, do it with interrupts off, and put it back to 0 before EI. onframe must leave it at 0 too.
waitf: LD HL, FLAG
wf1: LD A, (HL)
OR A
JR Z, wf1 ; not yet
LD (HL), 0 ; seen: ready for the next one
RET
This takes the place of S26's waitframe, and it's used the same way, at the top of the main loop. The difference is what it waits on: a byte of RAM that the interrupt sets, not the video chip. And TICKS goes on counting frames however busy the main loop is.
irqoff: DI
LD B, 1
CALL setreg
LD A, 0FBh ; MOS's own table
LD I, A
EI
RET
Call irqoff with A holding the value you want register 1 to keep, with bit 5 clear: 0C0h for MOS's normal display. It switches the interrupt off at the video chip and points I back at MOS's table.
tomos: DI
LD A, 02h ; R#0: Graphics II
LD B, 0
CALL setreg
LD A, 0Eh ; R#2: MOS's name table
LD B, 2
CALL setreg
LD A, 76h ; R#5: MOS's sprite table
LD B, 5
CALL setreg
XOR A ; R#11 and R#14: page 0
LD B, 11
CALL setreg
XOR A
LD B, 14
CALL setreg
EI
RST 8
DEFB 0BEh ; MOS: clear to a 40-column screen
RET
tomos puts back the video registers that later sections change, then calls MCAL BE to restore MOS's screen, font, sprites and palette. Programs that keep MOS's own screen don't strictly need it, but calling it on the way out is a habit worth having from the start.
The interrupt comes once a frame, so it follows the line-standard switch: 50 times a second at 625 lines, 60 at 525. A game that wants the same speed on every machine writes 2 to register 9, which makes the video chip produce 50 frames a second whatever the switch says. The program below does this before it turns the interrupt on.
Bit 0 of port 80h is a second switch for the frame interrupt. Write 01h there and the interrupt doesn't arrive, even with register 1's bit 5 set. MOS 2.1 leaves it unmasked, which is why S3's 224 crashed. frame.asm doesn't need port 80h, because register 1 does the job. But a program that has written 01h there gets no frame interrupt at all.
A ship crosses the screen at exactly one pixel a frame, driven by the frame interrupt, and after 200 frames the program hands the machine back to MOS. Save frame.asm in the same folder.
; S7: a game loop driven by the frame interrupt.
;
; A ship crosses the screen one pixel a frame. The interrupt handler in
; frame.asm counts the frames; the main loop waits for each one, moves the
; ship, and after 200 frames hands the machine back to MOS.
ORG 256
; --- the ship's shape: sprite pattern 128 (VRAM 1800h + 128*8)
DI
LD A, 00h ; VRAM 1C00h, for writing
OUT (9), A
LD A, 1Ch+64
OUT (9), A
LD HL, ship
LD B, 8
shape: LD A, (HL)
OUT (8), A
INC HL
DJNZ shape
; --- sprite 0: the ship, white; sprite 1 ends the list
LD A, 00h ; VRAM 3B00h, the sprite attributes
OUT (9), A
LD A, 3Bh+64
OUT (9), A
LD A, 95
OUT (8), A ; Y
XOR A
OUT (8), A ; X
LD A, 128
OUT (8), A ; pattern
LD A, 15
OUT (8), A ; colour
LD A, 208
OUT (8), A ; Y = 208: no more sprites
; --- 50 frames a second, whatever the switch says
LD A, 2
LD B, 9
CALL setreg ; R#9 = 2
; --- the frame interrupt on
CALL irqon ; our table in, I = 90h, interrupts still off
LD A, 0E0h ; R#1: display on, frame interrupt on (bit 5)
LD B, 1
CALL setreg
EI
; --- the game loop: once a frame
main: CALL waitf ; sleep until the next frame begins
LD HL, shipx
INC (HL) ; one pixel right
CALL place
LD HL, (TICKS) ; how many frames so far?
LD A, L
CP 200
JR C, main ; fewer than 200: go round again
; --- back to MOS
LD A, 0C0h ; R#1: display on, frame interrupt off
CALL irqoff ; and MOS's table back
CALL tomos ; and MOS's screen
RET
; --- place: write the ship's X into its attribute entry
place: DI
LD A, 01h ; VRAM 3B01h: sprite 0's X
OUT (9), A
LD A, 3Bh+64
OUT (9), A
LD A, (shipx)
OUT (8), A
EI
RET
; --- onframe: frame.asm calls this every frame, from the interrupt
onframe:
RET ; nothing extra to do in this program
shipx: DEFB 0
ship: DEFB 00011000b
DEFB 00011000b
DEFB 00111100b
DEFB 01111110b
DEFB 11111111b
DEFB 11111111b
DEFB 00100100b
DEFB 01000010b
INCLUDE "frame.asm"
The sprite is set up as in the assembly course's S19, in MOS's tables: the shape goes into pattern 128, at 1800h + 128 x 8 = 1C00h, and the attributes at 3B00h. MCAL BE puts MOS's character 128 back on the way out.
place writes the video address and the byte with interrupts off. The interrupt reads port 9 too, and keeping it out of the middle of a two-byte address is a habit the blitter sections will rely on.
The loop ends on TICKS, the interrupt's own count, not on the ship's position. Both read 200 at the end, because the main loop moves the ship once for every frame the interrupt counts.
Build and run it as in S2, with frame.asm in the same folder.
What you should see
A small white ship appears at the left of the screen, below MOS's text, and slides right at a steady 50 pixels a second, 25 pixels every half second. After four seconds it stops at X = 200, and the screen clears to MOS's blue with the register display and the prompt.

INC (HL) in the main loop to two INC (HL)s. How fast does the ship go now, and where is it when the program stops?onframe move the ship instead: LD HL, shipx, INC (HL), RET. What happens to the speed, and why?IN A, (9) in frame. What do you expect, and does the program ever return?CALL setreg after LD B, 9 and set the line-standard switch to 525 lines (S2). How far has the ship got after two seconds?| Symptom | Cause |
|---|---|
The program is back at Ready the moment register 1 is written | Bit 5 set with MOS's table still in place: irqon wasn't called first |
| The machine restarts at random while the program calls MOS | The interrupt entered below 8000h during a MOS call. Use frame.asm's stub, and never call MOS from onframe |
| The program stops dead once the interrupt is on | The handler doesn't read port 9, or register 15 was left non-zero when it arrived |
| MOS's clock is behind after your program has run | The tick entry in your table doesn't lead to MOS's handler. Copy MOS's table, don't build one |
You can switch on the 256's frame interrupt safely: a copy of MOS's IM 2 table at a 256-byte boundary, an FFh entry leading to a stub above 8000h, a handler that reads port 9. MOS's clock keeps going, register 9 = 2 makes it 50 a second everywhere, and waitf gives the main loop its heartbeat. Two rules come with it: nothing in onframe calls MOS, and register 15 is 0 whenever an interrupt can arrive.
S8, Eight Sprites a Line: the 256's sprite mode, driven from this loop. S15 comes back to vector FFh: the line interrupt arrives through the same entry.