.png)
S26 mentioned it almost in passing: the machine has its own interrupt, and it ticks about once a second - a wall clock, not a game clock, which is why S26 polls the screen instead of waiting for it.
That tick comes from somewhere, and it is worth knowing where. It is not the VDP, and it is not magic - it is a second chip, entirely separate from the one that draws the screen, whose only job is counting and interrupting. Reprogram it, and it will interrupt at whatever rate you calculate, on its own, whether or not your main loop is watching. A sound effect can be one shot fired and forgotten. A tune is notes that start on time - and that needs exactly this.
Ports again, and for the same reason as S15: the Z80 CTC - Counter/Timer Circuit - is a chip the processor talks to rather than memory it can read and write directly.
It sits at ports 40 to 43, one port per channel, four independent channels. Each channel counts down and raises an interrupt when it reaches zero, then starts again on its own - a heartbeat you configure once and then leave alone.
You are not the first program to use this chip. MOS's own ~1 Hz tick - the one S26 told you to ignore - is generated by two of these four channels, chained together. The other two, channels 0 and 1, drive the serial port's baud rate instead. This section uses channel 0 anyway: nothing here talks to the serial port, so there is nothing on it to disturb, but it is worth knowing you are borrowing a channel with an existing job, not an idle one.
A channel's state on entry is whatever the last thing to touch it left
behind, so the first step is always to reset it - a control byte with its
reset bit set, 3:
LD A,3
OUT (40),A
OUT (40),A
Written twice, deliberately. A channel that is currently expecting a time
constant byte (because something before you left it that way) will consume
the first 3 as that constant instead of as a fresh control word - and only
the second write is then guaranteed to land as the reset it is meant to be.
Cheap insurance, and worth keeping out of habit.
Resetting every channel resets MOS's clock too. Ports 41 to 43 belong to
channels you are not using, and writing 3 to all four - the usual way to
start from a known state - silences whatever they were doing, including that
~1 Hz tick. Harmless for a game that is about to run its own show entirely,
worth knowing if you ever wonder where the tick went.
Up to now, an interrupt has been something the machine does to itself, invisibly. Taking one over means telling the processor two things: which addresses count as a valid interrupt handler, and where to find them.
IM 2 puts the processor into the mode that supports this. Instead of
jumping to one fixed address the way the machine's own interrupt does, the
processor builds a 16-bit address out of two halves - a high byte you supply
once, in register I, and a low byte the interrupting device itself puts on
the bus - and jumps through that address, read from a table you provide.
The table has to sit on an 8-byte boundary, and each entry is two bytes - the address of your handler:
ORG 256
JP start
DEFS 5 ; pad: the vector table must sit on an 8-byte boundary
vector: DEFW handler,handler,handler,handler
Four identical entries, one per channel, because only channel 0 is actually going to interrupt here - the others are filled in so that a stray interrupt from an unused channel lands somewhere sane rather than in whatever bytes happen to follow.
Telling the processor about the table is three writes: the table's high byte
into I, then the table's low byte to the channel itself, which is how the
CTC knows where its own entries begin:
IM 2
LD A,1 ; vector table high byte: the table sits at 0108h
LD I,A
LD A,8 ; vector table low byte
OUT (40),A
One more byte configures what the channel actually does. A5h (binary
10100101) means: interrupt enabled, timer mode, and a prescaler of 256 -
divide the system clock by 256 before counting at all. Then a time
constant - the actual divider - follows:
LD A,165 ; A5h - interrupt enable, timer mode, prescaler 256
OUT (40),A
LD A,0 ; time constant 0 = 256: about 62 interrupts a second
OUT (40),A
The rate that produces is measured, not guessed: with the prescaler at 256
and the time constant at its maximum (0, which the chip reads as 256), the
channel interrupts a clean 62.1 times a second. Halve the time constant
and the rate doubles - the relationship is
rate = (about 4.07 million) / 256 / time constant
so a time constant of 16 gives roughly 1000 a second, and one of 160 gives roughly 100. Work backwards from a rate you want to the time constant that gives it, and you have a clock running at whatever speed the job needs, independent of the screen's own 50 Hz - which is untouched by any of this and keeps right on being what S26 polls.
handler:
PUSH AF
PUSH HL
LD HL,(count)
INC HL
LD (count),HL
IN A,(9) ; re-arm: reading status drops the VDP's INT line
POP HL
POP AF
EI
RETI
Same discipline as any subroutine that borrows registers - S12's rule, not a
new one: push what you touch, pop it back. RETI is RET's counterpart for
an interrupt: it returns exactly the same way, and tells the daisy-chained
devices behind this one that the interrupt has been serviced. EI
immediately before it re-arms the processor for the next one - the processor
disables further interrupts the moment it takes one, and nothing turns them
back on for you.
ORG 256
JP start
DEFS 5 ; pad: the vector table must sit on an 8-byte boundary
vector: DEFW handler,handler,handler,handler
start: DI
LD B,2 ; reset all four channels, twice - a channel that
LD A,3 ; already expects a time constant would eat the
kill: OUT (40),A ; first 3 as data instead of as a fresh control word
OUT (41),A
OUT (42),A
OUT (43),A
DJNZ kill
IM 2
LD A,1 ; vector table high byte: the table sits at 0108h
LD I,A
LD A,8 ; vector table low byte
OUT (40),A
LD A,165 ; A5h - interrupt enable, timer mode, prescaler 256
OUT (40),A
LD A,0 ; time constant 0 = 256: about 62 interrupts a second
OUT (40),A
IN A,(9) ; clear the VDP's line before the first edge can arrive
LD HL,0
LD (count),HL
EI
wait: LD HL,(count)
LD DE,500
OR A
SBC HL,DE
JR C,wait ; the main loop does nothing but watch a number climb
DI
LD A,3 ; stop channel 0 before handing back to MOS
OUT (40),A
LD HL,(count)
RET
handler:
PUSH AF
PUSH HL
LD HL,(count)
INC HL
LD (count),HL
IN A,(9) ; re-arm: reading status drops the VDP's INT line
POP HL
POP AF
EI
RETI
count: DEFW 0
What you should see
Nothing, for a few seconds - the main loop's wait does nothing but read a
number that an interrupt is climbing in the background. Then:
A BC DE HL PC SZ-H-PNC
03 0041 01F4 01F4 0769 01000010
HL reads 01F4 - 500, the target - reached entirely by the handler while
wait did nothing but ask. Nothing in the main loop counted, delayed, or
watched the clock. It just wasn't finished yet, and then it was.
Look at the two instructions right before RET. They stop channel 0
before handing control back to MOS, and they are not decoration.
Delete them and run the program again. The same dump appears - and then a
second one, uninvited, at an address nowhere in this program, with registers
this program never touched. The channel is still interrupting, the processor
is still in IM 2, and the vector table still points at this program's
handler - all of which MOS's own command prompt was never written to expect.
One more interrupt landing in that context is enough to send it somewhere
meaningless.
The rule: if a program that has taken over the CTC is going to return to MOS, it must disable the channel first. A program that does not return - which by S29 is every one of them, ending only at Restart - does not need to, since there is no handoff to protect.
0 to 128. Recalculate the expected rate
from the formula, predict how long the existing wait for 500 will now
take, and check.RET, exactly as described
above, and watch it happen once on purpose.OUT (40),A to OUT (41),A throughout the setup, moving everything
to channel 1. Does it still work? What has to move with it?IN A,(9) from inside the handler. Does the count still climb at
the same rate? What does that tell you about what that line is actually
for, versus what the comment guesses?handler and the rest point at a label containing just RETI. Nothing
should change - channel 0 is the only one enabled - which is the point:
confirm it rather than assume it.28.1 - Pick a rate. Using the formula, work out the time constant for 200 interrupts a second, set it up, and confirm by counting for a measured number of them - as the section's own example does - rather than by watching a clock.
28.2 - A metronome. Toggle the backdrop colour (S24) every N interrupts instead of counting to a fixed target and stopping. Pick an N that makes the flip happen roughly twice a second.
28.3 - Time a tune. S27 built sounds and silences out of pause loops. Rebuild one short sequence so that the CTC interrupt advances which note is playing, at a rate you choose, while the main loop is free to do something else entirely - the point of this whole section.
28.4 - Two clocks at once. Run this section's counter alongside S26's frame-flag wait in the same program, each doing its own thing. Confirm the two counts drift apart at difference rates rather than staying in step - they are answering to two completely independent clocks.
| What you see | What it means |
|---|---|
| A second, unrequested register dump after the expected one | The CTC and IM 2 were left running when the program returned to MOS. Disable the channel and DI before RET, or never return at all. |
| The count never reaches its target | Check the vector table sits on an 8-byte boundary, and that I holds the table's actual high byte. A wrong vector jumps somewhere that is not your handler at all. |
| The machine's own clock (S26's ~1 Hz tick) stops working in a later program | Something reset all four CTC channels. Channels 2 and 3 are MOS's; reset only the one you use if you need MOS's tick to survive. |
| The count climbs far faster or slower than the formula predicts | Check the time constant byte and the prescaler bit in the control word - A5h is prescaler 256; clearing that bit changes the divisor to 16. |
| Everything after taking the CTC feels subtly wrong, keys included | Undiagnosed here - see Appendix III and treat any MCAL call as untested territory once IM 2 is in charge. Direct hardware access, as S17 onward already uses for everything else, is the safe ground. |
IM 2 reads a jump address from a table you provide - high byte in I,
table on an 8-byte boundary, low byte given to the channel itself.A5h plus a time constant sets timer mode, and the rate
is ~4.07MHz / 256 / time constant - a clock ticking at whatever speed the
job needs, confirmed at 62.1/s when the constant is 256.EI then RETI.S29. The first game. Every technique from S1 onward, in one place: sprites, input, collisions, sound, a score, and now, if you want it, a clock that keeps its own time.