// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The basic direct memory access controller of RM0433 rev 9 chapter 16, in the D3 domain, written
//! against the `bdma` layout.
//!
//! Eight channels share one AHB master. A channel moves one data item at a time: it reads the item from
//! the source, writes it to the destination, and counts it, section 16.4.3. It has no FIFO, no burst and
//! no packing, so a source and a destination of different widths convert the item rather than repack
//! it, Table 120. Each channel listens to the channel of the same number on `DMAMUX2`, section 17.3.3.
//! Every citation is RM0433 rev 9 unless it names another document.
//!
//! **What reaches what.** Section 2.1.6 limits the controller to the D3 domain: "It can access the
//! internal SRAM4, backup RAM, and AHB4 and APB4 peripherals", and Table 3 gives it no path to anything
//! else. The model ends a channel in `TEIF` on an access outside those four spans and doesn't make it,
//! since the manual's one word for a refused access is the bus error of section 16.4.7, which it gives
//! "reading from or writing to a reserved address space".
//!
//! **What the controller tells the multiplexer.** Section 16.4.3 has it acknowledge a request as soon as
//! it grants it. The multiplexer counts served requests, so the channel drives its request output `n`
//! high and low again at the instant it makes the access to the peripheral. Output `n` is the
//! acknowledge of channel `n`.
//!
//! **What the model handles and what falls to the register file.** Every register of the chapter has an
//! arm in [`Device::write`] and the model overrides no read. `BDMA_ISR` is read-only and the file drops a
//! write to it. `BDMA_IFCR` takes its write in the model, which applies it to the status register and
//! clears itself. The model owns the flags, `GIFx` among them, the counter, the enable, and the
//! protection of the registers section 16.6.3 makes read-only while a channel is enabled.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | Layout | `ISR` and `IFCR`, then five registers to a channel: `CCRx`, `CNDTRx`, `CPARx`, `CM0ARx` and `CM1ARx` | 16.6.1 to 16.6.8 | `the_channels_registers_run_in_blocks_of_five` |
//! | Flags | Four to a channel, `GIFx`, `TCIFx`, `HTIFx` and `TEIFx` in bits `4x` to `4x + 3` of `ISR` and the clear bits at the same places of `IFCR` | 16.6.1, 16.6.2 | `every_channels_flags_sit_at_four_bits_a_channel` |
//! | Request mapping | A channel hears `DMAMUX2`'s output of its own number and no other instance's | 16.3.2, 17.3.3 | `a_channel_hears_its_own_multiplexer_channel_and_nothing_else` |
//! | A request paces a transfer | With no `MEM2MEM`, one request is one data item: the channel reads the source, writes the destination and counts, and the next waits for the next request. The channel acknowledges the request at the access to the peripheral | 16.4.3 | `each_request_moves_one_item_and_is_acknowledged` |
//! | `MEM2MEM` | Runs as soon as the channel is enabled, with no request, until the counter is spent | 16.4.5 | `memory_to_memory_copies_a_block_with_no_request` |
//! | `DIR` | 0 reads the peripheral address and writes the memory address, 1 the other way round, and the same in memory-to-memory and peripheral-to-peripheral transfers | 16.4.5, 16.6.3 | `dir_picks_the_source_and_the_destination` |
//! | Widths | A narrow source into a wide destination is zero-extended and a wide source into a narrow destination keeps its low bytes | 16.4.6, Table 120 | `a_narrow_item_is_zero_extended_and_a_wide_one_truncated` |
//! | `PINC`, `MINC` | Each pointer steps by its own width after an item, or stands still | 16.4.5 | `pointers_step_by_their_own_widths_or_stand_still` |
//! | Alignment | A pointer with a half-word or word size ignores the low bit or two bits of its register, and the register keeps what was written | 16.6.5 to 16.6.7 | `a_pointer_is_aligned_to_its_data_size` |
//! | `CNDTR` | Decrements after each item, stays at zero at the end of a block that isn't circular, and serves nothing at zero, enabled or not. It is read-only while the channel is enabled | 16.4.5, 16.6.4 | `the_counter_counts_down_and_a_zero_counter_serves_nothing`, `the_registers_section_16_6_3_makes_read_only_are_while_a_channel_runs` |
//! | `EN` at the end of a block | A block that isn't circular leaves `EN` set, since the manual has hardware clear it for a transfer error alone | 16.6.3 | `the_counter_counts_down_and_a_zero_counter_serves_nothing` |
//! | `HTIF`, `TCIF`, `GIF` | Half and whole of the block. `GIF` stands while any of the three does | 16.4.5, 16.5, 16.6.1 | `the_half_and_complete_flags_stand_at_the_halves_and_gif_follows` |
//! | `IFCR` | `CGIFx` clears all four flags of the channel. An individual clear bit clears its flag and clears `GIFx` only if the other two are clear | 16.6.2 | `the_clear_bits_clear_their_flags_and_gif_only_when_nothing_else_stands`, `the_clear_register_reads_zero` |
//! | Circular | After the last item the counter and the pointers start again and requests go on being served | 16.4.5 | `circular_mode_reloads_the_counter_and_the_pointers` |
//! | Double buffer | `CT` toggles at the end of each block and picks the pointer of the next, and `CT` set picks `CM1AR` whether or not `DBM` is set | 16.4.5, 16.6.3 | `double_buffer_toggles_the_target_at_the_end_of_each_block`, `ct_picks_memory_one_whether_or_not_dbm_is_set` |
//! | Register protection | `CT`, `DBM`, `MEM2MEM`, `PL`, `MSIZE`, `PSIZE`, `MINC`, `PINC`, `DIR` and the counter take no write while `EN` stands. `CIRC`, the three enables and the three address registers do | 16.6.3 to 16.6.7 | `the_registers_section_16_6_3_makes_read_only_are_while_a_channel_runs` |
//! | A transfer error | An access the bus refuses, or one outside the controller's reach, sets `TEIFx` and `GIFx`, clears `EN` and ends the block. `EN` cannot be set again until `TEIFx` is cleared | 16.4.5, 16.4.7, 2.1.6, Table 3 | `a_transfer_into_memory_the_controller_cannot_reach_ends_in_a_transfer_error`, `a_transfer_from_memory_the_controller_cannot_reach_ends_in_a_transfer_error`, `a_refused_access_is_a_transfer_error`, `enable_cannot_be_set_again_until_the_error_is_cleared`, `every_d3_instance_lies_inside_a_reachable_span_and_no_other_does` |
//! | Disabling a channel | Clearing `EN` stops and aborts the channel. An access in flight when it clears is not taken | 16.4.5 | `disabling_a_channel_aborts_it_and_a_stale_answer_changes_nothing` |
//! | Interrupts | Each channel raises its own line while a flag stands with its enable set: channels 0 to 7 on lines 129 to 136 | 16.5, Table 121, Table 147 | `each_channel_raises_its_own_line_while_an_enabled_flag_stands` |
//! | Reset | A system reset leaves every channel idle | 16.6.3 | `a_reset_leaves_every_channel_idle` |
//! | Determinism and the snapshot | Two runs of one script are one run, and the state serializes mid-transfer | none | `two_runs_of_one_script_are_the_same_run`, `the_model_state_round_trips_through_a_snapshot` |
//! | ES0392 rev 15 2.21.1 | Applicable to revision V, marked `A`. An LPUART request served during a concurrent transfer "may not be served and the DMA stream may stay locked". The workaround is bit 20 of `DMA_SxCR`, which `BDMA_CCRx` doesn't have, and `LPUART1` reaches only the BDMA through `DMAMUX2`, so no workaround exists on this part. See the table below | ES0392 rev 15 | none |
//! | ES0392 rev 15 2.4.1 | Applicable to revision V, marked `A`. A transfer error and a write of `CGIFx` in the same clock cycle leave the channel enabled and `TEIFx` clear. See the table below | ES0392 rev 15 | none |
//!
//! **What the model leaves out, and why.**
//!
//! | Behavior | Source | Reason |
//! |---|---|---|
//! | The arbiter, and `PL` as anything but a stored field | 16.4.4 | Priority decides which of two channels the one master serves first, and every channel here is served in channel order, which is the arbiter's own tie-break. Nothing a firmware can read distinguishes the two |
//! | The alternation of a memory-to-memory channel with another requested channel | 16.4.4 | It needs the single master's turns, which a model with a request standing for each channel at its own instant doesn't have |
//! | ES0392 rev 15 2.4.1: the disable fails and `TEIFx` is not raised when the error and a `CGIFx` write share a clock cycle | ES0392 rev 15 | The condition is two events in one clock cycle, and the model has no cycle to put them in. The workaround, not clearing `GIFx` while the channel is active, is how the model's own flags behave |
//! | Suspending and resuming a channel | 16.4.5 | The manual says the hardware "does not guarantee" the remaining transfers, so there is no behavior to model. A channel disabled and enabled again takes the counter the first left |
//! | The counter after an aborted channel | 16.4.5 | "the `BDMA_CNDTRx` register content may not correctly reflect the remaining data transfers", which is no behavior either. The counter holds what the last completed item left |
//! | The data duplicated on the unused lanes of a narrow write | 16.4.6 | The bus carries the width of the access and the runtime puts the data in its lane, so a slave that ignores the width never sees the lanes |
//! | `DBM` without `CIRC`, and `MEM2MEM` with `CIRC` or `DBM` | 16.4.5, 16.6.3 | The manual says they "must not" be used and gives no behavior. The model toggles `CT` at the end of any block with `DBM` set and reloads only a circular one |
//! | A peripheral-to-peripheral channel paced by the destination | 16.4.5 | The channel is paced by the request its `DMAMUX2` channel selects whichever side that peripheral is, which is how the manual describes it |
//! | ES0392 rev 15 2.21.1: a channel locked by an LPUART request served during a concurrent transfer | ES0392 rev 15 | The errata gives no condition for a concurrent transfer and no instant, so the model would have to invent both, and the workaround it names does not exist on the BDMA |
//! | Clock gating and the reset registers | 8.7 | The part gates the instance through the bus, which is `memory_map.rs`. Being held at reset is a third behavior and isn't this |
//!
//! **Manual ambiguities, and what the model took.** Section 16.4.5 has the controller toggle between two
//! pointers "at the end of each BDMA transfer", where section 16.4.3 defines a "single BDMA transfer" as one
//! read and one write. Section 16.6.3 says `CT` is "toggled by hardware at the end of each channel
//! transfer" and the purpose of the mode is for software to work on one area while the controller fills
//! the other, so the model toggles at the end of each block. Section 16.6.3 lists `DBM` among the fields
//! read-only while `EN` is set, and the description of the bit says it "is not read-only when the channel
//! is enabled". The model takes the list and the sentence that it must not be written then. The same
//! section lists `CIRC` as writable while enabled and the three address registers likewise, and the model
//! lets a write land and takes effect at the next block, since the controller keeps its own current
//! pointers, section 16.4.5.
//!
//! **Where this model departs from the vendor's file.** Section 16.6.2 types every bit of `BDMA_IFCR`
//! write-only and ST's file types the register read-write, so a written one stays in the register where
//! the part would drop it. The model clears it itself.
//!
//! **Determinism.** The model reads no clock and draws nothing. `two_runs_of_one_script_are_the_same_run`
//! drives two controllers through one script and compares every register, the memory, the interrupts and
//! the whole state of each.
use crate::device::{Device, DeviceContext, Level, RegisterFile, RequestChange};
use crate::snapshot::store;
use crate::soc::stm32h743::generated::DMAMUX2;
use crate::soc::stm32h743::generated::bdma::{
Register, bdma_ccr0 as ccr, bdma_cndtr0 as cndtr, bdma_isr as isr,
};
use crate::soc::{FieldSpec, Layout, Width};
use super::dma::Span;
/// Channels, section 16.3.1.
pub(crate) const CHANNELS: usize = 8;
/// SRAM4, 64 Kbytes in the D3 domain, section 2.4.
pub(crate) const SRAM4: Span = Span {
base: 0x3800_0000,
bytes: 64 << 10,
};
/// The backup SRAM, 4 Kbytes in the D3 domain, section 2.4.
pub(crate) const BACKUP_SRAM: Span = Span {
base: 0x3880_0000,
bytes: 4 << 10,
};
/// The APB4 peripherals, Table 8: `0x5800_0000` to `0x5800_6BFF`.
pub(crate) const APB4: Span = Span {
base: 0x5800_0000,
bytes: 0x6C00,
};
/// The AHB4 peripherals, Table 8: `0x5802_0000` to `0x5802_73FF`.
pub(crate) const AHB4: Span = Span {
base: 0x5802_0000,
bytes: 0x7400,
};
/// All the controller reaches, section 2.1.6: "It can access the internal SRAM4, backup RAM, and AHB4
/// and APB4 peripherals".
pub(crate) const REACHABLE: [Span; 4] = [SRAM4, BACKUP_SRAM, APB4, AHB4];
const FIRST_CHANNEL_ROW: usize = 2;
const ROWS_PER_CHANNEL: usize = 5;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Row {
Control = 0,
Count = 1,
Peripheral = 2,
Memory0 = 3,
Memory1 = 4,
}
const ROWS: [Row; ROWS_PER_CHANNEL] = [
Row::Control,
Row::Count,
Row::Peripheral,
Row::Memory0,
Row::Memory1,
];
fn register(channel: usize, row: Row) -> Register {
let index = FIRST_CHANNEL_ROW + channel * ROWS_PER_CHANNEL + row as usize;
Register::from_index(index).expect("a controller carries eight channels of five registers")
}
const FLAGS: [[&FieldSpec; 4]; CHANNELS] = [
[isr::GIF0, isr::TCIF0, isr::HTIF0, isr::TEIF0],
[isr::GIF1, isr::TCIF1, isr::HTIF1, isr::TEIF1],
[isr::GIF2, isr::TCIF2, isr::HTIF2, isr::TEIF2],
[isr::GIF3, isr::TCIF3, isr::HTIF3, isr::TEIF3],
[isr::GIF4, isr::TCIF4, isr::HTIF4, isr::TEIF4],
[isr::GIF5, isr::TCIF5, isr::HTIF5, isr::TEIF5],
[isr::GIF6, isr::TCIF6, isr::HTIF6, isr::TEIF6],
[isr::GIF7, isr::TCIF7, isr::HTIF7, isr::TEIF7],
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Flag {
Global = 0,
Complete = 1,
Half = 2,
Error = 3,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
enum Side {
Source = 0,
Destination = 1,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Direction {
FromPeripheral,
FromMemory,
}
#[derive(Clone, Copy, Debug)]
struct Shape {
direction: Direction,
memory_to_memory: bool,
peripheral_width: Width,
memory_width: Width,
peripheral_step: Option<u64>,
memory_step: Option<u64>,
circular: bool,
double_buffer: bool,
}
impl Shape {
fn source_width(self) -> Width {
if self.direction == Direction::FromMemory {
self.memory_width
} else {
self.peripheral_width
}
}
fn destination_width(self) -> Width {
if self.direction == Direction::FromMemory {
self.peripheral_width
} else {
self.memory_width
}
}
fn peripheral_is(self, side: Side) -> bool {
(side == Side::Source) != (self.direction == Direction::FromMemory)
}
}
fn data_width(encoding: u64) -> Width {
match encoding {
0 => Width::W8,
1 => Width::W16,
_ => Width::W32,
}
}
fn aligned(address: u64, width: Width) -> u64 {
address & !u64::from(width.bytes() - 1)
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct Channel {
peripheral_at: u64,
memory_at: u64,
remaining: u64,
total: u64,
datum: u64,
outstanding: Option<Side>,
requested: bool,
halved: bool,
guarded: [u64; ROWS_PER_CHANNEL],
interrupt: Level,
}
impl Default for Channel {
fn default() -> Self {
Self {
peripheral_at: 0,
memory_at: 0,
remaining: 0,
total: 0,
datum: 0,
outstanding: None,
requested: false,
halved: false,
guarded: [0; ROWS_PER_CHANNEL],
interrupt: Level::Low,
}
}
}
/// The basic direct memory access controller of an STM32H743, RM0433 rev 9 chapter 16.
#[derive(Debug)]
pub struct Bdma {
channels: [Channel; CHANNELS],
}
store!(Bdma {
values: [channels],
parts: [],
built: [],
});
impl Default for Bdma {
fn default() -> Self {
Self::new()
}
}
impl Bdma {
/// A controller whose channel `n` hears `DMAMUX2`'s channel `n`, section 17.3.3.
///
/// The part names the multiplexer a reader of requests, and the controller as a reader too, so the
/// channel hears the level the multiplexer drives. It names the controller as a reader for the
/// multiplexer as well, which counts the acknowledges the channel drives on its own request outputs,
/// output `n` for channel `n`.
#[must_use]
pub fn new() -> Self {
Self {
channels: std::array::from_fn(|_| Channel::default()),
}
}
}
fn held(registers: &RegisterFile, channel: usize, row: Row) -> u64 {
registers.get(register(channel, row))
}
fn modify(registers: &mut RegisterFile, channel: usize, row: Row, change: impl FnOnce(u64) -> u64) {
let target = register(channel, row);
let value = registers.get(target);
registers.set(target, change(value));
}
fn set_flag(registers: &mut RegisterFile, channel: usize, flag: Flag, raised: bool) {
let flags = &FLAGS[channel];
let mut value = registers.get(Register::BdmaIsr);
value = flags[flag as usize].set(value, u64::from(raised));
let any = [Flag::Complete, Flag::Half, Flag::Error]
.into_iter()
.any(|each| flags[each as usize].is_set(value));
registers.set(
Register::BdmaIsr,
flags[Flag::Global as usize].set(value, u64::from(any)),
);
}
fn flag_stands(registers: &RegisterFile, channel: usize, flag: Flag) -> bool {
FLAGS[channel][flag as usize].is_set(registers.get(Register::BdmaIsr))
}
fn reaches(address: u64, width: Width) -> bool {
let bytes = u64::from(width.bytes());
REACHABLE.iter().any(|span| span.holds(address, bytes))
}
fn token(channel: usize, side: Side) -> u64 {
let channel = u64::try_from(channel).expect("eight channels");
channel * 2 + side as u64
}
fn from_token(token: u64) -> (usize, Side) {
let channel = usize::try_from(token / 2).expect("eight channels");
let side = if token.is_multiple_of(2) {
Side::Source
} else {
Side::Destination
};
(channel, side)
}
fn acknowledge(context: &mut DeviceContext<'_>, channel: usize) {
context.request(channel, Level::High);
context.request(channel, Level::Low);
}
impl Bdma {
fn shape(registers: &RegisterFile, channel: usize) -> Shape {
let control = held(registers, channel, Row::Control);
let peripheral_width = data_width(ccr::PSIZE.get(control));
let memory_width = data_width(ccr::MSIZE.get(control));
Shape {
direction: if ccr::DIR.is_set(control) {
Direction::FromMemory
} else {
Direction::FromPeripheral
},
memory_to_memory: ccr::MEM2MEM.is_set(control),
peripheral_width,
memory_width,
peripheral_step: ccr::PINC
.is_set(control)
.then(|| u64::from(peripheral_width.bytes())),
memory_step: ccr::MINC
.is_set(control)
.then(|| u64::from(memory_width.bytes())),
circular: ccr::CIRC.is_set(control),
double_buffer: ccr::DBM.is_set(control),
}
}
fn enabled(registers: &RegisterFile, channel: usize) -> bool {
ccr::EN.is_set(held(registers, channel, Row::Control))
}
fn take_pointers(&mut self, channel: usize, registers: &RegisterFile, shape: Shape) {
let control = held(registers, channel, Row::Control);
let memory = if ccr::CT.is_set(control) {
Row::Memory1
} else {
Row::Memory0
};
let state = &mut self.channels[channel];
state.peripheral_at = aligned(
held(registers, channel, Row::Peripheral),
shape.peripheral_width,
);
state.memory_at = aligned(held(registers, channel, memory), shape.memory_width);
}
fn start(
&mut self,
channel: usize,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
if flag_stands(registers, channel, Flag::Error) {
modify(registers, channel, Row::Control, |control| {
ccr::EN.set(control, 0)
});
return;
}
let shape = Self::shape(registers, channel);
let items = cndtr::NDT.get(held(registers, channel, Row::Count));
let state = &mut self.channels[channel];
state.total = items;
state.remaining = items;
state.halved = false;
state.outstanding = None;
self.take_pointers(channel, registers, shape);
self.pump(channel, registers, context);
}
fn fail(
&mut self,
channel: usize,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
set_flag(registers, channel, Flag::Error, true);
modify(registers, channel, Row::Control, |control| {
ccr::EN.set(control, 0)
});
self.channels[channel].outstanding = None;
self.refresh_interrupt(channel, registers, context);
}
fn pump(
&mut self,
channel: usize,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
let state = &self.channels[channel];
if state.outstanding.is_some() || state.remaining == 0 || !Self::enabled(registers, channel)
{
return;
}
let shape = Self::shape(registers, channel);
if !shape.memory_to_memory && !state.requested {
return;
}
let width = shape.source_width();
let address = if shape.peripheral_is(Side::Source) {
state.peripheral_at
} else {
state.memory_at
};
if !reaches(address, width) {
self.fail(channel, registers, context);
return;
}
if !shape.memory_to_memory && shape.peripheral_is(Side::Source) {
acknowledge(context, channel);
}
context.read_from(address, width, token(channel, Side::Source));
self.channels[channel].outstanding = Some(Side::Source);
}
fn answered(
&mut self,
token: u64,
value: u64,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
let (channel, side) = from_token(token);
// A channel that was stopped while its access was in flight isn't waiting for this one.
if self.channels[channel].outstanding != Some(side) {
return;
}
self.channels[channel].outstanding = None;
let shape = Self::shape(registers, channel);
match side {
Side::Source => self.read_done(channel, value, registers, context),
Side::Destination => self.item_done(channel, shape, registers, context),
}
}
fn read_done(
&mut self,
channel: usize,
value: u64,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
let shape = Self::shape(registers, channel);
let width = shape.destination_width();
let state = &mut self.channels[channel];
state.datum = value & width.mask();
let address = if shape.peripheral_is(Side::Destination) {
state.peripheral_at
} else {
state.memory_at
};
if !reaches(address, width) {
self.fail(channel, registers, context);
return;
}
if !shape.memory_to_memory && shape.peripheral_is(Side::Destination) {
acknowledge(context, channel);
}
let datum = self.channels[channel].datum;
context.write_to(address, width, datum, token(channel, Side::Destination));
self.channels[channel].outstanding = Some(Side::Destination);
}
fn item_done(
&mut self,
channel: usize,
shape: Shape,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
let state = &mut self.channels[channel];
if let Some(step) = shape.peripheral_step {
state.peripheral_at = state.peripheral_at.wrapping_add(step);
}
if let Some(step) = shape.memory_step {
state.memory_at = state.memory_at.wrapping_add(step);
}
state.remaining -= 1;
let (remaining, done, total) =
(state.remaining, state.total - state.remaining, state.total);
modify(registers, channel, Row::Count, |count| {
cndtr::NDT.set(count, remaining)
});
if !self.channels[channel].halved && done * 2 >= total {
self.channels[channel].halved = true;
set_flag(registers, channel, Flag::Half, true);
}
if remaining == 0 {
self.complete(channel, shape, registers);
}
self.refresh_interrupt(channel, registers, context);
self.pump(channel, registers, context);
}
fn complete(&mut self, channel: usize, shape: Shape, registers: &mut RegisterFile) {
set_flag(registers, channel, Flag::Complete, true);
if shape.double_buffer {
modify(registers, channel, Row::Control, |control| {
ccr::CT.set(control, u64::from(!ccr::CT.is_set(control)))
});
}
if !shape.circular {
return;
}
let state = &mut self.channels[channel];
state.remaining = state.total;
state.halved = false;
let total = state.total;
modify(registers, channel, Row::Count, |count| {
cndtr::NDT.set(count, total)
});
self.take_pointers(channel, registers, shape);
}
fn refresh_interrupt(
&mut self,
channel: usize,
registers: &RegisterFile,
context: &mut DeviceContext<'_>,
) {
let control = held(registers, channel, Row::Control);
let wanted = Level::from(
[
(Flag::Error, ccr::TEIE),
(Flag::Half, ccr::HTIE),
(Flag::Complete, ccr::TCIE),
]
.into_iter()
.any(|(flag, enable)| enable.is_set(control) && flag_stands(registers, channel, flag)),
);
if wanted == self.channels[channel].interrupt {
return;
}
self.channels[channel].interrupt = wanted;
context.interrupt(channel, wanted);
}
}
fn channel_row(register: Register) -> Option<(usize, Row)> {
let index = register.index().checked_sub(FIRST_CHANNEL_ROW)?;
Some((index / ROWS_PER_CHANNEL, ROWS[index % ROWS_PER_CHANNEL]))
}
fn locked_while_enabled() -> u64 {
[
ccr::CT,
ccr::DBM,
ccr::MEM2MEM,
ccr::PL,
ccr::MSIZE,
ccr::PSIZE,
ccr::MINC,
ccr::PINC,
ccr::DIR,
]
.into_iter()
.map(FieldSpec::mask)
.sum()
}
impl Bdma {
fn remember(&mut self, channel: usize, registers: &RegisterFile) {
for row in ROWS {
self.channels[channel].guarded[row as usize] = held(registers, channel, row);
}
}
fn clear_flags(
&mut self,
written: u64,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
for (channel, flags) in FLAGS.iter().enumerate() {
if flags[Flag::Global as usize].is_set(written) {
for flag in [Flag::Complete, Flag::Half, Flag::Error] {
set_flag(registers, channel, flag, false);
}
continue;
}
for flag in [Flag::Complete, Flag::Half, Flag::Error] {
if flags[flag as usize].is_set(written) {
set_flag(registers, channel, flag, false);
}
}
}
// Section 16.6.2 types every bit write-only and ST's file types the register read-write, so
// what was written would otherwise stay where the part keeps nothing.
registers.set(Register::BdmaIfcr, 0);
for channel in 0..CHANNELS {
self.refresh_interrupt(channel, registers, context);
}
}
fn write_channel(
&mut self,
channel: usize,
row: Row,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
let kept = self.channels[channel].guarded[row as usize];
let running = ccr::EN.is_set(self.channels[channel].guarded[Row::Control as usize]);
let target = register(channel, row);
match row {
Row::Control if running => {
registers.restore(target, locked_while_enabled(), kept);
}
Row::Count if running => registers.set(target, kept),
_ => {}
}
if row != Row::Control {
return;
}
match (running, Self::enabled(registers, channel)) {
(false, true) => self.start(channel, registers, context),
(true, false) => {
self.channels[channel].outstanding = None;
self.refresh_interrupt(channel, registers, context);
}
_ => self.refresh_interrupt(channel, registers, context),
}
}
}
impl Device for Bdma {
type Register = Register;
fn write(
&mut self,
register: Register,
value: u64,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
if register == Register::BdmaIfcr {
self.clear_flags(value, registers, context);
} else if let Some((channel, row)) = channel_row(register) {
self.write_channel(channel, row, registers, context);
self.remember(channel, registers);
}
}
/// The level DMAMUX2 drives on the channel a BDMA channel listens to is its request, RM0433 rev 9
/// sections 16.3.2 and 17.3.3. Every other instance's request is somebody else's.
///
/// The level is kept whether or not the channel is enabled, so a channel enabled while a request
/// already stands starts at once, which is what a level means.
fn on_request(
&mut self,
change: RequestChange,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
if change.instance != DMAMUX2.name || change.request >= CHANNELS {
return;
}
let channel = change.request;
let high = change.level == Level::High;
self.channels[channel].requested = high;
if high {
self.pump(channel, registers, context);
}
self.remember(channel, registers);
}
fn on_access(
&mut self,
token: u64,
value: u64,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
self.answered(token, value, registers, context);
self.remember(from_token(token).0, registers);
}
/// An access the bus wouldn't make is the bus error of RM0433 rev 9 section 16.4.7. The transfer
/// error stands and hardware clears the enable, so the block ends where it is rather than leaving the
/// channel waiting on an answer that's never coming.
fn on_refused(
&mut self,
token: u64,
registers: &mut RegisterFile,
context: &mut DeviceContext<'_>,
) {
let (channel, side) = from_token(token);
if self.channels[channel].outstanding == Some(side) {
self.fail(channel, registers, context);
}
self.remember(channel, registers);
}
/// A system reset leaves every channel idle with no request standing.
fn reset(&mut self) {
self.channels = std::array::from_fn(|_| Channel::default());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::soc::stm32h743::generated::{BDMA, PERIPHERALS, bdma::REGISTERS};
use crate::soc::stm32h743::{self, MEMORY};
/// The layout is regular, which is what lets [`register`] find a row by arithmetic instead of a table
/// of forty names. RM0433 rev 9 section 16.6.3 gives the shape in as many words, "Address offset: 0x08 +
/// 0x14 * x", and sections 16.6.4 to 16.6.7 put the other four a word apart after it.
#[test]
fn the_channels_registers_run_in_blocks_of_five() {
const SUFFIXES: [&str; ROWS_PER_CHANNEL] = ["CCR", "CNDTR", "CPAR", "CM0AR", "CM1AR"];
assert_eq!(
REGISTERS.len(),
FIRST_CHANNEL_ROW + CHANNELS * ROWS_PER_CHANNEL,
"ISR and IFCR and five to a channel"
);
assert_eq!(Register::BdmaIsr.spec().offset, 0x0);
assert_eq!(Register::BdmaIfcr.spec().offset, 0x4);
for number in 0..CHANNELS {
for (index, row) in ROWS.into_iter().enumerate() {
let spec = register(number, row).spec();
assert_eq!(spec.name, format!("BDMA_{}{number}", SUFFIXES[index]));
assert_eq!(
u64::from(spec.offset),
0x08 + 0x14 * number as u64 + 4 * index as u64
);
}
let fields = ®ister(number, Row::Control).spec().fields;
let first = ®ister(0, Row::Control).spec().fields;
assert_eq!(
fields
.iter()
.map(|field| (field.name, field.bit, field.width))
.collect::<Vec<_>>(),
first
.iter()
.map(|field| (field.name, field.bit, field.width))
.collect::<Vec<_>>(),
"channel {number}'s control register has channel 0's fields"
);
}
}
/// Each channel's four flags sit at bits `4x` to `4x + 3` of `ISR` in the order global, complete, half,
/// error, and each clear bit of `IFCR` is at its flag's own position, sections 16.6.1 and 16.6.2, which
/// is what lets a write to `IFCR` be applied to `ISR` as a mask.
#[test]
fn every_channels_flags_sit_at_four_bits_a_channel() {
const NAMES: [(&str, &str); 4] = [
("GIF", "CGIF"),
("TCIF", "CTCIF"),
("HTIF", "CHTIF"),
("TEIF", "CTEIF"),
];
for (number, flags) in FLAGS.into_iter().enumerate() {
for (index, field) in flags.into_iter().enumerate() {
let (flag, clear) = NAMES[index];
assert_eq!(field.name, format!("{flag}{number}"));
assert_eq!(usize::from(field.bit), 4 * number + index);
let clearing = Register::BdmaIfcr
.spec()
.fields
.iter()
.find(|spec| spec.name == format!("{clear}{number}"))
.unwrap_or_else(|| panic!("{clear}{number} clears {}", field.name));
assert_eq!(clearing.bit, field.bit);
}
}
}
/// The controller reaches the D3 domain and nothing else, section 2.1.6: every instance the part
/// has in the AHB4 or APB4 address range is inside a span it reaches, no other instance is, and the
/// two memories are the part's SRAM4 and backup SRAM.
#[test]
fn every_d3_instance_lies_inside_a_reachable_span_and_no_other_does() {
const AHB4_AND_APB4: std::ops::Range<u64> = 0x5800_0000..0x5803_0000;
for peripheral in &PERIPHERALS {
let inside = REACHABLE
.iter()
.any(|span| span.holds(peripheral.base, peripheral.size));
assert_eq!(
inside,
AHB4_AND_APB4.contains(&peripheral.base),
"{} at {:#x}",
peripheral.name,
peripheral.base
);
}
for (span, name) in [(SRAM4, "sram4"), (BACKUP_SRAM, "backup_sram")] {
let region = MEMORY
.iter()
.find(|region| region.name == name)
.unwrap_or_else(|| panic!("the map has {name}"));
assert_eq!(
(span.base, span.bytes),
(region.base, region.size),
"{name}"
);
}
for region in MEMORY
.iter()
.filter(|region| !region.name.contains("sram4") && region.name != "backup_sram")
{
assert!(
!REACHABLE
.iter()
.any(|span| span.overlaps(region.base, region.size)),
"{} is outside the D3 domain",
region.name
);
}
assert_eq!(BDMA.interrupts.len(), CHANNELS, "a line to each channel");
}
#[derive(Debug, Default)]
struct Ram(std::collections::BTreeMap<u64, u8>);
impl crate::emulator::Memory for Ram {
fn read(&mut self, address: u64, width: Width) -> Result<u64, crate::emulator::Error> {
Ok((0..u64::from(width.bytes()))
.map(|offset| {
u64::from(self.0.get(&(address + offset)).copied().unwrap_or(0)) << (offset * 8)
})
.sum())
}
fn write(
&mut self,
address: u64,
width: Width,
value: u64,
) -> Result<(), crate::emulator::Error> {
for offset in 0..u64::from(width.bytes()) {
let byte = u8::try_from((value >> (offset * 8)) & 0xFF).expect("one byte");
self.0.insert(address + offset, byte);
}
Ok(())
}
}
/// The model's state serializes mid-block, with an item read and not yet written and the counter part
/// way down.
#[test]
fn the_model_state_round_trips_through_a_snapshot() {
use crate::mcu::{Operate, Single};
use crate::snapshot;
use crate::time::VirtualNanos;
const AT: VirtualNanos = VirtualNanos(0);
let mut soc =
Single::new(stm32h743::builder(), "BDMA", Bdma::new()).expect("the bus builds");
let mut memory = Ram::default();
let address = |register: Register| BDMA.base + u64::from(register.spec().offset);
let write = |soc: &mut Single<Bdma>, register: Register, value: u64| {
soc.write(AT, address(register), Width::W32, value)
.expect("the controller answers");
};
write(&mut soc, Register::BdmaCpar1, SRAM4.base);
write(&mut soc, Register::BdmaCm0ar1, BACKUP_SRAM.base);
write(&mut soc, Register::BdmaCndtr1, 8);
let control = ccr::PINC.set(ccr::MINC.set(ccr::EN.set(0, 1), 1), 1);
write(&mut soc, Register::BdmaCcr1, control);
for level in [Level::High, Level::Low] {
soc.drive(AT, |model, registers, context| {
let change = RequestChange {
instance: "DMAMUX2",
request: 1,
level,
};
model.on_request(change, registers, context);
});
}
soc.settle(&mut memory).expect("the memory answers");
assert_eq!(
cndtr::NDT.get(soc.pair().1.get(Register::BdmaCndtr1)),
7,
"one item moved"
);
let saved = snapshot::stored(&soc.model).expect("the model serializes");
let mut restored = Bdma::new();
snapshot::load(&mut restored, snapshot::keep(saved)).expect("and deserializes");
assert_eq!(format!("{restored:?}"), format!("{:?}", soc.model));
}
}