imxrt-rs

User and developer documentation for the imxrt-rs project.

Getting started

If you're not familiar with our work, take a look at the ecosystem walkthrough. The walkthrough demonstrates how you can combine imxrt-rs packages to support your embedded Rust development.

If you're using an imxrt-rs package, consult the toolchain setup to prepare your development environment. Our recommended flashing and debugging tools can help you deploy your application on your hardware.

For links to helpful references, check out our external documentation sources.

If you're already familiar with embedded Rust and want to dive in, the best place to start is in the imxrt-hal repository. You'll find small hardware examples that run on various development boards.

About

We provide community support for using Rust on NXP's i.MX RT processors. We develop hardware drivers with embedded-hal compatibility. We also provide packages for embedded application development and debugging. To support these goals, we collaborate with the broader embedded Rust community.

By supporting this platform, we want to improve the general quality and accessibility of embedded Rust.

Contact

Ecosystem walkthrough

The imxrt-rs project develops various packages to support embedded Rust development on i.MX RT microcontrollers. This walkthrough demonstrates core no_std imxrt-rs packages by example. The guide shows

  • what crates are necessary to boot an i.MX RT processor.
  • how the register access layer simplifies register access and resource ownership.
  • how to use drivers in the hardware abstraction layer.

The walkthrough steps through a series of examples that turns on a evaluation kit's LED. Each example introduces one or more imxrt-rs package and briefly explains the package's role in the ecosystem. For more information on a package, consult its documentation.

Prerequisites

Before reading this walkthrough, it helps to know general concepts of embedded Rust development. Familiarize yourself with The Embedded Rust Book before reading this walkthrough. The walkthrough specifically assumes knowledge of the portability concepts described in the book.

Setup

All examples target NXP's i.MXRT1010EVK development board. Nevertheless, the concepts apply for all i.MX RT microcontrollers supported by the imxrt-rs project.

The following Cargo configuration applies to all examples.

# Link with the imxrt-rt linker script.
[target.thumbv7em-none-eabihf]
rustflags = [
    "-C", "link-arg=-Timxrt-link.x",
]

Booting

In order to boot Rust firmware on an i.MX RT processor, you need at least three crates:

  1. a crate that defines a compatible boot header for your system.
  2. the imxrt-rt runtime.
  3. 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.

Register access layer

The imxrt-ral crate provides a register access layer (RAL) for i.MX RT 10xx and 11xx microcontrollers. Use the RAL to

  • manage peripherals as low-level resources.
  • conveniently access registers and register fields.

imxrt-ral also provides a device-specific interrupt table. Once you link the runtime with the RAL,

  • enable imxrt-rt's "device" feature.
  • enable imxrt-ral's "rt" feature.

ℹ️ The RAL is very similar to the peripheral access crate (PAC) found in other embedded Rust ecosystems. The major difference is the API used to access registers.

This example improves on the previous walkthrough by using imxrt-ral to access registers. Note the imxrt-ral feature flag for the target MCU. Also note that the "device" feature for the runtime is enabled, and the "rt" feature for imxrt-ral is enabled; even though the example doesn't use interrupts, you should prefer the device-specific vector table when available.

# Cargo.toml

[dependencies]
imxrt-ral = { version = "0.5", features = ["imxrt1011", "rt"] }
imxrt-rt = { version = "0.1", features = ["device"] }

# As before...
imxrt1010evk-fcb = "0.1"
panic-halt = "0.2"

[build-dependencies]
imxrt-rt = { version = "0.1", features = ["device"] }
//! build.rs (unchanged)

use imxrt_rt::{Family, RuntimeBuilder};

fn main() {
    RuntimeBuilder::from_flexspi(Family::Imxrt1010, 16 * 1024 * 1024)
        .build()
        .unwrap();
}
//! main.rs

#![no_main]
#![no_std]

use imxrt_ral as ral;

use imxrt1010evk_fcb as _;
use imxrt_rt::entry;
use panic_halt as _;

const LED_OFFSET: u32 = 1 << 11;

