Move NS16650A UART driver outside `arch`

This commit is contained in:
Ruihan Li 2025-12-22 21:07:53 +08:00 committed by Tate, Hongliang Tian
parent dee39e21d1
commit 81c2f8d4bd
9 changed files with 279 additions and 242 deletions

View File

@ -26,6 +26,7 @@ rto = "rto"
typ = "typ"
sigfault = "sigfault"
sems = "sems"
THRE = "THRE"
# Files with svg suffix are ignored to check.
[type.svg]

View File

@ -4,104 +4,62 @@
use spin::Once;
use crate::arch::{boot::DEVICE_TREE, mm::paddr_to_daddr};
use crate::{
arch::{boot::DEVICE_TREE, mm::paddr_to_daddr},
console::uart_ns16650a::{Ns16550aAccess, Ns16550aRegister, Ns16550aUart},
sync::{LocalIrqDisabled, SpinLock},
};
bitflags::bitflags! {
struct LineStatusRegisterFlags: u8 {
const DR = 1 << 0;
const OE = 1 << 1;
const PE = 1 << 2;
const FE = 1 << 3;
const BI = 1 << 4;
const TFE = 1 << 5;
const TE = 1 << 6;
const ERROR = 1 << 7;
}
/// The primary serial port, which serves as an early console.
pub static SERIAL_PORT: Once<SpinLock<Ns16550aUart<SerialAccess>, LocalIrqDisabled>> = Once::new();
/// Access to serial registers via I/O memory in LoongArch.
pub struct SerialAccess {
base: *mut u8,
}
/// A memory-mapped UART driver for LoongArch.
///
/// Reference: <https://loongson.github.io/LoongArch-Documentation/Loongson-3A5000-usermanual-EN.html#uart-controller>
struct Serial {
base_address: *mut u8,
}
unsafe impl Send for SerialAccess {}
unsafe impl Sync for SerialAccess {}
impl Serial {
const DATA_TRANSPORT_REGISTER_OFFSET: usize = 0;
const LINE_STATUS_REGISTER_OFFSET: usize = 5;
/// Creates a serial driver.
///
impl SerialAccess {
/// # Safety
///
/// The base address must be a valid UART base address.
const unsafe fn new(base_address: *mut u8) -> Self {
Self { base_address }
}
/// Sends data to the UART.
fn send(&self, c: u8) {
while !self
.line_status_register_flags()
.contains(LineStatusRegisterFlags::TFE)
{
core::hint::spin_loop();
}
// SAFETY: The safety requirements are upheld by the constructor.
unsafe {
self.base_address
.add(Self::DATA_TRANSPORT_REGISTER_OFFSET)
.write_volatile(c);
}
}
/// Receives data from the UART.
fn recv(&self) -> Option<u8> {
if self
.line_status_register_flags()
.contains(LineStatusRegisterFlags::DR)
{
// SAFETY: The safety requirements are upheld by the constructor.
Some(unsafe {
self.base_address
.add(Self::DATA_TRANSPORT_REGISTER_OFFSET)
.read_volatile()
})
} else {
None
}
}
fn line_status_register_flags(&self) -> LineStatusRegisterFlags {
// SAFETY: The safety requirements are upheld by the constructor.
let c = unsafe {
self.base_address
.add(Self::LINE_STATUS_REGISTER_OFFSET)
.read_volatile()
};
LineStatusRegisterFlags::from_bits_truncate(c)
/// The caller must ensure that the base address is a valid serial base address and that it has
/// exclusive ownership of the serial registers.
const unsafe fn new(base: *mut u8) -> Self {
Self { base }
}
}
// SAFETY: For correctness purposes, the UART registers should not be accessed concurrently.
// However, doing so will not cause memory safety violations.
unsafe impl Send for Serial {}
unsafe impl Sync for Serial {}
impl Ns16550aAccess for SerialAccess {
fn read(&self, reg: Ns16550aRegister) -> u8 {
// SAFETY: `self.base + reg` is a valid register of the serial port.
unsafe { core::ptr::read_volatile(self.base.add(reg as u8 as usize)) }
}
/// The console UART.
static CONSOLE_COM1: Once<Serial> = Once::new();
fn write(&mut self, reg: Ns16550aRegister, val: u8) {
// SAFETY: `self.base + reg` is a valid register of the serial port.
unsafe { core::ptr::write_volatile(self.base.add(reg as u8 as usize), val) };
}
}
/// Initializes the serial port.
pub(crate) fn init() {
let Some(base_address) = lookup_uart_base_address() else {
return;
};
// SAFETY: It is safe because the UART address is acquired from the device tree,
// and be mapped in DMW2.
CONSOLE_COM1.call_once(|| unsafe { Serial::new(paddr_to_daddr(base_address) as *mut u8) });
// SAFETY:
// 1. The base address is valid and correct because it is acquired from the device tree and
// mapped in DMW2.
// 2. FIXME: We should reserve the address region in `io_mem_allocator` to ensure the
// exclusive ownership.
let access = unsafe { SerialAccess::new(paddr_to_daddr(base_address) as *mut u8) };
let mut serial = Ns16550aUart::new(access);
serial.init();
SERIAL_PORT.call_once(move || SpinLock::new(serial));
}
// FIXME: We should reserve the address region in `io_mem_allocator`.
fn lookup_uart_base_address() -> Option<usize> {
let device_tree = DEVICE_TREE.get().unwrap();
let stdout_path = device_tree
@ -115,19 +73,3 @@ fn lookup_uart_base_address() -> Option<usize> {
None
}
}
/// Sends a byte on the serial port.
pub(crate) fn send(data: u8) {
// Note: It is the caller's responsibility to acquire the correct lock and ensure sequential
// access to the UART registers.
let Some(uart) = CONSOLE_COM1.get() else {
return;
};
match data {
b'\n' => {
uart.send(b'\r');
uart.send(b'\n');
}
c => uart.send(c),
}
}

View File

@ -2,10 +2,35 @@
//! The console I/O.
use core::fmt;
use spin::Once;
use crate::sync::{LocalIrqDisabled, SpinLock};
/// The primary serial port, which serves as an early console.
pub(crate) static SERIAL_PORT: Once<SpinLock<SbiSerial, LocalIrqDisabled>> =
Once::initialized(SpinLock::new(SbiSerial::new()));
/// A serial port that is implemented via RISC-V SBI.
pub(crate) struct SbiSerial {
_private: (),
}
impl SbiSerial {
const fn new() -> Self {
Self { _private: () }
}
}
impl fmt::Write for SbiSerial {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.as_bytes() {
sbi_rt::console_write_byte(*c);
}
Ok(())
}
}
/// Initializes the serial port.
pub(crate) fn init() {}
/// Sends a byte on the serial port.
pub(crate) fn send(data: u8) {
sbi_rt::console_write_byte(data);
}

View File

@ -3,4 +3,3 @@
//! Device-related APIs.
pub mod io_port;
pub mod serial;

View File

@ -1,92 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
//! A port-mapped UART. Copied from uart_16550.
#![expect(dead_code)]
use crate::{
arch::device::io_port::{ReadWriteAccess, WriteOnlyAccess},
io::IoPort,
};
/// A serial port.
///
/// Serial ports are a legacy communications port common on IBM-PC compatible computers.
/// Ref: <https://wiki.osdev.org/Serial_Ports>
pub struct SerialPort {
/// Data Register
data: IoPort<u8, ReadWriteAccess>,
/// Interrupt Enable Register
int_en: IoPort<u8, WriteOnlyAccess>,
/// First In First Out Control Register
fifo_ctrl: IoPort<u8, WriteOnlyAccess>,
/// Line control Register
line_ctrl: IoPort<u8, WriteOnlyAccess>,
/// Modem Control Register
modem_ctrl: IoPort<u8, WriteOnlyAccess>,
/// Line status Register
line_status: IoPort<u8, ReadWriteAccess>,
/// Modem Status Register
modem_status: IoPort<u8, ReadWriteAccess>,
}
impl SerialPort {
/// Creates a serial port.
///
/// # Safety
///
/// The caller must ensure that the base port is a valid serial base port and that it has
/// exclusive ownership of the serial ports.
pub const unsafe fn new(port: u16) -> Self {
// SAFETY: The safety is upheld by the caller.
unsafe {
Self {
data: IoPort::new(port),
int_en: IoPort::new(port + 1),
fifo_ctrl: IoPort::new(port + 2),
line_ctrl: IoPort::new(port + 3),
modem_ctrl: IoPort::new(port + 4),
line_status: IoPort::new(port + 5),
modem_status: IoPort::new(port + 6),
}
}
}
/// Initializes the serial port.
pub fn init(&self) {
// Disable interrupts
self.int_en.write(0x00);
// Enable DLAB
self.line_ctrl.write(0x80);
// Set maximum speed to 38400 bps by configuring DLL and DLM
self.data.write(0x03);
self.int_en.write(0x00);
// Disable DLAB and set data word length to 8 bits
self.line_ctrl.write(0x03);
// Enable FIFO, clear TX/RX queues and
// set interrupt watermark at 14 bytes
self.fifo_ctrl.write(0xC7);
// Mark data terminal ready, signal request to send
// and enable auxiliary output #2 (used as interrupt line for CPU)
self.modem_ctrl.write(0x0B);
// Enable interrupts
self.int_en.write(0x01);
}
/// Sends data to the data port
#[inline]
pub fn send(&self, data: u8) {
self.data.write(data);
}
/// Receives data from the data port
#[inline]
pub fn recv(&self) -> u8 {
self.data.read()
}
/// Gets line status
#[inline]
pub fn line_status(&self) -> u8 {
self.line_status.read()
}
}

View File

@ -2,42 +2,87 @@
//! The console I/O.
use super::device::serial::SerialPort;
use crate::io::reserve_io_port_range;
use spin::Once;
use x86_64::instructions::port::ReadWriteAccess;
bitflags::bitflags! {
struct LineSts: u8 {
const INPUT_FULL = 1;
const OUTPUT_EMPTY = 1 << 5;
use crate::{
console::uart_ns16650a::{Ns16550aAccess, Ns16550aRegister, Ns16550aUart},
io::{IoPort, reserve_io_port_range},
sync::{LocalIrqDisabled, SpinLock},
};
/// The primary serial port, which serves as an early console.
pub static SERIAL_PORT: Once<SpinLock<Ns16550aUart<SerialAccess>, LocalIrqDisabled>> =
Once::initialized(SpinLock::new(Ns16550aUart::new(
// SAFETY:
// 1. It is assumed that the serial port exists and can be accessed via the I/O registers.
// (FIXME: This needs to be confirmed by checking the ACPI table or using kernel
// parameters to obtain early information for building the early console.)
// 2. `reserve_io_port_range` guarantees exclusive ownership of the I/O registers.
unsafe { SerialAccess::new(0x3F8) },
)));
reserve_io_port_range!(0x3F8..0x400);
/// Access to serial registers via I/O ports in x86.
#[derive(Debug)]
pub struct SerialAccess {
data: IoPort<u8, ReadWriteAccess>,
int_en: IoPort<u8, ReadWriteAccess>,
fifo_ctrl: IoPort<u8, ReadWriteAccess>,
line_ctrl: IoPort<u8, ReadWriteAccess>,
modem_ctrl: IoPort<u8, ReadWriteAccess>,
line_stat: IoPort<u8, ReadWriteAccess>,
modem_stat: IoPort<u8, ReadWriteAccess>,
}
impl SerialAccess {
/// # Safety
///
/// The caller must ensure that the base port is a valid serial base port and that it has
/// exclusive ownership of the serial registers.
const unsafe fn new(port: u16) -> Self {
// SAFETY: The safety is upheld by the caller.
unsafe {
Self {
data: IoPort::new(port),
int_en: IoPort::new(port + 1),
fifo_ctrl: IoPort::new(port + 2),
line_ctrl: IoPort::new(port + 3),
modem_ctrl: IoPort::new(port + 4),
line_stat: IoPort::new(port + 5),
modem_stat: IoPort::new(port + 6),
}
}
}
}
static CONSOLE_COM1_PORT: SerialPort = unsafe { SerialPort::new(0x3F8) };
reserve_io_port_range!(0x3F8..0x400);
impl Ns16550aAccess for SerialAccess {
fn read(&self, reg: Ns16550aRegister) -> u8 {
match reg {
Ns16550aRegister::DataOrDivisorLo => self.data.read(),
Ns16550aRegister::IntEnOrDivisorHi => self.int_en.read(),
Ns16550aRegister::FifoCtrl => self.fifo_ctrl.read(),
Ns16550aRegister::LineCtrl => self.line_ctrl.read(),
Ns16550aRegister::ModemCtrl => self.modem_ctrl.read(),
Ns16550aRegister::LineStat => self.line_stat.read(),
Ns16550aRegister::ModemStat => self.modem_stat.read(),
}
}
fn write(&mut self, reg: Ns16550aRegister, val: u8) {
match reg {
Ns16550aRegister::DataOrDivisorLo => self.data.write(val),
Ns16550aRegister::IntEnOrDivisorHi => self.int_en.write(val),
Ns16550aRegister::FifoCtrl => self.fifo_ctrl.write(val),
Ns16550aRegister::LineCtrl => self.line_ctrl.write(val),
Ns16550aRegister::ModemCtrl => self.modem_ctrl.write(val),
Ns16550aRegister::LineStat => self.line_stat.write(val),
Ns16550aRegister::ModemStat => self.modem_stat.write(val),
}
}
}
/// Initializes the serial port.
pub(crate) fn init() {
CONSOLE_COM1_PORT.init();
}
fn line_sts() -> LineSts {
LineSts::from_bits_truncate(CONSOLE_COM1_PORT.line_status())
}
/// Sends a byte on the serial port.
pub(crate) fn send(data: u8) {
match data {
8 | 0x7F => {
while !line_sts().contains(LineSts::OUTPUT_EMPTY) {}
CONSOLE_COM1_PORT.send(8);
while !line_sts().contains(LineSts::OUTPUT_EMPTY) {}
CONSOLE_COM1_PORT.send(b' ');
while !line_sts().contains(LineSts::OUTPUT_EMPTY) {}
CONSOLE_COM1_PORT.send(8);
}
_ => {
while !line_sts().contains(LineSts::OUTPUT_EMPTY) {}
CONSOLE_COM1_PORT.send(data);
}
}
SERIAL_PORT.get().unwrap().lock().init();
}

View File

@ -2,35 +2,28 @@
//! Console output.
use core::fmt::{self, Arguments, Write};
use core::fmt::{Arguments, Write};
use crate::sync::{LocalIrqDisabled, SpinLock};
use crate::arch::serial::SERIAL_PORT;
struct Stdout;
impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
for &c in s.as_bytes() {
crate::arch::serial::send(c);
}
Ok(())
}
}
static STDOUT: SpinLock<Stdout, LocalIrqDisabled> = SpinLock::new(Stdout);
pub mod uart_ns16650a;
/// Prints formatted arguments to the console.
pub fn early_print(args: Arguments) {
let Some(serial) = SERIAL_PORT.get() else {
return;
};
#[cfg(target_arch = "x86_64")]
crate::arch::if_tdx_enabled!({
// Hold the lock to prevent the logs from interleaving.
let _guard = STDOUT.lock();
let _guard = serial.lock();
tdx_guest::print(args);
} else {
STDOUT.lock().write_fmt(args).unwrap();
serial.lock().write_fmt(args).unwrap();
});
#[cfg(not(target_arch = "x86_64"))]
STDOUT.lock().write_fmt(args).unwrap();
serial.lock().write_fmt(args).unwrap();
}
/// Prints to the console.

View File

@ -0,0 +1,124 @@
// SPDX-License-Identifier: MPL-2.0
//! NS16550A UART.
//!
//! This is used as an early console in x86 and LoongArch. It also exists on some (but not all)
//! RISC-V and ARM platforms.
//!
//! Reference: <https://bitsavers.trailing-edge.com/components/national/_appNotes/AN-0491.pdf>
use core::fmt;
use bitflags::bitflags;
/// Registers of a NS16550A UART.
#[repr(u8)]
#[derive(Clone, Copy, Debug)]
pub enum Ns16550aRegister {
/// Receive/Transmit Data Register or Divisor Latch Low.
DataOrDivisorLo,
/// Interrupt Enable Register or Divisor Latch High.
IntEnOrDivisorHi,
/// FIFO Control Register.
FifoCtrl,
/// Line Control Register.
LineCtrl,
/// Modem Control Register.
ModemCtrl,
/// Line Status Register.
LineStat,
/// Modem Status Register.
ModemStat,
}
/// A trait that provides methods to access NS16550A registers.
pub trait Ns16550aAccess {
/// Reads from an NS16550A register.
fn read(&self, reg: Ns16550aRegister) -> u8;
/// Writes to an NS16550A register.
fn write(&mut self, reg: Ns16550aRegister, val: u8);
}
/// An NS16550A UART.
#[derive(Debug)]
pub struct Ns16550aUart<A: Ns16550aAccess> {
access: A,
}
bitflags! {
struct LineStat: u8 {
/// Data ready (DR).
const DR = 1 << 0;
/// Transmitter holding register empty (THRE).
const THRE = 1 << 5;
}
}
impl<A: Ns16550aAccess> Ns16550aUart<A> {
/// Creates a new instance.
pub const fn new(access: A) -> Self {
Self { access }
}
/// Initializes the device.
///
/// This will set the baud rate to 115200 bps and configure IRQs to trigger when new data is
/// received.
pub fn init(&mut self) {
// Divisor Latch Access Bit.
const DLAB: u8 = 0x80;
// Baud Rate: 115200 bps / divisor
self.access.write(Ns16550aRegister::LineCtrl, DLAB);
self.access.write(Ns16550aRegister::DataOrDivisorLo, 0x01);
self.access.write(Ns16550aRegister::IntEnOrDivisorHi, 0x00);
// Line Control: 8-bit, no parity, one stop bit.
self.access.write(Ns16550aRegister::LineCtrl, 0x03);
// FIFO Control: Disabled.
self.access.write(Ns16550aRegister::FifoCtrl, 0x00);
// Modem Control: IRQs enabled, RTS/DSR set.
self.access.write(Ns16550aRegister::ModemCtrl, 0x0B);
// Interrupt Enable: IRQs on received data.
self.access.write(Ns16550aRegister::IntEnOrDivisorHi, 0x01);
}
/// Sends a byte.
///
/// If no room is available, it will spin until there is room.
pub fn send(&mut self, data: u8) {
while !self.line_stat().contains(LineStat::THRE) {
core::hint::spin_loop();
}
self.access.write(Ns16550aRegister::DataOrDivisorLo, data);
}
/// Receives a byte.
///
/// If no byte is available, it will return `None`.
pub fn recv(&mut self) -> Option<u8> {
if !self.line_stat().contains(LineStat::DR) {
return None;
}
Some(self.access.read(Ns16550aRegister::DataOrDivisorLo))
}
fn line_stat(&self) -> LineStat {
LineStat::from_bits_truncate(self.access.read(Ns16550aRegister::LineStat))
}
}
impl<A: Ns16550aAccess> fmt::Write for Ns16550aUart<A> {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.as_bytes() {
if *c == b'\n' {
self.send(b'\r');
}
self.send(*c);
}
Ok(())
}
}

View File

@ -21,7 +21,7 @@ use crate::{Error, prelude::*};
/// PORT.write(PORT.read() + 1)
/// }
/// ```
///
#[derive(Debug)]
pub struct IoPort<T, A> {
port: u16,
is_overlapping: bool,