33.10.4. ARM timer
The ARM timer is the simplest way to generate hardware interrupts periodically, and therefore serves as the simples example of ARM GIC usage.
Working on QEMU: baremetal/arch/aarch64/timer.c
./run --arch aarch64 --baremetal baremetal/arch/aarch64/timer.c
Output at lkmc d8dae268c0a3e4e361002aca3b382fedd77f2567 + 1:
cntv_ctl_el0 0x0 cntfrq_el0 0x3B9ACA0 cntv_cval_el0 0x0 cntvct_el0 0x105113 cntvct_el0 0x1080BC cntvct_el0 0x10A118 IRQ number 0x1B cntvct_el0 0x14D25B cntv_cval_el0 0x3CE9CD6 IRQ number 0x1B cntvct_el0 0x3CF516F cntv_cval_el0 0x7893217 IRQ number 0x1B cntvct_el0 0x789B733 cntv_cval_el0 0xB439642
and new IRQ number
section appears every second, when a clock interrupt is raised!
TODO make work on gem5. Fails with gem5 simulate() limit reached at the first WFI done in main, which means that the interrupt is never raised.
Once an interrupt is raised, the interrupt itself sets up a new interrupt to happen in one second in the future after cntv_cval_el0
is reached by the counter.
The timer is part of the aarch64 specification itself and is documented at: ARMv8 architecture reference manual db Chapter D10 "The Generic Timer in AArch64 state". The key registers to keep in mind are:
-
CNTVCT_EL0
: "Counter-timer Virtual Count register". The increasing current counter value. -
CNTFRQ_EL0
: "Counter-timer Frequency register". "Indicates the system counter clock frequency, in Hz." -
CNTV_CTL_EL0
: "Counter-timer Virtual Timer Control register". This control register is very simple and only has three fields:-
CNTV_CTL_EL0.ISTATUS
bit: set to 1 when the timer condition is met -
CNTV_CTL_EL0.IMASK
bit: if 1, the interrupt does not happen whenISTATUS
becomes one -
CNTV_CTL_EL0.ENABLE
bit: if 0, the counter is turned off, interrupts don’t happen
-
-
CNTV_CVAL_EL0
: "Counter-timer Virtual Timer CompareValue register". The interrupt happens whenCNTVCT_EL0
reaches the value in this register.
Due to QEMU’s non-determinism, each consecutive run has slightly different output values.
From the terminal output, we can see that the initial clock frequency is 0x3B9ACA0 == 62500000 Hz == 62.5MHz. Grepping QEMU source for that string leads us to:
/* Scale factor for generic timers, ie number of ns per tick. * This gives a 62.5MHz timer. */ #define GTIMER_SCALE 16
which in turn is used to set the initial reset value of the clock:
{ .name = "CNTFRQ_EL0", .state = ARM_CP_STATE_AA64, .opc0 = 3, .opc1 = 3, .crn = 14, .crm = 0, .opc2 = 0, .access = PL1_RW | PL0_R, .accessfn = gt_cntfrq_access, .fieldoffset = offsetof(CPUARMState, cp15.c14_cntfrq), .resetvalue = (1000 * 1000 * 1000) / GTIMER_SCALE,
where (1000 * 1000 * 1000) / 16 == 62500000
.
Trying to set the frequency on QEMU by writing to the CNTFRQ register does change the value of future reads, but has no effect on the actual clock frequency as commented on the QEMU source code https://github.com/qemu/qemu/blob/v4.0.0/target/arm/helper.c#L2647
static const ARMCPRegInfo generic_timer_cp_reginfo[] = { /* Note that CNTFRQ is purely reads-as-written for the benefit * of software; writing it doesn't actually change the timer frequency. * Our reset value matches the fixed frequency we implement the timer at. */ { .name = "CNTFRQ", .cp = 15, .crn = 14, .crm = 0, .opc1 = 0, .opc2 = 0, .type = ARM_CP_ALIAS, .access = PL1_RW | PL0_R, .accessfn = gt_cntfrq_access, .fieldoffset = offsetoflow32(CPUARMState, cp15.c14_cntfrq), },
At each interrupt, we increase the compare value CVAL
by about 1x the clock frequency 0x3B9ACA0 so that it will fire again in one second, e.g. 0x3CE9CD6 - 0x14D25B == 3B9CA7B
. The increment is not perfect because the counter keeps ticking even while our register read and print instructions are running inside the interrupt handler!
We then observe that the next interrupt happens soon after CNTV_CVAL_EL0 is reached by CNTVCT_EL0:
cntv_cval_el0 0x3CE9CD6 IRQ number 0x1B cntvct_el0 0x3CF516F
Bibliography: