Skip to main content

strat9_kernel/arch/x86_64/
serial.rs

1use core::{
2    fmt,
3    sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
4};
5use spin::Mutex;
6use uart_16550::SerialPort;
7
8/// Global serial port instance
9static SERIAL1: Mutex<SerialPort> = Mutex::new(unsafe { SerialPort::new(0x3F8) });
10
11// Normal runtime output is bounded and asynchronous. Boot/panic keep their
12// synchronous path; a slow or disconnected UART must not stall the console.
13const TX_CHUNK_LEN: usize = 256;
14const TX_QUEUE_LEN: usize = 256;
15#[derive(Clone, Copy)]
16struct TxChunk {
17    bytes: [u8; TX_CHUNK_LEN],
18    len: usize,
19}
20impl TxChunk {
21    const EMPTY: Self = Self {
22        bytes: [0; TX_CHUNK_LEN],
23        len: 0,
24    };
25}
26#[expect(
27    deprecated,
28    reason = "bounded diagnostics allow drops and delayed dequeue"
29)]
30static TX_QUEUE: heapless::mpmc::Queue<TxChunk, TX_QUEUE_LEN> = heapless::mpmc::Queue::new();
31static ASYNC_OUTPUT: AtomicBool = AtomicBool::new(false);
32static TX_FORMAT: Mutex<()> = Mutex::new(());
33static TX_DROPPED: AtomicUsize = AtomicUsize::new(0);
34
35struct QueuedWriter {
36    chunk: TxChunk,
37}
38impl QueuedWriter {
39    fn flush(&mut self) -> fmt::Result {
40        if self.chunk.len != 0 {
41            let chunk = core::mem::replace(&mut self.chunk, TxChunk::EMPTY);
42            if TX_QUEUE.enqueue(chunk).is_err() {
43                TX_DROPPED.fetch_add(1, Ordering::Relaxed);
44                return Err(fmt::Error);
45            }
46        }
47        Ok(())
48    }
49    fn byte(&mut self, byte: u8) -> fmt::Result {
50        self.chunk.bytes[self.chunk.len] = byte;
51        self.chunk.len += 1;
52        if self.chunk.len == TX_CHUNK_LEN {
53            self.flush()?;
54        }
55        Ok(())
56    }
57}
58impl fmt::Write for QueuedWriter {
59    fn write_str(&mut self, text: &str) -> fmt::Result {
60        for byte in text.bytes() {
61            // Match uart_16550::SerialPort::send's terminal translation.
62            match byte {
63                b'\n' => {
64                    self.byte(b'\r')?;
65                    self.byte(b'\n')?;
66                }
67                8 | 127 => {
68                    self.byte(8)?;
69                    self.byte(b' ')?;
70                    self.byte(8)?;
71                }
72                byte => self.byte(byte)?,
73            }
74        }
75        Ok(())
76    }
77}
78
79/// Number of runtime output chunks/format attempts dropped under saturation.
80pub fn dropped_output() -> usize {
81    TX_DROPPED.load(Ordering::Relaxed)
82}
83
84/// Drain bounded bursts without waiting for the UART or yielding under its lock.
85pub extern "C" fn serial_task_main() -> ! {
86    let mut chunk = TxChunk::EMPTY;
87    let mut offset = 0;
88    ASYNC_OUTPUT.store(true, Ordering::Release);
89    loop {
90        if !PANIC_IN_PROGRESS.load(Ordering::Relaxed) {
91            if let Some(mut port) = SERIAL1.try_lock() {
92                for _ in 0..TX_CHUNK_LEN {
93                    if offset == chunk.len {
94                        match TX_QUEUE.dequeue() {
95                            Some(next) => {
96                                chunk = next;
97                                offset = 0;
98                            }
99                            None => break,
100                        }
101                    }
102                    if port.try_send_raw(chunk.bytes[offset]).is_err() {
103                        break;
104                    }
105                    offset += 1;
106                }
107            }
108        }
109        crate::process::yield_task();
110    }
111}
112
113/// Fixed-size buffer for kernel cmdline (up to 2KB).
114/// SAFETY: Written once during early boot (single-threaded, IRQs disabled),
115/// then read-only. Safe for concurrent reads after initialization.
116static CMDLINE_BUF: [u8; 2048] = [0; 2048];
117static CMDLINE_LEN: AtomicUsize = AtomicUsize::new(0);
118static CMDLINE_READY: AtomicBool = AtomicBool::new(false);
119
120/// Flag indicating if the kernel is in a panic state.
121/// When true, serial output bypasses all locks to ensure messages are displayed.
122static PANIC_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
123static BOOT_LOG_PREFIX_ENABLED: AtomicBool = AtomicBool::new(false);
124static SERIAL_AT_LINE_START: AtomicBool = AtomicBool::new(true);
125
126/// Raw spinlock for `_print_force` to prevent multi-core character interleaving.
127/// Uses a ticket-style test-and-set: 0 = free, 1 = locked.
128///
129/// **Interrupt safety**: `force_lock_acquire` saves and disables IRQs before
130/// spinning, and `force_lock_release` restores them. This prevents a nested
131/// timer IRQ on the same CPU from trying to acquire `FORCE_LOCK` while it is
132/// already held by an outer `serial_force_println!` call, which would deadlock.
133static FORCE_LOCK: AtomicU8 = AtomicU8::new(0);
134
135#[inline(always)]
136fn force_lock_acquire() -> u64 {
137    // Disable IRQs before spinning to prevent a timer IRQ on this CPU from
138    // re-entering _print_force while FORCE_LOCK is held (nested IRQ deadlock).
139    let saved = crate::arch::x86_64::save_flags_and_cli();
140    while FORCE_LOCK
141        .compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
142        .is_err()
143    {
144        core::hint::spin_loop();
145    }
146    saved
147}
148
149#[inline(always)]
150fn force_lock_release(saved_flags: u64) {
151    FORCE_LOCK.store(0, Ordering::Release);
152    // Restore RFLAGS (re-enables IRQs if they were enabled before the acquire).
153    crate::arch::x86_64::restore_flags(saved_flags);
154}
155
156const ANSI_RESET: &str = "\x1b[0m";
157const ANSI_RED: &str = "\x1b[31m";
158const ANSI_GREEN: &str = "\x1b[32m";
159const ANSI_VIOLET: &str = "\x1b[35m";
160const TOKEN_BUF_CAP: usize = 64;
161
162/// Signal that the kernel has entered an emergency panic state.
163/// Write one raw byte to COM1 (used by debug breadcrumbs).
164pub fn putc(byte: u8) {
165    // SAFETY: COM1 port writes are always safe in kernel context.
166    unsafe {
167        core::arch::asm!("out 0xe9, al", in("al") byte, options(nomem, nostack));
168    }
169}
170
171pub fn enter_emergency_mode() {
172    PANIC_IN_PROGRESS.store(true, Ordering::SeqCst);
173}
174
175/// Enable or disable Linux-style boot timestamps at the beginning of each line.
176pub fn set_boot_log_prefix_enabled(enabled: bool) {
177    BOOT_LOG_PREFIX_ENABLED.store(enabled, Ordering::SeqCst);
178    SERIAL_AT_LINE_START.store(true, Ordering::SeqCst);
179}
180
181/// Returns whether token char.
182#[inline]
183fn is_token_char(b: u8) -> bool {
184    b.is_ascii_alphanumeric() || b == b'_'
185}
186
187/// Returns whether hex word.
188#[inline]
189fn is_hex_word(s: &str) -> bool {
190    if s.len() <= 2 {
191        return false;
192    }
193    let bytes = s.as_bytes();
194    if bytes[0] != b'0' || (bytes[1] != b'x' && bytes[1] != b'X') {
195        return false;
196    }
197    bytes[2..].iter().all(|b| b.is_ascii_hexdigit())
198}
199
200struct AnsiStylingWriter<'a, W: fmt::Write> {
201    inner: &'a mut W,
202    in_escape: bool,
203    token_buf: [u8; TOKEN_BUF_CAP],
204    token_len: usize,
205    token_passthrough: bool,
206}
207
208impl<'a, W: fmt::Write> AnsiStylingWriter<'a, W> {
209    /// Creates a new instance.
210    fn new(inner: &'a mut W) -> Self {
211        Self {
212            inner,
213            in_escape: false,
214            token_buf: [0u8; TOKEN_BUF_CAP],
215            token_len: 0,
216            token_passthrough: false,
217        }
218    }
219
220    /// Performs the flush token operation.
221    fn flush_token(&mut self) -> fmt::Result {
222        if self.token_len == 0 {
223            return Ok(());
224        }
225        let token = unsafe { core::str::from_utf8_unchecked(&self.token_buf[..self.token_len]) };
226        if token == "PASS" {
227            self.inner.write_str(ANSI_GREEN)?;
228            self.inner.write_str(token)?;
229            self.inner.write_str(ANSI_RESET)?;
230        } else if token == "FAIL" {
231            self.inner.write_str(ANSI_RED)?;
232            self.inner.write_str(token)?;
233            self.inner.write_str(ANSI_RESET)?;
234        } else if is_hex_word(token) {
235            self.inner.write_str(ANSI_VIOLET)?;
236            self.inner.write_str(token)?;
237            self.inner.write_str(ANSI_RESET)?;
238        } else {
239            self.inner.write_str(token)?;
240        }
241        self.token_len = 0;
242        Ok(())
243    }
244
245    /// Writes byte raw.
246    fn write_byte_raw(&mut self, b: u8) -> fmt::Result {
247        self.inner.write_char(b as char)
248    }
249
250    /// Performs the finish operation.
251    fn finish(&mut self) -> fmt::Result {
252        self.flush_token()
253    }
254}
255
256impl<W: fmt::Write> fmt::Write for AnsiStylingWriter<'_, W> {
257    /// Writes str.
258    fn write_str(&mut self, s: &str) -> fmt::Result {
259        for &b in s.as_bytes() {
260            if self.in_escape {
261                self.write_byte_raw(b)?;
262                if (b as char).is_ascii_alphabetic() {
263                    self.in_escape = false;
264                }
265                continue;
266            }
267
268            if b == 0x1b {
269                self.flush_token()?;
270                self.token_passthrough = false;
271                self.in_escape = true;
272                self.write_byte_raw(b)?;
273                continue;
274            }
275
276            if is_token_char(b) {
277                if self.token_passthrough {
278                    self.write_byte_raw(b)?;
279                    continue;
280                }
281                if self.token_len < TOKEN_BUF_CAP {
282                    self.token_buf[self.token_len] = b;
283                    self.token_len += 1;
284                } else {
285                    self.flush_token()?;
286                    self.token_passthrough = true;
287                    self.write_byte_raw(b)?;
288                }
289            } else {
290                self.flush_token()?;
291                self.token_passthrough = false;
292                self.write_byte_raw(b)?;
293            }
294        }
295        Ok(())
296    }
297}
298
299struct BootPrefixWriter<'a, W: fmt::Write> {
300    inner: &'a mut W,
301    line_start: bool,
302    prefix_enabled: bool,
303}
304
305impl<'a, W: fmt::Write> BootPrefixWriter<'a, W> {
306    fn new(inner: &'a mut W) -> Self {
307        Self {
308            inner,
309            line_start: SERIAL_AT_LINE_START.load(Ordering::Relaxed),
310            prefix_enabled: BOOT_LOG_PREFIX_ENABLED.load(Ordering::Relaxed),
311        }
312    }
313
314    fn write_prefix(&mut self) -> fmt::Result {
315        if !self.prefix_enabled {
316            return Ok(());
317        }
318        let elapsed_us = crate::arch::x86_64::boot_timestamp::elapsed_us();
319        let secs = elapsed_us / 1_000_000;
320        let micros = elapsed_us % 1_000_000;
321        write!(self.inner, "[{:>5}.{:06}] ", secs, micros)
322    }
323
324    fn finish(&mut self) {
325        SERIAL_AT_LINE_START.store(self.line_start, Ordering::Relaxed);
326    }
327}
328
329impl<W: fmt::Write> fmt::Write for BootPrefixWriter<'_, W> {
330    fn write_str(&mut self, s: &str) -> fmt::Result {
331        for ch in s.chars() {
332            if self.line_start && ch != '\n' {
333                self.write_prefix()?;
334                self.line_start = false;
335            }
336            self.inner.write_char(ch)?;
337            if ch == '\n' {
338                self.line_start = true;
339            }
340        }
341        Ok(())
342    }
343}
344
345/// Initialize the serial port
346pub fn init() {
347    unsafe {
348        core::arch::asm!("out 0xe9, al", in("al") b'x', options(nomem, nostack));
349    }
350    SERIAL1.lock();
351    unsafe {
352        core::arch::asm!("out 0xe9, al", in("al") b'y', options(nomem, nostack));
353    }
354    SERIAL1.lock().init();
355    unsafe {
356        core::arch::asm!("out 0xe9, al", in("al") b'z', options(nomem, nostack));
357    }
358}
359
360/// Parse kernel cmdline from UEFI bootloader boot arguments.
361///
362/// `ptr` is a pointer to a null-terminated C string provided by the bootloader.
363/// `len` is the length of the cmdline string (including the null terminator).
364///
365/// This function:
366/// - Stores the cmdline globally for `/proc/cmdline` access.
367/// - Detects `console=ttyS0,baud` parameters and logs the configuration.
368pub unsafe fn parse_cmdline(ptr: u64, len: u64) {
369    if ptr == 0 || len == 0 {
370        return;
371    }
372
373    // Convert C string to Rust &str and copy into static buffer.
374    let cstr = core::ffi::CStr::from_ptr(ptr as *const core::ffi::c_char);
375    let cmdline = cstr.to_str().unwrap_or("");
376
377    let copy_len = cmdline.len().min(2047);
378    // SAFETY: Single-threaded early boot, IRQs disabled. No concurrent access.
379    let buf_ptr = CMDLINE_BUF.as_ptr() as *mut u8;
380    core::ptr::copy_nonoverlapping(cmdline.as_ptr(), buf_ptr, copy_len);
381    CMDLINE_LEN.store(copy_len, Ordering::Release);
382    CMDLINE_READY.store(true, Ordering::Release);
383
384    // Parse console parameters.
385    let cmdline_str =
386        core::str::from_utf8_unchecked(core::slice::from_raw_parts(buf_ptr, copy_len));
387
388    let mut has_serial_console = false;
389    let mut baud: Option<u32> = None;
390
391    let mut pos = 0;
392    while pos < cmdline_str.len() {
393        while pos < cmdline_str.len() && cmdline_str.as_bytes()[pos].is_ascii_whitespace() {
394            pos += 1;
395        }
396        if pos >= cmdline_str.len() {
397            break;
398        }
399        let start = pos;
400        while pos < cmdline_str.len() && !cmdline_str.as_bytes()[pos].is_ascii_whitespace() {
401            pos += 1;
402        }
403        let token = &cmdline_str[start..pos];
404
405        if let Some(value) = token.strip_prefix("console=") {
406            if value.starts_with("ttyS0") {
407                has_serial_console = true;
408                if let Some((_, baud_str)) = value.split_once(',') {
409                    if let Ok(b) = baud_str.parse::<u32>() {
410                        baud = Some(b);
411                    }
412                }
413            }
414        }
415    }
416
417    if has_serial_console {
418        if let Some(b) = baud {
419            crate::serial_force_println!("[cmdline] console=ttyS0,{}", b);
420        } else {
421            crate::serial_force_println!("[cmdline] console=ttyS0 (115200 baud)");
422        }
423    } else {
424        crate::serial_force_println!("[cmdline] no serial console detected");
425    }
426}
427
428/// Returns the stored kernel cmdline for `/proc/cmdline`.
429pub fn get_cmdline() -> &'static str {
430    if !CMDLINE_READY.load(Ordering::Acquire) {
431        return "";
432    }
433    let len = CMDLINE_LEN.load(Ordering::Acquire);
434    // SAFETY: CMDLINE_READY guarantees CMDLINE_BUF has been written.
435    unsafe { core::str::from_utf8_unchecked(&CMDLINE_BUF[..len]) }
436}
437
438/// Print to serial port
439#[doc(hidden)]
440pub fn _print(args: fmt::Arguments) {
441    use core::fmt::Write;
442
443    // Gate all serial output at the function body level.
444    if !crate::debug_cfg::SERIAL_ENABLED {
445        return;
446    }
447
448    // Check if we are in emergency panic mode.
449    if PANIC_IN_PROGRESS.load(Ordering::Relaxed) {
450        // SAFETY: In emergency mode, we bypass the mutex to ensure output.
451        let mut port = unsafe { SerialPort::new(0x3F8) };
452        let _ = port.write_fmt(args);
453        return;
454    }
455
456    // In quiet mode, suppress all normal serial output.
457    if crate::debug_cfg::is_quiet() {
458        return;
459    }
460
461    if ASYNC_OUTPUT.load(Ordering::Acquire) {
462        // Nonblocking even if interrupted while another formatter is active.
463        let Some(_format_guard) = TX_FORMAT.try_lock() else {
464            TX_DROPPED.fetch_add(1, Ordering::Relaxed);
465            return;
466        };
467        let mut queued = QueuedWriter {
468            chunk: TxChunk::EMPTY,
469        };
470        let mut prefix_writer = BootPrefixWriter::new(&mut queued);
471        let mut writer = AnsiStylingWriter::new(&mut prefix_writer);
472        let _ = writer.write_fmt(args);
473        let _ = writer.finish();
474        prefix_writer.finish();
475        let _ = queued.flush();
476        return;
477    }
478
479    // Early boot: no worker exists yet. Never block acquiring the port lock.
480    if let Some(mut port) = SERIAL1.try_lock() {
481        let mut prefix_writer = BootPrefixWriter::new(&mut *port);
482        let mut writer = AnsiStylingWriter::new(&mut prefix_writer);
483        let _ = writer.write_fmt(args);
484        let _ = writer.finish();
485        prefix_writer.finish();
486    }
487}
488
489/// Print to serial port bypassing the shared mutex.
490///
491/// Uses a dedicated raw spinlock (with IRQs disabled) so that multiple CPUs
492/// cannot interleave their output at the character level, and so that a timer
493/// IRQ firing on the same CPU while this function is in progress cannot cause
494/// a deadlock by trying to re-acquire `FORCE_LOCK`.
495#[doc(hidden)]
496pub fn _print_force(args: fmt::Arguments) {
497    use core::fmt::Write;
498
499    // Gate all serial output at the function body level.
500    if !crate::debug_cfg::SERIAL_ENABLED {
501        return;
502    }
503
504    // Check if we are in emergency panic mode.
505    if PANIC_IN_PROGRESS.load(Ordering::Relaxed) {
506        // SAFETY: In emergency mode, we bypass the lock to ensure output.
507        let mut port = unsafe { SerialPort::new(0x3F8) };
508        let _ = port.write_fmt(args);
509        return;
510    }
511
512    // Acquire the raw force-lock (saves + clears IF, then spins until free).
513    let saved_flags = force_lock_acquire();
514    // SAFETY: We hold `FORCE_LOCK` with IRQs disabled, giving exclusive UART access.
515    let mut port = unsafe { SerialPort::new(0x3F8) };
516    let mut prefix_writer = BootPrefixWriter::new(&mut port);
517    let _ = prefix_writer.write_fmt(args);
518    prefix_writer.finish();
519    // Release lock and restore RFLAGS (re-enables IRQs if they were on before).
520    force_lock_release(saved_flags);
521}
522
523/// Print to serial port
524#[macro_export]
525macro_rules! serial_print {
526    ($($arg:tt)*) => {
527        if $crate::debug_cfg::SERIAL_ENABLED {
528            $crate::arch::x86_64::serial::_print(format_args!($($arg)*))
529        }
530    };
531}
532
533/// Print to serial port with newline
534#[macro_export]
535macro_rules! serial_println {
536    () => ($crate::serial_print!("\n"));
537    ($($arg:tt)*) => {
538        if $crate::debug_cfg::SERIAL_ENABLED {
539            $crate::arch::x86_64::serial::_print(format_args!("{}\n", format_args!($($arg)*)))
540        }
541    };
542}
543
544/// Print to serial port with newline, bypassing the shared mutex.
545#[macro_export]
546macro_rules! serial_force_println {
547    () => {
548        if $crate::debug_cfg::SERIAL_ENABLED {
549            $crate::arch::x86_64::serial::_print_force(format_args!("\n"))
550        }
551    };
552    ($($arg:tt)*) => {
553        if $crate::debug_cfg::SERIAL_ENABLED {
554            $crate::arch::x86_64::serial::_print_force(format_args!("{}\n", format_args!($($arg)*)))
555        }
556    };
557}