From 555f25f7ee92d7a74db6bb8d52e812dc7a79d41e Mon Sep 17 00:00:00 2001 From: James Braid Date: Thu, 29 Oct 2026 01:27:04 -0700 Subject: [PATCH 1/3] serial: move the host-side backends out of z85c30 and share the console wiring SerialBackend, the TCP, CI, tee and null backends lived in the Z85C30's file, so the O2's MACE UARTs and the Octane's IOC3 imported them from a chip they do not contain. Move them to dev/serial_host.rs. The O2 and Octane constructors each carried ~40 lines choosing the CI or TCP console, teeing it to the serial log and binding the second port. console_backends() does that once; open_log() opens the log (fresh per boot for those two, appended for the Indy as before) and is the Indy's only use of the helper, since its console is the Z85C30's existing channel B listener. No guest-visible change. Log messages now carry the machine name ("Indy" for the Indy's two), and the Octane's port-unavailable message gains the O2's "is another iris using it" hint. --- src/ci.rs | 2 +- src/dev/ioc3.rs | 2 +- src/dev/mod.rs | 1 + src/dev/ns16550.rs | 8 +- src/dev/serial_host.rs | 463 +++++++++++++++++++++++++++++++++++++++++ src/dev/z85c30.rs | 366 +------------------------------- src/ip30_machine.rs | 48 +---- src/ip32_machine.rs | 55 +---- src/machine.rs | 26 +-- src/workstation.rs | 2 +- 10 files changed, 494 insertions(+), 479 deletions(-) create mode 100644 src/dev/serial_host.rs diff --git a/src/ci.rs b/src/ci.rs index 9b04aed5..0bd3bb54 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 22fb5aa9..ce9caeed 100644 --- a/src/dev/ioc3.rs +++ b/src/dev/ioc3.rs @@ -57,7 +57,7 @@ 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::serial_host::{null_serial_backend, SerialBackend}; use crate::irq::{InterruptController, IrqLine}; use crate::pci::{Bar, PciBusDevice, PciConfig, PciDma, PciDmaStatus, PCI_INTA}; use crate::hptimer::{TimerId, TimerManager, TimerReturn}; diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 8b2f5e2a..89b14ac6 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -27,6 +27,7 @@ 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 f4856bb4..9418485b 100644 --- a/src/dev/ns16550.rs +++ b/src/dev/ns16550.rs @@ -4,7 +4,7 @@ use std::sync::Mutex; use std::io::Write; -use crate::dev::z85c30::SerialBackend; +use crate::dev::serial_host::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::*; @@ -102,7 +102,7 @@ impl Com16550 { Self { out: Mutex::new(std::collections::VecDeque::new()), sent: std::sync::atomic::AtomicUsize::new(0), - backend: Mutex::new(crate::dev::z85c30::null_serial_backend()), + backend: Mutex::new(crate::dev::serial_host::null_serial_backend()), input: Default::default(), regs: Mutex::new([0u8; 8]), name, @@ -111,7 +111,7 @@ impl Com16550 { } /// Wire the host end of the line: a TCP console - /// (`z85c30::tcp_serial_backend`), a `TeeBackend` logging to a file, or + /// (`serial_host::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; @@ -442,7 +442,7 @@ mod tests { 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"); + let (backend, port) = crate::dev::serial_host::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(); diff --git a/src/dev/serial_host.rs b/src/dev/serial_host.rs new file mode 100644 index 00000000..8d7ffa19 --- /dev/null +++ b/src/dev/serial_host.rs @@ -0,0 +1,463 @@ +//! 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) +} diff --git a/src/dev/z85c30.rs b/src/dev/z85c30.rs index 5007fcd6..859a42ca 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 159bd9af..ac94190e 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_machine.rs b/src/ip32_machine.rs index 06993845..45f42add 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). @@ -884,7 +843,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 c0099862..e6fb75c2 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/workstation.rs b/src/workstation.rs index ce1406c3..1cae3961 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; From 6c92904110f1ab75aff794429354abe74d35588c Mon Sep 17 00:00:00 2001 From: James Braid Date: Thu, 29 Oct 2026 01:46:46 -0700 Subject: [PATCH 2/3] mace: run the O2's UARTs on a port of QEMU's 16550A core The O2's 16550 was "just enough to be written to": the line status always said ready, the interrupt cause was a guess from the enable bits, there was one byte of receive queue with no overrun, and loopback only reached the modem status. The Octane's IOC3 carried a separate, fuller 16550 of its own. ns16550.rs is now a port of QEMU's hw/char/serial.c (MIT, notice kept): the register file, the 16-byte receive FIFO and its trigger levels, interrupt causes in priority order, the character timeout, line and modem status, loopback and overrun. Transmit stays instant, with no baud pacing; the module doc lists every place it departs from QEMU. The MACE shell (the lane decode, interrupt line and monitor) moves to mace_uart.rs, and the serial DMA engine keeps its own code, taking its characters from the core through dma_write, dma_read and dma_modem_control. Snapshots: each O2 UART now saves the core's state, so the schema version goes to 5 and an O2 snapshot from before is refused. Nothing else changed shape. Guest-visible changes on the O2 serial: the FIFO, the trigger levels and the timeout work as on a 16550A; the receive-data interrupt waits for the trigger level; the line status reports overruns and the transmit interrupt follows the holding register. --- CHANGELOG.md | 7 + HACKING.md | 9 + src/dev/mace.rs | 14 +- src/dev/mace_sdma.rs | 7 +- src/dev/mace_uart.rs | 286 ++++++++++ src/dev/mod.rs | 1 + src/dev/ns16550.rs | 1192 +++++++++++++++++++++++++++------------- src/dev/serial_host.rs | 31 ++ src/ip32.rs | 1 + src/ip32/tests.rs | 6 +- src/ip32_machine.rs | 6 + src/snapshot.rs | 10 +- 12 files changed, 1165 insertions(+), 405 deletions(-) create mode 100644 src/dev/mace_uart.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 719025b2..f136ed3b 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 20023ffa..6edcaa25 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/dev/mace.rs b/src/dev/mace.rs index 609d52ab..460036b4 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 040d0c72..2eb03c8e 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 00000000..87a4345a --- /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 89b14ac6..6b462607 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -22,6 +22,7 @@ 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; diff --git a/src/dev/ns16550.rs b/src/dev/ns16550.rs index 9418485b..37697b49 100644 --- a/src/dev/ns16550.rs +++ b/src/dev/ns16550.rs @@ -1,47 +1,78 @@ -//! The O2's two 16550 UARTs in MACE's ISA space. - -use std::sync::Mutex; - -use std::io::Write; - -use crate::dev::serial_host::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. +//! * 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 +84,826 @@ 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::serial_host::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 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 - /// (`serial_host::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 { + self.lsr &= !(LSR_DR | LSR_BI); + self.rbr + }; + 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::serial_host::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 index 8d7ffa19..a4a28aa3 100644 --- a/src/dev/serial_host.rs +++ b/src/dev/serial_host.rs @@ -461,3 +461,34 @@ pub fn console_backends( }; (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/ip32.rs b/src/ip32.rs index 05494440..a6f858db 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 9cf86c72..88b72593 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 45f42add..ceb69037 100644 --- a/src/ip32_machine.rs +++ b/src/ip32_machine.rs @@ -687,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(); diff --git a/src/snapshot.rs b/src/snapshot.rs index a25b9482..c5ef8f07 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"; From 1378781e345f52fd4eda66755c63ab322a312846 Mon Sep 17 00:00:00 2001 From: James Braid Date: Thu, 29 Oct 2026 01:49:42 -0700 Subject: [PATCH 3/3] ioc3: run the Octane's UARTs on the shared 16550 core The IOC3 carried its own 16550 next to the O2's. It now wraps ns16550.rs and keeps only what the chip adds around it: the pre-divider on the scratch register, the auto-flow bit in modem control, the factory-test writes to line and modem status, and the register layout with its ring DMA. The pre-divider sets the core's reference clock, so the receive timeout follows it. Ported from hw/misc/sgi-ioc3.c in jamesbraid/qemu-sgi-origami. The register-level tests written before the swap pass unchanged. To keep one of them (an empty receive register reads zero, as both earlier models answered) the core returns zero rather than QEMU's stale character; the module doc lists it. Guest-visible changes: with the FIFOs on, the receive interrupt waits for the trigger level and characters below it raise the timeout cause after four character times; toggling the FIFO enable flushes both FIFOs. --- src/dev/ioc3.rs | 240 ++++++------------------------------------ src/dev/ioc3_tests.rs | 5 +- src/dev/ns16550.rs | 9 +- 3 files changed, 43 insertions(+), 211 deletions(-) diff --git a/src/dev/ioc3.rs b/src/dev/ioc3.rs index ce9caeed..d4806308 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::serial_host::{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 5cd8e129..c758a5c2 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/ns16550.rs b/src/dev/ns16550.rs index 37697b49..8676af8b 100644 --- a/src/dev/ns16550.rs +++ b/src/dev/ns16550.rs @@ -48,6 +48,8 @@ //! * 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 @@ -246,6 +248,7 @@ impl Ns16550 { // ----- 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 } @@ -527,8 +530,12 @@ impl Ns16550 { 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); - self.rbr + ret }; self.update_irq(); ret