Embedded Rust on the Raspberry Pi Pico

I worked on bringing up embedded Rust on the RP2040 microcontroller during a summer internship at Harsch Systems. There was no official SDK support, so I worked from the C SDK as a reference, which was very helpful.

When
2021
Status
Completed
Role
Engineering Intern, Harsch Systems
  • Rust
  • Embedded HAL
  • RP2040

Overview

This was my last project at Harsch Systems before I left for college, and it was probably the most difficult as well. The Raspberry Pi Foundation’s SDK for the RP2040 was written in C, but my boss wanted to be able to run Rust. This meant bringing up the embedded Rust stack directly, since at the time the RP2040 was new and unsupported (now, Rust is supported on the RP2040). I used the C SDK as a reference.

The embedded Rust stack

Trying to get Embedded Rust working on the RP2040 was incredibly difficult, and I only managed to implement a few peripherals before I finished. The Embedded Rust crate stack splits the job of talking to hardware across several crates, from low-level register access up to friendly hardware abstractions.

Diagram of the embedded Rust crate stack, from board and HAL crates down through the micro-architecture and peripheral access crates to the microcontroller's hardware The embedded Rust stack, from high abstraction (left) to the hardware (right). The example microcontroller shown is an STM32F3, but the same structure applies to the RP2040. Figure: The Embedded Rust Book.

Register level control

Pinout diagram of the RP2040 chip, showing GPIO, QSPI, USB, power and debug pins around a 56-pin package
The RP2040's 56 pins: 30 GPIO (four doubling as ADC inputs), QSPI flash, USB, power and debug. Every peripheral used is reached through these pins and the registers behind them. Figure: Raspberry Pi RP2040 datasheet.

I wrote test programs for GPIO pins, the ADC, and PWM control, configuring each peripheral register by register directly via the peripheral access crate. This involved taking it out of reset, selecting pin functions, and setting up clocks, counters and duty cycles.

As an example of the code I had to write, here is the PWM setup, which brings the peripheral out of reset and configures channel 0 for a 50% duty cycle (note that I referred frequently to the official C SDK to understand the correct ordering here):

// Take the PWM block out of reset, and wait for it
p.RESETS.reset.modify(|_, w| w.pwm().clear_bit());
while p.RESETS.reset_done.read().pwm().bit_is_clear() {}

pwm.ch0_div.write(|w| unsafe { w.int().bits(0x01u8) });           // Clock divider = 1
pwm.ch0_top.write(|w| unsafe { w.ch0_top().bits(0xffffu16) });    // Counter wraps at max
pwm.ch0_cc.write(|w| unsafe { w.a().bits(0x7fffu16) });           // 50% duty cycle
pwm.ch0_csr.write(|w| w.en().set_bit());                          // Enable the channel

io.gpio[0].gpio_ctrl.write_with_zero(|w| w.funcsel().pwm_a_0());  // Route PWM to GPIO 0

This project took up only a short part of my time at Harsch Systems. In the end, I left my boss with some working examples that had been tested on an RP2040 and documentation with a guide on how to proceed.