Microcontroller projects

RISC-V assembler with Raspberry Pi Pico 2

last updated: 2025-10-17

Work in progress

Intro

The latest political signals from China (pact with dictators Putin and Kim Jong-un) lead me to believe that it is time to boycott as far as possible Chinese microcontrollers. America is no longer the friend to Europe, with M. Trump as president. American Tech Giants get too greedy.

It is time to change. Linux as OS and RISC-V for our microcontroller. And as far as possible no more chinese or american products.

The new Raspberry Pi Pico 2 processor was designed in Europe and has beneath ARM cores 2 RISC-V (Hazard3, RV32IMAC+) cores. So let's use them.

And here we will not use the SDK but get back to the real basics, It does have to hurt!

Please look here: https://www.weigu.lu/microcontroller/pico_risc-v/index.html to create the udev rules and install openOCD to program the Pico 2 with a Debugprobe.

Installing the RISC-V toolchain

We need a 32 bit toolchain for the Hazard3 core on the Raspberry Pi Pico 2 (RP2350).

The ISA supported by Hazard3 is rv32imac ( The easiest way is to use a pre-built toolchain from the jenkins server of embecosm.com. We download it in our own directory and extract the file:

mkdir -p ~/pi_pico2/risc_v_gcc
cd ~/pi_pico2/risc_v_gcc
wget https://buildbot.embecosm.com/job/corev-gcc-ubuntu2204/83/artifact/corev-openhw-gcc-ubuntu2204-20250302.tar.gz
tar -xzf corev-openhw-gcc-ubuntu2204-20250302.tar.gz

Open a new terminal and test the assembler with:

cd ~/pi_pico2/risc_v_gcc/corev-openhw-gcc-ubuntu2204-20250302/bin/
riscv32-corev-elf-gcc --version

Add the following line to your ~/.bashrc file:

export PATH="$PATH:/home/weigu/pi_pico2/risc_v_gcc/corev-openhw-gcc-ubuntu2204-20250302/bin"

Let`s blink the LED (RISC-V)

I was cool to use the blink example, but the C code is big (because it is also valid for a Pico2 W, that uses another pin for the LED).

Let's try with our own minimal code. We use GPIO15 on the pico2/pico2 w:

.section .text
.global _start

#.equ SYSCTL_BASE,    0x40000000
#.equ CLK_EN_REG,     SYSCTL_BASE + 0x100   # Clock enable register

.equ PAD_ISO_REG,    0x40038000 + 0x40     # Pad isolation control register for GPIO15

.equ IOMUX_BASE,     0x40028000
.equ IOMUX_GPIO15,   IOMUX_BASE + 0x7C     # IOMUX register for GPIO15

.equ SIO_BASE,       0xD0000000
.equ GPIO_OUT_REG,   SIO_BASE + 0x10       # GPIO output register
.equ GPIO_OUT_SET,   SIO_BASE + 0x14       # GPIO output set register
.equ GPIO_OUT_CLR,   SIO_BASE + 0x18       # GPIO output clear register
.equ GPIO_DIR_REG,   SIO_BASE + 0x30       # GPIO direction register

.equ GPIO15_MASK,    (1 << 15)             # Bitmask for GPIO15

_start:
    la sp, _stack_top   # Load stack pointer
    call main           # Call main
    wfi                 # Wait for interrupt (to save power)
    j _start            # Loop forever (should never reach here)

# -----------------------------------------------------------------------------
.p2align 8 # This special signature must appear within the first 4 kb of
image_def: # the memory image to be recognized as a valid RISC-V binary.
           # RP2350 data sheet: 5.9.1. Blocks and block loops
           # 5.9.3. Image definition items, 5.9.3.4. ENTRY_POINT item
# -----------------------------------------------------------------------------

.word 0xffffded3 # Header
.word 0x11010142 # Item 0 flags: 0b0001 0001 0000 0001
                 # IMAGE_TYPE_EXE EXE_SECURITY_UNSPECIFIED EXE_CPU_RISC EXE_CHIP_RP2350
.word 0x00000344 # Item 1 0x44: PICOBIN_BLOCK_ITEM_1BS_ENTRY_POINT block size = 3
.word _start     # Item 2 Inital PC (runtime) address (aka entry point)
.word _stack_top # Item 3 Initial SP address (aka stack pointer)
.word 0x000004ff # Item 4 Optional SP limit address (aka stack limit)
.word 0x00000000 # Link single block loop has 0
.word 0xab123579 # Footer

.section .text
.global main

main:
    # Setup
    # Configure IOMUX for GPIO15
    li t0, IOMUX_GPIO15
    lw t1, 0(t0)
    li t2, ~0x1F
    and t1, t1, t2   # clear it first
    ori t1, t1, 5        # Function 5 selects GPIO mode
    sw t1, 0(t0)         # Set IOMUX for GPIO15

    # Set GPIO15 as an output
    li t0, GPIO_DIR_REG
    lw t1, 0(t0)         # Read current GPIO direction register
    li t2, GPIO15_MASK
    or t1, t1, t2
    sw t1, 0(t0)         # Set GPIO15 as output

    # Clear Pad Isolation for GPIO
    li t0, PAD_ISO_REG
    lw t1, 0(t0)
    li t2, ~0x100
    and t1, t1, t2  # Clear GPIO15 isolation bit
    sw t1, 0(t0)

loop:
    # Turn LED ON (Use SIO fast register access)
    li t0, GPIO_OUT_REG
    lw t1, 0(t0)        # Read current GPIO_OUT
    li t2, GPIO15_MASK
    or t1, t1, t2
    sw t1, 0(t0)        # Set GPIO15 high

    # Turn LED OFF
    li t0, GPIO_OUT_REG
    lw t1, 0(t0)
    li t2, ~GPIO15_MASK
    and t1, t1, t2
    sw t1, 0(t0)        # Set GPIO15 low

    j loop

Create a directory:

mkdir -p ~/pi_pico2/pico2_riscv_ass/blink
cd ~/pi_pico2/pico2_riscv_ass/blink
nano blink.S

Copy and paste the code and save the file (Ctrl+o, Ctrl+x).

Next we create a linker.ld file:

OUTPUT_ARCH(riscv)
ENTRY(_start)
MEMORY {
    FLASH (rx)  : ORIGIN = 0x10000000, LENGTH = 2M
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 256K
}
SECTIONS {
    .text : {
        *(.text*)
    } > FLASH

    .data : {
        *(.data*)
    } > RAM

    .bss : {
        *(.bss*)
    } > RAM

    .stack (NOLOAD) : {
        . = ALIGN(8);
        _stack_top = . + 4K; /* 4KB stack */
    } > RAM
}
nano linker.ld

Copy and paste the code and save the file (Ctrl+o, Ctrl+x).

Next we save the following script lines as bash script:

#!/bin/bash
if [ "$#" -ne 1 ]; then
    printf 'ERROR! You must provide one and only one argument!\n' >&2
    exit 1
fi
riscv32-corev-elf-as -g -march=rv32imac -mabi=ilp32 -o $1.o $1.s
riscv32-corev-elf-ld -g -m elf32lriscv -T linker.ld -o $1.elf $1.o
riscv32-corev-elf-objcopy -O binary $1.elf $1.bin
riscv32-corev-elf-objdump -f $1.elf
nano ass_link_bin.sh

Copy and paste the code and save the file (Ctrl+o, Ctrl+x).

Program with openOCD and Debugprobe

cd ~/pi_pico2/pico2_riscv_ass/blink
openocd -f interface/cmsis-dap.cfg -f target/rp2350-riscv.cfg -c "adapter speed 5000" \
-c "program blink.elf verify reset exit"

.p2align 8: This is a GNU assembler directive that aligns the following code or data to an (8)-byte boundary. The "p2" stands for "power of 2" and 8 indicates the alignment is (2^{8}), or (256) bytes. This ensures that the subsequent code is placed at a specific address that is a multiple of (256), which can improve performance on some processors by ensuring that instructions or data are aligned on cache lines or other memory boundaries. # This special signature must appear within the first 4 kb of: This is a comment, ignored by the assembler, that provides important context about the following line. It explains that the signature is crucial for the binary to be recognized. imagedef:: This is a label that defines the start of the signature. In the context of RISC-V, this is part of a larger header that the bootloader or operating system uses to identify the image as a valid RISC-V binary. # the memory image to be recognized as a valid RISC-V binary.: This comment further clarifies that the combination of the alignment and imagedef: is the "signature" that the RISC-V boot process looks for to confirm the file is a valid executable binary. The requirement to be within the first (4) kilobytes is a common practice for embedded systems and bootloaders, as they often scan the beginning of a bootable device for these headers.

Downloads

Interesting links

http://blog.wolfman.com/articles/2025/5/19/bare-metal-gpio-twiddling-for-risc-v-on-rpi-pico2

https://github.com/wolfmanjm/RISC-V-RP2350-baremetal

https://mcyoung.xyz/2021/11/29/assembly-1/

https://smist08.wordpress.com/2024/10/21/risc-v-on-the-raspberry-pi-pico-2/