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:
- 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 fromcpu_clock_hzso direct-ELF guests never rebuild (see Bootloader prescaler init). - Programs a sub-timer's reload register (in µs) and writes its control
register with
EN|RS|IE(enable, restart-on-underflow, interrupt-enable). - 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.
- 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:
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¶
- IRQMP — where the underflow IRQ pulse lands.
- Clock frequency configuration.
- Multicore & timing.