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

  1. directly within the BSP.
  2. 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-ral chip selection
  • an imxrt-hal family 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.