consortium-hal-imx9 0.5.0

Consortium HAL for i.MX 9 family devices
Documentation

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 = consortium_hal_imx9::init();

let i2c = consortium_hal_imx9::i2c::I2c::new(p.LPI2C1, 24_000_000, 400_000);
let heartbeat = consortium_hal_imx9::gpio::Gpio::new(p.GPIO2).pin(0);

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:

#[interrupt]
fn LPUART2() {
    consortium_hal_imx9::uart::on_buffered_interrupt::<peripherals::LPUART2>();
}

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

cargo clippy -p consortium-hal-imx9 --all-targets -- -D warnings
just check-ipc-m8    # thumbv8m.main-none-eabihf, the CM33 target

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 consortium_hal_imx9::watchdog::{Config, Watchdog};

let p = consortium_hal_imx9::init();
let mut watchdog = Watchdog::new(p.WDOG3);
watchdog.start(Config { timeout_ticks: 32_000, ..Config::default() }).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.