use std::collections::VecDeque;
pub const COM1_BASE: u16 = 0x3f8;
pub const COM1_IRQ: u32 = 4;
const IER_ERBFI: u8 = 0x01; const IER_ETBEI: u8 = 0x02;
const IIR_NONE: u8 = 0x01; const IIR_THRE: u8 = 0x02; const IIR_RDA: u8 = 0x04;
const LSR_DR: u8 = 0x01; const LSR_THRE: u8 = 0x20; const LSR_TEMT: u8 = 0x40;
const LCR_DLAB: u8 = 0x80; const MCR_LOOP: u8 = 0x10;
#[derive(Default)]
pub struct Com1 {
ier: u8,
lcr: u8,
mcr: u8,
scr: u8,
dll: u8,
dlm: u8,
rx: VecDeque<u8>,
thre_pending: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Com1State {
pub ier: u8,
pub lcr: u8,
pub mcr: u8,
pub scr: u8,
pub dll: u8,
pub dlm: u8,
}
impl Com1 {
pub fn new() -> Self {
Self::default()
}
pub fn snapshot(&self) -> Com1State {
Com1State {
ier: self.ier,
lcr: self.lcr,
mcr: self.mcr,
scr: self.scr,
dll: self.dll,
dlm: self.dlm,
}
}
pub fn restore(&mut self, s: &Com1State) {
self.ier = s.ier;
self.lcr = s.lcr;
self.mcr = s.mcr;
self.scr = s.scr;
self.dll = s.dll;
self.dlm = s.dlm;
}
fn dlab(&self) -> bool {
self.lcr & LCR_DLAB != 0
}
pub fn push_rx(&mut self, byte: u8) {
self.rx.push_back(byte);
}
pub fn write(&mut self, port: u16, v: u8) -> Option<u8> {
match port - COM1_BASE {
0 if self.dlab() => self.dll = v, 0 => {
if self.mcr & MCR_LOOP != 0 {
self.rx.push_back(v);
} else {
if self.ier & IER_ETBEI != 0 {
self.thre_pending = true;
}
return Some(v);
}
}
1 if self.dlab() => self.dlm = v, 1 => {
self.ier = v;
if v & IER_ETBEI != 0 {
self.thre_pending = true;
}
}
2 => {} 3 => self.lcr = v,
4 => self.mcr = v,
5 | 6 => {} 7 => self.scr = v,
_ => {}
}
None
}
pub fn read(&mut self, port: u16) -> u8 {
match port - COM1_BASE {
0 if self.dlab() => self.dll,
0 => self.rx.pop_front().unwrap_or(0), 1 if self.dlab() => self.dlm,
1 => self.ier,
2 => self.iir(), 3 => self.lcr,
4 => self.mcr,
5 => self.lsr(),
6 => 0xb0, 7 => self.scr,
_ => 0,
}
}
fn iir(&mut self) -> u8 {
if self.ier & IER_ERBFI != 0 && !self.rx.is_empty() {
IIR_RDA
} else if self.ier & IER_ETBEI != 0 && self.thre_pending {
self.thre_pending = false;
IIR_THRE
} else {
IIR_NONE
}
}
fn lsr(&self) -> u8 {
let mut s = LSR_THRE | LSR_TEMT;
if !self.rx.is_empty() {
s |= LSR_DR;
}
s
}
pub fn irq_line(&self) -> bool {
(self.ier & IER_ERBFI != 0 && !self.rx.is_empty())
|| (self.ier & IER_ETBEI != 0 && self.thre_pending)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn wr(c: &mut Com1, off: u16, v: u8) -> Option<u8> {
c.write(COM1_BASE + off, v)
}
fn rd(c: &mut Com1, off: u16) -> u8 {
c.read(COM1_BASE + off)
}
#[test]
fn thr_write_transmits_byte() {
let mut c = Com1::new();
assert_eq!(wr(&mut c, 0, b'K'), Some(b'K'));
assert_eq!(wr(&mut c, 0, b'\n'), Some(b'\n'));
}
#[test]
fn dlab_routes_thr_ier_to_divisor_latches() {
let mut c = Com1::new();
wr(&mut c, 3, LCR_DLAB); assert_eq!(wr(&mut c, 0, 0x01), None); assert_eq!(wr(&mut c, 1, 0x00), None); assert_eq!(rd(&mut c, 0), 0x01); assert_eq!(rd(&mut c, 1), 0x00); wr(&mut c, 3, 0); assert_eq!(wr(&mut c, 0, b'A'), Some(b'A')); }
#[test]
fn lsr_reports_thre_always_and_dr_with_rx() {
let mut c = Com1::new();
assert_eq!(rd(&mut c, 5) & (LSR_THRE | LSR_TEMT), LSR_THRE | LSR_TEMT);
assert_eq!(rd(&mut c, 5) & LSR_DR, 0);
c.push_rx(b'x');
assert_eq!(rd(&mut c, 5) & LSR_DR, LSR_DR);
}
#[test]
fn rbr_pops_rx_in_order_and_clears_dr() {
let mut c = Com1::new();
c.push_rx(b'h');
c.push_rx(b'i');
assert_eq!(rd(&mut c, 0), b'h');
assert_eq!(rd(&mut c, 0), b'i');
assert_eq!(rd(&mut c, 5) & LSR_DR, 0); assert_eq!(rd(&mut c, 0), 0); }
#[test]
fn tx_interrupt_asserts_and_iir_reports_then_clears_thre() {
let mut c = Com1::new();
assert!(!c.irq_line());
wr(&mut c, 1, IER_ETBEI); assert!(c.irq_line(), "THRE int should latch when ETBEI enabled");
assert_eq!(rd(&mut c, 2), IIR_THRE); assert!(!c.irq_line(), "...and reading IIR clears it");
assert_eq!(wr(&mut c, 0, b'z'), Some(b'z'));
assert!(c.irq_line());
assert_eq!(rd(&mut c, 2), IIR_THRE);
assert!(!c.irq_line());
}
#[test]
fn rx_interrupt_asserts_while_data_and_enabled() {
let mut c = Com1::new();
wr(&mut c, 1, IER_ERBFI); assert!(!c.irq_line()); c.push_rx(b'q');
assert!(c.irq_line());
assert_eq!(rd(&mut c, 2), IIR_RDA); assert!(c.irq_line(), "RDA stays asserted until RBR is read");
assert_eq!(rd(&mut c, 0), b'q'); assert!(!c.irq_line()); }
#[test]
fn rda_outranks_thre_in_iir() {
let mut c = Com1::new();
wr(&mut c, 1, IER_ERBFI | IER_ETBEI); c.push_rx(b'r');
assert_eq!(rd(&mut c, 2), IIR_RDA, "RX has priority over THRE");
}
#[test]
fn loopback_feeds_tx_into_rx() {
let mut c = Com1::new();
wr(&mut c, 4, MCR_LOOP); assert_eq!(wr(&mut c, 0, b'L'), None, "loopback byte is not emitted");
assert_eq!(rd(&mut c, 5) & LSR_DR, LSR_DR);
assert_eq!(rd(&mut c, 0), b'L'); }
#[test]
fn scratch_register_round_trips() {
let mut c = Com1::new();
wr(&mut c, 7, 0xa5);
assert_eq!(rd(&mut c, 7), 0xa5);
}
}