use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{BusError, Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::sched::{AccessKind, LazyHandle};
use crate::core::space::{
AccessConstraints, MemAttrs, MemOps, MemResult, Region as MmioRegion, RegionRef,
};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{AtomicU64, LockRank, Mutex, Ordering};
use crate::core::value::Width;
use crate::core::wire::{Level, WireSource};
pub const REGISTER_BASE: u64 = 0xff01;
pub const REGISTER_LEN: u64 = 2;
pub const REGISTER_REGION: &str = "regs";
pub const IRQ_PIN: &str = "irq";
pub const TRANSFER_CLOCKS: u64 = 512 * 8;
pub const TRANSCRIPT_LIMIT: usize = 64 * 1024;
#[derive(Debug, Clone, Copy, Default)]
struct Regs {
data: u8,
control: u8,
remaining: u64,
}
impl Regs {
fn transferring(&self) -> bool {
self.control & 0x80 != 0
}
fn internal_clock(&self) -> bool {
self.control & 0x01 != 0
}
}
struct Shared {
regs: Mutex<Regs>,
transcript: Mutex<Vec<u8>>,
irq: Mutex<Option<WireSource>>,
lazy: Mutex<Option<LazyHandle>>,
tick: AtomicU64,
next_event: AtomicU64,
}
impl fmt::Debug for Shared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Shared").field("regs", &self.regs).finish()
}
}
impl Shared {
fn publish(&self, regs: &Regs, now: u64) {
self.tick.store(now, Ordering::Relaxed);
let event = if regs.transferring() && regs.internal_clock() {
now + regs.remaining.max(1)
} else {
u64::MAX
};
self.next_event.store(event, Ordering::Relaxed);
}
fn sync(&self, attrs: MemAttrs) {
let handle = self.lazy.lock().clone();
let Some(handle) = handle else {
return;
};
let kind = if attrs.debug {
AccessKind::Debug
} else {
AccessKind::Guest
};
let _ = handle.sync(kind);
}
fn push(&self, byte: u8) {
let mut transcript = self.transcript.lock();
if transcript.len() >= TRANSCRIPT_LIMIT {
transcript.remove(0);
}
transcript.push(byte);
}
fn drive_irq(&self) {
let source = self.irq.lock().clone();
if let Some(source) = source {
source.set(Level::High);
source.set(Level::Low);
}
}
}
pub struct GbSerial {
shared: Arc<Shared>,
regs_region: RegionRef,
}
impl fmt::Debug for GbSerial {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GbSerial")
.field("regs", &self.shared.regs)
.finish_non_exhaustive()
}
}
impl Default for GbSerial {
fn default() -> Self {
GbSerial::new()
}
}
impl GbSerial {
#[must_use]
pub fn new() -> GbSerial {
let shared = Arc::new(Shared {
regs: Mutex::with_rank(LockRank::DEVICE, Regs::default()),
transcript: Mutex::new(Vec::new()),
irq: Mutex::with_rank(LockRank::WIRE, None),
lazy: Mutex::new(None),
tick: AtomicU64::new(0),
next_event: AtomicU64::new(u64::MAX),
});
let regs_region = Arc::new(MmioRegion::io(
"gb.serial.regs",
REGISTER_LEN,
Arc::new(SerialPort {
shared: Arc::clone(&shared),
}) as Arc<dyn MemOps>,
));
GbSerial {
shared,
regs_region,
}
}
pub fn from_props(props: &Props) -> Result<GbSerial> {
let mut r = props.reader();
let _ = r.or_enum("link", "none", &["none"])?;
r.finish()?;
Ok(GbSerial::new())
}
#[must_use]
pub fn transcript(&self) -> Vec<u8> {
self.shared.transcript.lock().clone()
}
#[must_use]
pub fn transcript_text(&self) -> String {
let bytes = self.transcript();
bytes.iter().map(|b| *b as char).collect()
}
pub fn clear_transcript(&self) {
self.shared.transcript.lock().clear();
}
#[must_use]
pub fn data(&self) -> u8 {
self.shared.regs.lock().data
}
#[must_use]
pub fn control(&self) -> u8 {
self.shared.regs.lock().control | 0x7e
}
pub fn attach_irq(&self, source: WireSource) {
*self.shared.irq.lock() = Some(source);
}
pub fn attach_lazy(&self, handle: LazyHandle) {
*self.shared.lazy.lock() = Some(handle);
}
pub fn advance_to(&self, target: u64) {
let completed = {
let mut regs = self.shared.regs.lock();
let now = self.shared.tick.load(Ordering::Relaxed);
let elapsed = target.saturating_sub(now);
let mut completed = false;
if regs.transferring() && regs.internal_clock() {
if elapsed >= regs.remaining {
regs.remaining = 0;
regs.data = 0xff;
regs.control &= !0x80;
completed = true;
} else {
regs.remaining -= elapsed;
}
}
self.shared.publish(®s, target);
completed
};
if completed {
self.shared.drive_irq();
}
}
}
struct SerialPort {
shared: Arc<Shared>,
}
impl fmt::Debug for SerialPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SerialPort").finish_non_exhaustive()
}
}
impl MemOps for SerialPort {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let [byte] = dst else {
return Err(BusError::BadAccess);
};
self.shared.sync(attrs);
let regs = self.shared.regs.lock();
*byte = if offset == 0 {
regs.data
} else {
regs.control | 0x7e
};
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let [value] = src else {
return Err(BusError::BadAccess);
};
if attrs.debug {
return Err(BusError::BadAccess);
}
self.shared.sync(attrs);
let mut regs = self.shared.regs.lock();
if offset == 0 {
regs.data = *value;
} else {
let starting = *value & 0x80 != 0;
let byte = regs.data;
regs.control = *value & 0x81;
if starting {
regs.remaining = TRANSFER_CLOCKS;
drop(regs);
self.shared.push(byte);
regs = self.shared.regs.lock();
}
}
let now = self.shared.tick.load(Ordering::Relaxed);
self.shared.publish(®s, now);
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::IO.with_widths(Width::U8, Width::U8)
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: "gb.serial",
version: 1,
summary: "Game Boy serial link ($FF01-$FF02), with nothing on the other end",
properties: &[PropertySpec {
name: "link",
kind: ValueKind::Str,
required: false,
summary: "what is on the other end of the cable; only `none` exists",
}],
construct: |props| Ok(Box::new(GbSerial::from_props(props)?) as Box<dyn Device>),
};
pub fn register(reg: &mut crate::core::Registry) -> Result<()> {
reg.add(&CLASS)
}
impl Device for GbSerial {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn region(&self, name: &str) -> Option<RegionRef> {
(name.is_empty() || name == REGISTER_REGION).then(|| Arc::clone(&self.regs_region))
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
if port != IRQ_PIN {
return Err(Error::Config {
at: String::from(port),
message: alloc::format!("the serial link drives only `{IRQ_PIN}`"),
});
}
self.attach_irq(source);
Ok(())
}
fn reset(&self, kind: ResetKind) {
let now = self.shared.tick.load(Ordering::Relaxed);
let mut regs = self.shared.regs.lock();
*regs = Regs::default();
self.shared.publish(®s, now);
drop(regs);
if kind == ResetKind::Cold {
self.clear_transcript();
}
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let regs = *self.shared.regs.lock();
w.write_u8(regs.data)?;
w.write_u8(regs.control)?;
w.write_u64(regs.remaining)?;
w.write_u64(self.shared.tick.load(Ordering::Relaxed))?;
w.write_bytes(&self.shared.transcript.lock())?;
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let regs = Regs {
data: r.read_u8()?,
control: r.read_u8()?,
remaining: r.read_u64()?,
};
let tick = r.read_u64()?;
let transcript = r.read_bytes()?;
*self.shared.transcript.lock() = transcript.to_vec();
let mut slot = self.shared.regs.lock();
*slot = regs;
self.shared.publish(®s, tick);
Ok(())
}
fn is_lazy(&self) -> bool {
true
}
fn current_tick(&self) -> u64 {
self.shared.tick.load(Ordering::Relaxed)
}
fn advance_to(&self, tick: u64) {
GbSerial::advance_to(self, tick);
}
fn next_event_tick(&self) -> Option<u64> {
let event = self.shared.next_event.load(Ordering::Relaxed);
(event != u64::MAX).then_some(event)
}
fn attach_lazy(&self, handle: LazyHandle) {
GbSerial::attach_lazy(self, handle);
}
}
impl crate::machine::Instance for GbSerial {}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS.name, |props| {
Ok(Arc::new(GbSerial::from_props(props)?))
})
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
ClassSchema::new(CLASS.name)
.prop(PropSchema::new("link", ValueKind::Str).values(&["none"]))
.port(IRQ_PIN, PortDir::Out)
.region(REGISTER_REGION)
}