diff --git a/CHANGELOG.md b/CHANGELOG.md index 719025b20..f136ed3bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,13 @@ hashes are given where a change is easiest to understand by reading the commit. the Indigo2's second controller fold pending CHD diffs on a clean exit as the first controller always did. +### Serial ports + +- The O2 and Octane serial ports share one 16550A core ported from QEMU. On the + O2 this adds the receive FIFO with trigger levels and the character timeout; + O2 snapshots from before are refused (snapshot schema 5). +- Serial host backends moved out of the Z85C30 into `src/dev/serial_host.rs`. + ### Host efficiency on small machines - **Threads that have nothing to do now sleep in milliseconds, not diff --git a/HACKING.md b/HACKING.md index 20023ffaf..6edcaa251 100644 --- a/HACKING.md +++ b/HACKING.md @@ -158,6 +158,15 @@ channel), `FifoDevice` (serial byte streams), `InterruptController`/`IrqLine` (below), `GfxDisplay` (a graphics board's screen, for the window and screenshots), `CpuDevice` (what `Machine` holds for the CPU). +Serial lines have two halves. A UART model owns the registers: the Indy's +`Z85c30`, and `Ns16550` (`src/dev/ns16550.rs`), the 16550A core that the O2's +`MaceUart` (lane decode, interrupt line, monitor) and the Octane's IOC3 wrap. +The host end is a `SerialBackend` from `src/dev/serial_host.rs`, which the +machine constructors pick with `console_backends`: the CI backend, a TCP +console, or a log tee. A UART's transmit is instant and its receive side +pulls from the backend with back-pressure (`pump`), so nothing in the model +needs a thread of its own beyond the service loop's call. + ### The physical address map `Physical` (`src/physical.rs`) is the physical bus, and it belongs to no diff --git a/src/ci.rs b/src/ci.rs index 9b04aed54..0bd3bb547 100644 --- a/src/ci.rs +++ b/src/ci.rs @@ -22,7 +22,7 @@ use serde_json::Value; use crate::workstation::{MachineSlot, Workstation}; use crate::gfx_display::GfxDisplay; -use crate::dev::z85c30::CiSerialBackend; +use crate::dev::serial_host::CiSerialBackend; /// Set at `start_server`; consulted by `quit` so the socket file is cleaned up /// before `std::process::exit` (which skips Drop). diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index 22fb5aa92..d48063087 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -57,7 +57,8 @@ use crate::dev::ioc3_port::Ioc3EthPort; use crate::dev::ps2::Ps2Controller; use crate::dev::w1; use crate::dev::w1::sgi::{self, BoardRecord}; -use crate::dev::z85c30::{null_serial_backend, SerialBackend}; +use crate::dev::ns16550::Ns16550; +use crate::dev::serial_host::SerialBackend; use crate::irq::{InterruptController, IrqLine}; use crate::pci::{Bar, PciBusDevice, PciConfig, PciDma, PciDmaStatus, PCI_INTA}; use crate::hptimer::{TimerId, TimerManager, TimerReturn}; @@ -144,9 +145,7 @@ pub mod int { mod uart_reg { pub const RBR_THR: u32 = 0; - pub const IER: u32 = 1; pub const IIR_FCR: u32 = 2; - pub const LCR: u32 = 3; pub const MCR: u32 = 4; pub const LSR: u32 = 5; pub const MSR: u32 = 6; @@ -157,7 +156,6 @@ const LSR_OE: u8 = 0x02; const LSR_PE: u8 = 0x04; const LSR_FE: u8 = 0x08; const LSR_BI: u8 = 0x10; -const LSR_THRE_TEMT: u8 = 0x60; const MCR_DTR: u8 = 0x01; const MCR_RTS: u8 = 0x02; const MCR_OUT1: u8 = 0x04; @@ -167,27 +165,22 @@ const MCR_AFE: u8 = 0x20; const MSR_DCTS: u8 = 0x01; const MSR_CTS: u8 = 0x10; const MSR_DCD: u8 = 0x80; -const LCR_DLAB: u8 = 0x80; -const UART_FIFO: usize = 16; const LSR_TEST_MASK: u8 = 0x7f; const MSR_TEST_MASK: u8 = 0x0f; +/// The reference clock of the UART crystal: the pre-divider divides it, and +/// the divisor latch divides the result by 16 again. +const UART_CRYSTAL_HZ: u32 = 22_118_400; -/// A 16550 as IOC3 fits it. Transmit is instantaneous (the byte goes to the +/// A 16550 as IOC3 fits it: the shared core (`ns16550.rs`) with the chip's +/// own additions around it. Transmit is instantaneous (the byte goes to the /// backend inside the write), so the transmitter always reads empty. +/// +/// The additions: the auto-flow bit in modem control, which the core's modem +/// control masks away; factory-test writes to line and modem status; and the +/// scratch register doubling as the clock pre-divider while the divisor latch +/// is open. Ported from hw/misc/sgi-ioc3.c in jamesbraid/qemu-sgi-origami. struct Uart { - backend: Arc, - rx: VecDeque, - ier: u8, - fifo: bool, - lcr: u8, - mcr: u8, - scr: u8, - dll: u8, - dlm: u8, - lsr_err: u8, - msr_delta: u8, - msr_prev: u8, - thr_int: bool, + core: Ns16550, // IOC3 specifics, kept beside the 16550. predivider: u8, mcr_afe: u8, @@ -197,42 +190,11 @@ struct Uart { impl Uart { fn new() -> Uart { - let mut u = Uart { - backend: null_serial_backend(), - rx: VecDeque::new(), - ier: 0, - fifo: false, - lcr: 0, - mcr: MCR_OUT2, - scr: 0, - dll: 0x0c, - dlm: 0, - lsr_err: 0, - msr_delta: 0, - msr_prev: 0xb0, - thr_int: false, - predivider: 0, - mcr_afe: 0, - lsr_test: 0, - msr_test: 0, - }; - u.reset(); - u + Uart { core: Ns16550::new(115200), predivider: 0, mcr_afe: 0, lsr_test: 0, msr_test: 0 } } fn reset(&mut self) { - self.rx.clear(); - self.ier = 0; - self.fifo = false; - self.lcr = 0; - self.mcr = MCR_OUT2; - self.scr = 0; - self.dll = 0x0c; - self.dlm = 0; - self.lsr_err = 0; - self.msr_delta = 0; - self.msr_prev = self.msr_levels(); - self.thr_int = false; + self.core.reset(); self.mcr_afe = 0; self.lsr_test = 0; self.msr_test = 0; @@ -240,116 +202,25 @@ impl Uart { // specified for it. } - fn dlab(&self) -> bool { - self.lcr & LCR_DLAB != 0 - } - fn loopback(&self) -> bool { - self.mcr & MCR_LOOP != 0 - } - - /// The modem status input levels (bits 4..7). The port comes up with - /// carrier and clear-to-send asserted; in loopback the outputs wire back. - fn msr_levels(&self) -> u8 { - if self.loopback() { - ((self.mcr & (MCR_OUT1 | MCR_OUT2)) << 4) - | ((self.mcr & MCR_RTS) << 3) - | ((self.mcr & MCR_DTR) << 5) - } else { - 0xb0 - } - } - - fn lsr_now(&self) -> u8 { - let mut v = LSR_THRE_TEMT | self.lsr_err; - if !self.rx.is_empty() { - v |= LSR_DR; - } - v - } - - fn iir(&self) -> u8 { - let id = if self.ier & 4 != 0 && self.lsr_err & (LSR_OE | LSR_PE | LSR_FE | LSR_BI) != 0 { - 0x06 - } else if self.ier & 1 != 0 && !self.rx.is_empty() { - 0x04 - } else if self.ier & 2 != 0 && self.thr_int { - 0x02 - } else if self.ier & 8 != 0 && self.msr_delta & 0x0f != 0 { - 0x00 - } else { - 0x01 - }; - id | if self.fifo { 0xc0 } else { 0 } + self.core.loopback() } /// The interrupt line: low IIR bit clear. fn irq(&self) -> bool { - self.iir() & 1 == 0 - } - - fn push_rx(&mut self, b: u8) { - let cap = if self.fifo { UART_FIFO } else { 1 }; - if self.rx.len() >= cap { - self.lsr_err |= LSR_OE; - } else { - self.rx.push_back(b); - } + self.core.irq() } - /// Move whatever the host end has typed into the receive FIFO. - /// Returns whether anything arrived. + /// Move whatever the host end has typed into the receive FIFO and let + /// the receive timeout run out. Returns whether anything changed. fn pump(&mut self) -> bool { - let cap = if self.fifo { UART_FIFO } else { 1 }; - let mut moved = false; - while self.rx.len() < cap { - match self.backend.recv_byte() { - Ok(b) => { - self.rx.push_back(b); - moved = true; - } - Err(_) => break, - } - } - moved + let moved = self.core.pump(); + self.core.check_timeout(Instant::now()) || moved } /// The 16550 proper: what the ring engine sees. fn read_core(&mut self, r: u32) -> u8 { - match r { - uart_reg::RBR_THR => { - if self.dlab() { - self.dll - } else { - self.pump(); - self.rx.pop_front().unwrap_or(0) - } - } - uart_reg::IER => if self.dlab() { self.dlm } else { self.ier }, - uart_reg::IIR_FCR => { - let v = self.iir(); - if v & 0x0f == 0x02 { - self.thr_int = false; - } - v - } - uart_reg::LCR => self.lcr, - uart_reg::MCR => self.mcr, - uart_reg::LSR => { - self.pump(); - // Reading clears the error latches. - let v = self.lsr_now(); - self.lsr_err = 0; - v - } - uart_reg::MSR => { - let v = self.msr_levels() | self.msr_delta; - self.msr_delta = 0; - self.msr_prev = self.msr_levels(); - v - } - _ => self.scr, - } + self.core.read(r) } /// A guest's byte-wide read, with the chip's additions: the auto-flow @@ -374,67 +245,20 @@ impl Uart { fn write(&mut self, r: u32, v: u8) { match r { - uart_reg::RBR_THR => { - if self.dlab() { - self.dll = v; - } else { - self.transmit(v); - } - } - uart_reg::IER => { - if self.dlab() { - self.dlm = v; - } else { - let was = self.ier; - self.ier = v & 0x0f; - // Enabling the transmit interrupt with the holding - // register empty raises it at once. - if was & 2 == 0 && self.ier & 2 != 0 { - self.thr_int = true; - } - } - } - uart_reg::IIR_FCR => { - self.fifo = v & 1 != 0; - if v & 2 != 0 { - self.rx.clear(); - } - } - uart_reg::LCR => self.lcr = v, - uart_reg::MCR => { - self.mcr_afe = v & MCR_AFE; - self.mcr = v & 0x1f; - let now = self.msr_levels(); - let changed = (now ^ self.msr_prev) >> 4; - // CTS, DSR change bits straight; RI only on its trailing - // edge; carrier changes. - let mut delta = changed & 0x0b; - if self.msr_prev & 0x40 != 0 && now & 0x40 == 0 { - delta |= 0x04; - } - self.msr_delta |= delta; - self.msr_prev = now; - } + uart_reg::MCR => self.mcr_afe = v & MCR_AFE, uart_reg::LSR => self.lsr_test = v & LSR_TEST_MASK, uart_reg::MSR => self.msr_test = v & MSR_TEST_MASK, - _ => { - self.scr = v; + uart_reg::SCR => { // Scratch writes with the divisor latch up program the clock // pre-divider; twice the divide wanted. 0 and 1 mean nothing. - if self.dlab() && v / 2 != 0 { + if self.core.dlab() && v / 2 != 0 { self.predivider = v; + self.core.set_baudbase(UART_CRYSTAL_HZ / (v / 2) as u32 / 16); } } + _ => {} } - } - - fn transmit(&mut self, b: u8) { - if self.loopback() { - self.push_rx(b); - } else { - self.backend.send_byte(b); - } - self.thr_int = true; + self.core.write(r, v); } /// Modem status as the ring engine reads it with flow control looped back. @@ -1414,9 +1238,9 @@ impl St { /// A passive mirror of the 16550's lines; no read side effects. fn sdma_shadow(&self, p: usize) -> u32 { let u = &self.uart[p]; - let lsr = u.lsr_now(); - let mcr = u.mcr; - let mut msr = if u.loopback() { u.msr_levels() } else { 0xb0 | u.msr_delta }; + let lsr = u.core.lsr(); + let mcr = u.core.mcr(); + let mut msr = u.core.msr(); if u.flow_cts(self.sdma_ctl(p)) { msr |= MSR_CTS; } @@ -2590,7 +2414,7 @@ impl Ioc3 { /// Wire the host end of serial port `port` (0 is the console): a TCP /// listener, a log tee, or nothing. pub fn set_serial_backend(&self, port: usize, backend: Arc) { - self.sh.st.lock().uart[port].backend = backend; + self.sh.st.lock().uart[port].core.set_backend(backend); } /// Where parallel-port output goes. Without one it is dropped. No machine diff --git a/src/dev/ioc3_tests.rs b/src/dev/ioc3_tests.rs index 5cd8e1299..c758a5c20 100644 --- a/src/dev/ioc3_tests.rs +++ b/src/dev/ioc3_tests.rs @@ -192,6 +192,7 @@ fn the_scratch_register_programs_the_predivider_under_dlab() { r.w8(reg::SERIAL_A + 7, 6); assert_eq!(r.r8(reg::SERIAL_A + 7), 6, "reads back what was written"); assert_eq!(r.c.sh.st.lock().uart[0].predivider, 6); + assert_eq!(r.c.sh.st.lock().uart[0].core.baudbase(), 22_118_400 / 3 / 16, "the divisor's clock follows it"); // Written 0 or 1 means nothing. r.w8(reg::SERIAL_A + 7, 1); assert_eq!(r.c.sh.st.lock().uart[0].predivider, 6); @@ -1359,9 +1360,9 @@ fn stop_joins_the_threads_and_removes_the_timers() { // Typed input waits in the backend while stopped. r.wire[0].inp.lock().unwrap().extend(b"x"); std::thread::sleep(Duration::from_millis(20)); - assert!(r.c.sh.st.lock().uart[0].rx.is_empty()); + assert!(r.c.sh.st.lock().uart[0].core.rx_pending() == 0); r.c.start(); - assert!(within_two_seconds(|| !r.c.sh.st.lock().uart[0].rx.is_empty())); + assert!(within_two_seconds(|| r.c.sh.st.lock().uart[0].core.rx_pending() != 0)); assert!(r.c.sh.armed.lock()[K_INT_OUT].is_some(), "start arms the deadlines the chip holds"); r.c.stop(); } diff --git a/src/dev/mace.rs b/src/dev/mace.rs index 609d52abc..460036b4b 100644 --- a/src/dev/mace.rs +++ b/src/dev/mace.rs @@ -3,7 +3,7 @@ //! and hands the access to the block it names, as HPC3 does for its //! children on the Indigo2. The blocks themselves are in their own files //! (`mace_pci`, `mec`, `mace_isa`, `mace_ps2`, `mace_ust`, `mace_audio`, -//! `mace_sdma`, `ns16550`, `ds1687`, `mace_id`); what has no model yet is a +//! `mace_sdma`, `mace_uart`, `ds1687`, `mace_id`); what has no model yet is a //! storing register file (`regs`). use std::sync::{Arc, Mutex}; @@ -18,7 +18,7 @@ use crate::dev::mace_ps2::MacePs2; use crate::dev::mace_sdma::*; use crate::dev::mace_ust::MaceUst; use crate::dev::mec::Mec; -use crate::dev::ns16550::Com16550; +use crate::dev::mace_uart::MaceUart; use crate::dev::mace_id::{MaceId, DEFAULT_MAC}; use crate::ip32::{ Stub, IP32_MEC_CRIME_INT, MACE_BASE, MACE_COM0, MACE_COM1, MACE_COM_SIZE, MACE_ISA_FLASH_NIC_REG, @@ -41,8 +41,8 @@ pub struct Mace { pub rtc: MaceRtc, pub ps2: MacePs2, pub id: MaceId, - pub com0: Com16550, - pub com1: Com16550, + pub com0: MaceUart, + pub com1: MaceUart, /// The serial ports' DMA rings, which IRIX's console runs on. pub sdma: MaceSerialDma, /// The audio interface and its AD1843 (dev/mace_audio.rs). @@ -66,8 +66,8 @@ impl Mace { ps2: MacePs2::new(), // A locally-administered address; nothing depends on the value yet. id: MaceId::new(DEFAULT_MAC), - com0: Com16550::named("com0"), - com1: Com16550::named("com1"), + com0: MaceUart::named("com0"), + com1: MaceUart::named("com1"), sdma: MaceSerialDma::new(), audio: MaceAudio::new(), mec: Arc::new(Mec::new(ram.clone())), @@ -218,7 +218,7 @@ impl Mace { (0x20..0x38).contains(&off).then(|| ((off - 0x20) / 8) as usize) } - fn com_for(&self, addr: u32) -> Option<&Com16550> { + fn com_for(&self, addr: u32) -> Option<&MaceUart> { if (MACE_COM0..MACE_COM0 + MACE_COM_SIZE).contains(&addr) { Some(&self.com0) } else if (MACE_COM1..MACE_COM1 + MACE_COM_SIZE).contains(&addr) { diff --git a/src/dev/mace_sdma.rs b/src/dev/mace_sdma.rs index 040d0c721..2eb03c8e0 100644 --- a/src/dev/mace_sdma.rs +++ b/src/dev/mace_sdma.rs @@ -4,6 +4,7 @@ use std::sync::atomic::Ordering; use std::sync::Mutex; use crate::dev::crime_mem::O2Memory; +use crate::dev::mace_uart::MaceUart; use crate::snapshot::{get_field, hex_u32, hex_u64, toml_u32, toml_u64}; use crate::ip32::*; @@ -131,7 +132,7 @@ impl SdmaPort { /// where the rings are. pub struct SdmaIo<'a> { pub ram: &'a O2Memory, - pub com: [&'a Com16550; 2], + pub com: [&'a MaceUart; 2], /// The ring block's physical address, from the ring base register. pub ring_base: u32, } @@ -305,10 +306,10 @@ impl MaceSerialDma { let (c, d) = (pair[slot], pair[4 + slot]); match c >> 6 { sdma::TX_CMD_THR => { - io.com[p].transmit(d); + io.com[p].core().dma_write(d); port.tx.bytes += 1; } - sdma::TX_CMD_MCR => io.com[p].write_mcr(d), + sdma::TX_CMD_MCR => io.com[p].core().dma_modem_control(d), // Skip, or a delay: there is no wire to be idle on. _ => {} } diff --git a/src/dev/mace_uart.rs b/src/dev/mace_uart.rs new file mode 100644 index 000000000..87a4345a6 --- /dev/null +++ b/src/dev/mace_uart.rs @@ -0,0 +1,286 @@ +//! The O2's two 16550s in MACE's ISA space: the lane decode, the interrupt +//! line and the monitor around the shared [`Ns16550`] core. +//! +//! Register `n` lives at `base + (n << 8) + 7`: MACE spaces its ISA registers +//! 256 bytes apart and puts the byte in the last lane of the 64-bit slot +//! (`sgimips/bus.c`: `h + (o << 8) + 7`). Getting this wrong is silent: every +//! register reads back zero and the firmware simply never transmits. + +use std::io::Write; +use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; + +use crate::dev::ns16550::{com_reg, Ns16550}; +use crate::dev::serial_host::SerialBackend; +use crate::ip32::*; +use crate::irq::IrqLine; +use crate::traits::{BusDevice, BusRead16, BusRead32, BusRead64, BusRead8, Device, Resettable, Saveable, BUS_OK}; + +/// The TL16PIR552's crystal over 16: 1.8432 MHz. +const BAUDBASE: u32 = 115200; + +pub struct MaceUart { + core: Mutex, + /// Which UART this is (`com0`, `com1`): its monitor command. + name: &'static str, + /// Its interrupt, while it is driven by PIO (MACE ISA, port 0's group). + irq: OnceLock, +} + +impl Default for MaceUart { + fn default() -> Self { Self::new() } +} + +impl MaceUart { + pub fn new() -> Self { + Self::named("com") + } + + /// A UART answering monitor commands as `name`. + pub fn named(name: &'static str) -> Self { + Self { core: Mutex::new(Ns16550::new(BAUDBASE)), name, irq: OnceLock::new() } + } + + /// The 16550 itself, for the serial DMA engine in front of it. + pub fn core(&self) -> MutexGuard<'_, Ns16550> { + self.core.lock().unwrap() + } + + /// Wire the host end of the line: a TCP console, a log tee, or nothing. + pub fn set_backend(&self, backend: Arc) { + self.core().set_backend(backend); + } + + /// Move whatever the host end has typed into the receive FIFO, and let + /// the receive timeout run out. The machine's service loop calls this, + /// and so does a status read, so a driver polling for data sees it at + /// once. + pub fn pump(&self) { + let changed = { + let mut c = self.core(); + let a = c.pump(); + let b = c.check_timeout(std::time::Instant::now()); + a || b + }; + if changed { + self.update_irq(); + } + } + + /// Queue characters for the guest to read, as if they arrived on the line. + pub fn feed(&self, bytes: &[u8]) { + self.core().type_in(bytes); + self.update_irq(); + } + + /// Hand up to `buf.len()` received characters to the DMA engine, oldest + /// first, and say how many. They leave the UART, so the PIO path never + /// sees them as well. + pub fn take_input(&self, buf: &mut [u8]) -> usize { + let mut c = self.core(); + let mut n = 0; + while n < buf.len() { + let Some((b, _)) = c.dma_read() else { break }; + buf[n] = b; + n += 1; + } + n + } + + /// True when nothing has been received and not yet taken. + pub(crate) fn input_empty(&self) -> bool { + let mut c = self.core(); + c.pump(); + c.rx_pending() == 0 + } + + /// Characters still unread by the guest. + pub fn pending_input(&self) -> usize { + self.core().rx_pending() + } + + /// What was written to the transmit register, the last + /// [`crate::dev::ns16550::OUT_KEEP`] bytes of it. + pub fn output(&self) -> String { + self.output_tail(crate::dev::ns16550::OUT_KEEP) + } + + /// Bytes sent in all, including those no longer kept. + pub fn bytes_out(&self) -> usize { + self.core().bytes_out() as usize + } + + /// The last `n` bytes written. + pub fn output_tail(&self, n: usize) -> String { + self.core().output_tail(n) + } + + /// Wire the UART's interrupt (the console's: MACE ISA port 0's group). + pub fn connect_irq(&self, line: IrqLine) { + let _ = self.irq.set(line); + } + + /// Drive the interrupt from the UART's state. + pub fn update_irq(&self) { + if let Some(l) = self.irq.get() { + l.set(self.interrupting()); + } + } + + pub fn interrupting(&self) -> bool { + self.core().irq() + } + + /// Decode a MACE ISA address into a 16550 register number, if it names one. + fn reg_of(addr: u32) -> Option { + // Only the +7 lane carries the byte. + if addr & 7 != 7 { + return None; + } + Some((addr >> 8) & 7) + } + + pub fn read_reg(&self, addr: u32) -> u8 { + let Some(n) = Self::reg_of(addr) else { return 0 }; + self.core().read(n) + } + + pub fn write_reg(&self, addr: u32, val: u8) { + let Some(n) = Self::reg_of(addr) else { return }; + self.core().write(n, val); + } + + fn access_read(&self, addr: u64) -> u8 { + let v = self.read_reg((addr | 7) as u32); + self.update_irq(); + v + } + + fn access_write(&self, addr: u64, val: u8) -> u32 { + self.write_reg((addr | 7) as u32, val); + self.update_irq(); + BUS_OK + } +} + +impl Device for MaceUart { + fn step(&self, _cycles: u64) {} + fn stop(&self) {} + fn start(&self) {} + fn is_running(&self) -> bool { true } + fn get_clock(&self) -> u64 { 0 } + + fn register_commands(&self) -> Vec<(String, String)> { + vec![(self.name.to_string(), format!("16550 UART: {0} status | {0} output [bytes] | {0} type ", self.name))] + } + + fn execute_command(&self, cmd: &str, args: &[&str], mut w: Box) -> Result<(), String> { + if cmd != self.name { + return Err("Command not found".to_string()); + } + match args.first().copied().unwrap_or("status") { + "status" => { + let c = self.core(); + writeln!(w, "{}: IER {:#04x} LCR {:#04x} MCR {:#04x} LSR {:#04x} SCR {:#04x}{}{}{}; {} bytes out, {} waiting in", + self.name, c.ier(), c.lcr(), c.mcr(), c.lsr(), c.scr(), + if c.loopback() { ", loopback" } else { "" }, + if c.fifos_enabled() { ", FIFOs" } else { "" }, + if c.irq() { ", interrupting" } else { "" }, + c.bytes_out(), c.rx_pending()).ok(); + } + "output" => { + let n = args.get(1).and_then(|v| v.parse().ok()).unwrap_or(2000); + write!(w, "{}", self.output_tail(n)).ok(); + writeln!(w).ok(); + } + "type" => { + let text = args[1..].join(" ").replace("\\r", "\r").replace("\\n", "\n"); + self.feed(text.as_bytes()); + writeln!(w, "{}: queued {} bytes", self.name, text.len()).ok(); + } + _ => return Err(format!("Usage: {0} status | output [bytes] | type ", self.name)), + } + Ok(()) + } +} + +/// The 16550's registers and what has been received but not yet read. The +/// transcript is the host's record of the line, not the UART's state. +impl Saveable for MaceUart { + fn save_state(&self) -> toml::Value { + self.core().save_state() + } + + fn load_state(&self, v: &toml::Value) -> Result<(), String> { + self.core().load_state(v); + self.update_irq(); + Ok(()) + } +} + +impl Resettable for MaceUart { + /// Registers to their reset values; the transcript and anything a + /// console has typed stay in the host's hands. + fn power_on(&self) { + self.core().reset(); + self.update_irq(); + } +} + +impl BusDevice for MaceUart { + // Every width must be answered. An unimplemented one falls through to the + // trait default, which reports a bus error, and a DBE inside POST looks + // like a missing device rather than a missing method. + // The UART sits on MACE's 8-bit ISA bus, so its byte answers on any lane + // of the doubleword: IRIX and the BSDs use +7, Linux's 8250 driver +0 + // (mapbase 0x1f390000, regshift 8). + // Reading the data or IIR register and writing IER can each move the + // interrupt, so every access drives it again: nothing else does on an + // access (the service thread covers input arriving from the host). + fn read8(&self, addr: u64) -> BusRead8 { BusRead8::ok(self.access_read(addr)) } + fn read16(&self, addr: u64) -> BusRead16 { BusRead16::ok(self.access_read(addr) as u16) } + fn write16(&self, addr: u64, val: u16) -> u32 { self.access_write(addr, val as u8) } + fn write8(&self, addr: u64, val: u8) -> u32 { self.access_write(addr, val) } + fn read32(&self, addr: u64) -> BusRead32 { BusRead32::ok(self.access_read(addr) as u32) } + fn write32(&self, addr: u64, val: u32) -> u32 { self.access_write(addr, val as u8) } + fn read64(&self, addr: u64) -> BusRead64 { BusRead64::ok(self.access_read(addr) as u64) } + fn write64(&self, addr: u64, val: u64) -> u32 { self.access_write(addr, val as u8) } +} + + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::ns16550::com_reg; + use crate::irq::LevelProbe; + + /// Register `n` is at `+ (n << 8) + 7`, the byte answers on any lane, and + /// the port's interrupt reaches the line it was given. + #[test] + fn the_isa_register_spacing_maps_onto_the_core_and_drives_the_line() { + let c = MaceUart::new(); + let (probe, line) = LevelProbe::line(); + c.connect_irq(line); + let reg = |n: u32| MACE_COM0 as u64 + ((n as u64) << 8); + c.write8(reg(com_reg::SCR) + 7, 0x5a); + assert_eq!(c.read8(reg(com_reg::SCR) + 7).data, 0x5a); + assert_eq!(c.read32(reg(com_reg::SCR)).data, 0x5a, "lane 0 reads the same byte"); + assert_eq!(c.read8(reg(com_reg::SCR) + 3).data, 0x5a, "so does any other"); + c.write8(reg(com_reg::IER) + 7, com_reg::IER_TX); + assert!(probe.level(), "the transmit-empty cause is up"); + assert_eq!(c.read8(reg(com_reg::IIR_FCR) + 7).data & 0x0f, com_reg::IIR_TX_EMPTY); + assert!(!probe.level(), "and reading the cause drops it"); + } + + #[test] + fn a_saved_uart_loads_and_saves_the_same() { + let src = MaceUart::new(); + src.write8(MACE_COM0 as u64 + ((com_reg::IIR_FCR as u64) << 8) + 7, 0x81); + src.write8(MACE_COM0 as u64 + ((com_reg::IER as u64) << 8) + 7, com_reg::IER_RX); + src.feed(b"typed"); + let saved = src.save_state(); + let dst = MaceUart::new(); + dst.load_state(&saved).expect("load"); + assert_eq!(saved, dst.save_state()); + assert_eq!(dst.read8(MACE_COM0 as u64 + 7).data, b't', "unread input survives"); + } +} diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 8b2f5e2ad..6b4626074 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -22,11 +22,13 @@ pub mod mace_id; pub mod mace_pci; pub mod mace_ps2; pub mod mace_sdma; +pub mod mace_uart; pub mod mace_ust; pub mod ns16550; pub mod rad1; pub mod mgras; pub mod ng1; +pub mod serial_host; pub mod serial_log; pub mod sflash; pub mod xbow; diff --git a/src/dev/ns16550.rs b/src/dev/ns16550.rs index f4856bb4b..8676af8ba 100644 --- a/src/dev/ns16550.rs +++ b/src/dev/ns16550.rs @@ -1,47 +1,80 @@ -//! The O2's two 16550 UARTs in MACE's ISA space. - -use std::sync::Mutex; - -use std::io::Write; - -use crate::dev::z85c30::SerialBackend; -use crate::snapshot::{get_field, load_u8_slice, toml_u8, u8_slice_to_toml}; -use crate::traits::{BusDevice, BusRead8, BusRead16, BusRead32, BusRead64, Device, Resettable, Saveable, BUS_OK}; -use crate::ip32::*; - -/// A 16550 with just enough behaviour to be written to. -/// -/// Register `n` lives at `base + (n << 8) + 7`: MACE spaces its ISA registers -/// 256 bytes apart and puts the byte in the last lane of the 64-bit slot -/// (`sgimips/bus.c`: `h + (o << 8) + 7`). Getting this wrong is silent — every -/// register reads back zero and the firmware simply never transmits. -/// -/// Transmit only. The line status register always reports the holding register -/// empty, so the PROM never waits on us, and bytes are appended to a buffer -/// rather than going anywhere. -/// How much of a UART's output it keeps. +// Ported from hw/char/serial.c in jamesbraid/qemu-sgi-origami, which carries +// this notice: +// +// QEMU 16550A UART emulation +// +// Copyright (c) 2003-2004 Fabrice Bellard +// Copyright (c) 2008 Citrix Systems, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +//! The 16550A register interface, as copied by the O2's TI TL16PIR552 (behind +//! MACE) and the Octane's IOC3. +//! +//! This is a port of QEMU's `hw/char/serial.c`: the register file, the +//! 16-byte receive FIFO and its trigger levels, interrupt identification in +//! priority order, line and modem status, loopback, receive overrun, the +//! FIFO character timeout and the divisor latch. Both chips wrap it; this +//! file knows nothing of where its registers sit or what drives its +//! interrupt. [`crate::dev::mace_uart`] and [`crate::dev::ioc3`] are the shells. +//! +//! # Differences from the QEMU source +//! +//! * Transmit is instant. A write of the transmit register hands the byte to +//! the host backend (or, in loopback, back to the receiver) before the +//! write returns, so the holding register and the transmitter always read +//! empty and QEMU's transmit timer, retry and hold-off machinery is not +//! here. Nothing is paced to the baud rate; the divisor and line control +//! only set how long the receive timeout is. +//! * The host end is a [`SerialBackend`] polled by [`Ns16550::pump`], where +//! QEMU's chardev calls back. The receive timeout is a host-time deadline +//! that [`Ns16550::check_timeout`] resolves, where QEMU arms a timer. +//! * A receive overrun with the FIFOs off keeps the character already in the +//! receive register and drops the new one, which is what the part does. +//! QEMU overwrites the register. +//! * Reading the receive register with nothing received returns zero, where +//! QEMU returns the last character again. +//! * Modem status changes in loopback latch the delta bits, as the part's +//! datasheet has them and MAME's `ins8250` does; QEMU's loopback read returns +//! the levels alone. The +//! levels outside loopback are fixed at carrier, DSR and CTS asserted; +//! there is no real modem to poll. +//! * Not ported: break detection and set-break, the transmit FIFO (there is +//! nothing to queue), the live modem-line polling of a host serial port, +//! and the host baud-rate control. +//! * The TI part's extra register at the scratch position with the divisor +//! latch set is not modelled: there it is the scratch register. + +use std::collections::VecDeque; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use crate::dev::serial_host::{null_serial_backend, SerialBackend}; +use crate::snapshot::{get_field, hex_u8, toml_bool, toml_u32, toml_u8}; + +/// The receive FIFO's depth. +pub const FIFO_LENGTH: usize = 16; + +/// How much of a UART's output it keeps for the monitor. pub const OUT_KEEP: usize = 64 * 1024; -pub struct Com16550 { - /// The last [`OUT_KEEP`] bytes sent, for the monitor and tests, and how - /// many were sent in all. A console left running for days would - /// otherwise keep every byte it ever printed. - out: Mutex>, - sent: std::sync::atomic::AtomicUsize, - /// The host end of the serial line, as the Z85C30's channels have: - /// a TCP console, a log tee, or nothing. - backend: Mutex>, - /// Characters waiting to be read by the guest. The PROM polls LSR for - /// data-ready and then reads the receive register, so an empty queue simply - /// means "nobody has typed anything". - input: std::sync::Arc>>, - regs: Mutex<[u8; 8]>, - /// Which UART this is (`com0`, `com1`): its monitor command. - name: &'static str, - /// Its interrupt, while it is driven by PIO (MACE ISA, port 0's group). - irq: std::sync::OnceLock, -} - +/// Register numbers and bits. pub mod com_reg { /// Transmit holding / receive buffer (and divisor low when DLAB is set). pub const THR: u32 = 0; @@ -53,447 +86,831 @@ pub mod com_reg { pub const MSR: u32 = 6; pub const SCR: u32 = 7; - /// IER: interrupt on received data, and on the transmitter draining. + pub const LCR_DLAB: u8 = 0x80; + pub const LCR_PEN: u8 = 0x08; + pub const LCR_NSTB: u8 = 0x04; + pub const LCR_WLS: u8 = 0x03; + + /// IER: interrupt on received data, transmitter drained, line status + /// and modem status. pub const IER_RX: u8 = 0x01; pub const IER_TX: u8 = 0x02; + pub const IER_RLS: u8 = 0x04; + pub const IER_MS: u8 = 0x08; + /// IIR: bit 0 clear means "something to service"; the cause is above it. pub const IIR_NONE: u8 = 0x01; + pub const IIR_ID: u8 = 0x06; + pub const IIR_MSI: u8 = 0x00; pub const IIR_TX_EMPTY: u8 = 0x02; pub const IIR_RX_DATA: u8 = 0x04; + pub const IIR_RLSI: u8 = 0x06; + /// Character timeout (bit 3 set beside the receive-data cause). + pub const IIR_CTI: u8 = 0x0c; /// The two high bits a 16550 sets once its FIFOs are enabled. pub const IIR_FIFO: u8 = 0xc0; - /// LCR bit 7: the next accesses to 0 and 1 are the baud divisor. - pub const LCR_DLAB: u8 = 0x80; + pub const FCR_FE: u8 = 0x01; + pub const FCR_RFR: u8 = 0x02; + pub const FCR_XFR: u8 = 0x04; + /// LSR: a received character is waiting. pub const LSR_DR: u8 = 0x01; + pub const LSR_OE: u8 = 0x02; + pub const LSR_BI: u8 = 0x10; /// LSR: transmit holding register empty. pub const LSR_THRE: u8 = 0x20; /// LSR: transmitter completely empty. pub const LSR_TEMT: u8 = 0x40; + /// The status bits that raise a line-status interrupt. + pub const LSR_INT_ANY: u8 = 0x1e; - /// MCR bit 4: loopback. The transmitter is wired back to the receiver and - /// the modem control outputs to the modem status inputs. post1's serial - /// self-test runs entirely in this mode, so a UART without it fails POST. - pub const MCR_LOOP: u8 = 0x10; - /// MCR outputs that loop back to MSR while `MCR_LOOP` is set. pub const MCR_DTR: u8 = 0x01; pub const MCR_RTS: u8 = 0x02; pub const MCR_OUT1: u8 = 0x04; pub const MCR_OUT2: u8 = 0x08; - /// MSR inputs, in the order the loopback wires them. + /// MCR bit 4: loopback. The transmitter is wired back to the receiver and + /// the modem control outputs to the modem status inputs. The O2 PROM's + /// serial self-test runs entirely in this mode. + pub const MCR_LOOP: u8 = 0x10; + + pub const MSR_DCTS: u8 = 0x01; + pub const MSR_DDSR: u8 = 0x02; + pub const MSR_TERI: u8 = 0x04; + pub const MSR_DDCD: u8 = 0x08; + pub const MSR_ANY_DELTA: u8 = 0x0f; pub const MSR_CTS: u8 = 0x10; pub const MSR_DSR: u8 = 0x20; pub const MSR_RI: u8 = 0x40; pub const MSR_DCD: u8 = 0x80; } - -impl Default for Com16550 { - fn default() -> Self { Self::new() } +use com_reg::*; + +/// A character time is ten bit times at the reset divisor of 9600 baud. +const RESET_CHAR_TIME: Duration = Duration::from_nanos(1_000_000_000 / 9600 * 10); + +/// The modem inputs of a port with nothing plugged in: carrier, data set +/// ready and clear to send asserted. +const MSR_IDLE_LEVELS: u8 = MSR_DCD | MSR_DSR | MSR_CTS; + +pub struct Ns16550 { + backend: Arc, + baudbase: u32, + divider: u16, + rbr: u8, + ier: u8, + iir: u8, + lcr: u8, + mcr: u8, + lsr: u8, + msr: u8, + scr: u8, + fcr: u8, + recv_fifo: VecDeque, + recv_fifo_itl: usize, + thr_ipending: bool, + timeout_ipending: bool, + /// When the receive timeout fires, while characters sit unread. + timeout_deadline: Option, + char_transmit_time: Duration, + /// Characters a script or the monitor has typed, ahead of what the + /// backend delivers. They reach the receiver the way the backend's do, + /// as it will take them. + typed: VecDeque, + /// The last [`OUT_KEEP`] bytes sent, and how many were sent in all. + out: VecDeque, + sent: u64, } -impl Com16550 { - pub fn new() -> Self { - Self::named("com") - } - - /// A UART answering monitor commands as `name`. - pub fn named(name: &'static str) -> Self { - Self { - out: Mutex::new(std::collections::VecDeque::new()), - sent: std::sync::atomic::AtomicUsize::new(0), - backend: Mutex::new(crate::dev::z85c30::null_serial_backend()), - input: Default::default(), - regs: Mutex::new([0u8; 8]), - name, - irq: std::sync::OnceLock::new(), +impl Ns16550 { + /// A port in its reset state. `baudbase` is the reference clock over 16 + /// (115200 for a 1.8432 MHz crystal): the divisor divides it. + pub fn new(baudbase: u32) -> Self { + let mut s = Self { + backend: null_serial_backend(), + baudbase, + divider: 0, + rbr: 0, + ier: 0, + iir: IIR_NONE, + lcr: 0, + mcr: 0, + lsr: 0, + msr: 0, + scr: 0, + fcr: 0, + recv_fifo: VecDeque::with_capacity(FIFO_LENGTH), + recv_fifo_itl: 1, + thr_ipending: false, + timeout_ipending: false, + timeout_deadline: None, + char_transmit_time: RESET_CHAR_TIME, + typed: VecDeque::new(), + out: VecDeque::new(), + sent: 0, + }; + s.reset(); + s + } + + /// Reset to 9600 baud, 8N1, the FIFOs off and empty, the + /// transmitter empty, carrier up. The host end, the clock and the record + /// of what was sent stay. + pub fn reset(&mut self) { + self.rbr = 0; + self.ier = 0; + self.iir = IIR_NONE; + self.lcr = 0; + self.lsr = LSR_TEMT | LSR_THRE; + self.msr = MSR_IDLE_LEVELS; + self.divider = 0x0c; + self.mcr = MCR_OUT2; + self.scr = 0; + self.fcr = 0; + self.recv_fifo_itl = 1; + self.char_transmit_time = RESET_CHAR_TIME; + self.timeout_ipending = false; + self.timeout_deadline = None; + self.recv_fifo.clear(); + self.thr_ipending = false; + } + + pub fn set_backend(&mut self, backend: Arc) { + self.backend = backend; + } + + /// Change the reference clock the divisor divides (the IOC3 derives it + /// from its pre-divider). + pub fn set_baudbase(&mut self, baudbase: u32) { + self.baudbase = baudbase; + self.update_parameters(); + } + + // ----- state, read without side effects ----- + + pub fn baudbase(&self) -> u32 { self.baudbase } + pub fn ier(&self) -> u8 { self.ier } + pub fn lcr(&self) -> u8 { self.lcr } + pub fn mcr(&self) -> u8 { self.mcr } + pub fn lsr(&self) -> u8 { self.lsr } + pub fn msr(&self) -> u8 { self.msr } + pub fn scr(&self) -> u8 { self.scr } + pub fn fifos_enabled(&self) -> bool { self.fcr & FCR_FE != 0 } + + pub fn dlab(&self) -> bool { + self.lcr & LCR_DLAB != 0 + } + + pub fn loopback(&self) -> bool { + self.mcr & MCR_LOOP != 0 + } + + /// Received characters not yet read, and typed ones not yet received. + pub fn rx_pending(&self) -> usize { + self.typed.len() + + if self.fcr & FCR_FE != 0 { self.recv_fifo.len() } else { (self.lsr & LSR_DR != 0) as usize } + } + + /// Type characters at the port, as if the host had sent them. They wait + /// until the port will take them, as the backend's do. + pub fn type_in(&mut self, bytes: &[u8]) { + self.typed.extend(bytes.iter().copied()); + self.pump(); + } + + /// True while the port is asking for attention. + pub fn irq(&self) -> bool { + self.iir & IIR_NONE == 0 + } + + /// The last `n` bytes sent (at most [`OUT_KEEP`]). + pub fn output_tail(&self, n: usize) -> String { + let tail: Vec = self.out.iter().skip(self.out.len().saturating_sub(n)).copied().collect(); + String::from_utf8_lossy(&tail).to_string() + } + + /// Bytes sent in all, including those no longer kept. + pub fn bytes_out(&self) -> u64 { + self.sent + } + + // ----- interrupt identification ----- + + /// Pick the highest-priority cause. + fn update_irq(&mut self) { + let tmp = if self.ier & IER_RLS != 0 && self.lsr & LSR_INT_ANY != 0 { + IIR_RLSI + } else if self.ier & IER_RX != 0 && self.timeout_ipending { + // The enable bit masks this too: not in the specification but + // seen on existing parts. + IIR_CTI + } else if self.ier & IER_RX != 0 + && self.lsr & LSR_DR != 0 + && (self.fcr & FCR_FE == 0 || self.recv_fifo.len() >= self.recv_fifo_itl) + { + IIR_RX_DATA + } else if self.ier & IER_TX != 0 && self.thr_ipending { + IIR_TX_EMPTY + } else if self.ier & IER_MS != 0 && self.msr & MSR_ANY_DELTA != 0 { + IIR_MSI + } else { + IIR_NONE + }; + self.iir = tmp | (self.iir & 0xf0); + } + + /// Only the character time survives; there + /// is no host line to set. + fn update_parameters(&mut self) { + let mut frame = 1u32; + if self.lcr & LCR_PEN != 0 { + frame += 1; + } + let stop = if self.lcr & LCR_NSTB != 0 { 2 } else { 1 }; + let data = (self.lcr & LCR_WLS) as u32 + 5; + frame += data + stop; + // A zero divisor gives about 3500 baud. + let speed = if self.divider == 0 { 3500.0 } else { self.baudbase as f64 / self.divider as f64 }; + let speed = if speed > 0.0 { speed } else { 3500.0 }; + self.char_transmit_time = Duration::from_nanos((1e9 / speed * frame as f64) as u64); + } + + // ----- the register file ----- + + /// A byte read of register `reg` (0..8). Reading status acknowledges what + /// a 16550 acknowledges on a read. + pub fn read(&mut self, reg: u32) -> u8 { + match reg & 7 { + THR => { + if self.dlab() { + self.divider as u8 + } else { + // The host has typed, perhaps, since the last look. + self.pump(); + let v = self.read_rbr(); + // The guest made room: the backend may deliver more. + self.pump(); + v + } + } + IER => { + if self.dlab() { + (self.divider >> 8) as u8 + } else { + self.ier + } + } + IIR_FCR => { + self.check_timeout(Instant::now()); + let ret = self.iir; + if ret & IIR_ID == IIR_TX_EMPTY { + self.thr_ipending = false; + self.update_irq(); + } + ret + } + LCR => self.lcr, + MCR => self.mcr, + LSR => { + self.pump(); + self.check_timeout(Instant::now()); + let ret = self.lsr; + // Reading clears the break and overrun latches. + if self.lsr & (LSR_BI | LSR_OE) != 0 { + self.lsr &= !(LSR_BI | LSR_OE); + self.update_irq(); + } + ret + } + MSR => { + let ret = self.msr; + if self.msr & MSR_ANY_DELTA != 0 { + self.msr &= 0xf0; + self.update_irq(); + } + ret + } + _ => self.scr, } } - /// Wire the host end of the line: a TCP console - /// (`z85c30::tcp_serial_backend`), a `TeeBackend` logging to a file, or - /// nothing. - pub fn set_backend(&self, backend: std::sync::Arc) { - *self.backend.lock().unwrap() = backend; + /// A byte write of register `reg` (0..8). + pub fn write(&mut self, reg: u32, val: u8) { + match reg & 7 { + THR => { + if self.dlab() { + self.divider = (self.divider & 0xff00) | val as u16; + self.update_parameters(); + } else { + self.write_thr(val); + } + } + IER => { + if self.dlab() { + self.divider = (self.divider & 0x00ff) | (val as u16) << 8; + self.update_parameters(); + } else { + let changed = (self.ier ^ val) & 0x0f; + self.ier = val & 0x0f; + // Turning the transmit interrupt on with the holding + // register empty raises it, even if reading the cause + // masked it before. Not in the datasheet, but drivers + // toggle the enable and rely on it. + if changed & IER_TX != 0 { + self.thr_ipending = self.ier & IER_TX != 0 && self.lsr & LSR_THRE != 0; + } + if changed != 0 { + self.update_irq(); + } + } + } + IIR_FCR => self.write_fcr(val), + LCR => { + self.lcr = val; + self.update_parameters(); + } + MCR => { + self.mcr = val & 0x1f; + self.update_modem_levels(); + } + // Line and modem status are read-only; the IOC3 shell has test + // writes of its own. + LSR | MSR => {} + _ => self.scr = val, + } } - /// Move whatever the host end has typed into the receive queue. The - /// machine's service loop calls this, and so does a status read, so a - /// driver polling for data sees it at once. - pub fn pump(&self) { - let backend = self.backend.lock().unwrap().clone(); - let mut got = false; - while let Ok(b) = backend.recv_byte() { - self.input.lock().unwrap().push_back(b); - got = true; + fn write_fcr(&mut self, mut val: u8) { + // Flush both sides when the enable changes. + if (val ^ self.fcr) & FCR_FE != 0 { + val |= FCR_XFR | FCR_RFR; + } + if val & FCR_RFR != 0 { + self.lsr &= !(LSR_DR | LSR_BI); + self.timeout_deadline = None; + self.timeout_ipending = false; + self.recv_fifo.clear(); } - if got { - self.update_irq(); + if val & FCR_XFR != 0 { + self.lsr |= LSR_THRE; + self.thr_ipending = true; } + // Only the bits that stick. + let val = val & 0xc9; + self.fcr = val; + if val & FCR_FE != 0 { + self.iir |= IIR_FIFO; + self.recv_fifo_itl = match val & 0xc0 { + 0x00 => 1, + 0x40 => 4, + 0x80 => 8, + _ => 14, + }; + } else { + self.iir &= !IIR_FIFO; + } + self.update_irq(); } - /// Queue characters for the guest to read. - pub fn feed(&self, bytes: &[u8]) { - self.input.lock().unwrap().extend(bytes.iter().copied()); + /// The modem status inputs follow the outputs in loopback and sit at + /// their idle levels otherwise; a change latches the delta bits. + fn update_modem_levels(&mut self) { + let new = if self.loopback() { + ((self.mcr & (MCR_OUT1 | MCR_OUT2)) << 4) | ((self.mcr & MCR_RTS) << 3) | ((self.mcr & MCR_DTR) << 5) + } else { + MSR_IDLE_LEVELS + }; + let old = self.msr & 0xf0; + // CTS, DSR and carrier changes latch straight; ring only on its + // trailing edge. + let mut delta = ((new ^ old) >> 4) & (MSR_DCTS | MSR_DDSR | MSR_DDCD); + if old & MSR_RI != 0 && new & MSR_RI == 0 { + delta |= MSR_TERI; + } + self.msr = new | (self.msr & MSR_ANY_DELTA) | delta; self.update_irq(); } - /// Characters still unread by the guest. - pub fn pending_input(&self) -> usize { - self.input.lock().unwrap().len() - } + // ----- transmit ----- - /// What was written to the transmit register, the last [`OUT_KEEP`] - /// bytes of it. - pub fn output(&self) -> String { - self.output_tail(OUT_KEEP) + /// A write of the holding register and the transmit it starts, which here completes + /// at once: the byte goes to the host (or round to the receiver in + /// loopback), and the holding register is empty again with its interrupt + /// raised. + fn write_thr(&mut self, val: u8) { + self.thr_ipending = false; + self.lsr &= !(LSR_THRE | LSR_TEMT); + self.update_irq(); + if self.loopback() { + self.receive(&[val]); + } else { + if self.out.len() == OUT_KEEP { + self.out.pop_front(); + } + self.out.push_back(val); + self.sent += 1; + self.backend.send_byte(val); + } + self.lsr |= LSR_THRE | LSR_TEMT; + self.thr_ipending = true; + self.update_irq(); } - /// Bytes sent in all, including those no longer kept. - pub fn bytes_out(&self) -> usize { - self.sent.load(std::sync::atomic::Ordering::Relaxed) - } + // ----- receive ----- - /// The last `n` bytes written (at most [`OUT_KEEP`]). - pub fn output_tail(&self, n: usize) -> String { - let o = self.out.lock().unwrap(); - let tail: Vec = o.iter().skip(o.len().saturating_sub(n)).copied().collect(); - String::from_utf8_lossy(&tail).to_string() + fn read_rbr(&mut self) -> u8 { + let ret = if self.fcr & FCR_FE != 0 { + let ret = self.recv_fifo.pop_front().unwrap_or(0); + if self.recv_fifo.is_empty() { + self.lsr &= !(LSR_DR | LSR_BI); + self.timeout_deadline = None; + } else { + self.timeout_deadline = Some(Instant::now() + self.char_transmit_time * 4); + } + self.timeout_ipending = false; + ret + } else { + // With nothing received the register reads zero, as both models + // that preceded this one answered, where QEMU returns the last + // character again. + let ret = if self.lsr & LSR_DR != 0 { self.rbr } else { 0 }; + self.lsr &= !(LSR_DR | LSR_BI); + ret + }; + self.update_irq(); + ret + } + + /// How many characters the port will take now. With + /// the FIFOs on, only up to the trigger level, so the guest gets its + /// interrupt before the FIFO is full. + fn can_receive(&self) -> usize { + if self.fcr & FCR_FE != 0 { + let used = self.recv_fifo.len(); + if used >= FIFO_LENGTH { + 0 + } else if used <= self.recv_fifo_itl { + self.recv_fifo_itl - used + } else { + 1 + } + } else { + (self.lsr & LSR_DR == 0) as usize + } } - /// Decode a MACE ISA address into a 16550 register number, if it names one. - fn reg_of(addr: u32) -> Option { - // Only the +7 lane carries the byte. - if addr & 7 != 7 { - return None; + /// Characters arrive from the line (or from loopback). + pub fn receive(&mut self, buf: &[u8]) { + if buf.is_empty() { + return; } - let n = (addr >> 8) & 7; - Some(n) + if self.fcr & FCR_FE != 0 { + for &b in buf { + // Overruns do not overwrite FIFO contents. + if self.recv_fifo.len() < FIFO_LENGTH { + self.recv_fifo.push_back(b); + } else { + self.lsr |= LSR_OE; + } + } + self.lsr |= LSR_DR; + self.timeout_deadline = Some(Instant::now() + self.char_transmit_time * 4); + } else { + if self.lsr & LSR_DR != 0 { + // The register keeps the character it has. + self.lsr |= LSR_OE; + } else { + self.rbr = buf[0]; + } + self.lsr |= LSR_DR; + } + self.update_irq(); } - fn dlab(&self) -> bool { - self.regs.lock().unwrap()[com_reg::LCR as usize] & com_reg::LCR_DLAB != 0 + /// Take what the host has typed, as far as the port will accept it. + /// Returns whether anything arrived. Called by the shell's service loop + /// and on status and data reads, so a driver polling for data sees it at + /// once. + pub fn pump(&mut self) -> bool { + let mut moved = false; + loop { + let n = self.can_receive(); + if n == 0 { + break; + } + let mut buf = [0u8; FIFO_LENGTH]; + let mut got = 0; + while got < n { + match self.typed.pop_front().map(Ok).unwrap_or_else(|| self.backend.recv_byte()) { + Ok(b) => { + buf[got] = b; + got += 1; + } + Err(_) => break, + } + } + if got == 0 { + break; + } + self.receive(&buf[..got]); + moved = true; + } + moved + } + + /// Resolve the receive timeout at `now`: characters left unread for four + /// character times raise the timeout cause. Returns whether it did. + pub fn check_timeout(&mut self, now: Instant) -> bool { + match self.timeout_deadline { + Some(d) if d <= now => { + self.timeout_deadline = None; + if !self.recv_fifo.is_empty() { + self.timeout_ipending = true; + self.update_irq(); + return true; + } + false + } + _ => false, + } } - /// True while MCR selects internal loopback. - fn looped(&self) -> bool { - self.regs.lock().unwrap()[com_reg::MCR as usize] & com_reg::MCR_LOOP != 0 + #[cfg(test)] + fn timeout_deadline(&self) -> Option { + self.timeout_deadline } - /// What the chip wants serviced, in the form the driver reads it. - /// - /// A kernel does not poll: it fills the transmit FIFO and waits to be - /// told it has drained. Reporting "nothing pending" here leaves it - /// waiting forever, and the visible symptom is output that stops in the - /// middle of a word. - fn iir(&self) -> u8 { - let ier = self.regs.lock().unwrap()[com_reg::IER as usize]; - if ier & com_reg::IER_RX != 0 && !self.input.lock().unwrap().is_empty() { - return com_reg::IIR_RX_DATA; - } - if ier & com_reg::IER_TX != 0 { - // The transmitter is always drained here; there is no wire. - return com_reg::IIR_TX_EMPTY; - } - com_reg::IIR_NONE - } + // ----- hooks for a DMA engine in front of the port ----- - /// True while the chip is asking for attention. - /// Wire the UART's interrupt (the console's: MACE ISA port 0's group). - pub fn connect_irq(&self, line: crate::irq::IrqLine) { - let _ = self.irq.set(line); + /// A DMA engine's transmit: the byte goes as a write of the holding + /// register does. The transmitter is never busy, so it always takes it. + pub fn dma_write(&mut self, byte: u8) { + self.write_thr(byte); } - /// Drive the interrupt from the UART's state. - pub fn update_irq(&self) { - if let Some(l) = self.irq.get() { - l.set(self.interrupting()); + /// A DMA engine's read: the character and the line status that came with + /// it (reading the status clears its latches), or `None` if nothing is + /// waiting. + pub fn dma_read(&mut self) -> Option<(u8, u8)> { + // The host's characters arrive between the engine's looks. + self.pump(); + if self.lsr & LSR_DR == 0 { + return None; } + let status = self.read(LSR); + let byte = self.read_rbr(); + Some((byte, status)) } - pub fn interrupting(&self) -> bool { - self.iir() & com_reg::IIR_NONE == 0 + pub fn dma_modem_control(&mut self, val: u8) { + self.write(MCR, val); } - pub fn read_reg(&self, addr: u32) -> u8 { - let Some(n) = Self::reg_of(addr) else { return 0 }; - match n { - // Reading IIR is how the driver asks what to do next, and on a - // 16550 it also acknowledges a transmit interrupt. - com_reg::IIR_FCR => { - let fifo = if self.regs.lock().unwrap()[com_reg::IIR_FCR as usize] & 1 != 0 { - com_reg::IIR_FIFO - } else { - 0 - }; - self.iir() | fifo - } - // Always ready to take another byte, and data-ready whenever - // something has been typed. - com_reg::LSR => { - self.pump(); - let mut v = com_reg::LSR_THRE | com_reg::LSR_TEMT; - if !self.input.lock().unwrap().is_empty() { - v |= com_reg::LSR_DR; - } - v - } - // Register 0 reads the receive buffer (unless DLAB selects the - // divisor, which nothing reads back). - com_reg::THR if !self.dlab() => { - self.input.lock().unwrap().pop_front().unwrap_or(0) - } - // In loopback the modem control outputs appear as the status - // inputs. Nothing in POST depends on this, but a half-wired - // loopback is the kind of thing that misleads the next reader. - com_reg::MSR if self.looped() => { - let mcr = self.regs.lock().unwrap()[com_reg::MCR as usize]; - let mut v = 0; - for (out, inp) in [ - (com_reg::MCR_RTS, com_reg::MSR_CTS), - (com_reg::MCR_DTR, com_reg::MSR_DSR), - (com_reg::MCR_OUT1, com_reg::MSR_RI), - (com_reg::MCR_OUT2, com_reg::MSR_DCD), - ] { - if mcr & out != 0 { - v |= inp; - } - } - v - } - _ => self.regs.lock().unwrap()[n as usize], - } - } + // ----- snapshots ----- - pub fn write_reg(&self, addr: u32, val: u8) { - let Some(n) = Self::reg_of(addr) else { return }; - if n == com_reg::THR && !self.dlab() { - self.transmit(val); - return; - } - self.regs.lock().unwrap()[n as usize] = val; - } - - /// Send one character, as a write of the transmit register does. MACE's - /// serial DMA engine comes here too, so a byte sent from the ring reaches - /// the log and the TCP console by exactly the same road as one the - /// firmware writes by hand. - pub fn transmit(&self, val: u8) { - // Loopback: the byte never leaves the chip, it arrives back in - // the receive register. post1 writes 0..254 this way and insists - // on reading the same sequence back. - if self.looped() { - self.input.lock().unwrap().push_back(val); - return; - } - let mut o = self.out.lock().unwrap(); - if o.len() == OUT_KEEP { - o.pop_front(); - } - o.push_back(val); - drop(o); - self.sent.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let backend = self.backend.lock().unwrap().clone(); - backend.send_byte(val); - } - - /// Set the modem control register, for a DMA pair that names it rather - /// than the transmitter. - pub fn write_mcr(&self, val: u8) { - self.regs.lock().unwrap()[com_reg::MCR as usize] = val; - } - - /// Hand up to `buf.len()` received characters to the DMA engine, oldest - /// first, and say how many. They leave the queue, so the PIO path never - /// sees them as well. - pub fn take_input(&self, buf: &mut [u8]) -> usize { - let mut q = self.input.lock().unwrap(); - let n = buf.len().min(q.len()); - for (b, c) in buf.iter_mut().zip(q.drain(..n)) { - *b = c; + pub fn save_state(&self) -> toml::Value { + let mut t = toml::map::Map::new(); + t.insert("divider".into(), toml::Value::Integer(self.divider as i64)); + for (k, v) in [ + ("rbr", self.rbr), + ("ier", self.ier), + ("iir", self.iir), + ("lcr", self.lcr), + ("mcr", self.mcr), + ("lsr", self.lsr), + ("msr", self.msr), + ("scr", self.scr), + ("fcr", self.fcr), + ] { + t.insert(k.into(), hex_u8(v)); } - n + t.insert("thr_ipending".into(), toml::Value::Boolean(self.thr_ipending)); + t.insert("timeout_ipending".into(), toml::Value::Boolean(self.timeout_ipending)); + t.insert("recv_fifo".into(), toml::Value::Array(self.recv_fifo.iter().map(|&b| hex_u8(b)).collect())); + t.insert("typed".into(), toml::Value::Array(self.typed.iter().map(|&b| hex_u8(b)).collect())); + toml::Value::Table(t) } - /// True when nothing has been received and not yet taken. - pub(crate) fn input_empty(&self) -> bool { - self.input.lock().unwrap().is_empty() + pub fn load_state(&mut self, v: &toml::Value) { + let b = |k: &str| get_field(v, k).and_then(toml_u8).unwrap_or(0); + self.divider = get_field(v, "divider").and_then(toml_u32).unwrap_or(0x0c) as u16; + self.rbr = b("rbr"); + self.ier = b("ier"); + self.iir = b("iir"); + self.lcr = b("lcr"); + self.mcr = b("mcr"); + self.lsr = b("lsr"); + self.msr = b("msr"); + self.scr = b("scr"); + self.fcr = b("fcr"); + self.thr_ipending = get_field(v, "thr_ipending").and_then(toml_bool).unwrap_or(false); + self.timeout_ipending = get_field(v, "timeout_ipending").and_then(toml_bool).unwrap_or(false); + self.recv_fifo = match get_field(v, "recv_fifo") { + Some(toml::Value::Array(a)) => a.iter().take(FIFO_LENGTH).map(|x| toml_u8(x).unwrap_or(0)).collect(), + _ => VecDeque::new(), + }; + self.typed = match get_field(v, "typed") { + Some(toml::Value::Array(a)) => a.iter().map(|x| toml_u8(x).unwrap_or(0)).collect(), + _ => VecDeque::new(), + }; + self.recv_fifo_itl = match self.fcr & 0xc0 { + 0x40 => 4, + 0x80 => 8, + 0xc0 => 14, + _ => 1, + }; + self.update_parameters(); + // The timeout restarts from the load. + self.timeout_deadline = (!self.recv_fifo.is_empty() && !self.timeout_ipending) + .then(|| Instant::now() + self.char_transmit_time * 4); + self.update_irq(); } } -impl Device for Com16550 { - fn step(&self, _cycles: u64) {} - fn stop(&self) {} - fn start(&self) {} - fn is_running(&self) -> bool { true } - fn get_clock(&self) -> u64 { 0 } +#[cfg(test)] +mod tests { + use super::*; + use std::io; + use std::sync::Mutex; - fn register_commands(&self) -> Vec<(String, String)> { - vec![(self.name.to_string(), format!("16550 UART: {0} status | {0} output [bytes] | {0} type ", self.name))] + #[derive(Default)] + struct Wire { + out: Mutex>, + inp: Mutex>, } - fn execute_command(&self, cmd: &str, args: &[&str], mut w: Box) -> Result<(), String> { - if cmd != self.name { - return Err("Command not found".to_string()); + impl SerialBackend for Wire { + fn send_byte(&self, b: u8) { + self.out.lock().unwrap().push(b); } - match args.first().copied().unwrap_or("status") { - "status" => { - let r = *self.regs.lock().unwrap(); - writeln!(w, "{}: IER {:#04x} LCR {:#04x} MCR {:#04x} SCR {:#04x}{}{}; {} bytes out, {} waiting in", - self.name, r[com_reg::IER as usize], r[com_reg::LCR as usize], r[4], r[7], - if self.looped() { ", loopback" } else { "" }, if self.interrupting() { ", interrupting" } else { "" }, - self.bytes_out(), self.pending_input()).ok(); - } - "output" => { - let n = args.get(1).and_then(|v| v.parse().ok()).unwrap_or(2000); - write!(w, "{}", self.output_tail(n)).ok(); - writeln!(w).ok(); - } - "type" => { - let text = args[1..].join(" ").replace("\\r", "\r").replace("\\n", "\n"); - self.feed(text.as_bytes()); - writeln!(w, "{}: queued {} bytes", self.name, text.len()).ok(); - } - _ => return Err(format!("Usage: {0} status | output [bytes] | type ", self.name)), + fn recv_byte(&self) -> io::Result { + self.inp.lock().unwrap().pop_front().ok_or_else(|| io::Error::new(io::ErrorKind::WouldBlock, "empty")) } - Ok(()) } -} -/// The registers and what has been typed but not yet read. The transcript -/// is the host's record of the line, not the UART's state. -impl Saveable for Com16550 { - fn save_state(&self) -> toml::Value { - let mut t = toml::map::Map::new(); - t.insert("regs".into(), u8_slice_to_toml(&*self.regs.lock().unwrap())); - let input: Vec = self.input.lock().unwrap().iter().copied().collect(); - t.insert("input".into(), u8_slice_to_toml(&input)); - toml::Value::Table(t) + fn uart() -> (Ns16550, Arc) { + let w = Arc::new(Wire::default()); + let mut u = Ns16550::new(115200); + u.set_backend(w.clone()); + (u, w) } - fn load_state(&self, v: &toml::Value) -> Result<(), String> { - if let Some(r) = get_field(v, "regs") { - load_u8_slice(r, &mut *self.regs.lock().unwrap()); - } - if let Some(toml::Value::Array(a)) = get_field(v, "input") { - *self.input.lock().unwrap() = a.iter().map(|b| toml_u8(b).unwrap_or(0)).collect(); - } - self.update_irq(); - Ok(()) + /// Loopback, with the FIFOs on or off, so `write(THR)` is also a receive. + fn looped(fcr: u8) -> Ns16550 { + let (mut u, _) = uart(); + u.write(IIR_FCR, fcr); + u.write(MCR, MCR_LOOP); + u.read(MSR); + u } -} -impl Resettable for Com16550 { - /// Registers to zero; the transcript and anything a console has typed - /// stay, as they would on the far end of a real serial line. - fn power_on(&self) { - *self.regs.lock().unwrap() = [0u8; 8]; + #[test] + fn a_port_comes_up_idle_with_carrier_at_9600_baud() { + let (mut u, _) = uart(); + assert_eq!((u.read(IER), u.read(IIR_FCR), u.read(LCR), u.read(SCR)), (0, 1, 0, 0)); + assert_eq!((u.read(MCR), u.read(LSR), u.read(MSR)), (0x08, 0x60, 0xb0)); + u.write(LCR, 0x80); + assert_eq!((u.read(THR), u.read(IER)), (0x0c, 0), "divisor 12 of 115200"); } -} -impl Com16550 { - fn access_read(&self, addr: u64) -> u8 { - let v = self.read_reg((addr | 7) as u32); - self.update_irq(); - v + #[test] + fn the_divisor_latch_hides_data_and_enable_and_sets_the_character_time() { + let (mut u, w) = uart(); + u.write(IER, 0x05); + u.write(LCR, 0x83); + u.write(THR, 0x01); + u.write(IER, 0x00); + assert_eq!((u.read(THR), u.read(IER)), (1, 0)); + assert!(w.out.lock().unwrap().is_empty(), "the divisor is not transmitted"); + u.write(LCR, 0x03); + assert_eq!(u.read(IER), 0x05, "the enable register was not touched"); + assert_eq!(u.char_transmit_time.as_micros(), 86, "ten bits at 115200 baud"); + u.write(LCR, 0x1b); + assert_eq!(u.char_transmit_time.as_micros(), 95, "parity adds a bit"); } - fn access_write(&self, addr: u64, val: u8) -> u32 { - self.write_reg((addr | 7) as u32, val); - self.update_irq(); - BUS_OK + #[test] + fn without_fifos_a_second_byte_waits_in_the_backend_until_the_first_is_read() { + let (mut u, w) = uart(); + w.inp.lock().unwrap().extend(b"ab"); + assert_eq!(u.read(LSR) & 3, 1, "one byte in, no overrun"); + assert_eq!(w.inp.lock().unwrap().len(), 1, "the other is still the backend's"); + assert_eq!(u.read(THR), b'a'); + assert_eq!(u.read(LSR) & 3, 1, "reading made room for it"); + assert_eq!(u.read(THR), b'b'); + assert_eq!(u.read(LSR) & 1, 0); } -} -impl BusDevice for Com16550 { - // Every width must be answered. An unimplemented one falls through to the - // trait default, which reports a bus error — and a DBE inside POST looks - // like a missing device rather than a missing method. - // The UART sits on MACE's 8-bit ISA bus, so its byte answers on any lane - // of the doubleword: IRIX and the BSDs use +7, Linux's 8250 driver +0 - // (mapbase 0x1f390000, regshift 8). - // Reading the data or IIR register and writing IER can each move the - // interrupt, so every access drives it again: nothing else does on an - // access (the service thread covers input arriving from the host). - fn read8(&self, addr: u64) -> BusRead8 { BusRead8::ok(self.access_read(addr)) } - fn read16(&self, addr: u64) -> BusRead16 { BusRead16::ok(self.access_read(addr) as u16) } - fn write16(&self, addr: u64, val: u16) -> u32 { self.access_write(addr, val as u8) } - fn write8(&self, addr: u64, val: u8) -> u32 { self.access_write(addr, val) } - fn read32(&self, addr: u64) -> BusRead32 { BusRead32::ok(self.access_read(addr) as u32) } - fn write32(&self, addr: u64, val: u32) -> u32 { self.access_write(addr, val as u8) } - fn read64(&self, addr: u64) -> BusRead64 { BusRead64::ok(self.access_read(addr) as u64) } - fn write64(&self, addr: u64, val: u64) -> u32 { self.access_write(addr, val as u8) } -} + #[test] + fn the_fifo_interrupts_at_its_trigger_level_and_a_flush_empties_it() { + let (mut u, w) = uart(); + u.write(IER, IER_RX); + u.write(IIR_FCR, 0xc1); + assert_eq!(u.read(IIR_FCR) & 0xc0, 0xc0); + w.inp.lock().unwrap().extend(0..40u8); + u.pump(); + assert_eq!(u.rx_pending(), 14, "the port takes the host's bytes up to its trigger level"); + assert_eq!(u.iir & 0x0f, IIR_RX_DATA); + u.write(IIR_FCR, 0xc3); + assert_eq!((u.rx_pending(), u.irq()), (0, false)); + u.write(IIR_FCR, 0); + assert_eq!(u.read(IIR_FCR) & 0xc0, 0, "FCR bit 0 turns the FIFOs off"); + } -#[cfg(test)] -mod tests { - use super::*; + #[test] + fn interrupt_causes_come_in_priority_order_and_clear_on_their_read() { + let mut u = looped(0); + u.write(IER, 0x0f); + u.read(IIR_FCR); + u.write(THR, b'1'); + u.write(THR, b'2'); + u.write(MCR, MCR_LOOP | MCR_RTS); + assert_eq!(u.iir & 0x0f, IIR_RLSI, "line status first"); + u.read(LSR); + assert_eq!(u.iir & 0x0f, IIR_RX_DATA); + assert_eq!(u.read(THR), b'1'); + assert_eq!(u.read(IIR_FCR) & 0x0f, IIR_TX_EMPTY, "then the empty holding register"); + assert_eq!(u.read(IIR_FCR) & 0x0f, IIR_MSI, "reading the transmit cause cleared it; modem last"); + u.read(MSR); + assert_eq!(u.read(IIR_FCR) & 0x0f, IIR_NONE); + } - /// Bus address of a byte in the first MACE serial port. - fn com0(off: u32) -> u64 { - MACE_COM0 as u64 + off as u64 + #[test] + fn a_cause_needs_its_enable_bit_and_the_high_nibble_is_dropped() { + let mut u = looped(0); + u.write(THR, b'x'); + u.write(MCR, MCR_LOOP | MCR_RTS); + assert_eq!(u.read(IIR_FCR), IIR_NONE, "data, transmit-empty and a modem change pending, all masked"); + u.write(IER, 0xf1); + assert_eq!(u.read(IER), 0x01); + assert_eq!(u.read(IIR_FCR) & 0x0f, IIR_RX_DATA); + u.write(IER, IER_TX); + assert_eq!(u.read(IIR_FCR) & 0x0f, IIR_TX_EMPTY, "turning the enable on raises the cause"); } - /// The TCP console, through the shared serial backend: a client gets - /// the telnet handshake, then every byte the guest transmits, and what - /// it types becomes the guest's input -- with telnet's own commands - /// taken out on the way. #[test] - fn a_tcp_console_carries_both_directions() { - use std::io::{Read, Write}; - let c = Com16550::new(); - let (backend, port) = crate::dev::z85c30::tcp_serial_backend(0).expect("bind"); - c.set_backend(backend); - let mut client = std::net::TcpStream::connect(("127.0.0.1", port)).expect("connect"); - client.set_read_timeout(Some(std::time::Duration::from_secs(5))).unwrap(); - // The backend takes the client on one of its polls, and greets it - // then: keep polling until the greeting is all here. - let until = std::time::Instant::now() + std::time::Duration::from_secs(5); - let mut hs = [0u8; 12]; - let mut got = 0; - client.set_read_timeout(Some(std::time::Duration::from_millis(20))).unwrap(); - while got < hs.len() && std::time::Instant::now() < until { - c.pump(); - match client.read(&mut hs[got..]) { - Ok(n) => got += n, - Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {} - Err(e) => panic!("handshake: {e}"), - } - } - client.set_read_timeout(Some(std::time::Duration::from_secs(5))).unwrap(); - assert_eq!(hs, crate::net::telnet::TelnetFilter::initial_handshake()); - // Typed, with a telnet command (IAC DO SGA, which it already offered) in between. - client.write_all(&[b'l', 0xff, 0xfd, 0x03, b's', b'\r']).unwrap(); - while c.pending_input() < 3 && std::time::Instant::now() < until { - c.pump(); - std::thread::sleep(std::time::Duration::from_millis(5)); + fn line_status_reports_overrun_and_an_empty_transmitter() { + let mut u = looped(0); + assert_eq!(u.read(LSR), 0x60); + u.write(THR, b'a'); + u.write(THR, b'b'); + assert_eq!(u.read(LSR) & 3, 3, "a second byte overruns the one-byte register"); + assert_eq!(u.read(LSR) & 3, 1, "the overrun clears on read"); + assert_eq!((u.read(THR), u.read(THR)), (b'a', 0), "the first is kept; an empty register reads zero"); + let mut u = looped(1); + for b in 0..17u8 { + u.write(THR, b); } - assert_eq!(c.read8(com0(7)).data, b'l'); - assert_eq!(c.read8(com0(7)).data, b's'); - assert_eq!(c.read8(com0(7)).data, b'\r'); - assert_eq!(c.pending_input(), 0, "the telnet command is not input"); - // Transmitted: reaches the client. - for &b in b"ok" { - c.write8(com0(7), b); - } - let mut got = [0u8; 2]; - client.read_exact(&mut got).expect("output"); - assert_eq!(&got, b"ok"); - assert_eq!(c.output(), "ok", "the console buffer still has it too"); + assert_eq!(u.read(LSR) & 2, 2, "the seventeenth overruns the FIFO"); + assert_eq!((0..16u8).filter(|&b| u.read(THR) == b).count(), 16, "and the sixteen are kept"); + } + + #[test] + fn loopback_wires_the_modem_outputs_to_the_inputs_and_latches_changes() { + let (mut u, _) = uart(); + u.write(IER, IER_MS); + u.write(MCR, MCR_LOOP | MCR_OUT1 | MCR_RTS); + assert_eq!(u.iir & 0x0f, IIR_MSI); + assert_eq!(u.read(MSR), 0x5a, "ring and CTS up; DSR and carrier moved"); + u.write(MCR, MCR_LOOP | MCR_DTR | MCR_OUT2); + let msr = u.read(MSR); + assert_eq!(msr & 0xf0, 0xa0, "DSR and carrier"); + assert_eq!(msr & 0x04, 0x04, "ring fell: the trailing edge"); + assert_eq!(u.read(MSR) & 0x0f, 0, "a read clears the changes"); + assert_eq!(u.read(IIR_FCR), IIR_NONE); } #[test] - fn save_load_round_trip() { - let src = Com16550::new(); - src.write8(com0((com_reg::IER << 8) + 7), com_reg::IER_RX); - src.write8(com0((com_reg::LCR << 8) + 7), 0x03); - src.write8(com0((com_reg::SCR << 8) + 7), 0x5a); - src.feed(b"typed"); - let v1 = src.save_state(); - - let dst = Com16550::new(); - dst.load_state(&v1).expect("load_state"); - assert_eq!(v1, dst.save_state(), "Com16550 save_state mismatch after load_state round-trip"); - assert_eq!(dst.read8(com0(7)).data, b't', "unread input survives"); + fn the_timeout_restarts_after_a_partial_read_and_a_flush_clears_it() { + let (mut u, _) = uart(); + u.write(IER, IER_RX); + u.write(IIR_FCR, 0xc1); + u.receive(&[1, 2]); + let first = u.timeout_deadline().expect("armed"); + assert!(!u.check_timeout(first - Duration::from_micros(1))); + assert_eq!(u.read(THR), 1); + let second = u.timeout_deadline().expect("a partial read starts it again"); + assert!(second >= first); + assert!(u.check_timeout(second)); + assert_eq!(u.iir & 0x0f, IIR_CTI); + u.write(IIR_FCR, 0xc7); + assert_eq!((u.irq(), u.timeout_deadline()), (false, None), "a flush clears it"); + assert_eq!(u.read(IIR_FCR) & 0x0f, IIR_NONE); + u.write(IER, IER_TX); + u.write(IIR_FCR, 0xc7); + assert_eq!(u.iir & 0x0f, IIR_TX_EMPTY, "resetting the transmit FIFO raises the empty cause"); + } + + #[test] + fn transmit_is_instant_and_the_kept_output_is_bounded() { + let (mut u, w) = uart(); + for _ in 0..OUT_KEEP + 5 { + u.write(THR, b'z'); + } + u.write(THR, b'!'); + assert_eq!(u.read(LSR) & 0x60, 0x60, "the transmitter is never busy"); + assert_eq!(w.out.lock().unwrap().len(), OUT_KEEP + 6, "every byte reached the backend"); + assert_eq!((u.bytes_out(), u.output_tail(usize::MAX).len()), (OUT_KEEP as u64 + 6, OUT_KEEP)); + assert!(u.output_tail(2).ends_with("z!")); } } diff --git a/src/dev/serial_host.rs b/src/dev/serial_host.rs new file mode 100644 index 000000000..a4a28aa3b --- /dev/null +++ b/src/dev/serial_host.rs @@ -0,0 +1,494 @@ +//! The host end of a serial line: where a guest UART's bytes go and where +//! its input comes from. +//! +//! A [`SerialBackend`] is a TCP console, the in-process CI backend, a file +//! tee, or nothing. Every machine's UARTs (the Indy's Z85C30, the O2's MACE +//! 16550s, the Octane's IOC3) take one, and [`console_backends`] picks them +//! for the machines whose consoles are configured the same way. + +use std::fs::OpenOptions; +use std::collections::VecDeque; +use std::io::{self, Read, Write}; +#[cfg(unix)] +use std::os::unix::net::{UnixListener, UnixStream}; +use std::net::{TcpListener, TcpStream}; +use std::path::Path; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use parking_lot::{Condvar, Mutex}; + +use crate::config::MachineConfig; +use crate::dev::serial_log::SerialLog; + +pub trait SerialBackend: Send + Sync { + fn send_byte(&self, byte: u8); + fn recv_byte(&self) -> io::Result; +} + +/// Drops TX bytes and never yields RX. Used as a placeholder when a channel +/// isn't wired to a host I/O source (e.g. CI mode unused channel). +pub(crate) struct NullBackend; + +impl SerialBackend for NullBackend { + fn send_byte(&self, _byte: u8) {} + fn recv_byte(&self) -> io::Result { + Err(io::Error::new(io::ErrorKind::WouldBlock, "null")) + } +} + +/// Tees guest-emitted bytes to a host file in addition to delegating to an +/// inner backend. Used by `--serial-log ` to capture the serial +/// console transcript while the real TCP listener on port 8881 keeps +/// working unchanged. +pub struct TeeBackend { + inner: Arc, + log: Arc, +} + +impl TeeBackend { + pub fn new(inner: Arc, log: Arc) -> Self { + Self { inner, log } + } +} + +impl SerialBackend for TeeBackend { + fn send_byte(&self, byte: u8) { + self.log.write(&[byte]); + self.inner.send_byte(byte); + } + fn recv_byte(&self) -> io::Result { + self.inner.recv_byte() + } +} + +#[cfg(unix)] +struct UnixSocketBackend { + listener: UnixListener, + stream: Mutex>, +} + +#[cfg(unix)] +impl UnixSocketBackend { + fn new>(path: P) -> Self { + let _ = std::fs::remove_file(&path); + let listener = UnixListener::bind(path).expect("Failed to bind serial socket"); + listener.set_nonblocking(true).expect("Failed to set nonblocking"); + Self { + listener, + stream: Mutex::new(None), + } + } +} + +#[cfg(unix)] +impl SerialBackend for UnixSocketBackend { + fn send_byte(&self, byte: u8) { + let mut stream_guard = self.stream.lock(); + if let Some(ref mut stream) = *stream_guard { + if let Err(e) = stream.write_all(&[byte]) { + if e.kind() != io::ErrorKind::WouldBlock { + *stream_guard = None; + } + } + } + } + + fn recv_byte(&self) -> io::Result { + let mut guard = self.stream.lock(); + + if guard.is_none() { + match self.listener.accept() { + Ok((socket, _)) => { + socket.set_nonblocking(true)?; + *guard = Some(socket); + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + return Err(io::Error::new(io::ErrorKind::WouldBlock, "No connection")); + } + Err(e) => return Err(e), + } + } + + if let Some(ref mut stream) = *guard { + let mut buf = [0u8; 1]; + match stream.read(&mut buf) { + Ok(1) => Ok(buf[0]), + Ok(_) => { // EOF + *guard = None; + Err(io::Error::new(io::ErrorKind::NotConnected, "EOF")) + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => Err(io::Error::new(io::ErrorKind::WouldBlock, "WouldBlock")), + Err(e) => { + *guard = None; + Err(e) + } + } + } else { + Err(io::Error::new(io::ErrorKind::WouldBlock, "No connection")) + } + } +} + +struct TcpConn { + stream: TcpStream, + telnet: crate::net::telnet::TelnetFilter, +} + +pub(crate) struct TcpSocketBackend { + pub(crate) listener: TcpListener, + /// Every connected client: each sees all the output and may type, so a + /// person at the console and a script can share it. + conns: Mutex>, +} + +impl TcpSocketBackend { + /// Bind a TCP serial backend. Returns `None` (rather than panicking) if the + /// port can't be bound — most commonly because a stale emulator process from + /// an earlier crash is still holding it. Callers fall back to a NullBackend + /// so the machine still boots; that serial channel is simply unavailable + /// until the port frees. + pub(crate) fn new(addr: A) -> Option { + let listener = match TcpListener::bind(addr) { + Ok(l) => l, + Err(e) => { + log::warn!("serial TCP backend disabled: failed to bind socket: {e}"); + return None; + } + }; + if let Err(e) = listener.set_nonblocking(true) { + log::warn!("serial TCP backend disabled: failed to set nonblocking: {e}"); + return None; + } + Some(Self { + listener, + conns: Mutex::new(Vec::new()), + }) + } + + /// Take any clients waiting to connect, offering each telnet character + /// mode. One whose handshake cannot be written is dropped; it can + /// reconnect. + fn accept_waiting(&self, conns: &mut Vec) { + while let Ok((socket, _)) = self.listener.accept() { + if socket.set_nonblocking(true).is_err() { + continue; + } + // The console is latency-sensitive: without TCP_NODELAY, Nagle + // holds a keystroke or a log line back until the previous + // segment is acked, which shows up as a stutter on every + // interaction. Set it on accept, before any byte is written. + let _ = socket.set_nodelay(true); + let mut c = TcpConn { stream: socket, telnet: crate::net::telnet::TelnetFilter::new() }; + if c.stream.write_all(&crate::net::telnet::TelnetFilter::initial_handshake()).is_ok() { + conns.push(c); + } + } + } +} + +impl SerialBackend for TcpSocketBackend { + fn send_byte(&self, byte: u8) { + let mut conns = self.conns.lock(); + if conns.is_empty() { + return; + } + let mut buf = Vec::with_capacity(2); + crate::net::telnet::escape_byte(byte, &mut buf); + conns.retain_mut(|c| match c.stream.write_all(&buf) { + Ok(()) => true, + Err(e) => e.kind() == io::ErrorKind::WouldBlock, + }); + } + + fn recv_byte(&self) -> io::Result { + let mut conns = self.conns.lock(); + self.accept_waiting(&mut conns); + if conns.is_empty() { + return Err(io::Error::new(io::ErrorKind::WouldBlock, "No connection")); + } + // Each client in turn: pull bytes through its telnet filter until a + // real data byte falls out or it has nothing more to give. A client + // that has gone away is dropped. + let mut i = 0; + while i < conns.len() { + let mut gone = false; + loop { + let c = &mut conns[i]; + let mut buf = [0u8; 1]; + match c.stream.read(&mut buf) { + Ok(1) => { + let mut reply = Vec::new(); + let data = c.telnet.feed(buf[0], &mut reply); + if !reply.is_empty() { + if let Err(e) = c.stream.write_all(&reply) { + if e.kind() != io::ErrorKind::WouldBlock { + gone = true; + break; + } + } + } + if let Some(d) = data { + return Ok(d); + } + // Telnet command consumed; keep reading. + } + Ok(_) => { + gone = true; + break; + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break, + Err(_) => { + gone = true; + break; + } + } + } + if gone { + conns.remove(i); + } else { + i += 1; + } + } + Err(io::Error::new(io::ErrorKind::WouldBlock, "WouldBlock")) + } +} + +/// A TCP serial console on 127.0.0.1:`port` (0 takes any free port), in +/// telnet character mode, for any device with a serial line: every client +/// connected sees the output and may type. Returns the backend and the port +/// it bound, or `None` when the port is taken. +pub fn tcp_serial_backend(port: u16) -> Option<(Arc, u16)> { + let b = TcpSocketBackend::new(("127.0.0.1", port))?; + let bound = b.listener.local_addr().ok()?.port(); + Some((Arc::new(b), bound)) +} + +/// A serial line wired to nothing: output is dropped, nothing arrives. +pub fn null_serial_backend() -> Arc { + Arc::new(NullBackend) +} + +// ============================================================================ +// CiSerialBackend — in-process serial backend used by --ci mode. +// ============================================================================ + +/// Serial backend that the CI control socket reads from and writes to. The +/// guest sees this as channel A (the IRIX console). Host pushes bytes into +/// `host_to_guest` via `push_host`; the existing RX thread drains them into +/// `channel_a.rx_queue`. Guest output reaches `send_byte`, which pushes into +/// `guest_to_host` and wakes anyone waiting in `wait_for`. +pub struct CiSerialBackend { + host_to_guest: Mutex>, + guest_to_host: Mutex>, + cv: Condvar, + /// Optional file mirror: every byte that flows through ttyd1 in either + /// direction is appended here. Host-injected bytes are echoed by IRIX + /// anyway, so writing only guest output already captures the full + /// transcript without duplication. + log: Mutex>>, +} + +impl CiSerialBackend { + pub fn new() -> Self { + Self { + host_to_guest: Mutex::new(VecDeque::new()), + guest_to_host: Mutex::new(Vec::new()), + cv: Condvar::new(), + log: Mutex::new(None), + } + } + + /// Attach the shared serial-log sink. The caller opens the file (and + /// picks the timestamp mode), so the CI and TCP paths write the same bytes. + pub fn set_log(&self, log: Arc) { + *self.log.lock() = Some(log); + } + + /// Inject bytes from host to guest (the harness typing on the console). + pub fn push_host(&self, data: &[u8]) { + let mut q = self.host_to_guest.lock(); + q.extend(data.iter().copied()); + } + + /// Drain everything the guest has produced since the last call. Empties + /// the buffer; the returned Vec is the guest output as raw bytes. + pub fn drain_guest(&self) -> Vec { + let mut q = self.guest_to_host.lock(); + std::mem::take(&mut *q) + } + + /// Block until `needle` is seen in guest output, or `timeout` expires. + /// On success returns the consumed bytes up to and including the match; + /// bytes that arrived after the match stay in the buffer for the next + /// `serial-read`. On timeout returns `None` without consuming anything. + pub fn wait_for(&self, needle: &[u8], timeout: Duration) -> Option> { + if needle.is_empty() { + return Some(Vec::new()); + } + let deadline = Instant::now() + timeout; + let mut q = self.guest_to_host.lock(); + loop { + if let Some(pos) = find_subseq(&q, needle) { + let end = pos + needle.len(); + let consumed: Vec = q.drain(..end).collect(); + return Some(consumed); + } + let now = Instant::now(); + if now >= deadline { + return None; + } + if self.cv.wait_until(&mut q, deadline).timed_out() { + // One more scan in case bytes arrived between the last check + // and the timeout. + if let Some(pos) = find_subseq(&q, needle) { + let end = pos + needle.len(); + let consumed: Vec = q.drain(..end).collect(); + return Some(consumed); + } + return None; + } + } + } + + /// Clear both queues. Called on `restore`/`rollback` so stale serial + /// output from the previous run doesn't leak into the next test. + pub fn reset(&self) { + self.host_to_guest.lock().clear(); + self.guest_to_host.lock().clear(); + } +} + +fn find_subseq(haystack: &[u8], needle: &[u8]) -> Option { + if needle.is_empty() || haystack.len() < needle.len() { + return None; + } + haystack.windows(needle.len()).position(|w| w == needle) +} + +impl SerialBackend for CiSerialBackend { + fn send_byte(&self, byte: u8) { + self.guest_to_host.lock().push(byte); + if let Some(log) = self.log.lock().as_ref() { + log.write(&[byte]); + } + self.cv.notify_all(); + } + + fn recv_byte(&self) -> io::Result { + let mut q = self.host_to_guest.lock(); + match q.pop_front() { + Some(b) => Ok(b), + None => Err(io::Error::new(io::ErrorKind::WouldBlock, "empty")), + } + } +} + +/// How [`open_log`] treats a transcript that is already there. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum LogMode { + /// One boot's transcript: truncate, so scripts tailing the file see only + /// this boot's prompts. + Fresh, + /// Keep what an earlier run wrote and add to it. + Append, +} + +/// Open `cfg.serial_log`, if one is set, as the sink the machine's CI and +/// TCP consoles both write through, so the two logs are the same bytes +/// (timestamps included). A file that cannot be opened is reported and the +/// machine boots without a log. `tag` names the machine in the messages. +pub fn open_log(cfg: &MachineConfig, tag: &str, mode: LogMode) -> Option> { + let path = cfg.serial_log.as_deref()?; + let p = Path::new(path); + let opened = match mode { + LogMode::Fresh => SerialLog::create(p, cfg.serial_log_timestamps), + LogMode::Append => SerialLog::open(p, cfg.serial_log_timestamps), + }; + match opened { + Ok(log) => { + eprintln!("iris: {tag} serial console mirroring to {path}"); + Some(log) + } + Err(e) => { + eprintln!("iris: {tag} serial log {path}: {e}"); + None + } + } +} + +/// The host ends of a machine's two serial ports: `(console, second)`. +/// +/// The console is the CI backend in CI mode, otherwise TCP on +/// `serial_port_a` (nothing, if the port is taken), either teed to +/// `serial_log`. The second port serves TCP on `serial_port_b`, or is `None` +/// when that port is taken, so the caller leaves it unwired. +pub fn console_backends( + cfg: &MachineConfig, + tag: &str, + ci: Option<&Arc>, +) -> (Arc, Option>) { + let [port_a, port_b] = cfg.serial_ports(); + let log = open_log(cfg, tag, LogMode::Fresh); + let console: Arc = if let Some(ci) = ci { + if let Some(log) = log { + ci.set_log(log); + } + ci.clone() + } else { + let mut backend = null_serial_backend(); + match tcp_serial_backend(port_a) { + Some((b, p)) => { + eprintln!("iris: {tag} serial port 1 on 127.0.0.1:{p}"); + backend = b; + } + None => eprintln!("iris: {tag} serial port 1: port {port_a} unavailable; is another iris using it? (serial_port_a)"), + } + match log { + Some(log) => Arc::new(TeeBackend::new(backend, log)), + None => backend, + } + }; + let second = match tcp_serial_backend(port_b) { + Some((b, p)) => { + eprintln!("iris: {tag} serial port 2 on 127.0.0.1:{p}"); + Some(b) + } + None => { + eprintln!("iris: {tag} serial port 2: port {port_b} unavailable; is another iris using it? (serial_port_b)"); + None + } + }; + (console, second) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The console is the CI backend in CI mode and TCP otherwise, either + /// teed to the same fresh log, and the second port comes up on its own. + #[test] + fn the_console_is_ci_or_tcp_and_both_log_the_same_bytes() { + let dir = std::env::temp_dir().join(format!("iris-serial-host-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let log = dir.join("serial.log"); + std::fs::write(&log, "stale").unwrap(); + let mut cfg = MachineConfig::default(); + cfg.serial_log = Some(log.to_string_lossy().into_owned()); + cfg.serial_port_a = Some(0); + cfg.serial_port_b = Some(0); + + let ci = Arc::new(CiSerialBackend::new()); + let (console, second) = console_backends(&cfg, "Test", Some(&ci)); + console.send_byte(b'c'); + assert_eq!(ci.drain_guest(), b"c"); + assert_eq!(std::fs::read(&log).unwrap(), b"c", "a fresh log, not the stale one"); + assert!(second.is_some()); + + let (console, _) = console_backends(&cfg, "Test", None); + console.send_byte(b't'); + assert_eq!(std::fs::read(&log).unwrap(), b"t"); + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/src/dev/z85c30.rs b/src/dev/z85c30.rs index 5007fcd60..859a42cad 100644 --- a/src/dev/z85c30.rs +++ b/src/dev/z85c30.rs @@ -15,7 +15,7 @@ use std::io::Write as IoWrite; use crate::traits::{BusRead8, BusRead16, BusRead32, BusRead64, BUS_OK, BUS_ERR, Device, Resettable, Saveable}; use crate::snapshot::{get_field, toml_u8, u8_slice_to_toml, load_u8_slice, hex_u8}; use crate::devlog::LogModule; -use crate::dev::serial_log::SerialLog; +use crate::dev::serial_host::{NullBackend, SerialBackend, TcpSocketBackend}; pub mod scc_regs { pub const WR0: u8 = 0; // Command/Pointer @@ -347,254 +347,6 @@ impl Channel { } } -pub trait SerialBackend: Send + Sync { - fn send_byte(&self, byte: u8); - fn recv_byte(&self) -> io::Result; -} - -/// Drops TX bytes and never yields RX. Used as a placeholder when a channel -/// isn't wired to a host I/O source (e.g. CI mode unused channel). -struct NullBackend; - -impl SerialBackend for NullBackend { - fn send_byte(&self, _byte: u8) {} - fn recv_byte(&self) -> io::Result { - Err(io::Error::new(io::ErrorKind::WouldBlock, "null")) - } -} - -/// Tees guest-emitted bytes to a host file in addition to delegating to an -/// inner backend. Used by `--serial-log ` to capture the serial -/// console transcript while the real TCP listener on port 8881 keeps -/// working unchanged. -pub struct TeeBackend { - inner: Arc, - log: Arc, -} - -impl TeeBackend { - pub fn new(inner: Arc, log: Arc) -> Self { - Self { inner, log } - } -} - -impl SerialBackend for TeeBackend { - fn send_byte(&self, byte: u8) { - self.log.write(&[byte]); - self.inner.send_byte(byte); - } - fn recv_byte(&self) -> io::Result { - self.inner.recv_byte() - } -} - -#[cfg(unix)] -struct UnixSocketBackend { - listener: UnixListener, - stream: Mutex>, -} - -#[cfg(unix)] -impl UnixSocketBackend { - fn new>(path: P) -> Self { - let _ = std::fs::remove_file(&path); - let listener = UnixListener::bind(path).expect("Failed to bind serial socket"); - listener.set_nonblocking(true).expect("Failed to set nonblocking"); - Self { - listener, - stream: Mutex::new(None), - } - } -} - -#[cfg(unix)] -impl SerialBackend for UnixSocketBackend { - fn send_byte(&self, byte: u8) { - let mut stream_guard = self.stream.lock(); - if let Some(ref mut stream) = *stream_guard { - if let Err(e) = stream.write_all(&[byte]) { - if e.kind() != io::ErrorKind::WouldBlock { - *stream_guard = None; - } - } - } - } - - fn recv_byte(&self) -> io::Result { - let mut guard = self.stream.lock(); - - if guard.is_none() { - match self.listener.accept() { - Ok((socket, _)) => { - socket.set_nonblocking(true)?; - *guard = Some(socket); - } - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { - return Err(io::Error::new(io::ErrorKind::WouldBlock, "No connection")); - } - Err(e) => return Err(e), - } - } - - if let Some(ref mut stream) = *guard { - let mut buf = [0u8; 1]; - match stream.read(&mut buf) { - Ok(1) => Ok(buf[0]), - Ok(_) => { // EOF - *guard = None; - Err(io::Error::new(io::ErrorKind::NotConnected, "EOF")) - } - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => Err(io::Error::new(io::ErrorKind::WouldBlock, "WouldBlock")), - Err(e) => { - *guard = None; - Err(e) - } - } - } else { - Err(io::Error::new(io::ErrorKind::WouldBlock, "No connection")) - } - } -} - -struct TcpConn { - stream: TcpStream, - telnet: crate::net::telnet::TelnetFilter, -} - -struct TcpSocketBackend { - listener: TcpListener, - /// Every connected client: each sees all the output and may type, so a - /// person at the console and a script can share it. - conns: Mutex>, -} - -impl TcpSocketBackend { - /// Bind a TCP serial backend. Returns `None` (rather than panicking) if the - /// port can't be bound — most commonly because a stale emulator process from - /// an earlier crash is still holding it. Callers fall back to a NullBackend - /// so the machine still boots; that serial channel is simply unavailable - /// until the port frees. - fn new(addr: A) -> Option { - let listener = match TcpListener::bind(addr) { - Ok(l) => l, - Err(e) => { - log::warn!("serial TCP backend disabled: failed to bind socket: {e}"); - return None; - } - }; - if let Err(e) = listener.set_nonblocking(true) { - log::warn!("serial TCP backend disabled: failed to set nonblocking: {e}"); - return None; - } - Some(Self { - listener, - conns: Mutex::new(Vec::new()), - }) - } - - /// Take any clients waiting to connect, offering each telnet character - /// mode. One whose handshake cannot be written is dropped; it can - /// reconnect. - fn accept_waiting(&self, conns: &mut Vec) { - while let Ok((socket, _)) = self.listener.accept() { - if socket.set_nonblocking(true).is_err() { - continue; - } - // The console is latency-sensitive: without TCP_NODELAY, Nagle - // holds a keystroke or a log line back until the previous - // segment is acked, which shows up as a stutter on every - // interaction. Set it on accept, before any byte is written. - let _ = socket.set_nodelay(true); - let mut c = TcpConn { stream: socket, telnet: crate::net::telnet::TelnetFilter::new() }; - if c.stream.write_all(&crate::net::telnet::TelnetFilter::initial_handshake()).is_ok() { - conns.push(c); - } - } - } -} - -impl SerialBackend for TcpSocketBackend { - fn send_byte(&self, byte: u8) { - let mut conns = self.conns.lock(); - if conns.is_empty() { - return; - } - let mut buf = Vec::with_capacity(2); - crate::net::telnet::escape_byte(byte, &mut buf); - conns.retain_mut(|c| match c.stream.write_all(&buf) { - Ok(()) => true, - Err(e) => e.kind() == io::ErrorKind::WouldBlock, - }); - } - - fn recv_byte(&self) -> io::Result { - let mut conns = self.conns.lock(); - self.accept_waiting(&mut conns); - if conns.is_empty() { - return Err(io::Error::new(io::ErrorKind::WouldBlock, "No connection")); - } - // Each client in turn: pull bytes through its telnet filter until a - // real data byte falls out or it has nothing more to give. A client - // that has gone away is dropped. - let mut i = 0; - while i < conns.len() { - let mut gone = false; - loop { - let c = &mut conns[i]; - let mut buf = [0u8; 1]; - match c.stream.read(&mut buf) { - Ok(1) => { - let mut reply = Vec::new(); - let data = c.telnet.feed(buf[0], &mut reply); - if !reply.is_empty() { - if let Err(e) = c.stream.write_all(&reply) { - if e.kind() != io::ErrorKind::WouldBlock { - gone = true; - break; - } - } - } - if let Some(d) = data { - return Ok(d); - } - // Telnet command consumed; keep reading. - } - Ok(_) => { - gone = true; - break; - } - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break, - Err(_) => { - gone = true; - break; - } - } - } - if gone { - conns.remove(i); - } else { - i += 1; - } - } - Err(io::Error::new(io::ErrorKind::WouldBlock, "WouldBlock")) - } -} - -/// A TCP serial console on 127.0.0.1:`port` (0 takes any free port), in -/// telnet character mode, for any device with a serial line: every client -/// connected sees the output and may type. Returns the backend and the port -/// it bound, or `None` when the port is taken. -pub fn tcp_serial_backend(port: u16) -> Option<(Arc, u16)> { - let b = TcpSocketBackend::new(("127.0.0.1", port))?; - let bound = b.listener.local_addr().ok()?.port(); - Some((Arc::new(b), bound)) -} - -/// A serial line wired to nothing: output is dropped, nothing arrives. -pub fn null_serial_backend() -> Arc { - Arc::new(NullBackend) -} - #[derive(Clone)] pub struct Z85c30 { pub channel_a: Arc<(Mutex, Condvar)>, @@ -1141,124 +893,10 @@ impl Saveable for Z85c30 { } } -// ============================================================================ -// CiSerialBackend — in-process serial backend used by --ci mode. -// ============================================================================ - -/// Serial backend that the CI control socket reads from and writes to. The -/// guest sees this as channel A (the IRIX console). Host pushes bytes into -/// `host_to_guest` via `push_host`; the existing RX thread drains them into -/// `channel_a.rx_queue`. Guest output reaches `send_byte`, which pushes into -/// `guest_to_host` and wakes anyone waiting in `wait_for`. -pub struct CiSerialBackend { - host_to_guest: Mutex>, - guest_to_host: Mutex>, - cv: Condvar, - /// Optional file mirror: every byte that flows through ttyd1 in either - /// direction is appended here. Host-injected bytes are echoed by IRIX - /// anyway, so writing only guest output already captures the full - /// transcript without duplication. - log: Mutex>>, -} - -impl CiSerialBackend { - pub fn new() -> Self { - Self { - host_to_guest: Mutex::new(VecDeque::new()), - guest_to_host: Mutex::new(Vec::new()), - cv: Condvar::new(), - log: Mutex::new(None), - } - } - - /// Attach the shared serial-log sink. The caller opens the file (and - /// picks the timestamp mode), so the CI and TCP paths write the same bytes. - pub fn set_log(&self, log: Arc) { - *self.log.lock() = Some(log); - } - - /// Inject bytes from host to guest (the harness typing on the console). - pub fn push_host(&self, data: &[u8]) { - let mut q = self.host_to_guest.lock(); - q.extend(data.iter().copied()); - } - - /// Drain everything the guest has produced since the last call. Empties - /// the buffer; the returned Vec is the guest output as raw bytes. - pub fn drain_guest(&self) -> Vec { - let mut q = self.guest_to_host.lock(); - std::mem::take(&mut *q) - } - - /// Block until `needle` is seen in guest output, or `timeout` expires. - /// On success returns the consumed bytes up to and including the match; - /// bytes that arrived after the match stay in the buffer for the next - /// `serial-read`. On timeout returns `None` without consuming anything. - pub fn wait_for(&self, needle: &[u8], timeout: Duration) -> Option> { - if needle.is_empty() { - return Some(Vec::new()); - } - let deadline = Instant::now() + timeout; - let mut q = self.guest_to_host.lock(); - loop { - if let Some(pos) = find_subseq(&q, needle) { - let end = pos + needle.len(); - let consumed: Vec = q.drain(..end).collect(); - return Some(consumed); - } - let now = Instant::now(); - if now >= deadline { - return None; - } - if self.cv.wait_until(&mut q, deadline).timed_out() { - // One more scan in case bytes arrived between the last check - // and the timeout. - if let Some(pos) = find_subseq(&q, needle) { - let end = pos + needle.len(); - let consumed: Vec = q.drain(..end).collect(); - return Some(consumed); - } - return None; - } - } - } - - /// Clear both queues. Called on `restore`/`rollback` so stale serial - /// output from the previous run doesn't leak into the next test. - pub fn reset(&self) { - self.host_to_guest.lock().clear(); - self.guest_to_host.lock().clear(); - } -} - -fn find_subseq(haystack: &[u8], needle: &[u8]) -> Option { - if needle.is_empty() || haystack.len() < needle.len() { - return None; - } - haystack.windows(needle.len()).position(|w| w == needle) -} - -impl SerialBackend for CiSerialBackend { - fn send_byte(&self, byte: u8) { - self.guest_to_host.lock().push(byte); - if let Some(log) = self.log.lock().as_ref() { - log.write(&[byte]); - } - self.cv.notify_all(); - } - - fn recv_byte(&self) -> io::Result { - let mut q = self.host_to_guest.lock(); - match q.pop_front() { - Some(b) => Ok(b), - None => Err(io::Error::new(io::ErrorKind::WouldBlock, "empty")), - } - } -} - #[cfg(test)] mod tests { use super::*; + use crate::dev::serial_host::CiSerialBackend; /// Two clients on one TCP serial console: both see every byte the guest /// sends, and what either types arrives, with telnet's own commands diff --git a/src/ip30_machine.rs b/src/ip30_machine.rs index 159bd9afb..ac94190e9 100644 --- a/src/ip30_machine.rs +++ b/src/ip30_machine.rs @@ -86,7 +86,7 @@ pub struct OctaneMachine { monitor: Arc, monitor_port: u16, /// The console UART's in-process host end in CI mode (`--ci`). - ci_serial: Option>, + ci_serial: Option>, pub bridge: Arc, pub ioc3: Arc, pub scsi: [Arc; 2], @@ -335,7 +335,7 @@ impl OctaneMachine { g.register_locks(); } let monitor_port = cfg.monitor_port.unwrap_or(crate::monitor::DEFAULT_PORT); - let ci_serial = cfg.ci.then(|| Arc::new(crate::dev::z85c30::CiSerialBackend::new())); + let ci_serial = cfg.ci.then(|| Arc::new(crate::dev::serial_host::CiSerialBackend::new())); Self::connect_serial(cfg, &ioc3, ci_serial.as_ref()); // The Ethernet on the NAT gateway `[network]` describes. @@ -400,43 +400,11 @@ impl OctaneMachine { /// IOC3's first port is the console: the CI socket's backend in CI /// mode, otherwise TCP on `serial_port_a`, either teed to `serial_log`. /// The second port serves TCP on `serial_port_b`. - fn connect_serial(cfg: &MachineConfig, ioc3: &Ioc3, ci: Option<&Arc>) { - use crate::dev::z85c30::{null_serial_backend, tcp_serial_backend, TeeBackend}; - let [port_a, port_b] = cfg.serial_ports(); - let log = cfg.serial_log.as_deref().and_then(|path| { - match crate::dev::serial_log::SerialLog::create(std::path::Path::new(path), cfg.serial_log_timestamps) { - Ok(log) => Some(log), - Err(e) => { - eprintln!("iris: Octane serial log {path}: {e}"); - None - } - } - }); - if let Some(ci) = ci { - if let Some(log) = log { - ci.set_log(log); - } - ioc3.set_serial_backend(0, ci.clone()); - } else { - let mut backend = null_serial_backend(); - match tcp_serial_backend(port_a) { - Some((b, p)) => { - eprintln!("iris: Octane serial port 1 on 127.0.0.1:{p}"); - backend = b; - } - None => eprintln!("iris: Octane serial port 1: port {port_a} unavailable (serial_port_a)"), - } - if let Some(log) = log { - backend = Arc::new(TeeBackend::new(backend, log)); - } - ioc3.set_serial_backend(0, backend); - } - match tcp_serial_backend(port_b) { - Some((b, p)) => { - eprintln!("iris: Octane serial port 2 on 127.0.0.1:{p}"); - ioc3.set_serial_backend(1, b); - } - None => eprintln!("iris: Octane serial port 2: port {port_b} unavailable (serial_port_b)"), + fn connect_serial(cfg: &MachineConfig, ioc3: &Ioc3, ci: Option<&Arc>) { + let (console, second) = crate::dev::serial_host::console_backends(cfg, "Octane", ci); + ioc3.set_serial_backend(0, console); + if let Some(b) = second { + ioc3.set_serial_backend(1, b); } } @@ -624,7 +592,7 @@ impl Workstation for OctaneMachine { fn fasttick_count(&self) -> Arc { self.cpu.fasttick_count() } - fn get_ci_serial(&self) -> Option> { + fn get_ci_serial(&self) -> Option> { self.ci_serial.clone() } fn inject_serial_console(&self, bytes: &[u8]) { diff --git a/src/ip32.rs b/src/ip32.rs index 054944406..a6f858db7 100644 --- a/src/ip32.rs +++ b/src/ip32.rs @@ -118,6 +118,7 @@ pub use crate::dev::mace_pci::*; pub use crate::dev::mace_ps2::*; pub use crate::dev::mace_sdma::*; pub use crate::dev::mace_ust::*; +pub use crate::dev::mace_uart::*; pub use crate::dev::ns16550::*; pub use crate::dev::mace_id::*; diff --git a/src/ip32/tests.rs b/src/ip32/tests.rs index 9cf86c727..88b725934 100644 --- a/src/ip32/tests.rs +++ b/src/ip32/tests.rs @@ -323,7 +323,7 @@ fn the_flash_is_readable_and_writable() { /// silently inert — every register reads zero and nothing is ever sent. #[test] fn the_uart_decodes_maces_register_spacing() { - let c = Com16550::new(); + let c = MaceUart::new(); // Transmit register is offset 0, so byte lane +7 of the first slot. c.write8((MACE_COM0 + 7) as u64, b'A'); assert_eq!(c.output(), "A"); @@ -340,7 +340,7 @@ fn the_uart_decodes_maces_register_spacing() { /// With DLAB set, writes to register 0 are the baud divisor, not characters. #[test] fn the_baud_divisor_is_not_mistaken_for_output() { - let c = Com16550::new(); + let c = MaceUart::new(); c.write8((MACE_COM0 + 0x307) as u64, com_reg::LCR_DLAB); c.write8((MACE_COM0 + 7) as u64, 0x0c); assert_eq!(c.output(), "", "divisor writes are not characters"); @@ -354,7 +354,7 @@ fn the_baud_divisor_is_not_mistaken_for_output() { /// prompt — which looks exactly like a hang. #[test] fn a_typed_character_becomes_readable() { - let c = Com16550::new(); + let c = MaceUart::new(); let lsr = (MACE_COM0 + 0x507) as u64; assert_eq!(c.read8(lsr).data & com_reg::LSR_DR, 0, "nothing typed yet"); diff --git a/src/ip32_machine.rs b/src/ip32_machine.rs index 06993845c..ceb690374 100644 --- a/src/ip32_machine.rs +++ b/src/ip32_machine.rs @@ -132,7 +132,7 @@ pub struct O2Machine { monitor_port: u16, /// The console UART's in-process host end in CI mode (`--ci`), which the /// CI socket types into and reads. - ci_serial: Option>, + ci_serial: Option>, /// The NAT's controls and the addresses it hands the guest (client, /// gateway, netmask), when the Ethernet is plugged in. nat: Option<(Arc, (std::net::Ipv4Addr, std::net::Ipv4Addr, std::net::Ipv4Addr))>, @@ -228,52 +228,11 @@ impl O2Machine { // the CI socket types into, otherwise TCP on `serial_port_a` (as the // Indy's channel A), either teed to `serial_log` when one is set. The // second port serves TCP on `serial_port_b`. - let [port_a, port_b] = cfg.serial_ports(); - let ci_serial = cfg.ci.then(|| Arc::new(crate::dev::z85c30::CiSerialBackend::new())); - if let Some(ci) = &ci_serial { - if let Some(path) = cfg.serial_log.as_deref() { - match crate::dev::serial_log::SerialLog::create( - std::path::Path::new(path), - cfg.serial_log_timestamps, - ) { - Ok(log) => { - ci.set_log(log); - eprintln!("iris: O2 serial console mirroring to {path}"); - } - Err(e) => eprintln!("iris: O2 serial log {path}: {e}"), - } - } - bus.mace.com0.set_backend(ci.clone()); - } else { - let mut backend = crate::dev::z85c30::null_serial_backend(); - match crate::dev::z85c30::tcp_serial_backend(port_a) { - Some((b, p)) => { - eprintln!("iris: O2 serial port 1 on 127.0.0.1:{p}"); - backend = b; - } - None => eprintln!("iris: O2 serial port 1: port {port_a} unavailable; is another iris using it? (serial_port_a)"), - } - if let Some(path) = &cfg.serial_log { - // A fresh log each boot, as the O2 has always kept it: scripts - // look at its tail for this boot's prompts. - match crate::dev::serial_log::SerialLog::create( - std::path::Path::new(path), - cfg.serial_log_timestamps, - ) { - Ok(log) => { - backend = Arc::new(crate::dev::z85c30::TeeBackend::new(backend.clone(), log)); - } - Err(e) => eprintln!("iris: O2 serial log {path}: {e}"), - } - } - bus.mace.com0.set_backend(backend); - } - match crate::dev::z85c30::tcp_serial_backend(port_b) { - Some((b, p)) => { - eprintln!("iris: O2 serial port 2 on 127.0.0.1:{p}"); - bus.mace.com1.set_backend(b); - } - None => eprintln!("iris: O2 serial port 2: port {port_b} unavailable; is another iris using it? (serial_port_b)"), + let ci_serial = cfg.ci.then(|| Arc::new(crate::dev::serial_host::CiSerialBackend::new())); + let (console, second) = crate::dev::serial_host::console_backends(cfg, "O2", ci_serial.as_ref()); + bus.mace.com0.set_backend(console); + if let Some(b) = second { + bus.mace.com1.set_backend(b); } // The Ethernet on the NAT gateway `[network]` describes (`mode = // "off"` leaves it unplugged; validate refuses pcap here for now). @@ -728,6 +687,12 @@ impl O2Machine { if m.schema_version > SCHEMA_VERSION { return Err(format!("snapshot schema_version {} is newer than this iris build supports ({SCHEMA_VERSION})", m.schema_version)); } + if m.schema_version < SCHEMA_VERSION { + return Err(format!( + "snapshot {name} is schema {}; the O2's UART state changed shape in schema 5, so it cannot be restored", + m.schema_version + )); + } // As the Indy: build, CPU and disks must match; // IRIS_SNAPSHOT_SKIP_CHECK=1 makes those warnings. let mut fatal = Vec::new(); @@ -884,7 +849,7 @@ impl Workstation for O2Machine { fn get_timer_manager(&self) -> Arc { O2Machine::get_timer_manager(self) } fn get_cpu_debug(&self) -> Arc { self.cpu.clone().debug_adapter() } fn fasttick_count(&self) -> Arc { self.cpu.fasttick_count() } - fn get_ci_serial(&self) -> Option> { self.ci_serial.clone() } + fn get_ci_serial(&self) -> Option> { self.ci_serial.clone() } /// Typed at serial port 1, the console with `graphics = false`. fn inject_serial_console(&self, bytes: &[u8]) { match &self.ci_serial { diff --git a/src/machine.rs b/src/machine.rs index c00998627..e6fb75c25 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -116,7 +116,7 @@ pub struct Machine { timer_manager: Arc, /// When `cfg.ci` is set, the channel-A backend is replaced by this /// in-process one so the CI control socket can drive the console. - ci_serial: Option>, + ci_serial: Option>, /// The shared `--serial-log` sink, kept so `stop` can flush a final /// partial line (wall-timestamp mode holds one until its newline). serial_log: Option>, @@ -402,27 +402,13 @@ impl Machine { // One sink for both serial paths, so the CI and TCP logs are the same // bytes (timestamps included). Opened once, before any peripheral // `start()` clones the current backend Arc into the RX/TX threads. - let serial_log = cfg.serial_log.as_deref().and_then(|path| { - match crate::dev::serial_log::SerialLog::open( - std::path::Path::new(path), - cfg.serial_log_timestamps, - ) { - Ok(log) => { - eprintln!("iris: serial console mirroring to {}", path); - Some(log) - } - Err(e) => { - eprintln!("iris: serial_log: failed to open {}: {}", path, e); - None - } - } - }); + let serial_log = crate::dev::serial_host::open_log(&cfg, "Indy", crate::dev::serial_host::LogMode::Append); // CI mode replaces the default TCP backend on channel B (tty1, the // SGI serial console) with an in-process backend the control socket // drives directly. Channel A (tty2) keeps its default TCP backend. let ci_serial = if ci_enabled { - let b = Arc::new(crate::dev::z85c30::CiSerialBackend::new()); + let b = Arc::new(crate::dev::serial_host::CiSerialBackend::new()); if let Some(log) = &serial_log { b.set_log(log.clone()); } @@ -434,7 +420,7 @@ impl Machine { // guest-emitted bytes also reach the file. if let Some(log) = &serial_log { let inner = ioc.scc().backend_b(); - ioc.scc().set_backend_b(Arc::new(crate::dev::z85c30::TeeBackend::new(inner, log.clone()))); + ioc.scc().set_backend_b(Arc::new(crate::dev::serial_host::TeeBackend::new(inner, log.clone()))); } None }; @@ -1176,7 +1162,7 @@ impl Machine { /// The in-process serial backend used by `--ci` mode. `None` in /// interactive mode. - pub fn get_ci_serial(&self) -> Option> { + pub fn get_ci_serial(&self) -> Option> { self.ci_serial.clone() } @@ -2054,7 +2040,7 @@ impl Workstation for Machine { fn get_timer_manager(&self) -> Arc { Machine::get_timer_manager(self) } fn get_cpu_debug(&self) -> Arc { Machine::get_cpu_debug(self) } fn fasttick_count(&self) -> Arc { Machine::fasttick_count(self) } - fn get_ci_serial(&self) -> Option> { Machine::get_ci_serial(self) } + fn get_ci_serial(&self) -> Option> { Machine::get_ci_serial(self) } fn inject_serial_console(&self, bytes: &[u8]) { Machine::inject_serial_console(self, bytes) } fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } diff --git a/src/snapshot.rs b/src/snapshot.rs index a25b94820..c5ef8f07f 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -40,7 +40,15 @@ use toml::Value; /// v3 → v4: the L1D cache tag moved to the Figure 11-4 field positions. A v3 /// reader would take a v4 tag as clean and drop the line without writing it /// back, so the bump is what makes it refuse the snapshot instead. -pub const SCHEMA_VERSION: u32 = 4; +/// +/// v4 → v5: each of the O2's MACE UARTs saves the 16550 core's registers, +/// receive FIFO and typed backlog where it saved eight registers and a queue. +/// A v4 reader would find none of it in a v5 snapshot and restore a console +/// with every UART register at zero under a running kernel; a v5 reader does +/// the same with a v4 O2 snapshot, so the O2 refuses those. Nothing else +/// changed shape: the serial DMA, the Indy's SCC and the rest are as before, +/// and the Octane has no snapshots. +pub const SCHEMA_VERSION: u32 = 5; const MANIFEST_FILE: &str = "snapshot.toml"; diff --git a/src/workstation.rs b/src/workstation.rs index ce1406c39..1cae3961c 100644 --- a/src/workstation.rs +++ b/src/workstation.rs @@ -24,7 +24,7 @@ use parking_lot::Mutex; use crate::config::MachineConfig; use crate::gdb_stub::CpuDebug; use crate::dev::ps2::Ps2Controller; -use crate::dev::z85c30::CiSerialBackend; +use crate::dev::serial_host::CiSerialBackend; use crate::gfx_display::GfxDisplay; use crate::hptimer::TimerManager; use crate::machine::Machine;