RP2350 UART Driver
March 23, 2026 · View on GitHub
FREE Embedded Hacking Course HERE
VIDEO PROMO HERE
RP2350 UART Driver
An RP2350 UART driver written entirely in ARM Assembler.
Install ARM Toolchain (Windows / RP2350 Cortex-M33)
Official Raspberry Pi guidance for RP2350 ARM recommends the Arm GNU Toolchain from developer.arm.com.
Official References
Install (PowerShell)
$url = "https://developer.arm.com/-/media/Files/downloads/gnu/15.2.rel1/binrel/arm-gnu-toolchain-15.2.rel1-mingw-w64-x86_64-arm-none-eabi.zip"
$zipPath = "$env:TEMP\arm-toolchain-15-x64-win.zip"
$extractPath = "$env:TEMP\arm-extract"
$dest = "$HOME\arm-toolchain-15"
Invoke-WebRequest -Uri $url -OutFile $zipPath
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractPath -Force
Move-Item "$extractPath\arm-gnu-toolchain-*" $dest -Force
Get-ChildItem -Path $dest | Select-Object Name
Add Toolchain To User PATH (PowerShell)
$toolBin = "$HOME\arm-toolchain-15\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
arm-none-eabi-as --version
arm-none-eabi-ld --version
arm-none-eabi-objcopy --version
Build This Project
.\build.bat
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
Clean
.\clean.bat
Tutorial
Part I — Foundations (Chapters 1–6)
Chapter 1: What Is a Computer?
- Introduction
- The Fetch-Decode-Execute Cycle
- The Three Core Components
- Microcontroller vs Desktop Computer
- What Is RP2350?
- What Is ARM Cortex-M33?
- What Is Assembly Language?
- Why Learn Assembly?
- What We Will Build
- Summary
Chapter 2: Number Systems — Binary, Hexadecimal, and Decimal
- Introduction
- Decimal — Base 10
- Binary — Base 2
- Hexadecimal — Base 16
- The 0x Prefix
- Bit Numbering
- Common Bit Patterns in Our Firmware
- Two's Complement — Signed Numbers
- Data Sizes on ARM Cortex-M33
- Summary
Chapter 3: Memory — Addresses, Bytes, Words, and Endianness
- Introduction
- The Address Space
- Bytes, Halfwords, and Words
- Alignment
- Little-Endian Byte Order
- Memory-Mapped Registers
- The Stack
- Flash Memory (XIP)
- SRAM
- Reading the Address Map
- Summary
Chapter 4: What Is a Register?
- Introduction
- The ARM Cortex-M33 Register File
- Registers r0-r3: Arguments and Scratch
- Registers r4-r11: Callee-Saved
- Register r12 (IP): Intra-Procedure Scratch
- Register r13 (SP): Stack Pointer
- Register r14 (LR): Link Register
- Register r15 (PC): Program Counter
- Special Registers
- The Program Status Register (xPSR)
- Register Usage in Our Firmware
- Summary
Chapter 5: Load-Store Architecture — How ARM Accesses Memory
- Introduction
- Why Load-Store?
- The Load Instruction: ldr
- The Store Instruction: str
- The Load-Modify-Store Pattern
- Byte and Halfword Access
- Push and Pop
- Memory Access in Our Firmware
- Summary
Chapter 6: The Fetch-Decode-Execute Cycle in Detail
- Introduction
- The Three Stages
- The Pipeline
- A Concrete Example
- How Branch Instructions Affect the Pipeline
- The Cortex-M33 Execution Model
- Clock Speed
- Summary
Part II — The ARM Instruction Set (Chapters 7–12)
Chapter 7: ARM Cortex-M33 ISA Overview
- Introduction
- The ARM Design Philosophy
- Thumb-2 Instruction Encoding
- Instruction Categories
- Instruction Encoding Formats
- Instructions Used in Our Firmware
- Summary
Chapter 8: ARM Immediate and Move Instructions
- Introduction
- The mov Instruction
- The ldr Pseudo-Instruction
- The ldr Immediate Instruction
- Immediate Encoding in Thumb-2
- Constants in Our Firmware
- The add and sub Immediates
- Summary
Chapter 9: ARM Arithmetic and Logic Instructions
- Introduction
- Arithmetic Instructions
- Logic Instructions
- Shift Instructions
- The Suffix 's' — Flag Updates
- The Read-Modify-Write Pattern
- Summary
Chapter 10: ARM Memory Access Instructions — Load and Store Deep Dive
- Introduction
- ldr — Load Register
- str — Store Register
- push and pop — Stack Operations
- Addressing Modes
- Memory Access Sizes
- Memory Access in Hardware Configuration
- The Polling Loop Pattern
- Summary
Chapter 11: ARM Branch Instructions
- Introduction
- Unconditional Branch: b
- Branch with Link: bl
- Branch Exchange: bx
- Condition Flags
- Conditional Branches
- Branches Used in Our Firmware
- Polling Loops
- Branch Range
- Summary
Chapter 12: ARM Jumps, Calls, and Returns
- Introduction
- The bl Instruction — Function Call
- The bx lr Instruction — Function Return
- The Complete Call/Return Sequence
- The Problem: Nested Calls
- The Solution: push/pop
- Leaf Functions vs Non-Leaf Functions
- The Call Graph
- The Thumb Bit
- Summary
Part III — Assembly Programming (Chapters 13–17)
Chapter 13: Pseudo-Instructions — What the Assembler Does For You
- Introduction
- ldr r0, =value — Load Constant
- .equ — Define a Constant Symbol
- .include — Include Another File
- .global — Export a Symbol
- .type — Declare Symbol Type
- .size — Declare Symbol Size
- .word — Emit a 32-bit Constant
- .byte / .hword — Emit Smaller Constants
- Pseudo-Instructions vs Directives
- Summary
Chapter 14: Assembler Directives — Controlling the Assembly Process
- Introduction
- .syntax unified
- .cpu cortex-m33
- .thumb
- .section — Define Output Section
- .align — Alignment
- .equ — Define a Constant
- .global — Export Symbol
- .type — Symbol Type
- .size — Symbol Size
- .word — Emit 32-bit Value
- .byte / .hword — Emit Smaller Values
- .include — File Inclusion
- KEEP in Linker Context
- Summary of All Directives Used
- Summary
Chapter 15: The Calling Convention and Stack Frames
- Introduction
- The ARM AAPCS Calling Convention
- Caller-Saved Registers: r0-r3, r12
- Callee-Saved Registers: r4-r11
- The Stack Frame
- Function Prologue and Epilogue
- Leaf vs Non-Leaf Functions
- Our Firmware's Functions
- Summary
Chapter 16: Bitwise Operations for Hardware Programming
- Introduction
- The Bit Manipulation Toolkit
- Setting a Bit: orr
- Clearing a Bit: bic
- Testing a Bit: tst
- Isolating Bits: ands
- Masking Multiple Bits
- The Read-Modify-Write Pattern
- Shift for Bit Position
- Shifting Values into Position
- Clearing a Bit Field
- Summary
Chapter 17: Memory-Mapped I/O — Controlling Hardware Through Addresses
- Introduction
- How Memory-Mapped I/O Works
- Reading vs Writing Peripheral Registers
- The RP2350 Peripheral Address Map
- UART0 Register Map
- Volatile Behavior
- Atomic Aliases
- Why Not Use Special I/O Instructions?
- Barriers: dsb and isb
- Summary
Part IV — RP2350 Hardware (Chapter 18)
Chapter 18: The RP2350 Microcontroller — Architecture and Hardware
- Introduction
- RP2350 Block Diagram
- The ARM Cortex-M33 Core
- Memory Map
- The Bus Fabric
- The Reset Controller
- The Clock System
- The XOSC (Crystal Oscillator)
- UART0 Peripheral
- GPIO and Pin Multiplexing
- The Coprocessor Interface
- The Boot Process
- The Vector Table
- Summary
Part V — Build System (Chapters 19–20)
Chapter 19: The Linker Script — Placing Code in Memory
Chapter 20: The Build Pipeline — From Assembly to UF2
- Introduction
- Our Build Script: build.bat
- Stage 1: Assembly
- Stage 2: Linking
- Stage 3: Binary Extraction
- Stage 4: UF2 Conversion
- The Complete Flow
- Cleaning
- The Family ID: 0xe48bff59
- Summary
Part VI — Source Code Walkthroughs (Chapters 21–29)
Chapter 21: image_def.s — The PICOBIN Block Byte by Byte
- Introduction
- Full Source: image_def.s
- Line-by-Line Walkthrough
- The Complete Block in Memory
- Why Secure Mode?
- Summary
Chapter 22: constants.s — Every .equ Definition Explained
- Introduction
- Full Source: constants.s
- Line-by-Line Walkthrough
- How .equ Works
- The .include Mechanism
- Summary
Chapter 23: stack.s and vector_table.s — Stack Initialization and the Vector Table
Chapter 24: reset_handler.s — The Boot Sequence Line by Line
- Introduction
- Full Source: reset_handler.s
- Line-by-Line Walkthrough
- The Boot Sequence Diagram
- Why Order Matters
- Summary
Chapter 25: xosc.s — Crystal Oscillator Initialization
- Introduction
- Full Source: xosc.s
- Function 1: Init_XOSC
- Function 2: Enable_XOSC_Peri_Clock
- Clock Path Diagram
- Why XOSC Matters for UART
- Summary
Chapter 26: reset.s — Releasing IO_BANK0 from Reset
- Introduction
- Full Source: reset.s
- Line-by-Line Walkthrough
- The RP2350 Reset Controller
- Why Not Release Everything at Once?
- The Atomic Clear Alternative
- Summary
Chapter 27: uart.s Part 1 — UART_Release_Reset and UART_Init
Chapter 28: uart.s Part 2 — UART0_Out and UART0_In
- Introduction
- Function 1: UART0_Out (Transmit)
- Function 2: UART0_In (Receive)
- Register Usage Comparison
- The Polling Pattern
- Data Flow Through the UART
- Summary
Chapter 29: main.s — The Echo Loop
- Introduction
- Full Source: main.s
- Line-by-Line Walkthrough
- The Complete Execution Flow
- What Makes This a Complete Firmware
- Summary
Part VII — Full Integration (Chapter 30)
Chapter 30: Full Integration — From Power-On to Echo
- Introduction
- The Files
- Phase 1: Build
- Phase 2: Flash
- Phase 3: Boot ROM
- Phase 4: Hardware Reset Sequence
- Phase 5: Reset_Handler
- Phase 6: The Echo Loop (main.s)
- The Complete Address Map
- What We Built
main.s Code
/**
* FILE: main.s
*
* DESCRIPTION:
* RP2350 Bare-Metal UART Main Application.
*
* BRIEF:
* Main application entry point for RP2350 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
*/
.syntax unified // use unified assembly syntax
.cpu cortex-m33 // target Cortex-M33 core
.thumb // use Thumb instruction set
.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:
.Push_Registers:
push {r4-r12, lr} // push registers r4-r12, lr to the stack
.Loop:
bl UART0_In // call UART0_In
bl UART0_Out // call UART0_Out
b .Loop // loop forever
.Pop_Registers:
pop {r4-r12, lr} // pop registers r4-r12, lr from the stack
bx lr // 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