rigless 0.3.0

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The direct memory access controller of RM0090 rev 22 chapter 10, written against the `dma2`
//! layout, by the model RM0433 rev 9 gives the STM32H743 too, in [`crate::soc::st::dma`].
//!
//! One layout serves both controllers. What differs between them is the part's, not the layout's:
//! which peripheral request reaches which stream on which channel, tables 43 and 44, and whether
//! the peripheral port reaches the bus matrix at all, which the note under figure 34 gives DMA2
//! alone.
//!
//! A stream is a byte pipe with a FIFO in the middle. It loads from the source port and stores to
//! the destination port, one access at a time, as one AHB master is. Everything a transfer does
//! happens at the instant of the access or the request that caused it, because a settle is the part
//! reaching its next steady state rather than time passing.
//!
//! **The counter follows the peripheral port.** Section 10.3.10 gives it the peripheral bus's
//! width. Section 10.3.13 ends a transfer when it "has reached zero in the memory-to-peripheral
//! mode", where the peripheral is the destination, and section 10.3.15's `0xFFFF - DMA_SxNDTR`
//! counts what the peripheral moved. The manual doesn't agree with itself about the instant.
//! Section 10.3.2 makes the decrement the third of three operations, after the store, which for a
//! stream whose two ports are decoupled by the FIFO is a different moment from the peripheral's own
//! access. And section 10.3.12 has the counter after a flush "indicate how many data items are
//! currently available in the destination memory", which only holds if it hadn't already come down
//! for what the FIFO was holding. Both of those read the counter against the memory port. This
//! model takes the peripheral port, which is the reading the other three sections carry, and makes
//! the flush the exception by freezing the counter there.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | Channel selection | A request reaches the stream whose `CHSEL` names the channel it arrives on, and the part says which requests reach which channels | 10.3.3, tables 43 and 44 | `a_request_reaches_only_the_stream_whose_channel_selects_it` |
//! | Serving a peripheral | A stream takes a word off an interface, or gives one to it, while the request stands, and the access it makes is what clears the flag behind the request | 10.3.2, 28.3.9 | `a_serial_interface_drives_a_stream_each_way_and_the_peers_answers_come_back` |
//! | Either port on a peripheral | Both ports reach the bus matrix, so a stream moves words between two peripherals with no memory in between | 10.3.1, 10.3.6 | `a_stream_carries_words_from_one_peripheral_to_another` |
//! | `DIR` | Peripheral-to-memory, memory-to-peripheral, and memory-to-memory, with the source and destination of table 45 | 10.3.6 | `each_direction_moves_its_block_between_the_right_two_ports` |
//! | Memory to memory | Runs as soon as the stream is enabled, with no request, and only on a controller whose peripheral port reaches the bus matrix | 10.3.6, figure 34 note | `memory_to_memory_copies_a_block_with_no_request`, `the_first_controller_cannot_reach_memory_from_its_peripheral_port` |
//! | Direct mode | There's no FIFO to fill: a memory-to-peripheral stream preloads one item and a peripheral-to-memory one drains each straight through, `MSIZE` follows `PSIZE`, and the burst fields are forced to single | 10.3.12, 10.5.5 | `enabling_a_stream_applies_every_rule_the_configuration_forces`, `the_counter_counts_the_peripheral_ports_items_and_not_the_memory_ports` |
//! | FIFO mode | Bytes gather to the threshold before the destination is written, and then the FIFO's contents drain, the tail of a transfer drains whatever is left, and a memory-to-peripheral stream reloads the FIFO whole once its level falls to the threshold | 10.3.6, 10.3.12 | `the_fifo_holds_bytes_until_the_threshold_and_then_drains`, `a_half_threshold_drains_eight_bytes_at_a_time`, `a_memory_to_peripheral_stream_reloads_the_fifo_at_its_threshold` |
//! | Packing | Source and destination widths differ and the bytes repack little-endian, table 47 | 10.3.10 | `bytes_repack_between_two_widths_in_the_order_the_table_gives` |
//! | `FS` | The FIFO level as the encodings of section 10.5.10 give it | 10.5.10 | `the_fifo_holds_bytes_until_the_threshold_and_then_drains` |
//! | `PINC`, `MINC`, `PINCOS` | Each port's pointer steps by its own width, or by four where `PINCOS` fixes it, or stands still | 10.3.7, 10.5.5 | `a_fixed_pointer_writes_one_address_and_an_incrementing_one_walks`, `the_peripheral_step_is_four_where_pincos_survives_the_enable` |
//! | `NDTR` | Counts peripheral-port items, decrements as they move, is read-only while the stream is enabled, holds what's left after a suspension, is reloaded with what was last programmed at every enable, and serves nothing at zero | 10.3.2, 10.3.14, 10.5.6 | `the_counter_follows_the_peripheral_port_and_survives_a_suspension`, `the_counter_counts_the_peripheral_ports_items_and_not_the_memory_ports`, `a_stream_enabled_again_runs_the_block_it_was_programmed_with`, `a_stream_enabled_with_no_count_serves_nothing_and_stays_enabled` |
//! | Circular | The counter and the pointers start again and the requests go on being served | 10.3.8 | `circular_mode_starts_the_block_again` |
//! | Double buffer | `CT` names the pointer in use and swaps at each end of transaction. The current one is write-protected, and writing it is a transfer error | 10.3.9 | `double_buffer_swaps_its_target_at_each_end`, `writing_the_target_in_use_is_a_transfer_error` |
//! | `HTIF`, `TCIF` | Half and whole of the block as the destination port takes it | 10.3.17 | `the_half_and_complete_flags_stand_at_the_halves_of_the_block` |
//! | `TEIF`, `FEIF` | An access the bus refuses, a write to the target in use, and a FIFO threshold the memory burst doesn't divide, each of which clears `EN` and ends the block where it stands | 10.3.18, table 49 | `a_stream_pointed_at_an_instance_the_part_refuses_ends_in_a_transfer_error`, `a_threshold_the_burst_does_not_divide_is_a_fifo_error`, `a_stream_stopped_while_an_access_was_in_flight_does_not_take_the_answer` |
//! | `LIFCR`, `HIFCR` | A written one clears its flag in `LISR` or `HISR` | 10.5.3, 10.5.4 | `a_written_one_clears_its_own_flag_and_no_other` |
//! | Interrupts | Each stream raises its own line while an enabled flag stands | 10.4, table 51 | `each_enable_raises_the_streams_own_line` |
//! | Suspension | Clearing `EN` ends the transfer, sets `TCIF`, and drains what the FIFO still holds toward memory, a last partial item at the memory width | 10.3.12, 10.3.14 | `clearing_the_enable_flushes_what_the_fifo_holds`, `a_flush_writes_a_partial_last_item_at_the_memory_width` |
//! | `PFCTRL`, the peripheral as flow controller | The counter is forced to `0xFFFF` at the enable. The stream ends, `TCIF` set and `EN` cleared, when the peripheral signals its last data and what the FIFO holds has reached memory, and `0xFFFF - NDTR` is what moved. Section 10.3.15 names one peripheral that can signal it, the card interface, and the part says which of its lines is the signal | 10.3.13, 10.3.15, 10.5.5 | `a_stream_the_peripheral_controls_ends_when_it_signals_the_last_data`, `a_write_the_peripheral_controls_ends_the_same_way`, `a_stream_the_dma_controls_outlives_the_card_interfaces_last_data` |
//! | Register protection | The configuration, counter, and pointer registers take no write while `EN` stands | 10.5.5, 10.5.6 | `the_configuration_registers_take_no_write_while_the_stream_runs` |
//! | Enable procedure | `DBM` forces `CIRC`, memory-to-memory forces `PFCTRL` clear and the FIFO on, and the counter and pointers are taken at the enable | 10.3.17, 10.5.5, 10.5.10 | `enabling_a_stream_applies_every_rule_the_configuration_forces` |
//! | The reset bit | A write that leaves the controller's bit of `RCC_AHB1RSTR` set puts it back as it came out of reset: its registers, every stream idle with an empty FIFO, and its lines lowered. The requests the peripherals hold stand, since the bit doesn't reach them | 6.3.5 | `a_reset_bit_puts_its_controller_back_and_keeps_the_requests_standing` |
//! | ES0206 rev 24 | No item of Table 3 names the DMA controller. 2.6.1 is the DAC's own request, and the part has no DAC model | not applicable | |
//!
//! **What the model leaves out, and why.**
//!
//! | Behavior | Source | Reason |
//! |---|---|---|
//! | The arbiter, and `PL` as anything but a stored field | 10.3.4 | Priority decides which of two streams reaches the bus matrix first in a cycle neither of them has. Every stream here is served in stream order, which is the arbiter's own tie-break, and nothing a firmware can read distinguishes the two |
//! | `DMEIF`, the direct mode error | 10.3.18 | It's set "because the memory bus was not granted" while a second request arrives. The memory bus here is always granted within the access that asked for it, so the condition can't arise. The bit and its enable are stored and never set |
//! | `FEIF` from an underrun or an overrun | 10.3.18 | The same: both are the FIFO caught out by a bus that didn't grant in time. The threshold error of table 49, which is a configuration and not a race, is modeled |
//! | A burst as an indivisible run of AHB beats, and `PBURST` as a count of them per request | 10.3.11 | Each beat is its own access, so nothing observes the lock. And a request here is the level section 10.3.2 gives it, so what decides how many beats one request is worth is the peripheral's own flag rather than a count. `MBURST` is read, because the threshold rule of table 49 is a configuration error and not a race |
//! | Clock gating, and being held at reset while the bit of `RCC_AHB1RSTR` stands | 6.3.5, 6.3.10 | The part gates the instance through the bus, which is `memory_map.rs`, and resets it at each write that leaves the bit set. Held in reset it would answer its reset values and take no write, but ChibiOS sets and clears the bit in two writes, so nothing reaches it while the bit stands |
//!
//! **Where this model departs from the vendor's file.** Section 10.5.3 types every bit of `LIFCR`
//! and `HIFCR` write-only and ST's file types both registers read-write, so a written one stays in
//! the register where the part would drop it. The model clears them itself. ST also gives `SxFCR`
//! no write rule for `FS`, which section 10.5.10 types read-only, and the register file enforces
//! that from the table, so the model writes the level through [`RegisterFile::set`](crate::device::RegisterFile::set).

