Raspberry Pi Pico 2 - Deep Dive into RISC-V Development
Why Bother with RISC-V?
Before we dive in, why would you want to use RISC-V instead of the ARM cores?
Good reasons:
- Open source processor design - you can actually see how it works
- No licensing fees if you’re building commercial products
- Simpler instruction set to understand
- Future-proof (RISC-V is growing fast)
Not so good reasons:
- No real performance advantage over ARM
- Smaller software ecosystem
- Some Pico features only work on ARM cores
Honestly, for most projects, ARM is still easier. But if you want to learn RISC-V or need open hardware, read on.
Setting Up the RISC-V Toolchain
This is the tricky part. The regular GCC toolchain doesn’t support the Hazard3 cores properly, so you need a special one.
Get the Right Tools
First, grab the RISC-V toolchain. It’s the easiest option that actually works:
1
2
3
4
5
6
# Download the pre-built toolchain
cd ~/
wget https://buildbot.embecosm.com/job/corev-gcc-ubuntu2204/47/artifact/corev-openhw-gcc-ubuntu2204-20240530.tar.gz
# Extract it
tar xvf corev-openhw-gcc-ubuntu2204-20240530.tar.gz
Note: That URL is for Ubuntu 22.04. Check Embecosm’s site for other versions.
Set Environment Variables
1
2
3
4
5
6
7
# Set the toolchain paths
export PICO_TOOLCHAIN_PATH=~/corev-openhw-gcc-ubuntu2204-20240530
export PICO_RISCV_TOOLCHAIN_PATH=~/corev-openhw-gcc-ubuntu2204-20240530
# Add to your .bashrc so you don't have to do this every time
echo 'export PICO_TOOLCHAIN_PATH=~/corev-openhw-gcc-ubuntu2204-20240530' >> ~/.bashrc
echo 'export PICO_RISCV_TOOLCHAIN_PATH=~/corev-openhw-gcc-ubuntu2204-20240530' >> ~/.bashrc
Get the Pico SDK
1
2
3
4
5
6
7
8
# Clone the SDK if you don't have it
git clone https://github.com/raspberrypi/pico-sdk
cd pico-sdk
git submodule update --init
# Set the path
export PICO_SDK_PATH=$(pwd)
echo "export PICO_SDK_PATH=$(pwd)" >> ~/.bashrc
Your First RISC-V Program
Let’s start with the classic blink example to make sure everything works.
Build the ARM Version First
Always good to test with ARM first to make sure your hardware works:
1
2
3
4
5
6
7
8
cd ~/
git clone https://github.com/raspberrypi/pico-examples.git
cd pico-examples/blink_simple
# Build for ARM first
mkdir build && cd build
cmake -DPICO_PLATFORM=rp2350 ..
make -j4
You should get a blink_simple.uf2
file. Copy it to your Pico 2 and make sure the LED blinks.
Now Build for RISC-V
1
2
3
4
# Clean up and rebuild for RISC-V
rm -rf *
cmake -DPICO_PLATFORM=rp2350-riscv ..
make -j4
Check that it actually built for RISC-V:
1
2
3
# Verify the architecture
file blink_simple.elf
# Should say: "ELF 32-bit LSB executable, UCB RISC-V, RVC, soft-float ABI..."
Flash it to your Pico 2. If the LED blinks, you’re now running RISC-V code!
Getting Serial Output Working
Blinking LEDs are fun, but you’ll want to see actual output. There are two ways to do this.
Method 1: USB Serial (Easiest)
Use the USB connection for serial output:
1
2
3
cd ~/pico-examples/hello_world/hello_usb
cmake -DPICO_PLATFORM=rp2350-riscv ..
make -j4
Flash hello_usb.uf2
to your board. Then connect with any serial terminal:
1
2
3
4
5
6
7
8
# Find the device (usually /dev/ttyACM0 or similar)
ls /dev/ttyACM*
# Connect with screen
sudo screen /dev/ttyACM0 115200
# Or use minicom
sudo minicom -D /dev/ttyACM0 -b 115200
Method 2: Hardware Serial
If you want to use the UART pins:
1
2
3
cd ~/pico-examples/hello_world/hello_serial
cmake -DPICO_PLATFORM=rp2350-riscv ..
make -j4
Wire up pins:
- Pin 1 (GP0) → RX of USB-to-serial adapter
- Pin 2 (GP1) → TX of USB-to-serial adapter
- Pin 3 (GND) → Ground
The Code is Identical
Here’s the thing - both hello programs are exactly the same:
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include "pico/stdlib.h"
int main() {
stdio_init_all();
while (true) {
printf("Hello, world!\n");
sleep_ms(1000);
}
}
The difference is just in CMakeLists.txt:
1
2
3
4
5
6
7
# For USB output
pico_enable_stdio_usb(hello_usb 1)
pico_enable_stdio_uart(hello_usb 0)
# For UART output
pico_enable_stdio_usb(hello_serial 0)
pico_enable_stdio_uart(hello_serial 1)
Pretty neat - the SDK handles all the low-level stuff.
Advanced RISC-V Features
Once you’re comfortable with basic development, you can explore RISC-V specific features.
Control and Status Registers
Access RISC-V system registers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Read machine status register
uint32_t get_mstatus() {
uint32_t value;
asm volatile ("csrr %0, mstatus" : "=r" (value));
return value;
}
// Get cycle counter
uint64_t get_cycle_count() {
uint32_t lo, hi;
asm volatile ("csrr %0, mcycle" : "=r" (lo));
asm volatile ("csrr %0, mcycleh" : "=r" (hi));
return ((uint64_t)hi << 32) | lo;
}
Memory Ordering
RISC-V has explicit memory fences:
1
2
3
4
5
6
7
8
9
void memory_barrier() {
// Full memory fence
asm volatile ("fence");
}
void io_barrier() {
// I/O specific fence
asm volatile ("fence io,io");
}
Debugging RISC-V Code
Using GDB
The RISC-V toolchain includes GDB:
1
2
3
4
5
6
7
8
9
# In one terminal, start OpenOCD (if you have a debug probe)
openocd -f interface/cmsis-dap.cfg -f target/rp2350.cfg
# In another terminal, start GDB
riscv64-unknown-elf-gdb build/your_program.elf
(gdb) target remote localhost:3333
(gdb) load
(gdb) break main
(gdb) continue
Debug Prints
Add debug output to your code:
1
2
3
4
5
6
7
8
9
10
11
12
#ifdef DEBUG
#define DEBUG_PRINT(fmt, ...) printf("[DEBUG] " fmt, ##__VA_ARGS__)
#else
#define DEBUG_PRINT(fmt, ...)
#endif
int main() {
DEBUG_PRINT("Starting RISC-V program\n");
// Your code here
DEBUG_PRINT("Initialization complete\n");
}
Build with debugging:
1
cmake -DCMAKE_BUILD_TYPE=Debug ..
Makefile Template
Here’s a template CMakeLists.txt for RISC-V projects:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
cmake_minimum_required(VERSION 3.13)
# Set platform for RISC-V
set(PICO_PLATFORM rp2350-riscv)
include(pico_sdk_import.cmake)
project(my_riscv_project)
pico_sdk_init()
# Your source files
add_executable(my_riscv_project
main.c
# Add more source files here
)
# Link libraries
target_link_libraries(my_riscv_project
pico_stdlib
hardware_gpio
hardware_adc
pico_multicore
# Add more libraries as needed
)
# Enable USB serial output
pico_enable_stdio_usb(my_riscv_project 1)
pico_enable_stdio_uart(my_riscv_project 0)
# Generate UF2 file
pico_add_extra_outputs(my_riscv_project)
# Optional: Add compile definitions
target_compile_definitions(my_riscv_project PRIVATE
RISC_V_BUILD=1
)