pico_stdlib — Calling SDK Functions from RISC-V Assembly

This document shows how to call Pico SDK functions from RISC-V Assembly code, following the RISC-V ABI (Application Binary Interface). Each function includes its C signature, the argument-to-register mapping, and an annotated assembly example.

RISC-V ABI Conventions — Summary

Registers ABI Name Role in function calls Preserved?
x10–x17 a0–a7 Function arguments; a0 (and a1) also hold the return value No (caller-saved)
x1 ra Return address — overwritten by call; must be saved to the stack if the function makes further calls No (caller-saved)
x5–x7, x28–x31 t0–t6 Temporaries — the called function may freely overwrite them No (caller-saved)
x8–x9, x18–x27 s0–s11 Saved registers — the called function must restore them before returning Yes (callee-saved)
x2 sp Stack pointer — must be 16-byte aligned at the point of call Yes (callee-saved)
Golden rule: before calling any function with call, place the arguments in a0, a1, a2… (in that order). After the function returns, the result is in a0 (and a1 for 64-bit values). Any value held in t0–t6 or a0–a7 before the call may have been destroyed — save them to the stack if you need them afterwards.

Function Prologue and Epilogue (Stack Frame)

Whenever an assembly function calls other functions, it must save ra (and any s registers it uses) onto the stack.

my_function:
    # ── Prologue ─────────────────────────────────────────
    addi  sp, sp, -16       # allocate 16 bytes on the stack (multiple of 16 = ABI compliant)
    sw    ra,  12(sp)       # save return address
    sw    s0,   8(sp)       # save s0 (frame pointer, if used)
    sw    s1,   4(sp)       # save s1 (if used by this function)
    # ── Function body ────────────────────────────────────
    # ... your instructions here ...
    # ── Epilogue ─────────────────────────────────────────
    lw    s1,   4(sp)       # restore s1
    lw    s0,   8(sp)       # restore s0
    lw    ra,  12(sp)       # restore return address
    addi  sp, sp,  16       # deallocate stack frame
    ret                     # return (equivalent to: jalr zero, ra, 0)
    
Why use s registers instead of t for loop variables?
t registers are caller-saved: any call may destroy them. s registers are callee-saved: the called function guarantees it restores them. Therefore, values that must survive a call (such as the SIO base address or a bitmask used in a loop) should be kept in s0–s11.

hardware_gpio 🔗 docs

gpio_init

C Signature void gpio_init(uint gpio)
ArgumentRegisterTypeDescription
gpioa0uintGPIO pin number (0–29)
ReturnvoidNo return value
    li    a0, 15            # a0 = GPIO 15 (1st argument)
    call  gpio_init         # calls gpio_init(15)
                            # void return — a0 has no useful value after
    

gpio_set_dir

C Signature void gpio_set_dir(uint gpio, bool out)
ArgumentRegisterTypeDescription
gpioa0uintGPIO pin number
outa1bool1 = output, 0 = input
ReturnvoidNo return value
    li    a0, 15            # a0 = GPIO 15
    li    a1, 1             # a1 = 1 (output)
    call  gpio_set_dir      # calls gpio_set_dir(15, true)
    

gpio_put

C Signature void gpio_put(uint gpio, bool value)
ArgumentRegisterTypeDescription
gpioa0uintGPIO pin number
valuea1bool1 = high (LED on), 0 = low (LED off)
ReturnvoidNo return value
    # Turn LED on
    li    a0, 15            # a0 = GPIO 15
    li    a1, 1             # a1 = 1 (high)
    call  gpio_put          # calls gpio_put(15, 1)

    # Turn LED off
    li    a0, 15            # a0 = GPIO 15
    li    a1, 0             # a1 = 0 (low)
    call  gpio_put          # calls gpio_put(15, 0)
    

gpio_get

C Signature bool gpio_get(uint gpio)
ArgumentRegisterTypeDescription
gpioa0uintGPIO pin number
Returna0bool1 if pin is high, 0 if pin is low
    li    a0, 15            # a0 = GPIO 15
    call  gpio_get          # calls gpio_get(15)
                            # after return: a0 = 0 or 1
    bnez  a0, pin_high      # branch if a0 != 0 (pin is high)
    

gpio_pull_up / gpio_pull_down

C Signature void gpio_pull_up(uint gpio)  /  void gpio_pull_down(uint gpio)
ArgumentRegisterTypeDescription
gpioa0uintGPIO pin number
ReturnvoidNo return value
    li    a0, 14            # a0 = GPIO 14 (e.g. a button)
    call  gpio_pull_up      # enables internal pull-up resistor on GPIO 14
    

gpio_set_function

