FREE Embedded Hacking Course HERE
VIDEO PROMO HERE
An RP2350 UART driver written entirely in ARM Assembler.
Official Raspberry Pi guidance for RP2350 ARM recommends the Arm GNU Toolchain from developer.arm.com.
$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$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.
arm-none-eabi-as --version
arm-none-eabi-ld --version
arm-none-eabi-objcopy --version.\build.bat- Speed: 115200
- Data bits: 8
- Stop bits: 1
- Parity: None
- Flow control: None
- 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
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
.\build.bat
.\clean.bat
- 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
- 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
- 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
- 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
- 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
- Introduction
- The Three Stages
- The Pipeline
- A Concrete Example
- How Branch Instructions Affect the Pipeline
- The Cortex-M33 Execution Model
- Clock Speed
- Summary
- Introduction
- The ARM Design Philosophy
- Thumb-2 Instruction Encoding
- Instruction Categories
- Instruction Encoding Formats
- Instructions Used in Our Firmware
- Summary
- 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
- Introduction
- Arithmetic Instructions
- Logic Instructions
- Shift Instructions
- The Suffix 's' — Flag Updates
- The Read-Modify-Write Pattern
- Summary
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- Introduction
- Full Source: image_def.s
- Line-by-Line Walkthrough
- The Complete Block in Memory
- Why Secure Mode?
- Summary
- Introduction
- Full Source: constants.s
- Line-by-Line Walkthrough
- How .equ Works
- The .include Mechanism
- Summary
- Introduction
- Full Source: reset_handler.s
- Line-by-Line Walkthrough
- The Boot Sequence Diagram
- Why Order Matters
- Summary
- Introduction
- Full Source: xosc.s
- Function 1: Init_XOSC
- Function 2: Enable_XOSC_Peri_Clock
- Clock Path Diagram
- Why XOSC Matters for UART
- Summary
- Introduction
- Full Source: reset.s
- Line-by-Line Walkthrough
- The RP2350 Reset Controller
- Why Not Release Everything at Once?
- The Atomic Clear Alternative
- Summary
- Introduction
- Function 1: UART0_Out (Transmit)
- Function 2: UART0_In (Receive)
- Register Usage Comparison
- The Polling Pattern
- Data Flow Through the UART
- Summary
- Introduction
- Full Source: main.s
- Line-by-Line Walkthrough
- The Complete Execution Flow
- What Makes This a Complete Firmware
- Summary
- 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
/**
* 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