← Back to Courses
module
28

A clock of your own

Why this matters

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.

A second chip, again

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.

Resetting a channel

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.

IM 2, and the interrupt vector table

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

The control word, and a rate you choose

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.

The handler

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.

The code

        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.

Cleaning up

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.

Change one thing

  • Change the time constant from 0 to 128. Recalculate the expected rate from the formula, predict how long the existing wait for 500 will now take, and check.
  • Delete the two clean-up instructions before RET, exactly as described above, and watch it happen once on purpose.
  • Change 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?
  • Remove 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?
  • Change the vector table's four entries so that only the first points at 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.

Exercises

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.

When it goes wrong

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.

Summary

  • The Z80 CTC is a second, independent chip at ports 40-43, four channels, each counting down and interrupting on its own once configured.
  • Reset a channel with a control byte carrying the reset bit, written twice defensively. Resetting every channel also silences MOS's own ~1 Hz tick, which turns out to be two of these same four channels, chained.
  • 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.
  • A control word of 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.
  • A handler follows the same discipline as any other borrowed registers - push what you touch - and ends EI then RETI.
  • A program that takes over the CTC must disable it before returning to MOS, or the next interrupt lands somewhere MOS never prepared for and corrupts it. A program that never returns does not need to.

Next

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.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.