Reflections on imxrt-hal (2024)
I released version 0.5 of imxrt-hal around two years ago. The
release produced a package interface that didn’t exclusively favor
embedded-hal. It also exposes the low-level details of the hardware
to the user. Questions about this design decision came up this time
last year and again last week.
I’m happy with this design decision to share low-level details of
hardware with users. It makes it easier for me to use and maintain the
package. I don’t think the decision excludes users from the project,
although I understand the project may not immediately support all
use-cases. Finally, I think the package’s design lets us support new
capabilities, including async drivers, and drivers that are
externally maintained.
Package design and usability
I engage with two categories of imxrt-hal users, each having
different thoughts on the package API. The first user group expects
imxrt-hal to implement embedded-hal traits and nothing more. These
users aren’t interested in the low-level details of how their
peripherals work. This is the larger, majority user group.
The second user group knows that their system has an i.MX RT MCU,
knows how their MCU’s peripherals work, and knows how to build what
they want. These users would use the imxrt-ral register access layer
if there was nothing more convenient. This group is smaller than the
first group. But importantly, this group contains me.
I’ve benefited from a driver package that presents the low-level
details of the underlying hardware. For example, I’ve needed to talk
with SPI devices with unusual frame sizes (10, 13, or 25 bits).
embedded-hal doesn’t help me, nor would a driver that only supports
the common SPI traits. On the other hand, the imxrt-hal LPSPI driver
exposes its command queue, so I can transact however many bits I need.
I’ve also needed DMA- and interrupt-driven I/O without the
conveniences of async Rust. With today’s package, I can achieve
that, and I can optimize my drivers for my non-async runtime.
Despite my personal preference, I still believe imxrt-hal should
support the larger user group. Since we already have the low-level
details, we have what we need to implement embedded-hal traits.
Those implementations spin the CPU while waiting for I/O to complete.
I believe that efficiently realizing blocking I/O requires a
runtime, or different driver design to support a runtime. I haven’t
figured out this driver design, one that de-couples a blocking driver
from its runtime.
Async Rust changes how we can implement drivers and support embedded
runtimes. I’m happy to support async drivers in imxrt-hal. Again,
we have what we need to implement embedded-hal-async traits.
Achieving this goal doesn’t require that we remove the low-level
details from the public API.
Users from the first group might be confused that the package presents
low-level details of the hardware. We can address the concern by
better user documentation and examples, and by package construction or
alternate API design. At the extreme, imxrt-hal becomes exactly what
the larger group wants. I and the smaller group will adopt
imxrt-ral, with mild to moderate inconvenience.
Package maintenance
If imxrt-hal provides a low-level API to its users, that means users
can build their own drivers outside the package. That was a design
goal. It’s already possible for the ambitious, since imxrt-ral is
its own package. Of course, these users have no need to contribute
back to the project.
When I provide contracting services, I’m compensated for my familiarity with embedded Rust and my experience with these MCUs. That’s not the same as being compensated to maintain an open source project. Helping users develop their own drivers results in fewer contributions for me to review, by myself, in my free time. That’s OK with me. I would love to change this! That change needs either a different kind of contract with my clients, or additional maintainers and contributors to put in the work.
In my experience, users who want to use an internal API will maintain a private fork. They may also include their system’s details directly into the package, forming a code base that couldn’t be upstreamed. I don’t see how removing low-level details from the public API decreases this possibility.
Package organization
Today’s imxrt-hal implements most of the drivers supported by our
project. We have other drivers that are not implemented in
imxrt-hal. The package re-exports (or intends to re-export) these
externally-managed drivers. I’m not sure this re-exporting part is
always correct.
The first type of externally-managed drivers are those like
imxrt-usbd. They have a separate interface package (usb-device)
that dictates the driver’s compatibility with a larger ecosystem.
Other examples of this driver type include the prototype imxrt-enet
and imxrt-usdhc drivers (respectively implementing smoltcp and
embedded-sdmmc). These are separately maintained so that they can be
released when the interface package breaks. Users can depend on these
packages without imxrt-hal.
It’s not clear to me that imxrt-hal should re-export these kinds of
drivers. It may be better if users took direct dependencies on these
drivers; that way, users could upgrade their drivers and interface
packages without waiting for imxrt-hal. I have commits in my public
imxrt-hal fork that remove imxrt-usbd, and I may propose this for
the next breaking imxrt-hal release.
Our package organization could have a one-to-one correspondence of interface package to implementation package. It could look something like this:
| Interface package | Implementation package |
|---|---|
embedded-hal |
imxrt-hal |
embedded-hal-async |
imxrt-hal-async |
usb-device |
imxrt-usbd |
smoltcp |
imxrt-enet |
embedded-sdmmc |
imxrt-usdhc |
Just as embedded-hal-async depends on embedded-hal, the
(fictitious) imxrt-hal-async package depends on imxrt-hal.
Separating async drivers into their own package means that users who
don’t want async don’t need async.
The second type of externally-managed drivers are those that provide a
(perceived) improvement over imxrt-ral. The packages imxrt-iomuxc
and imxrt-dma are these type. Although they may be used within and
re-exported by imxrt-hal, these packages have value without
imxrt-hal. Users may use them to create their own drivers. I think
it’s OK for imxrt-hal to re-export these packages, particularly when
they’re part of the imxrt-hal public interface.
As for drivers that don’t have an interface package: I’m not sure
where they belong. They can go within imxrt-hal or their own
package. If it’s the latter, let me know if you want to maintain a
package within the imxrt-rs GitHub organization. I would be happy to
create a repository for you and make you a repo administrator.