imxrt-rt 0.1.0

Startup and runtime support for i.MX RT processors.
Documentation
//! Host-side configurations for the target.
//!
//! See [`RuntimeBuilder::build`] to understand the linker script generation
//! steps.

// Please explicitly match all `Family` variants. If someone wants to add
// a new `Family`, this will show help them find all the settings they need
// to consider.
#![warn(clippy::wildcard_enum_match_arm)]

use std::{
    env,
    fmt::Display,
    fs::{self, File},
    io::{self, Write},
    path::PathBuf,
};

/// Memory partitions.
///
/// Use with [`RuntimeBuilder`] to specify the placement of sections
/// in the final program. Note that the `RuntimeBuilder` only does limited
/// checks on memory placements. Generally, it's OK to place data in ITCM,
/// and instructions in DTCM; however, this isn't recommended for optimal
/// performance.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Memory {
    /// Place the section in (external) flash.
    ///
    /// Reads and writes are translated into commands on an external
    /// bus, like FlexSPI.
    Flash,
    /// Place the section in data tightly coupled memory (DTCM).
    Dtcm,
    /// Place the section in instruction tightly coupled memory (ITCM).
    Itcm,
    /// Place the section in on-chip RAM (OCRAM).
    ///
    /// If your chip includes dedicated OCRAM memory, the implementation
    /// utilizes that OCRAM before utilizing any FlexRAM OCRAM banks.
    Ocram,
}

/// The FlexSPI peripheral that interfaces your flash chip.
///
/// The [`RuntimeBuilder`] selects `FlexSpi1` for nearly all chip
/// families. However, it selects `FlexSpi2` for the 1064 in order
/// to utilize its on-board flash. You can override the selection
/// using [`RuntimeBuilder::flexspi()`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlexSpi {
    /// Interface flash using FlexSPI 1.
    FlexSpi1,
    /// Interface flash using FlexSPI 2.
    FlexSpi2,
}

impl FlexSpi {
    fn family_default(family: Family) -> Self {
        match family {
            Family::Imxrt1064 => FlexSpi::FlexSpi2,
            Family::Imxrt1010
            | Family::Imxrt1015
            | Family::Imxrt1020
            | Family::Imxrt1050
            | Family::Imxrt1060
            | Family::Imxrt1170 => FlexSpi::FlexSpi1,
        }
    }
    fn start_address(self, family: Family) -> Option<u32> {
        match (self, family) {
            // FlexSPI1, 10xx
            (
                FlexSpi::FlexSpi1,
                Family::Imxrt1010
                | Family::Imxrt1015
                | Family::Imxrt1020
                | Family::Imxrt1050
                | Family::Imxrt1060
                | Family::Imxrt1064,
            ) => Some(0x6000_0000),
            // FlexSPI2 not available on 10xx families
            (
                FlexSpi::FlexSpi2,
                Family::Imxrt1010 | Family::Imxrt1015 | Family::Imxrt1020 | Family::Imxrt1050,
            ) => None,
            // FlexSPI 2 available on 10xx families
            (FlexSpi::FlexSpi2, Family::Imxrt1060 | Family::Imxrt1064) => Some(0x7000_0000),
            // 11xx support
            (FlexSpi::FlexSpi1, Family::Imxrt1170) => Some(0x3000_0000),
            (FlexSpi::FlexSpi2, Family::Imxrt1170) => Some(0x6000_0000),
        }
    }
    fn supported_for_family(self, family: Family) -> bool {
        self.start_address(family).is_some()
    }
}

impl Display for Memory {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::Flash => f.write_str("FLASH"),
            Self::Itcm => f.write_str("ITCM"),
            Self::Dtcm => f.write_str("DTCM"),
            Self::Ocram => f.write_str("OCRAM"),
        }
    }
}

