SchOS/kernel/arch/x86_64/lapic.c
Nexoniarz 617a4aec0a boot: one line when it works, all of it when it does not
A boot printed about thirty lines before reaching a prompt. Every one was
useful once, and printed on every boot for ever they train the person in front
of the machine to stop reading their own console -- which is expensive on the
day one of them changes.

    SchOS UEFI loader
    Press F8 for boot options... booting

    Welcome to SchOS!  (F8 at boot for the full log, or dmesg once you are in)

    root@schos / »

**Nothing was deleted, it was routed.** consputc() already wrote to the klog
ring before anywhere else, so a line can be kept in full while being held back
from the two places a person looks. kinfo() is cprintf() with that routing, and
`dmesg` still has the whole boot with timestamps afterwards. The rule for which
is which is not how interesting a line is -- everything was interesting to
somebody once -- but whether you would want to see it on a machine that is
*working*. `pci: 7 devices` is yes the first time and no every time after;
`e1000: no DMA memory for the rings` is yes for ever and stays a cprintf.

The verbose boot the F8 menu already offered now means something much bigger,
which is the other half of this: it is the only way to see any of it live.

Userspace follows through /proc/bootmode, one word. schinit, schinitd and vgpud
hold back their progress lines and never their failures -- a supervisor that
goes quiet when a job dies is worse than one that chatters.

Two things found by testing rather than by reading:

  - schinit read /proc/bootmode *before* bootstrap() mounted /proc, so it got
    "quiet" every time and the verbose boot was quiet in the one process that
    runs first. It reads after the mount now.
  - resolvconf announced "already exists; -f to replace it" on every boot after
    the first, which is a line about having done nothing. It is quiet when the
    file already says what it would have written, and speaks only when the two
    disagree -- which means the nameserver changed and the machine is using the
    old one.

Verified both ways: quiet boot is six lines, a forced-verbose build prints all
thirty plus the `verbose boot:` steps, and /proc/kmsg holds every line in both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 21:19:50 +02:00

362 lines
12 KiB
C

