boot: stop assuming the machine is an emulator
Every one of these works under QEMU and stops a real machine:
- MADT processors that are "online capable" but not enabled were
counted as CPUs. AMD firmware lists absent cores that way (so does
QEMU with maxcpus), and startothers() then waited for ever for an
AP that does not exist. Only enabled entries count now, x2APIC
(type 9) entries are read too, and the wait is bounded to a second;
an AP that misses it is parked with INIT and the boot goes on.
- The PIT calibration loop had no bound. Recent boards clock-gate the
8254: the output never rises (a hang) or floats high (TSC "0 MHz"
and every clock thousands of times fast). The loop is bounded and
checked, and falls back to the ACPI PM timer, then CPUID 0x15/0x16.
Under QEMU pit=off the PM timer gives the same figures the PIT did.
- Firmware that hands over in x2APIC mode left lapic[] reading
garbage: cpunum() panicked or the timer never started. Each CPU is
switched back to xAPIC first (x2APIC -> disabled -> xAPIC).
- The IOAPIC was always at 0xFEC00000 and interrupts were aimed at
APIC ID "0". Both now come from the firmware's tables.
- INIT-to-STARTUP was 100 us ("Bochs is slow"); it is the 10 ms the
MP specification asks for.
- mouseinit() drained the 8042 with an unbounded loop. With no
controller the port reads 0xFF and the loop never ends. It now
notices there is no controller and skips the mouse.
Tested in QEMU: i440fx and q35, -smp 4, -smp 2,maxcpus=4, pit=off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HH7xukt4aaiGj1nQEAoQf1
This commit is contained in:
parent
f1798e7443
commit
94c936ed98
7 changed files with 355 additions and 23 deletions
|
|
@ -80,6 +80,116 @@ 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
|
||||
|
||||
// The TSC's frequency from the processor itself, for when there is no PIT to
|
||||
// time it against. 0 if the processor does not say.
|
||||
//
|
||||
// Leaf 0x15 is the ratio of the TSC to the core crystal, and the crystal's
|
||||
// frequency when the part reports it; leaf 0x16 is the base frequency in MHz,
|
||||
// which is the TSC's on every part that has an invariant one. The first is
|
||||
// exact and the second is a nominal figure, so the first is preferred.
|
||||
static uint64
|
||||
tsc_hz_from_cpuid(void)
|
||||
{
|
||||
uint32 max, a, b, c, d;
|
||||
|
||||
cpuid_leaf(0, &max, 0, 0, 0);
|
||||
if (max >= 0x15) {
|
||||
cpuid_leaf(0x15, &a, &b, &c, &d);
|
||||
if (a && b && c)
|
||||
return (uint64)c * b / a;
|
||||
}
|
||||
if (max >= 0x16) {
|
||||
cpuid_leaf(0x16, &a, 0, 0, 0);
|
||||
if (a & 0xFFFF)
|
||||
return (uint64)(a & 0xFFFF) * 1000000;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The fallback when the PIT does not count.
|
||||
//
|
||||
// Something else has to be the stopwatch. In order: the ACPI PM timer, which
|
||||
// is a real clock at a fixed 3.579545 MHz on every PC that is not
|
||||
// hardware-reduced; then the TSC frequency the processor states in CPUID,
|
||||
// with the TSC itself as the stopwatch; and only then a guess.
|
||||
extern uint32 acpi_pmtmr_port;
|
||||
extern int acpi_pmtmr_32;
|
||||
|
||||
#define PMTMR_HZ 3579545
|
||||
|
||||
// 50 ms of the PM timer, with the LAPIC and the TSC counted across it.
|
||||
// Returns 0 if the timer did not move, which is a port that is not one.
|
||||
static int
|
||||
calibrate_pmtimer(void)
|
||||
{
|
||||
uint32 mask = acpi_pmtmr_32 ? 0xFFFFFFFFu : 0x00FFFFFFu;
|
||||
uint32 want = PMTMR_HZ / 20, t0, t;
|
||||
|
||||
lapicw(TDCR, X1);
|
||||
lapicw(TIMER, MASKED);
|
||||
lapicw(TICR, 0xFFFFFFFF);
|
||||
|
||||
t0 = inl(acpi_pmtmr_port) & mask;
|
||||
uint64 tsc0 = rdtsc();
|
||||
uint lap0 = lapic[TCCR];
|
||||
do {
|
||||
t = inl(acpi_pmtmr_port) & mask;
|
||||
if (rdtsc() - tsc0 > 4000000000ULL) {
|
||||
lapicw(TICR, 0);
|
||||
return 0;
|
||||
}
|
||||
} while (((t - t0) & mask) < want);
|
||||
uint lap1 = lapic[TCCR];
|
||||
uint64 tsc1 = rdtsc();
|
||||
lapicw(TICR, 0);
|
||||
|
||||
uint32 ticks = (t - t0) & mask;
|
||||
tsc_hz = (tsc1 - tsc0) * PMTMR_HZ / ticks;
|
||||
lapic_timer_hz = (uint)((uint64)(lap0 - lap1) * PMTMR_HZ / ticks);
|
||||
boot_tsc = tsc1;
|
||||
return tsc_hz >= 20000000ULL;
|
||||
}
|
||||
|
||||
static void
|
||||
calibrate_without_pit(void)
|
||||
{
|
||||
if (acpi_pmtmr_port && calibrate_pmtimer()) {
|
||||
kinfo("cpu0: the PIT does not count; timed against the ACPI PM timer\n");
|
||||
} else {
|
||||
tsc_hz = tsc_hz_from_cpuid();
|
||||
if (tsc_hz == 0) {
|
||||
// Nothing left to measure against. Wrong-but-running, as below: time
|
||||
// will run at the wrong speed and the line says that it may.
|
||||
tsc_hz = 2000000000ULL;
|
||||
cprintf("cpu0: no PIT, no PM timer and no TSC frequency from CPUID; "
|
||||
"assuming 2 GHz\n");
|
||||
} else {
|
||||
kinfo("cpu0: the PIT does not count; TSC frequency from CPUID\n");
|
||||
}
|
||||
|
||||
lapicw(TDCR, X1);
|
||||
lapicw(TIMER, MASKED);
|
||||
lapicw(TICR, 0xFFFFFFFF);
|
||||
uint64 tsc0 = rdtsc();
|
||||
uint lap0 = lapic[TCCR];
|
||||
uint64 until = tsc0 + tsc_hz / 20; // 50 ms
|
||||
while (rdtsc() < until)
|
||||
asm volatile("pause");
|
||||
uint lap1 = lapic[TCCR];
|
||||
uint64 tsc1 = rdtsc();
|
||||
lapicw(TICR, 0);
|
||||
|
||||
lapic_timer_hz = (uint)((uint64)(lap0 - lap1) * tsc_hz / (tsc1 - tsc0));
|
||||
boot_tsc = tsc1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
calibrate_timers(void)
|
||||
{
|
||||
|
|
@ -105,12 +215,41 @@ calibrate_timers(void)
|
|||
uint64 tsc0 = rdtsc();
|
||||
uint lap0 = lapic[TCCR];
|
||||
|
||||
while (!(inb(PIT_GATE) & 0x20)) // ch2 output rises at terminal count
|
||||
;
|
||||
// Bounded. "Always present" stopped being true: recent Intel boards
|
||||
// clock-gate the 8254 to save power, and then the output never rises and
|
||||
// this loop was the whole of the boot -- a machine stopped at the line
|
||||
// before "lapic timer", with nothing to say why. Four billion TSC cycles is
|
||||
// over a second on anything that exists and twenty times the window on a
|
||||
// 4 GHz part, so a working PIT never comes near it.
|
||||
//
|
||||
// And checked at the start: in mode 0 the output goes low the moment the
|
||||
// count is written, so a bit that is already high is no PIT at all. An
|
||||
// absent one floats -- port 0x61 reads 0xFF -- and then the loop ended at
|
||||
// once, the TSC was "measured" at 0 MHz, and every clock in the system ran
|
||||
// thousands of times fast. Under QEMU with pit=off uptime read four days
|
||||
// thirty seconds after boot.
|
||||
int pit_ok = !(inb(PIT_GATE) & 0x20);
|
||||
while (pit_ok && !(inb(PIT_GATE) & 0x20)) { // ch2 output rises at terminal count
|
||||
if (rdtsc() - tsc0 > 4000000000ULL) {
|
||||
pit_ok = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint lap1 = lapic[TCCR];
|
||||
uint64 tsc1 = rdtsc();
|
||||
|
||||
// 50 ms of a TSC slower than 20 MHz is no processor this runs on: the
|
||||
// window was not 50 ms, so the PIT was not counting it.
|
||||
if (pit_ok && tsc1 - tsc0 < 1000000)
|
||||
pit_ok = 0;
|
||||
|
||||
if (!pit_ok) {
|
||||
lapicw(TICR, 0);
|
||||
calibrate_without_pit();
|
||||
return;
|
||||
}
|
||||
|
||||
lapicw(TICR, 0); // stop counting
|
||||
|
||||
// TCCR counts down, so elapsed counts are start minus end.
|
||||
|
|
@ -134,12 +273,61 @@ calibrate_timers(void)
|
|||
}
|
||||
}
|
||||
|
||||
#define MSR_APIC_BASE 0x1B
|
||||
#define APIC_BASE_EN (1ULL << 11) // globally enabled
|
||||
#define APIC_BASE_EXTD (1ULL << 10) // x2APIC mode
|
||||
|
||||
// Put this processor's local APIC in xAPIC mode, which is the mode everything
|
||||
// here speaks: registers at an MMIO address, read and written as memory.
|
||||
//
|
||||
// Firmware is allowed to hand over in x2APIC mode, and on recent Intel
|
||||
// laptops it usually does. In that mode the MMIO page is not the APIC any
|
||||
// more -- the registers are MSRs -- so every lapic[] access read nothing
|
||||
// useful: the ID came back wrong, cpunum() panicked "unknown apicid", and the
|
||||
// timer was never started. A hang or a panic within the first second, on the
|
||||
// newest machines and on no emulator, because none of them hand over that way.
|
||||
//
|
||||
// The architecture allows exactly one way back: x2APIC to disabled, disabled
|
||||
// to xAPIC. Disabling discards the APIC's state, which costs nothing here
|
||||
// because lapicinit() is about to set all of it.
|
||||
//
|
||||
// It cannot be done when the processor has locked xAPIC out (some server
|
||||
// parts, which report it in IA32_XAPIC_DISABLE_STATUS), or when its APIC ID
|
||||
// does not fit in eight bits. Both get said plainly rather than left to fail
|
||||
// at the first register access.
|
||||
void
|
||||
lapic_xapic_mode(void)
|
||||
{
|
||||
uint32 d;
|
||||
uint64 base = rdmsr(MSR_APIC_BASE);
|
||||
|
||||
if ((base & (APIC_BASE_EN | APIC_BASE_EXTD)) == APIC_BASE_EN)
|
||||
return; // xAPIC already: the usual case
|
||||
|
||||
if (base & APIC_BASE_EXTD) {
|
||||
cpuid_leaf(7, 0, 0, 0, &d);
|
||||
if (d & (1u << 29)) { // IA32_ARCH_CAPABILITIES exists
|
||||
uint64 caps = rdmsr(0x10A);
|
||||
if ((caps & (1ULL << 21)) && (rdmsr(0xBD) & 1))
|
||||
panic("lapic: firmware has locked the APIC in x2APIC mode");
|
||||
}
|
||||
if (rdmsr(0x802) > 254) // x2APIC ID register
|
||||
panic("lapic: APIC id too large for xAPIC mode");
|
||||
wrmsr(MSR_APIC_BASE, base & ~(APIC_BASE_EN | APIC_BASE_EXTD));
|
||||
}
|
||||
// From disabled (whether firmware left it so or the line above did) to
|
||||
// xAPIC. The base address bits are kept: they are where lapic[] points.
|
||||
wrmsr(MSR_APIC_BASE, (base & ~APIC_BASE_EXTD) | APIC_BASE_EN);
|
||||
}
|
||||
|
||||
void
|
||||
lapicinit(void)
|
||||
{
|
||||
if(!lapic)
|
||||
return;
|
||||
|
||||
lapic_xapic_mode();
|
||||
|
||||
// Enable local APIC; set spurious interrupt vector.
|
||||
lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS));
|
||||
|
||||
|
|
@ -281,7 +469,11 @@ lapicstartap(uchar apicid, uint addr)
|
|||
lapicw(ICRLO, SINIT | LEVEL | ASSERT);
|
||||
microdelay(200);
|
||||
lapicw(ICRLO, SINIT | LEVEL);
|
||||
microdelay(100); // should be 10ms, but too slow in Bochs!
|
||||
// The MP specification's 10 ms. This was 100 us, "because Bochs is slow",
|
||||
// which is a figure for an emulator: older real processors want the full
|
||||
// wait between INIT and STARTUP, and starting one too early is an AP that
|
||||
// does not come up.
|
||||
microdelay(10000);
|
||||
|
||||
// Send startup IPI (twice!) to enter code.
|
||||
// Regular hardware is supposed to only accept a STARTUP
|
||||
|
|
@ -295,6 +487,20 @@ lapicstartap(uchar apicid, uint addr)
|
|||
}
|
||||
}
|
||||
|
||||
// Hold a processor in wait-for-SIPI. For an AP that did not report in on
|
||||
// time: if it is merely slow, this stops it from waking later and reading the
|
||||
// trampoline parameters startothers() has since written for the next one --
|
||||
// which would be two processors on one stack.
|
||||
void
|
||||
lapic_park(uchar apicid)
|
||||
{
|
||||
lapicw(ICRHI, apicid<<24);
|
||||
lapicw(ICRLO, SINIT | LEVEL | ASSERT);
|
||||
microdelay(200);
|
||||
lapicw(ICRLO, SINIT | LEVEL);
|
||||
microdelay(100);
|
||||
}
|
||||
|
||||
#define CMOS_STATA 0x0a
|
||||
#define CMOS_STATB 0x0b
|
||||
#define CMOS_UIP (1 << 7) // RTC update in progress
|
||||
|
|
|
|||
Loading…
Reference in a new issue