use crate::soc::st::dma::{Reach, Requests};
use crate::soc::stm32f427::generated::dma2::{Register, s0cr};

pub use crate::soc::st::dma::{CHANNELS, STREAMS, Wire};

/// One direct memory access controller of an STM32F42xxx, RM0090 rev 22 chapter 10.
pub type Dma = crate::soc::st::dma::Dma<Register>;

impl Dma {
    /// A controller whose streams carry `wiring`, the part's reading of tables 43 and 44, and whose
    /// streams under `PFCTRL` end on `last_data`, the lines section 10.3.15 has a peripheral signal
    /// on.
    ///
    /// `reaches_memory_from_the_peripheral_port` is the note under figure 34. One of the two
    /// controllers has its peripheral port on the bus matrix, so it can move memory to memory, and
    /// the other can't. A controller that can't takes a transfer error rather than moving a byte.
    #[must_use]
    pub fn new(
        wiring: &'static [Wire],
        last_data: &'static [Wire],
        reaches_memory_from_the_peripheral_port: bool,
    ) -> Self {
        Self::wired(
            Requests::Channels {
                wiring,
                last_data,
                select: s0cr::CHSEL,
            },
            Reach {
                memory_to_memory: reaches_memory_from_the_peripheral_port,
                unreachable: &[],
            },
        )
    }
}