/// Define an alias for `name` that maps to a memory block named `placement`.
fn region_alias(output: &mut dyn Write, name: &str, placement: Memory) -> io::Result<()> {
    writeln!(output, "REGION_ALIAS(\"REGION_{}\", {});", name, placement)
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct FlashOpts {
    size: usize,
    flexspi: FlexSpi,
}

/// Builder for the i.MX RT runtime.
///
/// `RuntimeBuilder` let you assign sections to memory regions. It also lets
/// you partition FlexRAM DTCM/ITCM/OCRAM. Call [`build()`](RuntimeBuilder::build) to commit the
/// runtime configuration.
///
/// # Behaviors
///
/// The implementation tries to place the stack in the lowest-possible memory addresses.
/// This means the stack will grow down into reserved memory below DTCM and OCRAM for most
/// chip families. The outlier is the 1170, where the stack will grow into OCRAM backdoor for
/// the CM4 coprocessor. Be careful here...
///
/// Similarly, the implementation tries to place the heap in the highest-possible memory
/// addresses. This means the heap will grow up into reserved memory above DTCM and OCRAM
/// for most chip families.
///
/// The vector table requires a 1024-byte alignment. The vector table's placement is prioritized
/// above all other sections, except the stack. If placing the stack and vector table in the
/// same section (which is the default behavior), consider keeping the stack size as a multiple
/// of 1 KiB to minimize internal fragmentation.
///
/// # Default values
///
/// The example below demonstrates the default `RuntimeBuilder` memory placements,
/// stack sizes, and heap sizes.
///
/// ```
/// use imxrt_rt::{Family, RuntimeBuilder, Memory};
///
/// const FLASH_SIZE: usize = 16 * 1024;
/// let family = Family::Imxrt1060;
///
/// let mut b = RuntimeBuilder::from_flexspi(family, FLASH_SIZE);
/// // FlexRAM banks represent default fuse values.
/// b.flexram_banks(family.default_flexram_banks());
/// b.text(Memory::Itcm);    // Copied from flash.
/// b.rodata(Memory::Ocram); // Copied from flash.
/// b.data(Memory::Ocram);   // Copied from flash.
/// b.vectors(Memory::Dtcm); // Copied from flash.
/// b.bss(Memory::Ocram);
/// b.uninit(Memory::Ocram);
/// b.stack(Memory::Dtcm);
/// b.stack_size(8 * 1024);  // 8 KiB stack.
/// b.heap(Memory::Dtcm);    // Heap in DTCM...
/// b.heap_size(0);          // ...but no space given to the heap.
///
/// assert_eq!(b, RuntimeBuilder::from_flexspi(family, FLASH_SIZE));
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeBuilder {
    family: Family,
    flexram_banks: FlexRamBanks,
    text: Memory,
    rodata: Memory,
    data: Memory,
    vectors: Memory,
    bss: Memory,
    uninit: Memory,
    stack: Memory,
    stack_size: usize,
    heap: Memory,
    heap_size: usize,
    flash_opts: Option<FlashOpts>,
    linker_script_name: String,
}

const DEFAULT_LINKER_SCRIPT_NAME: &str = "imxrt-link.x";

impl RuntimeBuilder {
    /// Creates a runtime that can execute and load contents from
    /// FlexSPI flash.
    ///
    /// `flash_size` is the size of your flash component, in bytes.
    pub fn from_flexspi(family: Family, flash_size: usize) -> Self {
        Self {
            family,
            flexram_banks: family.default_flexram_banks(),
            text: Memory::Itcm,
            rodata: Memory::Ocram,
            data: Memory::Ocram,
            vectors: Memory::Dtcm,
            bss: Memory::Ocram,
            uninit: Memory::Ocram,
            stack: Memory::Dtcm,
            stack_size: 8 * 1024,
            heap: Memory::Dtcm,
            heap_size: 0,
            flash_opts: Some(FlashOpts {
                size: flash_size,
                flexspi: FlexSpi::family_default(family),
            }),
            linker_script_name: DEFAULT_LINKER_SCRIPT_NAME.into(),
        }
    }
    /// Set the FlexRAM bank allocation.
    ///
    /// Use this to customize the sizes of DTCM, ITCM, and OCRAM.
    /// See the `FlexRamBanks` documentation for requirements on the
    /// bank allocations.
    pub fn flexram_banks(&mut self, flexram_banks: FlexRamBanks) -> &mut Self {
        self.flexram_banks = flexram_banks;
        self
    }
    /// Set the memory placement for code.
    pub fn text(&mut self, memory: Memory) -> &mut Self {
        self.text = memory;
        self
    }
    /// Set the memory placement for read-only data.
    pub fn rodata(&mut self, memory: Memory) -> &mut Self {
        self.rodata = memory;
        self
    }
    /// Set the memory placement for mutable data.
    pub fn data(&mut self, memory: Memory) -> &mut Self {
        self.data = memory;
        self
    }
    /// Set the memory placement for the vector table.
    pub fn vectors(&mut self, memory: Memory) -> &mut Self {
        self.vectors = memory;
        self
    }
    /// Set the memory placement for zero-initialized data.
    pub fn bss(&mut self, memory: Memory) -> &mut Self {
        self.bss = memory;
        self
    }
    /// Set the memory placement for uninitialized data.
    pub fn uninit(&mut self, memory: Memory) -> &mut Self {
        self.uninit = memory;
        self
    }
    /// Set the memory placement for stack memory.
    pub fn stack(&mut self, memory: Memory) -> &mut Self {
        self.stack = memory;
        self
    }
    /// Set the size, in bytes, of the stack.
    pub fn stack_size(&mut self, bytes: usize) -> &mut Self {
        self.stack_size = bytes;
        self
    }
    /// Set the memory placement for the heap.
    ///
    /// Note that the default heap has no size. Use [`heap_size`](Self::heap_size)
    /// to allocate space for a heap.
    pub fn heap(&mut self, memory: Memory) -> &mut Self {
        self.heap = memory;
        self
    }
    /// Set the size, in bytes, of the heap.
    pub fn heap_size(&mut self, bytes: usize) -> &mut Self {
        self.heap_size = bytes;
        self
    }
    /// Set the FlexSPI peripheral that interfaces flash.
    ///
    /// See the [`FlexSpi`] to understand the default values.
    /// If this builder is not configuring a flash-loaded runtime, this
    /// call is silently ignored.
    pub fn flexspi(&mut self, peripheral: FlexSpi) -> &mut Self {
        if let Some(flash_opts) = &mut self.flash_opts {
            flash_opts.flexspi = peripheral;
        }
        self
    }

    /// Set the name of the linker script file.
    ///
    /// You can use this to customize the linker script name for your users.
    /// See the [crate-level documentation](crate#linker-script) for more
    /// information.
    pub fn linker_script_name(&mut self, name: &str) -> &mut Self {
        self.linker_script_name = name.into();
        self
    }

    /// Commit the runtime configuration.
    ///
    /// # Errors
    ///
    /// The implementation ensures that your chip can support the FlexRAM bank
    /// allocation. An invalid allocation is signaled by an error.
    ///
    /// Returns an error if any of the following sections are placed in flash:
    ///
    /// - data
    /// - vectors
    /// - bss
    /// - uninit
    /// - stack
    /// - heap
    ///
    /// The implementation may rely on the _linker_ to signal other errors.
    /// For example, suppose a runtime configuration with no ITCM banks. If a
    /// section is placed in ITCM, that error could be signaled here, or through
    /// the linker. No matter the error path, the implementation ensures that there
    /// will be an error.
    pub fn build(&self) -> Result<(), Box<dyn std::error::Error>> {
        self.check_configurations()?;

        // Since `build` is called from a build script, the output directory
        // represents the path to the _user's_ crate.
        let out_dir = PathBuf::from(env::var("OUT_DIR")?);
        println!("cargo:rustc-link-search={}", out_dir.display());

        // The main linker script expects to INCLUDE this file. This file
        // uses region aliases to associate region names to actual memory
        // regions (see the Memory enum).
        let mut memory_x = File::create(out_dir.join("imxrt-memory.x"))?;

        if let Some(flash_opts) = &self.flash_opts {
            write_flash_memory_map(&mut memory_x, self.family, flash_opts, &self.flexram_banks)?;
        } else {
            write_ram_memory_map(&mut memory_x, self.family, &self.flexram_banks)?;
        }

        #[cfg(feature = "device")]
        writeln!(&mut memory_x, "INCLUDE device.x")?;

        // Keep these alias names in sync with the primary linker script.
        // The main linker script uses these region aliases for placing
        // sections. Then, the user specifies the actual placement through
        // the builder. This saves us the step of actually generating SECTION
        // commands.
        region_alias(&mut memory_x, "TEXT", self.text)?;
        region_alias(&mut memory_x, "VTABLE", self.vectors)?;
        region_alias(&mut memory_x, "RODATA", self.rodata)?;
        region_alias(&mut memory_x, "DATA", self.data)?;
        region_alias(&mut memory_x, "BSS", self.bss)?;
        region_alias(&mut memory_x, "UNINIT", self.uninit)?;

        region_alias(&mut memory_x, "STACK", self.stack)?;
        region_alias(&mut memory_x, "HEAP", self.heap)?;
        // Used in the linker script and / or target code.
        writeln!(&mut memory_x, "__stack_size = {:#010X};", self.stack_size)?;
        writeln!(&mut memory_x, "__heap_size = {:#010X};", self.heap_size)?;

        if self.flash_opts.is_some() {
            // Runtime will see different VMA and LMA, and copy the sections.
            region_alias(&mut memory_x, "LOAD_VTABLE", Memory::Flash)?;
            region_alias(&mut memory_x, "LOAD_TEXT", Memory::Flash)?;
            region_alias(&mut memory_x, "LOAD_RODATA", Memory::Flash)?;
            region_alias(&mut memory_x, "LOAD_DATA", Memory::Flash)?;
        } else {
            // When the VMA and LMA are equal, the runtime performs no copies.
            region_alias(&mut memory_x, "LOAD_VTABLE", self.vectors)?;
            region_alias(&mut memory_x, "LOAD_TEXT", self.text)?;
            region_alias(&mut memory_x, "LOAD_RODATA", self.rodata)?;
            region_alias(&mut memory_x, "LOAD_DATA", self.data)?;
        }

        // Referenced in target code.
        writeln!(
            &mut memory_x,
            "__flexram_config = {:#010X};",
            self.flexram_banks.config()
        )?;
        // The target runtime looks at this value to predicate some pre-init instructions.
        // Could be helpful for binary identification, but it's an undocumented feature.
        writeln!(&mut memory_x, "__imxrt_family = {};", self.family.id(),)?;

        // Place the primary linker script in the user's output directory. Name may be decided
        // by the user.
        let link_x = include_bytes!("host/imxrt-link.x");
        fs::write(out_dir.join(&self.linker_script_name), link_x)?;

        // Also place the boot header in the search path. Do this unconditionally (even if
        // the user is booting from RAM). Name matters, since it's INCLUDEd in our linker
        // scripts.
        let boot_header_x = include_bytes!("host/imxrt-boot-header.x");
        fs::write(out_dir.join("imxrt-boot-header.x"), boot_header_x)?;

        Ok(())
    }

    /// Implement i.MX RT specific sanity checks.
    ///
    /// This might not check everything! If the linker may detect a condition, we'll
    /// let the linker do that.
    fn check_configurations(&self) -> Result<(), String> {
        if self.family.flexram_bank_count() < self.flexram_banks.bank_count() {
            return Err(format!(
            "Chip {:?} only has {} total FlexRAM banks. Cannot allocate {:?}, a total of {} banks",
            self.family,
            self.family.flexram_bank_count(),
            self.flexram_banks,
            self.flexram_banks.bank_count()
        ));
        }
        if self.flexram_banks.ocram < self.family.bootrom_ocram_banks() {
            return Err(format!(
                "Chip {:?} requires at least {} OCRAM banks for the bootloader ROM",
                self.family,
                self.family.bootrom_ocram_banks()
            ));
        }
        if let Some(flash_opts) = &self.flash_opts {
            if !flash_opts.flexspi.supported_for_family(self.family) {
                return Err(format!(
                    "Chip {:?} does not support {:?}",
                    self.family, flash_opts.flexspi
                ));
            }
        }

        fn prevent_flash(name: &str, memory: Memory) -> Result<(), String> {
            if memory == Memory::Flash {
                Err(format!("Section '{}' cannot be placed in flash", name))
            } else {
                Ok(())
            }
        }
        macro_rules! prevent_flash {
            ($sec:ident) => {
                prevent_flash(stringify!($sec), self.$sec)
            };
        }

        prevent_flash!(data)?;
        prevent_flash!(vectors)?;
        prevent_flash!(bss)?;
        prevent_flash!(uninit)?;
        prevent_flash!(stack)?;
        prevent_flash!(heap)?;

        Ok(())
    }
}

/// Write RAM-like memory blocks.
///
/// Skips a section if there's no FlexRAM block allocated. If a user references one
/// of this skipped sections, linking fails.
fn write_flexram_memories(
    output: &mut dyn Write,
    family: Family,
    flexram_banks: &FlexRamBanks,
) -> io::Result<()> {
    if flexram_banks.itcm > 0 {
        writeln!(
            output,
            "ITCM (RWX) : ORIGIN = 0x00000000, LENGTH = {:#X}",
            flexram_banks.itcm * family.flexram_bank_size(),
        )?;
    }
    if flexram_banks.dtcm > 0 {
        writeln!(
            output,
            "DTCM (RWX) : ORIGIN = 0x20000000, LENGTH = {:#X}",
            flexram_banks.dtcm * family.flexram_bank_size(),
        )?;
    }

    let ocram_size =
        flexram_banks.ocram * family.flexram_bank_size() + family.dedicated_ocram_size();
    if ocram_size > 0 {
        writeln!(
            output,
            "OCRAM (RWX) : ORIGIN = {:#X}, LENGTH = {:#X}",
            family.ocram_start(),
            ocram_size,
        )?;
    }
    Ok(())
}

/// Generate a linker script MEMORY command that includes a FLASH block.
///
/// If called, the linker script includes the boot header, which is also
/// expressed as a linker script.
fn write_flash_memory_map(
    output: &mut dyn Write,
    family: Family,
    flash_opts: &FlashOpts,
    flexram_banks: &FlexRamBanks,
) -> io::Result<()> {
    writeln!(
        output,
        "/* Memory map for '{:?}' with custom flash length {}. */",
        family, flash_opts.size
    )?;
    writeln!(output, "MEMORY {{")?;
    writeln!(
        output,
        "FLASH (RX) : ORIGIN = {:#X}, LENGTH = {:#X}",
        flash_opts
            .flexspi
            .start_address(family)
            .expect("Already checked"),
        flash_opts.size
    )?;
    write_flexram_memories(output, family, flexram_banks)?;
    writeln!(output, "}}")?;
    writeln!(output, "__fcb_offset = {:#X};", family.fcb_offset())?;
    writeln!(output, "INCLUDE imxrt-boot-header.x")?;
    Ok(())
}

/// Generate a linker script MEMORY command that supports RAM execution.
///
/// It's like [`write_flash_memory_map`], but it doesn't include the flash
/// important tidbits.
fn write_ram_memory_map(
    output: &mut dyn Write,
    family: Family,
    flexram_banks: &FlexRamBanks,
) -> io::Result<()> {
    writeln!(
        output,
        "/* Memory map for '{:?}' that executes from RAM. */",
        family,
    )?;
    writeln!(output, "MEMORY {{")?;
    write_flexram_memories(output, family, flexram_banks)?;
    writeln!(output, "}}")?;
    Ok(())
}

/// i.MX RT chip family.
///
/// Chip families are designed by reference manuals and produce categories.
/// Supply this to a [`RuntimeBuilder`] in order to check runtime configurations.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Family {
    Imxrt1010,
    Imxrt1015,
    Imxrt1020,
    Imxrt1050,
    Imxrt1060,
    Imxrt1064,
    Imxrt1170,
}

