Bare metal gpio twiddling for RISC-V on rpi pico2
Posted by Jim Morris on Sun Mar 23 08:16:32 -0700 2025
My little project this week was to try to a get bare metal RISC-V assembler coded GPIO twiddler working on a pi pico2. (Actually PICO2-W)
With little to no useful help from chatgpt 4o, I finally managed to get something working although it appears a regular daplink probe is a little flaky with the pico and openocd, maybe a pi pico programmed with daplink would be better?
It turns out the pico2 needs a little more than the pico to get a simple gpio to work, so reading the manual was required.
I'm not sure what advantage using the RISC-V in the pico is over the arm, it is certainly less well documented and supported, but it did eventually work, and the reduced instruction set is certainly easier to remember than arm assembly code.
I'm not sure I'll bother with RISC-V for the moment though as it is fiddly to get working.
For anyone trying to get this working (and googling did not yield much) here are some pointers...
I had to fetch the RISC-V toolchain from here... https://embecosm.com/downloads/tool-chain-downloads/#core-v-top-of-tree-compilers which is the recommended one.
The build parameters are...
riscv32-corev-elf-as -g -march=rv32imac -mabi=ilp32 -o startup.o startup.s
riscv32-corev-elf-ld -g -m elf32lriscv -T linker.ld -o blink.elf startup.o
riscv32-corev-elf-objcopy -O binary blink.elf blink.bin
riscv32-corev-elf-objdump -f blink.elf
The linker.ld I used was...
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
}
The pico2 refence guide is quite good although somewhat hard to find details although they are there scattered around. Also the current version of pico-sdk does support RISC-V but at the c level not assembler.
You need to add a secret block of numbers to your code to get the bootloader to recognize a RISC-V program and get it to switch to the riscv cores properly (well not so secret, it is documented although poorly).
The block I used was:
.word 0xffffded3
.word 0x11010142
.word 0x00000344
.word _start
.word _stack_top
.word 0x000004ff
.word 0x00000000
.word 0xab123579
This sets it to a RISC-V executable and tells it where to start the execution from.
You need to clear a bit in the PAD_ISO_REG
to get GPIO to work in addition
to the other normal settings you would use on a regualr pico, oh also you
need to set the IOMUX_BASE to function 5 for a regular gpio pin (offset for
the actual GPIO you are using), again not needed in a pico.
Here is the full assembly code for toggling GPIO15 on a pipco2 W (I used GPIO15 becuase the PICO2-W does not have the led on a regular GPIO pin)
.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 # ✅ **Corrected: 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 recognised as a valid RISC-V binary.
# -----------------------------------------------------------------------------
.word 0xffffded3
.word 0x11010142
.word 0x00000344
.word _start
.word _stack_top
.word 0x000004ff
.word 0x00000000
.word 0xab123579
.section .text
.global main
main:
# 1 Enable clock for GPIO peripheral (NOT NEEDED)
; li t0, CLK_EN_REG
; lw t1, 0(t0) # Read current clock enable register
; li t2, 0x00000020 # Enable GPIO clock (check RP2350 datasheet)
; or t1, t1, t2
; sw t1, 0(t0) # Write back to enable GPIO clock
# 2 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
# 3 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
# 4 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:
# 5 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
call delay
# 6 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
call delay
j loop
delay:
li t0, 500000 # Simple delay loop
1:
addi t0, t0, -1
bnez t0, 1b
ret
FWIW I did ask chatgpt to write it, but it got 80% of it wrong :) this is the code it gave me that I had to mostly correct. It got most of the register addresses wrong, and missed many of the steps until I corrected it, it also got some of the assembly wrong, and totally missed the chunk of data needed for the bootloader to work. Anyway it kept me busy for a while, and was an interesting exercise :)