pamoja

Boards

RP2040#

The RP2040 is Raspberry Pi's microcontroller, and the Raspberry Pi Pico is the board it ships on: two Cortex-M0+ cores, no operating system, no radio on the plain model, and a price under five dollars. pamoja's no_std crates run on it over its own peripherals through rp2040-hal, the community's Rust hardware layer for the chip. This page works through the Pico; the hardware page has its card.

The board#

From the Pico datasheet: an RP2040 with 2 MB of flash, dual-core Cortex-M0+ at up to 133 MHz, 264 kB of SRAM, 26 multi-function 3.3 V GPIO on a 40-pin footprint with castellated edges, 3 of them ADC capable, two UARTs, two I2C controllers, two SPI controllers, 16 PWM channels, a 12-bit 500 ksps ADC, USB 1.1, and two programmable I/O blocks. The board's I/O voltage is fixed at 3.3 V, and the datasheet is plain that the pinout is by GP number, printed on the board, not by the header position.

Programming needs no debugger. Hold the BOOTSEL button while plugging the board in and it appears as a USB mass-storage device; dragging a UF2 file onto it writes the flash and restarts the board. picotool, Raspberry Pi's own tool, does the same from the command line, and the program below names it as the runner.

The board's own SDK fixes the default pins, and the program uses them:

BusSignalGP
I2C0SDAGP4
I2C0SCLGP5
SPI0SCKGP18
SPI0TX (MOSI)GP19
SPI0RX (MISO)GP16
SPI0CSnGP17
UART0TXGP0
UART0RXGP1
LEDon boardGP25

Wiring a BME280#

Wire the BME280's SDA to GP4, its SCL to GP5, VIN to the 3V3(OUT) pin, and GND to any ground pin. To see the readings, a USB serial adapter on GP0 (the board's TX, to the adapter's RX) and GP1 (RX, to the adapter's TX) at 115200 baud, with its ground joined to the board's. The chip answers at 0x76 unless the breakout's jumper moves it to 0x77.

The toolchain#

Shell
rustup target add thumbv6m-none-eabi
cargo install picotool

The Cortex-M0+ is the thumbv6m-none-eabi target. The program's .cargo/config.toml sets that target and names picotool as the runner, so with the board in BOOTSEL mode cargo run loads and starts it.

The first program#

The program below is a complete package at examples/boards/rp2040, cross-compiled in CI on every change. It brings up the clocks from the board's crystal, opens I2C0 on GP4 and GP5, hands it to the BME280 driver, and writes a compensated reading over UART0 every two seconds, toggling the LED on each.

From examples/boards/rp2040/src/main.rs:

Rust
use hal::gpio::{FunctionI2C, Pin};
use hal::uart::{DataBits, StopBits, UartConfig, UartPeripheral};
use pamoja_sensors::bme280::{Bme280, I2C_ADDRESS_PRIMARY};

#[hal::entry]
fn main() -> ! {
    {
        const HEAP_SIZE: usize = 4 * 1024;
        static mut HEAP_MEM: [core::mem::MaybeUninit<u8>; HEAP_SIZE] =
            [core::mem::MaybeUninit::uninit(); HEAP_SIZE];
        // SAFETY: the heap is initialized once, here, before anything allocates.
        unsafe { HEAP.init(core::ptr::addr_of_mut!(HEAP_MEM) as usize, HEAP_SIZE) }
    }

    let mut pac = hal::pac::Peripherals::take().expect("the peripherals are taken once");
    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
    let clocks = hal::clocks::init_clocks_and_plls(
        XTAL_FREQ_HZ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .expect("the clocks come up from the crystal");
    let sio = hal::Sio::new(pac.SIO);
    let pins = hal::gpio::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    // I2C0 on the pins the board's own SDK calls its default I2C, GP4 and GP5, at the
    // 400 kHz the BME280 accepts.
    let sda: Pin<_, FunctionI2C, _> = pins.gpio4.reconfigure();
    let scl: Pin<_, FunctionI2C, _> = pins.gpio5.reconfigure();
    let i2c = hal::I2C::i2c0(
        pac.I2C0,
        sda,
        scl,
        400.kHz(),
        &mut pac.RESETS,
        &clocks.system_clock,
    );

    // UART0 on GP0 and GP1 carries the readings to a serial adapter; the LED on GP25
    // toggles on each one.
    let uart_pins = (pins.gpio0.into_function(), pins.gpio1.into_function());
    let mut uart = UartPeripheral::new(pac.UART0, uart_pins, &mut pac.RESETS)
        .enable(
            UartConfig::new(115_200.Hz(), DataBits::Eight, None, StopBits::One),
            clocks.peripheral_clock.freq(),
        )
        .expect("a valid UART configuration");
    let mut led = pins.gpio25.into_push_pull_output();

    // The timer is the driver's delay: the datasheet's start-up and measurement waits.
    let mut timer = hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
    let mut sensor = Bme280::i2c(i2c, I2C_ADDRESS_PRIMARY, timer);
    sensor.init().expect("the BME280 answers on I2C0");

    loop {
        let measurement = sensor.measure().expect("a measurement");
        let _ = writeln!(
            uart,
            "{:.2} C, {:.2} hPa, {:.2} % humidity\r",
            measurement.celsius(),
            measurement.hectopascals(),
            measurement.relative_humidity_percent()
        );
        let _ = led.toggle();
        timer.delay_ms(2000);
    }
}

Hold BOOTSEL, plug the board in, and run it from that directory:

Shell
cd examples/boards/rp2040
cargo run --release

Three things above the snippet in the source are the chip's own requirements. The first 256 bytes of flash hold a second-stage bootloader that configures the flash chip, which rp2040-boot2 supplies and the linker script memory.x places. A panic halts the core, through panic-halt. And the binary declares a small heap through embedded-alloc: the core's own types hold an owned topic and payload, so a no_std binary that links them needs an allocator, though the driver itself never allocates.

Where next#

Sources#

  • Raspberry Pi Pico Datasheet, release 21, for the board's features, its I/O voltage, the BOOTSEL mode, and the pinout.
  • pico.h in the Raspberry Pi Pico SDK, for the board's default UART, I2C, SPI, and LED pins.
  • rp2040-hal on docs.rs, version 0.12, for the clocks, the I2C and UART drivers, and the timer that implements the embedded-hal delay, with the crate's own examples for the boot block and linker layout.

Edit this page on GitHub