A from-scratch ARM Cortex-M0+ emulator for the Raspberry Pi RP2040 microcontroller, capable of loading and executing UF2 firmware with accurate memory mapping and peripheral emulation.
Bramble successfully boots RP2040 firmware, executes the complete Thumb-1 instruction set, provides working UART0 output, full GPIO emulation, hardware timer support with alarms, and dual-core support! The emulator cleanly executes test programs with proper halting via BKPT instructions.
-
Complete RP2040 Memory Map: Flash (0x10000000), SRAM (0x20000000), SIO (0xD0000000), and APB peripherals
-
UF2 Firmware Loading: Parses and loads UF2 blocks into flash with proper address validation
-
Full ARM Cortex-M0+ Thumb Instruction Set: 60+ instructions across 4 phases:
Phase 1 - Foundational (Bootloader Essential):
- Data movement: MOVS, MOV (with high register support)
- Arithmetic: ADDS/SUBS (imm3, imm8, register)
- Comparison: CMP (imm8, register with high regs)
- Load/Store: LDR/STR (imm5, reg offset, PC-relative, SP-relative)
- Stack: PUSH/POP (with LR/PC support)
- Branches: B, Bcond (all 14 conditions), BL, BX, BLX
Phase 2 - Essential (Program Execution):
- Byte operations: LDRB/STRB (imm5, reg), LDRSB (sign-extended)
- Halfword operations: LDRH/STRH (imm5, reg), LDRSH (sign-extended)
- Shifts: LSLS, LSRS, ASRS, RORS (immediate and register)
- Logical: ANDS, EORS, ORRS, BICS, MVNS
- Multiplication: MULS
- Multiple load/store: LDMIA, STMIA
Phase 3 - Important (Advanced Features):
- Special comparison: CMN, TST
- System: SVC, MSR, MRS
- High register operations
Phase 4 - Optional (Optimization & Polish):
- Hints: NOP, YIELD, WFE, WFI, SEV, IT
- Sign/zero extend: SXTB, SXTH, UXTB, UXTH
- Byte reverse: REV, REV16, REVSH
- Address generation: ADR, ADD/SUB SP
- Interrupt control: CPSID, CPSIE
- Debug: BKPT, UDF
-
Accurate Flag Handling: N, Z, C, V flags with proper carry/overflow detection
-
UART0 Emulation: Character output via
UART0_DR(0x40034000) with TX FIFO status -
GPIO Emulation: Complete 30-pin GPIO peripheral with:
- SIO fast GPIO access (direct read/write, atomic operations)
- IO_BANK0 per-pin configuration (function select, control)
- PADS_BANK0 pad control (pull-up/down, drive strength)
- Interrupt registers (enable, force, status)
- All 10 GPIO functions supported (SIO, UART, SPI, I2C, PWM, PIO, etc.)
-
✨ Hardware Timer: Full 64-bit microsecond timer with:
- 64-bit counter incrementing with CPU cycles
- 4 independent alarm channels (ALARM0-3)
- Interrupt generation on alarm match
- Pause/resume functionality
- Write-1-to-clear interrupt handling
- Cycle-accurate timing simulation
-
✨ Dual-Core Support: Full second Cortex-M0+ core emulation with:
- Independent Core 0 and Core 1 state machines
- Shared flash memory (2 MB)
- Separate core-local RAM (128 KB each)
- Shared RAM (64 KB for inter-core communication)
- Dual FIFO channels for core-to-core messaging
- Spinlock support for synchronization
- SIO (Single-cycle I/O) for atomic operations
-
Proper Reset Sequence: Vector table parsing, SP/PC initialization from flash
-
Clean Halt Detection: BKPT instruction properly stops execution with register dump
hello_world.uf2 (Assembly version):
Total steps: 187
Output: "Hello from ASM!" ✅
Halt: Clean BKPT #0 at 0x1000005A ✅
gpio_test.uf2 (GPIO test):
GPIO Test Starting...
GPIO 25 configured as output
LED ON / LED OFF (x5 cycles) ✅
Final state: LED OFF ✅
GPIO Test Complete!
Total steps: 2,001,191 ✅
timer_test.uf2 (Timer test):
Timer Test Starting...
Timer value 1: 99 us ✅
Timer value 2: 10,224 us ✅
Elapsed time: ~10,125 us ✅
Timer Test Complete!
Total steps: 20,808 ✅
alarm_test.uf2 (Timer alarm test):
Timer Alarm Test Starting...
Alarm set for +1000us ✅
SUCCESS: Alarm fired! ✅
Interrupt cleared ✅
Timer Alarm Test Complete!
Total steps: 1,948 ✅
All registers preserved correctly, flags set accurately, GPIO state properly managed, timer counting accurately!
- NVIC (Interrupt Controller): Core structure implemented but missing 3 key features:
- Memory bus routing (NVIC registers not accessible via MMIO)
- Priority scheduling (wrong interrupt executes when multiple pending)
- Exception return mechanism (ISRs cannot return cleanly)
- See NVIC Audit Report for detailed analysis
- Limited Peripheral Emulation: UART0, GPIO, and Timer implemented; other peripherals (DMA, USB, etc.) return stub values
- No Cycle Accuracy: Instructions execute in logical order; timer uses simplified 1 cycle = 1 microsecond model
- Limited Multi-Core Testing: Dual-core framework implemented; extensive testing pending
- GCC cross-compiler:
arm-none-eabi-gcc - CMake 3.10+
- Python 3 (for UF2 conversion)
- Standard C library (host)
cd Bramble
./build.shThis builds the bramble executable in the project root.
Edit include/emulator.h to select single-core or dual-core:
/* For SINGLE-CORE: */
// #define DUAL_CORE_ENABLED
/* For DUAL-CORE: */
#define DUAL_CORE_ENABLEDThen rebuild:
make clean && makeHello World (prints "Hello from ASM!"):
cd test-firmware
chmod +x build.sh
./build.sh hello_worldGPIO Test (toggles LED on GPIO 25):
cd test-firmware
./build.sh gpioTimer Test (measures elapsed time):
cd test-firmware
./build.sh timerAlarm Test (tests timer alarms):
cd test-firmware
./build.sh alarmBuild All Tests:
cd test-firmware
./build.sh allHello World:
./bramble hello_world.uf2GPIO Test:
./bramble gpio_test.uf2Timer Test:
./bramble timer_test.uf2Alarm Test:
./bramble alarm_test.uf2Bramble now supports flexible debug output modes:
Single-Core CPU Step Tracing (verbose CPU and peripheral logging):
./bramble -debug timer_test.uf2Assembly Instruction Tracing (detailed POP/BX/branch operations):
./bramble -asm alarm_test.uf2Combined Debug + Assembly Tracing:
./bramble -debug -asm alarm_test.uf2No Debug Output:
./bramble hello_world.uf2Dual-Core Specific:
./bramble firmware.uf2 -debug # Core 0 debug output
./bramble firmware.uf2 -debug -debug1 # Both cores debug
./bramble firmware.uf2 -status # Periodic status updates
./bramble firmware.uf2 -debug -status # Debug + status combinedExpected output:
╔════════════════════════════════════════════════════════════╗
║ Bramble RP2040 Emulator - Dual-Core Mode ║
╚════════════════════════════════════════════════════════════╝
[Init] Initializing dual-core RP2040 emulator...
[Init] Loading firmware: littleOS.uf2
[Init] Firmware loaded successfully
[Boot] Starting Core 0 from flash...
[Boot] Core 0 SP = 0x20020000
[Boot] Core 0 PC = 0x10000104
[Boot] Core 1 held in reset (waiting for Core 0 to start)
═══════════════════════════════════════════════════════════
Executing...
═══════════════════════════════════════════════════════════
Bramble/
├── src/
│ ├── main.c # Unified entry point, boot, execution (single & dual)
│ ├── cpu.c # Cortex-M0+ core: fetch, decode, dispatch
│ ├── cpu_dual.c # Dual-core CPU with FIFO and spinlock support
│ ├── instructions.c # 60+ Thumb instruction implementations
│ ├── membus.c # Memory system: flash, RAM, peripherals
│ ├── multicore.c # Dual-core coordination and SIO
│ ├── gpio.c # GPIO peripheral emulation
│ ├── timer.c # Hardware timer emulation
│ ├── nvic.c # NVIC interrupt controller (core structure)
│ └── uf2.c # UF2 file loader
├── include/
│ ├── emulator.h # Core definitions and single-core CPU state
│ ├── emulator_dual.h # Dual-core definitions and state structures
│ ├── instructions.h # Instruction handler prototypes
│ ├── gpio.h # GPIO register definitions
│ ├── timer.h # Timer register definitions
│ └── nvic.h # NVIC register definitions
├── test-firmware/
│ ├── hello_world.S # Assembly UART test
│ ├── gpio_test.S # Assembly GPIO test
│ ├── timer_test.S # Assembly timer test
│ ├── alarm_test.S # Assembly alarm test
│ ├── linker.ld # Memory layout definition
│ ├── uf2conv.py # UF2 conversion utility
│ └── build.sh # Firmware build script
├── docs/
│ ├── GPIO.md # GPIO peripheral documentation
│ └── NVIC_audit_report.md # NVIC audit findings and recommendations
├── assets/
│ └── bramble-logo.jpg # Project logo
├── CMakeLists.txt # Build configuration
├── build.sh # Top-level build script
├── CHANGELOG.md # Version history and changes
└── README.md # This file
- 64-bit Counter: Microsecond-resolution time tracking
- 4 Independent Alarms: ALARM0-3 with configurable trigger points
- Interrupt Generation: Sets INTR bits when alarms fire (NVIC integration pending)
- Write-1-to-Clear: Standard ARM interrupt acknowledgment
- Pause/Resume: Stop timer for debugging
- Atomic Operations: Armed register shows active alarms
- TIMER_TIMELR/TIMEHR (0x4005400C/08): Read 64-bit counter
- TIMER_TIMELW/TIMEHW (0x40054004/00): Write 64-bit counter
- TIMER_ALARM0-3 (0x40054010-1C): Set alarm compare values
- TIMER_ARMED (0x40054020): Shows which alarms are active
- TIMER_INTR (0x40054034): Raw interrupt status (W1C)
- TIMER_INTE (0x40054038): Interrupt enable mask
- TIMER_INTS (0x40054040): Masked interrupt status
/* Read current time */
ldr r0, =0x4005400C /* TIMER_TIMELR */
ldr r1, [r0] /* R1 = current time in microseconds */
/* Set alarm for 1000us in future */
ldr r2, =1000
add r1, r2 /* R1 = target time */
ldr r0, =0x40054010 /* TIMER_ALARM0 */
str r1, [r0] /* Alarm armed automatically */
/* Wait for alarm (polling) */
poll:
ldr r0, =0x40054034 /* TIMER_INTR */
ldr r1, [r0]
movs r2, #1
tst r1, r2 /* Check bit 0 */
beq poll
/* Clear interrupt */
movs r1, #1
ldr r0, =0x40054034 /* TIMER_INTR */
str r1, [r0] /* Write 1 to clear */- 30 GPIO Pins (GPIO 0-29, matching RP2040)
- SIO Fast Access: Direct read/write at 0xD0000000
- Atomic Operations: SET, CLR, XOR registers for thread-safe bit manipulation
- Function Select: All 10 GPIO functions (SIO, UART, SPI, I2C, PWM, PIO0/1, etc.)
- Per-Pin Configuration: Control and status registers via IO_BANK0
- Pad Control: Pull-up/down, drive strength via PADS_BANK0
- Interrupt Support: Registers implemented (NVIC integration pending)
/* Configure GPIO 25 as output (LED on Pico) */
ldr r0, =0x400140CC /* GPIO25_CTRL */
movs r1, #5 /* Function 5 = SIO */
str r1, [r0]
/* Enable output */
ldr r0, =0xD0000024 /* SIO_GPIO_OE_SET */
ldr r1, =(1 << 25) /* Bit 25 */
str r1, [r0]
/* Turn LED on */
ldr r0, =0xD0000014 /* SIO_GPIO_OUT_SET */
str r1, [r0]See docs/GPIO.md for complete documentation.
-
Independent Core Execution: Both cores run independently with their own:
- Program counters (PC)
- Stack pointers (SP)
- Register sets (R0-R12, LR)
- Debug flags (-debug, -debug1)
-
Memory Sharing:
- Flash (2 MB): Shared and execute-only
- Core 0 RAM (128 KB): 0x20000000 - 0x2001FFFF
- Core 1 RAM (128 KB): 0x20020000 - 0x2003FFFF
- Shared RAM (64 KB): 0x20040000 - 0x2004FFFF
-
Inter-Core Communication:
- Dual FIFOs: Core 0 ↔ Core 1 messaging
- Spinlocks: Hardware-level synchronization (32 spinlocks)
- SIO Atomic Operations: Thread-safe bit manipulation
Flash: 0x10000000 - 0x10200000 (2 MB, shared)
Core 0 RAM: 0x20000000 - 0x20020000 (128 KB, core-local)
Core 1 RAM: 0x20020000 - 0x20040000 (128 KB, core-local)
Shared RAM: 0x20040000 - 0x20050000 (64 KB, shared)
Total RAM: 320 KB usable, 264 KB available
// In C firmware code for dual-core operation:
// Core 0: Send message to Core 1
fifo_push(CORE0, 0x12345678);
// Core 1: Receive message
uint32_t msg = fifo_pop(CORE1);
// Both cores: Synchronized access to shared memory
spinlock_acquire(0);
shared_counter++;
spinlock_release(0);Most firmware builds naturally for dual-core:
# In your firmware makefile:
make CORES=2 # Compiles with dual-core definitionsThen run with:
./bramble firmware.uf2 -status # Show status for both coresThe emulator accurately models the RP2040 address space:
- Flash (XIP): 0x10000000 - 0x101FFFFF (2MB executable)
- SRAM: 0x20000000 - 0x20041FFF (264KB)
- APB Peripherals: 0x40000000 - 0x4FFFFFFF (UART, GPIO, timers, etc.)
- SIO: 0xD0000000 - 0xD0000FFF (Single-cycle I/O, GPIO fast access)
All accesses respect alignment requirements and return appropriate values for unimplemented regions.
Peripherals are integrated into the memory bus (membus.c):
- Reads/writes to peripheral address ranges are routed to peripheral modules
- GPIO:
0x40014000(IO_BANK0),0x4001C000(PADS),0xD0000000(SIO) - Timer:
0x40054000(64-bit counter, 4 alarms, interrupts) - UART0:
0x40034000 - Stub responses for unimplemented peripherals
The timer increments every CPU cycle with a simplified timing model:
- 1 CPU cycle = 1 microsecond (for simulation speed)
- Real RP2040: 125 MHz = 125 cycles per microsecond
- Configurable in
timer_tick()for accuracy vs. speed tradeoff
Alarms trigger when:
if (timer_low_32bits >= alarm_value) {
set_interrupt_bit();
disarm_alarm();
}Thumb instructions are decoded using a priority-ordered bitmask dispatch system in cpu_step():
- 32-bit instructions checked first (BL, extended operations)
- Control-flow instructions (branches, BKPT) handle PC directly and return
- Data processing (ALU, shifts, logical ops) execute and fall through
- PC increment (+2 bytes) happens automatically for non-branch instructions
Critical insight: The dispatcher must check more-specific masks before less-specific ones to avoid false matches.
The emulator implements full APSR flag semantics:
- N (Negative): Bit 31 of result
- Z (Zero): Result equals zero
- C (Carry): Unsigned overflow (for ADD) or NOT borrow (for SUB)
- V (Overflow): Signed overflow (operands same sign, result different)
Helper functions update_add_flags() and update_sub_flags() ensure consistency across all arithmetic instructions.
The loader validates:
- Magic numbers (0x0A324655, 0x9E5D5157)
- Target address in flash range
- Payload size (476 bytes standard)
- Family ID (0xE48BFF56 for RP2040)
Multi-block firmware images are supported with sequential loading.
Core Synchronization:
- Both cores execute independently in the main loop
dual_core_step()advances both cores one instruction eachany_core_running()checks if either core is still executing- Shared state (memory, peripherals) is automatically synchronized
FIFO Implementation:
typedef struct {
uint32_t buffer[FIFO_DEPTH]; // 8 entries per FIFO
uint16_t write_ptr;
uint16_t read_ptr;
uint16_t count;
} fifo_t;Spinlock Implementation:
void spinlock_acquire(uint8_t lock_id) {
while (spinlocks[lock_id].locked) {
// Spin until lock is released
}
spinlocks[lock_id].locked = 1;
}The emulator executes simple firmware at approximately:
- Simple loop: ~10,000 instructions/second (development build)
- UART output: Limited by
putchar()overhead - GPIO operations: Instant (no electrical simulation)
- Timer operations: Lightweight counter increment per cycle
- Memory access: Direct array indexing (no caching overhead)
- Dual-core: Both cores step together (no penalty)
The GPIO test executes 2M+ instructions in under 1 second. Performance is adequate for firmware debugging and testing. Optimization (JIT, caching) could achieve 100x improvement but isn't currently needed.
-
Complete NVIC Integration (estimated 3 hours)
- Implement MMIO access to NVIC registers (0xE000E000)
- Fix interrupt priority scheduling
- Implement exception return mechanism
- Enable full interrupt support
-
Enhanced Dual-Core Features
- Comprehensive dual-core test suite
- Core reset/halt control registers
- Improved FIFO status reporting
- Spinlock timeout support
-
Additional Peripherals
- UART Rx (currently Tx only)
- SPI interface
- I2C interface
- PWM generators
- PIO state machines
-
Performance Optimization
- JIT compilation for hot loops
- Instruction caching
- Optimized memory access patterns
- Cycle-accurate timing
-
Debugging Features
- Hardware breakpoints
- Watchpoints on memory locations
- Instruction-level stepping
- Register inspection utilities
- GDB integration
-
Extended Test Coverage
- Interrupt-driven firmware examples
- Multi-peripheral test suites
- Edge case handling
- Performance benchmarks
- Quality of Life
- Web-based visualization
- Real-time register display
- Memory map explorer
- Instruction statistics
The Bramble project is open for contributions! Areas that need help:
- NVIC Completion: Three clearly-defined issues ready for implementation
- Peripheral Emulation: UART Rx, SPI, I2C, PWM
- Testing: New test firmware, edge cases, performance benchmarks
- Documentation: Register descriptions, usage examples, architecture guides
See CHANGELOG.md for recent updates and docs/ for detailed technical documentation.
MIT License - See LICENSE file for details
For issues, questions, or contributions:
- Open an issue on GitHub
- Check existing documentation in docs/
- Review CHANGELOG.md for recent changes
- See NVIC Audit Report for known limitations