Amiga CIA Timers, TOD Clock, and Interrupt Control
Trace the Amiga 8520 CIA timer latches, TOD counters, interrupt masks, and operating-system ownership without confusing them with custom-chip timing.
The Amiga’s Complex Interface Adapters (CIAs) are timing and I/O devices, not aliases for the custom-chip raster or audio clocks. Each CIA contains two 16-bit interval timers, a time-of-day (TOD) counter, an interrupt control register, serial logic, and parallel ports. The timer latches, running counters, event inputs, interrupt flags, and CPU-visible masks form separate pieces of state. Collapsing them into one software timer is a common source of inaccurate disk, input, and timing behavior.
The Amiga Hardware Reference Manual documents the CIA register map and behavior, while the AmigaOS documentation explains how system software allocates CIA resources. The two perspectives matter: a register-level emulator must reproduce hardware, but an Amiga program that runs under the operating system should normally acquire CIA resources through the supported resource interface rather than commandeering registers behind the OS’s back.
Two CIAs, separate machine roles
The Amiga uses CIA-A and CIA-B at distinct custom-I/O address ranges. Their registers are byte-wide and appear on spaced addresses in the 68000 I/O map; code should use the documented register definitions instead of assuming a packed array of sixteen adjacent bytes. This is particularly important in an emulator, where a memory-mapped access layer may otherwise accept a wrong byte lane and still appear to work for a narrow test.
The ports connect to different machine functions. CIA-A is associated with keyboard and joystick/button inputs and system control signals; CIA-B serves serial and floppy-control lines. Both chips also contain timers, TOD and interrupt state, but the event wiring differs. CIA-A TOD can receive the vertical-sync timing input; CIA-B TOD can receive a horizontal-sync-derived input. The practical clock rate and source must therefore be tied to a specific Amiga configuration rather than assumed to be a universal wall-clock 50 or 60 Hz signal.
The 8520 used in Amiga systems should not be treated as a generic 6526 with every peripheral behavior unchanged. In particular, the TOD register organization is a 24-bit counter in the 8520 implementation described by the Amiga documentation, while other CIA-family variants have their own details. Keep the device model selected by the machine configuration and avoid importing behavior from a C64 CIA test without checking the relevant chip and board.
Timer latch versus counter
Timer A and Timer B each have a 16-bit write latch and a 16-bit read counter. A write loads or changes the latch value; reads report the current counter. The control register selects whether the timer is stopped or started, whether it operates continuously or one-shot, whether its output is exposed on a port-B pin, and what source drives the count. Timer B can count timer-A underflows in addition to other documented input choices, allowing a longer period or a derived timing sequence.
This latch/counter distinction should be explicit in code. A convenient host implementation might represent reload, counter, running, one_shot, source, and underflow_pending separately. Treating every write as an immediate replacement of the current count can produce off-by-one behavior when software programs a timer while it is active. The manual’s force-load strobe is a control operation; it is not a stored bit that should remain set after the write.
The timer’s count input also matters. When clocked from the Amiga’s E-clock, the timer is based on the processor bus timing domain. In event-counting modes, it counts transitions or underflows from a selected signal rather than CPU instructions. Never advance it by “one tick per emulated frame” just because a game happens to use it near vertical blank. A correct system schedule advances CIA input edges and timer state on the same deterministic timeline as memory-mapped accesses and interrupts.
When the counter underflows, the chip records an event and reloads or stops according to mode. The observable point is not equivalent to a host callback firing late. A timer event can set its interrupt flag even while the interrupt source is masked; unmasking later should expose pending state according to the hardware protocol. This is why flag, mask, and CPU interrupt delivery must not be collapsed to one Boolean.
Interrupt Control Register semantics
The ICR reports pending CIA interrupt sources and controls which sources are enabled. Its write protocol uses the high bit to distinguish setting interrupt-mask bits from clearing them. A value that sets the Timer A mask is not merely a conventional “write-one-to-enable” register; software writes a set/clear command plus selected source bits. Reading ICR returns source flags and acknowledges/clears the latched flags as defined by the chip interface. An emulator should implement those read and write side effects, not store the last byte and return it unchanged.
The CIA interrupt output is only one stage in the Amiga interrupt system. The CPU sees an interrupt level selected by system wiring, and the Amiga’s interrupt controller/software layer has its own enable and service behavior. A CIA source can be pending while its mask is clear, or enabled at the CIA while the system-level interrupt is disabled. Debug output should show the chain: timer event, CIA flag, CIA mask, CIA interrupt line, system interrupt enable, CPU delivery, handler acknowledgement.
AmigaOS makes CIA access a resource-managed operation. The official programming guidance warns that the operating system and devices may already use CIA hardware, and recommends the cia.resource interfaces for allocating interrupt bits and timers. Writing ICR directly while the OS is active can interfere with unrelated users of the same chip. A technical article or emulator may describe direct hardware semantics, but application guidance should not encourage unsynchronized register ownership on a multitasking system.
TOD as an event counter, not a calendar
The TOD registers form a counter driven by external timing pulses. They do not intrinsically know the current date or UTC time. Depending on the CIA and configuration, an external sync signal supplies the tick. The counter has high, middle, and low bytes; reading the high byte latches a consistent snapshot for subsequent reads of the remaining bytes, and writes use an alarm-versus-clock selection controlled by the associated control register. Correct software must observe the documented read/write order.
This snapshot behavior is an atomicity contract. If the emulator reads three separate live bytes without latching, the counter could advance between reads and return a hybrid value that never existed. Conversely, latching on the wrong register or releasing the snapshot too early can make a well-formed software read produce inconsistent time. Test the high-mid-low sequence across a tick boundary and verify both ordinary reads and a low-byte rollover.
The TOD counter is useful as a coarse event clock or alarm source, but its rate depends on the actual input source. A PAL and NTSC configuration may supply different event cadence. If an emulator synthesizes a VSync pulse, the source should be the machine’s timing model and its region. Using the host’s local system clock would make deterministic replay, frame stepping, and save-state restoration impossible.
A state model for timer traces
The Python record below captures the minimum useful evidence for a timer transition. It is a diagnostic schema, not a CIA implementation. Keeping event source, timer identity, and before/after state lets a test distinguish a timer underflow from a register write that merely loads a new latch.
from dataclasses import dataclass
@dataclass(frozen=True)
class CiaEvent:
cycle: int
cia: str
timer: str
kind: str
counter_before: int
counter_after: int
icr_flags: int
def validate_event(event):
if event.cia not in {"A", "B"}:
raise ValueError("CIA identity must be A or B")
if event.timer not in {"A", "B", "TOD", "none"}:
raise ValueError("unknown CIA timing source")
if not 0 <= event.counter_before <= 0xFFFF:
raise ValueError("timer counter is 16-bit")
if not 0 <= event.counter_after <= 0xFFFF:
raise ValueError("timer counter is 16-bit")
if not 0 <= event.icr_flags <= 0xFF:
raise ValueError("ICR snapshot is a byte")
A full trace should additionally record the memory access address, register offset, bus cycle, latch value, timer source, run mode, interrupt-mask state and the CPU/system interrupt enables. Saving this trace around one missed disk or input event is more actionable than dumping every CIA register once per frame.
Verification matrix
Begin with independent timer tests. Load a latch, force-load the counter, run a small count, stop before underflow, then resume. Exercise continuous and one-shot modes, Timer A and Timer B, and Timer B’s documented Timer-A-underflow source. Validate the output-pin pulse/toggle state only when the relevant port direction and output-enable controls are configured. Include counter values of zero, one, and the largest 16-bit value to expose boundary and reload errors.
Next test the ICR protocol. Raise a timer event with its mask disabled and confirm the flag is observable without CPU delivery. Enable it and confirm the interrupt line is asserted as expected; then read ICR and verify the acknowledgement side effect. Clear one mask while another is set, and verify that a set/clear command affects only the selected source. Run these tests independently for CIA-A and CIA-B because their system wiring is not identical.
For TOD, inject a deterministic pulse train, read high-mid-low across rollover, set and inspect the alarm state, and test the documented interrupt flag. Repeat with the machine’s PAL/NTSC timing source. Save and reload while a timer is active and while a TOD snapshot is latched; the restored machine should deliver the same subsequent event cycle and byte sequence.
At integration level, use AmigaOS resource calls for software running with the OS present, and isolate bare-metal tests that intentionally take over CIA state. Include disk and keyboard use cases so that an emulator does not pass a synthetic timer test while breaking the board’s connected devices. Record whether a result is hardware-observed, reference-manual-derived, or emulator-tested; do not mislabel one as another.
Acceptance criteria
A robust Amiga CIA model has distinct latches and counters, input-source-aware timer clocks, correct one-shot/continuous operation, event flags independent from masks, side-effecting ICR reads, and coherent TOD snapshots. It also exposes resource ownership clearly enough that a debugger can tell when software bypasses AmigaOS management. Those boundaries make timer and interrupt defects reproducible rather than mysterious timing drift.
Related:
- Amiga Paula Audio DMA: Channel Reloads, Periods, and Sample Ownership
- Amiga Copper and Bitplane DMA: Beam-Synchronized Display Lists
Sources: