//! Host-side configurations for the target. //! //! See [`RuntimeBuilder::build`] to understand the linker script generation //! steps. 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 { if Family::Imxrt1064 == family { FlexSpi::FlexSpi2 } else { FlexSpi::FlexSpi1 } } fn start_address(self, family: Family) -> Option { 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, 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> { 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, _ => 0x000, } } /// Where does the OCRAM region begin? /// /// This includes dedicated any OCRAM regions, if any exist for the chip. fn ocram_start(self) -> u32 { if Family::Imxrt1170 == self { // 256 KiB offset from the OCRAM M4 backdoor. 0x2024_0000 } else { // Either starts the FlexRAM OCRAM banks, or the // dedicated OCRAM regions (for supported devices). 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:?}" ); } } }