devices6502 0.1.0

Helper library for cpu6502 implementing memory devices
Documentation
//! Hot-swappable device socket.
//!
//! This module provides the [`Socket`] struct, which allows a device to be dynamically inserted or removed
//! at runtime. This is useful for emulating cartridge slots or other hot-swappable memory-mapped devices
//! in 6502-based systems.
//!
//! This module requires the `alloc` feature to be enabled.

use super::device::Device;
use alloc::boxed::Box;

/// A socket that can hold a device and allows hot-swapping at runtime.
///
/// The `Socket<ADDR_BITS>` struct wraps an optional device, allowing the device to be inserted
/// or removed dynamically. All device operations are forwarded to the contained device if present,
/// or will panic or do nothing otherwise, depending on the method.
///
/// # Type Parameters
/// - `ADDR_BITS`: The number of address bits for the device to be connected.
#[derive(Default)]
pub struct Socket<const ADDR_BITS: u8> {
    connected_device: Option<Box<dyn Device>>,
}

impl<const ADDR_BITS: u8> Device for Socket<ADDR_BITS> {
    /// Creating a socket with data is not supported and will panic.
    fn with_data(_: &[u8]) -> Self {
        panic!("attempting to init an empty socket with data")
    }

    /// Initializes the contained device with the provided data, if present.
    ///
    /// # Panics
    /// Panics if no device is connected.
    fn init_data(&mut self, data: &[u8]) {
        self.connected_device
            .as_mut()
            .expect("calling init device with an empty socket")
            .init_data(data);
    }

    /// Copies the current contents of the contained device into the provided buffer, if present.
    ///
    /// # Panics
    /// Panics if no device is connected.
    fn cache_current_read_data(&self, destination: &mut [u8]) {
        self.connected_device
            .as_ref()
            .expect("calling cache_current_read_data with an empty socket")
            .cache_current_read_data(destination);
    }

    /// Reads a byte from the contained device.
    ///
    /// # Panics
    /// Panics if no device is connected.
    fn read(&self, addr: u16) -> u8 {
        self.connected_device
            .as_ref()
            .expect("calling read with an empty socket")
            .read(addr)
    }

    /// Writes a byte to the contained device, if present.
    fn write(&mut self, data: u8, addr: u16) {
        if let Some(connected_device) = self.connected_device.as_mut() {
            connected_device.write(data, addr);
        }
    }

    /// Returns the address space size for the socket.
    fn addr_space_size() -> u32 {
        2u32.pow(ADDR_BITS as u32)
    }

    /// Returns the number of address bits for the socket.
    fn addr_bits_count() -> u8 {
        ADDR_BITS
    }

    /// Returns the address space size for the socket (dynamic).
    fn addr_space_size_dyn(&self) -> u32 {
        2u32.pow(ADDR_BITS as u32)
    }

    /// Returns the number of address bits for the socket (dynamic).
    fn addr_bits_count_dyn(&self) -> u8 {
        ADDR_BITS
    }

    /// Returns `true` if a device is currently inserted in the socket.
    fn is_connected(&self) -> bool {
        self.connected_device.is_some()
    }
}

impl<const ADDR_BITS: u8> Socket<ADDR_BITS> {
    /// Creates a new, empty socket with no device inserted.
    pub fn new() -> Self {
        Self::default()
    }

    /// Checks if the given device is compatible with this socket.
    ///
    /// # Arguments
    /// * `device` - The device to check.
    ///
    /// # Returns
    /// `true` if the device's address bits match the socket's address bits.
    pub fn is_device_compatible(&self, device: &dyn Device) -> bool {
        device.addr_bits_count_dyn() == ADDR_BITS
    }

    /// Disconnects and returns the currently connected device, if any.
    ///
    /// # Returns
    /// The removed device, or `None` if the socket was empty.
    pub fn disconnect_device(&mut self) -> Option<Box<dyn Device>> {
        self.connected_device.take()
    }

    /// Connects a device to the socket, replacing any existing device.
    ///
    /// # Arguments
    /// * `device_to_connect` - The device to connect.
    ///
    /// # Panics
    /// Panics if the device is not compatible with the socket.
    pub fn connect_device(&mut self, device_to_connect: Box<dyn Device>) {
        let _ = self.disconnect_device();
        assert!(
            self.is_device_compatible(device_to_connect.as_ref()),
            "Trying to connect a memory mapped device to an incompatible socket"
        );
        self.connected_device = Some(device_to_connect);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::size_const::*;
    use crate::Ram;

    #[test]
    #[should_panic(expected = "Trying to connect a memory mapped device to an incompatible socket")]
    fn connectdevicetoincompatiblesocket_panics() {
        let ram = Box::new(Ram::<{ SIZE_64K }>::new());
        let mut socket = Socket::<4>::new();
        socket.connect_device(ram);
    }

    #[test]
    fn connectdevicetocompatiblesocket_doesntpanic() {
        let ram = Box::new(Ram::<16>::new());
        let mut socket = Socket::<4>::new();
        socket.connect_device(ram);

        let ram = Box::new(Ram::<{ SIZE_64K }>::new());
        let mut socket = Socket::<16>::new();
        socket.connect_device(ram);
    }

    #[test]
    fn connectram_fillwithdata_datacorrect() {
        let ram = Box::new(Ram::<{ SIZE_64K }>::new());
        let mut socket = Socket::<16>::new();
        socket.connect_device(ram);

        for i in 0..u16::MAX {
            socket.write(i as u8, i);
        }

        for i in 0..u16::MAX {
            assert_eq!(socket.read(i), i as u8, "\nerroneous data at {}", i);
        }
    }
}