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
- Join the Matrix chat: #imxrt-rs:matrix.org.
- Reach out on GitHub issues and discussions.
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:
- 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.
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
- 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-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 take
n. 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
- Power off your board.
- Locate the boot mode switches on your board. Consult your board's documentation for the switch locations.
- Use the switches to change the boot mode to "serial downloader." Consult your board's documentation to understand possible boot mode configurations.
- Power on your board. Observe that the program stored in the board's flash is not executing.
- Use your normal process to flash a known, good program. This step should now succeed.
- Power off your board.
- Revert the boot mode switches back to their previous configuration.
- 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.
- AN12077: Using the i.MX RT
FlexRAM talks
about how to configure tightly-coupled memory (TCM). It's useful information
for understanding
imxrt-rt
. - AN12238: i.MX RT Flashloader Use
Case is
helpful for understanding the i.MX RT boot image. It's also useful for
understanding
imxrt-rt
. - AN13264: i.MX RT1170 Dual Core Application summarizes the boot process for the second core on an 11xx processor.
- AN13148: i.MX RT1170 Low-Power Modes and AN13104: Debug and Application for i.MX RT1170 Clock and Low Power Feature describe the complexities of 11xx clock and power management.
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.