#[entry]
fn main() -> ! {
    // Safety: we're the only code that "owns" the IOMUXC and GPIO1 peripherals.
    let iomuxc = unsafe { ral::iomuxc::IOMUXC::instance() };
    let gpio1 = unsafe { ral::gpio::GPIO1::instance() };

    // Configure the pad named "GPIO_11" as a GPIO pin
    // (as opposed to a UART TX pin, for example).
    ral::write_reg!(ral::iomuxc, iomuxc, SW_MUX_CTL_PAD_GPIO_11, 5);
    // Set the GPIO as an output with a RMW operation.
    ral::modify_reg!(ral::gpio, gpio1, GDIR, |gdir| gdir | LED_OFFSET);
    // Turn on the LED.
    ral::write_reg!(ral::gpio, gpio1, DR_SET, LED_OFFSET);

    loop {}
}

Acquiring peripheral instances is still unsafe. However, macros make it easier to read, write, and modify registers; and there's no need to maintain register addresses.

Consider using imxrt-ral when you want to implement higher-level peripheral drivers. These kinds of convenient, re-usable, and portable peripheral drivers are the topic of the next section: the hardware abstraction layer.

Hardware abstraction layer

The imxrt-hal crate provides the hardware abstraction layer (HAL) for i.MX RT microcontrollers. The HAL includes re-usable hardware drivers for 10xx and 11xx MCUs. Most of the HAL's drivers implement their corresponding traits from embedded-hal, allowing you to pair the hardware driver with third-party sensor, actuator, and device drivers.

imxrt-hal drivers adapt the low-level peripheral instances provided by imxrt-ral, so you can use the HAL by adding it as another package in your project. Unlike the previous example, this new example use imxrt-hal APIs to access pads and prepare a GPIO output.

# Cargo.toml

[dependencies]
imxrt-hal = { version = "0.5", features = ["imxrt1010"] }

# As before...
imxrt-ral = { version = "0.5", features = ["imxrt1011", "rt"] }
imxrt-rt = { version = "0.1", features = ["device"] }
imxrt1010evk-fcb = "0.1"
panic-halt = "0.2"

[build-dependencies]
imxrt-rt = { version = "0.1", features = ["device"] }
//! build.rs (unchanged)

use imxrt_rt::{Family, RuntimeBuilder};

fn main() {
    RuntimeBuilder::from_flexspi(Family::Imxrt1010, 16 * 1024 * 1024)
        .build()
        .unwrap();
}
//! main.rs

#![no_main]
#![no_std]

use imxrt_hal as hal;
use imxrt_ral as ral;

use imxrt1010evk_fcb as _;
use imxrt_rt::entry;
use panic_halt as _;

#[entry]
fn main() -> ! {
    // Safety: we're the only code that "owns" the IOMUXC and GPIO1 peripherals.
    let iomuxc = unsafe { ral::iomuxc::IOMUXC::instance() };
    let gpio1 = unsafe { ral::gpio::GPIO1::instance() };

    let mut gpio1 = hal::gpio::Port::new(gpio1);
    let pads = hal::iomuxc::into_pads(iomuxc);

    // Configures the pad named "GPIO_11" as a GPIO output.
    let led = gpio1.output(pads.gpio.p11);
    // Turn on the LED.
    led.set();

    loop {}
}

Try the HAL

The imxrt-hal repository includes a collection of hardware examples that work on various boards listed in the repository.

You can use these examples to try the HAL on your hardware. See the repository documentation for more information.

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.

Extra packages

The imxrt-rs project maintains additional packages to support embedded Rust development on i.MX RT processors. This page lists some of those extra packages. For the complete list of packages, check out our GitHub organization.

imxrt-iomuxc

imxrt-iomuxc provides an alternate IOMUXC API than what's provided by imxrt-ral. This package

  • provides a pad configuration API.
  • defines processor pads.
  • specifies the peripheral functions supported by each pad.

imxrt-hal re-exports and extends this API; if you're using imxrt-hal, you already depend on this package. However, there are some reasons to use imxrt-iomuxc as your direct dependency.

imxrt-usbd

imxrt-usbd implements a high-speed USB bus. The driver is compatible with the usb-device ecosystem. imxrt-hal re-exports and extends the API, but you may use the package directly.