impl Family {
    /// Family identifier.
    ///
    /// These values may be stored in the image and observe by the runtime
    /// initialzation routine. Make sure these numbers are kept in sync with
    /// any hard-coded values.
    const fn id(self) -> u32 {
        match self {
            Family::Imxrt1010 => 1010,
            Family::Imxrt1015 => 1015,
            Family::Imxrt1020 => 1020,
            Family::Imxrt1050 => 1050,
            Family::Imxrt1060 => 1060,
            Family::Imxrt1064 => 1064,
            Family::Imxrt1170 => 1170,
        }
    }
    /// How many FlexRAM banks are available?
    pub const fn flexram_bank_count(self) -> u32 {
        match self {
            Family::Imxrt1010 | Family::Imxrt1015 => 4,
            Family::Imxrt1020 => 8,
            Family::Imxrt1050 | Family::Imxrt1060 | Family::Imxrt1064 => 16,
            // No ECC support; treating all banks as equal.
            Family::Imxrt1170 => 16,
        }
    }
    /// How large (bytes) is each FlexRAM bank?
    const fn flexram_bank_size(self) -> u32 {
        32 * 1024
    }
    /// How many OCRAM banks does the boot ROM need?
    const fn bootrom_ocram_banks(self) -> u32 {
        match self {
            Family::Imxrt1010 | Family::Imxrt1015 | Family::Imxrt1020 | Family::Imxrt1050 => 1,
            // 9.5.1. memory maps point at OCRAM2.
            Family::Imxrt1060 | Family::Imxrt1064 => 0,
            // Boot ROM uses dedicated OCRAM1.
            Family::Imxrt1170 => 0,
        }
    }
    /// Where's the FlexSPI configuration bank located?
    fn fcb_offset(self) -> usize {
        match self {
            Family::Imxrt1010 | Family::Imxrt1170 => 0x400,
            Family::Imxrt1015
            | Family::Imxrt1020
            | Family::Imxrt1050
            | Family::Imxrt1060
            | Family::Imxrt1064 => 0x000,
        }
    }

