Weather and air quality station: Part 2
Before starting with any kind of development on Raspberry Pi Pico2W I first need to setup the toolchain: SDK, compiler, linker and debugger. In this post I describe the following steps:
- Downloading and setting up Raspberry Pi Pico2W SDK,
- preparing and compiliing the GNU cross-compiler to compile RISC-V binaries,
- Setting up OpenOCD
It’s important to note, these steps work well on my machine (x86-64, Arch Linux, btw), some slight changes are probably required if there is a need to build on other machine and/or different Linux distro. To verify I have everything I need, and everything is setup properly, I’ll develop a simple LED blinky program, and enable UART.
Build environment
To set-up my build environment for Raspberry Pi Pico2_W, i’ll combine instructions from Raspberry Pi Pico-series C/C++ SDK document and instructions from riscv-gnu-toolchain Github page.
First I’ll install all dependencies: $ sudo pacman -Syu curl python3 libmpc mpfr gmp base-devel texinfo gperf patchutils bc zlib expat libslirp
Then I want to build a cross compiler that runs on my host machine (e.g. x86-64 Linux PC) but produces executables for a different target architecture (e.g. RISC-V).
Host = where the compiler runs. Target = where the generated program runs.
There are two flavors of a cross compiler for x86-64 Linux host:
-
Newlib cross compiler (riscv64-unknown-elf-gcc) Target: bare-metal systems (no OS, or minimal runtime). Uses Newlib as the C standard library. Good for firmware, bootloaders, small MCUs.
-
Linux cross compiler (riscv64-unknown-linux-gnu-gcc) Target: RISC-V systems running Linux. Uses glibc (GNU C library), same as native Linux distros. Produces programs you can run under a Linux kernel on RISC-V hardware (or emulators like QEMU).
I’ll now proceed with building Newlib cross compiler and the acompanying binuitls. First I’ll create a directory where the sources and the built binaries will be stored. This directory (install path) needs to be writeable. $ mkdir -p ~/rp2350/gcc-rp2350-no-zcmp
Change into ~/rp2350 directory, clone the riscv-gnu-toolchain repository and change into the riscv-gnu-toolchain directory: $ cd ~/rp2350 $ git clone https://github.com/riscv-collab/riscv-gnu-toolchain.git $ cd risc-vgnu-toolchain.git
Configure the build process to target the RISC-V ISA extensions supported by the RP2350, and to place the built binaries into the ~/rp2350/gcc-rp2350-no-zcmp directory: $ ./configure –prefix=~/rp2350/gcc-rp2350-no-zcmp –with-arch=rv32ima_zicsr_zifencei_zba_zbb_zbs_zbkb_zca_zcb –with-abi=ilp32 –with-multilib-generator=”rv32ima_zicsr_zifencei_zba_zbb_zbs_zbkb_zca_zcb-ilp32–;rv32imac_zicsr_zifencei_zba_zbb_zbs_zbkb-ilp32–”
Build the binaries: make -j”$(nproc)”
Optional reading:
When I first built the RISC-V bare-metal toolchain, I’ve blindly copy-pasted ./configure arguments, but I wandered what do they mean? We’ll here is the explanation:
RISC-V --with-arch breakdown
--with-arch=rv32ima_zicsr_zifencei_zba_zbb_zbs_zbkb_zca_zcb
Components
- rv32ima — 32-bit base ISA (I) plus M (mul/div) and A (atomics).
- zicsr — Control/status register access (
csrr*,csrw*, etc.). - zifencei — Instruction-fetch fence (
FENCE.I) to sync I-cache after code/data writes. - zba — Bit-manip “address generation” subset (add/shift patterns that speed indexing).
- zbb — Bit-manip “basic” subset (clz/ctz/popcount/min/max/extends/rotates, etc.).
- zbs — Bit-manip “single-bit” subset (set/clear/invert/extract by bit index).
- zbkb — Bit-manip helpers oriented toward crypto (pack/permute/byte ops); part of scalar-crypto-friendly sets.
- zca — Core compressed integer subset (16-bit encodings for common integer ops).
- zcb — Extra compressed encodings (requires
zca) for further code-size reduction.
RISC-V --with-abi=ilp32 breakdown
--with-abi=ilp32
What it sets
- Chooses the default ABI for the toolchain (override per compile with
-mabi=...). - ilp32 =
int,long, and pointers are 32-bit. - Soft-float ABI: no FP registers used for args/returns; FP math is done in software.
When to use
- RV32 targets without an FPU (no
F/Dextensions), or when you explicitly want a soft-float calling convention. This is our case, since RP2350 doesn’t have any hard floating-point unit.
If the RP2350 had single-precision FPU, I’d use
ilp32f; if it had double-precision FPU, I’d useilp32d.
Practical effects
- Defines the calling convention and type sizes (
sizeof(long)==4,sizeof(void*)==4). - Determines which multilib variant is selected/built.
- All linked libraries must match the ABI (mixing
ilp32withilp32f/dwill cause link/ABI errors).
What --with-multilib-generator="…" does (RISC-V)
--with-multilib-generator="rv32ima_zicsr_zifencei_zba_zbb_zbs_zbkb_zca_zcb-ilp32--;rv32imac_zicsr_zifencei_zba_zbb_zbs_zbkb-ilp32--"
Purpose
--with-multilib-generator= tells the bare-metal RISC-V toolchain build exactly which multilib variants (prebuilt libgcc, newlib, etc.) to produce.
The value is a semicolon-separated list of multilib configs.
Each config has four parts:
<arch string>-<ABI>-<reuse rule with arch>-<reuse rule with sub-extension>
(Empty fields mean “no reuse rule here”. This option is supported for riscv*--elf builds.)
What your string builds
You’re asking the build to create two multilib variants:
1) rv32ima_zicsr_zifencei_zba_zbb_zbs_zbkb_zca_zcb + ilp32
- 32-bit I/M/A base with CSR + fence.i, bit-manip subsets (Zba/Zbb/Zbs), crypto-oriented bit-manip (Zbkb), and compressed subsets (Zca + Zcb).
- ABI:
ilp32(32-bit int/long/pointers; soft-float). - Reuse rules: none (the trailing
--).
2) rv32imac_zicsr_zifencei_zba_zbb_zbs_zbkb + ilp32
- 32-bit I/M/A/C base (so compressed via
C), plus the same Z* subsets except Zca/Zcb. - ABI:
ilp32. - Reuse rules: none.
In short: build two library sets—one for an RV32 IMA core with Z* + Zc* compressed subsets, and one for an RV32 IMAC core with Z* (and C)—both using the ilp32 ABI.
Why/when to do this
- Smaller, faster build than the default multilib set—only the libraries you actually need are built.
- Lets GCC auto-select the “best-fit” multilib at compile/link time among the ones you built.
How to use & verify
Configure (example):
./configure --prefix=/opt/riscv \
--with-multilib-generator="rv32ima_zicsr_zifencei_zba_zbb_zbs_zbkb_zca_zcb-ilp32--;rv32imac_zicsr_zifencei_zba_zbb_zbs_zbkb-ilp32--"
make -j"$(nproc)"
See what was built:
riscv64-unknown-elf-gcc --print-multi-lib
(Shows the two multilibs you requested.)
Pick one at compile time (GCC will choose automatically if compatible, but you can be explicit):
# Matches variant (1)
-march=rv32ima_zicsr_zifencei_zba_zbb_zbs_zbkb_zca_zcb -mabi=ilp32
# Matches variant (2)
-march=rv32imac_zicsr_zifencei_zba_zbb_zbs_zbkb -mabi=ilp32
If you request an
-march/-mabicombo with no compatible multilib, you can hit link/ABI errors—add another entry to the generator string or adjust your compile flags.
Reference: See the toolchain README section “Build with customized multi-lib configure” for the full format, examples, and notes on reuse rules.
export PICO_TOOLCHAIN_PATH=/ssd1/workspace/pico2w/build-tools/gcc15-rp2350-no-zcmp export PICO_PLATFORM=rp2350-riscv export PICO_SDK_PATH=/ssd1/workspace/pico2w/pico-sdk export PICO_BOARD=pico2_w
The next thing we want to do is we want to test if the board is working correctly. In the pico-sdk-examples directory there are couple of examples. I’ll use the blink example to see if I can actually program this thing and the hello_world USB example to see if UART is working correctly. Both of these things will be very useful in debugging.
- Hold to BOOTSEL button on the Pico2w while connecting it via USB cable to the PC
- Type lsblk -fs, you should see your device listed
sdb1 vfat FAT16 RP2350 62D0-C970 └─sdb - Create a directory and mount it
- mdir -p /ssd1/workspace/pico2w/flash
- sudo mount /dev/sdb1 /dds1/workspace/pico2w/flash
#TODO:
- instruction download sdk
- instructions download examples
- instruction test with hello_world and blink
- add riscv32-unknown-elf* binaries to the $PATH
Let’s now build our bare metal ‘Hello UART’ application. I’ve defined the following goals:
- The application will be written in pure RISC-V assembly,
- It needs to run on a RISC-V core,
- I want the ‘Hello UART’ message printed on the UART every one second,
- I should be able to flash the application using the USB bootloader
An overview how code is executed on RP2350
The RP2350 contains a (fixed) bootrom and this bootrom is executed first, not the user application. The boot outcome depends on multiple variables, all described in Chapter 5.2.1 Boot outcomes in [1]. In our case, we want to run our application from the USB bootloader. The USB bootloader will load the application stored in the UF2 [reference] format, and it will write this application to flash. But how does the bootrom know on which core do we want to run this application, RISC-V or ARM? Bootrom needs to locate and viable metadata. This metadata is defined in the Chapter 5.9 Metadata block details in [1]. The Chapter 5.9.5.2 describes Minimum RISC-V IMAGE_DEF. We will use this information