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:
| Bus | Signal | GP |
|---|---|---|
| I2C0 | SDA | GP4 |
| I2C0 | SCL | GP5 |
| SPI0 | SCK | GP18 |
| SPI0 | TX (MOSI) | GP19 |
| SPI0 | RX (MISO) | GP16 |
| SPI0 | CSn | GP17 |
| UART0 | TX | GP0 |
| UART0 | RX | GP1 |
| LED | on board | GP25 |
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#
rustup target add thumbv6m-none-eabi
cargo install picotoolThe 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:
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:
cd examples/boards/rp2040
cargo run --releaseThree 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#
- Buses and links, for what each bus on the chip is for, and the bus layer guide for the traits and the scripted bus a driver is tested against with nothing plugged in.
- Sensor drivers and actuator drivers, for every shipped part.
- Your own device, for a part pamoja has never heard of.
Sources#
- Raspberry Pi Pico Datasheet, release 21, for the board's features, its I/O voltage, the BOOTSEL mode, and the pinout.
pico.hin the Raspberry Pi Pico SDK, for the board's default UART, I2C, SPI, and LED pins.rp2040-halon docs.rs, version 0.12, for the clocks, the I2C and UART drivers, and the timer that implements theembedded-haldelay, with the crate's own examples for the boot block and linker layout.