Booting
In order to boot Rust firmware on an i.MX RT processor, you need at least three crates:
- a crate that defines a compatible boot header for your system.
- the
imxrt-rt
runtime. - a panic handler.
A Cargo.toml
package manifest with these dependencies is shown below.
# Cargo.toml
[dependencies]
# The boot header package.
imxrt1010evk-fcb = "0.1"
# The runtime.
imxrt-rt = "0.1"
# The panic handler.
panic-halt = "0.2"
[build-dependencies]
# The same runtime as above.
imxrt-rt = "0.1"
The runtime is configured in an application's build.rs
script, so the
dependency is included twice. The following build script configures a default
runtime for the i.MXRT1010EVK development board.
//! build.rs use imxrt_rt::{Family, RuntimeBuilder}; fn main() { // The iMXRT1010EVK has 16 MiB of flash. RuntimeBuilder::from_flexspi(Family::Imxrt1010, 16 * 1024 * 1024) .build() .unwrap(); }
Finally, main.rs
defines the program that turns on the i.MXRT1010EVK's LED.
//! main.rs #![no_main] #![no_std] // Include the boot header like this. Otherwise, // it may be removed by the linker. use imxrt1010evk_fcb as _; // Same goes for the panic handler. use panic_halt as _; // The entry macro adorns your main function. use imxrt_rt::entry; const LED_OFFSET: u32 = 1 << 11; // Register addresses come from the reference manual. const IOMUXC_MUX_CTL_PAD_GPIO_11: *mut u32 = 0x401F_8090 as _; const GPIO1_GDIR: *mut u32 = (0x401B_8000 + 0x04) as _; const GPIO1_DR_SET: *mut u32 = (0x401B_8000 + 0x84) as _; #[entry] fn main() -> ! { unsafe { // Configure the pad named "GPIO_11" as a GPIO pin // (as opposed to a UART TX pin, for example). IOMUXC_MUX_CTL_PAD_GPIO_11.write_volatile(5); // Set the GPIO as an output with a RMW operation. let mut gpio1_gdir = GPIO1_GDIR.read_volatile(); gpio1_gdir |= LED_OFFSET; GPIO1_GDIR.write_volatile(gpio1_gdir); // Turn on the LED. GPIO1_DR_SET.write_volatile(LED_OFFSET); } loop {} }
Limitations
This example is sufficient to boot the processor and run a very basic application. However, it's not a good demonstration of how to write an embedded Rust application. In particular,
- the application needs to know register addresses in order to configure IOMUXC pads and GPIO outputs.
- register access is unsafe, since we're reading and writing to raw pointers.
- we need to explicitly use volatile reads and writes, and manually code read-modify-write actions on register fields.
- we are not modeling ownership of peripherals.
The next section introduces a register access layer that provides register addresses, exposes safe and convenient register access, and lets you model peripheral ownership.
The remaining sections describe the boot headers and runtime used in this example.
Boot headers
The specifics of a boot header vary by system. For example, if your system interfaces NOR flash using a FlexSPI peripheral, you'll need a crate that provides a FlexSPI configuration block (FCB). On the other hand, systems that use parallel NAND flash may need a different kind of boot header. Consult your processor's reference manual for more information about boot headers.
As an user of these crates, you simply need to include the proper boot header
for your system. imxrt-rs maintains FCBs for various development boards; they're
listed here.
These FCBs are developed alongside the imxrt-boot-gen
package. If no boot header exists
for your system, use imxrt-boot-gen
to define your system's boot header.
Other boot headers, like the
teensy4-fcb
, may be maintained in
separate projects. Boot headers may also be integrated directly within a board
support package.
Runtime
The imxrt-rt
runtime is an extension
of the cortex-m-rt
package. The custom
runtime satisfies the boot requirements of i.MX RT processors. It also lets you
customize the program memory layout and utilize tightly-coupled memory (TCM)
regions through the FlexRAM interface.
imxrt-rt
uses a convention of symbol names and link sections to properly place
boot headers. See the runtime documentation for a discussion on this convention.