1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//! Support for basic serial port access, including initialization, transmit, and receive.
//!
//! This is a near-standalone crate with very minimal dependencies and a basic feature set
//! intended for use during early Theseus boot up and initialization.
//! For a more featureful serial port driver, use the `serial_port` crate.
//!
//! # Notes
//! Some serial port drivers use special cases for transmitting some byte values,
//! specifically `0x08` and `0x7F`, which are ASCII "backspace" and "delete", respectively.
//! They do so by writing them as three distinct values (with proper busy waiting in between):
//! 1. `0x08`
//! 2. `0x20` (an ascii space character)
//! 3. `0x08` again.
//!
//! This isn't necessarily a bad idea, as it "clears out" whatever character was there before,
//! presumably to prevent rendering/display issues for a deleted character. 
//! But, this isn't required, and I personally believe it should be handled by a higher layer,
//! such as a shell or TTY program. 
//! We don't do anything like that here, in case a user of this crate wants to send binary data
//! across the serial port, rather than "smartly-interpreted" ASCII characters.
//!
//! On `x86_64`, this uses I/O ports to access the standard COM1 to COM4 serial ports. On
//! Aarch64 (ARMv8), the system is assumed to present serial ports through the PL011 standard
//! interface. The `arm_boards` crate contains the base addresses for each port.
//!
//! # Resources
//! * <https://en.wikibooks.org/wiki/Serial_Programming/8250_UART_Programming>
//! * <https://tldp.org/HOWTO/Modem-HOWTO-4.html>
//! * <https://wiki.osdev.org/Serial_Ports>
//! * <https://www.sci.muni.cz/docs/pc/serport.txt>

#![no_std]

use sync_irq::IrqSafeMutex;

#[cfg_attr(target_arch = "x86_64", path = "x86_64.rs")]
#[cfg_attr(target_arch = "aarch64", path = "aarch64.rs")]
mod arch;

pub use arch::*;

impl SerialPortAddress {
    /// Returns a reference to the static instance of this serial port.
    fn to_static_port(self) -> &'static IrqSafeMutex<TriState<SerialPort>> {
        match self {
            SerialPortAddress::COM1 => &COM1_SERIAL_PORT,
            SerialPortAddress::COM2 => &COM2_SERIAL_PORT,
            SerialPortAddress::COM3 => &COM3_SERIAL_PORT,
            SerialPortAddress::COM4 => &COM4_SERIAL_PORT,
        }
    }
}

/// This type is used to ensure that an object of type `T` is only initialized once,
/// but still allows for a caller to take ownership of the object `T`.
enum TriState<T> {
    Uninited,
    Inited(T),
    Taken,
}

impl<T> TriState<T> {
    fn take(&mut self) -> Option<T> {
        if let Self::Inited(_) = self {
            if let Self::Inited(v) = core::mem::replace(self, Self::Taken) {
                return Some(v);
            }
        }
        None
    }
}

// Serial ports cannot be reliably probed (discovered dynamically), thus,
// we ensure they are exposed safely as singletons through the below static instances.
static COM1_SERIAL_PORT: IrqSafeMutex<TriState<SerialPort>> = IrqSafeMutex::new(TriState::Uninited);
static COM2_SERIAL_PORT: IrqSafeMutex<TriState<SerialPort>> = IrqSafeMutex::new(TriState::Uninited);
static COM3_SERIAL_PORT: IrqSafeMutex<TriState<SerialPort>> = IrqSafeMutex::new(TriState::Uninited);
static COM4_SERIAL_PORT: IrqSafeMutex<TriState<SerialPort>> = IrqSafeMutex::new(TriState::Uninited);

/// Takes ownership of the [`SerialPort`] specified by the given [`SerialPortAddress`].
///
/// This function initializes the given serial port if it has not yet been initialized.
/// If the serial port has already been initialized and taken by another crate,
/// this returns `None`.
///
/// On aarch64, initializing a serial port includes mapping memory pages; Make sure to have
/// called `memory::init()` ahead of calling this function.
///
/// The returned [`SerialPort`] will be restored to this crate upon being dropped.
pub fn take_serial_port(
    serial_port_address: SerialPortAddress
) -> Option<SerialPort> {
    let sp = serial_port_address.to_static_port();
    let mut locked = sp.lock();
    if let TriState::Uninited = &*locked {
        *locked = TriState::Inited(SerialPort::new(serial_port_address as _));
    }
    locked.take()
}

/// The types of events that can trigger an interrupt on a serial port.
#[derive(Debug)]
#[repr(u8)]
pub enum SerialPortInterruptEvent {
    DataReceived     = 1 << 0,
    TransmitterEmpty = 1 << 1,
    ErrorOrBreak     = 1 << 2,
    StatusChange     = 1 << 3,
}