imxrt-log

imxrt-log builds on imxrt-hal to provide convenient developer logging. imxrt-log works with the log and defmt logging frameworks. Transports include USB serial and LPUART.

imxrt-dma

imxrt-dma provides a DMA driver for supported i.MX RT processors. Use it to schedule DMA transfers, and to await DMA transfers in async code. imxrt-hal configures a DMA driver and re-exports most of the API.

Toolchain setup

In order to build Rust applications for i.MX RT MCUs, you'll need a Rust toolchain and target that supports the MCU. This brief setup guide shows how to install a compatible Rust target for the i.MX RT.

rustup is an official Rust project that simplifies toolchain setup and maintenance. This setup guide assumes that you're using rustup to manage your existing Rust toolchain.

Releases of most imxrt-rs packages target the latest, stable Rust release. Make sure that you have the latest, stable Rust release by updating your toolchain:

rustup update stable

Then, use rustup to install the thumbv7em-none-eabihf target:

rustup target add thumbv7em-none-eabihf

Since the CPU supports double-precision floating point operations, most imxrt-rs documentation assumes the hard-float target. Consider installing and using the hard-float target if you want to precisely follow imxrt-rs documentation. You're otherwise free to substitute your preferred, equivalent target.

Once the target is installed, supply it to compatible Cargo commands with --target.

cargo build --target=thumbv7em-none-eabihf

Alternatively, use a Cargo configuration to set thumbv7em-none-eabihf as the default target. The snippet below shows an example of the configuration.

[build]
target = "thumbv7em-none-eabihf"

Flashing and debugging

Once you've developed an embedded Rust application for your i.MX RT target, you'll need additional tools to flash and debug your target. This page links to tools that can flash and debug i.MX RT targets. It also describes tips for integrating flashing and debugging tools in your Rust project.

In order to flash and debug an i.MX RT target, you'll need a physical debug probe that

  • is compatible with your chosen flashing and debugging software.
  • works with your hardware.

Consult each software tool's documentation to understand its support for your debug probe.

Recommendations for i.MX RT EVKs

When working with NXP's i.MX RT EVKs, we recommend either pyOCD or a probe-rs-based tool. Both of these tools can flash and debug the i.MX RT processor through the OpenSDA probe available on the board. This approach doesn't require an external JTAG / SWD debug probe.

Connect to the OpenSDA probe using a USB cable, and ensure that the board's jumpers are configured to use the OpenSDA probe. Consult your board's documentation for the specific USB port and jumper configurations.

If you've flashed a bad program to your EVK and you're having trouble reprogramming the board, see recovering an EVK.

pyOCD

pyOCD is a Python toolkit for debugging and programming Arm microcontrollers. It includes support for debugging i.MX RT 10xx and 11xx processors. It can also program external NOR flash chips, making it suitable for persistent device programming.

See our quick-start guide if you're interested in using pyOCD as a flashing and debugging tool.

You can also use pyOCD as a Cargo runner. For more information, see the Tips section.

probe-rs

probe-rs develops embedded debugging tools in Rust. All tools that build on probe-rs should support i.MX RT 10xx and 11xx processors. probe-rs can write your binary to external NOR flash chips, just like pyOCD. Consult probe-rs documentation for tool installation and usage.

ℹ️ Patches for i.MX RT targets may not yet be accepted or released. If that's the case, consider building tools from source.

Tips

These tips may be helpful for integrating flashing and debugging tools in your workflow. They may work for flashing and debugging tools that are not covered by this guide.

Use a Cargo runner

A runner describes the behavior of cargo run and other cargo commands. You can use a Cargo runner to invoke your flashing and debugging tool. See the Configuration chapter of The Cargo Book for more information.

Here's an example of a runner that uses pyocd to program a i.MX RT 1010 microcontroller. This assumes that you have pyocd installed. Once this configuration is specified in your project, cargo run --target=thumbv7em-none-eabihf will invoke pyocd to flash your target.

[target.thumbv7em-none-eabihf]
runner = [
    "pyocd", "load",
    "--target=mimxrt1010",
    "--format=elf",
]

