From 6ee1900c1a02a95f28e9c7f33745a3e0d0bb9fba Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Fri, 18 Sep 2026 20:21:25 +0200 Subject: [PATCH] Nadir 8254 PIT timer driver --- include/pit.h | 56 +++++++++++++++++++++++ kernel/arch/x86_64/pit.c | 95 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 include/pit.h create mode 100644 kernel/arch/x86_64/pit.c diff --git a/include/pit.h b/include/pit.h new file mode 100644 index 0000000..c9001be --- /dev/null +++ b/include/pit.h @@ -0,0 +1,56 @@ +/* Nadir 8254 PIT timer driver (see pit.c). + * + * Runs in 64-bit long mode at Ring 0 (CPL 0): programs the PIT via `outb` + * to ISA ports PIT_CMD (0x43) and PIT_CH0 (0x40), and handles IRQ0 + * (8259 PIC IRQ0 -> CPU vector 32 after pic_remap). + * + * Ports used: 0x43 (command/mode register, write-only), 0x40 (channel 0 + * data, write-only for the divisor latch). + * + * Preconditions: caller owns interrupt policy — pit_init() neither enables + * (sti) nor disables (cli) interrupts; target init order is + * pic_remap -> idt_install_irqs -> pit_init -> pic_unmask(0) -> sti. + * pit_sleep() requires interrupts enabled, otherwise `hlt` never wakes. + * + * Failure modes: divisor is fixed at compile time (PIT_DIVISOR); a zero + * divisor would mean "65536" to the hardware and the wrong rate, so it is + * rejected with a compile-time _Static_assert. Frequency is fixed at 100Hz; + * any other rate needs a deliberate driver change, not a runtime argument. + */ + +#ifndef NADIR_PIT_H +#define NADIR_PIT_H + +#include + +/* PIT input clock: crystal-derived base rate shared by all IBM-PC PITs. */ +#define PIT_BASE_HZ 1193182U +/* Driver tick rate: 100Hz = 10ms per tick. */ +#define PIT_HZ 100U +/* Divisor latched into channel 0 (1193182/100 = 11931). */ +#define PIT_DIVISOR (PIT_BASE_HZ / PIT_HZ) +/* 8259 PIC IRQ line carrying channel-0 ticks (remapped to vector 32). */ +#define PIT_IRQ 0U + +/* Program channel 0 for mode 3 square-wave at PIT_HZ and register + * pit_on_tick as the IRQ0 handler (if irq_register_handler is linked in). + * Ring 0 only. Idempotent: second and later calls do nothing. Leaves IF + * exactly as found; the caller decides when to sti. */ +void pit_init(void); + +/* IRQ0 handler: increments the tick counter. Called from IRQ context + * (interrupts already gated by the CPU/IRQ stub); must stay async-safe + * and never block. */ +void pit_on_tick(void); + +/* Monotonic tick count since pit_init (10ms per tick at 100Hz). Wraps only + * after ~5.8 billion years; unsigned subtraction stays correct across it. */ +uint64_t pit_ticks(void); + +/* Busy-wait `ticks` ticks using `hlt` to save power between interrupts. + * Requires interrupts enabled (IF=1) and the PIT IRQ unmasked, otherwise + * the CPU sleeps forever. Uses unsigned delta so a counter wrap mid-sleep + * still terminates. */ +void pit_sleep(uint64_t ticks); + +#endif /* NADIR_PIT_H */ diff --git a/kernel/arch/x86_64/pit.c b/kernel/arch/x86_64/pit.c new file mode 100644 index 0000000..8428212 --- /dev/null +++ b/kernel/arch/x86_64/pit.c @@ -0,0 +1,95 @@ +/* 8254 PIT timer driver (see pit.h). + * + * Programs channel 0 in mode 3 (square-wave generator) at 100Hz on IRQ0. + * + * Why 1193182Hz base: the PIT input clock is the IBM-PC crystal frequency + * (14.31818MHz / 12); every PC-compatible PIT ticks at this rate, so the + * divisor for N Hz is always 1193182 / N. + * Why mode 3: square-wave output stays high/low for half the period and + * retriggers the IRQ line cleanly each cycle; mode 2 (rate generator) + * emits narrow pulses that are fine too, but mode 3 is the conventional + * choice for periodic system ticks on channel 0. + * Why volatile + no locking: single-core kernel; s_ticks is written only + * in IRQ0 context and read in mainline code. `volatile` keeps the compiler + * from caching the counter across the hlt loop, and on x86-64 an aligned + * 64-bit load/store is atomic, so no spinlock is needed while IF gates the + * writer. + */ + +#include + +#include "idt.h" +#include "io.h" +#include "pit.h" + +#define PIT_CMD 0x43U +#define PIT_CH0 0x40U +/* Ch0, lobyte/hibyte access, mode 3 square-wave, binary (not BCD). */ +#define PIT_CMD_CH0_MODE3 0x36U + +/* Compile-time guard: a zero divisor would mean 65536 to the hardware and + * silently run at ~18Hz instead of PIT_HZ. Frequency is fixed on purpose. */ +_Static_assert(PIT_DIVISOR != 0, "PIT divisor must not be zero"); +_Static_assert(PIT_DIVISOR <= 0xFFFFU, "PIT divisor must fit in 16 bits"); + +/* Port I/O is owned by include/io.h (single owner); irq_register_handler + * is owned by idt.c via include/idt.h. */ + +/* Monotonic tick counter: written in IRQ context. */ +static volatile uint64_t s_ticks; +/* pit_init is idempotent; re-entry after the first programming is a no-op. */ +static uint8_t s_initialized; + +void pit_init(void) +{ + uint16_t divisor; + uint8_t lo; + uint8_t hi; + + if (s_initialized != 0) { + return; + } + + /* No cli/sti here on purpose: latch programming is two plain outb + * writes (safe with IF=0 at boot) and interrupt policy belongs to the + * caller (kmain integrator), which unmasks IRQ0 and stis afterwards. */ + divisor = (uint16_t)PIT_DIVISOR; + lo = (uint8_t)(divisor & 0xFFU); + hi = (uint8_t)((divisor >> 8) & 0xFFU); + + /* Latch order matters: command byte first selects ch0 + lobyte/hibyte + * mode, then the divisor follows low byte first, high byte second. */ + outb(PIT_CMD, PIT_CMD_CH0_MODE3); + outb(PIT_CH0, lo); + outb(PIT_CH0, hi); + + /* Strong idt.h registration: IRQ0 -> vector 32 wires pit_on_tick. */ + irq_register_handler(PIT_IRQ, pit_on_tick); + + s_initialized = 1; +} + +void pit_on_tick(void) +{ + /* Single increment async-safe, no logging or port I/O here. The PIC + * EOI is owned by the IRQ dispatcher not this driver. */ + s_ticks++; +} + +uint64_t pit_ticks(void) +{ + return s_ticks; +} + +void pit_sleep(uint64_t ticks) +{ + uint64_t start = s_ticks; + + /* hlt (not a spin loop) so the CPU sleeps until the next interrupt. + * Assumes IF=1 and IRQ0 unmasked; with IF=0 this would halt forever, + * which is why pit_init leaves interrupt policy to the caller. The + * unsigned delta keeps working if s_ticks wraps mid-sleep. */ + while ((s_ticks - start) < ticks) { + __asm__ volatile("hlt"); + } +}