Skip to content
RetrogamingDeep Dive Published Updated 8 min readViews unavailable

PC Engine HuC6280 Timer IRQ: Reload Semantics, Prescaling, and Acknowledgement

Reproduce the HuC6280's seven-bit interval timer, oscillator prescaler, reload edge, interrupt request flag, and counter-read caveat in PC Engine software.

The PC Engine’s HuC6280 timer is a small interval source with a surprisingly strict read and interrupt contract. It has a seven-bit downcounter, a seven-bit reload register, a start/stop control bit, and a prescaler. On borrow, the timer requests an interrupt and reloads for another interval while enabled. The interrupt request remains separate from the interrupt mask and CPU’s global interrupt flag. An emulator that replaces this with a host timer callback can lose the exact relationship between a reload write, counter phase, IRQ assertion, and acknowledgement.

The HuC6280 hardware manual describes the timer’s structure, prescaler, reload behavior, and a caution that a counter read may be invalid and should be checked with a second read. MAME and Mednafen-derived emulators provide independently inspectable implementations of timer state, reload, IRQ, and read behavior. The manual is the source for hardware claims; implementation code is corroborating evidence for the event model, not silicon measurement.

Register window and address mapping

The timer is accessed through the HuC6280’s I/O page. In logical space, the reload/counter register appears at offset $0C00 and the timer control register at $0C01 when the applicable memory-page register maps the device page as documented. Writes to the even register program the low seven bits of the reload value; reads from it return the current seven-bit downcounter. The odd register uses bit zero as start/stop control. This address path is part of the processor’s memory map, so translation through the MPR setting must be considered before a CPU bus write reaches the timer.

The distinction between read and write at the same offset is important. Software writes a reload value but reads the live counter. If an implementation returns the reload value on every read, it may appear correct just after programming and then fail any code that polls the current count. A debugger should label the register according to direction: reload latch on writes, counter on reads.

The hardware manual also documents that, following reset, the reload and downcounter data are invalid while the timer is stopped. Do not invent deterministic power-on values for those latches unless the console reset path or a measured device establishes them. The start transition initializes a running interval from the current reload value, which is a more meaningful test boundary than a guessed reset count.

Prescaler and interval calculation

The internal timer clock is derived from OSC1 through a divide-by-three stage and a further divide-by-1,024 prescaler. The seven-bit downcounter receives the resulting periodic tick. The manual’s 21.48 MHz example gives a prescaler output near 6.992 kHz and an interrupt interval spanning roughly 143 microseconds to 18.3 milliseconds. Those numbers describe the cited oscillator configuration; they are not a universal host-time constant for every build or timing model.

The period is quantized in prescaler ticks. Software selects a reload value from 0 through 127. In the common implementation model, the count interval is the programmed seven-bit value plus one timer tick; MAME’s h6280.cpp expresses the reload in master-clock units with that +1, followed by the divide-by-three and divide-by-1,024 scaling. Keep this formula tied to the chip clock and internal clock relationship. Do not convert the count to milliseconds and schedule a host sleep, because host scheduling granularity and machine-region clock are unrelated.

When the reload register is changed while the timer is running, the new value is held for a later reload rather than necessarily replacing the counter immediately. The current countdown can complete and then load the updated interval. A start from stopped to running is another defined reload boundary. Model reload_latch, counter, prescaler_phase, and enabled independently; one integer cannot represent all four.

Stopping the timer resets the prescaler phase according to the hardware manual. When it is restarted, the reload transition begins from the documented state rather than continuing a partial divide chain from before the stop. This affects phase-sensitive software even if the average timer frequency is right. A useful regression stops one tick before underflow, stops the timer, changes the reload value, restarts, and checks the first subsequent IRQ.

Borrow, request flag, and CPU interrupt delivery

When the downcounter borrows, the timer sets the timer interrupt request bit in the HuC6280 interrupt-request register and reloads the downcounter. The request flag is not the same thing as delivery to the CPU. The timer-specific interrupt-disable bit can mask that source, and the CPU’s global interrupt-disable flag can also prevent immediate service. Maintain the request flag even when delivery is masked so software can observe pending state later.

After the interrupt handler has dealt with the event, software acknowledges the timer request by writing the interrupt request register. Returning from the interrupt handler with RTI restores processor flags but does not itself acknowledge the timer. An emulator that clears the request automatically on interrupt entry or RTI can lose a pending timer event, especially when the handler shares the IRQ path with other sources.

