c64 0.1.0-alpha.1

Driver for the Commodore 64 platform
Documentation
//! The On-Chip I/O.

use core::panic;

use bitflags::bitflags;
use ufmt::{uDebug, uwrite, uwriteln};
use volatile_register::RW;

#[repr(C, packed)]
pub struct RegisterBlock {
    data_direction: RW<u8>,
    port: RW<u8>,
}

impl RegisterBlock {
    /// Fetches the current value of the processor port `$01`, which includes the memory
    /// configuration.
    pub fn port(&self) -> Port {
        Port::from_bits(self.port.read()).expect("No unknown bits")
    }

    /// Alters the memory configuration.
    ///
    /// For each parameter, `None` means "preserve the current configuration".
    ///
    /// # Panics
    ///
    /// Panics on an inconsistent parameter setting (e.g. `kernal = Some(false)`,
    /// `basic = Some(true)`).
    pub unsafe fn configure_memory(
        &self,
        kernal: Option<bool>,
        basic: Option<bool>,
        io_config: Option<IoConfig>,
    ) {
        unsafe {
            self.port.modify(|p| {
                let cur_hiram = (p & Port::HIRAM.bits()) != 0;
                let cur_loram = (p & Port::LORAM.bits()) != 0;
                let cur_charen = (p & Port::CHAREN.bits()) != 0;

                // Valid settings are defined by this constraint table:
                //
                //       Bit+-------------+-----------+------------+
                //       210| $8000-$BFFF |$D000-$DFFF|$E000-$FFFF |
                //  +---+---+-------------+-----------+------------+
                //  | 7 |111| Cart.+Basic |    I/O    | Kernal ROM |
                //  +---+---+-------------+-----------+------------+
                //  | 6 |110|     RAM     |    I/O    | Kernal ROM |
                //  +---+---+-------------+-----------+------------+
                //  | 5 |101|     RAM     |    I/O    |    RAM     |
                //  +---+---+-------------+-----------+------------+
                //  | 4 |100|     RAM     |    RAM    |    RAM     |
                //  +---+---+-------------+-----------+------------+
                //  | 3 |011| Cart.+Basic | Char. ROM | Kernal ROM |
                //  +---+---+-------------+-----------+------------+
                //  | 2 |010|     RAM     | Char. ROM | Kernal ROM |
                //  +---+---+-------------+-----------+------------+
                //  | 1 |001|     RAM     | Char. ROM |    RAM     |
                //  +---+---+-------------+-----------+------------+
                //  | 0 |000|     RAM     |    RAM    |    RAM     |
                //  +---+---+-------------+-----------+------------+
                //       |||
                // /CharEn|/LoRam
                //        |
                //      /HiRam

                // First, evaluate the desired configuration by replacing `None`s with the
                // respective current configurations.
                let kernal = kernal.unwrap_or(cur_hiram);
                let basic = basic.unwrap_or(cur_loram && cur_hiram);
                let io_config =
                    io_config.unwrap_or_else(|| match (cur_loram || cur_hiram, cur_charen) {
                        (false, _) => IoConfig::Ram,
                        (true, false) => IoConfig::CharacterRom,
                        (true, true) => IoConfig::Io,
                    });

                // Now determine the correct bit settings from the constraint table. If
                // the desired configuration doesn't exist in the table, we panic.
                let new_bits: u8 = match (basic, io_config, kernal) {
                    (true, IoConfig::Io, true) => 0b111,
                    (false, IoConfig::Io, true) => 0b110,
                    (false, IoConfig::Io, false) => 0b101,
                    (true, IoConfig::CharacterRom, true) => 0b011,
                    (false, IoConfig::CharacterRom, true) => 0b010,
                    (false, IoConfig::CharacterRom, false) => 0b001,
                    // This configuration appears twice in the table; normalize for
                    // reliability.
                    (false, IoConfig::Ram, false) => 0b000,
                    _ => panic!(),
                };

                // Clear the current bits and then set the new bits.
                (p & !(Port::HIRAM | Port::LORAM | Port::CHAREN).bits()) | new_bits
            });
        }
    }
}

