Post

Raspberry Pi Pico 2 - Deep Dive into RISC-V Development

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
)
This post is licensed under CC BY 4.0 by the author.