Skip to main content

ostd/arch/x86/
serial.rs

1// SPDX-License-Identifier: MPL-2.0
2
3//! The console I/O.
4
5use spin::Once;
6use x86_64::instructions::port::ReadWriteAccess;
7
8use crate::{
9    boot::EarlyCmdline,
10    console::uart_ns16650a::{Ns16550aAccess, Ns16550aRegister, Ns16550aUart},
11    io::{IoPort, reserve_io_port_range},
12    sync::{LocalIrqDisabled, SpinLock},
13};
14
15/// The primary serial port, which serves as an early console.
16pub static SERIAL_PORT: Once<SpinLock<Ns16550aUart<SerialAccess>, LocalIrqDisabled>> = Once::new();
17
18/// Access to serial registers via I/O ports in x86.
19#[derive(Debug)]
20pub struct SerialAccess {
21    data: IoPort<u8, ReadWriteAccess>,
22    int_en: IoPort<u8, ReadWriteAccess>,
23    fifo_ctrl: IoPort<u8, ReadWriteAccess>,
24    line_ctrl: IoPort<u8, ReadWriteAccess>,
25    modem_ctrl: IoPort<u8, ReadWriteAccess>,
26    line_stat: IoPort<u8, ReadWriteAccess>,
27    modem_stat: IoPort<u8, ReadWriteAccess>,
28}
29
30impl SerialAccess {
31    /// # Safety
32    ///
33    /// The caller must ensure that the base port is a valid serial base port and that it has
34    /// exclusive ownership of the serial registers.
35    const unsafe fn new(port: u16) -> Self {
36        // SAFETY: The safety is upheld by the caller.
37        unsafe {
38            Self {
39                data: IoPort::new(port),
40                int_en: IoPort::new(port + 1),
41                fifo_ctrl: IoPort::new(port + 2),
42                line_ctrl: IoPort::new(port + 3),
43                modem_ctrl: IoPort::new(port + 4),
44                line_stat: IoPort::new(port + 5),
45                modem_stat: IoPort::new(port + 6),
46            }
47        }
48    }
49
50    /// Detects whether a UART is present at the legacy COM1 serial port.
51    ///
52    /// Reference: <https://elixir.bootlin.com/linux/v7.2.2/source/drivers/tty/serial/8250/8250_port.c#L1094>
53    fn probe(&mut self) -> bool {
54        // A real UART echoes values written to its interrupt enable register, while
55        // an unbacked port reads 0xFF or 0x00 on every access. We perform the
56        // existence check by checking if the register works as expected.
57
58        // Some UARTs (e.g., the TL 16C754B) only allow IER[7:4] to be modified when
59        // an EFR bit is set, so only the low four bits are tested.
60        const IER_ALL_INTR: u8 = 0x0F;
61
62        let saved_ier = self.read(Ns16550aRegister::IntEnOrDivisorHi);
63
64        let is_ok1 = {
65            self.write(Ns16550aRegister::IntEnOrDivisorHi, 0x00);
66            self.read(Ns16550aRegister::IntEnOrDivisorHi) & IER_ALL_INTR == 0
67        };
68        let is_ok2 = {
69            self.write(Ns16550aRegister::IntEnOrDivisorHi, IER_ALL_INTR);
70            self.read(Ns16550aRegister::IntEnOrDivisorHi) & IER_ALL_INTR == IER_ALL_INTR
71        };
72
73        self.write(Ns16550aRegister::IntEnOrDivisorHi, saved_ier);
74
75        is_ok1 && is_ok2
76    }
77}
78
79impl Ns16550aAccess for SerialAccess {
80    fn read(&self, reg: Ns16550aRegister) -> u8 {
81        match reg {
82            Ns16550aRegister::DataOrDivisorLo => self.data.read(),
83            Ns16550aRegister::IntEnOrDivisorHi => self.int_en.read(),
84            Ns16550aRegister::FifoCtrl => self.fifo_ctrl.read(),
85            Ns16550aRegister::LineCtrl => self.line_ctrl.read(),
86            Ns16550aRegister::ModemCtrl => self.modem_ctrl.read(),
87            Ns16550aRegister::LineStat => self.line_stat.read(),
88            Ns16550aRegister::ModemStat => self.modem_stat.read(),
89        }
90    }
91
92    fn write(&mut self, reg: Ns16550aRegister, val: u8) {
93        match reg {
94            Ns16550aRegister::DataOrDivisorLo => self.data.write(val),
95            Ns16550aRegister::IntEnOrDivisorHi => self.int_en.write(val),
96            Ns16550aRegister::FifoCtrl => self.fifo_ctrl.write(val),
97            Ns16550aRegister::LineCtrl => self.line_ctrl.write(val),
98            Ns16550aRegister::ModemCtrl => self.modem_ctrl.write(val),
99            Ns16550aRegister::LineStat => self.line_stat.write(val),
100            Ns16550aRegister::ModemStat => self.modem_stat.write(val),
101        }
102    }
103}
104
105/// Initializes the serial port.
106///
107/// # Safety
108///
109/// This function should be called only once.
110pub(crate) unsafe fn init(early_cmdline: &EarlyCmdline) {
111    if !early_cmdline.has_early_console {
112        return;
113    }
114
115    // SAFETY:
116    // 1. The legacy COM1 serial port at 0x3F8 can be disabled via the command line.
117    //    (FIXME: This needs to be confirmed by checking the ACPI table or using more specific
118    //    kernel parameters to obtain early information for building the early console.)
119    // 2. `reserve_io_port_range` guarantees exclusive ownership of the I/O registers.
120    let mut access = unsafe { SerialAccess::new(0x3F8) };
121    if !access.probe() {
122        // The `IoPort`s in the access are backed by statically reserved ranges,
123        // but dropping the access would recycle these ranges through the port
124        // allocator, which is not initialized yet at this point.
125        core::mem::forget(access);
126        return;
127    }
128
129    let mut serial = Ns16550aUart::new(access);
130    serial.init();
131
132    SERIAL_PORT.call_once(|| SpinLock::new(serial));
133}
134reserve_io_port_range!(0x3F8..0x400);