Skip to content

GPTIMER — General-purpose timer unit

GRLIB GPTIMER (GR712RC §11 / GRLIB IP core): a single prescaler driving up to four sub-timers, each with an independent control register and an optional underflow interrupt. In Tero this is the RTEMS system tick source and the backing for the rtems_timecounter_simple time-of-day counter.

Property Value
Class peripherals::GPTimer
Default MMIO base 0x80000300 (GR712RC) / 0xFF908000 (GR740)
MMIO window size 0x100
Sub-timers 4 (NumTimers)
Recipe IRQ 8 (GR712RC) / 1 (GR740)
Source src/peripherals/src/gptimer.cpp

Using it

Driver-visible model

The RTEMS LEON3 BSP uses the GPTIMER as a 1 MHz down-counter. The boot path:

  1. Programs the prescaler reload so the prescaler underflows at 1 MHz (one prescaler underflow = one 1 µs sub-timer tick). Tero's initialize() pre-programs this from cpu_clock_hz so direct-ELF guests never rebuild (see Bootloader prescaler init).
  2. Programs a sub-timer's reload register (in µs) and writes its control register with EN|RS|IE (enable, restart-on-underflow, interrupt-enable).
  3. On each underflow the timer reloads (RS=1), latches the IP status bit, and pulses the IRQ line to the IRQMP if IE is set.
  4. The interrupt handler clears IP by writing 1 to control bit 4 (write-1-to-clear).

Register map

All registers are 32-bit, word-aligned, word-only (sub-word access returns ErrorCode::AlignmentError).

Offset Name Access Reset Description
0x00 SCALER value R/W 0xFFFF Prescaler counter, decremented every system-clock tick.
0x04 SCALER reload R/W 0xFFFF Prescaler reload value (16-bit; period = reload+1).
0x08 CONFIG R + DF bit R/W SI=1 (separate interrupts), IRQ=8, TIMERS=4; bit 9 (DF) is the only writable bit.
0x10 + 0x10*n TIMER n+1 counter R/W timer-dependent Current value, decrements on prescaler underflow.
0x14 + 0x10*n TIMER n+1 reload R/W timer-dependent Reload value, copied into counter when RS=1.
0x18 + 0x10*n TIMER n+1 control R/W timer-dependent See the control bit map below.

The timer block starts at 0x10 (TimerBlockBase) with a 0x10 stride (TimerStride); within each block the counter/reload/control are at +0x00, +0x04, +0x08. The classic GPTIMER also defines a per-timer latch at +0x0C; Tero does not model a separate latch register (the RTEMS LEON3 BSP does not use it), so that sub-offset reads back 0.

CONFIG register

CONFIG (0x08) reports the fixed implementation parameters (gptimer.cpp:38): SI = 1 (bit 8 — separate interrupt per timer), IRQ = 8 (bits 5:3 — base IRQ line), TIMERS = 4 (bits 2:0). Bit 9 (DF, "disable timer freeze") is the only writable field and is stored verbatim.

Control register bit map

Bit Name Access Meaning
0 EN R/W Enable: 1 = counting.
1 RS R/W Restart: 1 = reload counter on underflow, 0 = stop (clears EN).
2 LD W (trigger) Load: writing 1 copies RELOAD → COUNTER; reads as 0, value not stored.
3 IE R/W Interrupt-enable on underflow.
4 IP write-1-to-clear Interrupt-pending status (sticky). See below.
5 CH R/W Chain: decrement only when the previous timer underflows.
6 DH R (0) Debug-halt: read-only, always 0 in this model.

The writable mask is CtrlWritableMask = EN|RS|IE|CH = 0x2B (gptimer.hpp:61). LD is a write-only trigger (not stored); IP is computed separately; DH is read-only.

IP is WRITE-1-TO-CLEAR

The IP status bit (control bit 4) is write-1-to-clear, matching GRLIB hardware and the SIS reference simulator (grlib.c gpt_ctrl_write: if (val & 0x10) ctrl &= ~0x10):

  • Writing a 1 to bit 4 clears IP.
  • Writing a 0 to bit 4 leaves IP set.
  • Software never sets IP — only a timer underflow latches it.

In code (gptimer.cpp:114):

const auto new_ip = (value & (1U << CtrlIpBit))
                      ? 0U                              // write-1 clears
                      : (ctrl & (1U << CtrlIpBit));     // write-0 preserves

This was previously inverted, which left IP stuck; the rtems_timecounter_simple pending check then over-compensated and uptime ran backwards (sptest spnsext01). Reading the control register masks LD off (ctrl & ~(1 << CtrlLdBit), gptimer.cpp:55).

Reset state

reset() (gptimer.cpp:12):

Register Reset value Notes
SCALER value / reload 0xFFFF / 0xFFFF ScalerMask width is 16 bits.
CONFIG (DF) 0
Timer 1–3 control / counter / reload 0x00 / 0x00 / 0x00 Disabled.
Timer 4 control / counter / reload 0x09 / 0xFFFF / 0xFFFF 0x09 = EN \| IE (bits 0 and 3): enabled with interrupt-enable, RS = 0 (one-shot, stops after the first underflow).

