RP2350 UART Driver RISC-V
March 23, 2026 · View on GitHub
FREE Embedded Hacking Course HERE
VIDEO PROMO HERE
RP2350 UART Driver RISC-V
An RP2350 UART driver written entirely in RISC-V Assembler.
Install RISC-V Toolchain (Windows / RP2350 Hazard3)
Official Raspberry Pi guidance for RP2350 RISC-V points to pico-sdk-tools prebuilt releases.
Official References
Install (PowerShell)
$url = "https://github.com/raspberrypi/pico-sdk-tools/releases/download/v2.0.0-5/riscv-toolchain-14-x64-win.zip"
$zipPath = "$env:TEMP\riscv-toolchain-14-x64-win.zip"
$dest = "$HOME\riscv-toolchain-14"
Invoke-WebRequest -Uri $url -OutFile $zipPath
New-Item -ItemType Directory -Path $dest -Force | Out-Null
Expand-Archive -LiteralPath $zipPath -DestinationPath $dest -Force
Get-ChildItem -Path $dest | Select-Object Name
Add Toolchain To User PATH (PowerShell)
$toolBin = "$HOME\riscv-toolchain-14\bin"
$currentUserPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($currentUserPath -notlike "*$toolBin*") {
[Environment]::SetEnvironmentVariable("Path", "$currentUserPath;$toolBin", "User")
}
Close and reopen your terminal after updating PATH.
Verify Toolchain
riscv32-unknown-elf-as --version
riscv32-unknown-elf-ld --version
riscv32-unknown-elf-objcopy --version
Build This Project
.\build.bat
If your toolchain uses a different prefix, pass it explicitly:
.\build.bat riscv-none-elf
UART Terminal Setup (PuTTY)
- Speed: 115200
- Data bits: 8
- Stop bits: 1
- Parity: None
- Flow control: None
UART Wiring (Pico 2 Target)
- GP0 = UART0 TX (target output)
- GP1 = UART0 RX (target input)
- GND must be common between target and USB-UART adapter/debug probe
- Cross wiring is required: adapter TX -> GP1, adapter RX -> GP0
Hardware
Raspberry Pi Pico 2 w/ Header BUY
USB A-Male to USB Micro-B Cable BUY
Raspberry Pi Pico Debug Probe BUY
Complete Component Kit for Raspberry Pi BUY
10pc 25v 1000uF Capacitor BUY
10% PiShop DISCOUNT CODE - KVPE_HS320548_10PC
Build
.\build.bat
Optional Toolchain Prefix Override
.\build.bat riscv-none-elf
Clean
.\clean.bat
Tutorial
A comprehensive 30-chapter technical book teaching RP2350 RISC-V assembly from absolute scratch. Every line of assembler is explained.
Foundations
Chapter 1: What Is a Computer?
- The Fetch-Decode-Execute Cycle
- The Three Core Components
- Microcontroller vs Desktop Computer
- What Is RP2350?
- What Is RISC-V?
- Why Assembly Language?
- What We Are Building
Chapter 2: Number Systems — Binary, Hexadecimal, and Decimal
Chapter 3: Memory — Addresses, Bytes, Words, and Endianness
- The Byte-Addressable Model
- Words and Alignment
- Endianness
- Memory Regions on RP2350
- Memory-Mapped I/O
- Address Arithmetic
Chapter 4: What Is a Register?
- Why Registers?
- The RISC-V Register File
- Register x0: The Hardwired Zero
- ABI Register Names
- Visualizing Registers
Chapter 5: Load-Store Architecture — How RISC-V Accesses Memory
- The Load-Store Principle
- Why Load-Store?
- RISC-V Load Instructions
- RISC-V Store Instructions
- Base + Offset Addressing
- The Memory Bus
Chapter 6: The Fetch-Decode-Execute Cycle in Detail
- The Cycle Step by Step
- Pipeline Concept
- Tracing Through Our Firmware
- The Program Counter is Everything
RISC-V Instruction Set
Chapter 7: RISC-V ISA Overview
- The RISC-V Design Philosophy
- Our ISA String: rv32imac_zicsr
- Instruction Encoding Summary
- How This Maps to Our Firmware
Chapter 8: RISC-V Immediate and Upper-Immediate Instructions
- What Is an Immediate?
- I-Type Immediates (12-bit Signed)
- Shift Immediates
- U-Type Instructions: LUI and AUIPC
- Building 32-bit Constants: LUI + ADDI
- The LI Pseudoinstruction
- LA: Load Address
Chapter 9: RISC-V Arithmetic and Logic Instructions
- R-Type Format Recap
- Addition and Subtraction
- Logical Operations
- Shift Operations
- Comparison Instructions
- MUL from M Extension
- Read-Modify-Write Pattern
Chapter 10: RISC-V Memory Access Instructions — Load and Store Deep Dive
- Load Instruction Family
- Store Instruction Family
- Why Our Firmware Uses Only LW and SW
- Offset Encoding Constraints
- Complete Memory Access Map for Our Firmware
Chapter 11: RISC-V Branch Instructions
- How Branches Work
- B-Type Encoding
- The Six Branch Instructions
- Signed vs Unsigned Comparison
- Branches in Our Firmware
- Local Labels
- Branch Range Limitation
- No Flags Register
Chapter 12: RISC-V Jumps, Calls, and Returns
- JAL: Jump and Link
- JALR: Jump and Link Register
- CALL Pseudoinstruction
- RET Pseudoinstruction
- TAIL Pseudoinstruction
- The Complete Call Chain in Our Firmware
- Infinite Loops
- Nested Calls and the Stack
Assembly Programming
Chapter 13: Pseudoinstructions — What the Assembler Does For You
- What Is a Pseudoinstruction?
- Complete Pseudoinstruction Reference
- Why Pseudoinstructions Matter
- How to Tell if Something Is a Pseudoinstruction
Chapter 14: Assembler Directives — Controlling the Assembly Process
- Sections
- Symbol Visibility
- Alignment
- Data Embedding
- Constant Definitions
- File Inclusion
- Labels
- Putting It All Together
Chapter 15: The Calling Convention and Stack Frames
- The RISC-V ilp32 Calling Convention
- The Stack
- Stack Frame Layout
- Function Types in Our Firmware
- How Arguments Flow in Our Firmware
- Caller-Saved in Action
Chapter 16: Bitwise Operations for Hardware Programming
- Bit Numbering
- The Four Fundamental Bit Operations
- The Read-Modify-Write Pattern
- Multi-Bit Fields
- Bit Testing: The BGEZ Trick
- Constants in Our Firmware
- Common Bit Patterns Summary
Hardware Concepts
Chapter 17: Memory-Mapped I/O — Controlling Hardware Through Addresses
- The Principle
- RP2350 Address Space Map
- Peripheral Register Structure
- Register Types
- Volatility
- Why Order Matters
- The PPB (Private Peripheral Bus)
- Atomic Access Concerns
Chapter 18: The RP2350 Microcontroller — Architecture and Hardware
- RP2350 Block Diagram
- The Hazard3 RISC-V Core
- Memory System
- Reset Infrastructure
- Clock Infrastructure
- GPIO System
- UART Hardware
- Boot Sequence (High Level)
- What Our Firmware Must Do
Build System
Chapter 19: The Linker Script — Placing Code in Memory
Chapter 20: The Build Pipeline — From Assembly to UF2
Source Code Walkthroughs
Chapter 21: image_def.s — Boot Metadata Line by Line
- Section and Alignment
- Start Marker
- Image Type Item
- Entry Point Item
- Last Item Marker
- Block Loop Pointer
- End Marker
- Complete Binary Dump
- Boot ROM Sequence
Chapter 22: constants.s — Every Definition Explained
- Stack Constants
- Crystal Oscillator Constants
- System Constants
- Clock Constants
- Reset Controller Constants
- GPIO Constants
- UART Constants
- How Constants Are Used
Chapter 23: stack.s and vector_table.s — Line by Line
Chapter 24: reset_handler.s — The Boot Sequence Line by Line
Chapter 25: xosc.s — Crystal Oscillator Initialization Line by Line
- Function 1: Init_XOSC
- Function 2: Enable_XOSC_Peri_Clock
- Register Usage Summary
- Read-Modify-Write Pattern
Chapter 26: reset.s — Releasing IO_BANK0 from Reset
- Background: The Reset Controller
- Line-by-Line Walkthrough
- The Bit-Clear Pattern in Detail
- The Polling Pattern
Chapter 27: uart.s Part 1 — Release Reset and Initialization
Chapter 28: uart.s Part 2 — Transmit and Receive
- Function 1: UART0_Out — Blocking Transmit
- Function 2: UART0_In — Blocking Receive
- The Echo Loop
- The UARTDR Register: Dual-Purpose
- Potential Issues
Chapter 29: main.s — The Application Entry Point
- The .text Section
- The Echo Loop
- The Data Sections
- Register Usage Throughout the Loop
- Why This Code Is Minimal
- Complete Execution Timeline
Integration
Chapter 30: Full Integration — Build, Flash, Wire, and Test
- Part 1: The Project Structure
- Part 2: The Build Pipeline
- Part 3: The Memory Map After Build
- Part 4: Hardware Setup
- Part 5: Flashing the Firmware
- Part 6: Testing with a Terminal
- Part 7: The Complete Boot Flow
- Part 8: Debugging Tips
- Part 9: What You Have Learned
- Part 10: Where to Go Next
main.s Code
/**
* FILE: main.s
*
* DESCRIPTION:
* RP2350 Bare-Metal UART Main Application (RISC-V).
*
* BRIEF:
* Main application entry point for RP2350 RISC-V UART driver. Contains the
* main loop that echoes UART input to output.
*
* AUTHOR: Kevin Thomas
* CREATION DATE: November 2, 2025
* UPDATE DATE: November 27, 2025
*/
.include "constants.s"
/**
* Initialize the .text section.
* The .text section contains executable code.
*/
.section .text # code section
.align 2 # align to 4-byte boundary
/**
* @brief Main application entry point.
*
* @details Implements the infinite blink loop.
*
* @param None
* @retval None
*/
.global main # export main
.type main, @function # mark as function
main:
.Loop:
call UART0_In # call UART0_In
call UART0_Out # call UART0_Out
j .Loop # loop forever
ret # return to caller
/**
* Test data and constants.
* The .rodata section is used for constants and static data.
*/
.section .rodata # read-only data section
/**
* Initialized global data.
* The .data section is used for initialized global or static variables.
*/
.section .data # data section
/**
* Uninitialized global data.
* The .bss section is used for uninitialized global or static variables.
*/
.section .bss # BSS section