RP2350 Blink Driver
March 23, 2026 · View on GitHub
FREE Embedded Hacking Course HERE
VIDEO PROMO HERE
RP2350 Blink Driver
An RP2350 Blink 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
LED Wiring (Pico 2 Target)
- GP16 (Pin 21) → 330 Ω resistor → LED anode
- LED cathode → GND (Pin 23)
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 Rd, =value Pseudo-Instruction
- Literal Pool Placement
- Our Firmware's Use of Immediates
- Why Not Always Use mov?
- Summary
Chapter 9: ARM Arithmetic and Logic Instructions
- Introduction
- Arithmetic Instructions
- Logic Instructions
- The APSR Flags
- Read-Modify-Write Pattern
- Summary
Chapter 10: ARM Memory Access Instructions
- Introduction
- ldr — Load Register
- str — Store Register
- push and pop — Stack Operations
- Memory Map and Peripheral Access
- Alignment Requirements
- msr and mrs — Special Register Access
- Summary
Chapter 11: ARM Branch Instructions
- Introduction
- Unconditional Branches
- Conditional Branches
- Branch Encoding and Range
- Condition Codes
- Polling Loops
- Summary
Chapter 12: ARM Calls, Returns, and the Stack Frame
- Introduction
- The Link Register
- Leaf Functions vs. Non-Leaf Functions
- The Call Chain
- The Stack Frame
- Nested Calls
- The ARM Calling Convention (AAPCS)
- Parameter Passing Example
- Reset_Handler: A Special Case
- Summary
Part III — Assembly Programming (Chapters 13–17)
Chapter 13: Assembler Directives
- Introduction
- Syntax and Instruction Set Directives
- Section Directives
- Symbol Directives
- Data Directives
- Function Directives
- Include Directive
- Summary
Chapter 14: Labels, Symbols, and the Symbol Table
- Introduction
- Defining Labels
- Global vs. Local Labels
- Label Types in Our Firmware
- The Symbol Table
- .equ Constants in the Symbol Table
- Cross-File Resolution
- The Linking Process
- The Thumb Bit
- Summary
Chapter 15: Sections, Memory Layout, and the Linker Script
- Introduction
- What Are Sections?
- Our Linker Script
- Section-by-Section Walkthrough
- The Final Memory Map
- ENTRY Directive
- Symbol Exports from the Linker Script
- Why Sections Matter
- Summary
Chapter 16: System Registers and Coprocessor Interface
- Introduction
- Special-Purpose Registers
- Memory-Mapped System Registers
- Barrier Instructions
- The Coprocessor Interface
- The Complete Coprocessor Flow
- Summary
Chapter 17: Bit Manipulation Patterns
- Introduction
- The Fundamental Operations
- Set a Single Bit
- Clear a Single Bit
- Clear a Multi-Bit Field
- Test a Bit
- Combined Patterns
- Bit Fields in Our Registers
- Why This Matters
- Summary
Part IV — RP2350 Hardware (Chapter 18)
Chapter 18: RP2350 Hardware Architecture
- Introduction
- RP2350 Block Diagram
- Memory Map
- Crystal Oscillator (XOSC)
- Clock System
- Reset Controller
- GPIO Architecture
- GPIO16 and the LED
- Boot Sequence
- Summary
Part V — Build System (Chapters 19–20)
Chapter 19: The Linker Script
- Introduction
- Entry Point
- Memory Constants
- Memory Regions
- Program Headers
- Section Placement
- The Resulting Memory Layout
- Summary
Chapter 20: The Build System
- Introduction
- The Build Pipeline
- Stage 1: Assembly
- Stage 2: Linking
- Stage 3: Binary Extraction
- Stage 4: UF2 Conversion
- Error Handling
- Flashing the Firmware
- The Clean Script
- Summary
Part VI — Source Code Walkthroughs (Chapters 21–29)
Chapter 21: image_def.s — The PICOBIN Boot Block
- Introduction
- Complete Source Code
- Section Placement
- Block Structure
- Byte-by-Byte Analysis
- Why This Matters
- Summary
Chapter 22: constants.s — Memory Addresses and Constants
- Introduction
- Complete Source Code
- Preamble
- Stack Constants
- Crystal Oscillator Constants
- System Registers
- Clock Constants
- Reset Controller Constants
- GPIO Constants
- How .include Works
- Design Principle
- Summary
Chapter 23: vector_table.s and stack.s — Boot Foundation
- Introduction
- vector_table.s — Complete Source Code
- stack.s — Complete Source Code
- The Boot Sequence
- Summary
Chapter 24: reset_handler.s — The Boot Sequence
- Introduction
- Complete Source Code
- Symbol Metadata
- The Initialization Sequence
- Dependency Chain
- Reset_Handler Is Not a Normal Function
- Summary
Chapter 25: xosc.s — Crystal Oscillator and Clock Configuration
- Introduction
- Complete Source Code
- Init_XOSC — Line-by-Line
- Enable_XOSC_Peri_Clock — Line-by-Line
- Both Functions Are Leaf Functions
- Clock Domain After Configuration
- Summary
Chapter 26: reset.s — Releasing Peripherals from Reset
- Introduction
- Complete Source Code
- Line-by-Line Walkthrough
- Reset Register Bit Map
- Why the Wait Is Necessary
- Function Characteristics
- Local Labels
- Summary
Chapter 27: gpio.s Part 1 — GPIO_Config
- Introduction
- Complete Source Code — GPIO_Config
- Function Signature
- Register Save and Restore
- Phase 1: Pad Configuration
- Phase 2: Function Select
- Phase 3: Enable Output via Coprocessor
- The Three Layers of GPIO Configuration
- How main.s Calls GPIO_Config
- Summary
Chapter 28: gpio.s Part 2, delay.s, and coprocessor.s — Output Control and Timing
- Introduction
- GPIO_Set — Drive Pin High
- GPIO_Clear — Drive Pin Low
- Delay_MS — Millisecond Delay
- Enable_Coprocessor — CP0 Access
- The Runtime Flow
- Summary
Chapter 29: main.s — The Blink Loop
- Introduction
- Complete Source Code
- Function Metadata
- Register Save
- GPIO16 Configuration (One-Time Setup)
- The Infinite Blink Loop
- Unreachable Code
- Data Sections
- Complete Execution Flow
- Summary
Part VII — Full Integration (Chapter 30)
Chapter 30: Full Integration — From Source to Blinking LED
- Introduction
- Step 1: Verify the Toolchain
- Step 2: Build the Firmware
- Step 3: Verify Build Artifacts
- Step 4: Wire the Hardware
- Step 5: Flash the Firmware
- Step 6: Verify Operation
- Troubleshooting
- The Complete Architecture
- What You Have Learned
- Summary
main.s Code
/**
* FILE: main.s
*
* DESCRIPTION:
* RP2350 Bare-Metal Blink Main Application.
*
* BRIEF:
* Main application entry point for RP2350 blink driver. Contains the
* main loop that toggles GPIO16 to blink an LED.
*
* 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
.GPIO16_Config:
ldr r0, =PADS_BANK0_GPIO16_OFFSET // load PADS_BANK0_GPIO16_OFFSET
ldr r1, =IO_BANK0_GPIO16_CTRL_OFFSET // load IO_BANK0_GPIO16_CTRL_OFFSET
ldr r2, =16 // load GPIO number
bl GPIO_Config // call GPIO_Config
.Loop:
ldr r0, =16 // load GPIO number
bl GPIO_Set // call GPIO_Set
ldr r0, =500 // 500ms
bl Delay_MS // call Delay_MS
ldr r0, =16 // load GPIO number
bl GPIO_Clear // call GPIO_Clear
ldr r0, =500 // 500ms
bl Delay_MS // call Delay_MS
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