diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 14:21:11 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 14:21:11 +0200 |
| commit | 6447eb12948d37b74adae2fdbf1db5a9d7b363d5 (patch) | |
| tree | 0aa03cc79db7ff49f5a64a484ff197ba05f9b6bc /rsnix/src | |
| parent | klog: kernel log ring + /dev/klog + klog shell command (diff) | |
| download | rs-dos-6447eb12948d37b74adae2fdbf1db5a9d7b363d5.tar.gz rs-dos-6447eb12948d37b74adae2fdbf1db5a9d7b363d5.zip | |
klog: [ssss.mmm] millisecond timestamps from the PIT
- interrupts::uptime_ms(): monotonic ms since boot — the 100 Hz PIT
tick (10 ms) plus interpolation from the PIT's live 1.19 MHz
down-counter (read twice around the tick load; a count jump means a
tick fired between reads) for true sub-ms resolution. TICKS widened
AtomicU16 -> AtomicU64 (u16 wrapped after ~11 min).
- PIT_READY gates interpolation until interrupts::init configures the
timer, so early-boot lines are deterministically [0000.000] instead
of uninitialised-PIT noise; klog clamps timestamps monotonic.
- The CMOS RTC cannot provide milliseconds (1 Hz BCD device, seconds
only) — millis come from the PIT.
- klog stores ms per line at append time; dump format is now
'[0000.123] 000000 === rsnix boot ===' (user-specified format).
Verified in QEMU: first line [0000.000], Phase-2 lines show real
monotonic ms (fs: vfs ready at [0000.519], self-test [0000.524]),
per-dump monotonicity, boot.log persists timestamps, test-disk round
trip fsck-clean.
Diffstat (limited to '')
| -rw-r--r-- | rsnix/src/interrupts.rs | 64 | ||||
| -rw-r--r-- | rsnix/src/klog.rs | 30 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 12 |
3 files changed, 89 insertions, 17 deletions
diff --git a/rsnix/src/interrupts.rs b/rsnix/src/interrupts.rs index 2b82a3d..c176276 100644 --- a/rsnix/src/interrupts.rs +++ b/rsnix/src/interrupts.rs | |||
| @@ -2,7 +2,7 @@ | |||
| 2 | // Interrupt handling: IDT, PIC remap, ISR dispatch. | 2 | // Interrupt handling: IDT, PIC remap, ISR dispatch. |
| 3 | // --------------------------------------------------------------------------- | 3 | // --------------------------------------------------------------------------- |
| 4 | use core::arch::asm; | 4 | use core::arch::asm; |
| 5 | use core::sync::atomic::{AtomicU16, Ordering}; | 5 | use core::sync::atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering}; |
| 6 | 6 | ||
| 7 | // --------------------------------------------------------------------------- | 7 | // --------------------------------------------------------------------------- |
| 8 | // IDT entry and table | 8 | // IDT entry and table |
| @@ -91,11 +91,17 @@ fn eoi(irq: u8) { | |||
| 91 | 91 | ||
| 92 | const PIT_CMD: u16 = 0x43; | 92 | const PIT_CMD: u16 = 0x43; |
| 93 | const PIT_CH0: u16 = 0x40; | 93 | const PIT_CH0: u16 = 0x40; |
| 94 | const PIT_FREQ: u64 = 1_193_182; | 94 | const PIT_FREQ: u64 = 1_193_182; // PIT clock: 1.193182 MHz |
| 95 | const PIT_HZ: u64 = 100; | ||
| 95 | 96 | ||
| 96 | /// Set up PIT channel 0 in mode 2 (rate generator) at `hz` (1–1000ish). | 97 | /// Counter divisor loaded for the ~100 Hz tick (matches `pit_set_rate`). |
| 97 | fn pit_set_rate(hz: u64) { | 98 | pub const PIT_DIVISOR: u16 = (PIT_FREQ / PIT_HZ) as u16; |
| 98 | let divisor = (PIT_FREQ / hz) as u16; | 99 | |
| 100 | /// Set up PIT channel 0 in mode 2 (rate generator) at the fixed 100 Hz | ||
| 101 | /// tick rate — the divisor must stay `PIT_DIVISOR`, which `uptime_ms` | ||
| 102 | /// relies on for interpolation. | ||
| 103 | fn pit_set_rate() { | ||
| 104 | let divisor = PIT_DIVISOR; | ||
| 99 | unsafe { | 105 | unsafe { |
| 100 | // Mode 2, channel 0, lobyte/hibyte, binary | 106 | // Mode 2, channel 0, lobyte/hibyte, binary |
| 101 | asm!("out dx, al", in("dx") PIT_CMD, in("al") 0b0011_0100u8, | 107 | asm!("out dx, al", in("dx") PIT_CMD, in("al") 0b0011_0100u8, |
| @@ -107,6 +113,45 @@ fn pit_set_rate(hz: u64) { | |||
| 107 | } | 113 | } |
| 108 | } | 114 | } |
| 109 | 115 | ||
| 116 | /// Latch and read the PIT channel 0 down-counter (0..=PIT_DIVISOR). | ||
| 117 | /// In mode 2 the counter counts down and reloads on reaching 0, so it | ||
| 118 | /// only ever jumps *up* when a tick fires. | ||
| 119 | fn pit_count() -> u16 { | ||
| 120 | unsafe { | ||
| 121 | asm!("out dx, al", in("dx") PIT_CMD, in("al") 0x00u8, | ||
| 122 | options(nomem, nostack, preserves_flags)); | ||
| 123 | let lo: u8; | ||
| 124 | let hi: u8; | ||
| 125 | asm!("in al, dx", out("al") lo, in("dx") PIT_CH0, | ||
| 126 | options(nomem, nostack, preserves_flags)); | ||
| 127 | asm!("in al, dx", out("al") hi, in("dx") PIT_CH0, | ||
| 128 | options(nomem, nostack, preserves_flags)); | ||
| 129 | u16::from_le_bytes([lo, hi]) | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | /// True once the PIT tick is configured (`interrupts::init`). Before | ||
| 134 | /// that the PIT counter is uninitialised, so `uptime_ms` reports 0 | ||
| 135 | /// (early-boot messages are all at [0000.000], which is truthful — the | ||
| 136 | /// clock hasn't started). | ||
| 137 | static PIT_READY: AtomicBool = AtomicBool::new(false); | ||
| 138 | |||
| 139 | /// Monotonic milliseconds since boot: the 100 Hz tick (10 ms each) plus | ||
| 140 | /// interpolation from the PIT's live 1.19 MHz down-counter for the | ||
| 141 | /// sub-10 ms fraction. The countdown is read twice around the tick | ||
| 142 | /// load: if it jumped up, a tick fired between the reads. | ||
| 143 | pub fn uptime_ms() -> u64 { | ||
| 144 | if !PIT_READY.load(Ordering::Relaxed) { | ||
| 145 | return 0; | ||
| 146 | } | ||
| 147 | let c1 = pit_count(); | ||
| 148 | let ticks = TICKS.load(Ordering::Relaxed); | ||
| 149 | let c2 = pit_count(); | ||
| 150 | let ticks = if c2 > c1 { ticks + 1 } else { ticks }; | ||
| 151 | let c = u64::from(c2.min(PIT_DIVISOR)); | ||
| 152 | ticks * (1000 / PIT_HZ) + (u64::from(PIT_DIVISOR) - c) * 1000 / PIT_FREQ | ||
| 153 | } | ||
| 154 | |||
| 110 | // --------------------------------------------------------------------------- | 155 | // --------------------------------------------------------------------------- |
| 111 | // Lock-free SPSC ring buffer for keyboard scancodes | 156 | // Lock-free SPSC ring buffer for keyboard scancodes |
| 112 | // --------------------------------------------------------------------------- | 157 | // --------------------------------------------------------------------------- |
| @@ -286,8 +331,10 @@ pub fn read_scancode() -> Option<u8> { | |||
| 286 | KEYBUF.pop() | 331 | KEYBUF.pop() |
| 287 | } | 332 | } |
| 288 | 333 | ||
| 289 | /// Global tick counter (incremented by timer IRQ, ~100 Hz). | 334 | /// Global tick counter (incremented by timer IRQ, ~100 Hz). u64 so |
| 290 | pub static TICKS: AtomicU16 = AtomicU16::new(0); | 335 | /// uptime never wraps; `uptime_ms` interpolates the PIT counter for the |
| 336 | /// sub-tick fraction. | ||
| 337 | pub static TICKS: AtomicU64 = AtomicU64::new(0); | ||
| 291 | 338 | ||
| 292 | /// Initialise the IDT, remap the PIC, set up the PIT, and enable interrupts. | 339 | /// Initialise the IDT, remap the PIC, set up the PIT, and enable interrupts. |
| 293 | pub fn init() { | 340 | pub fn init() { |
| @@ -326,7 +373,8 @@ pub fn init() { | |||
| 326 | remap_pic(); | 373 | remap_pic(); |
| 327 | 374 | ||
| 328 | // Set up PIT timer at ~100 Hz. | 375 | // Set up PIT timer at ~100 Hz. |
| 329 | pit_set_rate(100); | 376 | pit_set_rate(); |
| 377 | PIT_READY.store(true, Ordering::Relaxed); | ||
| 330 | 378 | ||
| 331 | // Unmask timer (IRQ0 → bit 0) and keyboard (IRQ1 → bit 1) on master. | 379 | // Unmask timer (IRQ0 → bit 0) and keyboard (IRQ1 → bit 1) on master. |
| 332 | let mut mask: u8; | 380 | let mut mask: u8; |
diff --git a/rsnix/src/klog.rs b/rsnix/src/klog.rs index 1e23460..0c7dc06 100644 --- a/rsnix/src/klog.rs +++ b/rsnix/src/klog.rs | |||
| @@ -32,6 +32,10 @@ struct Ring { | |||
| 32 | count: usize, | 32 | count: usize, |
| 33 | /// Slot the next line will be written to (oldest once full). | 33 | /// Slot the next line will be written to (oldest once full). |
| 34 | next: usize, | 34 | next: usize, |
| 35 | /// Highest timestamp seen, so the log stays monotonic even before | ||
| 36 | /// the timer is running (early-boot PIT reads are uninitialised). | ||
| 37 | last_ms: u64, | ||
| 38 | ms: [u64; MAX_LINES], | ||
| 35 | lens: [u8; MAX_LINES], | 39 | lens: [u8; MAX_LINES], |
| 36 | bytes: [[u8; MAX_LINE]; MAX_LINES], | 40 | bytes: [[u8; MAX_LINE]; MAX_LINES], |
| 37 | } | 41 | } |
| @@ -41,6 +45,8 @@ static mut RING: Ring = Ring { | |||
| 41 | dropped: 0, | 45 | dropped: 0, |
| 42 | count: 0, | 46 | count: 0, |
| 43 | next: 0, | 47 | next: 0, |
| 48 | last_ms: 0, | ||
| 49 | ms: [0; MAX_LINES], | ||
| 44 | lens: [0; MAX_LINES], | 50 | lens: [0; MAX_LINES], |
| 45 | bytes: [[0; MAX_LINE]; MAX_LINES], | 51 | bytes: [[0; MAX_LINE]; MAX_LINES], |
| 46 | }; | 52 | }; |
| @@ -49,10 +55,16 @@ fn ring() -> &'static mut Ring { | |||
| 49 | unsafe { &mut *core::ptr::addr_of_mut!(RING) } | 55 | unsafe { &mut *core::ptr::addr_of_mut!(RING) } |
| 50 | } | 56 | } |
| 51 | 57 | ||
| 52 | /// Append one line to the ring (line-oriented; newlines are stripped). | 58 | /// Append one line to the ring with its timestamp in ms (line-oriented; |
| 53 | /// Truncated at MAX_LINE bytes. Never allocates, never logs. | 59 | /// newlines are stripped). Truncated at MAX_LINE bytes. Never |
| 54 | pub fn append(line: &str) { | 60 | /// allocates, never logs. Timestamps are clamped to be monotonic. |
| 61 | pub fn append(line: &str, mut ms: u64) { | ||
| 55 | let r = ring(); | 62 | let r = ring(); |
| 63 | if ms > r.last_ms { | ||
| 64 | r.last_ms = ms; | ||
| 65 | } else { | ||
| 66 | ms = r.last_ms; | ||
| 67 | } | ||
| 56 | let b = line.as_bytes(); | 68 | let b = line.as_bytes(); |
| 57 | let n = b.len().min(MAX_LINE); | 69 | let n = b.len().min(MAX_LINE); |
| 58 | if r.count == MAX_LINES { | 70 | if r.count == MAX_LINES { |
| @@ -60,6 +72,7 @@ pub fn append(line: &str) { | |||
| 60 | } else { | 72 | } else { |
| 61 | r.count += 1; | 73 | r.count += 1; |
| 62 | } | 74 | } |
| 75 | r.ms[r.next] = ms; | ||
| 63 | r.bytes[r.next][..n].copy_from_slice(&b[..n]); | 76 | r.bytes[r.next][..n].copy_from_slice(&b[..n]); |
| 64 | r.lens[r.next] = n as u8; | 77 | r.lens[r.next] = n as u8; |
| 65 | r.next = (r.next + 1) % MAX_LINES; | 78 | r.next = (r.next + 1) % MAX_LINES; |
| @@ -68,11 +81,14 @@ pub fn append(line: &str) { | |||
| 68 | 81 | ||
| 69 | /// Append the formatted output of a `serial_println!` — the macro hook. | 82 | /// Append the formatted output of a `serial_println!` — the macro hook. |
| 70 | /// `fmt::Write` may deliver the line in several chunks, so accumulate | 83 | /// `fmt::Write` may deliver the line in several chunks, so accumulate |
| 71 | /// into one line buffer first, then append the whole line. | 84 | /// into one line buffer first, then append the whole line. The |
| 85 | /// timestamp is taken when the line is emitted (monotonic ms since | ||
| 86 | /// boot, from the PIT tick + live-counter interpolation). | ||
| 72 | pub fn append_fmt(args: core::fmt::Arguments<'_>) { | 87 | pub fn append_fmt(args: core::fmt::Arguments<'_>) { |
| 88 | let ms = crate::interrupts::uptime_ms(); | ||
| 73 | let mut line = LineBuf::new(); | 89 | let mut line = LineBuf::new(); |
| 74 | let _ = core::fmt::write(&mut line, args); | 90 | let _ = core::fmt::write(&mut line, args); |
| 75 | append(line.as_str()); | 91 | append(line.as_str(), ms); |
| 76 | } | 92 | } |
| 77 | 93 | ||
| 78 | struct LineBuf { | 94 | struct LineBuf { |
| @@ -124,7 +140,9 @@ pub fn dump() -> String { | |||
| 124 | let idx = (start + i) % MAX_LINES; | 140 | let idx = (start + i) % MAX_LINES; |
| 125 | let n = r.lens[idx] as usize; | 141 | let n = r.lens[idx] as usize; |
| 126 | let text = core::str::from_utf8(&r.bytes[idx][..n]).unwrap_or(""); | 142 | let text = core::str::from_utf8(&r.bytes[idx][..n]).unwrap_or(""); |
| 127 | out += &alloc::format!("{:06} {}\n", first + i as u64, text); | 143 | let s = r.ms[idx] / 1000; |
| 144 | let m = r.ms[idx] % 1000; | ||
| 145 | out += &alloc::format!("[{:04}.{:03}] {:06} {}\n", s, m, first + i as u64, text); | ||
| 128 | } | 146 | } |
| 129 | out | 147 | out |
| 130 | } | 148 | } |
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 709ea71..53821b7 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -224,7 +224,13 @@ fn put_byte(b: u8) { | |||
| 224 | } | 224 | } |
| 225 | 225 | ||
| 226 | fn param(i: usize, default: usize) -> usize { | 226 | fn param(i: usize, default: usize) -> usize { |
| 227 | unsafe { if i < NPARAMS { PARAMS[i] } else { default } } | 227 | unsafe { |
| 228 | if i < NPARAMS { | ||
| 229 | PARAMS[i] | ||
| 230 | } else { | ||
| 231 | default | ||
| 232 | } | ||
| 233 | } | ||
| 228 | } | 234 | } |
| 229 | 235 | ||
| 230 | /// Place a printable glyph at the cursor and advance (wrapping/scrolling). | 236 | /// Place a printable glyph at the cursor and advance (wrapping/scrolling). |
| @@ -409,7 +415,7 @@ fn newline() { | |||
| 409 | /// only the newly-exposed rows. | 415 | /// only the newly-exposed rows. |
| 410 | const SCROLL_BATCH: usize = 16; | 416 | const SCROLL_BATCH: usize = 16; |
| 411 | static mut PENDING_SCROLLS: usize = 0; | 417 | static mut PENDING_SCROLLS: usize = 0; |
| 412 | static mut LAST_SYNC_TICK: u16 = 0; | 418 | static mut LAST_SYNC_TICK: u64 = 0; |
| 413 | 419 | ||
| 414 | /// Shift the logical grid up one row; defer the framebuffer shift. | 420 | /// Shift the logical grid up one row; defer the framebuffer shift. |
| 415 | fn scroll_up() { | 421 | fn scroll_up() { |
| @@ -424,7 +430,7 @@ fn scroll_up() { | |||
| 424 | // Batch: sync when enough scrolls piled up, or when the stream | 430 | // Batch: sync when enough scrolls piled up, or when the stream |
| 425 | // pauses (~40 ms at the 100 Hz timer) so interactive output still | 431 | // pauses (~40 ms at the 100 Hz timer) so interactive output still |
| 426 | // appears promptly. | 432 | // appears promptly. |
| 427 | const TICK_LIMIT: u16 = 4; | 433 | const TICK_LIMIT: u64 = 4; |
| 428 | let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); | 434 | let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); |
| 429 | if PENDING_SCROLLS >= SCROLL_BATCH || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT { | 435 | if PENDING_SCROLLS >= SCROLL_BATCH || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT { |
| 430 | sync_scroll(); | 436 | sync_scroll(); |