    /// Where does the OCRAM region begin?
    ///
    /// This includes dedicated any OCRAM regions, if any exist for the chip.
    fn ocram_start(self) -> u32 {
        match self {
            // 256 KiB offset from the OCRAM M4 backdoor.
            Family::Imxrt1170 => 0x2024_0000,
            // Either starts the FlexRAM OCRAM banks, or the
            // dedicated OCRAM regions (for supported devices).
            Family::Imxrt1010
            | Family::Imxrt1015
            | Family::Imxrt1020
            | Family::Imxrt1050
            | Family::Imxrt1060
            | Family::Imxrt1064 => 0x2020_0000,
        }
    }

    /// What's the size, in bytes, of the dedicated OCRAM section?
    ///
    /// This isn't supported by all chips.
    const fn dedicated_ocram_size(self) -> u32 {
        match self {
            Family::Imxrt1010 | Family::Imxrt1015 | Family::Imxrt1020 | Family::Imxrt1050 => 0,
            Family::Imxrt1060 | Family::Imxrt1064 => 512 * 1024,
            // - Two dedicated OCRAMs
            // - Two dedicated OCRAM ECC regions that aren't used for ECC
            // - One FlexRAM OCRAM ECC region that's strictly OCRAM, without ECC
            Family::Imxrt1170 => (2 * 512 + 2 * 64 + 128) * 1024,
        }
    }