Recovering an EVK

Sometimes, your debug probe fails to connect to the i.MX RT processor on your NXP EVK. Simple troubleshooting approaches, like resetting power, cannot resolve the issue. As a result, you're unable to flash a known, good program to recover the board.

If you're experiencing this problem, try these steps to recover your board. This guide assumes

  • you're using a NXP EVK, or equivalent hardware, that has switches to control the processor boot order.
  • your processor typically executes out of on-board flash.
  • you have a known, good program to load onto your board's flash.

Recovery steps

  1. Power off your board.
  2. Locate the boot mode switches on your board. Consult your board's documentation for the switch locations.
  3. Use the switches to change the boot mode to "serial downloader." Consult your board's documentation to understand possible boot mode configurations.
  4. Power on your board. Observe that the program stored in the board's flash is not executing.
  5. Use your normal process to flash a known, good program. This step should now succeed.
  6. Power off your board.
  7. Revert the boot mode switches back to their previous configuration.
  8. Power on your board. Observe that the known, good program executes from flash.

After flashing the known, good program, you should be able to reprogram the board without changing the boot mode.

Discussion

By changing the boot mode to "serial downloader," we use the serial bootloader, stored in the processor's boot ROM, as an ephemeral, known, good program. Your probe should have better luck connecting to the processor when the processor is simply waiting for commands over LPUART / USB.

If these steps are not sufficient to recover your board, your hardware may be in a more troublesome state. Refer to your board's documentation for additional troubleshooting steps.

pyOCD quick-start guide

This guide was developed against pyOCD 0.34. For the most up-to-date information, study the pyOCD documentation.

Check out the pyOCD documentation for installation methods. Once installed, list all supported targets, and filter for the i.MX RT targets.

pyocd list --targets | grep -i imxrt

The left-most column has a list of i.MX RT targets. Select the one that most closely matches your processor. We'll call this $TARGET in the rest of this section.

Note that pyOCD identifies processors by their chip families, not part numbers. You should select the target by the chip family. For example, use the 1060 pyOCD target if your chip is numbered 1062.

Connect your hardware to your development host. To reset the processor, use the reset command. To make it more obvious that a reset succeeded, consider using --halt to stop the processor after the reset.

pyocd reset --target=$TARGET --halt

To load / flash a (ELF) program that's built for your target, use the load command.

pyocd load --target=$TARGET --format=elf [path/to/your/program]

Cargo generates ELF files by default, so you should prefer the ELF format in most cases. However, you can change the --format argument if your executable is in a different format.

To debug your program, use pyOCD as a GDB server.

pyocd gdbserver --target=$TARGET

Then, connect to the GDB server with your tool of choice. See GDB setup for more information. Note that you'll need a minimally-optimized Rust program in order to have an effective debug session.

External documentation

This page links to external documentation that might help you understand i.MX RT processors. It also links to embedded Rust documentation, which you should study if you're new to embedded Rust development.

Datasheets

i.MX RT data sheets are available as free downloads here. The data sheets are useful for understanding high-level capabilities of i.MX RT processors. Select your processor, then go to "Documentation," then "Data Sheet."

Reference manuals

i.MX RT reference manuals are available from NXP. The reference manuals describe the i.MX RT registers and peripheral capabilities, and they're the source of truth for most driver development.

To download a reference manual, go here and select your processor. Then, go to "Documentation," and scroll down to "Reference Manual." You'll need a free NXP account to access the reference manuals.

Application notes

There's many application notes (AN) for i.MX RT processors. They're available through the same documentation portal that serves datasheets and reference manuals.

Some ANs of interest are listed below.

Embedded Rust

If you're brand new to embedded Rust, read through The Embedded Rust Book. This will help you understand some of the concepts that appear throughout imxrt-rs packages. Once you've read through the book, also check out the resources listed on the front page.

Software references

If you're looking for external code references, check out

  • the Zephyr Project.
  • the ARM CMSIS Packs. Here's the MIMXRT1062 pack; NXP and ARM also provide CMSIS packs for the other i.MX RT variants.
  • NXP's MCUXpresso SDK, available here.

