diff --git a/README.md b/README.md
index fee4f74..6b1d8dd 100644
--- a/README.md
+++ b/README.md
@@ -511,6 +511,7 @@ Register-level C drivers for the RP2350 with no SDK dependencies. Each driver fo
| [0x0c_multicore_cbm](drivers/0x0c_multicore_cbm) | Dual-core launch & FIFO messaging | SIO FIFO, PSM |
| [0x0d_timer_cbm](drivers/0x0d_timer_cbm) | Repeating timer alarm callbacks | TIMER0, TICKS |
| [0x0e_watchdog_cbm](drivers/0x0e_watchdog_cbm) | Watchdog enable, feed & reboot detection | WATCHDOG, PSM |
+| [0x0f_flash_cbm](drivers/0x0f_flash_cbm) | On-chip flash erase, program & XIP read | ROM, XIP |
diff --git a/drivers/0x0f_flash_cbm/Inc/rp2350.h b/drivers/0x0f_flash_cbm/Inc/rp2350.h
new file mode 100644
index 0000000..99bd8cf
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Inc/rp2350.h
@@ -0,0 +1,299 @@
+/**
+ ******************************************************************************
+ * @file rp2350.h
+ * @author Kevin Thomas
+ * @brief RP2350 Device Peripheral Access Layer Header File.
+ *
+ * Memory-mapped register structures and peripheral base addresses
+ * for the RP2350 microcontroller (Cortex-M33 dual-core). All
+ * register offsets verified against the RP2350 datasheet
+ * (RP-008373-DS-2).
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#ifndef __RP2350_H
+#define __RP2350_H
+
+#include
+#include
+
+/*!< Defines 'read / write' permissions */
+#define __IO volatile
+
+/*!< Stack addresses */
+#define STACK_TOP 0x20082000UL
+#define STACK_LIMIT 0x2007A000UL
+
+/*!< Memory map */
+#define XIP_BASE 0x10000000UL
+#define SRAM_BASE 0x20000000UL
+#define SIO_BASE 0xD0000000UL
+#define PPB_BASE 0xE0000000UL
+
+/*!< APB peripherals */
+#define PSM_BASE 0x40018000UL
+#define CLOCKS_BASE 0x40010000UL
+#define RESETS_BASE 0x40020000UL
+#define IO_BANK0_BASE 0x40028000UL
+#define PADS_BANK0_BASE 0x40038000UL
+#define XOSC_BASE 0x40048000UL
+#define UART0_BASE 0x40070000UL
+#define WATCHDOG_BASE 0x400D8000UL
+#define TICKS_BASE 0x40108000UL
+
+/*!< Atomic register alias offsets */
+#define ATOMIC_SET_OFFSET 0x2000UL
+#define ATOMIC_CLR_OFFSET 0x3000UL
+
+/**
+ * @brief XOSC (External Crystal Oscillator)
+ */
+typedef struct
+{
+ __IO uint32_t CTRL; // Control register Address offset: 0x00
+ __IO uint32_t STATUS; // Status register Address offset: 0x04
+ __IO uint32_t DORMANT; // Dormant mode Address offset: 0x08
+ __IO uint32_t STARTUP; // Startup delay Address offset: 0x0C
+ __IO uint32_t COUNT; // Frequency count Address offset: 0x10
+} XOSC_TypeDef;
+
+/**
+ * @brief CLOCKS
+ */
+typedef struct
+{
+ __IO uint32_t RESERVED0[12]; // GPOUT0..GPOUT3 registers Address offset: 0x00-0x2C
+ __IO uint32_t CLK_REF_CTRL; // Reference clock control Address offset: 0x30
+ __IO uint32_t RESERVED1[5]; // CLK_REF_DIV..CLK_SYS_SELECTED Address offset: 0x34-0x44
+ __IO uint32_t CLK_PERI_CTRL; // Peripheral clock control Address offset: 0x48
+} CLOCKS_TypeDef;
+
+/**
+ * @brief RESETS
+ */
+typedef struct
+{
+ __IO uint32_t RESET; // Reset control Address offset: 0x00
+ __IO uint32_t WDSEL; // Watchdog select Address offset: 0x04
+ __IO uint32_t RESET_DONE; // Reset done status Address offset: 0x08
+} RESETS_TypeDef;
+
+/**
+ * @brief IO_BANK0 GPIO Control (one per GPIO)
+ */
+typedef struct
+{
+ __IO uint32_t STATUS; // GPIO status Address offset: 0x00
+ __IO uint32_t CTRL; // GPIO control Address offset: 0x04
+} IO_BANK0_GPIO_TypeDef;
+
+/**
+ * @brief IO_BANK0
+ */
+typedef struct
+{
+ IO_BANK0_GPIO_TypeDef GPIO[30]; // GPIO 0-29 status/ctrl pairs Address offset: 0x000-0x0E8
+} IO_BANK0_TypeDef;
+
+/**
+ * @brief PADS_BANK0
+ */
+typedef struct
+{
+ __IO uint32_t VOLTAGE_SELECT; // Voltage select Address offset: 0x00
+ __IO uint32_t GPIO[30]; // GPIO 0-29 pad control Address offset: 0x04-0x78
+} PADS_BANK0_TypeDef;
+
+/**
+ * @brief PSM (Power-on State Machine)
+ */
+typedef struct
+{
+ __IO uint32_t FRCE_ON; // Force block out of reset Address offset: 0x00
+ __IO uint32_t FRCE_OFF; // Force block into reset Address offset: 0x04
+ __IO uint32_t WDSEL; // Watchdog select Address offset: 0x08
+ __IO uint32_t DONE; // Subsystem ready status Address offset: 0x0C
+} PSM_TypeDef;
+
+/**
+ * @brief WATCHDOG
+ */
+typedef struct
+{
+ __IO uint32_t CTRL; // Watchdog control Address offset: 0x00
+ __IO uint32_t LOAD; // Load the watchdog timer (bits 23:0) Address offset: 0x04
+ __IO uint32_t REASON; // Reason for last reset Address offset: 0x08
+ __IO uint32_t SCRATCH[8]; // Scratch registers 0-7 Address offset: 0x0C-0x28
+} WATCHDOG_TypeDef;
+
+/**
+ * @brief TICKS (Tick Generator entry, one per source)
+ */
+typedef struct
+{
+ __IO uint32_t CTRL; // Enable tick generator (bit 0) Address offset: 0x00
+ __IO uint32_t CYCLES; // Number of CLK_REF cycles per tick (bits 8:0) Address offset: 0x04
+ __IO uint32_t COUNT; // Running counter of ticks (read-only) Address offset: 0x08
+} TICKS_Entry_TypeDef;
+
+/**
+ * @brief Peripheral Definitions
+ */
+#define PSM ((PSM_TypeDef *) PSM_BASE)
+#define XOSC ((XOSC_TypeDef *) XOSC_BASE)
+#define CLOCKS ((CLOCKS_TypeDef *) CLOCKS_BASE)
+#define RESETS ((RESETS_TypeDef *) RESETS_BASE)
+#define IO_BANK0 ((IO_BANK0_TypeDef *) IO_BANK0_BASE)
+#define PADS_BANK0 ((PADS_BANK0_TypeDef *) PADS_BANK0_BASE)
+#define SIO ((volatile uint32_t *) SIO_BASE)
+#define CPACR ((volatile uint32_t *) (PPB_BASE + 0x0ED88UL))
+#define WATCHDOG ((WATCHDOG_TypeDef *) WATCHDOG_BASE)
+
+/**
+ * @brief TICKS entries (indexed from TICKS_BASE, 12-byte stride)
+ */
+#define TICKS_WATCHDOG ((TICKS_Entry_TypeDef *) (TICKS_BASE + 0x00UL))
+
+/**
+ * @brief XOSC bit definitions
+ */
+#define XOSC_STATUS_STABLE_SHIFT 31U
+
+/**
+ * @brief CPACR bit definitions
+ */
+#define CPACR_CP0_SHIFT 0U
+#define CPACR_CP1_SHIFT 1U
+
+/**
+ * @brief CLK_REF bit definitions
+ */
+#define CLK_REF_CTRL_SRC_XOSC 2U
+
+/**
+ * @brief CLOCKS bit definitions
+ */
+#define CLK_PERI_CTRL_ENABLE_SHIFT 11U
+#define CLK_PERI_CTRL_AUXSRC_SHIFT 5U
+#define CLK_PERI_CTRL_AUXSRC_MASK (0x07U << CLK_PERI_CTRL_AUXSRC_SHIFT)
+#define CLK_PERI_CTRL_AUXSRC_XOSC 4U
+
+/**
+ * @brief RESETS bit definitions
+ */
+#define RESETS_RESET_IO_BANK0_SHIFT 6U
+#define RESETS_RESET_PADS_BANK0_SHIFT 9U
+#define RESETS_RESET_UART0_SHIFT 26U
+
+/**
+ * @brief IO_BANK0 bit definitions
+ */
+#define IO_BANK0_CTRL_FUNCSEL_MASK 0x1FU
+#define IO_BANK0_CTRL_FUNCSEL_UART 0x02U
+#define IO_BANK0_CTRL_FUNCSEL_SIO 0x05U
+#define IO_BANK0_CTRL_FUNCSEL_NULL 0x1FU
+
+/**
+ * @brief PADS_BANK0 bit definitions
+ */
+#define PADS_BANK0_OD_SHIFT 7U
+#define PADS_BANK0_IE_SHIFT 6U
+#define PADS_BANK0_ISO_SHIFT 8U
+#define PADS_BANK0_PUE_SHIFT 3U
+#define PADS_BANK0_PDE_SHIFT 2U
+
+/**
+ * @brief SIO GPIO register offsets (word indices from SIO_BASE)
+ */
+#define SIO_GPIO_OUT_SET_OFFSET (0x018U / 4U)
+#define SIO_GPIO_OUT_CLR_OFFSET (0x020U / 4U)
+#define SIO_GPIO_OE_SET_OFFSET (0x038U / 4U)
+
+/**
+ * @brief UART register offsets (word indices from UART0_BASE)
+ */
+#define UART_DR_OFFSET (0x000U / 4U)
+#define UART_FR_OFFSET (0x018U / 4U)
+#define UART_IBRD_OFFSET (0x024U / 4U)
+#define UART_FBRD_OFFSET (0x028U / 4U)
+#define UART_LCR_H_OFFSET (0x02CU / 4U)
+#define UART_CR_OFFSET (0x030U / 4U)
+
+/**
+ * @brief UART flag register bit definitions
+ */
+#define UART_FR_TXFF_MASK 32U
+#define UART_FR_RXFE_MASK 16U
+
+/**
+ * @brief UART line control and enable values
+ */
+#define UART_LCR_H_8N1_FIFO 0x70U
+#define UART_CR_ENABLE ((3U << 8) | 1U)
+
+/**
+ * @brief PSM bit definitions
+ */
+#define PSM_WDSEL_ALL_MASK 0x01FFFFFFUL
+#define PSM_WDSEL_ROSC_SHIFT 2U
+#define PSM_WDSEL_XOSC_SHIFT 3U
+
+/**
+ * @brief WATCHDOG CTRL bit definitions
+ */
+#define WATCHDOG_CTRL_TRIGGER_SHIFT 31U
+#define WATCHDOG_CTRL_ENABLE_SHIFT 30U
+#define WATCHDOG_CTRL_PAUSE_DBG1_SHIFT 26U
+#define WATCHDOG_CTRL_PAUSE_DBG0_SHIFT 25U
+#define WATCHDOG_CTRL_PAUSE_JTAG_SHIFT 24U
+#define WATCHDOG_CTRL_TIME_MASK 0x00FFFFFFUL
+
+/**
+ * @brief WATCHDOG LOAD bit definitions
+ */
+#define WATCHDOG_LOAD_MAX 0x00FFFFFFUL
+
+/**
+ * @brief WATCHDOG REASON bit definitions
+ */
+#define WATCHDOG_REASON_FORCE_SHIFT 1U
+#define WATCHDOG_REASON_TIMER_SHIFT 0U
+
+/**
+ * @brief TICKS bit definitions
+ */
+#define TICKS_CTRL_ENABLE_SHIFT 0U
+#define TICKS_CYCLES_12MHZ 12U
+
+/*!< Bootrom function lookup */
+#define BOOTROM_TABLE_LOOKUP_OFFSET 0x16U
+#define RT_FLAG_FUNC_ARM_SEC 0x0004U
+#define RT_FLAG_FUNC_ARM_NONSEC 0x0010U
+
+/*!< ROM function codes */
+#define ROM_FUNC_CONNECT_INTERNAL_FLASH (('I') | (('F') << 8))
+#define ROM_FUNC_FLASH_EXIT_XIP (('E') | (('X') << 8))
+#define ROM_FUNC_FLASH_RANGE_ERASE (('R') | (('E') << 8))
+#define ROM_FUNC_FLASH_RANGE_PROGRAM (('R') | (('P') << 8))
+#define ROM_FUNC_FLASH_FLUSH_CACHE (('F') | (('C') << 8))
+#define ROM_FUNC_FLASH_ENTER_CMD_XIP (('C') | (('X') << 8))
+
+/*!< Flash geometry */
+#define FLASH_SIZE (4U * 1024U * 1024U)
+#define FLASH_SECTOR_SIZE 4096U
+#define FLASH_PAGE_SIZE 256U
+#define FLASH_BLOCK_SIZE (1U << 16)
+#define FLASH_BLOCK_ERASE_CMD 0xD8U
+
+#endif /* __RP2350_H */
diff --git a/drivers/0x0f_flash_cbm/Inc/rp2350_coprocessor.h b/drivers/0x0f_flash_cbm/Inc/rp2350_coprocessor.h
new file mode 100644
index 0000000..6e26fa0
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Inc/rp2350_coprocessor.h
@@ -0,0 +1,33 @@
+/**
+ ******************************************************************************
+ * @file rp2350_coprocessor.h
+ * @author Kevin Thomas
+ * @brief Coprocessor access control driver header for RP2350.
+ *
+ * Enables coprocessor access via the CPACR register.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#ifndef __RP2350_COPROCESSOR_H
+#define __RP2350_COPROCESSOR_H
+
+#include "rp2350.h"
+
+/**
+ * @brief Enable coprocessor access via CPACR with DSB/ISB barriers.
+ * @retval None
+ */
+void coprocessor_enable(void);
+
+#endif /* __RP2350_COPROCESSOR_H */
diff --git a/drivers/0x0f_flash_cbm/Inc/rp2350_flash.h b/drivers/0x0f_flash_cbm/Inc/rp2350_flash.h
new file mode 100644
index 0000000..ec046b7
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Inc/rp2350_flash.h
@@ -0,0 +1,53 @@
+/**
+ ******************************************************************************
+ * @file rp2350_flash.h
+ * @author Kevin Thomas
+ * @brief On-chip flash driver header for RP2350.
+ *
+ * Bare-metal flash erase, program, and read driver using ROM
+ * bootrom functions. Write operations run from RAM to avoid
+ * XIP conflicts.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#ifndef __RP2350_FLASH_H
+#define __RP2350_FLASH_H
+
+#include "rp2350.h"
+
+/**
+ * @brief Erase the containing sector(s) and program data to flash.
+ *
+ * The data buffer must reside in RAM (not flash). Interrupts
+ * are disabled for the duration of the erase/program cycle.
+ * The write length must be a multiple of FLASH_PAGE_SIZE
+ * (256 bytes); pad with 0xFF if necessary.
+ *
+ * @param offset byte offset from the start of flash (sector-aligned)
+ * @param data pointer to the source buffer in RAM
+ * @param len number of bytes to write
+ * @retval None
+ */
+void flash_write(uint32_t offset, const uint8_t *data, uint32_t len);
+
+/**
+ * @brief Read bytes from on-chip flash via the XIP memory map.
+ * @param offset byte offset from the start of flash
+ * @param out pointer to the destination buffer
+ * @param len number of bytes to read
+ * @retval None
+ */
+void flash_read(uint32_t offset, uint8_t *out, uint32_t len);
+
+#endif /* __RP2350_FLASH_H */
diff --git a/drivers/0x0f_flash_cbm/Inc/rp2350_reset.h b/drivers/0x0f_flash_cbm/Inc/rp2350_reset.h
new file mode 100644
index 0000000..4a10831
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Inc/rp2350_reset.h
@@ -0,0 +1,33 @@
+/**
+ ******************************************************************************
+ * @file rp2350_reset.h
+ * @author Kevin Thomas
+ * @brief Reset controller driver header for RP2350.
+ *
+ * Provides subsystem reset release for IO_BANK0.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#ifndef __RP2350_RESET_H
+#define __RP2350_RESET_H
+
+#include "rp2350.h"
+
+/**
+ * @brief Release IO_BANK0 from reset and wait until ready.
+ * @retval None
+ */
+void reset_init_subsystem(void);
+
+#endif /* __RP2350_RESET_H */
diff --git a/drivers/0x0f_flash_cbm/Inc/rp2350_reset_handler.h b/drivers/0x0f_flash_cbm/Inc/rp2350_reset_handler.h
new file mode 100644
index 0000000..2565e29
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Inc/rp2350_reset_handler.h
@@ -0,0 +1,33 @@
+/**
+ ******************************************************************************
+ * @file rp2350_reset_handler.h
+ * @author Kevin Thomas
+ * @brief Reset handler header for RP2350.
+ *
+ * Entry point after reset. Performs stack initialization, XOSC
+ * setup, subsystem reset release, UART initialization,
+ * coprocessor enable, and branches to main().
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#ifndef __RP2350_RESET_HANDLER_H
+#define __RP2350_RESET_HANDLER_H
+
+/**
+ * @brief Reset handler entry point (naked, noreturn).
+ * @retval None
+ */
+void Reset_Handler(void) __attribute__((noreturn));
+
+#endif /* __RP2350_RESET_HANDLER_H */
diff --git a/drivers/0x0f_flash_cbm/Inc/rp2350_stack.h b/drivers/0x0f_flash_cbm/Inc/rp2350_stack.h
new file mode 100644
index 0000000..952d26e
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Inc/rp2350_stack.h
@@ -0,0 +1,34 @@
+/**
+ ******************************************************************************
+ * @file rp2350_stack.h
+ * @author Kevin Thomas
+ * @brief Stack pointer initialization header for RP2350.
+ *
+ * Sets MSP, PSP, MSPLIM, and PSPLIM from the STACK_TOP and
+ * STACK_LIMIT values defined in rp2350.h.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#ifndef __RP2350_STACK_H
+#define __RP2350_STACK_H
+
+#include "rp2350.h"
+
+/**
+ * @brief Initialize MSP, PSP, MSPLIM, and PSPLIM stack pointers.
+ * @retval None
+ */
+void stack_init(void);
+
+#endif /* __RP2350_STACK_H */
diff --git a/drivers/0x0f_flash_cbm/Inc/rp2350_uart.h b/drivers/0x0f_flash_cbm/Inc/rp2350_uart.h
new file mode 100644
index 0000000..6e9df10
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Inc/rp2350_uart.h
@@ -0,0 +1,73 @@
+/**
+ ******************************************************************************
+ * @file rp2350_uart.h
+ * @author Kevin Thomas
+ * @brief UART0 driver header for RP2350.
+ *
+ * Bare-metal UART0 driver supporting TX/RX on GPIO 0/1 at
+ * 115200 baud (12 MHz XOSC clock).
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#ifndef __RP2350_UART_H
+#define __RP2350_UART_H
+
+#include "rp2350.h"
+
+/**
+ * @brief Release UART0 from reset and wait until ready.
+ * @retval None
+ */
+void uart_release_reset(void);
+
+/**
+ * @brief Initialize UART0 pins, baud rate, line control, and enable.
+ * @retval None
+ */
+void uart_init(void);
+
+/**
+ * @brief Check whether a received byte is waiting in the UART FIFO.
+ * @retval bool true if at least one byte is available
+ */
+bool uart_is_readable(void);
+
+/**
+ * @brief Read one character from UART0 (blocking).
+ * @retval char the received character
+ */
+char uart_getchar(void);
+
+/**
+ * @brief Transmit one character over UART0 (blocking).
+ * @param c character to transmit
+ * @retval None
+ */
+void uart_putchar(char c);
+
+/**
+ * @brief Transmit a null-terminated string over UART0.
+ * @param str pointer to the string to send
+ * @retval None
+ */
+void uart_puts(const char *str);
+
+/**
+ * @brief Convert a lowercase ASCII character to uppercase.
+ * @param c input character
+ * @retval char uppercase equivalent or original character
+ */
+char uart_to_upper(char c);
+
+#endif /* __RP2350_UART_H */
diff --git a/drivers/0x0f_flash_cbm/Inc/rp2350_xosc.h b/drivers/0x0f_flash_cbm/Inc/rp2350_xosc.h
new file mode 100644
index 0000000..0dd40d8
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Inc/rp2350_xosc.h
@@ -0,0 +1,46 @@
+/**
+ ******************************************************************************
+ * @file rp2350_xosc.h
+ * @author Kevin Thomas
+ * @brief XOSC driver header for RP2350.
+ *
+ * External crystal oscillator initialization and peripheral
+ * clock enable using the XOSC registers.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#ifndef __RP2350_XOSC_H
+#define __RP2350_XOSC_H
+
+#include "rp2350.h"
+
+/**
+ * @brief Initialize the external crystal oscillator and wait until stable.
+ * @retval None
+ */
+void xosc_init(void);
+
+/**
+ * @brief Enable the XOSC peripheral clock via CLK_PERI_CTRL.
+ * @retval None
+ */
+void xosc_enable_peri_clk(void);
+
+/**
+ * @brief Switch CLK_REF source to XOSC for a stable 12 MHz clk_sys.
+ * @retval None
+ */
+void xosc_set_clk_ref(void);
+
+#endif /* __RP2350_XOSC_H */
diff --git a/drivers/0x0f_flash_cbm/Makefile b/drivers/0x0f_flash_cbm/Makefile
new file mode 100644
index 0000000..b507680
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Makefile
@@ -0,0 +1,78 @@
+# ------------------------------------------------------------------------------
+# @file Makefile
+# @author Kevin Thomas
+# @brief Build script for RP2350 bare-metal C flash driver.
+#
+# Compiles, links, and generates UF2 firmware for the RP2350.
+# ------------------------------------------------------------------------------
+
+# Toolchain
+CC = arm-none-eabi-gcc
+OBJCOPY = arm-none-eabi-objcopy
+SIZE = arm-none-eabi-size
+
+# Target
+TARGET = flash
+
+# Directories
+SRC_DIR = Src
+INC_DIR = Inc
+BUILD_DIR = build
+
+# CPU flags
+CPU_FLAGS = -mcpu=cortex-m33 -mthumb
+
+# Compiler flags
+CFLAGS = $(CPU_FLAGS) -Og -g3 -Wall -Wextra \
+ -ffunction-sections -fdata-sections \
+ -I$(INC_DIR)
+
+# Linker flags
+LDFLAGS = $(CPU_FLAGS) -T linker.ld -nostdlib -Wl,--gc-sections
+
+# Source files
+SRCS = $(SRC_DIR)/vector_table.c \
+ $(SRC_DIR)/rp2350_reset_handler.c \
+ $(SRC_DIR)/rp2350_stack.c \
+ $(SRC_DIR)/rp2350_xosc.c \
+ $(SRC_DIR)/rp2350_reset.c \
+ $(SRC_DIR)/rp2350_coprocessor.c \
+ $(SRC_DIR)/rp2350_uart.c \
+ $(SRC_DIR)/rp2350_flash.c \
+ $(SRC_DIR)/main.c \
+ $(SRC_DIR)/image_def.c
+
+# Object files
+OBJS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))
+
+# Rules
+.PHONY: all clean flash
+
+all: $(BUILD_DIR)/$(TARGET).uf2
+ @echo "==================================="
+ @echo "SUCCESS! Created $(TARGET).bin and $(TARGET).uf2"
+ @echo "==================================="
+
+$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
+ $(CC) $(CFLAGS) -c $< -o $@
+
+$(BUILD_DIR)/$(TARGET).elf: $(OBJS)
+ $(CC) $(LDFLAGS) $(OBJS) -o $@
+ $(SIZE) $@
+
+$(BUILD_DIR)/$(TARGET).bin: $(BUILD_DIR)/$(TARGET).elf
+ $(OBJCOPY) -O binary $< $@
+
+$(BUILD_DIR)/$(TARGET).uf2: $(BUILD_DIR)/$(TARGET).bin
+ python3 uf2conv.py -b 0x10000000 -f 0xe48bff59 -o $@ $<
+
+$(BUILD_DIR):
+ mkdir -p $(BUILD_DIR)
+
+clean:
+ rm -rf $(BUILD_DIR)
+
+flash: $(BUILD_DIR)/$(TARGET).elf
+ openocd -f interface/cmsis-dap.cfg -f target/rp2350.cfg \
+ -c "adapter speed 5000" \
+ -c "program $< verify reset exit"
diff --git a/drivers/0x0f_flash_cbm/Src/image_def.c b/drivers/0x0f_flash_cbm/Src/image_def.c
new file mode 100644
index 0000000..9a34198
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Src/image_def.c
@@ -0,0 +1,35 @@
+/**
+ ******************************************************************************
+ * @file image_def.c
+ * @author Kevin Thomas
+ * @brief RP2350 IMAGE_DEF block for boot ROM image recognition.
+ *
+ * Must appear within the first 4 KB of flash for the boot ROM
+ * to accept the image.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#include
+
+/**
+ * @brief IMAGE_DEF block structure placed in flash
+ */
+__attribute__((section(".embedded_block"), used))
+const uint8_t picobin_block[] = {
+ 0xD3, 0xDE, 0xFF, 0xFF,
+ 0x42, 0x01, 0x21, 0x10,
+ 0xFF, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0x35, 0x12, 0xAB
+};
diff --git a/drivers/0x0f_flash_cbm/Src/main.c b/drivers/0x0f_flash_cbm/Src/main.c
new file mode 100644
index 0000000..91b7d1e
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Src/main.c
@@ -0,0 +1,78 @@
+/**
+ ******************************************************************************
+ * @file main.c
+ * @author Kevin Thomas
+ * @brief On-chip flash write / read demonstration.
+ *
+ * Writes "Embedded Hacking flash driver demo" to the last
+ * sector of flash, reads it back via XIP, and prints the
+ * result over UART.
+ *
+ * Wiring:
+ * GPIO0 -> UART TX (USB-to-UART adapter RX)
+ * GPIO1 -> UART RX (USB-to-UART adapter TX)
+ * No external components required
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#include "rp2350_flash.h"
+#include "rp2350_uart.h"
+
+/**
+ * @brief Target offset: last sector of the 4 MB flash chip.
+ */
+#define FLASH_TARGET_OFFSET (FLASH_SIZE - FLASH_SECTOR_SIZE)
+
+/**
+ * @brief Fill a page buffer with 0xFF and copy the demo string.
+ * @param buf destination buffer (FLASH_PAGE_SIZE bytes in RAM)
+ * @retval None
+ */
+static void _prepare_write_buf(uint8_t *buf)
+{
+ uint32_t i;
+ const char *msg = "Embedded Hacking flash driver demo";
+ for (i = 0; i < FLASH_PAGE_SIZE; i++)
+ buf[i] = 0xFFU;
+ for (i = 0; msg[i] != '\0'; i++)
+ buf[i] = (uint8_t)msg[i];
+ buf[i] = 0x00U;
+}
+
+/**
+ * @brief Write the demo string to flash and print the read-back.
+ * @retval None
+ */
+static void _write_and_verify(void)
+{
+ uint8_t write_buf[FLASH_PAGE_SIZE];
+ uint8_t read_buf[FLASH_PAGE_SIZE];
+ _prepare_write_buf(write_buf);
+ flash_write(FLASH_TARGET_OFFSET, write_buf, FLASH_PAGE_SIZE);
+ flash_read(FLASH_TARGET_OFFSET, read_buf, FLASH_PAGE_SIZE);
+ uart_puts("Flash readback: ");
+ uart_puts((const char *)read_buf);
+ uart_puts("\r\n");
+}
+
+/**
+ * @brief Application entry point for the on-chip flash demo.
+ * @retval int does not return
+ */
+int main(void)
+{
+ _write_and_verify();
+ while (1)
+ __asm volatile ("wfi");
+}
diff --git a/drivers/0x0f_flash_cbm/Src/rp2350_coprocessor.c b/drivers/0x0f_flash_cbm/Src/rp2350_coprocessor.c
new file mode 100644
index 0000000..b637b36
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Src/rp2350_coprocessor.c
@@ -0,0 +1,34 @@
+/**
+ ******************************************************************************
+ * @file rp2350_coprocessor.c
+ * @author Kevin Thomas
+ * @brief Coprocessor access control driver implementation for RP2350.
+ *
+ * Grants access to coprocessors 0 and 1 by setting the
+ * corresponding bits in CPACR with DSB/ISB barriers.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#include "rp2350_coprocessor.h"
+
+void coprocessor_enable(void)
+{
+ uint32_t value;
+ value = *CPACR;
+ value |= (1U << CPACR_CP1_SHIFT);
+ value |= (1U << CPACR_CP0_SHIFT);
+ *CPACR = value;
+ __asm__ volatile ("dsb");
+ __asm__ volatile ("isb");
+}
diff --git a/drivers/0x0f_flash_cbm/Src/rp2350_flash.c b/drivers/0x0f_flash_cbm/Src/rp2350_flash.c
new file mode 100644
index 0000000..cdc3a48
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Src/rp2350_flash.c
@@ -0,0 +1,131 @@
+/**
+ ******************************************************************************
+ * @file rp2350_flash.c
+ * @author Kevin Thomas
+ * @brief On-chip flash driver implementation for RP2350.
+ *
+ * Erases and programs flash sectors using ROM bootrom
+ * functions. The erase/program trampoline executes from RAM
+ * (placed in .ram_func) because XIP is disabled while the
+ * flash chip is being modified.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#include "rp2350_flash.h"
+
+/**
+ * @brief ROM table lookup function pointer type (RP2350 ARM).
+ */
+typedef void *(*rom_table_lookup_fn)(uint32_t code, uint32_t mask);
+
+/**
+ * @brief ROM void function pointer (no parameters, no return).
+ */
+typedef void (*rom_void_fn)(void);
+
+/**
+ * @brief ROM flash range erase function pointer.
+ */
+typedef void (*rom_flash_erase_fn)(uint32_t addr, uint32_t count,
+ uint32_t block_size, uint8_t block_cmd);
+
+/**
+ * @brief ROM flash range program function pointer.
+ */
+typedef void (*rom_flash_program_fn)(uint32_t addr, const uint8_t *data,
+ uint32_t count);
+
+/**
+ * @brief Collection of ROM flash function pointers.
+ */
+typedef struct
+{
+ rom_void_fn connect; /**< @brief Connect to internal flash */
+ rom_void_fn exit_xip; /**< @brief Exit XIP mode */
+ rom_flash_erase_fn erase; /**< @brief Erase flash range */
+ rom_flash_program_fn program; /**< @brief Program flash range */
+ rom_void_fn flush_cache; /**< @brief Flush XIP cache */
+ rom_void_fn enter_xip; /**< @brief Re-enter XIP mode */
+} FlashRomFns;
+
+/**
+ * @brief Look up a ROM function by its two-character code.
+ * @param code ROM_FUNC_* code from rp2350.h
+ * @retval void* pointer to the ROM function
+ */
+static void *_rom_func_lookup(uint32_t code)
+{
+ rom_table_lookup_fn fn =
+ (rom_table_lookup_fn)(uintptr_t)(*(uint16_t *)BOOTROM_TABLE_LOOKUP_OFFSET);
+ return fn(code, RT_FLAG_FUNC_ARM_SEC);
+}
+
+/**
+ * @brief Populate all ROM flash function pointers.
+ * @param fns pointer to the struct to fill
+ * @retval None
+ */
+static void _lookup_rom_fns(FlashRomFns *fns)
+{
+ fns->connect = (rom_void_fn)_rom_func_lookup(ROM_FUNC_CONNECT_INTERNAL_FLASH);
+ fns->exit_xip = (rom_void_fn)_rom_func_lookup(ROM_FUNC_FLASH_EXIT_XIP);
+ fns->erase = (rom_flash_erase_fn)_rom_func_lookup(ROM_FUNC_FLASH_RANGE_ERASE);
+ fns->program = (rom_flash_program_fn)_rom_func_lookup(ROM_FUNC_FLASH_RANGE_PROGRAM);
+ fns->flush_cache = (rom_void_fn)_rom_func_lookup(ROM_FUNC_FLASH_FLUSH_CACHE);
+ fns->enter_xip = (rom_void_fn)_rom_func_lookup(ROM_FUNC_FLASH_ENTER_CMD_XIP);
+}
+
+/**
+ * @brief RAM-resident trampoline that erases and programs flash.
+ *
+ * Must not call any function residing in flash. All ROM
+ * function pointers are passed via the fns struct.
+ *
+ * @param fns pointer to ROM function pointers (in RAM/stack)
+ * @param offset byte offset from start of flash
+ * @param data pointer to source buffer (must be in RAM)
+ * @param len number of bytes to program
+ * @retval None
+ */
+__attribute__((section(".ram_func"), noinline))
+static void _flash_erase_program_ram(const FlashRomFns *fns, uint32_t offset,
+ const uint8_t *data, uint32_t len)
+{
+ fns->connect();
+ fns->exit_xip();
+ uint32_t erase_addr = offset & ~(FLASH_SECTOR_SIZE - 1U);
+ uint32_t erase_end = (offset + len + FLASH_SECTOR_SIZE - 1U) & ~(FLASH_SECTOR_SIZE - 1U);
+ fns->erase(erase_addr, erase_end - erase_addr, FLASH_BLOCK_SIZE, FLASH_BLOCK_ERASE_CMD);
+ fns->program(offset, data, len);
+ fns->flush_cache();
+ fns->enter_xip();
+}
+
+void flash_write(uint32_t offset, const uint8_t *data, uint32_t len)
+{
+ FlashRomFns fns;
+ _lookup_rom_fns(&fns);
+ uint32_t primask;
+ __asm volatile ("mrs %0, primask" : "=r" (primask));
+ __asm volatile ("cpsid i");
+ _flash_erase_program_ram(&fns, offset, data, len);
+ __asm volatile ("msr primask, %0" :: "r" (primask));
+}
+
+void flash_read(uint32_t offset, uint8_t *out, uint32_t len)
+{
+ const uint8_t *src = (const uint8_t *)(XIP_BASE + offset);
+ for (uint32_t i = 0; i < len; i++)
+ out[i] = src[i];
+}
diff --git a/drivers/0x0f_flash_cbm/Src/rp2350_reset.c b/drivers/0x0f_flash_cbm/Src/rp2350_reset.c
new file mode 100644
index 0000000..b0e18e8
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Src/rp2350_reset.c
@@ -0,0 +1,33 @@
+/**
+ ******************************************************************************
+ * @file rp2350_reset.c
+ * @author Kevin Thomas
+ * @brief Reset controller driver implementation for RP2350.
+ *
+ * Releases IO_BANK0 from reset and waits until the subsystem
+ * is ready.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#include "rp2350_reset.h"
+
+void reset_init_subsystem(void)
+{
+ uint32_t value;
+ value = RESETS->RESET;
+ value &= ~(1U << RESETS_RESET_IO_BANK0_SHIFT);
+ RESETS->RESET = value;
+ while ((RESETS->RESET_DONE & (1U << RESETS_RESET_IO_BANK0_SHIFT)) == 0) {
+ }
+}
diff --git a/drivers/0x0f_flash_cbm/Src/rp2350_reset_handler.c b/drivers/0x0f_flash_cbm/Src/rp2350_reset_handler.c
new file mode 100644
index 0000000..b1d7e38
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Src/rp2350_reset_handler.c
@@ -0,0 +1,74 @@
+/**
+ ******************************************************************************
+ * @file rp2350_reset_handler.c
+ * @author Kevin Thomas
+ * @brief Reset handler implementation for RP2350.
+ *
+ * Entry point after power-on or system reset. Copies the
+ * .data section from flash to RAM, initializes the stack,
+ * XOSC, subsystem resets, UART, coprocessor, then branches
+ * to main().
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#include "rp2350_reset_handler.h"
+#include "rp2350_stack.h"
+#include "rp2350_xosc.h"
+#include "rp2350_reset.h"
+#include "rp2350_uart.h"
+#include "rp2350_coprocessor.h"
+
+extern int main(void);
+
+/**
+ * @brief Linker-defined symbol: flash LMA of .data section.
+ */
+extern uint32_t __data_lma;
+
+/**
+ * @brief Linker-defined symbol: RAM VMA start of .data section.
+ */
+extern uint32_t __data_start;
+
+/**
+ * @brief Linker-defined symbol: RAM VMA end of .data section.
+ */
+extern uint32_t __data_end;
+
+/**
+ * @brief Copy initialized data and RAM-resident code from flash to RAM.
+ * @retval None
+ */
+void data_copy_init(void)
+{
+ uint32_t *src = &__data_lma;
+ uint32_t *dst = &__data_start;
+ while (dst < &__data_end)
+ *dst++ = *src++;
+}
+
+void __attribute__((naked, noreturn)) Reset_Handler(void)
+{
+ __asm__ volatile (
+ "bl stack_init\n\t"
+ "bl data_copy_init\n\t"
+ "bl xosc_init\n\t"
+ "bl xosc_enable_peri_clk\n\t"
+ "bl reset_init_subsystem\n\t"
+ "bl uart_release_reset\n\t"
+ "bl uart_init\n\t"
+ "bl coprocessor_enable\n\t"
+ "b main\n\t"
+ );
+}
diff --git a/drivers/0x0f_flash_cbm/Src/rp2350_stack.c b/drivers/0x0f_flash_cbm/Src/rp2350_stack.c
new file mode 100644
index 0000000..9c328dc
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Src/rp2350_stack.c
@@ -0,0 +1,38 @@
+/**
+ ******************************************************************************
+ * @file rp2350_stack.c
+ * @author Kevin Thomas
+ * @brief Stack pointer initialization for RP2350.
+ *
+ * Sets MSP, PSP, MSPLIM, and PSPLIM using inline assembly.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#include "rp2350_stack.h"
+
+void stack_init(void)
+{
+ __asm__ volatile (
+ "ldr r0, =%0\n\t"
+ "msr PSP, r0\n\t"
+ "ldr r0, =%1\n\t"
+ "msr MSPLIM, r0\n\t"
+ "msr PSPLIM, r0\n\t"
+ "ldr r0, =%0\n\t"
+ "msr MSP, r0\n\t"
+ :
+ : "i" (STACK_TOP), "i" (STACK_LIMIT)
+ : "r0"
+ );
+}
diff --git a/drivers/0x0f_flash_cbm/Src/rp2350_uart.c b/drivers/0x0f_flash_cbm/Src/rp2350_uart.c
new file mode 100644
index 0000000..44e67a9
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Src/rp2350_uart.c
@@ -0,0 +1,126 @@
+/**
+ ******************************************************************************
+ * @file rp2350_uart.c
+ * @author Kevin Thomas
+ * @brief UART0 driver implementation for RP2350.
+ *
+ * Configures UART0 on GPIO 0 (TX) and GPIO 1 (RX) at 115200
+ * baud using the 12 MHz XOSC clock.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#include "rp2350_uart.h"
+
+#define UART_BASE ((volatile uint32_t *) UART0_BASE)
+
+/**
+ * @brief Clear the UART0 reset bit in the reset controller.
+ * @retval None
+ */
+static void _uart_clear_reset_bit(void)
+{
+ uint32_t value;
+ value = RESETS->RESET;
+ value &= ~(1U << RESETS_RESET_UART0_SHIFT);
+ RESETS->RESET = value;
+}
+
+/**
+ * @brief Wait until the UART0 block is out of reset.
+ * @retval None
+ */
+static void _uart_wait_reset_done(void)
+{
+ while ((RESETS->RESET_DONE & (1U << RESETS_RESET_UART0_SHIFT)) == 0) {
+ }
+}
+
+/**
+ * @brief Configure GPIO pins 0 (TX) and 1 (RX) for UART function.
+ * @retval None
+ */
+static void _uart_configure_pins(void)
+{
+ IO_BANK0->GPIO[0].CTRL = IO_BANK0_CTRL_FUNCSEL_UART;
+ IO_BANK0->GPIO[1].CTRL = IO_BANK0_CTRL_FUNCSEL_UART;
+ PADS_BANK0->GPIO[0] = 0x04;
+ PADS_BANK0->GPIO[1] = 0x40;
+}
+
+/**
+ * @brief Set UART0 baud rate divisors for 115200 at 12 MHz.
+ * @retval None
+ */
+static void _uart_set_baud(void)
+{
+ UART_BASE[UART_CR_OFFSET] = 0;
+ UART_BASE[UART_IBRD_OFFSET] = 6;
+ UART_BASE[UART_FBRD_OFFSET] = 33;
+}
+
+/**
+ * @brief Configure line control and enable UART0.
+ * @retval None
+ */
+static void _uart_enable(void)
+{
+ UART_BASE[UART_LCR_H_OFFSET] = UART_LCR_H_8N1_FIFO;
+ UART_BASE[UART_CR_OFFSET] = UART_CR_ENABLE;
+}
+
+void uart_release_reset(void)
+{
+ _uart_clear_reset_bit();
+ _uart_wait_reset_done();
+}
+
+void uart_init(void)
+{
+ _uart_configure_pins();
+ _uart_set_baud();
+ _uart_enable();
+}
+
+bool uart_is_readable(void)
+{
+ return (UART_BASE[UART_FR_OFFSET] & UART_FR_RXFE_MASK) == 0;
+}
+
+char uart_getchar(void)
+{
+ while (UART_BASE[UART_FR_OFFSET] & UART_FR_RXFE_MASK) {
+ }
+ return (char)(UART_BASE[UART_DR_OFFSET] & 0xFF);
+}
+
+void uart_putchar(char c)
+{
+ while (UART_BASE[UART_FR_OFFSET] & UART_FR_TXFF_MASK) {
+ }
+ UART_BASE[UART_DR_OFFSET] = (uint32_t)c;
+}
+
+void uart_puts(const char *str)
+{
+ while (*str) {
+ uart_putchar(*str++);
+ }
+}
+
+char uart_to_upper(char c)
+{
+ if (c >= 'a' && c <= 'z')
+ return (char)(c - 32);
+ return c;
+}
diff --git a/drivers/0x0f_flash_cbm/Src/rp2350_xosc.c b/drivers/0x0f_flash_cbm/Src/rp2350_xosc.c
new file mode 100644
index 0000000..3383559
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Src/rp2350_xosc.c
@@ -0,0 +1,46 @@
+/**
+ ******************************************************************************
+ * @file rp2350_xosc.c
+ * @author Kevin Thomas
+ * @brief XOSC driver implementation for RP2350.
+ *
+ * Configures the external crystal oscillator and enables the
+ * peripheral clock sourced from XOSC.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#include "rp2350_xosc.h"
+
+void xosc_init(void)
+{
+ XOSC->STARTUP = 0x00C4U;
+ XOSC->CTRL = 0x00FABAA0U;
+ while ((XOSC->STATUS & (1U << XOSC_STATUS_STABLE_SHIFT)) == 0) {
+ }
+}
+
+void xosc_enable_peri_clk(void)
+{
+ uint32_t value;
+ value = CLOCKS->CLK_PERI_CTRL;
+ value &= ~CLK_PERI_CTRL_AUXSRC_MASK;
+ value |= (1U << CLK_PERI_CTRL_ENABLE_SHIFT);
+ value |= (CLK_PERI_CTRL_AUXSRC_XOSC << CLK_PERI_CTRL_AUXSRC_SHIFT);
+ CLOCKS->CLK_PERI_CTRL = value;
+}
+
+void xosc_set_clk_ref(void)
+{
+ CLOCKS->CLK_REF_CTRL = CLK_REF_CTRL_SRC_XOSC;
+}
diff --git a/drivers/0x0f_flash_cbm/Src/vector_table.c b/drivers/0x0f_flash_cbm/Src/vector_table.c
new file mode 100644
index 0000000..5d234c5
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/Src/vector_table.c
@@ -0,0 +1,38 @@
+/**
+ ******************************************************************************
+ * @file vector_table.c
+ * @author Kevin Thomas
+ * @brief Vector table with initial stack pointer and reset handler.
+ *
+ * Placed in the .vectors section at the start of flash.
+ * The Thumb bit (bit 0 = 1) is automatically set by the
+ * linker for function pointers in Thumb mode.
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+#include
+
+extern uint32_t _stack_top;
+extern void Reset_Handler(void);
+
+typedef void (*vector_func_t)(void);
+
+/**
+ * @brief Vector table placed in .vectors section
+ */
+__attribute__((section(".vectors"), used))
+const void *_vectors[2] = {
+ &_stack_top,
+ Reset_Handler
+};
diff --git a/drivers/0x0f_flash_cbm/linker.ld b/drivers/0x0f_flash_cbm/linker.ld
new file mode 100644
index 0000000..0e24993
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/linker.ld
@@ -0,0 +1,142 @@
+/**
+ ******************************************************************************
+ * @file linker.ld
+ * @author Kevin Thomas
+ * @brief Minimal linker script for bare-metal RP2350 development.
+ *
+ * Defines FLASH (XIP 32 MB) and RAM (520 kB SRAM) regions.
+ * The vector table is placed at the start of flash (0x10000000).
+ *
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2026 Kevin Thomas.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+/**
+ * Entry point.
+ */
+ENTRY(Reset_Handler)
+
+/**
+ * Define memory regions.
+ */
+__XIP_BASE = 0x10000000;
+__XIP_SIZE = 32M;
+
+__SRAM_BASE = 0x20000000;
+__SRAM_SIZE = 520K;
+__STACK_SIZE = 32K;
+
+/**
+ * Memory layout.
+ */
+MEMORY
+{
+ RAM (rwx) : ORIGIN = __SRAM_BASE, LENGTH = __SRAM_SIZE
+ FLASH (rx) : ORIGIN = __XIP_BASE, LENGTH = __XIP_SIZE
+}
+
+/**
+ * Program headers.
+ */
+PHDRS
+{
+ text PT_LOAD FLAGS(5);
+ data PT_LOAD FLAGS(6);
+}
+
+/**
+ * Section placement.
+ */
+SECTIONS
+{
+ . = ORIGIN(FLASH);
+
+ /**
+ * Vector table MUST be first at 0x10000000.
+ */
+ .vectors :
+ {
+ KEEP(*(.vectors))
+ } > FLASH :text
+
+ /**
+ * Verify vector table placement.
+ */
+ ASSERT((ADDR(.vectors) == ORIGIN(FLASH)),
+ "Vector table must be at start of flash (0x10000000)")
+
+ /**
+ * Text and read-only data.
+ */
+ .text :
+ {
+ . = ALIGN(4);
+ *(.text*)
+ *(.rodata*)
+ KEEP(*(.init))
+ KEEP(*(.fini))
+ KEEP(*(.ARM.attributes))
+ } > FLASH :text
+
+ /**
+ * IMAGE_DEF block at end of code.
+ */
+ .embedded_block :
+ {
+ KEEP(*(.embedded_block))
+ } > FLASH :text
+
+ /**
+ * Initialized data and RAM-resident code (LMA in flash, VMA in RAM).
+ * Startup copies from __data_lma to __data_start..__data_end.
+ */
+ .data :
+ {
+ . = ALIGN(4);
+ __data_start = .;
+ *(.ram_func*)
+ *(.data*)
+ . = ALIGN(4);
+ __data_end = .;
+ } > RAM AT> FLASH :data
+
+ __data_lma = LOADADDR(.data);
+
+ /**
+ * Uninitialized data (BSS) in RAM.
+ */
+ .bss (NOLOAD) :
+ {
+ . = ALIGN(4);
+ *(.bss*)
+ *(COMMON)
+ . = ALIGN(4);
+ } > RAM
+
+ /**
+ * Non-secure stack symbols.
+ */
+ __StackTop = ORIGIN(RAM) + LENGTH(RAM);
+ __StackLimit = __StackTop - __STACK_SIZE;
+ __stack = __StackTop;
+ _stack_top = __StackTop;
+
+ /**
+ * Stack section (no load).
+ */
+ .stack (NOLOAD) : { . = ALIGN(8); } > RAM
+
+ /**
+ * Provide vector table symbol to startup code.
+ */
+ PROVIDE(__Vectors = ADDR(.vectors));
+}
diff --git a/drivers/0x0f_flash_cbm/uf2conv.py b/drivers/0x0f_flash_cbm/uf2conv.py
new file mode 100644
index 0000000..529dd96
--- /dev/null
+++ b/drivers/0x0f_flash_cbm/uf2conv.py
@@ -0,0 +1,438 @@
+#!/usr/bin/env python3
+import sys
+import struct
+import subprocess
+import re
+import os
+import os.path
+import argparse
+import json
+from time import sleep
+
+
+UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
+UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
+UF2_MAGIC_END = 0x0AB16F30 # Ditto
+
+INFO_FILE = "/INFO_UF2.TXT"
+
+appstartaddr = 0x2000
+familyid = 0x0
+
+
+def is_uf2(buf):
+ w = struct.unpack(" 476:
+ assert False, "Invalid UF2 data size at " + ptr
+ newaddr = hd[3]
+ if (hd[2] & 0x2000) and (currfamilyid == None):
+ currfamilyid = hd[7]
+ if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid):
+ currfamilyid = hd[7]
+ curraddr = newaddr
+ if familyid == 0x0 or familyid == hd[7]:
+ appstartaddr = newaddr
+ padding = newaddr - curraddr
+ if padding < 0:
+ assert False, "Block out of order at " + ptr
+ if padding > 10 * 1024 * 1024:
+ assert False, "More than 10M of padding needed at " + ptr
+ if padding % 4 != 0:
+ assert False, "Non-word padding size at " + ptr
+ while padding > 0:
+ padding -= 4
+ outp.append(b"\x00\x00\x00\x00")
+ if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]):
+ outp.append(block[32 : 32 + datalen])
+ curraddr = newaddr + datalen
+ if hd[2] & 0x2000:
+ if hd[7] in families_found.keys():
+ if families_found[hd[7]] > newaddr:
+ families_found[hd[7]] = newaddr
+ else:
+ families_found[hd[7]] = newaddr
+ if prev_flag == None:
+ prev_flag = hd[2]
+ if prev_flag != hd[2]:
+ all_flags_same = False
+ if blockno == (numblocks - 1):
+ print("--- UF2 File Header Info ---")
+ families = load_families()
+ for family_hex in families_found.keys():
+ family_short_name = ""
+ for name, value in families.items():
+ if value == family_hex:
+ family_short_name = name
+ print(
+ "Family ID is {:s}, hex value is 0x{:08x}".format(
+ family_short_name, family_hex
+ )
+ )
+ print("Target Address is 0x{:08x}".format(families_found[family_hex]))
+ if all_flags_same:
+ print("All block flag values consistent, 0x{:04x}".format(hd[2]))
+ else:
+ print("Flags were not all the same")
+ print("----------------------------")
+ if len(families_found) > 1 and familyid == 0x0:
+ outp = []
+ appstartaddr = 0x0
+ return b"".join(outp)
+
+
+def convert_to_carray(file_content):
+ outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
+ outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
+ for i in range(len(file_content)):
+ if i % 16 == 0:
+ outp += "\n"
+ outp += "0x%02x, " % file_content[i]
+ outp += "\n};\n"
+ return bytes(outp, "utf-8")
+
+
+def convert_to_uf2(file_content):
+ global familyid
+ datapadding = b""
+ while len(datapadding) < 512 - 256 - 32 - 4:
+ datapadding += b"\x00\x00\x00\x00"
+ numblocks = (len(file_content) + 255) // 256
+ outp = []
+ for blockno in range(numblocks):
+ ptr = 256 * blockno
+ chunk = file_content[ptr : ptr + 256]
+ flags = 0x0
+ if familyid:
+ flags |= 0x2000
+ hd = struct.pack(
+ b"