bitflags! {
    /// The processor port `$01`, which includes the [memory configuration].
    ///
    /// [memory configuration]: http://unusedino.de/ec64/technical/aay/c64/memcfg.htm
    pub struct Port: u8 {
        /// Selects ROM or RAM at `$A000`.
        ///
        /// - `0` = RAM
        /// - `1` = BASIC ROM
        const LORAM = 0b00000001;

        /// Selects ROM or RAM at `$E000`.
        ///
        /// - `0` = RAM
        /// - `1` = Kernal ROM
        const HIRAM = 0b00000010;

        /// Selects character ROM or I/O devices.
        ///
        /// - `0` = Character ROM
        /// - `1` = I/O
        const CHAREN  = 0b00000100;

        /// Cassette Data Output line.
        ///
        /// This line is connected to the Cassette Data Write line on the cassette port,
        /// and is used to send the data which is written to tape.
        const CASSETTE_DATA_OUTPUT  = 0b00001000;

        /// Cassette Switch Sense.
        const CASSETTE_SWITCH_SENSE = 0b00010000;

        /// Cassette Motor Switch Control.
        const CASSETTE_MOTOR_SWITCH_CONTROL = 0b00100000;

        /// Not connected, no function presently defined.
        const RESERVED_6 = 0b01000000;
        const RESERVED_7 = 0b10000000;
    }
}

impl uDebug for Port {
    fn fmt<W>(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
    where
        W: ufmt::uWrite + ?Sized,
    {
        uwriteln!(f, "On-Chip I/O Port:")?;
        uwriteln!(f, "   loram: {}", self.contains(Self::LORAM))?;
        uwriteln!(f, "   hiram: {}", self.contains(Self::HIRAM))?;
        uwrite!(f, "  charen: {}", self.contains(Self::CHAREN))
    }
}

impl Port {
    /// Returns whether the Kernal ROM is selected instead of RAM.
    ///
    /// Because the BASIC interpreter uses the Kernal, it is also switched out and
    /// replaced by RAM whenever the Kernal ROM is switched out.
    ///
    /// - When this is `false`, addresses `$A000-$BFFF` and `$E000-$FFFF` are RAM.
    ///   - Note that the RAM in these address rangees is still writable; it just cannot
    ///     be read.
    /// - When this is `true`, addresses `$E000-$FFFF` are the Kernal ROM.
    pub fn kernal(&self) -> bool {
        self.contains(Self::HIRAM)
    }

    /// Returns whether the BASIC ROM is selected instead of RAM.
    ///
    /// - When this is `false`, addresses `$A000-$BFFF` are RAM.
    /// - When this is `true`, addresses `$A000-$BFFF` are the BASIC ROM.
    ///   - Note that the RAM in this address range is still writable; it just cannot be
    ///     read.
    pub fn basic(&self) -> bool {
        self.contains(Self::LORAM | Self::HIRAM)
    }

    /// Returns whether I/O devices or the character ROM are available.
    ///
    /// This specifies the configuration of addresses `$D000-$DFFF`.
    pub fn io_config(&self) -> IoConfig {
        if self.intersects(Self::LORAM | Self::HIRAM) {
            if self.contains(Self::CHAREN) {
                IoConfig::Io
            } else {
                IoConfig::CharacterRom
            }
        } else {
            IoConfig::Ram
        }
    }
}

/// Whether I/O devices or the character ROM are available.
pub enum IoConfig {
    /// Addresses `$D000-$DFFF` are RAM.
    Ram,
    /// Addresses `$D000-$DFFF` are the Character ROM.
    CharacterRom,
    /// Addresses `$D000-$DFFF` are I/O devices.
    Io,
}