consortium-hal-imx9
Consortium HAL for the NXP i.MX 9 family: peripheral drivers over
consortium-pac-imx9, with owned peripheral singletons.
Layering
consortium-hal Peri, PeripheralType, peripherals!, narrow traits
└── consortium-hal-imx9 (this crate) drivers + generated singletons
└── consortium-pac-imx9 register blocks, Interrupt, vector table
Both neighbours are re-exported as hal and pac, so a firmware crate needs one
dependency. interrupts, Interrupt, and — behind rt — the #[interrupt] attribute
macro come straight from the PAC.
Peripherals are owned, not addressed
init() hands out the chip's singletons once, and each driver constructor consumes the
one it drives:
let p = init;
let i2c = new;
let heartbeat = new.pin;
Because the base address and NVIC line travel with the singleton, no driver takes an
MMIO base and no constructor is unsafe. init() panics if called twice — handing
the singletons out again would let two drivers claim the same registers.
peripherals::<NAME>::steal() is the escape hatch for bring-up outside init(); it
carries the exclusivity obligation the safe path discharges.
Drivers
Each behind a feature, all on by default:
| Feature | Module | Covers |
|---|---|---|
adc |
adc |
ADC |
can |
can |
FlexCAN |
dma |
dma |
eDMA |
gpio |
gpio |
GPIO / pin handles |
i2c |
i2c |
LPI2C |
i3c |
i3c |
I3C |
spi |
spi |
LPSPI |
timer |
timer, lptmr |
General timers, LPTMR (also pwm alias) |
time-driver-lpit |
timer_driver |
Embassy time driver over LPIT1 |
uart |
uart |
LPUART, including IRQ-buffered BufferedUart |
watchdog |
watchdog |
WDOG1–5 |
timer_driver is what consortium-cfg brings up in the generated init() unless
[profile] time_driver = false. uart's BufferedUart is what
consortium-ipc-transport-uart-embedded binds to.
Chip selection
mimx93 or mimx95 — exactly one. The feature selects which generated singleton
map is mounted and is forwarded to the PAC so its register and interrupt definitions
match the target. Enabling both is a duplicate-definition error.
| Chip | MU instances |
|---|---|
mimx93 |
2 |
mimx95 |
8 |
Default is mimx95. A dependent selecting mimx93 must set
default-features = false — otherwise mimx95 comes along too and the PAC's
Interrupt enum becomes ambiguous. Every doorbell and transport crate in this
workspace that depends on a chip HAL does this.
Interrupts are the application's
This crate installs no #[interrupt] handlers. The application owns the vector table
and forwards each vector into the driver's entry point, parameterized with the same
singleton the driver consumed — so there is no global static to name:
Enable the rt feature so the #[interrupt] macro and the PAC vector table are
available to that code. The MU doorbell (consortium-ipc-transport-memory::bell::mu) follows the same
convention — MU1_B/MU2_B on the i.MX93 CM33, MU5_A/MU7_B/MU8_B on the i.MX95
CM7 — but those vectors, like the LPIT1 time-driver vector, are generated by
consortium-cfg into consortium.gen.rs; only peripherals the application drives
itself need a hand-written forwarder.
Generated code
src/peripherals/<chip>.rs is generated by scripts/pac/gen_peripherals.py from the
same consortium-data chip dumps that feed gen_interrupts.py, and mounted by
lib.rs as the private chip module.
It declares the singletons (consortium_hal::peripherals!) and invokes each driver's
impl_<driver>! macro with that instance's base and NVIC line. The generated file is
data only — the driver module owns the macro and therefore the logic. Re-run the
generator after changing a chip dump; do not hand-edit.
One wrinkle: i.MX9 eDMA channels have no chip-dump entry, so the generator synthesizes
<CONTROLLER>_CH<n> singletons from the layout table in dma.rs. (STM32MP2 dumps list
channels directly, so its HAL needs no equivalent.)
Testing
Keep this crate firmware-friendly: no_std, narrow unsafe blocks each explaining the
hardware or aliasing invariant, and chip-specific detail here rather than pushed up into
consortium-hal's shared traits.
License
Apache-2.0. See LICENSE.
Watchdog
The watchdog feature exposes an owned Watchdog with new, start, feed,
and is_running. Construction does not modify hardware. Start explicitly after
setting up the peripheral's access permissions, clocks and reset routing:
use ;
let p = init;
let mut watchdog = new;
watchdog.start.unwrap;
// Feed periodically after checking that the application is healthy.
watchdog.feed;
Timeout values are clock ticks, not milliseconds; select them using the actual watchdog clock. Window mode and early-warning interrupts are disabled. Dropping an active watchdog does not stop it. A synchronization error leaves the handle available, since hardware may already be running and still require feeding. Synchronization waits use a bounded status-read budget, not an Embassy timer.
The driver supports both 16-bit and 32-bit command modes during takeover and
masks interrupts around unlock/program/refresh sequences. Supply a critical-section
implementation that masks local interrupts. A watchdog locked by a previous owner
rejects reconfiguration with Error::Locked but can still be fed.
Register sequencing follows NXP's RTWDOG driver.