cge_nes 0.1.0

Cycle-accurate NES (Nintendo Entertainment System) emulator library: CPU, PPU, cartridge, input, and iNES ROM loading.
Documentation
//! Direct Memory Access (DMA) functionality for the NES.
//!
//! The DMA controller allows for efficient bulk transfer of data between memory and
//! various components of the system, particularly the PPU's Object Attribute Memory (OAM)
//! and the APU's audio channels.

/// Represents the type of DMA transfer being performed.
#[derive(Copy, Clone, PartialEq, Eq, Default)]
pub enum DmaType {
    /// DMA transfer to PPU's OAM memory
    #[default]
    Ppu,
    /// DMA transfer to APU's audio channels
    Apu,
}

/// Represents the current state of a DMA transfer.
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub enum DmaState {
    /// No DMA transfer is in progress
    #[default]
    Disabled,
    /// DMA transfer initiated but waiting for CPU to complete current instruction
    WaitingForCpuHalt,
    /// DMA transfer is actively copying data
    Transferring,
}

/// Controls DMA transfers between memory and various components.
///
/// The DMA controller manages 256-byte block transfers from a page in memory
/// to either the PPU's OAM or the APU. During transfer, the CPU is suspended
/// to avoid memory conflicts.
#[derive(Default)]
pub struct Dma {
    state: DmaState,
    last_addr: u16,
    addr: u16,
    data: Option<u8>,
}

impl DmaType {
    /// Toggles between PPU and APU DMA types.
    pub fn toggle(&mut self) {
        if *self == DmaType::Ppu {
            *self = DmaType::Apu;
        } else {
            *self = DmaType::Ppu;
        }
    }
}

impl Dma {
    /// Resets the DMA controller to its default state.
    pub fn reset(&mut self) {
        *self = Default::default();
    }

    /// Initiates a new DMA transfer from the specified page address.
    ///
    /// The transfer will copy 256 bytes starting from addr_page << 8.
    pub fn initiate_dma(&mut self, addr_page: u8) {
        self.addr = (addr_page as u16) << 8;
        self.last_addr = self.addr + u8::MAX as u16;
        self.state = DmaState::WaitingForCpuHalt;
    }

    /// Returns the current memory address being accessed by the DMA.
    pub fn addr(&self) -> u16 {
        self.addr
    }

    /// Returns the current state of the DMA transfer.
    pub fn state(&self) -> DmaState {
        self.state
    }

    /// Begins the actual data transfer after CPU has been halted.
    pub fn start_transfer(&mut self) {
        assert_eq!(self.state, DmaState::WaitingForCpuHalt);
        self.state = DmaState::Transferring;
    }

    /// Reads a byte from memory during DMA transfer.
    ///
    /// This method is called during the read phase of the DMA transfer to store
    /// the data that will be written in the next cycle.
    ///
    /// # Arguments
    ///
    /// * `data` - The byte read from memory at the current DMA source address
    ///
    /// # Effects
    ///
    /// * Stores the provided data for the next write cycle
    /// * Increments the source address counter
    pub fn data_pull(&mut self, data: u8) {
        self.data = Some(data);
        self.addr = self.addr.wrapping_add(1);
    }

    /// Writes the previously pulled byte to the destination.
    ///
    /// This method is called during the write phase of the DMA transfer to retrieve
    /// the data that was stored in the previous read cycle.
    ///
    /// # Returns
    ///
    /// * `Some(u8)` - The byte to be written to the destination
    /// * `None` - If there is no data to write or the transfer is complete
    ///
    /// # Effects
    ///
    /// * Clears the stored data after returning it
    /// * Resets the DMA controller if this was the last byte of the transfer
    pub fn data_put(&mut self) -> Option<u8> {
        let data_to_write = self.data.take();

        if self.addr > self.last_addr {
            self.reset();
        }

        data_to_write
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn dma_transfer() {
        let mut dma = Dma::default();
        assert_eq!(dma.state, DmaState::Disabled);

        let addr_page = 0x1A;
        dma.initiate_dma(addr_page);
        assert_eq!(dma.state, DmaState::WaitingForCpuHalt);

        dma.start_transfer();

        for i in 0..=u8::MAX {
            assert_eq!(dma.state, DmaState::Transferring);

            let expected_addr = 0x1A00 + (i as u16);
            assert_eq!(dma.addr(), expected_addr);

            let expected_data = u8::MAX - i;
            dma.data_pull(expected_data);
            let data_to_put = dma.data_put();
            assert_eq!(data_to_put, Some(expected_data));
        }

        assert_eq!(dma.state, DmaState::Disabled);
    }
}