Phase 2d (i): a kernel wall-clock from the CMOS RTC

Adds real (calendar) time, the foundation for filesystem timestamps. Monotonic
time (`clock`) says how long since boot; this says what time it actually is.

- cpu.zig (x86_64): readRtcUnixSeconds() reads the CMOS real-time clock (ports
  0x70/0x71) — waits out an update-in-progress, reads twice until stable, handles
  BCD-vs-binary and 12-vs-24-hour per status register B — and converts to Unix
  epoch seconds (UTC).
- kernel/wall-clock.zig: reads the RTC once at boot and anchors it to the monotonic
  clock, so a query is a cheap arithmetic offset — no per-call CMOS poll, no lock,
  no SMP hazard on the shared ports. kmain calls init() once the monotonic clock is
  final and logs the epoch.
- wall_clock() syscall (33) -> Unix epoch seconds, wrapped by runtime.system
  .wallClock(). Wall-clock *seconds* are mechanism the kernel owns like the
  monotonic clock; calendars/timezones are user-space policy (the stale comment on
  systemClock that called wall-clock a "user-space service" is updated in spirit by
  the new handler's doc).
- A `wall-clock` kernel test asserts the boot RTC read is a plausible current epoch.

Verified against the host: the guest read epoch 1783971244 while `date -u +%s` gave
1783971245 (one second of boot lag) — the CMOS read + epoch conversion are correct to
the second. zig build, zig build test, and smoke/clock/init are green.
This commit is contained in:
Daniel Samson
2026-07-13 20:35:03 +01:00
parent 54635eecf5
commit 8a38540312
8 changed files with 157 additions and 0 deletions
+89
View File
@@ -469,6 +469,95 @@ pub fn clockHz() u64 {
return apic.tscHz();
}
// --- real-time clock (CMOS) --------------------------------------------------
//
// The battery-backed CMOS clock, read once at boot and thereafter anchored to the
// monotonic clock (see kernel/wall-clock.zig) — so this is never on a hot path and
// needs no lock. Wall-clock *seconds* are mechanism the kernel owns (the hardware's
// value), like the monotonic clock; calendars/timezones are policy layered on top.
fn cmosRead(register: u8) u8 {
io.outb(0x70, register);
return io.inb(0x71);
}
const RtcFields = struct { second: u8, minute: u8, hour: u8, day: u8, month: u8, year: u8 };
fn rtcRaw() RtcFields {
while (cmosRead(0x0A) & 0x80 != 0) {} // wait out any update in progress (status A bit 7)
return .{
.second = cmosRead(0x00),
.minute = cmosRead(0x02),
.hour = cmosRead(0x04),
.day = cmosRead(0x07),
.month = cmosRead(0x08),
.year = cmosRead(0x09),
};
}
fn bcdToBinary(v: u8) u8 {
return (v & 0x0F) + ((v >> 4) * 10);
}
fn isLeapYear(y: u32) bool {
return (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0);
}
/// Read the CMOS real-time clock and convert it to Unix epoch seconds (UTC).
pub fn readRtcUnixSeconds() u64 {
// Read until two consecutive reads agree, so we never latch a half-updated time.
var a = rtcRaw();
while (true) {
const b = rtcRaw();
if (a.second == b.second and a.minute == b.minute and a.hour == b.hour and
a.day == b.day and a.month == b.month and a.year == b.year) break;
a = b;
}
const status_b = cmosRead(0x0B);
const binary_mode = status_b & 0x04 != 0; // else BCD
const hour_24 = status_b & 0x02 != 0; // else 12-hour with a PM bit
var second = a.second;
var minute = a.minute;
var hour_field = a.hour;
var day = a.day;
var month = a.month;
var year = a.year;
if (!binary_mode) {
second = bcdToBinary(second);
minute = bcdToBinary(minute);
hour_field = bcdToBinary(hour_field & 0x7F) | (hour_field & 0x80); // preserve the PM bit
day = bcdToBinary(day);
month = bcdToBinary(month);
year = bcdToBinary(year);
}
var hour: u32 = hour_field & 0x7F;
if (!hour_24) {
const pm = hour_field & 0x80 != 0;
hour %= 12; // 12 AM/PM -> 0
if (pm) hour += 12;
}
// The CMOS year is 0..99; QEMU and modern hardware mean 20xx (there is no
// reliable century register on QEMU). Treat < 70 as 20xx, else 19xx.
const full_year: u32 = if (year < 70) 2000 + @as(u32, year) else 1900 + @as(u32, year);
var days: u64 = 0;
var y: u32 = 1970;
while (y < full_year) : (y += 1) days += if (isLeapYear(y)) 366 else 365;
const month_lengths = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
var m: u8 = 1;
while (m < month) : (m += 1) {
days += month_lengths[m - 1];
if (m == 2 and isLeapYear(full_year)) days += 1;
}
days += @as(u64, day) - 1;
return ((days * 24 + hour) * 60 + minute) * 60 + second;
}
/// Whether the CPU guarantees an **invariant** TSC (CPUID 0x80000007 EDX[8] on
/// x86; the analogous architectural guarantee elsewhere). When false the TSC is not
/// used as the clocksource.