Contributing new MCUs

The imxrt-rs project has various levels of support for about eight i.MX RT MCUs in the 1000 series. We also have basic support for one core of one 1100 MCU. However, we do not support all MCUs within the 1000 and 1100 series, and we have no support for the 500 and 600 series. We welcome your support to add new MCUs.

This document summarizes the steps for contributing new MCU support in the imxrt-rs project. It also notes the challenges you may face when contributing a new MCU.

This document is comprehensive, and it assumes that the project has no support for the MCU you're considering. If the project has partial support for your MCU, you can skip some of these steps. Read the effort estimates section to understand which sections you can skip.

If you're having issues contributing a chip, reach out to an imxrt-rs maintainer.

Prerequisites

Your MCU contribution requires that you study and modify packages throughout the imxrt-rs ecosystem. Therefore, you should be familiar with the imxrt-rs ecosystem, especially the packages used for

  • booting the MCU.
  • specifying peripheral registers.
  • defining drivers.

The rest of this document elides discussions of imxrt-rs packages, since they're covered in the ecosystem walkthrough.

Familiarize yourself with the recommended external documentation. In particular,

  • acquire the reference manual for your MCU.
  • locate the SVD for your MCU. These can be found in CMSIS Pack files. Find links to CMSIS Pack files under software references.

You'll need the reference manual to understand your MCU's start-up and peripheral capabilities. You'll use the SVD to generate the register access API in imxrt-ral.

You'll need some kind of flashing / debugging tool that works with your MCU and target system. This guide assumes that some tool already supports your system.

Effort estimates

Use this section to understand the general effort required to support your MCU. These estimates can help you understand what kinds of contributions you'll make, and what efforts you can skip.

1000 MCUs

The 1010 and 1060 MCUs are our best-supported MCUs. These MCUs have dedicated support in imxrt-hal. Boards carrying these MCUs support our hardware testing. If you're bringing up a new board with these MCUs, you should only need a boot configuration crate; see booting for more information.

Other 1000 MCUs that have imxrt-ral support, like the 1020 and 1050, are not tested on hardware. Although imxrt-hal tests its baseline build against these MCUs, these MCUs do not have dedicated imxrt-hal support. This means clock control and specialized drivers are not available or tested. Additionally, there may not be complete boot support for these MCUs. Nevertheless, adding imxrt-hal support for these MCUs should be the easiest way to contribute an MCU.

1000 MCUs that do not have imxrt-ral support, like the 1040, will require changes throughout the imxrt-rs ecosystem. We still expect these efforts to be easier than adding 1100, 500, and 600 MCU support.

1100 MCUs

The 1170 series, specifically the 1176, has basic imxrt-hal support, enough for simple hardware testing. See the imxrt-hal tracking issue for more information.

Other 1100 MCUs require boot and imxrt-ral support. We expect this to be straightforward, since we've shown support for at least one 1170 MCU. We generally expect that the baseline imxrt-hal drivers will work on these MCUs.

We have not shown any dual-core support for any 1100 MCU. We welcome your help to demonstrate this feature.

500 and 600 MCUs

Peripherals in these MCUs are different than what we support in imxrt-hal. This means that imxrt-hal drivers may not be compatible, and you may need to build new drivers. Despite peripheral differences, we might be able to boot the Cortex-M core with our existing packages. These MCUs will require support in imxrt-ral; this may have unanticipated challenges, since it hasn't been attempted.

Support for the DSP co-processors requires Xtensa support in the Rust compiler. This exceeds the scope of our project. Follow esp-rs' fork of the Rust compiler and Espressif's fork of LLVM to understand support for this architecture.

Booting

i.MX RT MCUs have various ways to boot. If your MCU and target system support serial NOR flash over FlexSPI, it should be simple to support your MCU in imxrt-rt and imxrt-boot-gen. To understand your MCU's booting methods, consult the "System Boot" (or equivalent) section of the reference manual.

imxrt-rt may already support your chip; see its API documentation for more information. imxrt-boot-gen has an issue that describes how to evaluate the FlexSPI configuration block for compatibility, and how to define a FlexSPI configuration block for your MCU and board. Direct imxrt-boot-gen questions to that issue.

