Adding a Safe API for PIO

A few weeks ago, we cleaned up parallel I/O by adding a module that provides low-level access to that feature. This kind of module is useful, but potentially error-prone. We can do better than that.

Rust's type system allows us to write APIs that largely ensure at compile time that the API is being used correctly. In a lot of languages, incorrect use of an API would result in runtime exceptions. Trying to use a feature before enabling it or attempting to write to a file after closing it are examples of this. Rust allows us to write APIs in such a way, that a program containing these errors wouldn't even compile.

Before we take a look at the new API, a few remarks:

But enough introduction. Let's get started:

pub fn a() -> Controller { Controller(pio::PIO_A) }
pub fn b() -> Controller { Controller(pio::PIO_B) }
pub fn c() -> Controller { Controller(pio::PIO_C) }
pub fn d() -> Controller { Controller(pio::PIO_D) }
pub fn e() -> Controller { Controller(pio::PIO_E) }
pub fn f() -> Controller { Controller(pio::PIO_F) }

For each PIO controller[1], there's a function to retrieve it. As you can see, the wrapper module accesses the low-level PIO module via the pio:: prefix.

pub struct Controller(*mut pio::Controller);

impl Controller {
	pub unsafe fn pin_27(&self) -> Pin<StatusUndefined, OutputStatusUndefined> {
		let &Controller(controller) = self;
		Pin::new(pio::P27, controller)

Controller wraps the low-level pio::Controller, but doesn't directly provide access to its features. Instead, it provides access to the controller's pins. There are two things to note here:

  1. Only a pin 27 can be accessed. As that's all we need for now, I decided to leave it at that. Repeating that method 32 times by hand would be too error-prone, and I didn't want to introduce a macro at this time (although I'm sure I will do so later).
  2. The method that retrieves the pin is marked unsafe. The Pin type encodes the pin's current status to enable compile-time error checking, but the user can circumvent that protection by creating multiple Pin structs for a single pin. The unsafe qualifier marks this as an error-prone operation that needs to be reviewed carefully.

Here's the Pin type:

pub struct Pin<Status, OutputStatus> {
	mask      : u32,
	controller: *mut pio::Controller,

	status       : PhantomData<Status>,
	output_status: PhantomData<OutputStatus>,

It contains the pin's mask and a pointer to the controller, to be able to perform I/O operations. The more interesting element are the type parameters though. They encode, statically at compile-time, the pin's general status (is it enabled or not) and the pin's output status (is it configured for input or output).

If you take a look at the Controller code above, you'll see that both are initially undefined. That's because we can't really know whether the pin was used before, or even what status a given pin has after system reset[2].

As Rust disallows unused type parameters, but we don't really have a use for those type parameters within the data structure, we need to add those two dummy fields of type PhantomData.

impl<Status, OutputStatus> Pin<Status, OutputStatus> {
	fn new(mask: u32, controller: *mut pio::Controller)
		-> Pin<Status, OutputStatus>
		Pin {
			mask      : mask,
			controller: controller,

			status       : PhantomData,
			output_status: PhantomData,

This is the first implementation block for Pin, which contains the constructor. As you can see from the two type parameters after impl, this implementation block is fully generic. This means the new method can be used to create Pins with any status. This is ok, as the constructor is not public and just used internally.

impl<OutputStatus> Pin<StatusUndefined, OutputStatus> {
	pub fn enable(self) -> Pin<StatusEnabled, OutputStatus> {
		unsafe { (*self.controller).pio_enable = self.mask };
		Pin::new(self.mask, self.controller)

This implementation block only applies to pins with an undefined status, but doesn't put restrictions on the output status. For these pins, the enable method is defined.

There are two things that are important to understand about this construct:

  1. The enable method encodes what it's doing at the type level. If you call enable on a Pin<StatusUndefined, OutputStatusUndefined>, you will get back a Pin<StatusEnabled, OutputStatusUndefined>. You can't call it on pins that are already enabled.
  2. As enable takes the self argument by value, the old pin is being consumed by it, due to Rust's move semantics. That means you can't keep a copy of your old Pin and use it to trick the API into doing stuff that's not supposed to be possible.

I'm not actually sure whether this block is entirely correct though. It assumes that the output status is always preserved, that you can just take a pin whose status you know nothing about, enable it and assume that the output status is the same as it was before. I don't know if that's a safe assumption. I'll have to test this at some point.

impl Pin<StatusEnabled, OutputStatusUndefined> {
	pub fn enable_output(self) -> Pin<StatusEnabled, OutputStatusEnabled> {
		unsafe { (*self.controller).output_enable = self.mask };
		Pin::new(self.mask, self.controller)

The next implementation block allows you to enable pin output. Note that it's only defined for enabled pins with an undefined output status. This API makes it impossible to configure output on a pin that you forgot to enable. A program attempting to do this just won't compile.

impl Pin<StatusEnabled, OutputStatusEnabled> {
	pub fn set_output(&self) {
		unsafe { (*self.controller).set_output_data = self.mask };

	pub fn clear_output(&self) {
		unsafe { (*self.controller).clear_output_data = self.mask };

The final implementation block provides methods to control the output signal of the pin. The methods that provide this capability are only defined for Pins that are enabled and whose output is enabled.

pub struct StatusUndefined;
pub struct StatusEnabled;

pub struct OutputStatusUndefined;
pub struct OutputStatusEnabled;

Finally, these are the different state types for Pin. Those are just empty structs, which means they are just used as marker types and don't have any footprint at runtime.

That's the API! As I said, it's incomplete and only provides the bare minimum that is required to support our program. We can extend it as needed in the future.

Let's take a look at how it's used. You might remember, the old version of the program using the low-level PIO module had a big unsafe block and dereferenced all those unsafe pointers. The new API makes things a lot nicer.

The first thing we need to do is to get the Pin instance.

let led = unsafe { pio::b().pin_27() };

As I explained above, this operation is unsafe, as it allows us to retrieve multiple instances for the same pin and circumvent the guarantees of the API that way. It's the only unsafe block that is required to use the new API.

let led = led

This is the pin initialization code that enables the pin and configures it for output. As enable and enable_output consume the pin, we need to capture the result in a new variable[3]. Otherwise, there would be no way for us to access the enabled pin ever again.

loop {

The last missing piece is the meat of our program: The loop that makes the LED blink.

For any of you who don't believe me that this API basically prevents you from misusing it, here's an example. This is what happens if we leave out the middle part of the program and try to use the LED without initializing it:

blink/main.rs:21:7: 21:19 error: no method named `set_output` found for type `hardware::api::pio::Pin<hardware::api::pio::StatusUndefined, hardware::api::pio::OutputStatusUndefined>` in the current scope
blink/main.rs:21 		led.set_output();
blink/main.rs:23:7: 23:21 error: no method named `clear_output` found for type `hardware::api::pio::Pin<hardware::api::pio::StatusUndefined, hardware::api::pio::OutputStatusUndefined>` in the current scope
blink/main.rs:23 		led.clear_output();
error: aborting due to 2 previous errors

The methods simply aren't available. Without cheating (i.e. using unsafe), there's just no way to call them unless you properly initialize the pin.

That's it for today. As always, the full code is available on GitHub. See you next time!