imxrt-rs / imxrt-hal

Rust for NXP i.MX RT
Apache License 2.0
137 stars 33 forks source link

RFC: separate pad configurations from the HAL #73

Closed mciantyre closed 4 years ago

mciantyre commented 4 years ago

RFC: separate pad configurations from the HAL

The PR introduces the imxrt-iomuxc crate family. The goal is to de-couple pad definitions from the HAL, supporting a split HAL (#56). Additional, the crates introduce a new pin muxing interface, reaching the ideal described in #26. Finally, the pad configurations and interfaces are re-usable without requiring any HAL, which supports out-of-tree driver development, and also encourages alternative HAL implementations, such as fully-asyc HALs.

I'm looking for feedback on the approach. If you'd like to try this out today, you can use the iomuxc-integration branch of the teensy4-rs project. I've updated and tested all (non-RTIC) examples with the new interface.

Proposal

Walkthrough

We introduce imxrt-iomuxc, the top-level crate for the IOMUXC crate family. In the spirit of the IOMUXC hardware peripheral, which provides pad configuration and multiplexing, we name these crates to include iomuxc.

imxrt-iomuxc defines pin traits. An example Pin trait for a UART-compatible pin resembles

/// A UART pin
pub trait Pin: super::IOMUX {
    /// The alternate value for the UART pin
    const ALT: u32;
    /// The daisy register which will select the pad
    const DAISY: Option<super::Daisy>;
    /// Pin direction
    type Direction: Direction;
    /// UART module; `U3` for `UART3`
    type Module: super::consts::Unsigned;
}

where

The alternate value is an associated constant. In today's iomuxc module, it's a type state. The type state leaks into user code, code that doesn't care about this implementation detail. The detail also leaks into our pad definition macros, which needs to know what alternate values a pad may take (see below). The associated constant lets us mark it once, then never worry about it again.

We favor typenum type-level constants. These replace the hand-rolled type constants that the HAL was using and exposing.

We have similar traits in today's iomuxc module. The only difference from today is in dependency management. We can use these traits independent of the HAL, which is not possible today.

A driver implementer uses these traits in their interfaces:

impl UART {
    pub fn new<T, R>(mut tx: T, mut rx: R, /* ... */) -> UART
    where
        T: Pin<Direction = TX>,
        R: Pin<Direction = RX, Module = <T as Pin>::Module>,
    {
        // Peripheral implementer resposible for calling unsafe functions
        unsafe {
            imxrt_iomuxc::uart::prepare(&mut tx);
            imxrt_iomuxc::uart::prepare(&mut rx);
        }
        // ...
    }

    pub unsafe fn new_unchecked(tx: ErasedPad, rx: ErasedPad) -> UART {
        // ...
    }
}

Notice the calls to prepare(). imxrt-iomuxc provides all functions for configuring pads. These functions are designed to a common trait, such that they're usable with strongly-typed pads or type-erased pads. The imxrt-iomuxc's interface is typically unsafe, which puts the onus on driver and HAL designers to guarantee safety. The unsafe interface also signals that these functions might not be suitable for normal user code. See the crate documentation for justification on the unsafe interfaces.

Also note the ErasedPad types. The new crates provide type-erased pads, which users may prefer over strongly-typed pads (at the cost of memory overhead). The documentation provides guidance on how to design interfaces to strongly-typed and type-erased pads.

We introduce the imxrt106x-iomuxc crate. The crate

  1. implements imxrt-iomuxc pin trats
  2. defines all of the available 106x pads

An example of 1 is

use imxrt_iomuxc::uart::{Pin, TX, RX};
use imxrt_iomuxc::consts::*;

impl Pin for AD_B1_03 {
    const ALT: u32 = 2;
    const DAISY: Option<Daisy> = Some(DAISY_LPUART2_RX_AD_B1_03);
    type Direction = RX;
    type Module = U2;
}

impl Pin for AD_B1_02 {
    const ALT: u32 = 2;
    const DAISY: Option<Daisy> = Some(DAISY_LPUART2_TX_AD_B1_02);
    type Direction = TX;
    type Module = U2;
}

Unlike today's iomuxc module, which heavily relies on macros, we can simply implement all Pin traits with associated constants and types from the imxrt-iomuxc crate. The constants prefixed with DAISY_* may be written by hand, or auto-generated from an i.MX RT SVD file. We introduce a script that generates these constants.

Note the AD_B1_03 pad type. This leads to 2: defining the processor pads. We generate pads at build-time using the imxrt-iomuxc-build crate:

// ~~ build.rs ~~
use imxrt_iomuxc_build as build;

let ad_b1 = build::PadRange::new("AD_B1", 0..16);
let b0 = build::PadRange::new("B0", 0..16);
// ...

let mut pads_rs = fs::File::create(out_dir.join("pads.rs"))?;

build::write_pads(
    &mut pads_rs,
    vec![&ad_b1, &b0, /* ... */],
)?;

We introduce the imxrt-iomuxc-build crate. Processor-specific crates like imxrt106x-iomuxc use build scripts to define the pads. The build scripts depend on imxrt-iomuxc-build. The build scripts generate

Auto-generating the pads lets us quickly support other i.MX RT processor variants. We prefer to add pad definitions and pin implementations in separate crates (like imxrt101x-iomuxc), but we could provide definitions in the processor-specific HALs.

This PR includes all of these new crates, and it integrates the crates through the HAL. The crates have other details that aren't covered here, but are commented in source. Also see the documentation in the new crates.

This is a breaking change. The ways users reference and pass pad objects to HAL code is different. Additionally, there's a new GPIO driver which takes advantage of the new pad objects.

Discussion

The updated documentation, as well as the integration in the teensy4-rs project, shows that this achieves the goals described by #26. Do we think this is a sufficient approach for pin configuration?

What are our thoughts on using this as the foundation for split HALs, as part of #56? Pad definitions are one of the most processor-specific features we need to support, and this approach should let us de-couple the pad definitions from more general peripheral drivers.

I favor separating the pad definitions from the HAL. Specifically, I favor having a imxrt106x-iomuxc crate that's used by a imxrt106x-hal, rather than incorporating the contents directly into the HAL crate. These pin traits and pad implementations could have value for others, and keeping them separate from any HAL implementation could help others with prototyping. Do we think that's a worthy goal? Or, are there reasons that pad definitions should live in processor-specific HALs?

I went back and forth on how much Rust code we should auto-generate. At one point, I was writing a custom SVD -> Rust translator that would auto-generate all of the Pin implementations based on the SVD definitions. I eventually settled on this middle-ground, where we auto-generate the pad definitions, then manually implement Pin traits. I thought a fully-automated approach would be overkill, particularly since the number of processors we need to support is bounded (I have six different reference manuals; there's probably six crates we'd need to implement). The proposed approach does require us to manually check against the reference manuals. Should we consider more, or less, automation?

Any preference on maintaining these new crates in this repo? Or, should they live in a separate repo? This PR adds the new crates to this repo, but my first plan was to maintain them in a separate repo (the crates were all developed outside of the HAL).

TODOs