Timer 4 at reset

The reset control word for timer 4 is exactly 0x09 (TimerResetCtrl[3], gptimer.hpp:68): EN=1, IE=1, RS=0. Because RS is clear it is not a self-restarting watchdog in this model — it counts its 0xFFFF reload down once, latches IP, pulses the IRQ (IE=1), and then clears EN and stops. The boot prescaler init writes the SCALER registers (not timer 4), so the RTEMS BSP reprograms its own tick timer regardless.


Internals / how it's modelled

Prescaler + sub-timer tick

tick(now) (gptimer.cpp:204) converts elapsed simulated nanoseconds into system-clock cycles and advances the prescaler:

delta_ns = now - last_tick_time
cycles   = delta_ns / ns_per_cycle          # ns_per_cycle injected at attach()
advance_by_cycles(cycles)

advance_by_cycles (gptimer.cpp:164) decrements the prescaler by cycles, firing process_timer_tick() once per prescaler underflow (it handles partial periods and multiple full periods in one call, so the result is simulated-time accurate at the cycle level regardless of how coarse the tick cadence is). The prescaler period is scaler_reload + 1 (or 0x10000 when the reload is 0).

process_timer_tick() (gptimer.cpp:135) is the per-underflow step for each enabled timer:

for each timer with EN == 1:
    counter -= 1
    if counter == 0:
        IP = 1                       # latch sticky status
        if RS == 1: counter = reload # restart
        else:       EN = 0           # one-shot: stop
        if IE == 1: ctx.irq->raise() # pulse the IRQ line (every underflow)

IP latch vs the IRQ pulse

The IP bit is a sticky software-visible status bit — it stays set until software writes 1 to clear it. The IRQ line to the IRQMP is edge-like: it is pulsed (ctx.irq->raise()) on every underflow with IE == 1, regardless of whether IP was already set. This is required because the IRQMP auto-clears its pending bit on trap acknowledge; without a fresh pulse per underflow the RTEMS clock would stop advancing.

There is a second IRQ-raise site in mmio_write (gptimer.cpp:122): if a control write transitions IP from clear to set and IE is set, the line is raised. In practice software never sets IP (it is write-1-clear), so this branch is dormant; it exists to mirror the hardware "IP becomes set with IE enabled" edge.

ns_per_cycle and clock frequency

The prescaler is clocked by the system clock, so its tick period is the SoC's per-cycle time. The runtime injects this via PeripheralContext::ns_per_cycle (derived from EmulatorConfig::cpu_clock_hz) at attach() (gptimer.hpp:83); the GPTIMER no longer hardcodes 20 ns. A 0 (unset) value falls back to the historical 20 ns ≈ 50 MHz default. Changing cpu_clock_hz therefore scales the prescaler correctly. See the clock-frequency configuration.

Bootloader prescaler init

During Emulator::initialize() (emulator.cpp:394) the runtime simulates the GR712RC/GR740 ROM bootloader's prescaler setup: it writes a scaler reload (and the current scaler value) so the prescaler underflows at exactly 1 MHz — the rate the RTEMS LEON3 BSP assumes when it programs only the per-timer reload in µs. The divisor is derived straight from the clock in MHz:

scaler_reload = (cpu_clock_hz + 500'000) / 1'000'000 - 1;  // round-to-nearest MHz

Without this, the prescaler would start at 0xFFFF and take ~65 ms before the first underflow, delaying the first RTEMS clock interrupt enough to break test timing. Computing the divisor from MHz (rather than from ns_per_cycle) keeps the 1 MHz tick exact at any cpu_clock_hz (e.g. 80 MHz → exact divisor 80, not the 76 you'd get from a rounded 13 ns period), so direct-ELF guests never need rebuilding when the clock changes.

RTEMS timecounter usage

RTEMS uses the GPTIMER both as the periodic clock-tick source and (via rtems_timecounter_simple) as the time-of-day counter. The simple-timecounter reads the counter and the IP pending bit to derive a monotonic uptime; the write-1-clear IP semantics above are exactly what make that arithmetic monotonic. The GR740's IRQAMP exposes no usable high-resolution timestamp counter (its TIMESTAMP block reads zero), so the GPTIMER remains the timecounter on GR740 as well — consistent with the round-granular system clock.

Reading current state

control(idx) and counter(idx) (gptimer.cpp:233) expose a timer's current control/counter to host code (tests, the publisher) without an MMIO round-trip. publish() registers scaler_value, scaler_reload, and per-timer counter/reload/control fields.

Tests

  • tests/unit/test_gptimer.cpp — writable mask, LD trigger, IP write-1-clear, prescaler underflow, RS=0 stopping / RS=1 reloading, IE=1 IRQ raise, CH chaining between adjacent timers, and the timer-4 reset state.
  • The RTEMS sptests exercise the GPTIMER end-to-end as the system clock source.

See also