git | cv | links | quotes | ascii | mail


Difficult to use from bare metal? (not that much)

While some might have mixed feelings about using the rp2040 bare-bones, it is really not that bad.

Something like this is enough to get it to turn the Pico board's led (GPIO 25) on: The usual reset handler calling main at the end, and enabling the clocks for the peripheral you need, then writing to the peripheral itself:

$ cat rp2040.c

#include <stdint.h>
#include "rp2040.h"

#define GPIO_LED 25

	/* enable the GPIO and the PADs by taking them out of reset state */

	/* enable output mode for the LED pin */

	/* continuously turn the led on */
	for (;;)
	return 0;

	extern char __data_start, __data_end, __data_load_start;
	extern char __bss_start, __bss_end;

	volatile char *src, *dst;

	/* fill initialised and uninitialised variables */
	src = &__data_load_start;
	for (dst = &__data_start; dst < &__data_end; *dst++ = *src++);
	for (dst = &__bss_start; dst < &__bss_end; *dst++ = 0);


 * Boot stage 2 bootloader: padded and checksummed version of
 * pico-sdk/src/rp2_common/boot_stage2/bs2_default.bin
 * It is placed at the top of the ROM by the linker script
char __bootloader[] = {
	0x00, 0xb5, 0x32, 0x4b, 0x21, 0x20, 0x58, 0x60, 0x98, 0x68, 0x02, 0x21,
	0x88, 0x43, 0x98, 0x60, 0xd8, 0x60, 0x18, 0x61, 0x58, 0x61, 0x2e, 0x4b,
	0x00, 0x21, 0x99, 0x60, 0x02, 0x21, 0x59, 0x61, 0x01, 0x21, 0xf0, 0x22,
	0x99, 0x50, 0x2b, 0x49, 0x19, 0x60, 0x01, 0x21, 0x99, 0x60, 0x35, 0x20,
	0x00, 0xf0, 0x44, 0xf8, 0x02, 0x22, 0x90, 0x42, 0x14, 0xd0, 0x06, 0x21,
	0x19, 0x66, 0x00, 0xf0, 0x34, 0xf8, 0x19, 0x6e, 0x01, 0x21, 0x19, 0x66,
	0x00, 0x20, 0x18, 0x66, 0x1a, 0x66, 0x00, 0xf0, 0x2c, 0xf8, 0x19, 0x6e,
	0x19, 0x6e, 0x19, 0x6e, 0x05, 0x20, 0x00, 0xf0, 0x2f, 0xf8, 0x01, 0x21,
	0x08, 0x42, 0xf9, 0xd1, 0x00, 0x21, 0x99, 0x60, 0x1b, 0x49, 0x19, 0x60,
	0x00, 0x21, 0x59, 0x60, 0x1a, 0x49, 0x1b, 0x48, 0x01, 0x60, 0x01, 0x21,
	0x99, 0x60, 0xeb, 0x21, 0x19, 0x66, 0xa0, 0x21, 0x19, 0x66, 0x00, 0xf0,
	0x12, 0xf8, 0x00, 0x21, 0x99, 0x60, 0x16, 0x49, 0x14, 0x48, 0x01, 0x60,
	0x01, 0x21, 0x99, 0x60, 0x01, 0xbc, 0x00, 0x28, 0x00, 0xd0, 0x00, 0x47,
	0x12, 0x48, 0x13, 0x49, 0x08, 0x60, 0x03, 0xc8, 0x80, 0xf3, 0x08, 0x88,
	0x08, 0x47, 0x03, 0xb5, 0x99, 0x6a, 0x04, 0x20, 0x01, 0x42, 0xfb, 0xd0,
	0x01, 0x20, 0x01, 0x42, 0xf8, 0xd1, 0x03, 0xbd, 0x02, 0xb5, 0x18, 0x66,
	0x18, 0x66, 0xff, 0xf7, 0xf2, 0xff, 0x18, 0x6e, 0x18, 0x6e, 0x02, 0xbd,
	0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00,
	0x00, 0x03, 0x5f, 0x00, 0x21, 0x22, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x18,
	0x22, 0x20, 0x00, 0xa0, 0x00, 0x01, 0x00, 0x10, 0x08, 0xed, 0x00, 0xe0,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x74, 0xb2, 0x4e, 0x7a,

char __stack_top;
void *__stack_pointer = &__stack_top;

void (*__vectors[])(void) = {
	/* other interrupts here... */

The big block might feel a bit like a proprietary blob. Thankfully it is not: the source is easily available from the pico-sdk repo, and you can compile it from source, and put the generated assembly onto a bootloader.S, or like the above in an array of bytes.

Some registers definitions needed for that C file:

$ cat rp2040.h

#define RESETS ((struct mcu_resets *)0x4000C000)

struct mcu_resets {

	/* 0x00: Reset control. */
	uint32_t volatile RESET;
#define RESETS_RESET_PADS_BANK0					(1u << 8)
#define RESETS_RESET_IO_BANK0					(1u << 5)

	/* 0x04: Watchdog select. */
	uint32_t volatile WDSEL;

	/* 0x08: Reset done. */
	uint32_t volatile RESET_DONE;
#define RESETS_RESET_DONE_PADS_BANK0				(1u << 8)
#define RESETS_RESET_DONE_IO_BANK0				(1u << 5)

	/* ... */

#define IO_BANK0 ((struct mcu_io_bank0 *)0x40014000)

struct mcu_io_bank0 {

	struct mcu_io_bank0_gpio {

		/* 0x00: GPIO status */
		uint32_t volatile STATUS;

		/* 0x04: GPIO control including function select and overrides. */
		uint32_t volatile CTRL;
		/* selects pin function according to the gpio table */
#define IO_BANK0_GPIO_CTRL_FUNCSEL_SIO				(0x5u << 0)

	} GPIO[29];

	/* ... */

#define SIO ((struct mcu_sio *)0xD0000000)

struct mcu_sio {

	/* 0x00: Processor core identifier */
	uint32_t volatile CPUID;

	/* 0x04: Input value for GPIO pins */
	uint32_t volatile GPIO_IN;

	/* 0x08: Input value for QSPI pins */
	uint32_t volatile GPIO_HI_IN;

	/* 0x0C */
	uint8_t RESERVED0[0x10u-0x0Cu];

	/* 0x10: GPIO output value */
	uint32_t volatile GPIO_OUT;

	/* 0x14: GPIO output value set */
	uint32_t volatile GPIO_OUT_SET;

	/* 0x18: GPIO output value clear */
	uint32_t volatile GPIO_OUT_CLR;

	/* 0x1C: GPIO output value XOR */
	uint32_t volatile GPIO_OUT_XOR;

	/* 0x20: GPIO output enable */
	uint32_t volatile GPIO_OE;

	/* 0x24: GPIO output enable set */
	uint32_t volatile GPIO_OE_SET;

	/* 0x28: GPIO output enable clear */
	uint32_t volatile GPIO_OE_CLR;

	/* 0x2C: GPIO output enable XOR */
	uint32_t volatile GPIO_OE_XOR;

	/* ... */

And a regular, straightforward linker script:

$ cat rp2040.ld
	ROM (rx) : ORIGIN = 0x10000000, LENGTH = 2048K
	RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 256K


	.text : {
		. = ALIGN(4);
		*(.text .text.*)
		*(.rodata .rodata.*)
	} > ROM

	.data : {
		PROVIDE(__data_start = .);
		*(.data .data.*)
		PROVIDE(__data_end = .);
	} > RAM AT> ROM

	.bss ADDR(.data) + SIZEOF (.data) : AT (ADDR (.bss)) {
		PROVIDE(__bss_start = .);
		PROVIDE(__bss_end = .);
	} > RAM

	__data_load_start = LOADADDR(.data);
	__data_load_end = __data_load_start + SIZEOF(.data);
	__stack_top = ORIGIN(RAM) + LENGTH(RAM);

Then a Makefile to wrap it all together:

$ cat Makefile

all: rp2040.uf2

rp2040.elf: rp2040.o
	arm-none-eabi-ld --gc-sections -Trp2040.ld -nostdlib -static -o $@ rp2040.o

.SUFFIXES: .elf .uf2

	rm -f *.o *.elf *.uf2 *.map

	arm-none-eabi-gcc -mthumb -mcpu=cortex-m0plus -msoft-float -ffunction-sections -fdata-sections -c -o $@ $<

	elf2uf2 $< $@

Trying "make" then copying the rp2040.uf2 file generated did work.

Drag-and-Drop to flash: useless bloat?

What about that fancy UF2 bootloader that lets you drag-and-drop binaries to flash the firmware? It comes for free: there is an USB peripheral on the pico, and while pressing the "BOOTSEL" button at startup, the RP2040 loads a firmware that make use of that USB peripheral to expose a FAT drive device from this bootloader firmware.

So it sounds a bit of an overkill, it was not compromising to add it, since it does not require anything special in hardware at all, only a firmware that handles that complexity.

Would you like a different interface like SWD? That is also completely possible through projects like can help.

Use of CMSIS-DAPv2

While most tools around use CMSIS-DAPv1, which works fine, RP2040 having two cores motivated the engineering team to use the CMSIS-DAPv2 interface, which features the "multi-drop" extension, allowing to debug multiple targets through the same link.

This was probably to make debugging multiple cores easier.

Debug access is via independent DAPs (one per core) attached to a shared multidrop SWD bus (SWD v2). Each DAP will only respond to debug commands if correctly addressed by a SWD TARGETSEL command; all others tristate their outputs. Additionally, a Rescue DP (see Section is available which is connected to system control features. Default addresses of each debug port are given below:

Lately, the custom OpenOCD fork was dropped as support came back upstream.

The Black Magic Debug project is also able to debug the RP2040.

Not as convenient as STM32?

Besides this, it might have a few things made different, but it is also not an STM32, as the STM32 are the best at being an STM32 already, and yet another "STM32 killer" would not be so useful.


As soon as I managed to boot that RP2040 with this few code, I started to consider that board seriously, as very little boilerplate is required to make it start executing your code after all, even if the pico-sdk is having a lot, it definitely is usable stand-alone just like any other microcontroller.