Once imxrt-boot-gen supports your MCU, you'll need a FCB crate that's compatible with your target system's external flash. If your target system is a publicly-available development board, like a NXP EVK, we would be happy to help you maintain a FCB crate within the imxrt-boot-gen repository.

If your MCU or target system do not support booting from serial NOR flash over FlexSPI, then there's more work to add boot support. Specifically, imxrt-rt may need to place the program image differently, and imxrt-boot-gen may need a new API for defining configuration blocks. Once you understand the boot support required for your system, open issues in their respective repositories.

Register access layer

imxrt-ral is our preferred peripheral access crate for i.MX RT MCUs. It's the foundation on which we build imxrt-hal. By adding support for your MCU into imxrt-ral, you should be able to realize parts of imxrt-hal for free.

We generate imxrt-ral from SVD files. We patch those SVD files before code generation, then consolidate the peripheral blocks to ensure cross-MCU compatibility. After you acquire your MCU's SVD, see the imxrt-ral contribution documentation for more information on generating imxrt-ral.

Issues you may face include an SVD that is superficially different from supported i.MX RT SVDs. We typically resolve these issues by SVD patches and codegen transforms. It's important to address these differences to ensure a clean integration into imxrt-hal.

Larger issues include an SVD -- actually, an MCU -- that is fundamentally different from already-supported i.MX RT MCUs. This means that peripheral layouts or registers are completely different. To catch these issues before writing any code, use your MCU's reference manual to understand peripheral capabilities. Then, compare your MCU's peripherals against those exposed by imxrt-ral. Focus on the peripherals that have drivers in imxrt-hal.

Fundamental differences should not prevent us from supporting your MCU in imxrt-ral. But, depending on the differences, it may complicate an integration into imxrt-hal. If you're not interested in new driver development, then it may not be worthwhile to add imxrt-ral support for your MCU.

The i.MX RT 600 series fall into the latter category. Since the baseline peripherals vary from peripherals in the 1000 and 1100 series, we cannot support these MCUs in today's imxrt-hal without new driver development. On the other hand, if we were to add i.MX RT 1040 or 1160 support to imxrt-ral, we would expect imxrt-hal support to come for free, since most baseline peripherals are identical to what we already support.

Hardware abstraction layer

If you can boot your MCU, and if imxrt-ral supports your MCU, try to add support for your MCU in imxrt-hal. In the best case, all drivers build and work for your system. In the worst case, you need to develop new drivers.

The baseline imxrt-hal exposes drivers that it believes are common across all supported MCUs. This includes drivers for GPIOs, LPUART, and LPSPI, among others. Study the imxrt-hal contributing docs to make sure this build still works when your MCU is available in imxrt-ral.

If the baseline imxrt-hal fails to build once your MCU is available in imxrt-ral, this may mean that the common drivers are not really common. Open an issue in imxrt-hal if you notice this failure, and we can help you evaluate the next steps.

Once the common drivers build for your MCU, you'll receive most of the embedded-hal implementations. However, you may want to

  • add specialized drivers that are specific to your chip.
  • re-export existing drivers that are compatible with your chip.

You can achieve this with chip features within imxrt-hal. Study the module layout and feature usage to understand how you can extend imxrt-hal support for your MCU's drivers. The contributing documentation has more information.

To take advantage of peripheral-pin type states in imxrt-hal, consider contributing MCU support into imxrt-iomuxc. See the imxrt-iomuxc contributor documentation for more information. However, this can be a non-trivial effort, since we do not have complete automation to help with the integration. Therefore, as of 0.5 imxrt-hal, the imxrt-iomuxc integration is optional; you can use all drivers without dedicated pad objects and pin traits.

imxrt-hal includes in-tree examples based on a thin board support package. Once the ecosystem supports your MCU, you should be able to add support for your board and use existing examples for hardware testing. The imxrt-hal contributor docs discuss how you can add board support.

Extra packages

We expect that imxrt-dma and imxrt-usbd will work as-is with MCUs in the 1000 and 1100 series. These same packages may not be portable to 500 and 600 MCUs.