    /// Returns the default FlexRAM bank allocations for this chip.
    ///
    /// The default values represent the all-zero fuse values.
    pub fn default_flexram_banks(self) -> FlexRamBanks {
        match self {
            Family::Imxrt1010 | Family::Imxrt1015 => FlexRamBanks {
                ocram: 2,
                itcm: 1,
                dtcm: 1,
            },
            Family::Imxrt1020 => FlexRamBanks {
                ocram: 4,
                itcm: 2,
                dtcm: 2,
            },
            Family::Imxrt1050 | Family::Imxrt1060 | Family::Imxrt1064 => FlexRamBanks {
                ocram: 8,
                itcm: 4,
                dtcm: 4,
            },
            Family::Imxrt1170 => FlexRamBanks {
                ocram: 0,
                itcm: 8,
                dtcm: 8,
            },
        }
    }
}

/// FlexRAM bank allocations.
///
/// Depending on your device, you may need a non-zero number of
/// OCRAM banks to support the boot ROM. Consult your processor's
/// reference manual for more information.
///
/// You should keep the sum of all banks below or equal to the
/// total number of banks supported by your device. Unallocated memory
/// banks are disabled.
///
/// Banks are typically 32KiB large.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FlexRamBanks {
    /// How many banks are allocated for OCRAM?
    ///
    /// This may need to be non-zero to support the boot ROM.
    /// Consult your reference manual.
    ///
    /// Note: these are FlexRAM OCRAM banks. Do not include any banks
    /// that would represent dedicated OCRAM; the runtime implementation
    /// allocates those automatically. In fact, if your chip includes
    /// dedicated OCRAM, you may set this to zero in order to maximize
    /// DTCM and ITCM utilization.
    pub ocram: u32,
    /// How many banks are allocated for ITCM?
    pub itcm: u32,
    /// How many banks are allocated for DTCM?
    pub dtcm: u32,
}

