Expand description
This package provides a register access layer (RAL) for i.MX RT processors. For more information, see the README.
Getting started
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
.
Fabricate a peripheral instance
use imxrt_ral as ral;
use ral::lpuart;
let mut lpuart2 = unsafe { lpuart::LPUART2::instance() };
let version = ral::read_reg!(lpuart, lpuart2, VERID);
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.
Interact directly with pointers
use imxrt_ral as ral;
use ral::lpuart;
let version = unsafe { ral::read_reg!(lpuart, lpuart::LPUART2, VERID) };
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.
Register access macros
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.
Resource management
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.
Usage
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:
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 {
// ...
}
}
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;
}
impl<const N: u8> GpioDriver<N> {
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...
gpio2.set_high(&mut ad_b1_00);
If you provide a GPIO1 pin to the GPIO2 driver, it fails at compile time:
/// GPIO1[6]
struct SD_B0_03 { /* ... */ }
impl GpioPin for SD_B0_03 {
type GpioInstance = Const<1>;
const OFFSET: u32 = 6;
}
let mut gpio2 = GpioDriver::new(unsafe { gpio::GPIO2::instance() });
let mut sd_b0_03 = // Ownership of pin...
// 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:
impl<const N: u8> GpioDriver<N> {
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...
// 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.
[Peripheral name]
vs Instance<N>
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>
Valid instance numbers
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"),
}
}
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
To catch an invalid N at compile time, use Valid
, a trait implemented on
all valid Instance<N>
types.
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"),
}
}
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); // Does not compile!
Advanced usage
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);
Modules
Macros
- Modify a RWRegister or UnsafeRWRegister.
- Read the value from a RORegister, RWRegister, UnsafeRORegister, or UnsafeRWRegister.
- Write to a RWRegister or UnsafeRWRegister.
Structs
- An owned peripheral of type
T
, instanceN
. - Instances for all of this device’s peripherals.
- A read-only register of type T.
- A read-write register of type T.
- A write-only register of type T.
Enums
Constants
- Number available in the NVIC for configuring priority
- The instance number for a peripheral singleton.
Traits
- Vouches for an
Instance<T, N>
’s validity.