Board support package
A board support package (BSP) combines the previously-covered packages -- the runtime, boot header, RAL, and HAL -- into a crate for a specific hardware system. You can describe this system in terms of
- its i.MX RT processor
- the pinout and supported peripherals
As of this writing, the imxrt-rs project is not actively maintaining BSPs. But with your help and contributions, we're happy to start BSP development. If you're interested in using or maintaining a BSP, reach out to an imxrt-rs maintainer.
Some BSPs, like the teensy4-bsp,
depend on imxrt-rs packages but are maintained as separate projects. If you're
interested in designing a BSP, the teensy4-bsp may have ideas for you.
The rest of this document has recommendations for BSP design, and it demonstrates a small BSP that can manage hardware resources.
Renaming pads
Your board may have a pad (pin) naming convention that differs from the i.MX RT processor pad naming. For example, the Teensy 4.0 and 4.1 identifies pins by incrementing numbers starting at zero, and these pins are mapped to i.MX RT 1062 processor pads. Similarly, an NXP EVK may identify pins by a header & pin number combination, rather than a processor pad. Users might prefer using board names, rather than processor pad names, in their firmware, and the BSP can provide this renaming.
As a BSP designer, you can choose to rename pads
- directly within the BSP.
- as a separate "pins" package that's co-developed with the BSP.
If you're choosing the first approach, you can refer to pad types and objects
through imxrt-hal. See the imxrt-hal documentation and API for more
information.
If you're choosing the second approach, you should directly use the
imxrt-iomuxc crate. By designing
directly to imxrt-iomuxc, you do not need to depend on the larger HAL package
for your pins package. And since imxrt-hal re-exports imxrt-iomuxc, your
pins package will still work with imxrt-hal. For more design guidance, see the
imxrt-iomuxc documentation. This second approach lets others re-use your pins
package without needing to adopt an imxrt-hal dependency.
Take a look at the teensy4-pins
package for an example of a pins
package. Notice how the package renames, and restricts, the i.MX RT processor
pads to those supported by the Teensy 4. Also notice how it depends only on
imxrt-iomuxc, and how it fits within the teensy4-bsp package.
Manage peripheral resources
If you re-read the code presented in this walkthrough, you'll notice that the
unsafe keyword appears in all three examples. This includes the example that
uses imxrt-hal. By design, acquiring an imxrt-ral peripheral instance is
unsafe; see the imxrt-ral API documentation for the rational. This means that
constructing an imxrt-hal driver needs an unsafe call to acquire the
peripheral instance.
A BSP may be the place to implement a resource management policy. This is especially true if the BSP
- only supports a single application with a single entrypoint.
- dictates the available hardware resources for the user.
If your BSP follows these concepts, you can design your BSP to configure and
release hardware resources to the user. With a simple atomic boolean, the BSP
can ensure that resources are only configured and released once, meeting the
safety requirements for imxrt-ral instance access.
For a rough example of this pattern, see the board package maintained in the
imxrt-hal repository. The board
package is designed to expedite hardware testing and example development, and it
handles unsafe instance access on behalf of the example user.
A small multi-BSP example
This small BSP example demonstrates the peripheral resource management concept, though with some limitations. It builds on the previous example that turns on one board's LED. To make it interesting, the example supports three different boards:
- Teensy 4
- i.MXRT1010EVK
- i.MXRT1170EVK (Cortex-M7)
However, to stay concise, the example only demonstrates resource initialization for the i.MXRT1010EVK.
The example uses Cargo features to select a target board. The Cargo.toml
snippet below demonstrates the dependencies and feature configurations. A
feature combines
- an
imxrt-ralchip selection - an
imxrt-halfamily selection - a boot header
to describe a board. The imxrt-ral and imxrt-hal features ensure that the
peripherals and drivers are configured for the board's processor. Similarly, the
boot header ensures that the runtime can boot the board's processor.
# Cargo.toml
[dependencies]
imxrt-hal = { version = "0.5" }
imxrt-ral = { version = "0.5", features = ["rt"] }
imxrt-rt = { version = "0.1", features = ["device"] }
teensy4-fcb = { version = "0.4", optional = true }
imxrt1010evk-fcb = { version = "0.1", optional = true }
imxrt1170evk-fcb = { version = "0.1", optional = true }
panic-halt = "0.2"
cfg-if = "1"
[build-dependencies]
imxrt-rt = { version = "0.1", features = ["device"] }
[features]
# board = [
# "imxrt-ral/${CHIP},
# "imxrt-hal/${FAMILY},
# "${BOOT_HEADER},
# ]
teensy4 = [
"imxrt-ral/imxrt1062",
"imxrt-hal/imxrt1060",
"dep:teensy4-fcb",
]
imxrt1010evk = [
"imxrt-ral/imxrt1011",
"imxrt-hal/imxrt1010",
"dep:imxrt1010evk-fcb",
]
imxrt1170evk-cm7 = [
"imxrt-ral/imxrt1176_cm7",
"imxrt-hal/imxrt1170",
"dep:imxrt1170evk-fcb",
]
The build.rs runtime configuration is aware of these three boards, and it
configures the runtime based on the board's chip and flash size.
//! build.rs use imxrt_rt::{Family, RuntimeBuilder}; struct Board { family: Family, flash_size: usize, } const BOARD: Board = if cfg!(feature = "teensy4") { Board { family: Family::Imxrt1060, flash_size: 1984 * 1024, } } else if cfg!(feature = "imxrt1010evk") { Board { family: Family::Imxrt1010, flash_size: 16 * 1024 * 1024, } } else if cfg!(feature = "imxrt1170evk-cm7") { Board { family: Family::Imxrt1170, flash_size: 16 * 1024 * 1024, } } else { panic!("No board selected!") }; fn main() { RuntimeBuilder::from_flexspi(BOARD.family, BOARD.flash_size) .build() .unwrap(); }
Here's the application code. The board module conditionally exposes a board's
Resources based on the board selection.
By convention, all boards define a Resources struct, which can be taken. The
object contains a led member of type Led. The Led type is an alias for an
imxrt-hal GPIO output, which wraps a specific processor pin.
Notice that there is no unsafe in this application code. The board module,
and its submodules, make sure that board Resources are only taken once. Our
dependencies are not also constructing imxrt-ral peripheral instances, which
means that all unsafe peripheral instance access happens within board.
//! main.rs #![no_main] #![no_std] use imxrt_hal as hal; use imxrt_ral as ral; use imxrt_rt::entry; use panic_halt as _; mod board { use core::sync::atomic::{AtomicBool, Ordering}; /// Called by a board implementation to mark peripherals taken. fn take() -> Option<()> { static BOARD_FREE: AtomicBool = AtomicBool::new(true); BOARD_FREE.swap(false, Ordering::SeqCst).then_some(()) } cfg_if::cfg_if! { if #[cfg(feature = "teensy4")] { mod teensy4; pub use teensy4::Resources; } else if #[cfg(feature = "imxrt1010evk")] { mod imxrt1010evk; pub use imxrt1010evk::Resources; } else if #[cfg(feature = "imxrt1170evk-cm7")] { mod imxrt1170evk_cm7; pub use imxrt1170evk_cm7::Resources; } else { compile_error!("No board selected!"); } } } #[entry] fn main() -> ! { let board::Resources { led, .. } = board::Resources::take().unwrap(); led.set(); loop {} }
The i.MXRT1010EVK board implementation is shown below. The implementation
demonstrates the convention of items expected by the application. If the call to
super::take() returns None, it means that the imxrt-ral peripheral
instances already exist, and board refuses to alias those instances and their
wrapping Resources. Otherwise, this is the first time that Resources are
being taken, so it's safe to create imxrt-ral peripheral instances and their
drivers.
#![allow(unused)] fn main() { //! board/imxrt1010evk.rs use crate::{ hal::{self, iomuxc::pads}, ral, }; use imxrt1010evk_fcb as _; pub type Led = hal::gpio::Output<pads::gpio::GPIO_11>; #[non_exhaustive] pub struct Resources { pub led: Led, } impl Resources { pub fn take() -> Option<Self> { super::take()?; let iomuxc = unsafe { ral::iomuxc::IOMUXC::instance() }; let gpio1 = unsafe { ral::gpio::GPIO1::instance() }; let mut port = hal::gpio::Port::new(gpio1); let pads = hal::iomuxc::into_pads(iomuxc); let led = port.output(pads.gpio.p11); Some(Resources { led }) } } }
The board implementation also uses the boot header crate, meeting the
requirements discussed in booting. Although it's not depicted in
this example, the Led type and Resources::take() implementation vary for
each board. And although it's not required for this small BSP, a
non_exhaustive attribute on Resources requires that users match only the
board resources they expect, permitting boards to add new resources without
breaking users.
A BSP following this design can manage lower-level peripheral instances for the user, and present higher-level drivers to the user. Furthermore, it presents an interface that may let users port their applications across different boards. However, the approach has some limitations.
Limitations
As of this writing, the developer is the imxrt-ral resource management
strategy. Specifically, the developer must ensure that it's safe to acquire
imxrt-ral peripheral instances in their system. In this BSP example, the
developer knows that this application is the only software executing on the
hardware, so it's the sole owner of the imxrt-ral peripheral instances.
However, it may not be safe to deploy this BSP in systems where multiple (Rust)
applications concurrently execute and use the same hardware resources. In lieu
of an integrated resource management strategy, the unsafe instance access is
the developer's cue to handle these possibilities, or to document assumptions.