The timer borrows repeatedly while enabled. Every borrow sets the request flag and reloads the counter, but if the flag is already set, software may not be able to distinguish multiple elapsed intervals from one pending request without additional timing observation. A deterministic scheduler should still process all timer borrows, even if the guest-visible request bit stays asserted. This distinction becomes important when the CPU is masked for several intervals and then unmasks the timer.

Timer, IRQ1, and IRQ2 share the HuC6280’s interrupt infrastructure but have separate request/mask bits. Preserve each source independently, apply the documented priority when several requests are pending, and test timer acknowledgement without clearing unrelated external IRQ state. A status-register read and a write acknowledge are not interchangeable operations.

Counter reads are a measurement, not a free snapshot

The manufacturer manual explicitly warns that a downcounter read can return an invalid value and advises reading twice and comparing the readings to assure the result. This is a hardware caveat, not a license to return arbitrary noise. Represent the actual observed read behavior in a chip model and test it at the prescaler edge. A safe software routine can take two samples and check for a stable value according to the manual; it should not assume that the first read is a perfect atomic snapshot.

The register read returns the seven-bit count, while the top bit of the byte is not part of that count. Open-bus or I/O-buffer behavior can affect reserved bits depending on the machine model and emulator core. MAME returns its I/O buffer in the unused bit when reading the timer register, illustrating why the CPU bus value may not be equivalent to a zero-extended counter. If an emulator makes that bit observable, its provenance should be stated as implementation-derived or hardware-tested rather than silently assumed from the timer block diagram.

Debugging should expose both values and phase: reload byte, current counter, prescaler remainder, timer enable state, request flag, timer mask, global interrupt flag, and returned bus byte. This lets a developer distinguish a counter rounding error from a correctly timed borrow whose interrupt has not been enabled or acknowledged.

A reference period calculator

The Python helper below calculates timer periods from an oscillator frequency using the reload-plus-one interval convention implemented by MAME. It is a test utility for timebase conversions, not a substitute for a cycle-by-cycle prescaler and counter model.

def huc6280_timer_period_seconds(reload_value, osc1_hz=21_480_000):
    if not 0 <= reload_value <= 0x7F:
        raise ValueError("HuC6280 timer reload is seven bits")
    if osc1_hz <= 0:
        raise ValueError("oscillator frequency must be positive")
    return (reload_value + 1) * 3 * 1024 / osc1_hz


shortest = huc6280_timer_period_seconds(0)
longest = huc6280_timer_period_seconds(0x7F)
assert round(shortest * 1_000_000) == 143
assert round(longest * 1_000) == 18

The rounded assertions correspond to the manual’s approximate range for a 21.48 MHz source. A more exact test should preserve the rational expression and apply the actual console’s master-clock ratio, avoiding floating-point drift over long emulated runs.

Verification matrix

Test reload values 0, 1, 126, and 127. For each, confirm the first start transition loads the expected counter state, counts at the divided clock, borrows on the expected master-clock cycle, raises TIQ, and reloads for the next interval. Then write a different reload while the counter is active and verify that the current interval is not incorrectly truncated or restarted.

Exercise stop/start phase behavior. Stop just before a prescaler tick and at a timer borrow boundary; confirm the prescaler reset rule and next interval. Test an IRQ with the timer mask enabled and disabled, then with the CPU interrupt flag both clear and set. Verify that masked borrows remain represented in request state, that the interrupt reaches the correct vector when enabled, and that a write acknowledgement clears TIQ without altering IRQ1 or IRQ2.

For read consistency, poll at ordinary times and around the downcounter transition. Compare paired reads according to the manual’s guidance and record returned high-bit bus state. Test save-state restore with a partial prescaler phase, a running counter one tick from underflow, an asserted but masked TIQ, and a CPU handler that has not acknowledged the flag. Each restored run should deliver the same next event and read sequence.

At system level, compare against a PC Engine/TurboGrafx-16 test ROM or trusted emulator trace. Keep timer events on the CPU’s master-clock schedule so they remain synchronized with VDC interrupts, audio generation, and frame pacing. Identify whether evidence came from the HuC6280 manual, MAME/Mednafen source, or a physical console capture; those have different evidentiary weight.

Acceptance criteria

A trustworthy HuC6280 timer model has a seven-bit reload and live counter, a clock derived from the chip’s prescaler, start-edge reload semantics, repeated borrow/reload behavior, separate request/mask/CPU-delivery state, and explicit acknowledgement. It also respects the documented counter-read caveat and records the bus value of reserved bits accurately. Those details turn timer-driven music tempo and gameplay timing from a guessed millisecond loop into an observable processor peripheral.

Related:

Sources:

Comments