impl FlexRamBanks {
    /// Total FlexRAM banks.
    const fn bank_count(&self) -> u32 {
        self.ocram + self.itcm + self.dtcm
    }

    /// Produces the FlexRAM configuration.
    fn config(&self) -> u32 {
        assert!(
            self.bank_count() <= 16,
            "Something is wrong; this should have been checked earlier."
        );

        // If a FlexRAM memory type could be allocated
        // to _all_ memory banks, these would represent
        // the configuration masks...
        const OCRAM: u32 = 0x5555_5555; // 0b01...
        const DTCM: u32 = 0xAAAA_AAAA; // 0b10...
        const ITCM: u32 = 0xFFFF_FFFF; // 0b11...

        fn mask(bank_count: u32) -> u32 {
            1u32.checked_shl(bank_count * 2)
                .map(|bit| bit - 1)
                .unwrap_or(u32::MAX)
        }

        let ocram_mask = mask(self.ocram);
        let dtcm_mask = mask(self.dtcm).checked_shl(self.ocram * 2).unwrap_or(0);
        let itcm_mask = mask(self.itcm)
            .checked_shl((self.ocram + self.dtcm) * 2)
            .unwrap_or(0);

        (OCRAM & ocram_mask) | (DTCM & dtcm_mask) | (ITCM & itcm_mask)
    }
}

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

    #[test]
    fn flexram_config() {
        /// Testing table of banks and expected configuration mask.
        #[allow(clippy::unusual_byte_groupings)] // Spacing delimits ITCM / DTCM / OCRAM banks.
        const TABLE: &[(FlexRamBanks, u32)] = &[
            (
                FlexRamBanks {
                    ocram: 16,
                    dtcm: 0,
                    itcm: 0,
                },
                0x55555555,
            ),
            (
                FlexRamBanks {
                    ocram: 0,
                    dtcm: 16,
                    itcm: 0,
                },
                0xAAAAAAAA,
            ),
            (
                FlexRamBanks {
                    ocram: 0,
                    dtcm: 0,
                    itcm: 16,
                },
                0xFFFFFFFF,
            ),
            (
                FlexRamBanks {
                    ocram: 0,
                    dtcm: 0,
                    itcm: 0,
                },
                0,
            ),
            (
                FlexRamBanks {
                    ocram: 1,
                    dtcm: 1,
                    itcm: 1,
                },
                0b11_10_01,
            ),
            (
                FlexRamBanks {
                    ocram: 3,
                    dtcm: 3,
                    itcm: 3,
                },
                0b111111_101010_010101,
            ),
            (
                FlexRamBanks {
                    ocram: 5,
                    dtcm: 5,
                    itcm: 5,
                },
                0b1111111111_1010101010_0101010101,
            ),
            (
                FlexRamBanks {
                    ocram: 1,
                    dtcm: 1,
                    itcm: 14,
                },
                0b1111111111111111111111111111_10_01,
            ),
            (
                FlexRamBanks {
                    ocram: 1,
                    dtcm: 14,
                    itcm: 1,
                },
                0b11_1010101010101010101010101010_01,
            ),
            (
                FlexRamBanks {
                    ocram: 14,
                    dtcm: 1,
                    itcm: 1,
                },
                0b11_10_0101010101010101010101010101,
            ),
        ];

        for (banks, expected) in TABLE {
            let actual = banks.config();
            assert!(
                actual == *expected,
                "\nActual:   {actual:#034b}\nExpected: {expected:#034b}\nBanks: {banks:?}"
            );
        }
    }
}