This package provides a register access layer (RAL) for i.MX RT processors. For more information, see the README.
Select your chip and enable its feature flag. See the README for more information on available chip features.
There are two ways to interact with peripherals and registers:
- Fabricate a peripheral instance with the unsafe
instance()
method. - Interact directly with the peripheral pointers, and mark all accesses as
unsafe
.
use imxrt_ral as ral;
use ral::lpuart;
let mut lpuart2 = unsafe { lpuart::LPUART2::instance() };
let version = ral::read_reg!(lpuart, lpuart2, VERID);
# let byte = 0;
ral::modify_reg!(lpuart, lpuart2, CTRL, TE: 1, RE: 1);
ral::write_reg!(lpuart, lpuart2, DATA, byte);
Fabricating an instance is always unsafe
. There are no checks that prevent
aliases to the same peripheral memory. If you're using this API, you need to
make sure that creating an instance is appropriate in your program's context.
It's helpful to design drivers to peripheral instances, since register accesses do
not need an unsafe
block. The driver assumes that it has complete ownership
of the instance, and uses the instance to manage the hardware. See the Usage section
for more ideas.
use imxrt_ral as ral;
use ral::lpuart;
let version = unsafe { ral::read_reg!(lpuart, lpuart::LPUART2, VERID) };
# let byte = 0;
unsafe { ral::modify_reg!(lpuart, lpuart::LPUART2, CTRL, TE: 1, RE: 1) };
unsafe { ral::write_reg!(lpuart, lpuart::LPUART2, DATA, byte) };
If you're familiar with using C for embedded code, this is C mode. You're responsible for making sure that register accesses are coordinated across all contexts. You also need to coordinate with anyone who's using the instance API.
imxrt-ral
re-exports the ral-registers
API. These macros make it easy to access register and register fields. For more information,
see [read_reg
], [write_reg
], and [modify_reg
]. Note that the documentation assumes an
STM32 processor, and may demonstrate a different API for accessing instances.
Note:
imxrt-ral
does not yet support thereset_reg
macro, and it does not expose reset structs.
Unlike some peripheral access crates (PACs) or register access layers, imxrt-ral
does not
provide a resource management policy for register blocks. Instead, the API uses unsafe
to signal that you may be mutably aliasing peripheral registers. This package expects
peripheral resource management to be handled by a higher-level crate, like a BSP or a custom
package that's aware of multi-core execution and resource management.
imxrt-ral APIs use const generics to differentiate different peripheral instances. This section describes some design techniques for using, or ignoring, these type hints.
A function that accepts any GPIO instance:
use imxrt_ral::gpio;
fn any_gpio<const N: u8>(gpio: gpio::Instance<N>) { /* ... */ }
let gpio1 = unsafe { gpio::GPIO1::instance() };
let gpio2 = unsafe { gpio::GPIO2::instance() };
any_gpio(gpio1);
any_gpio(gpio2);
A function that only accepts GPIO1:
use imxrt_ral::gpio;
fn only_gpio1(gpio: &gpio::Instance<1>) { /* ... */ }
fn only_gpio1_alias(gpio: &gpio::GPIO1) { /* ... */ }
let gpio1 = unsafe { gpio::GPIO1::instance() };
only_gpio1(&gpio1);
only_gpio1_alias(&gpio1);
only_gpio1
rejects GPIO2 at compile time:
# use imxrt_ral::gpio; fn only_gpio1(gpio: &gpio::Instance<1>) { /* ... */ }
let gpio2 = unsafe { gpio::GPIO2::instance() };
only_gpio1(&gpio2);
These function designs apply to structures, too:
use imxrt_ral::gpio;
struct GpioDriver<const N: u8> {
gpio: gpio::Instance<N>,
// ...
}
impl<const N: u8> GpioDriver<N> {
pub fn new(gpio: gpio::Instance<N>) -> Self {
// ...
# GpioDriver { gpio }
}
}
let gpio1_driver = GpioDriver::new(unsafe { gpio::GPIO1::instance() });
Require that other resources, which are tagged with instance identifiers, match their peripheral instance:
use imxrt_ral::gpio;
/// Type-level constant...
enum Const<const N: u8> {}
/// Represents a GPIO pin
trait GpioPin {
/// The associated GPIO instance number
type GpioInstance;
const OFFSET: u32;
}
/// GPIO2[17]
struct AD_B1_00 { /* ... */ }
impl GpioPin for AD_B1_00 {
type GpioInstance = Const<2>;
const OFFSET: u32 = 17;
}
# struct GpioDriver<const N: u8> {
# gpio: gpio::Instance<N>,
# }
impl<const N: u8> GpioDriver<N> {
# pub fn new(gpio: gpio::Instance<N>) -> Self { Self { gpio } }
pub fn set_high<P>(&mut self, pin: &mut P)
where
P: GpioPin<GpioInstance = Const<N>>, // <-- Requirement here
{
// ...
}
}
let mut gpio2 = GpioDriver::new(unsafe { gpio::GPIO2::instance() });
let mut ad_b1_00 = // Ownership of pin...
# AD_B1_00 {};
gpio2.set_high(&mut ad_b1_00);
If you provide a GPIO1 pin to the GPIO2 driver, it fails at compile time:
# use imxrt_ral::gpio;
# enum Const<const N: u8> {}
# trait GpioPin {
# /// The associated GPIO instance number
# type GpioInstance;
# const OFFSET: u32;
# }
/// GPIO1[6]
struct SD_B0_03 { /* ... */ }
impl GpioPin for SD_B0_03 {
type GpioInstance = Const<1>;
const OFFSET: u32 = 6;
}
# struct GpioDriver<const N: u8> {
# gpio: gpio::Instance<N>,
# }
# impl<const N: u8> GpioDriver<N> {
# pub fn new(gpio: gpio::Instance<N>) -> Self { Self { gpio } }
# pub fn set_high<P>(&mut self, pin: &mut P)
# where
# P: GpioPin<GpioInstance = Const<N>>, // <-- Requirement here
# {
# // ...
# }
# }
let mut gpio2 = GpioDriver::new(unsafe { gpio::GPIO2::instance() });
let mut sd_b0_03 = // Ownership of pin...
# SD_B0_03 {};
// Incorrect: GPIO1 pin with GPIO2 driver
gpio2.set_high(&mut sd_b0_03);
If you would like such a statement to compile, remove the GpioPin
constraint
on the set_high
function:
# use imxrt_ral::gpio;
# enum Const<const N: u8> {}
# trait GpioPin {
# /// The associated GPIO instance number
# type GpioInstance;
# const OFFSET: u32;
# }
# /// GPIO1[6]
# struct SD_B0_03 { /* ... */ }
# impl GpioPin for SD_B0_03 {
# type GpioInstance = Const<1>;
# const OFFSET: u32 = 6;
# }
# struct GpioDriver<const N: u8> {
# gpio: gpio::Instance<N>,
# }
impl<const N: u8> GpioDriver<N> {
# pub fn new(gpio: gpio::Instance<N>) -> Self { Self { gpio } }
pub fn set_high<P>(&mut self, pin: &mut P)
where
P: GpioPin/*<GpioInstance = Const<N>>*/, // No constraint that pin matches driver
{
// ...
}
}
let mut gpio2 = GpioDriver::new(unsafe { gpio::GPIO2::instance() });
let mut sd_b0_03 = // Ownership of pin...
# SD_B0_03 {};
// Now OK: GPIO1 pin with GPIO2 driver
gpio2.set_high(&mut sd_b0_03);
If you don't want to carry around a generic type for your driver struct,
you can still model peripheral ownership, and work with a pointer to the
register block. You'll need to use some unsafe
code, as shown below:
use imxrt_ral::gpio;
struct GpioDriver {
gpio: &'static gpio::RegisterBlock,
}
impl GpioDriver {
pub fn new<const N: u8>(gpio: gpio::Instance<N>) -> GpioDriver {
// Instance derefs to a register block
let register: *const gpio::RegisterBlock = &*gpio;
// Safety: pointer points to static peripheral memory,
// which will outlive the gpio Instance.
let register = unsafe { &*register };
GpioDriver { gpio: register }
}
}
let gpio2 = GpioDriver::new(unsafe { gpio::GPIO2::instance() });
This approach loses some of the compile-time checks, but may be simpler for others to use.
Design to a concrete type when you know that there's only one, single instance of that peripheral across all chips. This simplifies your driver API while still supporting all i.MX RT chips. The CCM peripheral is an example of a peripheral with one instance across all i.MX RT chips.
use imxrt_ral::ccm;
// A truly single instance:
fn new_ccm(_: &ccm::CCM) { /* ... */ }
// Still works, but more general (though the generality isn't
// necessary, since there's only one CCM instance)
fn new_ccm_explicit<const N: u8>(_: &ccm::Instance<N>) { /* ... */ }
let ccm = unsafe { ccm::CCM::instance() };
new_ccm(&ccm);
new_ccm_explicit(&ccm);
A CCM
is actually an Instance<imxrt_ral::SOLE_INSTANCE>
. So, you could
always design to a generic Instance
type to be explicit, but it's not necessary.
When there's a chance for a peripheral to have multiple instances across
different chips, favor Instance<N>
for maximal reuse. Since all instances
are generic, the same function should work no matter how many peripheral
instances exist on your chip. For example, this same function works for 1021
chips -- having only one USB instance -- and 1062 chips -- having two USB instances.
use imxrt_ral::usb;
fn new_usb_driver<const N: u8>(_: usb::Instance<N>) { /* ... */ }
#[cfg(feature = "imxrt1021")]
new_usb_driver(unsafe { usb::USB::instance() });
#[cfg(feature = "imxrt1062")]
{
new_usb_driver(unsafe { usb::USB1::instance() });
new_usb_driver(unsafe { usb::USB2::instance() });
}
When compared to the USB implementation, you would not want to use
the a concrete USB Instance
, since there are chips that have multiple
USB instances:
use imxrt_ral::usb;
/// A function that only takes the sole USB instance.
fn new_usb_driver(_: usb::Instance<0>) { /* ... */ }
#[cfg(feature = "imxrt1062")]
new_usb_driver(unsafe { usb::USB1::instance() }); // <-- Fails to compile! Instance<1> != Instance<0>
#[cfg(feature = "imxrt1021")]
new_usb_driver(unsafe { usb::USB::instance() }); // <-- Doesn't work here, either! USB == Instance<0> != Instance<1>
# #[cfg(feature = "imxrt1021")]
# compile_error!("Forced failure to meet test requirements");
Consider a function that needs to change behavior given only the const generic instance number:
use imxrt_ral::ccm;
/// Enable the LPUART clock gate in the CCM.
///
/// # Panics
///
/// Panics if `LPUART_N` does not represent a valid LPUART instance.
fn ccm_enable_lpuart_clock_gate<const LPUART_N: u8>(ccm: &mut ccm::CCM) {
match LPUART_N {
1 => { /* ... */ }
2 => { /* ... */ }
3 => { /* ... */ }
// 4..=8
_ => panic!("Unhandled LPUART instance number"),
}
}
# || -> Option<()> {
let mut ccm = unsafe { ccm::CCM::instance() };
ccm_enable_lpuart_clock_gate::<3>(&mut ccm); // OK: LPUART3 is valid.
ccm_enable_lpuart_clock_gate::<9>(&mut ccm); // panic! LPUART9 isn't valid
# Some(()) }();
To catch an invalid N at compile time, use Valid
, a trait implemented on
all valid Instance<N>
types.
# use imxrt_ral::ccm;
use imxrt_ral::lpuart;
fn ccm_enable_lpuart_clock_gate<const LPUART_N: u8>(ccm: &mut ccm::CCM)
where
lpuart::Instance<LPUART_N>: imxrt_ral::Valid, // NEW: constrain LPUART_N to valid instance numbers.
{
match LPUART_N {
1 => { /* ... */ }
2 => { /* ... */ }
3 => { /* ... */ }
// 4..=8
_ => unreachable!("Handled all LPUART instances"),
}
}
# || -> Option<()> {
let mut ccm = unsafe { ccm::CCM::instance() };
ccm_enable_lpuart_clock_gate::<3>(&mut ccm); // OK: LPUART3 is valid.
# Some(()) }();
# use imxrt_ral::ccm;
# use imxrt_ral::lpuart;
# fn ccm_enable_lpuart_clock_gate<const LPUART_N: u8>(ccm: &mut ccm::CCM)
# where
# lpuart::Instance<LPUART_N>: imxrt_ral::Valid,
# {}
# || -> Option<()> {
# let mut ccm = unsafe { ccm::CCM::instance() };
ccm_enable_lpuart_clock_gate::<9>(&mut ccm); // Does not compile!
# Some(()) }();
You can unsafe
ly instantiate any Instance
from a pointer using new
. This
is helpful if your strongly-numbered adapter is only using a pointer / static
reference to a register block, yet you need to reconstruct the Instance
for a user.
use imxrt_ral::lpuart;
pub struct Lpuart<const N: u8> {
/// Not holding lpuart::Instance<N>.
/// Instead, we're just keeping a reference
/// after taking ownership of the instance.
ptr: &'static lpuart::RegisterBlock,
}
impl<const N: u8> Lpuart<N> {
pub fn new(inst: lpuart::Instance<N>) -> Self {
let ptr: *const lpuart::RegisterBlock = &*inst;
// Safety: pointer truly points to static memory.
Self { ptr: unsafe { &*ptr }}
}
pub fn release(self) -> lpuart::Instance<N> {
// Safety: The N associated with this type
// is still associated with its register block.
// We're not accidentally returning Instance<1>
// when we have a reference to Instance<2>.
//
// The pointer points to valid LPUART memory.
unsafe { lpuart::Instance::new(self.ptr) }
}
}
If you're fully discarding all type information, you can use
the number
function in each peripheral module to acquire the
instance number for a register block. Note that this incurs a
small runtime cost of up to N
pointer compares, where N
is
the number of valid instances.
use imxrt_ral::lpuart;
/// Note that there's no `N` const generic,
/// so that information isn't in the type system.
pub struct AnyLpuart {
ptr: &'static lpuart::RegisterBlock,
}
impl AnyLpuart {
pub fn new<const N: u8>(inst: lpuart::Instance<N>) -> Self {
let ptr: *const lpuart::RegisterBlock = &*inst;
// Safety: pointer truly points to static memory.
Self { ptr: unsafe { &*ptr }}
}
pub fn instance(&self) -> u8 {
// Unwrap OK; `new` guarantees that it's one of
// the N LPUART instances.
lpuart::number(self.ptr).unwrap()
}
}
use imxrt_ral::{ccm, lpuart};
assert_eq!(ccm::number(ccm::CCM), Some(0));
assert_eq!(lpuart::number(lpuart::LPUART2), Some(2));
assert_eq!(lpuart::number(ccm::CCM as _), None);