Tick ownership & preemption-integration policy¶
Resolves arch-interface.md findings F5 (arch-shaped board
glue) and F6 (timekeeping mis-filed under the switch contract). Complements
timekeeping.md, which audits every timer source in the tree; this
document is scoped to the one timer that drives the scheduler — who owns it, who calls
into the kernel, and in what shape.
Method. Read the tick core in kernel/tk_kernel.c (tk_kernel_tick,
tk_kernel_tick_preempt, tk_kernel_preempt_select, tk_tick, tk_tick_advance), the
Tier B port contract in kernel/tk_kernel_port.h, the Tier A time contract in
kernel/tk_time.h, and every arch’s tick wiring in arch/<cpu>/kernel_arch.c plus the
board glue in boards/<board>/tk_tick_*.c. (These “Tier A/B” letters are the
kernel-module layering from tk_kernel_port.h/tk_time.h, unrelated to the arch
capability “Tier 0-3” numbering in arch-interface.md — both schemes are named “Tier”
by convention, in different documents, for different axes.)
The three actors (this is the whole model)¶
A tick involves three roles the tree keeps implicitly separate:
The tick OWNER — owns the ~1 ms hardware source and its ISR. This is the board (SysTick / RTC1 / CLINT / CPU-timer) or a vendor SDK sharing its own timer (CubeHAL’s SysTick, an SDK’s timer). The kernel is never the owner.
The kernel — provides a tick entry the owner’s ISR calls, and consumes a monotonic ms base. It programs no timer and reads no timer register.
The time base — a small monotonic-ms service the owner advances and the kernel (and the app) read. At root this is per-instance
k->ticks_msinkernel/tk_kernel.c(advanced by each instance’sticker_core; review C4 — was one globalg_ticks_ms), exposed astk_kernel_now_ms()and, one layer up,tk_time_ms()(Tier A) — seetimekeeping.md.
Policy resolutions¶
P1 — The kernel never owns the tick; it consumes it¶
Board-owned: the board programs the timer and its ISR calls the kernel entry (§ below).
arch_timer_init()is declared inkernel_arch.hbut the header comments admit it: “provided by the BOARD” on the ARM ports (RTC1 on nRF52 because SysTick can’t survive its sleep mode; plain SysTick elsewhere) — a naming smell still live in the tree; see P5.SDK-owned: the SDK already runs the timer + ISR; the integrator adds one call (
tk_tick()) into the SDK’s existing ISR — e.g. CubeHALSysTick_Handler→HAL_IncTick()+tk_tick(). The kernel programs nothing.
→ The kernel’s tick pre-requirement is “a ~1 ms interrupt that calls the kernel entry” — satisfiable by owning OR sharing, identically.
P2 — Two tick entries, split by ENTRY SHAPE, not by hardware mechanism (F3)¶
This is easy to mis-read as “PendSV vs no PendSV.” It isn’t — c2000 has no PendSV and
still uses the “deferred” entry (below). The real split is whether the port has an
ordinary callable arch_context_switch(from,to) or not:
Deferred-entry ports (
arm_v6m,arm_v7m,arm_v8m, andc2000): the owner’s ISR callstk_kernel_tick_preempt()(void in, void out). The kernel computes from/to and calls the port’sarch_context_switch(from,to). On the ARM family this swap is deferred into PendSV (lowest priority) — the classic reading. Onc2000there is no deferral hardware at all:arch/c2000/kernel_arch.ccallstk_tick()→tk_kernel_tick_preempt()→arch_context_switch()synchronously, inside the same ISR — it lands in this bucket purely because C28x’s switch is a plain callable function on twoarch_context*, not because the switch is deferred in time.Trap-frame-entry ports (
risc_v, confirmed inarch/risc_v/kernel_arch.c; andtricoreper its header, body not yet grafted — seearch-interface.md§7): the tick trap has already saved the FULL frame on the thread’s own stack at a raw SP. It callstk_kernel_preempt_select(sp), gets back the SP to resume, and the trap’s own return path restores it. There is noarch_context_switchcall anywhere in this path — the trap-frame save/restore is the switch.
Both funnel into the shared tk_kernel_tick() core in kernel/tk_kernel.c (wake due
sleepers, refresh quantum, pick next). This split is intrinsic to whether the port’s
switch is a callable function or a trap convention, and stays.
P4 — Timekeeping is a consumed service, not part of the switch contract (F6)¶
Three timer roles stay distinct: (1) kernel scheduler tick + monotonic ms base, (2)
tk_gpt-style application countdown timers (separate HW, board-provided), (3) busy-wait delay. Seetimekeeping.mdfor the full audit.The monotonic ms base is a consumed service, not something the switch layer owns for its own sake:
kernel/tk_time.h(Tier A) is shipped —tk_time_init()/tk_time_ms()/tk_time_us()/tk_kernel_now_ms()all exist and are what the app and the kernel’s own sleep/timeout/EDF math read. This is implemented, not proposed — seetimekeeping.mdfor the flip from “proposal” to “shipped.”What did NOT happen:
arch_timer_now_ms()/arch_timer_init()were meant to leavekernel_arch.hfor their own small time-contract header (the file-split half of F6). At root they are still declared insidekernel_arch.h’s Tier 1 section on every port. The layering F6 was chasing shipped (the app never touchesarch_timer_now_ms— it readstk_time_ms()/tk_kernel_now_ms()); the file split did not. Tracked as open inarch-interface.mdF6.
P5 — The ARM board glue was NOT de-arched (F5, still open)¶
arch_kernel_tick(irq) and arch_sched_init() are ARM-family board glue, not the
portable contract: arch_kernel_tick is the shared tick-ISR body reused by the ARM
tick boards, and arch_sched_init sets PendSV priority. Non-ARM ports do the
equivalent inside their trap and never declare these names. A rename to a clearly
board-glue name (armv7m_tick_isr / armv7m_sched_init) was proposed here but was
not adopted — arch/arm_v6m|arm_v7m|arm_v8m/kernel_arch.h still declare
arch_sched_init(void) / arch_kernel_tick(uint8_t irq) verbatim today, and
boards/nrf52832_pca10040/tk_tick_nrf52.c still calls them by those names. What DID
happen: arch_kernel_tick() on every ARM port is now a one-line shim that delegates
to tk_tick() (arch/arm_v7m/kernel_arch.c: void arch_kernel_tick(uint8_t irq) { (void)irq; tk_tick(); }), kept only so existing board files link unchanged — new
integrations are told to call tk_tick() directly and never see the ARM-only names.
Status: open, not blocking — a new non-ARM port never has to know these two names
exist.
The board tick contract (what an integrator writes)¶
Deferred-entry, board owns the timer (ARM family, direct):
void SysTick_Handler(void) { /* board-owned ISR */
tk_tick(); /* kernel front door: ms base++ AND tk_kernel_tick_preempt() (pre-launch-safe, P3) */
}
Deferred-entry, SDK shares its own timer (CubeHAL SysTick):
void SysTick_Handler(void) { /* SDK-owned ISR; integrator adds one line */
HAL_IncTick(); /* SDK keeps its own timebase */
tk_tick(); /* + kernel entry — unconditional, no gate (P3) */
}
Deferred-entry, immediate hardware switch, no PendSV (c2000):
interrupt void cpu_timer0_isr(void) {
/* C28x auto-saves the caller-saved set on IRQ entry (hardware context save) */
tk_tick(); /* -> tk_kernel_tick_preempt() -> arch_context_switch(), INLINE, same ISR */
/* board re-arms IER — the C28x hardware clears it on IRQ entry */
}
Trap-frame-entry (RISC-V): the port’s timer trap (mtvec) advances the base via
tk_tick_advance() (boot hart only, so the one global ms clock advances once per ms,
not once per hart — see tk_kernel_port.h) and switches via
next_sp = tk_kernel_preempt_select(current_sp) — the ISR shape lives entirely in
arch/risc_v/kernel_arch.c, not board C; there is no arch_context_switch call to
write.
What this settles¶
The kernel never owns the tick (own-vs-shared is just who owns it, P1); two entries stay, split by whether the switch is a callable function or a trap convention — not by whether PendSV exists (P2, corrects the historical “deferred = PendSV” shorthand using
c2000as the counter-example); the entry is pre-launch-safe on three checks so a shared-timer SDK needs no board gate (P3); timekeeping is a consumed, shipped service (P4,kernel/tk_time.h); the ARM tick/sched glue is still un-renamed — P5 is open, not a blocker, and does not leak into new ports.→ SMP: the tick core already indexes per-core state (
current_thread[cpu],g_quantum_remaining[cpu],arch_core_id()), andtk_kernel_preempt_selectreuses the SMP re-entry guard (g_sched_entering[cpu]) for the single-core pre-launch case too. Seesmp-model.mdfor the full per-core model and the cooperative-switch counterpart of this split.→ time µs / sub-tick (
tk_time_us) is board-by-board, audited intimekeeping.md§7.3 — out of scope for the tick contract itself.