// The local APIC manages internal (non-I/O) interrupts.
// See Chapter 8 & Appendix C of Intel processor manual volume 3.
#include "param.h"
#include "types.h"
#include "defs.h"
#include "date.h"
#include "memlayout.h"
#include "traps.h"
#include "mmu.h"
#include "x86.h"
#include "proc.h" // ncpu
// Local APIC registers, divided by 4 for use as uint[] indices.
#define ID (0x0020/4) // ID
#define VER (0x0030/4) // Version
#define TPR (0x0080/4) // Task Priority
#define EOI (0x00B0/4) // EOI
#define SVR (0x00F0/4) // Spurious Interrupt Vector
#define ENABLE 0x00000100 // Unit Enable
#define ESR (0x0280/4) // Error Status
#define ICRLO (0x0300/4) // Interrupt Command
#define SINIT 0x00000500 // INIT/RESET
#define STARTUP 0x00000600 // Startup IPI
#define DELIVS 0x00001000 // Delivery status
#define ASSERT 0x00004000 // Assert interrupt (vs deassert)
#define LEVEL 0x00008000 // Level triggered
#define BCAST 0x00080000 // Send to all APICs, including self.
#define BUSY 0x00001000
#define ICRHI (0x0310/4) // Interrupt Command [63:32]
#define TIMER (0x0320/4) // Local Vector Table 0 (TIMER)
#define X1 0x0000000B // divide counts by 1
#define PERIODIC 0x00020000 // Periodic
#define PCINT (0x0340/4) // Performance Counter LVT
#define LINT0 (0x0350/4) // Local Vector Table 1 (LINT0)
#define LINT1 (0x0360/4) // Local Vector Table 2 (LINT1)
#define ERROR (0x0370/4) // Local Vector Table 3 (ERROR)
#define MASKED 0x00010000 // Interrupt masked
#define TICR (0x0380/4) // Timer Initial Count
#define TCCR (0x0390/4) // Timer Current Count
#define TDCR (0x03E0/4) // Timer Divide Configuration
volatile uint *lapic; // Initialized in mp.c
static void
lapicw(int index, int value)
{
lapic[index] = value;
lapic[ID]; // wait for write to finish, by reading
}
// ---------------------------------------------------------------------
// Timer calibration
// ---------------------------------------------------------------------
// The LAPIC timer counts down at the APIC bus frequency, and nothing in the
// architecture tells you what that frequency is. xv6 loaded TICR with
// 10000000 and hoped; its own comment admitted the gap -- "if xv6 cared more
// about precise timekeeping, TICR would be calibrated using an external time
// source." That value yields the intended 100 Hz only if the bus happens to
// run at exactly 1 GHz.
//
// Under KVM it does not, and the error is not subtle. The kernel believed
// ticks arrived 100 times a second while they were arriving roughly 18 times
// a second, so every number derived from ticks was wrong by that same factor,
// in the same direction, everywhere at once: uptime ran slow, `time` reported
// three seconds for twenty, ping's one-second interval stretched to six. It
// read as three unrelated bugs in three unrelated programs. It was this line.
//
// So measure it. The 8254 PIT is exactly the external time source the comment
// asked for: its input clock is fixed at 1193182 Hz by the hardware, it is
// always present, and channel 2 is the one whose gate software can raise and
// whose output software can poll -- a stopwatch that needs no interrupt, which
// matters because this runs before the IDT is ready to take one.
#define PIT_HZ 1193182 // 8254 input clock, fixed by definition
#define PIT_CH2 0x42
#define PIT_CMD 0x43
#define PIT_GATE 0x61 // bit 0: ch2 gate, bit 1: speaker, bit 5: ch2 output
uint lapic_timer_hz; // measured LAPIC timer counts per second
uint64 tsc_hz; // measured TSC cycles per second
uint64 boot_tsc; // the TSC when calibration finished
static void
calibrate_timers(void)
{
// 50 ms: long enough that the port I/O around the measurement is noise,
// short enough that nobody notices it in boot time. The count fits in the
// 16 bits the PIT has (59659), which is the real upper bound here.
const uint us = 50000;
uint count = (uint)(((uint64)PIT_HZ * us) / 1000000);
// Channel 2, mode 0 (interrupt on terminal count), lobyte then hibyte.
// Gate high so it counts; the speaker bit stays clear so the box is silent.
outb(PIT_GATE, (inb(PIT_GATE) & ~0x02) | 0x01);
outb(PIT_CMD, 0xB0);
outb(PIT_CH2, count & 0xFF);
outb(PIT_CH2, (count >> 8) & 0xFF);
// A free-running LAPIC counter for the duration: one-shot, masked, started
// at the largest count there is so it cannot wrap inside 50 ms.
lapicw(TDCR, X1);
lapicw(TIMER, MASKED);
lapicw(TICR, 0xFFFFFFFF);
uint64 tsc0 = rdtsc();
uint lap0 = lapic[TCCR];
while (!(inb(PIT_GATE) & 0x20)) // ch2 output rises at terminal count
;
uint lap1 = lapic[TCCR];
uint64 tsc1 = rdtsc();
lapicw(TICR, 0); // stop counting
// TCCR counts down, so elapsed counts are start minus end.
lapic_timer_hz = (uint)(((uint64)(lap0 - lap1) * 1000000) / us);
tsc_hz = ((tsc1 - tsc0) * 1000000) / us;
// Where the clock starts. Everything the system reports as elapsed time is
// measured from here, against the TSC, rather than by counting timer
// interrupts -- see kernel/core/time.c.
boot_tsc = tsc1;
// If the measurement came back implausible -- no LAPIC, a hypervisor that
// stops the counter, a PIT that never raises its output -- fall back to the
// old assumption rather than dividing by zero and taking the machine down.
// Wrong-but-running beats a triple fault, and the printed value says which
// case this is.
if (lapic_timer_hz < HZ * 1000u) {
cprintf("cpu0: lapic calibration implausible (%u Hz), assuming 1 GHz bus\n",
lapic_timer_hz);
lapic_timer_hz = 1000000000u; // the 1 GHz that TICR=10000000 implied
}
}
void
lapicinit(void)
{
if(!lapic)
return;
// Enable local APIC; set spurious interrupt vector.
lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS));
// The timer counts down at the APIC bus frequency and interrupts at zero.
// What that frequency *is*, calibrate_timers() had to go and measure --
// see the comment above it for why loading a constant here does not work.
if (lapic_timer_hz == 0) {
calibrate_timers();
kinfo("cpu0: lapic timer %u Hz, tsc %u MHz, tick = %u counts at %d Hz\n",
lapic_timer_hz, (uint)(tsc_hz / 1000000),
lapic_timer_hz / HZ, HZ);
}
lapicw(TDCR, X1);
lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));
lapicw(TICR, lapic_timer_hz / HZ);
// Disable logical interrupt lines.
lapicw(LINT0, MASKED);
lapicw(LINT1, MASKED);
// Disable performance counter overflow interrupts
// on machines that provide that interrupt entry.
if(((lapic[VER]>>16) & 0xFF) >= 4)
lapicw(PCINT, MASKED);
// Map error interrupt to IRQ_ERROR.
lapicw(ERROR, T_IRQ0 + IRQ_ERROR);
// Clear error status register (requires back-to-back writes).
lapicw(ESR, 0);
lapicw(ESR, 0);
// Ack any outstanding interrupts.
lapicw(EOI, 0);
// Send an Init Level De-Assert to synchronise arbitration ID's.
lapicw(ICRHI, 0);
lapicw(ICRLO, BCAST | SINIT | LEVEL);
while(lapic[ICRLO] & DELIVS)
;
// Enable interrupts on the APIC (but not on the processor).
lapicw(TPR, 0);
}
int
cpunum(void)
{
int apicid, i;
// Cannot call cpu when interrupts are enabled:
// result not guaranteed to last long enough to be used!
// Would prefer to panic but even printing is chancy here:
// almost everything, including cprintf and panic, calls cpu,
// often indirectly through acquire and release.
if(readeflags()&FL_IF){
static int n;
if(n++ == 0)
cprintf("cpu called from %x with interrupts enabled\n",
__builtin_return_address(0));
}
if (!lapic)
return 0;
apicid = lapic[ID] >> 24;
for (i = 0; i < ncpu; ++i) {
if (cpus[i].apicid == apicid)
return i;
}
panic("unknown apicid\n");
}
// Acknowledge interrupt.
void
lapiceoi(void)
{
if(lapic)
lapicw(EOI, 0);
}
// Spin for a given number of microseconds.
//
// This was an empty function body -- xv6's stub, carrying a comment saying
// real hardware would want to tune it dynamically. Nothing ever did, so every
// microdelay() in the tree returned immediately: the xHCI reset sequence, the
// USB hub power-on wait, the UART drain. Those delays are written into the
// specifications because the hardware genuinely needs the time, and a port
// that "works anyway" is one that is racing and winning.
//
// The TSC is the right clock for this: calibrate_timers() measured it in the
// same pass as the LAPIC, and unlike the LAPIC timer it is not already busy
// driving the tick.
void
microdelay(int us)
{
if (us <= 0)
return;
// Before calibration (early boot, and the AP trampoline) there is no
// measured frequency to work from. Spin on a rough count instead: the
// figure is wrong, but being approximately late is the entire point of the
// call, and being instantly early is what broke it.
if (tsc_hz == 0) {
for (volatile int i = 0; i < us * 100; i++)
;
return;
}
uint64 end = rdtsc() + ((uint64)us * tsc_hz) / 1000000;
while (rdtsc() < end)
asm volatile("pause");
}
#define CMOS_PORT 0x70
#define CMOS_RETURN 0x71
// Start additional processor running entry code at addr.
// See Appendix B of MultiProcessor Specification.
void
lapicstartap(uchar apicid, uint addr)
{
int i;
ushort *wrv;
// "The BSP must initialize CMOS shutdown code to 0AH
// and the warm reset vector (DWORD based at 40:67) to point at
// the AP startup code prior to the [universal startup algorithm]."
outb(CMOS_PORT, 0xF); // offset 0xF is shutdown code
outb(CMOS_PORT+1, 0x0A);
wrv = (ushort*)P2V((0x40<<4 | 0x67)); // Warm reset vector
wrv[0] = 0;
wrv[1] = addr >> 4;
// "Universal startup algorithm."
// Send INIT (level-triggered) interrupt to reset other CPU.
lapicw(ICRHI, apicid<<24);
lapicw(ICRLO, SINIT | LEVEL | ASSERT);
microdelay(200);
lapicw(ICRLO, SINIT | LEVEL);
microdelay(100); // should be 10ms, but too slow in Bochs!
// Send startup IPI (twice!) to enter code.
// Regular hardware is supposed to only accept a STARTUP
// when it is in the halted state due to an INIT. So the second
// should be ignored, but it is part of the official Intel algorithm.
// Bochs complains about the second one. Too bad for Bochs.
for(i = 0; i < 2; i++){
lapicw(ICRHI, apicid<<24);
lapicw(ICRLO, STARTUP | (addr>>12));
microdelay(200);
}
}
#define CMOS_STATA 0x0a
#define CMOS_STATB 0x0b
#define CMOS_UIP (1 << 7) // RTC update in progress
#define SECS 0x00
#define MINS 0x02
#define HOURS 0x04
#define DAY 0x07
#define MONTH 0x08
#define YEAR 0x09
static uint cmos_read(uint reg)
{
outb(CMOS_PORT, reg);
microdelay(200);
return inb(CMOS_RETURN);
}
static void fill_rtcdate(struct rtcdate *r)
{
r->second = cmos_read(SECS);
r->minute = cmos_read(MINS);
r->hour = cmos_read(HOURS);
r->day = cmos_read(DAY);
r->month = cmos_read(MONTH);
r->year = cmos_read(YEAR);
}
//PAGEBREAK!
// qemu seems to use 24-hour GWT and the values are BCD encoded
void cmostime(struct rtcdate *r)
{
struct rtcdate t1, t2;
int sb, bcd;
sb = cmos_read(CMOS_STATB);
bcd = (sb & (1 << 2)) == 0;
// make sure CMOS doesn't modify time while we read it
for(;;) {
fill_rtcdate(&t1);
if(cmos_read(CMOS_STATA) & CMOS_UIP)
continue;
fill_rtcdate(&t2);
if(memcmp(&t1, &t2, sizeof(t1)) == 0)
break;
}
// convert
if(bcd) {
#define CONV(x) (t1.x = ((t1.x >> 4) * 10) + (t1.x & 0xf))
CONV(second);
CONV(minute);
CONV(hour );
CONV(day );
CONV(month );
CONV(year );
#undef CONV
}
*r = t1;
r->year += 2000;
}