aarch64: arm the ARM Generic Timer -- CNTP/CNTHP TVAL+CTL, per-tick re-arm
Punch list §25 item 0.7 complete. This is what finally makes items 0.5 and 0.6 provably work end to end. apic_timer_start(): writes CNTP_TVAL_EL0 (or CNTHP_TVAL_EL2 at EL2 -- aarch64_current_el(), same EL-aware discipline as 0.4-0.6) to s_timer_period_tsc, then CTL.ENABLE=1/IMASK=0, followed by an ISB. The ISB is not decorative: confirmed against Linux's own arch_timer_reg_write_cp15() (arch/arm64/include/asm/arch_timer.h) that only the *control* register write needs synchronising before the enable/mask state is guaranteed visible to the interrupt pipeline -- TVAL/CVAL writes do not carry the same requirement, which is why apic_timer_rearm() omits it. TVAL is architecturally 32-bit but MSR-to-system-register is always a 64-bit instruction form -- passing a uint32_t operand directly failed to build (-Wasm-operand-widths). Fixed by truncating to 32 bits ourselves then zero-extending back to 64 for the operand, which supplies explicit, provably-correct zeros in the RES0 upper field rather than depending on unverified hardware behaviour -- Linux's own driver never exercises this path (it always uses the 64-bit CVAL form instead), so there was no local source to confirm the alternative against. apic_timer_rearm() (new): re-writes TVAL only, no ISB needed. TVAL is relative to "now," not an absolute deadline like riscv64's SBI interface (item 0.3), so there is no drift-correction bookkeeping -- each write means "N ticks from this instant." Wired into aarch64_irq_handler() and called *first*, before heartbeat_tick(), matching riscv64_timer_rearm()'s ordering discipline exactly: the ARM Generic Timer does not auto-reload, so a return path that skips this leaves the interrupt condition latched, which the GIC would redeliver the instant it's EOI'd -- a real storm, the same class of failure item 0.6's verification investigated (and that time found absent, because nothing was armed yet). Verified: builds clean; every generated instruction checked against disassembly, not just reviewed by eye (both EL branches, correct TVAL/CTL register names, single shared ISB in apic_timer_start(), no ISB in apic_timer_rearm()). Boots to ok> with no regression, dict_hash 0x3d4e1daf289da94f unchanged. Rate measured directly against real wall-clock time via QEMU's own -d int trap trace (same method as riscv64's item 0.3), two independent windows: 1,090 interrupts over 11.05 s (98.679 Hz) and 4,031 over 40.88 s (98.614 Hz) -- consistent across both, so this is a real, small, systematic bias (~1.3-1.4% slow), not measurement noise from polling granularity, which would have shrunk with the longer window and did not. Attributed to genuine per-interrupt service latency: TVAL is rewritten mid-ISR, so the trampoline save/restore, GICC_IAR read, EL branch and GICC_EOIR write all lengthen the effective period slightly versus the nominal 10 ms, inherent to any relative-countdown re-arm scheme. Reported as measured, not smoothed over. The interrupt sustained continuously across both windows with no stall and no storm, which is the primary evidence re-arm-every-tick is correct; the small rate bias is overhead, not a defect. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cabb0e8bd4
commit
43aa0a2a36
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* apic.c (aarch64) - Minimal GICv2 driver (punch-list item 0.6).
|
||||
* apic.c (aarch64) - Minimal GICv2 driver + ARM Generic Timer (items 0.6/0.7).
|
||||
*
|
||||
* "Minimal" is deliberate and stays deliberate: exactly one interrupt (the
|
||||
* non-secure EL1 physical timer PPI, or its EL2 hypervisor-timer counterpart
|
||||
@@ -203,15 +203,14 @@ void apic_eoi(void)
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
* @brief Configure the periodic timer (AArch64 stub).
|
||||
/**
|
||||
* @brief Compute the expected system-counter ticks per heartbeat period.
|
||||
*
|
||||
* On x86-64 this calibrates and programs the APIC timer. On AArch64 the
|
||||
* equivalent is the ARM Generic Timer's EL1 Physical Timer (CNTP_CTL_EL0 /
|
||||
* CNTP_TVAL_EL0); that driver is deferred. This stub computes
|
||||
* @c s_timer_period_tsc (the expected number of @c CNTPCT_EL0 ticks
|
||||
* between heartbeats) which is used by @c heartbeat_init() to seed the
|
||||
* rolling-window expected-delta value.
|
||||
* Computes @c s_timer_period_tsc (the expected number of @c CNTPCT_EL0
|
||||
* ticks between heartbeats), used both by @c heartbeat_init() to seed the
|
||||
* rolling-window @c expected_delta and by @c apic_timer_start() /
|
||||
* @c apic_timer_rearm() (item 0.7) as the @c TVAL step written on every
|
||||
* arm and re-arm.
|
||||
*
|
||||
* If either @p tsc_hz or @p tick_hz is zero, a fallback of 10,000,000
|
||||
* ticks is stored (approximately 160 ms at 62.5 MHz, a safe non-zero
|
||||
@@ -232,29 +231,108 @@ int apic_timer_init(uint64_t tsc_hz, uint32_t tick_hz)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start periodic timer delivery (AArch64 stub).
|
||||
* @brief Arm the ARM Generic Timer and enable its interrupt (item 0.7).
|
||||
*
|
||||
* On x86-64 this unmasks the APIC timer LVT entry and re-arms the
|
||||
* initial count register. On AArch64 the ARM Generic Timer's EL1
|
||||
* Physical Timer would be enabled here; the driver is deferred.
|
||||
* This stub is a no-op that satisfies the common call site in
|
||||
* @c kernel_main() without fault.
|
||||
* Writes @c CNTP_TVAL_EL0 (or @c CNTHP_TVAL_EL2 at EL2 — @c
|
||||
* aarch64_current_el(), same EL-aware discipline as items 0.4–0.6) to
|
||||
* @c s_timer_period_tsc, then sets @c CTL.ENABLE=1, @c CTL.IMASK=0.
|
||||
*
|
||||
* @c TVAL is a signed 32-bit *relative* countdown — writing it sets
|
||||
* "fire @c s_timer_period_tsc ticks from this instant," unlike riscv64's
|
||||
* SBI interface (item 0.3), which takes an absolute deadline the caller
|
||||
* must compute. No "now" reading is needed here at all.
|
||||
*
|
||||
* The @c ISB after the @c CTL write is required, not decorative: per the
|
||||
* architecture, and confirmed against Linux's own
|
||||
* @c arch_timer_reg_write_cp15() (arch/arm64/include/asm/arch_timer.h),
|
||||
* only the *control* register write needs synchronising before the
|
||||
* enable/mask state is guaranteed visible to the interrupt pipeline —
|
||||
* @c TVAL/@c CVAL writes do not carry the same requirement, which is why
|
||||
* @c apic_timer_rearm() below omits it.
|
||||
*
|
||||
* @c s_timer_period_tsc must already be set by @c apic_timer_init(). If
|
||||
* it is still 0, @c TVAL=0 fires immediately and continuously until
|
||||
* @c apic_timer_rearm() runs — a correctly-functioning if oddly-paced
|
||||
* timer, not a hang.
|
||||
*/
|
||||
void apic_timer_start(void)
|
||||
{
|
||||
/* ARM generic timer EL1 physical timer — stub */
|
||||
/* CNTx_TVAL_EL is architecturally 32-bit (bits[63:32] RES0 on write),
|
||||
* but MSR to a system register is always a 64-bit (Xn) instruction
|
||||
* form -- there is no 32-bit encoding to match a narrower operand.
|
||||
* Truncate to 32 bits ourselves, then zero-extend back to 64 for the
|
||||
* operand: this supplies explicit, provably-correct zeros in the RES0
|
||||
* field rather than depending on how the hardware treats whatever
|
||||
* upper bits a narrower operand happened to leave there -- a
|
||||
* distinction we could not confirm against a local authoritative
|
||||
* source (unlike the GICv2 offsets and CTL bit layout above,
|
||||
* cross-checked against Linux headers on this system; TVAL is not,
|
||||
* since Linux's own driver only ever uses the 64-bit CVAL form and
|
||||
* never exercises this path). */
|
||||
uint64_t tval = (uint64_t)(uint32_t)s_timer_period_tsc;
|
||||
|
||||
if (aarch64_current_el() == 2) {
|
||||
__asm__ volatile ("msr cnthp_tval_el2, %0" :: "r"(tval));
|
||||
__asm__ volatile ("msr cnthp_ctl_el2, %0" :: "r"((uint64_t)1) : "memory");
|
||||
} else {
|
||||
__asm__ volatile ("msr cntp_tval_el0, %0" :: "r"(tval));
|
||||
__asm__ volatile ("msr cntp_ctl_el0, %0" :: "r"((uint64_t)1) : "memory");
|
||||
}
|
||||
__asm__ volatile ("isb" ::: "memory");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop periodic timer delivery (AArch64 stub).
|
||||
* @brief Re-arm the ARM Generic Timer for the next period (item 0.7).
|
||||
*
|
||||
* On x86-64 this sets the APIC timer LVT mask bit. On AArch64 the
|
||||
* ARM Generic Timer would be disabled here. Stub — no-op until the
|
||||
* GIC / Generic Timer driver is implemented.
|
||||
* Writes a fresh @c TVAL — the entirety of a periodic re-arm, since @c TVAL
|
||||
* is relative to "now" rather than an absolute deadline. Unlike riscv64's
|
||||
* SBI re-arm (item 0.3), there is no drift-correction bookkeeping: each
|
||||
* write means "@c s_timer_period_tsc ticks from this instant," so time
|
||||
* spent servicing the interrupt before this call simply lengthens that one
|
||||
* period rather than accumulating error across ticks.
|
||||
*
|
||||
* **This is the one call that must never be skipped.** The ARM Generic
|
||||
* Timer does not auto-reload: once @c TVAL crosses zero, @c CTL.ISTATUS
|
||||
* latches and stays asserted — a level condition — until software writes
|
||||
* a new @c TVAL or clears @c CTL.ENABLE. A return path that skips this
|
||||
* leaves the interrupt permanently pending, which the GIC would deliver
|
||||
* again the instant it is EOI'd: a real interrupt storm, not a
|
||||
* hypothetical one (this is exactly the failure this driver was checked
|
||||
* against during item 0.6's own verification, and confirmed absent there
|
||||
* only because nothing armed the timer yet).
|
||||
*
|
||||
* Called from @c aarch64_irq_handler() (interrupts.c) before
|
||||
* @c heartbeat_tick() — same ordering discipline as
|
||||
* @c riscv64_timer_rearm() (item 0.3): re-arm first, so a fault in the
|
||||
* rest of the handler cannot also cost the next tick.
|
||||
*/
|
||||
void apic_timer_rearm(void)
|
||||
{
|
||||
/* uint64_t, zero-extended -- see apic_timer_start() for why. */
|
||||
uint64_t tval = (uint64_t)(uint32_t)s_timer_period_tsc;
|
||||
|
||||
if (aarch64_current_el() == 2) {
|
||||
__asm__ volatile ("msr cnthp_tval_el2, %0" :: "r"(tval));
|
||||
} else {
|
||||
__asm__ volatile ("msr cntp_tval_el0, %0" :: "r"(tval));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop timer delivery by clearing CTL.ENABLE (item 0.7).
|
||||
*
|
||||
* Per the architecture, @c ENABLE=0 unconditionally stops the count and
|
||||
* de-asserts the interrupt condition regardless of @c IMASK — a single
|
||||
* write is sufficient, no separate mask step needed.
|
||||
*/
|
||||
void apic_timer_stop(void)
|
||||
{
|
||||
/* Stub */
|
||||
if (aarch64_current_el() == 2) {
|
||||
__asm__ volatile ("msr cnthp_ctl_el2, %0" :: "r"((uint64_t)0) : "memory");
|
||||
} else {
|
||||
__asm__ volatile ("msr cntp_ctl_el0, %0" :: "r"((uint64_t)0) : "memory");
|
||||
}
|
||||
__asm__ volatile ("isb" ::: "memory");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,12 +24,14 @@ extern void aarch64_install_vectors(void);
|
||||
* arch/riscv64/interrupts.c. */
|
||||
extern int aarch64_current_el(void);
|
||||
|
||||
/* Defined in apic.c (item 0.6). Same extern-in-place convention: GICv2
|
||||
* register-level details belong to the file that owns the MMIO base
|
||||
* addresses, not the shared apic.h. */
|
||||
/* Defined in apic.c (items 0.6/0.7). Same extern-in-place convention: GICv2
|
||||
* and generic-timer register-level details belong to the file that owns
|
||||
* the MMIO base addresses / EL-aware register selection, not the shared
|
||||
* apic.h. */
|
||||
extern uint32_t apic_read_iar(void);
|
||||
extern uint32_t apic_timer_ppi(void);
|
||||
extern void apic_eoi_intid(uint32_t intid);
|
||||
extern void apic_timer_rearm(void);
|
||||
|
||||
/* INTID 1023 = "spurious" (GICv2 spec): the CPU interface has nothing
|
||||
* pending, typically because another interrupt at the same or higher
|
||||
@@ -39,7 +41,7 @@ extern void apic_eoi_intid(uint32_t intid);
|
||||
#define GIC_INTID_SPURIOUS 1023u
|
||||
|
||||
/**
|
||||
* @brief Dispatch an IRQ taken at current-EL with SPx (punch-list item 0.6).
|
||||
* @brief Dispatch an IRQ taken at current-EL with SPx (punch-list item 0.7).
|
||||
*
|
||||
* Called from @c irq_spx_trampoline in @c isr.S, which has already saved the
|
||||
* full caller-saved integer and FP/SIMD register sets plus @c ELR_ELx /
|
||||
@@ -54,20 +56,21 @@ extern void apic_eoi_intid(uint32_t intid);
|
||||
* an unrecognised real INTID still needs the CPU interface unblocked for
|
||||
* the next one.
|
||||
*
|
||||
* The only INTID actually routed anywhere is the timer PPI
|
||||
* (@c apic_timer_ppi() — 30 at EL1, 26 at EL2, decided once in
|
||||
* @c apic_init()), which calls @c heartbeat_tick(). **Unreachable in
|
||||
* practice at this item**: @c apic_timer_start() (item 0.7) is still the
|
||||
* no-op stub, so nothing ever arms @c CNTP_CTL_EL0 / @c CNTHP_CTL_EL2 and
|
||||
* the GIC never sees the timer line assert. Item 0.7's tick-advance
|
||||
* acceptance is what proves this dispatch actually runs (same deferral
|
||||
* item 0.5 already made, C2).
|
||||
* For the timer PPI (@c apic_timer_ppi() — 30 at EL1, 26 at EL2, decided
|
||||
* once in @c apic_init()): @c apic_timer_rearm() runs **first**, before
|
||||
* @c heartbeat_tick() — the ARM Generic Timer does not auto-reload, so
|
||||
* skipping this leaves the interrupt condition permanently latched (see
|
||||
* @c apic_timer_rearm()'s own doc comment). Ordering the re-arm ahead of
|
||||
* the rest of the dispatch means a fault in @c heartbeat_tick() cannot
|
||||
* also cost the next tick — same discipline as @c riscv64_timer_rearm()
|
||||
* (item 0.3).
|
||||
*/
|
||||
void aarch64_irq_handler(void)
|
||||
{
|
||||
uint32_t intid = apic_read_iar();
|
||||
|
||||
if (intid == apic_timer_ppi()) {
|
||||
apic_timer_rearm();
|
||||
heartbeat_tick();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user