use super::interrupt::{InterruptController, bits};
use serde::{Deserialize, Serialize};
const CPU_FREQ: u32 = 16_777_216;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Sio {
siocnt: u16,
rcnt: u16,
transfer_active: bool,
remaining_cycles: u32,
}
impl Default for Sio {
fn default() -> Self {
Self::new()
}
}
impl Sio {
pub fn new() -> Self {
Self {
siocnt: 0,
rcnt: 0,
transfer_active: false,
remaining_cycles: 0,
}
}
pub fn read_siocnt(&self) -> u16 {
let mode = self.mode();
let mask = if mode == SioMode::Uart {
0x7FAF
} else {
0x7F8F
};
let val = match mode {
SioMode::Normal8 | SioMode::Normal32 | SioMode::Multi => {
if self.transfer_active {
self.siocnt | 0x0080
} else {
self.siocnt & !0x0080
}
}
SioMode::Uart | SioMode::Gpio => self.siocnt,
};
val & mask
}
pub fn write_siocnt(&mut self, value: u16) -> bool {
let old_mode = self.mode();
self.siocnt = value;
if self.mode() != old_mode {
self.transfer_active = false;
self.remaining_cycles = 0;
}
let mode = self.mode();
if matches!(mode, SioMode::Normal8 | SioMode::Normal32 | SioMode::Multi) {
self.siocnt &= !0x0080;
}
let was_busy = self.transfer_active;
let mut started = false;
if !was_busy
&& value & 0x0080 != 0
&& matches!(mode, SioMode::Normal8 | SioMode::Normal32 | SioMode::Multi)
{
self.transfer_active = true;
started = true;
match mode {
SioMode::Normal8 | SioMode::Normal32 => {
self.remaining_cycles = self.normal_transfer_cycles();
}
SioMode::Multi => {
self.remaining_cycles = u32::MAX;
}
SioMode::Uart | SioMode::Gpio => unreachable!(),
}
}
started
}
pub fn step(&mut self, cycles: u32, ic: &mut InterruptController) {
if !self.transfer_active {
return;
}
if self.mode() == SioMode::Multi {
return;
}
self.remaining_cycles = self.remaining_cycles.saturating_sub(cycles);
if self.remaining_cycles == 0 {
self.transfer_active = false;
if self.siocnt & 0x4000 != 0 {
ic.raise(bits::SERIAL);
}
}
}
pub fn write_rcnt(&mut self, value: u16) {
self.rcnt = value;
}
fn mode(&self) -> SioMode {
if self.rcnt & 0x8000 != 0 {
return SioMode::Gpio;
}
match (self.siocnt >> 12) & 3 {
0 => SioMode::Normal8,
1 => SioMode::Normal32,
2 => SioMode::Multi,
3 => SioMode::Uart,
_ => unreachable!(),
}
}
fn normal_transfer_cycles(&self) -> u32 {
let bits = match self.mode() {
SioMode::Normal8 => 8,
SioMode::Normal32 => 32,
_ => return 0,
};
let clock_khz: u32 = if self.siocnt & 0x0002 != 0 {
2048 } else {
256 };
bits * CPU_FREQ / (clock_khz * 1024)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SioMode {
Normal8,
Normal32,
Multi,
Uart,
Gpio,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_sio() -> Sio {
Sio::new()
}
fn make_ic() -> InterruptController {
InterruptController::new()
}
#[test]
fn normal8_256khz_takes_512_cycles() {
let mut sio = make_sio();
sio.siocnt = 0x0000;
assert_eq!(sio.normal_transfer_cycles(), 512);
}
#[test]
fn normal8_2mhz_takes_64_cycles() {
let mut sio = make_sio();
sio.siocnt = 0x0002;
assert_eq!(sio.normal_transfer_cycles(), 64);
}
#[test]
fn normal32_256khz_takes_2048_cycles() {
let mut sio = make_sio();
sio.siocnt = 0x1000;
assert_eq!(sio.normal_transfer_cycles(), 2048);
}
#[test]
fn normal32_2mhz_takes_256_cycles() {
let mut sio = make_sio();
sio.siocnt = 0x1002;
assert_eq!(sio.normal_transfer_cycles(), 256);
}
#[test]
fn write_siocnt_bit7_starts_normal_transfer() {
let mut sio = make_sio();
sio.write_siocnt(0x0080);
assert!(sio.transfer_active);
assert_eq!(sio.remaining_cycles, 512);
}
#[test]
fn write_siocnt_without_bit7_does_not_start_transfer() {
let mut sio = make_sio();
sio.write_siocnt(0x0000);
assert!(!sio.transfer_active);
}
#[test]
fn write_siocnt_multi_mode_stays_busy_indefinitely() {
let mut sio = make_sio();
sio.write_siocnt(0x2080);
assert!(sio.transfer_active);
let mut ic = make_ic();
sio.step(100_000, &mut ic);
assert!(sio.transfer_active);
assert_ne!(sio.read_siocnt() & 0x0080, 0);
}
#[test]
fn switching_from_busy_multi_to_normal_allows_new_normal_transfer() {
let mut sio = make_sio();
sio.write_siocnt(0x6080);
assert!(sio.transfer_active);
sio.write_siocnt(0x4001);
sio.write_siocnt(0x4081);
assert!(sio.transfer_active);
assert_eq!(sio.remaining_cycles, 512);
}
#[test]
fn normal_transfer_completes_after_exact_cycles() {
let mut sio = make_sio();
let mut ic = make_ic();
sio.write_siocnt(0x0082); assert!(sio.transfer_active);
sio.step(63, &mut ic);
assert!(sio.transfer_active);
assert_ne!(sio.read_siocnt() & 0x0080, 0);
sio.step(1, &mut ic);
assert!(!sio.transfer_active);
assert_eq!(sio.read_siocnt() & 0x0080, 0);
}
#[test]
fn normal_transfer_completes_with_overshoot() {
let mut sio = make_sio();
let mut ic = make_ic();
sio.write_siocnt(0x1080);
sio.step(5000, &mut ic);
assert!(!sio.transfer_active);
assert_eq!(sio.read_siocnt() & 0x0080, 0);
}
#[test]
fn completion_raises_irq_when_bit14_set() {
let mut sio = make_sio();
let mut ic = make_ic();
ic.ie = bits::SERIAL;
ic.ime = true;
sio.write_siocnt(0x4082);
sio.step(64, &mut ic);
assert_ne!(ic.if_flags & bits::SERIAL, 0, "IRQ_SIO should be raised");
}
#[test]
fn completion_does_not_raise_irq_when_bit14_clear() {
let mut sio = make_sio();
let mut ic = make_ic();
ic.ie = bits::SERIAL;
ic.ime = true;
sio.write_siocnt(0x0082);
sio.step(64, &mut ic);
assert_eq!(
ic.if_flags & bits::SERIAL,
0,
"IRQ_SIO should not be raised"
);
}
#[test]
fn read_siocnt_reflects_bit7_during_transfer() {
let mut sio = make_sio();
sio.write_siocnt(0x0080); assert_ne!(sio.read_siocnt() & 0x0080, 0);
}
#[test]
fn read_siocnt_reflects_bit7_cleared_after_completion() {
let mut sio = make_sio();
let mut ic = make_ic();
sio.write_siocnt(0x0082); sio.step(64, &mut ic);
assert_eq!(sio.read_siocnt() & 0x0080, 0);
}
#[test]
fn read_siocnt_preserves_other_bits() {
let mut sio = make_sio();
sio.write_siocnt(0x4002);
let val = sio.read_siocnt();
assert_ne!(val & 0x4000, 0, "bit 14 (IRQ enable) should be preserved");
assert_ne!(val & 0x0002, 0, "bit 1 (clock) should be preserved");
}
#[test]
fn read_siocnt_preserves_written_bit7_in_uart_mode() {
let mut sio = make_sio();
sio.write_siocnt(0xF080);
assert_ne!(sio.read_siocnt() & 0x0080, 0);
assert!(!sio.transfer_active);
}
#[test]
fn read_siocnt_preserves_written_bit7_in_gpio_mode() {
let mut sio = make_sio();
sio.write_rcnt(0x8000);
sio.write_siocnt(0xC080);
assert_ne!(sio.read_siocnt() & 0x0080, 0);
assert!(!sio.transfer_active);
}
}