C Signature void gpio_set_function(uint gpio, gpio_function_t fn)
ArgumentRegisterTypeDescription
gpioa0uintGPIO pin number
fna1enumFunction: GPIO_FUNC_SIO=5, GPIO_FUNC_PWM=4, GPIO_FUNC_I2C=3, GPIO_FUNC_SPI=1, GPIO_FUNC_UART=2
ReturnvoidNo return value
    li    a0, 0             # a0 = GPIO 0 (UART0 TX)
    li    a1, 2             # a1 = GPIO_FUNC_UART (value 2)
    call  gpio_set_function # configures GPIO 0 for UART function
    

pico_time 🔗 docs

sleep_ms

C Signature void sleep_ms(uint32_t ms)
ArgumentRegisterTypeDescription
msa0uint32_tDelay time in milliseconds
ReturnvoidNo return value
    li    a0, 250           # a0 = 250 ms
    call  sleep_ms          # waits 250 milliseconds
    

sleep_us  ⚠ 64-bit argument — special attention required!

C Signature void sleep_us(uint64_t us)
ArgumentRegisterTypeDescription
us (bits 31:0)a0uint32_tLower 32 bits of the microsecond value
us (bits 63:32)a1uint32_tUpper 32 bits (typically 0 for small values)
ReturnvoidNo return value
RISC-V 32-bit ABI: 64-bit values are passed in two consecutive registers (a0 = lower half, a1 = upper half). For values up to ~4 billion µs, the upper half is zero.
    li    a0, 500           # a0 = lower half: 500 µs
    li    a1, 0             # a1 = upper half: 0 (value < 2^32)
    call  sleep_us          # waits 500 microseconds
    

hardware_uart 🔗 docs

uart_init

C Signature uint uart_init(uart_inst_t *uart, uint baudrate)
ArgumentRegisterTypeDescription
uarta0pointerInstance: uart0 = address 0x40034000, uart1 = 0x40038000
baudratea1uintBaud rate (e.g. 115200)
Returna0uintActual baud rate configured by the hardware (may differ slightly)
    li    a0, 0x40034000    # a0 = uart0 (peripheral base address)
    li    a1, 115200        # a1 = baud rate
    call  uart_init         # calls uart_init(uart0, 115200)
                            # a0 = actual baud rate set by hardware
    

uart_putc

C Signature void uart_putc(uart_inst_t *uart, char c)
ArgumentRegisterTypeDescription
uarta0pointerUART instance
ca1charCharacter to send (ASCII code)
ReturnvoidNo return value
    li    a0, 0x40034000    # a0 = uart0
    li    a1, 'A'           # a1 = ASCII code of 'A' (65)
    call  uart_putc         # sends character 'A' over UART0
    

uart_getc

C Signature char uart_getc(uart_inst_t *uart)
ArgumentRegisterTypeDescription
uarta0pointerUART instance (blocks until a byte is received)
Returna0charReceived character (ASCII code in a0)
    li    a0, 0x40034000    # a0 = uart0
    call  uart_getc         # waits for and reads one byte (blocking)
                            # after return: a0 = ASCII code of received byte
    mv    s0, a0            # save in s0 (callee-saved) to survive subsequent calls
    

Complete Example — Blink using gpio_put and sleep_ms

This example shows how to combine multiple SDK calls in a loop while correctly preserving s registers across calls.

.equ LED_PIN,   15
.equ DELAY_MS, 250

main:
    # ── Prologue ─────────────────────────────────────────────
    addi  sp, sp, -16
    sw    ra, 12(sp)        # save ra (will be overwritten by calls)
    sw    s0,  8(sp)        # save s0 (we will use it to hold LED_PIN)

    # ── Initialization ───────────────────────────────────────
    li    a0, LED_PIN
    call  gpio_init         # gpio_init(LED_PIN)

    li    a0, LED_PIN
    li    a1, 1             # 1 = output
    call  gpio_set_dir      # gpio_set_dir(LED_PIN, true)

    li    s0, LED_PIN       # s0 = LED_PIN — callee-saved: survives calls inside the loop

loop:
    # Turn LED on
    mv    a0, s0            # a0 = LED_PIN (read from s0, not li — s0 survived)
    li    a1, 1
    call  gpio_put          # gpio_put(LED_PIN, 1)

    li    a0, DELAY_MS
    call  sleep_ms          # sleep_ms(250) — a0 destroyed, but s0 is intact

    # Turn LED off
    mv    a0, s0            # a0 = LED_PIN (s0 still valid)
    li    a1, 0
    call  gpio_put          # gpio_put(LED_PIN, 0)

    li    a0, DELAY_MS
    call  sleep_ms          # sleep_ms(250)

    j     loop              # repeat forever

    # (epilogue never reached in this example, but good practice to include)
    lw    s0,  8(sp)
    lw    ra, 12(sp)
    addi  sp, sp,  16
    ret
    
Why mv a0, s0 instead of li a0, LED_PIN inside the loop?
Both work in this case. However, using s0 demonstrates the correct pattern: across a loop with multiple call instructions, values in a and t registers are destroyed. Storing values in s registers is the ABI-correct way to keep them alive across calls.

Official documentation: