diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..149162c
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,91 @@
+name: Publish NuGet
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Versioning is handled by MinVer (Directory.Build.props): a `v*` tag produces a
+# stable release version, any other ref produces a preview version derived from
+# the last tag + commit height. Packages are always built and uploaded as a
+# workflow artifact; they are pushed to nuget.org only for `v*` tags (or when a
+# manual run sets push_public=true). Compatible with GitHub and Gitea runners.
+# ─────────────────────────────────────────────────────────────────────────────
+on:
+ push:
+ tags:
+ - "v*"
+ workflow_dispatch:
+ inputs:
+ push_public:
+ description: "Push to nuget.org as well? (otherwise pack + artifact only)"
+ required: false
+ default: "false"
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+
+ steps:
+ # ── Checkout ────────────────────────────────────────────────────────────
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # MinVer derives the version from the full tag history
+
+ # ── .NET setup ──────────────────────────────────────────────────────────
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "10.0.x"
+
+ # ── Restore ─────────────────────────────────────────────────────────────
+ - name: Restore
+ run: dotnet restore RP2040.sln
+
+ # ── Build ───────────────────────────────────────────────────────────────
+ - name: Build
+ run: dotnet build RP2040.sln -c Release --no-restore /p:ContinuousIntegrationBuild=true
+
+ # ── Test (unit only; integration tests download firmware) ───────────────
+ - name: Test
+ run: >
+ dotnet test tests/RP2040Sharp.Tests/RP2040Sharp.Tests.csproj
+ -c Release
+ --no-build
+ --logger "console;verbosity=normal"
+
+ # ── Pack ────────────────────────────────────────────────────────────────
+ - name: Pack RP2040Sharp
+ run: >
+ dotnet pack src/RP2040Sharp/RP2040Sharp.csproj
+ -c Release
+ --no-build
+ --output ./nupkgs
+
+ - name: Pack RP2040Sharp.TestKit
+ run: >
+ dotnet pack src/RP2040.TestKit/RP2040.TestKit.csproj
+ -c Release
+ --no-build
+ --output ./nupkgs
+
+ # ── Push to nuget.org (tags, or a manual push_public run) ───────────────
+ - name: Push to nuget.org
+ env:
+ API_KEY: ${{ secrets.NUGET_API_KEY }}
+ if: >
+ ${{ github.server_url == 'https://github.com' &&
+ env.API_KEY != '' &&
+ (startsWith(github.ref, 'refs/tags/v') ||
+ github.event.inputs.push_public == 'true') }}
+ run: >
+ dotnet nuget push "./nupkgs/*.nupkg"
+ --source https://api.nuget.org/v3/index.json
+ --api-key "${{ secrets.NUGET_API_KEY }}"
+ --skip-duplicate
+
+ # ── Always upload the packages as an artifact ───────────────────────────
+ - name: Upload NuGet Packages
+ uses: actions/upload-artifact@v4
+ with:
+ name: nupkgs
+ path: ./nupkgs/*.nupkg
+ retention-days: 14
+ if-no-files-found: error
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1e44895..3c82c93 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,48 +15,64 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
with:
- fetch-depth: 0
-
- - name: Set up JDK 17
- uses: actions/setup-java@v3
- with:
- java-version: 17
- distribution: 'zulu'
+ fetch-depth: 0 # MinVer needs full tag history for the pack step
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.100'
- - name: Install SonarCloud scanner
- run: dotnet tool install --global dotnet-sonarscanner
-
- name: Restore dependencies
run: dotnet restore
-
- - name: Begin SonarQube Analysis
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- run: |
- dotnet sonarscanner begin /k:"begeistert_RP2040Sharp" \
- /o:"begeistert" \
- /d:sonar.host.url="${SONAR_HOST_URL}" \
- /d:sonar.token="${SONAR_TOKEN}" \
- /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" \
- /d:sonar.exclusions="tests/**" \
- /d:sonar.scanner.scanAll=false
-
+
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Execute xUnit Tests
run: |
dotnet test --configuration Release --no-build --verbosity normal \
- --collect:"XPlat Code Coverage" \
- -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
+ --filter "Category!=Integration"
+
+ - name: Pack (verify packability)
+ run: dotnet pack --configuration Release --no-build --output ./nupkgs
+
+ integration-tests:
+ name: Integration Tests (${{ matrix.micropython-version }})
+ runs-on: ubuntu-latest
+ needs: unit-tests
+ strategy:
+ fail-fast: false
+ matrix:
+ micropython-version:
+ - v1.19.1
+ - v1.20.0
+ - v1.21.0
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- - name: End SonarQube Analysis
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- run: dotnet sonarscanner end /d:sonar.login="${SONAR_TOKEN}"
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.100'
+
+ - name: Cache MicroPython firmware
+ uses: actions/cache@v4
+ with:
+ path: /tmp/rp2040sharp-firmware-cache
+ key: micropython-firmware-${{ matrix.micropython-version }}
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --configuration Release --no-restore
+
+ - name: Run Integration Tests
+ run: |
+ dotnet test tests/RP2040Sharp.IntegrationTests/ \
+ --configuration Release --no-build --verbosity normal \
+ --filter "Category=Integration"
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..aa556ff
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,27 @@
+
+
+ Iván Montiel Cardona
+ begeistert
+ https://github.com/begeistert/RP2040Sharp
+ git
+ MIT
+ README.md
+ https://github.com/begeistert/RP2040Sharp
+ rp2040;raspberry-pi;emulator;cortex-m0plus;microcontroller;simulation
+ Copyright © 2024-2026 Iván Montiel Cardona
+ true
+ true
+
+ minimal
+ preview
+
+ true
+ latest
+ enable
+
+
+
+
+
+
+
diff --git a/LICENSE b/LICENSE
index 97eedc1..dea9ca0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,8 +1,8 @@
MIT License
-Copyright (c) 2025 Iván Montiel Cardona
-Copyright (c) 2025 Sergio Domínguez Rojas
-Copyright (c) 2025 Uri Shaked
+Copyright (c) 2026 Iván Montiel Cardona
+Copyright (c) 2026 Sergio Domínguez Rojas
+Copyright (c) 2026 Uri Shaked
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 9afe5e7..5cd4157 100644
--- a/README.md
+++ b/README.md
@@ -3,93 +3,174 @@



-[](https://sonarcloud.io/summary/new_code?id=begeistert_RP2040Sharp)
-**RP2040Sharp** is a high-performance emulator for the Raspberry Pi RP2040 microcontroller, written entirely in **modern C# (.NET 10)**.
+**RP2040Sharp** is a high-performance emulator for the Raspberry Pi RP2040 microcontroller, written entirely in **modern C# (.NET 10)**. It runs real RP2040 firmware — including **MicroPython** — without modification.
This project is a port and re-imagination of the excellent [rp2040js](https://github.com/wokwi/rp2040js) project by Uri Shaked. The goal is to bring embedded emulation to the .NET ecosystem with a strong focus on speed and type safety, leveraging the latest runtime features.
-> 🚧 **Project Status:** Work in Progress. The CPU core (Cortex-M0+) is under active development and passing instruction tests.
+## Performance
-## 🚀 Technical Features
+Measured on Apple Silicon (macOS, .NET 10, Release build):
-* **Architecture:** Faithful emulation of the **ARM Cortex-M0+** core.
-* **Performance:** Heavy use of `Span`, `Unsafe`, and pointers for direct emulated memory access, minimizing Garbage Collector overhead.
-* **Bus Interconnect:** Memory mapping system handling Flash, SRAM, BootROM, and Peripherals.
-* **Testing:** Robust unit test suite using **xUnit** and **FluentAssertions** to validate the Thumb instruction set.
+| Workload | Throughput |
+|---|---|
+| Tight arithmetic loop (Flash, steady-state) | **~460 MIPS** |
+| MicroPython boot | ~250 MIPS |
+| MicroPython REPL execution | ~250 MIPS |
-## 🛠️ Requirements
+The emulator boots MicroPython v1.21.0 and reaches the interactive REPL in approximately **3–4 seconds of simulated time** (wall time varies by host). On iOS/MAUI (Mono AOT, no JIT), throughput is lower but the proportional optimizations still apply.
-* **.NET 10 SDK**.
-* Visual Studio 2022 or JetBrains Rider.
+## Features
-## 📦 Solution Structure
+- **ARM Cortex-M0+** full instruction set (Thumb-1), including exceptions and NVIC
+- **Real RP2040 BootROM** (B1) — loaded as an embedded resource; `rom_table_lookup`, `memcpy44`, `memset4` and bit-manipulation helpers run natively
+- **Flash erase/program** via C# native hooks — MicroPython's LittleFS filesystem works correctly
+- **MicroPython** boots to interactive REPL over emulated USB-CDC
+- **Dual-core:** Core 1 launches via the SIO FIFO multicore handshake (RP2040 §2.8.3); both cores advance in lock-step
+- **GDB stub:** debug Core 0 with `arm-none-eabi-gdb` over `target remote :3333` (registers, memory, stepi, breakpoints)
+- **Peripherals:** GPIO, SIO, UART0/1, SPI0/1, I2C0/1 (master + slave simulation), ADC, PWM, PIO0/1, DMA, Timer, Watchdog, RTC, USB (CDC-ACM host for the MicroPython REPL), Clocks, PSM, Resets, and more
+- **Per-pin GPIO API** (`SetGpioExternalIn`, `GetGpioOutputEnable`, `GetGpioOut`) for embedding in circuit simulators
+- **TestKit** fluent API for writing firmware integration tests
-* `RP2040.Core`: The heart of the emulator. Contains the instruction decoder, registers, memory bus, and CPU logic.
-* `RP2040.Peripherals`: Implementation of hardware peripherals (UART, GPIO, PWM, etc.) *[In Development]*.
-* `RP2040.Core.Tests`: Unit tests validating opcode execution and logic.
+## Getting Started
-## 💻 Getting Started
+```bash
+git clone https://github.com/begeistert/RP2040Sharp.git
+cd RP2040Sharp
+dotnet restore
+dotnet build
+```
-1. **Clone the repository:**
- ```bash
- git clone https://github.com/begeistert/RP2040Sharp.git
- cd RP2040Sharp
- ```
+**Run the demo** (downloads MicroPython, boots it, executes REPL snippets, reports MIPS):
-2. **Restore dependencies and Build:**
- ```bash
- dotnet restore
- dotnet build
- ```
+```bash
+dotnet run --project src/RP2040Sharp.Demo -c Release
+```
-3. **Run the Tests:**
- The project includes comprehensive tests to validate arithmetic, logic, and flow control instructions.
- ```bash
- dotnet test
- ```
+**Run the tests:**
-## 🗺️ Roadmap
+```bash
+dotnet test
+```
-### Core Emulation
-- [x] Basic Instruction Decoder
-- [x] Arithmetic Operations (ADD, SUB, MUL, CMP)
-- [x] Bitwise Operations (AND, ORR, EOR, LSL, LSR)
-- [x] Flow Control (Branching, BL, BLX)
-- [x] Stack Management (PUSH, POP)
-- [ ] Exceptions and Interrupts (NVIC)
-- [ ] Dual Core Support (SIO)
+## Basic Usage
+
+```csharp
+using RP2040.Peripherals;
+
+var machine = new RP2040Machine();
+machine.LoadFlash(File.ReadAllBytes("firmware.bin"));
+
+// Capture UART output
+machine.Uart0.OnByteTransmit += b => Console.Write((char)b);
+
+// Run 125 000 cycles (1 ms at 125 MHz)
+machine.Run(125_000);
+```
+
+### TestKit
+
+```csharp
+using RP2040.TestKit;
+
+var sim = RP2040TestSimulation.Create()
+ .WithBinary(File.ReadAllBytes("firmware.bin"))
+ .AddUart(0, out var uart);
+
+sim.RunMilliseconds(100);
+Assert.Contains("Hello", uart.Text);
+```
+
+### GPIO integration (circuit simulators)
+
+```csharp
+// Inject an external signal on GP5
+machine.Sio.SetGpioExternalIn(5, high: true);
+
+// Read firmware output state
+bool isHigh = machine.Sio.GetGpioOut(3);
+bool isOutput = machine.Sio.GetGpioOutputEnable(3);
+```
+
+### Debugging with GDB
+
+Run the demo with `--gdb` to expose Core 0 over the GDB Remote Serial Protocol:
+
+```bash
+dotnet run --project src/RP2040Sharp.Demo -c Release -- --gdb
+# in another terminal:
+arm-none-eabi-gdb -ex "target remote :3333"
+```
+
+Or embed the server in your own host:
+
+```csharp
+using RP2040.Gdb;
+
+var server = new GdbTcpServer(myGdbTarget, port: 3333); // myGdbTarget : IGdbTarget
+server.Start();
+```
+
+## Solution Structure
+
+| Project | Description |
+|---|---|
+| `src/RP2040Sharp` | Core library — CPU, bus, peripherals, machine |
+| `src/RP2040.TestKit` | Fluent test harness for firmware integration tests |
+| `src/RP2040Sharp.Demo` | Demo: boots MicroPython and drives the REPL |
+
+## Architecture Notes
+
+- **Instruction decoder:** 65 536-entry flat table of `delegate*` function pointers — O(1) dispatch with no branch on opcode
+- **Bus reads:** explicit SRAM → Flash → BootROM fast paths with direct pointer arithmetic; no table indirection
+- **Native hook guard:** registered hooks are bounded by `_nativeHookMax`; Flash-region instructions skip the dictionary lookup entirely via a single uint comparison
+- **Fetch cache:** region and base pointer cached in `Run()` locals; region changes (rare) flush the cache
+
+## Roadmap
+
+### Core / CPU
+- [x] Full Thumb-1 instruction set
+- [x] Exceptions, NVIC, SysTick, PendSV
+- [x] Native hooks (BootROM ROM API, flash erase/program)
+- [x] WFI / WFE sleep with correct peripheral wakeup
+- [x] Dual-core (Core 1 launch, SIO FIFO)
+- [x] GDB stub for step-debugging firmware
### Peripherals
-- [ ] GPIO & Pin Access
-- [ ] UART (Serial Communication)
-- [ ] Timer & Alarm System
-- [ ] PWM
-- [ ] SPI / I2C
-
-### Ecosystem & Targets
-- [ ] **Native AOT Compilation:**
- - [ ] Windows (x64/arm64)
- - [ ] Linux (x64/arm64)
- - [ ] macOS (Apple Silicon)
-- [ ] **WebAssembly (WASM):** Run RP2040Sharp directly in the browser.
-- [ ] Loader for `.elf` and `.uf2` files.
-- [ ] GDB Server implementation for debugging.
-
-## 🤝 Contributing
-
-Contributions are welcome! This is a collaborative project.
-
-1. Fork the repository.
-2. Create a feature branch (`git checkout -b feature/AmazingFeature`).
-3. Ensure **all tests pass**.
-4. Commit your changes (`git commit -m 'Add some AmazingFeature'`).
-5. Push to the branch (`git push origin feature/AmazingFeature`).
-6. Open a Pull Request.
-
-## 📄 License
-
-This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
-
-Based on the original work from [rp2040js](https://github.com/wokwi/rp2040js) © 2021 Uri Shaked.
-C# Port © 2025 Iván Montiel Cardona.
\ No newline at end of file
+- [x] GPIO, SIO (spinlocks, interpolator)
+- [x] UART0 / UART1
+- [x] SPI0 / SPI1
+- [x] I2C0 / I2C1 (master + slave-mode simulation)
+- [x] ADC
+- [x] PWM (all 8 slices)
+- [x] PIO0 / PIO1 (state machines, GPIO integration)
+- [x] DMA (all channels, DREQ sources)
+- [x] USB (CDC-ACM host driver for the MicroPython REPL)
+- [x] Timer / Alarms, Watchdog, RTC
+- [x] Clocks, Resets
+- [~] XOSC, ROSC, PLL, PSM, VREG — register stubs (report stable/locked; no frequency model)
+- [ ] Flash programming via SSI (XIP hardware path)
+
+### Ecosystem
+- [x] UF2 parser in demo
+- [x] Real RP2040 B1 BootROM (embedded resource)
+- [x] MicroPython v1.21.0 boots to REPL
+- [x] Per-pin GPIO API for circuit simulator embedding
+- [ ] iCircuit element (`RP2040Elm`) — in design
+- [ ] NativeAOT targets (Windows, Linux, macOS, iOS)
+- [ ] WebAssembly (WASM) target
+
+## Contributing
+
+1. Fork the repository.
+2. Create a feature branch (`git checkout -b feature/my-feature`).
+3. Ensure all tests pass (`dotnet test`).
+4. Commit following [Conventional Commits](https://www.conventionalcommits.org/).
+5. Open a Pull Request against `master`.
+
+## License
+
+MIT License — see [LICENSE](LICENSE).
+
+Based on the original work from [rp2040js](https://github.com/wokwi/rp2040js) © 2021 Uri Shaked.
+C# Port © 2026 Iván Montiel Cardona.
diff --git a/RP2040.sln b/RP2040.sln
index a158221..04ccbaa 100644
--- a/RP2040.sln
+++ b/RP2040.sln
@@ -2,36 +2,112 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F168743D-BA33-466E-AFAF-BFC9DD2AF698}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{12B236AC-549E-45C1-B903-5DB631964EDE}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RP2040.TestKit", "src\RP2040.TestKit\RP2040.TestKit.csproj", "{6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RP2040.Core", "src\RP2040.Core\RP2040.Core.csproj", "{0CD99A82-23B4-42DD-AE63-30F24BD6948D}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RP2040Sharp", "src\RP2040Sharp\RP2040Sharp.csproj", "{64A5412F-D091-4CA1-8A03-E3217DCDD198}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RP2040.Peripherals", "src\RP2040.Peripherals\RP2040.Peripherals.csproj", "{6295E002-DAEB-4107-A209-E81AFFD4B8CA}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RP2040.Core.Tests", "tests\RP2040.Core.Tests\RP2040.Core.Tests.csproj", "{5E19B58C-4B89-4FED-82E7-706262AA90D2}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RP2040Sharp.Tests", "tests\RP2040Sharp.Tests\RP2040Sharp.Tests.csproj", "{B00740D9-7665-4FD0-8BA5-845AA7B8EB73}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RP2040Sharp.IntegrationTests", "tests\RP2040Sharp.IntegrationTests\RP2040Sharp.IntegrationTests.csproj", "{C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RP2040Sharp.Demo", "src\RP2040Sharp.Demo\RP2040Sharp.Demo.csproj", "{0B49F4F8-6B4B-40F0-978D-B8799AABC99C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RP2040Sharp.Demo.CircuitPython.Blink", "src\RP2040Sharp.Demo.CircuitPython.Blink\RP2040Sharp.Demo.CircuitPython.Blink.csproj", "{9D8E08C3-BF88-41FB-BC88-DE36F64A6157}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {0CD99A82-23B4-42DD-AE63-30F24BD6948D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0CD99A82-23B4-42DD-AE63-30F24BD6948D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0CD99A82-23B4-42DD-AE63-30F24BD6948D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0CD99A82-23B4-42DD-AE63-30F24BD6948D}.Release|Any CPU.Build.0 = Release|Any CPU
- {6295E002-DAEB-4107-A209-E81AFFD4B8CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6295E002-DAEB-4107-A209-E81AFFD4B8CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6295E002-DAEB-4107-A209-E81AFFD4B8CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6295E002-DAEB-4107-A209-E81AFFD4B8CA}.Release|Any CPU.Build.0 = Release|Any CPU
- {5E19B58C-4B89-4FED-82E7-706262AA90D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5E19B58C-4B89-4FED-82E7-706262AA90D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5E19B58C-4B89-4FED-82E7-706262AA90D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5E19B58C-4B89-4FED-82E7-706262AA90D2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Debug|x64.Build.0 = Debug|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Debug|x86.Build.0 = Debug|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Release|x64.ActiveCfg = Release|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Release|x64.Build.0 = Release|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Release|x86.ActiveCfg = Release|Any CPU
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B}.Release|x86.Build.0 = Release|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Debug|x64.Build.0 = Debug|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Debug|x86.Build.0 = Debug|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Release|Any CPU.Build.0 = Release|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Release|x64.ActiveCfg = Release|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Release|x64.Build.0 = Release|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Release|x86.ActiveCfg = Release|Any CPU
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198}.Release|x86.Build.0 = Release|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Debug|x64.Build.0 = Debug|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Debug|x86.Build.0 = Debug|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Release|x64.ActiveCfg = Release|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Release|x64.Build.0 = Release|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Release|x86.ActiveCfg = Release|Any CPU
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73}.Release|x86.Build.0 = Release|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Debug|x64.Build.0 = Debug|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Debug|x86.Build.0 = Debug|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Release|x64.ActiveCfg = Release|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Release|x64.Build.0 = Release|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Release|x86.ActiveCfg = Release|Any CPU
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347}.Release|x86.Build.0 = Release|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Debug|x64.Build.0 = Debug|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Debug|x86.Build.0 = Debug|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Release|x64.ActiveCfg = Release|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Release|x64.Build.0 = Release|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Release|x86.ActiveCfg = Release|Any CPU
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C}.Release|x86.Build.0 = Release|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Debug|x64.Build.0 = Debug|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Debug|x86.Build.0 = Debug|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Release|x64.ActiveCfg = Release|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Release|x64.Build.0 = Release|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Release|x86.ActiveCfg = Release|Any CPU
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {0CD99A82-23B4-42DD-AE63-30F24BD6948D} = {F168743D-BA33-466E-AFAF-BFC9DD2AF698}
- {6295E002-DAEB-4107-A209-E81AFFD4B8CA} = {F168743D-BA33-466E-AFAF-BFC9DD2AF698}
- {5E19B58C-4B89-4FED-82E7-706262AA90D2} = {12B236AC-549E-45C1-B903-5DB631964EDE}
+ {6DB0B64D-2D93-4CA7-BA34-F149A8AC122B} = {F168743D-BA33-466E-AFAF-BFC9DD2AF698}
+ {64A5412F-D091-4CA1-8A03-E3217DCDD198} = {F168743D-BA33-466E-AFAF-BFC9DD2AF698}
+ {B00740D9-7665-4FD0-8BA5-845AA7B8EB73} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {C5A7E891-3F2B-4D8A-9B1C-2E6F5A8D0347} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {0B49F4F8-6B4B-40F0-978D-B8799AABC99C} = {F168743D-BA33-466E-AFAF-BFC9DD2AF698}
+ {9D8E08C3-BF88-41FB-BC88-DE36F64A6157} = {F168743D-BA33-466E-AFAF-BFC9DD2AF698}
EndGlobalSection
EndGlobal
diff --git a/bootrom_gen.py b/bootrom_gen.py
new file mode 100644
index 0000000..6433887
--- /dev/null
+++ b/bootrom_gen.py
@@ -0,0 +1,113 @@
+import struct, textwrap
+
+bootrom = bytearray(16384)
+
+def u32(buf, offset, val):
+ struct.pack_into(' end
+ 0xB28B, # UXTH r3, r1 ; r3 = code & 0xFFFF
+ 0x429A, # CMP r2, r3
+ 0xD002, # BEQ found
+ 0x3004, # ADDS r0, r0, #4
+ 0xE7F9, # B loop
+ 0x8840, # LDRH r0, [r0, #2] ; found:
+ 0x4770, # BX LR
+ 0x2000, # MOVS r0, #0 ; not_found:
+ 0x4770, # BX LR
+]
+for i, op in enumerate(LOOKUP):
+ u16(bootrom, 0x0060 + i*2, op)
+
+# memcpy44 at 0x0100
+# void *memcpy44(void *dst, void *src, uint n) -- n bytes, multiple of 4
+MEMCPY = [
+ 0xB510, # PUSH {r4, lr}
+ 0x4604, # MOV r4, r0 ; save dst
+ 0xC908, # LDMIA r1!, {r3} ; loop: r3 = *src++
+ 0xC008, # STMIA r0!, {r3} ; *dst++ = r3
+ 0x3A04, # SUBS r2, r2, #4
+ 0xD1FC, # BNE loop ; offset -8 to LDMIA (index 2)
+ 0x4620, # MOV r0, r4
+ 0xBD10, # POP {r4, pc}
+]
+# BNE at index 5 (offset 10): PC_next=12, loop=4, delta=(4-12)/2=-4, 0xFC -> D1FC
+for i, op in enumerate(MEMCPY):
+ u16(bootrom, 0x0100 + i*2, op)
+
+# memset4 at 0x0120
+# void *memset4(void *dst, uint8_t c, uint n) -- n bytes, multiple of 4
+MEMSET = [
+ 0xB510, # PUSH {r4, lr}
+ 0x4604, # MOV r4, r0 ; save dst
+ 0xB249, # UXTB r1, r1 ; r1 = c & 0xFF
+ 0x020B, # LSLS r3, r1, #8 ; r3 = c << 8
+ 0x4319, # ORRS r1, r3 ; r1 = c|(c<<8)
+ 0x040B, # LSLS r3, r1, #16
+ 0x4319, # ORRS r1, r3 ; r1 = 4-byte word
+ 0xE001, # B test ; -> SUBS first
+ 0xC002, # STMIA r0!, {r1} ; loop: *dst++ = word
+ 0x3A04, # SUBS r2, r2, #4 ; test:
+ 0xD1FD, # BNE loop ; offset -6 to STMIA (index 8)
+ 0x4620, # MOV r0, r4
+ 0xBD10, # POP {r4, pc}
+]
+# BNE at index 10 (offset 20): PC_next=22, STMIA=16, delta=(16-22)/2=-3, 0xFD -> D1FD
+for i, op in enumerate(MEMSET):
+ u16(bootrom, 0x0120 + i*2, op)
+
+# Function lookup table at 0x0200: {code, ptr} pairs, terminated by {0,0}
+entries = [
+ (0x434D, 0x0100), # 'MC' -> memcpy44
+ (0x534D, 0x0120), # 'MS' -> memset4
+ (0x4649, 0x0180), # 'IF' -> connect_internal_flash (noop)
+ (0x5845, 0x0180), # 'EX' -> flash_exit_xip (noop)
+ (0x4346, 0x0180), # 'FC' -> flash_flush_cache (noop)
+ (0x5843, 0x0180), # 'CX' -> flash_enter_cmd_xip (noop)
+ (0x5052, 0x0180), # 'RP' -> flash_range_program (noop)
+ (0x4546, 0x0180), # 'RE' -> flash_range_erase (noop)
+ (0x0000, 0x0000), # terminator
+]
+for i, (code, ptr) in enumerate(entries):
+ u16(bootrom, 0x0200 + i*4, code)
+ u16(bootrom, 0x0202 + i*4, ptr)
+
+# Data table at 0x0250: just terminator
+u16(bootrom, 0x0250, 0x0000)
+
+# Emit as C# array
+print("new byte[]")
+print("{")
+for row in range(0, len(bootrom), 16):
+ chunk = bootrom[row:row+16]
+ hex_str = ', '.join(f'0x{b:02X}' for b in chunk)
+ print(f" {hex_str},")
+print("}")
diff --git a/src/RP2040.Core/Cpu/CortexM0Plus.cs b/src/RP2040.Core/Cpu/CortexM0Plus.cs
deleted file mode 100644
index dd23ced..0000000
--- a/src/RP2040.Core/Cpu/CortexM0Plus.cs
+++ /dev/null
@@ -1,252 +0,0 @@
-using System.Runtime.CompilerServices;
-using RP2040.Core.Memory;
-
-[module: SkipLocalsInit]
-
-namespace RP2040.Core.Cpu;
-
-public unsafe class CortexM0Plus
-{
- public readonly BusInterconnect Bus;
- public Registers Registers;
- public long Cycles;
-
- private readonly InstructionDecoder _decoder;
-
- private byte* _fetchPtr;
- private uint _fetchMask;
- private uint _currentRegionId;
-
- private const uint EXC_RETURN_HANDLER = 0xFFFFFFF1; // Return to Handler mode, using MSP
- private const uint EXC_RETURN_THREAD_MSP = 0xFFFFFFF9; // Return to Thread mode, using MSP
- private const uint EXC_RETURN_THREAD_PSP = 0xFFFFFFFD; // Return to Thread mode, using PSP
-
- public CortexM0Plus(BusInterconnect bus)
- {
- Bus = bus;
- _decoder = InstructionDecoder.Instance;
- Reset();
- }
-
- public void Reset()
- {
- Registers.SP = Bus.ReadWord(0x00000000);
- Registers.PC = Bus.ReadWord(0x00000004);
-
- UpdateFetchCache(Registers.PC);
-
- Registers.N = false;
- Registers.Z = false;
- Registers.C = false;
- Registers.V = false;
-
- Cycles = 0;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void UpdateFetchCache(uint pc)
- {
- _currentRegionId = pc >> 28;
-
- switch (_currentRegionId)
- {
- case BusInterconnect.REGION_FLASH:
- _fetchPtr = Bus.PtrFlash;
- _fetchMask = BusInterconnect.MASK_FLASH & ~1u;
- break;
- case BusInterconnect.REGION_SRAM:
- _fetchPtr = Bus.PtrSram;
- _fetchMask = BusInterconnect.MASK_SRAM & ~1u;
- break;
- case BusInterconnect.REGION_BOOTROM:
- _fetchPtr = Bus.PtrBootRom;
- _fetchMask = BusInterconnect.MASK_BOOTROM & ~1u;
- break;
- default:
- _fetchPtr = null;
- break;
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveOptimization)]
- public void Run(int instructions)
- {
- var decoder = _decoder;
-
- var fetchPtr = _fetchPtr;
- var fetchMask = _fetchMask;
- var regionId = _currentRegionId;
-
- while (instructions-- > 0)
- {
- var pc = Registers.PC;
-
- // FAST GUARD
- if ((pc >> 28) != regionId)
- {
- // FALLBACK
- UpdateFetchCache(pc);
-
- fetchPtr = _fetchPtr;
- fetchMask = _fetchMask;
- regionId = _currentRegionId;
-
- if (fetchPtr == null)
- break;
- }
-
- // ULTRA-FAST FETCH
- var opcode = Unsafe.ReadUnaligned(fetchPtr + (pc & fetchMask));
-
- // PRE-UPDATE PC (Speculative)
- Registers.PC = pc + 2;
-
- Cycles++;
-
- // DISPATCH
- decoder.Dispatch(opcode, this);
- }
-
- _currentRegionId = regionId;
- _fetchPtr = fetchPtr;
- _fetchMask = fetchMask;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void Step()
- {
- var pc = Registers.PC;
- var opcode = Bus.ReadHalfWord(pc);
- Registers.PC = pc + 2;
- Cycles++;
- _decoder.Dispatch(opcode, this);
- }
-
- [MethodImpl(MethodImplOptions.NoInlining)] // NoInlining (it is not used commonly)
- public void UpdateStackPointerSource()
- {
- if (Registers.IPSR != 0)
- return;
-
- var switchToPsp = (Registers.CONTROL & 2) != 0;
-
- if (switchToPsp)
- {
- Registers.MSP_Storage = Registers.SP;
- Registers.SP = Registers.PSP_Storage;
- }
- else
- {
- Registers.PSP_Storage = Registers.SP;
- Registers.SP = Registers.MSP_Storage;
- }
- }
-
- [MethodImpl(MethodImplOptions.NoInlining)]
- public void ExceptionEntry(uint exceptionNumber)
- {
- var framePtr = Registers.SP;
-
- var needsAlign = (framePtr & 4) != 0;
- var framePtrAlign = needsAlign ? 1u : 0u;
-
- var stackAdjust = 0x20u + (needsAlign ? 4u : 0u);
- var finalSp = framePtr - stackAdjust;
-
- var frameBase = finalSp;
-
- Bus.WriteWord(frameBase + 0x00, Registers.R0);
- Bus.WriteWord(frameBase + 0x04, Registers.R1);
- Bus.WriteWord(frameBase + 0x08, Registers.R2);
- Bus.WriteWord(frameBase + 0x0C, Registers.R3);
- Bus.WriteWord(frameBase + 0x10, Registers.R12);
- Bus.WriteWord(frameBase + 0x14, Registers.LR);
- Bus.WriteWord(frameBase + 0x18, Registers.PC & 0xFFFFFFFE); // Return Address
-
- var xpsr = Registers.GetxPsr() | (framePtrAlign << 9);
- Bus.WriteWord(frameBase + 0x1C, xpsr);
-
- if (Registers.IPSR > 0)
- {
- Registers.LR = EXC_RETURN_HANDLER;
- }
- else
- {
- Registers.LR =
- (Registers.CONTROL & 2) != 0 ? EXC_RETURN_THREAD_PSP : EXC_RETURN_THREAD_MSP;
- }
-
- if ((Registers.CONTROL & 2) != 0)
- {
- Registers.PSP_Storage = finalSp;
- Registers.SP = Registers.MSP_Storage;
- }
- else
- {
- Registers.SP = finalSp;
- }
-
- Registers.IPSR = exceptionNumber;
- Registers.CONTROL &= ~2u;
-
- uint vtor = 0; // TODO: Read from Registers.VTOR or PPB
- var vectorAddress = vtor + (exceptionNumber * 4);
-
- var targetPc = Bus.ReadWord(vectorAddress);
- Registers.PC = targetPc & 0xFFFFFFFE;
-
- Cycles += 12; // Exception Entry cost (aprox 12-15 cycles)
- }
-
- [MethodImpl(MethodImplOptions.NoInlining)]
- public void ExceptionReturn(uint excReturn)
- {
- var returnToThread = (excReturn & 8) != 0;
- var usePsp = (excReturn & 4) != 0;
-
- if (!returnToThread && usePsp)
- {
- usePsp = false;
- }
-
- if (returnToThread)
- {
- Registers.IPSR = 0;
-
- if (usePsp)
- {
- Registers.MSP_Storage = Registers.SP;
- Registers.SP = Registers.PSP_Storage;
- Registers.CONTROL |= 2;
- }
- else
- {
- Registers.CONTROL &= ~2u;
- }
- }
-
- var framePtr = Registers.SP;
-
- Registers.R0 = Bus.ReadWord(framePtr + 0x00);
- Registers.R1 = Bus.ReadWord(framePtr + 0x04);
- Registers.R2 = Bus.ReadWord(framePtr + 0x08);
- Registers.R3 = Bus.ReadWord(framePtr + 0x0C);
- Registers.R12 = Bus.ReadWord(framePtr + 0x10);
- Registers.LR = Bus.ReadWord(framePtr + 0x14);
- var retPC = Bus.ReadWord(framePtr + 0x18);
- var xpsr = Bus.ReadWord(framePtr + 0x1C);
-
- Registers.N = (xpsr & 0x80000000) != 0;
- Registers.Z = (xpsr & 0x40000000) != 0;
- Registers.C = (xpsr & 0x20000000) != 0;
- Registers.V = (xpsr & 0x10000000) != 0;
-
- var alignAdjust = (xpsr & (1 << 9)) != 0;
- var stackFree = 0x20u + (alignAdjust ? 4u : 0u);
-
- Registers.SP += stackFree;
- Registers.PC = retPC & 0xFFFFFFFE;
-
- Cycles += 10;
- }
-}
diff --git a/src/RP2040.Core/Cpu/Instructions/MemoryOps.cs b/src/RP2040.Core/Cpu/Instructions/MemoryOps.cs
deleted file mode 100644
index 1cadf8c..0000000
--- a/src/RP2040.Core/Cpu/Instructions/MemoryOps.cs
+++ /dev/null
@@ -1,272 +0,0 @@
-using System.Numerics;
-using System.Runtime.CompilerServices;
-using RP2040.Core.Memory;
-
-namespace RP2040.Core.Cpu.Instructions;
-
-public static unsafe class MemoryOps
-{
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void LdrImmediate(ushort opcode, CortexM0Plus cpu)
- {
- var rt = opcode & 0x7;
- var rn = (opcode >> 3) & 0x7;
- var imm5 = (uint)((opcode >> 6) & 0x1F) << 2;
-
- cpu.Registers[rt] = ReadWordWithCycles(cpu, cpu.Registers[rn] + imm5);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void LdrLiteral(ushort opcode, CortexM0Plus cpu)
- {
- var rt = (opcode >> 8) & 0x7;
- var imm8 = (uint)(opcode & 0xFF) << 2;
- var nextPc = cpu.Registers.PC + 2;
- var addr = (nextPc & 0xFFFFFFFC) + imm8;
- cpu.Registers[rt] = ReadWordWithCycles(cpu, addr);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void LdrRegister(ushort opcode, CortexM0Plus cpu)
- {
- var rt = opcode & 0x7;
- var rn = (opcode >> 3) & 0x7;
- var rm = (opcode >> 6) & 0x7;
- cpu.Registers[rt] = ReadWordWithCycles(cpu, cpu.Registers[rn] + cpu.Registers[rm]);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void LdrSpRelative(ushort opcode, CortexM0Plus cpu)
- {
- var rt = (opcode >> 8) & 0x7;
- var imm8 = (uint)(opcode & 0xFF) << 2;
- cpu.Registers[rt] = ReadWordWithCycles(cpu, cpu.Registers.SP + imm8);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Pop(ushort opcode, CortexM0Plus cpu)
- {
- var mask = (uint)(opcode & 0xFF);
- var regCount = (uint)BitOperations.PopCount(mask);
-
- var sp = cpu.Registers.SP;
- var finalSp = sp + (regCount * 4);
-
- if ((sp >> 28) == BusInterconnect.REGION_SRAM)
- {
- var rawPtr = cpu.Bus.PtrSram + (sp & BusInterconnect.MASK_SRAM);
-
- while (mask != 0)
- {
- var regIdx = BitOperations.TrailingZeroCount(mask);
- cpu.Registers[regIdx] = Unsafe.ReadUnaligned(rawPtr);
-
- rawPtr += 4;
- mask &= (mask - 1);
- }
- }
- else
- {
- while (mask != 0)
- {
- var regIdx = BitOperations.TrailingZeroCount(mask);
- cpu.Registers[regIdx] = cpu.Bus.ReadWord(sp);
- sp += 4;
- mask &= (mask - 1);
- }
- }
-
- cpu.Registers.SP = finalSp;
- cpu.Cycles += 1 + regCount;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void PopPc(ushort opcode, CortexM0Plus cpu)
- {
- var mask = (uint)(opcode & 0xFF);
- var regCount = (uint)BitOperations.PopCount(mask);
-
- var sp = cpu.Registers.SP;
- var finalSp = sp + ((regCount + 1) * 4);
-
- if ((sp >> 28) == BusInterconnect.REGION_SRAM)
- {
- var rawPtr = cpu.Bus.PtrSram + (sp & BusInterconnect.MASK_SRAM);
-
- while (mask != 0)
- {
- var regIdx = BitOperations.TrailingZeroCount(mask);
- cpu.Registers[regIdx] = Unsafe.ReadUnaligned(rawPtr);
- rawPtr += 4;
- mask &= (mask - 1);
- }
- var newPc = Unsafe.ReadUnaligned(rawPtr);
-
- cpu.Registers.PC = newPc & 0xFFFFFFFE;
- }
- else
- {
- while (mask != 0)
- {
- var regIdx = BitOperations.TrailingZeroCount(mask);
- cpu.Registers[regIdx] = cpu.Bus.ReadWord(sp);
- sp += 4;
- mask &= (mask - 1);
- }
- var newPc = cpu.Bus.ReadWord(sp);
- cpu.Registers.PC = newPc & 0xFFFFFFFE;
- }
-
- cpu.Registers.SP = finalSp;
- cpu.Cycles += 4 + regCount;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Push(ushort opcode, CortexM0Plus cpu)
- {
- var mask = (uint)(opcode & 0xFF);
- var regCount = (uint)BitOperations.PopCount(mask);
- var totalBytes = regCount * 4;
-
- var oldSp = cpu.Registers.SP;
- var newSp = oldSp - totalBytes;
-
- if ((newSp >> 28) == BusInterconnect.REGION_SRAM)
- {
- var rawPtr = cpu.Bus.PtrSram + (newSp & BusInterconnect.MASK_SRAM);
-
- while (mask != 0)
- {
- var regIdx = BitOperations.TrailingZeroCount(mask);
-
- Unsafe.WriteUnaligned(rawPtr, cpu.Registers[regIdx]);
-
- rawPtr += 4;
- mask &= (mask - 1);
- }
- }
- else
- {
- var writePtr = newSp;
- while (mask != 0)
- {
- var regIdx = BitOperations.TrailingZeroCount(mask);
- var val = cpu.Registers[regIdx];
- cpu.Bus.WriteWord(writePtr, val);
-
- writePtr += 4;
- mask &= (mask - 1);
- }
- }
-
- cpu.Registers.SP = newSp;
- cpu.Cycles += regCount;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void PushLr(ushort opcode, CortexM0Plus cpu)
- {
- var mask = (uint)(opcode & 0xFF);
- var regCount = (uint)BitOperations.PopCount(mask);
- var totalBytes = (regCount + 1) * 4; // +1 because of LR
-
- var oldSp = cpu.Registers.SP;
- var newSp = oldSp - totalBytes;
-
- if ((newSp >> 28) == BusInterconnect.REGION_SRAM)
- {
- var rawPtr = cpu.Bus.PtrSram + (newSp & BusInterconnect.MASK_SRAM);
-
- while (mask != 0)
- {
- var regIdx = BitOperations.TrailingZeroCount(mask);
- Unsafe.WriteUnaligned(rawPtr, cpu.Registers[regIdx]);
- rawPtr += 4;
- mask &= (mask - 1);
- }
- Unsafe.WriteUnaligned(rawPtr, cpu.Registers.LR);
- }
- else
- {
- var writePtr = newSp;
- while (mask != 0)
- {
- var regIdx = BitOperations.TrailingZeroCount(mask);
- cpu.Bus.WriteWord(writePtr, cpu.Registers[regIdx]);
- writePtr += 4;
- mask &= (mask - 1);
- }
- cpu.Bus.WriteWord(writePtr, cpu.Registers.LR);
- }
-
- cpu.Registers.SP = newSp;
- cpu.Cycles += regCount + 1;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Ldmia(ushort opcode, CortexM0Plus cpu)
- {
- var rn = (opcode >> 8) & 0x7;
- var mask = (uint)(opcode & 0xFF);
-
- var regCount = (uint)BitOperations.PopCount(mask);
- var baseAddr = cpu.Registers[rn];
-
- var isRnInList = (mask >> rn) & 1;
- var writeBackOffset = (regCount * 4) * (isRnInList ^ 1);
-
- if ((baseAddr >> 28) == BusInterconnect.REGION_SRAM)
- {
- var ptr = cpu.Bus.PtrSram + (baseAddr & BusInterconnect.MASK_SRAM);
-
- while (mask != 0)
- {
- var regIdx = BitOperations.TrailingZeroCount(mask);
- cpu.Registers[regIdx] = Unsafe.ReadUnaligned(ptr);
-
- ptr += 4;
- mask &= (mask - 1);
- }
- }
- else // SLOW PATH
- {
- var readPtr = baseAddr;
- while (mask != 0)
- {
- var regIdx = BitOperations.TrailingZeroCount(mask);
- cpu.Registers[regIdx] = cpu.Bus.ReadWord(readPtr);
-
- readPtr += 4;
- mask &= (mask - 1);
- }
- }
-
- cpu.Registers[rn] += writeBackOffset;
- cpu.Cycles += (int)regCount;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static uint ReadWordWithCycles(CortexM0Plus cpu, uint address)
- {
- var region = address >> 28;
-
- switch (region)
- {
- case <= BusInterconnect.REGION_SRAM:
- cpu.Cycles += 1;
- break;
- case 0x4: // APB/AHB
- case 0x5:
- cpu.Cycles += 2;
- break;
- // SIO (Single-cycle IO)
- case 0xD:
- break;
- default:
- cpu.Cycles += 1; // Fallback
- break;
- }
-
- return cpu.Bus.ReadWord(address);
- }
-}
diff --git a/src/RP2040.Core/Memory/IMemoryMappedDevice.cs b/src/RP2040.Core/Memory/IMemoryMappedDevice.cs
deleted file mode 100644
index 547ac59..0000000
--- a/src/RP2040.Core/Memory/IMemoryMappedDevice.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace RP2040.Core.Memory;
-
-public interface IMemoryMappedDevice
-{
- uint Size { get; }
-
- byte ReadByte(uint address);
- ushort ReadHalfWord(uint address);
- uint ReadWord(uint address);
-
- void WriteByte(uint address, byte value);
- void WriteHalfWord(uint address, ushort value);
- void WriteWord(uint address, uint value);
-}
diff --git a/src/RP2040.Peripherals/RP2040.Peripherals.csproj b/src/RP2040.Peripherals/RP2040.Peripherals.csproj
deleted file mode 100644
index b7eb9cb..0000000
--- a/src/RP2040.Peripherals/RP2040.Peripherals.csproj
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
- net10.0
- enable
- enable
-
-
diff --git a/src/RP2040.TestKit/Assertions/CortexM0Assertions.cs b/src/RP2040.TestKit/Assertions/CortexM0Assertions.cs
new file mode 100644
index 0000000..fae3ac9
--- /dev/null
+++ b/src/RP2040.TestKit/Assertions/CortexM0Assertions.cs
@@ -0,0 +1,82 @@
+using FluentAssertions;
+using FluentAssertions.Execution;
+using FluentAssertions.Primitives;
+using RP2040.Core.Cpu;
+
+namespace RP2040.TestKit.Assertions;
+
+/// FluentAssertions extension for .
+public sealed class CortexM0Assertions : ReferenceTypeAssertions
+{
+ private readonly AssertionChain _chain;
+
+ public CortexM0Assertions(CortexM0Plus subject, AssertionChain chain) : base(subject, chain)
+ => _chain = chain;
+
+ protected override string Identifier => "cpu";
+
+ public AndConstraint HaveRegister(int index, uint expected,
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.Registers[index] == expected)
+ .FailWith("Expected R{0} to be 0x{1:X8}{reason}, but found 0x{2:X8}.",
+ index, expected, Subject.Registers[index]);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HavePC(uint expected,
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.Registers.PC == expected)
+ .FailWith("Expected PC to be 0x{0:X8}{reason}, but found 0x{1:X8}.",
+ expected, Subject.Registers.PC);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveSP(uint expected,
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.Registers.SP == expected)
+ .FailWith("Expected SP to be 0x{0:X8}{reason}, but found 0x{1:X8}.",
+ expected, Subject.Registers.SP);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveCycles(long expected,
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.Cycles == expected)
+ .FailWith("Expected Cycles to be {0}{reason}, but found {1}.",
+ expected, Subject.Cycles);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveZeroFlag(bool expected,
+ string because = "", params object[] becauseArgs)
+ => HaveFlag("Z", Subject.Registers.Z, expected, because, becauseArgs);
+
+ public AndConstraint HaveCarryFlag(bool expected,
+ string because = "", params object[] becauseArgs)
+ => HaveFlag("C", Subject.Registers.C, expected, because, becauseArgs);
+
+ public AndConstraint HaveNegativeFlag(bool expected,
+ string because = "", params object[] becauseArgs)
+ => HaveFlag("N", Subject.Registers.N, expected, because, becauseArgs);
+
+ public AndConstraint HaveOverflowFlag(bool expected,
+ string because = "", params object[] becauseArgs)
+ => HaveFlag("V", Subject.Registers.V, expected, because, becauseArgs);
+
+ private AndConstraint HaveFlag(string name, bool actual, bool expected,
+ string because, object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(actual == expected)
+ .FailWith("Expected flag {0} to be {1}{reason}, but found {2}.", name, expected, actual);
+ return new AndConstraint(this);
+ }
+}
diff --git a/src/RP2040.TestKit/Assertions/GpioAssertions.cs b/src/RP2040.TestKit/Assertions/GpioAssertions.cs
new file mode 100644
index 0000000..a3d0114
--- /dev/null
+++ b/src/RP2040.TestKit/Assertions/GpioAssertions.cs
@@ -0,0 +1,67 @@
+using FluentAssertions;
+using FluentAssertions.Execution;
+using FluentAssertions.Primitives;
+using RP2040.Peripherals.Gpio;
+
+namespace RP2040.TestKit.Assertions;
+
+/// FluentAssertions extension for .
+public sealed class GpioAssertions : ReferenceTypeAssertions
+{
+ private readonly AssertionChain _chain;
+
+ public GpioAssertions(GpioPin subject, AssertionChain chain) : base(subject, chain)
+ => _chain = chain;
+
+ protected override string Identifier => "pin";
+
+ public AndConstraint BeHigh(
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.DigitalValue)
+ .FailWith("Expected GPIO pin to be HIGH{reason}, but it was LOW.");
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint BeLow(
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(!Subject.DigitalValue)
+ .FailWith("Expected GPIO pin to be LOW{reason}, but it was HIGH.");
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint BeOutput(
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.IsOutput)
+ .FailWith("Expected GPIO pin to be configured as OUTPUT{reason}, but it was INPUT.");
+ return new AndConstraint(this);
+ }
+
+ ///
+ /// Assert that the pin is assigned to a PIO state machine (FUNCSEL = 6 or 7).
+ /// Use this for pins configured via pio_gpio_init(), which sets IO_BANK0 FUNCSEL
+ /// rather than SIO GPIO_OE (which checks).
+ ///
+ public AndConstraint BePioOutput(
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.IsPioOutput)
+ .FailWith("Expected GPIO pin to be assigned to a PIO state machine (FUNCSEL=6 or 7){reason}, but it was not.");
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint BeInput(
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(!Subject.IsOutput)
+ .FailWith("Expected GPIO pin to be configured as INPUT{reason}, but it was OUTPUT.");
+ return new AndConstraint(this);
+ }
+}
diff --git a/src/RP2040.TestKit/Assertions/UartProbeAssertions.cs b/src/RP2040.TestKit/Assertions/UartProbeAssertions.cs
new file mode 100644
index 0000000..2e2d233
--- /dev/null
+++ b/src/RP2040.TestKit/Assertions/UartProbeAssertions.cs
@@ -0,0 +1,85 @@
+using FluentAssertions;
+using FluentAssertions.Execution;
+using FluentAssertions.Primitives;
+using RP2040.TestKit.Probes;
+
+namespace RP2040.TestKit.Assertions;
+
+/// FluentAssertions extension for .
+public sealed class UartProbeAssertions : ReferenceTypeAssertions
+{
+ private readonly AssertionChain _chain;
+
+ public UartProbeAssertions(UartProbe subject, AssertionChain chain) : base(subject, chain)
+ => _chain = chain;
+
+ protected override string Identifier => "uart";
+
+ public AndConstraint Contain(string expected,
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.Text.Contains(expected))
+ .FailWith("Expected UART output to contain {0}{reason}, but found {1}.",
+ expected, Subject.Text);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint NotContain(string expected,
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(!Subject.Text.Contains(expected))
+ .FailWith("Expected UART output not to contain {0}{reason}, but found {1}.",
+ expected, Subject.Text);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint StartWith(string expected,
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.Text.StartsWith(expected, StringComparison.Ordinal))
+ .FailWith("Expected UART output to start with {0}{reason}, but found {1}.",
+ expected, Subject.Text);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint BeEmpty(
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.ByteCount == 0)
+ .FailWith("Expected UART to have no output{reason}, but found {0} bytes.", Subject.ByteCount);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveByteCount(int expected,
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.ByteCount == expected)
+ .FailWith("Expected UART to have {0} bytes{reason}, but found {1}.",
+ expected, Subject.ByteCount);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint ContainLine(string expected,
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.Lines.Contains(expected))
+ .FailWith("Expected UART output to contain line {0}{reason}.", expected);
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveLineCount(int expected,
+ string because = "", params object[] becauseArgs)
+ {
+ _chain.BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.Lines.Count == expected)
+ .FailWith("Expected UART output to have {0} lines{reason}, but found {1}.",
+ expected, Subject.Lines.Count);
+ return new AndConstraint(this);
+ }
+}
diff --git a/src/RP2040.TestKit/Boards/PicoSimulation.cs b/src/RP2040.TestKit/Boards/PicoSimulation.cs
new file mode 100644
index 0000000..deacce0
--- /dev/null
+++ b/src/RP2040.TestKit/Boards/PicoSimulation.cs
@@ -0,0 +1,58 @@
+using RP2040.Peripherals.Gpio;
+using RP2040.TestKit.Probes;
+
+namespace RP2040.TestKit.Boards;
+
+///
+/// Pre-configured simulation of a Raspberry Pi Pico board (125 MHz, UART0/1 probed, GPIO 0-29).
+///
+///
+/// using var pico = new PicoSimulation();
+/// pico.LoadFlash(firmware);
+/// pico.RunMilliseconds(100);
+/// pico.Uart0.Should().Contain("Hello");
+/// pico.Gpio[25].Should().BeHigh("onboard LED should be on");
+///
+///
+///
+public sealed class PicoSimulation : RP2040TestSimulation
+{
+ /// Probe for UART0 (GP0/GP1).
+ public UartProbe Uart0 { get; }
+
+ /// Probe for UART1 (GP4/GP5).
+ public UartProbe Uart1 { get; }
+
+ /// Auto-enumerated USB CDC-ACM channel (TinyUSB-compatible).
+ public UsbCdcProbe UsbCdc { get; }
+
+ /// All 30 GPIO pins.
+ public IReadOnlyList Gpio => Machine.Gpio;
+
+ public PicoSimulation(bool withUsbCdc = true)
+ {
+ WithFrequency(125_000_000);
+ AddUart(0, out var u0);
+ AddUart(1, out var u1);
+ Uart0 = u0;
+ Uart1 = u1;
+
+ if (withUsbCdc)
+ {
+ AddUsbCdc(out var cdc);
+ UsbCdc = cdc;
+ }
+ else
+ {
+ // Leave USB unattached so the device sees no USB host.
+ UsbCdc = new UsbCdcProbe();
+ }
+ }
+
+ /// Load firmware into Flash and reset.
+ public PicoSimulation LoadFlash(ReadOnlySpan bytes)
+ {
+ WithBinary(bytes);
+ return this;
+ }
+}
diff --git a/src/RP2040.TestKit/Extensions/AssertionExtensions.cs b/src/RP2040.TestKit/Extensions/AssertionExtensions.cs
new file mode 100644
index 0000000..94be057
--- /dev/null
+++ b/src/RP2040.TestKit/Extensions/AssertionExtensions.cs
@@ -0,0 +1,22 @@
+using FluentAssertions.Execution;
+using RP2040.Core.Cpu;
+using RP2040.Peripherals.Gpio;
+using RP2040.TestKit.Assertions;
+using RP2040.TestKit.Probes;
+
+namespace RP2040.TestKit.Extensions;
+
+///
+/// .Should() extension methods for RP2040 simulation types.
+///
+public static class AssertionExtensions
+{
+ public static CortexM0Assertions Should(this CortexM0Plus cpu)
+ => new(cpu, AssertionChain.GetOrCreate());
+
+ public static UartProbeAssertions Should(this UartProbe probe)
+ => new(probe, AssertionChain.GetOrCreate());
+
+ public static GpioAssertions Should(this GpioPin pin)
+ => new(pin, AssertionChain.GetOrCreate());
+}
diff --git a/src/RP2040.TestKit/Probes/UartProbe.cs b/src/RP2040.TestKit/Probes/UartProbe.cs
new file mode 100644
index 0000000..73bb3c9
--- /dev/null
+++ b/src/RP2040.TestKit/Probes/UartProbe.cs
@@ -0,0 +1,67 @@
+using System.Text;
+using RP2040.Peripherals.Uart;
+
+namespace RP2040.TestKit.Probes;
+
+///
+/// Captures bytes transmitted by a UART peripheral and allows injecting bytes into the RX FIFO.
+/// Attach to a via .
+///
+public sealed class UartProbe
+{
+ private readonly List _bytes = [];
+ private string? _textCache;
+ private string[]? _linesCache;
+
+ /// All bytes transmitted so far (Latin-1 encoded).
+ public IReadOnlyList Bytes => _bytes;
+
+ /// Number of bytes captured.
+ public int ByteCount => _bytes.Count;
+
+ /// Transmitted bytes decoded as Latin-1 text.
+ public string Text => _textCache ??= Encoding.Latin1.GetString(_bytes.ToArray());
+
+ /// Lines split on LF (CR stripped), cached until next byte arrives.
+ public IReadOnlyList Lines
+ => _linesCache ??= Text.Split('\n')
+ .Select(l => l.TrimEnd('\r'))
+ .ToArray();
+
+ private UartPeripheral? _uart;
+
+ /// Attach this probe to a UART peripheral.
+ public UartProbe Attach(UartPeripheral uart)
+ {
+ if (_uart != null)
+ _uart.OnByteTransmit -= Capture;
+ _uart = uart;
+ _uart.OnByteTransmit += Capture;
+ return this;
+ }
+
+ /// Inject a byte as if received from a remote device.
+ public void InjectByte(byte value) => _uart?.InjectByte(value);
+
+ /// Inject a string as Latin-1 bytes.
+ public void InjectString(string text)
+ {
+ foreach (var b in Encoding.Latin1.GetBytes(text))
+ _uart?.InjectByte(b);
+ }
+
+ /// Clear captured data.
+ public void Clear()
+ {
+ _bytes.Clear();
+ _textCache = null;
+ _linesCache = null;
+ }
+
+ private void Capture(byte b)
+ {
+ _bytes.Add(b);
+ _textCache = null;
+ _linesCache = null;
+ }
+}
diff --git a/src/RP2040.TestKit/Probes/UsbCdcProbe.cs b/src/RP2040.TestKit/Probes/UsbCdcProbe.cs
new file mode 100644
index 0000000..2bfdbe9
--- /dev/null
+++ b/src/RP2040.TestKit/Probes/UsbCdcProbe.cs
@@ -0,0 +1,66 @@
+using System.Runtime.InteropServices;
+using System.Text;
+using RP2040.Peripherals.Usb;
+
+namespace RP2040.TestKit.Probes;
+
+///
+/// Captures bytes that the device transmits over USB-CDC and exposes a writer
+/// that pushes data into the host-to-device direction. Attach to a
+/// via .
+///
+public sealed class UsbCdcProbe
+{
+ private readonly List _bytes = [];
+ private string? _textCache;
+ private string[]? _linesCache;
+
+ public IReadOnlyList Bytes => _bytes;
+ public int ByteCount => _bytes.Count;
+
+ ///
+ /// All captured output as a Latin-1 string.
+ /// Decodes directly from the list's backing buffer via
+ /// to avoid the O(n²) allocation that _bytes.ToArray() would cause on every cache miss
+ /// during high-throughput CDC streams (e.g. MicroPython boot).
+ ///
+ public string Text => _textCache ??= Encoding.Latin1.GetString(CollectionsMarshal.AsSpan(_bytes));
+
+ public IReadOnlyList Lines
+ => _linesCache ??= Text.Split('\n').Select(l => l.TrimEnd('\r')).ToArray();
+
+ /// True after the host has completed enumeration and SET_CONTROL_LINE_STATE.
+ public bool IsConnected => _cdc?.IsConnected ?? false;
+
+ private UsbCdcHost? _cdc;
+
+ public UsbCdcProbe Attach(UsbCdcHost cdc)
+ {
+ if (_cdc != null) _cdc.OnSerialData -= Capture;
+ _cdc = cdc;
+ _cdc.OnSerialData += Capture;
+ return this;
+ }
+
+ public void InjectByte(byte value) => _cdc?.SendSerialByte(value);
+
+ public void InjectString(string text)
+ {
+ if (_cdc == null) return;
+ foreach (var b in Encoding.Latin1.GetBytes(text)) _cdc.SendSerialByte(b);
+ }
+
+ public void Clear()
+ {
+ _bytes.Clear();
+ _textCache = null;
+ _linesCache = null;
+ }
+
+ private void Capture(byte[] data)
+ {
+ _bytes.AddRange(data);
+ _textCache = null;
+ _linesCache = null;
+ }
+}
diff --git a/src/RP2040.TestKit/RP2040.TestKit.csproj b/src/RP2040.TestKit/RP2040.TestKit.csproj
new file mode 100644
index 0000000..a158a0d
--- /dev/null
+++ b/src/RP2040.TestKit/RP2040.TestKit.csproj
@@ -0,0 +1,15 @@
+
+
+ RP2040Sharp.TestKit
+ Test utilities and helpers for writing unit tests against RP2040Sharp emulator firmware.
+ true
+ net10.0
+ enable
+
+
+
+
+
+
+
+
diff --git a/src/RP2040.TestKit/RP2040TestSimulation.cs b/src/RP2040.TestKit/RP2040TestSimulation.cs
new file mode 100644
index 0000000..12ce8d1
--- /dev/null
+++ b/src/RP2040.TestKit/RP2040TestSimulation.cs
@@ -0,0 +1,268 @@
+using RP2040.Peripherals;
+using RP2040.Peripherals.Gpio;
+using RP2040.Peripherals.Uart;
+using RP2040.Peripherals.Usb;
+using RP2040.TestKit.Probes;
+
+namespace RP2040.TestKit;
+
+///
+/// Fluent test harness for the RP2040 emulator.
+///
+///
+/// var sim = RP2040TestSimulation.Create()
+/// .WithFrequency(125_000_000)
+/// .WithBinary(flashBytes)
+/// .AddUart(0, out var uart);
+///
+/// sim.RunMilliseconds(10);
+/// uart.Should().Contain("Hello");
+///
+///
+///
+public class RP2040TestSimulation : IDisposable
+{
+ protected readonly RP2040Machine Machine;
+
+ /// Direct CPU access for low-level assertions.
+ public RP2040.Core.Cpu.CortexM0Plus Cpu => Machine.Cpu;
+ /// Core 1 CPU (only executing after firmware launches it via SIO FIFO).
+ public RP2040.Core.Cpu.CortexM0Plus Cpu1 => Machine.Cpu1;
+
+ ///
+ /// Direct access to the RP2040 machine for advanced probe scenarios
+ /// (e.g. attaching SPI callbacks, injecting GPIO signals).
+ ///
+ public RP2040Machine Rp2040 => Machine;
+
+ private uint _clkHz = RP2040Machine.CLK_HZ;
+
+ ///
+ /// Sequence of BKPT immediate values recorded during execution.
+ /// The test harness sets to capture
+ /// these rather than allowing BKPT to escalate to HardFault — this simulates the
+ /// ARMv6-M §C1.7.2 behaviour when a debug monitor IS attached (as in a real test environment).
+ /// Firmware panics (pico-sdk panic() uses BKPT #0) are therefore observable
+ /// via without halting the simulation.
+ ///
+ public IReadOnlyList BreakpointHits => _breakpointHits;
+ private readonly List _breakpointHits = new();
+
+ protected RP2040TestSimulation()
+ {
+ Machine = new RP2040Machine();
+ // Install a capturing breakpoint handler so BKPT does not escalate to HardFault.
+ // This is the correct ARMv6-M behaviour when a debugger/monitor is attached.
+ Machine.Cpu.OnBreakpoint = imm8 => _breakpointHits.Add(imm8);
+ Machine.Cpu1.OnBreakpoint = imm8 => _breakpointHits.Add(imm8);
+ }
+
+ /// Create a new simulation instance.
+ public static RP2040TestSimulation Create() => new();
+
+ // ── Configuration ────────────────────────────────────────────────
+
+ /// Override the simulated CPU frequency (default 125 MHz).
+ public RP2040TestSimulation WithFrequency(uint hz)
+ {
+ _clkHz = hz;
+ return this;
+ }
+
+ /// Load a binary image into Flash at 0x10000000 and reset the CPU.
+ public RP2040TestSimulation WithBinary(ReadOnlySpan bytes)
+ {
+ Machine.LoadFlash(bytes);
+ return this;
+ }
+
+ /// Load a binary image into BootROM at 0x00000000.
+ public RP2040TestSimulation WithBootRom(ReadOnlySpan bytes)
+ {
+ Machine.LoadBootRom(bytes);
+ return this;
+ }
+
+ /// Attach a to the specified UART (0 or 1).
+ public RP2040TestSimulation AddUart(int index, out UartProbe probe)
+ {
+ var uart = index == 0 ? Machine.Uart0 : Machine.Uart1;
+ probe = new UartProbe();
+ probe.Attach(uart);
+ return this;
+ }
+
+ /// Lazily-created CDC-ACM host driver bound to the device USB peripheral.
+ public UsbCdcHost UsbCdcHost => _usbCdcHost ??= new UsbCdcHost(Machine.Usb);
+ private UsbCdcHost? _usbCdcHost;
+
+ /// Attach a to the auto-enumerated USB-CDC channel.
+ public RP2040TestSimulation AddUsbCdc(out UsbCdcProbe probe)
+ {
+ probe = new UsbCdcProbe().Attach(UsbCdcHost);
+ return this;
+ }
+
+ ///
+ /// Get a reference to a GPIO pin for assertions.
+ /// Pin numbers are 0-29.
+ ///
+ public RP2040TestSimulation AddGpio(int pin, out GpioPin gpioPin)
+ {
+ gpioPin = Machine.Gpio[pin];
+ return this;
+ }
+
+ // ── Execution ────────────────────────────────────────────────────
+
+ /// Execute exactly instructions.
+ public RP2040TestSimulation RunInstructions(int instructions)
+ {
+ Machine.Run(instructions);
+ return this;
+ }
+
+ /// Execute for approximately CPU cycles.
+ public RP2040TestSimulation RunCycles(long cycles)
+ {
+ // Run in batches so time-aware peripherals (Timer, Watchdog, …) are ticked
+ // frequently enough for interrupt-driven wakeups (e.g. sleep_ms via WFE) to work.
+ // Batch ≈ 500 000 cycles (~4 ms at 125 MHz) gives ms-level timer accuracy while
+ // reducing bookkeeping overhead 10× vs the former 50 K batch — a measurable speedup
+ // for multi-second simulations such as MicroPython boot (which simulates ~60 simulated
+ // seconds of RP2040 execution, completed in wall-clock seconds on a modern host).
+ const int BatchSize = 500_000;
+ while (cycles > 0)
+ {
+ var batch = (int)Math.Min(cycles, BatchSize);
+ Machine.Run(batch);
+ cycles -= batch;
+ }
+ return this;
+ }
+
+ /// Execute for simulated microseconds.
+ public RP2040TestSimulation RunMicroseconds(double microseconds)
+ {
+ var cycles = (long)(microseconds * _clkHz / 1_000_000.0);
+ return RunCycles(cycles);
+ }
+
+ /// Execute for simulated milliseconds.
+ public RP2040TestSimulation RunMilliseconds(double milliseconds)
+ => RunMicroseconds(milliseconds * 1000.0);
+
+ /// Execute a single instruction.
+ public RP2040TestSimulation Step()
+ {
+ Machine.Cpu.Step();
+ return this;
+ }
+
+ ///
+ /// Execute until returns true or
+ /// is reached.
+ ///
+ public RP2040TestSimulation RunUntil(Func predicate,
+ int maxInstructions = 1_000_000)
+ {
+ for (var i = 0; i < maxInstructions && !predicate(this); i++)
+ Machine.Cpu.Step();
+ return this;
+ }
+
+ /// Execute until a BKPT instruction is encountered (or limit is reached).
+ public RP2040TestSimulation RunToBreak(int maxInstructions = 1_000_000)
+ {
+ byte? received = null;
+ var prev = Machine.Cpu.OnBreakpoint;
+ Machine.Cpu.OnBreakpoint = b => received = b;
+
+ for (var i = 0; i < maxInstructions && received is null; i++)
+ Machine.Cpu.Step();
+
+ Machine.Cpu.OnBreakpoint = prev;
+ return this;
+ }
+
+ /// Reset the CPU to its initial state.
+ public RP2040TestSimulation Reset()
+ {
+ _breakpointHits.Clear();
+ Machine.Reset();
+ return this;
+ }
+
+ // ── Output capture helpers ────────────────────────────────────────
+
+ ///
+ /// Run the simulation in batches until appears in
+ /// 's captured output, or elapses.
+ /// Returns true when the expected text was found.
+ ///
+ public bool RunUntilOutput(UartProbe uart, string expectedText, double timeoutMs = 10_000)
+ {
+ const double batchMs = 100.0;
+ var elapsed = 0.0;
+ while (elapsed < timeoutMs)
+ {
+ RunMilliseconds(batchMs);
+ if (uart.Text.Contains(expectedText, StringComparison.Ordinal))
+ return true;
+ elapsed += batchMs;
+ }
+ return false;
+ }
+
+ ///
+ /// Run the simulation in batches until over the captured UART
+ /// text returns true, or elapses.
+ ///
+ public bool RunUntilOutput(UartProbe uart, Func predicate, double timeoutMs = 10_000)
+ {
+ const double batchMs = 100.0;
+ var elapsed = 0.0;
+ while (elapsed < timeoutMs)
+ {
+ RunMilliseconds(batchMs);
+ if (predicate(uart.Text))
+ return true;
+ elapsed += batchMs;
+ }
+ return false;
+ }
+
+ public void Dispose() => Machine.Dispose();
+}
+
+public static class UsbCdcProbeRunExtensions
+{
+ /// Run the simulation until appears in the CDC stream.
+ public static bool RunUntilOutput(this RP2040TestSimulation sim, UsbCdcProbe cdc, string expectedText, double timeoutMs = 10_000)
+ {
+ const double batchMs = 100.0;
+ var elapsed = 0.0;
+ while (elapsed < timeoutMs)
+ {
+ sim.RunMilliseconds(batchMs);
+ if (cdc.Text.Contains(expectedText, StringComparison.Ordinal))
+ return true;
+ elapsed += batchMs;
+ }
+ return false;
+ }
+
+ /// Run the simulation until over the CDC text returns true.
+ public static bool RunUntilOutput(this RP2040TestSimulation sim, UsbCdcProbe cdc, Func predicate, double timeoutMs = 10_000)
+ {
+ const double batchMs = 100.0;
+ var elapsed = 0.0;
+ while (elapsed < timeoutMs)
+ {
+ sim.RunMilliseconds(batchMs);
+ if (predicate(cdc.Text)) return true;
+ elapsed += batchMs;
+ }
+ return false;
+ }
+}
diff --git a/src/RP2040Sharp.Demo.CircuitPython.Blink/Program.cs b/src/RP2040Sharp.Demo.CircuitPython.Blink/Program.cs
new file mode 100644
index 0000000..edfc717
--- /dev/null
+++ b/src/RP2040Sharp.Demo.CircuitPython.Blink/Program.cs
@@ -0,0 +1,271 @@
+using System.Diagnostics;
+using RP2040.Peripherals;
+using RP2040.TestKit;
+using RP2040.TestKit.Boards;
+
+namespace RP2040Sharp.Demo.CircuitPython.Blink;
+
+///
+/// CircuitPython "blink" demo — boots CircuitPython on the emulated Raspberry Pi Pico,
+/// pastes the Adafruit blink example into the REPL, and monitors GPIO 25 (board.LED)
+/// while the script toggles it for 20+ seconds.
+///
+/// Usage:
+/// dotnet run --project src/RP2040Sharp.Demo.CircuitPython.Blink
+///
+/// Output: a real-time, time-stamped log of every LED state change emitted by the
+/// CircuitPython firmware, plus the final blink count and total simulated time.
+///
+internal static class Program
+{
+ private const string CircuitPythonVersion = "9.2.1";
+ private const double RP2040_CLK_HZ = 125_000_000.0;
+ private const int LedPin = 25; // board.LED on the Raspberry Pi Pico
+ private const double TargetRunSeconds = 20.0; // user requirement: ≥ 20 s of blinking
+ private const double BlinkHalfPeriodSec = 0.25; // 250 ms on, 250 ms off ⇒ 2 Hz
+
+ private static async Task Main()
+ {
+ PrintBanner();
+
+ // ── 1. Firmware ───────────────────────────────────────────────────────
+ Console.Write($"Downloading CircuitPython {CircuitPythonVersion}... ");
+ var uf2Path = await DownloadFirmwareAsync(CircuitPythonVersion);
+ if (uf2Path is null)
+ {
+ Console.Error.WriteLine("FAILED (network unavailable or release not found)");
+ return 1;
+ }
+ Console.WriteLine("OK");
+
+ Console.Write("Parsing UF2... ");
+ var flash = RP2040Machine.Uf2ToFlash(await File.ReadAllBytesAsync(uf2Path))
+ ?? throw new InvalidDataException("Not a valid UF2 file.");
+ Console.WriteLine($"OK ({flash.Length / 1024} KB)");
+
+ // ── 2. Boot ───────────────────────────────────────────────────────────
+ Console.WriteLine();
+ Console.WriteLine("Booting CircuitPython on emulated Raspberry Pi Pico...");
+ Console.WriteLine(new string('─', 60));
+
+ using var pico = new PicoSimulation();
+ pico.LoadFlash(flash);
+
+ // Stream any CDC chatter (banner, REPL output) to stdout, dimmed so it doesn't
+ // compete visually with the GPIO event log we'll print below.
+ pico.UsbCdcHost.OnSerialData += data =>
+ {
+ var text = System.Text.Encoding.Latin1.GetString(data);
+ Console.ForegroundColor = ConsoleColor.DarkGray;
+ Console.Write(text);
+ Console.ResetColor();
+ };
+
+ var wallClock = Stopwatch.StartNew();
+
+ // CircuitPython prints a "Press any key to enter the REPL" banner when no
+ // code.py exists — answer it once so the REPL prompt actually appears.
+ var promptReached = WaitForRepl(pico, timeoutMs: 30_000);
+ if (!promptReached)
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.Error.WriteLine("\nERROR: CircuitPython did not produce a REPL prompt within 30 s.");
+ Console.ResetColor();
+ return 1;
+ }
+
+ var bootMs = wallClock.Elapsed.TotalMilliseconds;
+ var bootSimMs = pico.Cpu.Cycles / (RP2040_CLK_HZ / 1_000.0);
+ Console.WriteLine(new string('─', 60));
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine($"REPL ready! ({FormatTime(bootMs)} wall · {bootSimMs / 1000.0:F2} s simulated)");
+ Console.ResetColor();
+ Console.WriteLine();
+
+ // ── 3. Inject the Adafruit blink example ──────────────────────────────
+ // Loop count is sized so the script blinks for at least TargetRunSeconds.
+ // Each iteration toggles the LED on then off, taking 2*BlinkHalfPeriodSec.
+ var iterations = (int)Math.Ceiling(TargetRunSeconds / (2.0 * BlinkHalfPeriodSec));
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine($"Pasting blink program ({iterations} cycles, ~{2 * iterations * BlinkHalfPeriodSec:F1} s)...");
+ Console.ResetColor();
+
+ var script =
+ "import board, digitalio, time\n" +
+ "led = digitalio.DigitalInOut(board.LED)\n" +
+ "led.direction = digitalio.Direction.OUTPUT\n" +
+ $"for i in range({iterations}):\n" +
+ " led.value = True\n" +
+ $" time.sleep({BlinkHalfPeriodSec})\n" +
+ " led.value = False\n" +
+ $" time.sleep({BlinkHalfPeriodSec})\n" +
+ "print('blink: done')\n";
+
+ // CircuitPython's REPL paste mode (Ctrl-E … Ctrl-D) preserves the indentation
+ // of the for-loop body, which a plain newline-separated injection would lose.
+ pico.UsbCdc.InjectString("\x05"); // Ctrl-E: enter paste mode
+ pico.RunMilliseconds(50);
+ pico.UsbCdc.InjectString(script);
+ pico.UsbCdc.InjectString("\x04"); // Ctrl-D: run pasted block
+
+ // Drop everything captured so far (banner + paste-mode echo). After this point the
+ // CDC buffer only contains real program output, so the early-exit sentinel won't
+ // match the script's own source text.
+ pico.RunMilliseconds(50);
+ pico.UsbCdc.Clear();
+
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine("GPIO 25 (board.LED) event log:");
+ Console.ResetColor();
+ Console.WriteLine(new string('─', 60));
+
+ // ── 4. Run the simulation and log LED state changes ───────────────────
+ // The loop is gated on SIMULATED time, not wall time, because the demo's
+ // contract is "≥ 20 s of simulated blinking" regardless of host speed.
+ var blinkSimMs0 = pico.Cpu.Cycles / (RP2040_CLK_HZ / 1_000.0);
+ var maxSimMs = (TargetRunSeconds + 3) * 1000.0; // small grace window
+
+ var lastLed = pico.Gpio[LedPin].DigitalValue;
+ var transitions = 0;
+
+ while (true)
+ {
+ pico.RunMilliseconds(20);
+
+ var nowLed = pico.Gpio[LedPin].DigitalValue;
+ if (nowLed != lastLed)
+ {
+ transitions++;
+ lastLed = nowLed;
+ var simS = (pico.Cpu.Cycles / (RP2040_CLK_HZ / 1_000.0) - blinkSimMs0) / 1000.0;
+ Console.ForegroundColor = nowLed ? ConsoleColor.Green : ConsoleColor.DarkGray;
+ Console.WriteLine($" [t = {simS,6:F2} s] LED {(nowLed ? "ON " : "OFF")} ({transitions} transitions)");
+ Console.ResetColor();
+ }
+
+ var simElapsedMs = pico.Cpu.Cycles / (RP2040_CLK_HZ / 1_000.0) - blinkSimMs0;
+
+ // Stop when the script signals completion (now safe — buffer was cleared above)
+ // or when we've simulated past the deadline, whichever happens first.
+ if (pico.UsbCdc.Text.Contains("blink: done", StringComparison.Ordinal))
+ {
+ pico.RunMilliseconds(200);
+ break;
+ }
+ if (simElapsedMs >= maxSimMs) break;
+ }
+
+ // ── 5. Summary ────────────────────────────────────────────────────────
+ var totalWallMs = wallClock.Elapsed.TotalMilliseconds;
+ var totalSimMs = pico.Cpu.Cycles / (RP2040_CLK_HZ / 1_000.0);
+ var blinkSimS = (totalSimMs - blinkSimMs0) / 1000.0;
+
+ Console.WriteLine(new string('─', 60));
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine();
+ Console.WriteLine("Blink demo summary");
+ Console.ResetColor();
+ Console.WriteLine($" Iterations programmed : {iterations}");
+ Console.WriteLine($" GPIO 25 transitions seen: {transitions}");
+ Console.WriteLine($" Final LED state : {(lastLed ? "HIGH" : "LOW")}");
+ Console.WriteLine($" Blink-loop simulated time: {blinkSimS:F2} s");
+ Console.WriteLine($" Total wall-clock time : {FormatTime(totalWallMs)}");
+ Console.WriteLine($" Total simulated time : {totalSimMs / 1000.0:F2} s");
+
+ if (blinkSimS < TargetRunSeconds)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine();
+ Console.WriteLine($" Note: blink loop ran for less than the {TargetRunSeconds:F0} s target.");
+ Console.ResetColor();
+ return 1;
+ }
+
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine();
+ Console.WriteLine($" ✓ Demo ran for {blinkSimS:F2} s of simulated time (≥ {TargetRunSeconds:F0} s target).");
+ Console.ResetColor();
+ return 0;
+ }
+
+ ///
+ /// Run the simulation until CircuitPython prints its REPL prompt. CircuitPython 9.x
+ /// emits "Press any key to enter the REPL." when no code.py is present — when we see
+ /// that, we send a single CR to advance past it. Returns true if ">>> " is observed
+ /// before elapses.
+ ///
+ private static bool WaitForRepl(PicoSimulation pico, double timeoutMs)
+ {
+ const double batchMs = 100.0;
+ var elapsed = 0.0;
+ var keySent = false;
+ while (elapsed < timeoutMs)
+ {
+ pico.RunMilliseconds(batchMs);
+ elapsed += batchMs;
+
+ if (!keySent && pico.UsbCdc.Text.Contains("Press any key", StringComparison.OrdinalIgnoreCase))
+ {
+ pico.UsbCdc.InjectString("\r");
+ keySent = true;
+ }
+
+ if (pico.UsbCdc.Text.Contains(">>> ", StringComparison.Ordinal))
+ return true;
+ }
+ return false;
+ }
+
+ private static string FormatTime(double ms) =>
+ ms < 1000 ? $"{ms:F0} ms" : $"{ms / 1000.0:F2} s";
+
+ private static void PrintBanner()
+ {
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("╔══════════════════════════════════════════════════════════╗");
+ Console.WriteLine("║ RP2040Sharp Demo — CircuitPython Blink (board.LED) ║");
+ Console.WriteLine("╚══════════════════════════════════════════════════════════╝");
+ Console.ResetColor();
+ Console.WriteLine();
+ }
+
+ // ── Firmware download ─────────────────────────────────────────────────────
+
+ private static readonly string CacheDir =
+ Path.Combine(Path.GetTempPath(), "rp2040sharp-firmware-cache");
+
+ ///
+ /// Downloads the official CircuitPython UF2 image for the Raspberry Pi Pico and
+ /// caches it under the system temp directory so subsequent runs are offline.
+ ///
+ private static async Task DownloadFirmwareAsync(string version)
+ {
+ Directory.CreateDirectory(CacheDir);
+
+ var tag = version.StartsWith('v') ? version[1..] : version;
+ var path = Path.Combine(CacheDir, $"circuitpython-{tag}.uf2");
+
+ if (File.Exists(path) && new FileInfo(path).Length > 0)
+ return path;
+
+ try
+ {
+ using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
+ http.DefaultRequestHeaders.UserAgent.ParseAdd("RP2040Sharp-Demo/1.0");
+
+ var url = $"https://downloads.circuitpython.org/bin/raspberry_pi_pico/en_US/" +
+ $"adafruit-circuitpython-raspberry_pi_pico-en_US-{tag}.uf2";
+
+ var bytes = await http.GetByteArrayAsync(url);
+ await File.WriteAllBytesAsync(path, bytes);
+ return path;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Download failed: {ex.GetType().Name}: {ex.Message}");
+ if (File.Exists(path)) File.Delete(path);
+ return null;
+ }
+ }
+}
diff --git a/src/RP2040Sharp.Demo.CircuitPython.Blink/RP2040Sharp.Demo.CircuitPython.Blink.csproj b/src/RP2040Sharp.Demo.CircuitPython.Blink/RP2040Sharp.Demo.CircuitPython.Blink.csproj
new file mode 100644
index 0000000..ce48e0e
--- /dev/null
+++ b/src/RP2040Sharp.Demo.CircuitPython.Blink/RP2040Sharp.Demo.CircuitPython.Blink.csproj
@@ -0,0 +1,14 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ RP2040Sharp.Demo.CircuitPython.Blink
+ RP2040Sharp.Demo.CircuitPython.Blink
+ false
+
+
+
+
+
diff --git a/src/RP2040Sharp.Demo/Program.cs b/src/RP2040Sharp.Demo/Program.cs
new file mode 100644
index 0000000..a2118d8
--- /dev/null
+++ b/src/RP2040Sharp.Demo/Program.cs
@@ -0,0 +1,242 @@
+using System.Diagnostics;
+using RP2040.Gdb;
+using RP2040.Peripherals;
+using RP2040.TestKit;
+using RP2040.TestKit.Boards;
+
+namespace RP2040Sharp.Demo;
+
+///
+/// RP2040Sharp Demo — boots MicroPython on the emulated Raspberry Pi Pico and
+/// exposes an interactive REPL over the emulated USB-CDC connection.
+///
+/// Usage:
+/// dotnet run --project src/RP2040Sharp.Demo
+///
+/// Type any Python expression at the prompt and press Enter. Press Ctrl+C or
+/// pipe EOF to exit.
+///
+internal static class Program
+{
+ private const string MicroPythonVersion = "v1.21.0";
+ private const double RP2040_CLK_HZ = 125_000_000.0;
+
+ private static async Task Main(string[] args)
+ {
+ var gdbEnabled = args.Contains("--gdb");
+
+ PrintBanner();
+
+ // ── 1. Firmware ───────────────────────────────────────────────────────
+ Console.Write($"Downloading MicroPython {MicroPythonVersion}... ");
+ var uf2Path = await DownloadFirmwareAsync(MicroPythonVersion);
+ if (uf2Path is null)
+ {
+ Console.Error.WriteLine("FAILED (network unavailable or release not found)");
+ return 1;
+ }
+ Console.WriteLine("OK");
+
+ Console.Write("Parsing UF2... ");
+ var flash = RP2040Machine.Uf2ToFlash(await File.ReadAllBytesAsync(uf2Path))
+ ?? throw new InvalidDataException("Not a valid UF2 file.");
+ Console.WriteLine($"OK ({flash.Length / 1024} KB)");
+
+ // ── 2. Boot ───────────────────────────────────────────────────────────
+ Console.WriteLine();
+ Console.WriteLine("Booting on emulated Raspberry Pi Pico...");
+ Console.WriteLine(new string('─', 60));
+
+ using var pico = new PicoSimulation();
+ pico.LoadFlash(flash);
+
+ // Forward CDC output to the console immediately as bytes arrive.
+ pico.UsbCdcHost.OnSerialData += data =>
+ {
+ var text = System.Text.Encoding.Latin1.GetString(data);
+ Console.Write(text);
+ };
+
+ var wallClock = Stopwatch.StartNew();
+
+ // Wait for MicroPython to produce the first REPL prompt.
+ var booted = pico.RunUntilOutput(pico.UsbCdc, ">>> ", timeoutMs: 60_000);
+ if (!booted)
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.Error.WriteLine("\nERROR: MicroPython did not produce a REPL prompt within 60 s.");
+ Console.ResetColor();
+ return 1;
+ }
+
+ var bootMs = wallClock.Elapsed.TotalMilliseconds;
+ var bootSimMs = pico.Cpu.Cycles / (RP2040_CLK_HZ / 1_000.0);
+ Console.WriteLine(new string('─', 60));
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine($"MicroPython ready! ({FormatTime(bootMs)} wall · {bootSimMs / 1000.0:F2} s simulated)");
+ Console.ResetColor();
+ Console.WriteLine();
+ Console.WriteLine("Interactive MicroPython REPL — type Python and press Enter. Ctrl+C to exit.");
+ Console.WriteLine(new string('─', 60));
+
+ // ── 2b. Optional GDB server ───────────────────────────────────────────
+ // With --gdb, expose Core 0 over the GDB Remote Serial Protocol on :3333.
+ // The target gates the sim loop below: a debugger interrupt/breakpoint pauses
+ // execution, and `continue` resumes it.
+ GdbExecutionTarget? gdbTarget = null;
+ GdbTcpServer? gdbServer = null;
+ if (gdbEnabled)
+ {
+ gdbTarget = new GdbExecutionTarget(pico.Rp2040);
+ gdbTarget.Execute(); // MicroPython keeps running until a debugger pauses it
+ gdbServer = new GdbTcpServer(gdbTarget, 3333);
+ gdbServer.OnLog = msg => Console.Error.WriteLine($"[gdb] {msg}");
+ gdbServer.Start();
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine("GDB server listening on :3333 (arm-none-eabi-gdb → target remote :3333)");
+ Console.ResetColor();
+ }
+
+ // ── 3. Interactive REPL loop ──────────────────────────────────────────
+ // Use a CancellationToken so Ctrl+C cleanly stops both tasks.
+ using var cts = new CancellationTokenSource();
+ Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
+
+ // Sim task: advance the emulated CPU continuously in small slices so the
+ // MicroPython interpreter keeps running while we wait for console input.
+ var simTask = Task.Run(async () =>
+ {
+ while (!cts.Token.IsCancellationRequested)
+ {
+ try
+ {
+ if (gdbTarget is null || gdbTarget.Executing)
+ pico.RunMilliseconds(10);
+ else
+ await Task.Delay(5, cts.Token);
+ // Yield to the I/O task between sim slices.
+ await Task.Yield();
+ }
+ catch (OperationCanceledException) { break; }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"\n[sim crash] {ex.GetType().Name}: {ex.Message}");
+ cts.Cancel();
+ break;
+ }
+ }
+ }, cts.Token);
+
+ // I/O task: read lines from stdin and forward them to the MicroPython REPL.
+ var ioTask = Task.Run(async () =>
+ {
+ while (!cts.Token.IsCancellationRequested)
+ {
+ // ReadLineAsync does not accept a CancellationToken in all runtimes,
+ // so we poll cancellation after each line.
+ string? line;
+ try { line = await Console.In.ReadLineAsync(cts.Token); }
+ catch (OperationCanceledException) { break; }
+
+ if (line is null) { cts.Cancel(); break; } // EOF
+ pico.UsbCdc.InjectString(line + "\r\n");
+ }
+ }, cts.Token);
+
+ // Wait for either task to finish (Ctrl+C, EOF, or crash).
+ await Task.WhenAny(simTask, ioTask);
+ cts.Cancel();
+ try { await Task.WhenAll(simTask, ioTask); } catch { /* ignore cancellation */ }
+
+ gdbServer?.Dispose();
+
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("REPL session ended.");
+ Console.ResetColor();
+ return 0;
+ }
+
+ private static string FormatTime(double ms) =>
+ ms < 1000 ? $"{ms:F0} ms" : $"{ms / 1000.0:F2} s";
+
+ private static void PrintBanner()
+ {
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("╔══════════════════════════════════════════════════════════╗");
+ Console.WriteLine("║ RP2040Sharp Demo — Interactive MicroPython REPL ║");
+ Console.WriteLine("╚══════════════════════════════════════════════════════════╝");
+ Console.ResetColor();
+ Console.WriteLine();
+ }
+
+ // ── Firmware download ─────────────────────────────────────────────────────
+
+ private static readonly string CacheDir =
+ Path.Combine(Path.GetTempPath(), "rp2040sharp-firmware-cache");
+
+ private static async Task DownloadFirmwareAsync(string version)
+ {
+ Directory.CreateDirectory(CacheDir);
+ var path = Path.Combine(CacheDir, $"micropython-{version}.uf2");
+
+ if (File.Exists(path) && new FileInfo(path).Length > 0)
+ return path;
+
+ try
+ {
+ using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
+ http.DefaultRequestHeaders.UserAgent.ParseAdd("RP2040Sharp-Demo/1.0");
+
+ var url = await ResolveMicroPythonUrlAsync(http, version);
+ if (url is null) return null;
+
+ var bytes = await http.GetByteArrayAsync(url);
+ await File.WriteAllBytesAsync(path, bytes);
+ return path;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Download failed: {ex.GetType().Name}: {ex.Message}");
+ if (File.Exists(path)) File.Delete(path);
+ return null;
+ }
+ }
+
+ private static async Task ResolveMicroPythonUrlAsync(HttpClient http, string version)
+ {
+ // Firmware is listed at https://micropython.org/download/RPI_PICO/
+ // Each entry looks like: /resources/firmware/RPI_PICO-{date}-{version}.uf2
+ var page = await http.GetStringAsync("https://micropython.org/download/RPI_PICO/");
+ var tag = version.StartsWith('v') ? version : "v" + version;
+ const string needle = "/resources/firmware/RPI_PICO-";
+ var search = $"-{tag}.uf2";
+
+ var start = page.IndexOf(needle, StringComparison.Ordinal);
+ while (start >= 0)
+ {
+ var end = page.IndexOf('"', start + 1);
+ if (end < 0) break;
+ var rel = page[start..end];
+ if (rel.EndsWith(search, StringComparison.OrdinalIgnoreCase))
+ return "https://micropython.org" + rel;
+ start = page.IndexOf(needle, start + 1, StringComparison.Ordinal);
+ }
+ return null;
+ }
+}
+
+///
+/// Drives execution for the GDB server: the sim loop runs only while
+/// is true. A debugger interrupt or a breakpoint hit calls ; continue
+/// calls .
+///
+internal sealed class GdbExecutionTarget(RP2040Machine machine) : IGdbTarget
+{
+ private volatile bool _executing;
+
+ public RP2040Machine Machine => machine;
+ public bool Executing => _executing;
+ public void Execute() => _executing = true;
+ public void Stop() => _executing = false;
+}
diff --git a/src/RP2040Sharp.Demo/RP2040Sharp.Demo.csproj b/src/RP2040Sharp.Demo/RP2040Sharp.Demo.csproj
new file mode 100644
index 0000000..b73ee9c
--- /dev/null
+++ b/src/RP2040Sharp.Demo/RP2040Sharp.Demo.csproj
@@ -0,0 +1,14 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ RP2040Sharp.Demo
+ RP2040Sharp.Demo
+ false
+
+
+
+
+
diff --git a/src/RP2040Sharp/Core/Cpu/CortexM0Plus.cs b/src/RP2040Sharp/Core/Cpu/CortexM0Plus.cs
new file mode 100644
index 0000000..1d2ee3c
--- /dev/null
+++ b/src/RP2040Sharp/Core/Cpu/CortexM0Plus.cs
@@ -0,0 +1,494 @@
+using System.Runtime.CompilerServices;
+using RP2040.Core.Memory;
+
+[module: SkipLocalsInit]
+
+namespace RP2040.Core.Cpu;
+
+public sealed unsafe class CortexM0Plus
+{
+ public readonly BusInterconnect Bus;
+ public Registers Registers;
+ public long Cycles;
+
+ /// 0 = Core0, 1 = Core1. Used by SIO to return the correct CPUID and route FIFOs.
+ public int CoreId { get; set; }
+
+ ///
+ /// True when the CPU has entered the ARMv6-M lockup state (§B1.5.13).
+ /// Lockup occurs when a HardFault is triggered while already executing in the HardFault handler
+ /// (or NMI handler), because the hardware cannot escalate further. Once locked up, no more
+ /// instructions execute and the CPU effectively halts.
+ ///
+ public bool IsLockedUp { get; private set; }
+
+ private readonly InstructionDecoder _decoder;
+
+ private byte* _fetchPtr;
+ private uint _fetchMask;
+ private uint _currentRegionId;
+
+ private const uint EXC_RETURN_HANDLER = 0xFFFFFFF1; // Return to Handler mode, using MSP
+ private const uint EXC_RETURN_THREAD_MSP = 0xFFFFFFF9; // Return to Thread mode, using MSP
+ private const uint EXC_RETURN_THREAD_PSP = 0xFFFFFFFD; // Return to Thread mode, using PSP
+
+ private const uint EXC_NMI = 2;
+ private const uint EXC_HARDFAULT = 3;
+ private const uint EXC_SVCALL = 11;
+ private const uint EXC_PENDSV = 14;
+ private const uint EXC_SYSTICK = 15;
+
+ /// Called when a BKPT instruction is executed. Parameter is the imm8 value.
+ public Action? OnBreakpoint;
+
+ ///
+ /// Called when the CPU enters the ARMv6-M lockup state (§B1.5.13).
+ /// Parameters are the faulting PC and SP at the time of lockup.
+ /// When null, the default handler writes a diagnostic message to
+ /// .
+ ///
+ public Action? OnLockup;
+
+ ///
+ /// Native hooks: when the PC equals a registered address (Thumb bit stripped), the
+ /// corresponding delegate is called instead of fetching/executing an instruction.
+ /// The delegate is responsible for updating registers as needed. After the delegate
+ /// returns the CPU automatically performs PC = LR & ~1 (same as BX LR).
+ ///
+ private Dictionary>? _nativeHooks;
+ private uint _nativeHookMax;
+
+ public void RegisterNativeHook(uint address, Action hook)
+ {
+ _nativeHooks ??= new Dictionary>();
+ address &= ~1u;
+ _nativeHooks[address] = hook;
+ if (address > _nativeHookMax) _nativeHookMax = address;
+ }
+
+ public CortexM0Plus(BusInterconnect bus)
+ {
+ Bus = bus;
+ _decoder = InstructionDecoder.Instance;
+ Reset();
+ }
+
+ public void Reset()
+ {
+ IsLockedUp = false;
+ Registers.SP = Bus.ReadWord(0x00000000);
+ Registers.PC = Bus.ReadWord(0x00000004) & 0xFFFFFFFE; // strip Thumb bit, same as ExceptionEntry
+
+ UpdateFetchCache(Registers.PC);
+
+ Registers.N = false;
+ Registers.Z = false;
+ Registers.C = false;
+ Registers.V = false;
+
+ Cycles = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void UpdateFetchCache(uint pc)
+ {
+ _currentRegionId = pc >> 28;
+
+ switch (_currentRegionId)
+ {
+ case BusInterconnect.REGION_FLASH:
+ _fetchPtr = Bus.PtrFlash;
+ _fetchMask = Bus.MaskFlash & ~1u;
+ break;
+ case BusInterconnect.REGION_SRAM:
+ _fetchPtr = Bus.PtrSram;
+ _fetchMask = BusInterconnect.MASK_SRAM & ~1u;
+ break;
+ case BusInterconnect.REGION_BOOTROM:
+ _fetchPtr = Bus.PtrBootRom;
+ _fetchMask = BusInterconnect.MASK_BOOTROM & ~1u;
+ break;
+ default:
+ _fetchPtr = null;
+ break;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ public void Run(int instructions)
+ {
+ var decoder = _decoder;
+
+ var fetchPtr = _fetchPtr;
+ var fetchMask = _fetchMask;
+ var regionId = _currentRegionId;
+
+ while (instructions-- > 0)
+ {
+ // ARMv6-M §B1.5.13 Lockup: CPU halts when HardFault fires in HardFault/NMI handler.
+ if (IsLockedUp) return;
+
+ // Interrupt check — predictable branch (nearly always not taken)
+ if (Registers.InterruptsUpdated)
+ {
+ Registers.InterruptsUpdated = false;
+ if (CheckForInterrupts())
+ {
+ UpdateFetchCache(Registers.PC);
+ fetchPtr = _fetchPtr;
+ fetchMask = _fetchMask;
+ regionId = _currentRegionId;
+ }
+ }
+
+ // WFI/WFE sleep: bail out of the current batch, crediting the unused
+ // instruction budget as elapsed cycles so the outer Machine.Run can
+ // advance time-aware peripherals (Timer, Watchdog, ...) and let an
+ // alarm IRQ wake us on the next batch. Without this, a CPU that
+ // sleeps on the very first instruction of a batch produces delta=0
+ // and the simulation deadlocks: the timer never ticks → the alarm
+ // never fires → WFE never returns.
+ if (Registers.Waiting)
+ {
+ // SEV from another core (or FIFO-write) sets EventRegistered; wake WFE.
+ if (Registers.EventRegistered)
+ {
+ Registers.Waiting = false;
+ Registers.EventRegistered = false;
+ }
+ else
+ {
+ Cycles += (uint)(instructions + 1);
+ return;
+ }
+ }
+
+ var pc = Registers.PC;
+
+ // FAST GUARD
+ if ((pc >> 28) != regionId)
+ {
+ // FALLBACK
+ UpdateFetchCache(pc);
+
+ fetchPtr = _fetchPtr;
+ fetchMask = _fetchMask;
+ regionId = _currentRegionId;
+
+ if (fetchPtr == null)
+ {
+ // PC landed in an un-executable region — raise HardFault per ARMv6-M spec
+ ExceptionEntry(EXC_HARDFAULT);
+ if (IsLockedUp) return;
+ UpdateFetchCache(Registers.PC);
+ fetchPtr = _fetchPtr;
+ fetchMask = _fetchMask;
+ regionId = _currentRegionId;
+ continue;
+ }
+ }
+
+ // ULTRA-FAST FETCH
+ // Check for native hooks — only possible in BootROM (pc < 0x23C5 after LoadFlash).
+ if (_nativeHooks != null && pc <= _nativeHookMax && _nativeHooks.TryGetValue(pc, out var nativeHook))
+ {
+ var pcBeforeHook = Registers.PC; // equals pc (not yet advanced; advance is only in normal dispatch)
+ nativeHook(this);
+ // If the hook itself changed PC (e.g., to redirect execution), honor that.
+ // Otherwise do the standard BX LR return.
+ if (Registers.PC == pcBeforeHook)
+ {
+ var hookLr = Registers.LR;
+ if (hookLr >= 0xFFFFFFF0)
+ ExceptionReturn(hookLr);
+ else
+ Registers.PC = hookLr & ~1u;
+ }
+ UpdateFetchCache(Registers.PC);
+ fetchPtr = _fetchPtr;
+ fetchMask = _fetchMask;
+ regionId = _currentRegionId;
+ Cycles++;
+ continue;
+ }
+
+ var opcode = Unsafe.ReadUnaligned(fetchPtr + (pc & fetchMask));
+
+ // PRE-UPDATE PC (Speculative)
+ Registers.PC = pc + 2;
+
+ Cycles++;
+
+ // DISPATCH
+ decoder.Dispatch(opcode, this);
+ }
+
+ _currentRegionId = regionId;
+ _fetchPtr = fetchPtr;
+ _fetchMask = fetchMask;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Step()
+ {
+ var pc = Registers.PC;
+ var opcode = Bus.ReadHalfWord(pc);
+ Registers.PC = pc + 2;
+ Cycles++;
+ _decoder.Dispatch(opcode, this);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)] // NoInlining (it is not used commonly)
+ public void UpdateStackPointerSource()
+ {
+ if (Registers.IPSR != 0)
+ return;
+
+ var switchToPsp = (Registers.CONTROL & 2) != 0;
+
+ if (switchToPsp)
+ {
+ Registers.MSP_Storage = Registers.SP;
+ Registers.SP = Registers.PSP_Storage;
+ }
+ else
+ {
+ Registers.PSP_Storage = Registers.SP;
+ Registers.SP = Registers.MSP_Storage;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public void ExceptionEntry(uint exceptionNumber)
+ {
+ if (exceptionNumber == EXC_HARDFAULT)
+ {
+ // ARMv6-M §B1.5.13: a HardFault that occurs while executing the HardFault handler
+ // (or NMI handler) cannot escalate further — the processor enters Lockup.
+ // In lockup the CPU stops fetching instructions and drives the bus with a defined
+ // repeating pattern. We model this by setting IsLockedUp and returning early so
+ // the Run() loop stops executing instructions.
+ if (Registers.IPSR == EXC_HARDFAULT || Registers.IPSR == EXC_NMI)
+ {
+ IsLockedUp = true;
+ if (OnLockup is not null)
+ OnLockup(Registers.PC, Registers.SP);
+ else
+ System.Console.Error.WriteLine(
+ $"CPU LOCKUP: HardFault in handler mode (IPSR={Registers.IPSR}) " +
+ $"callerPC=0x{Registers.PC:X8} SP=0x{Registers.SP:X8}");
+ return;
+ }
+ System.Console.Error.WriteLine($"HardFault: callerPC=0x{Registers.PC:X8} LR=0x{Registers.LR:X8} SP=0x{Registers.SP:X8}");
+ }
+ var framePtr = Registers.SP;
+
+ var needsAlign = (framePtr & 4) != 0;
+ var framePtrAlign = needsAlign ? 1u : 0u;
+
+ var stackAdjust = 0x20u + (needsAlign ? 4u : 0u);
+ var finalSp = framePtr - stackAdjust;
+
+ var frameBase = finalSp;
+
+ Bus.WriteWord(frameBase + 0x00, Registers.R0);
+ Bus.WriteWord(frameBase + 0x04, Registers.R1);
+ Bus.WriteWord(frameBase + 0x08, Registers.R2);
+ Bus.WriteWord(frameBase + 0x0C, Registers.R3);
+ Bus.WriteWord(frameBase + 0x10, Registers.R12);
+ Bus.WriteWord(frameBase + 0x14, Registers.LR);
+ Bus.WriteWord(frameBase + 0x18, Registers.PC & 0xFFFFFFFE); // Return Address
+
+ var xpsr = Registers.GetxPsr() | (framePtrAlign << 9);
+ Bus.WriteWord(frameBase + 0x1C, xpsr);
+
+ if (Registers.IPSR > 0)
+ {
+ Registers.LR = EXC_RETURN_HANDLER;
+ }
+ else
+ {
+ Registers.LR =
+ (Registers.CONTROL & 2) != 0 ? EXC_RETURN_THREAD_PSP : EXC_RETURN_THREAD_MSP;
+ }
+
+ if ((Registers.CONTROL & 2) != 0)
+ {
+ Registers.PSP_Storage = finalSp;
+ Registers.SP = Registers.MSP_Storage;
+ }
+ else
+ {
+ Registers.SP = finalSp;
+ }
+
+ Registers.IPSR = exceptionNumber;
+ Registers.CONTROL &= ~2u;
+
+ uint vtor = Registers.VTOR;
+ var vectorAddress = vtor + (exceptionNumber * 4);
+
+ var targetPc = Bus.ReadWord(vectorAddress);
+ Registers.PC = targetPc & 0xFFFFFFFE;
+
+ Cycles += 12; // Exception Entry cost (aprox 12-15 cycles)
+ }
+
+ // ================================================================
+ // Interrupt / Exception management (called by PPB peripheral)
+ // ================================================================
+
+ public void SetInterrupt(int irq, bool pending)
+ {
+ if (irq is < 0 or > 25) return;
+ var bit = 1u << irq;
+ if (pending)
+ Registers.PendingInterrupts |= bit;
+ else
+ Registers.PendingInterrupts &= ~bit;
+ Registers.InterruptsUpdated = true;
+ }
+
+ public void TriggerNmi() { Registers.PendingNMI = true; Registers.InterruptsUpdated = true; }
+ public void TriggerSysTick() { Registers.PendingSystick = true; Registers.InterruptsUpdated = true; }
+ public void TriggerPendSv() { Registers.PendingPendSV = true; Registers.InterruptsUpdated = true; }
+ public void TriggerHardFault() => ExceptionEntry(EXC_HARDFAULT);
+
+ /// Returns true if an interrupt was taken (PC changed).
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private bool CheckForInterrupts()
+ {
+ // Per ARMv6-M spec, WFI wakes when any pending+enabled exception exists,
+ // even if PRIMASK=1 prevents it from being taken. The common firmware
+ // pattern is: disable_irq → check_work → WFI → enable_irq. Without this
+ // wake-only behaviour the CPU would sleep forever with PRIMASK=1.
+ if (Registers.Waiting)
+ {
+ var wakeIrq = (Registers.PendingInterrupts & Registers.EnabledInterrupts) != 0
+ || Registers.PendingNMI
+ || Registers.PendingSystick
+ || Registers.PendingPendSV;
+ if (wakeIrq)
+ Registers.Waiting = false;
+ }
+
+ if (Registers.PRIMASK != 0 && !Registers.PendingNMI)
+ return false;
+
+ // NMI (priority -2, always takes over everything)
+ if (Registers.PendingNMI)
+ {
+ Registers.PendingNMI = false;
+ Registers.Waiting = false;
+ ExceptionEntry(EXC_NMI);
+ return true;
+ }
+
+ // SVCall — only when triggered via SVC instruction
+ if (Registers.PendingSVCall && Registers.PRIMASK == 0)
+ {
+ Registers.PendingSVCall = false;
+ Registers.Waiting = false;
+ ExceptionEntry(EXC_SVCALL);
+ return true;
+ }
+
+ // SysTick
+ if (Registers.PendingSystick && Registers.PRIMASK == 0)
+ {
+ Registers.PendingSystick = false;
+ Registers.Waiting = false;
+ ExceptionEntry(EXC_SYSTICK);
+ return true;
+ }
+
+ // PendSV (lowest priority system exception)
+ if (Registers.PendingPendSV && Registers.PRIMASK == 0)
+ {
+ Registers.PendingPendSV = false;
+ Registers.Waiting = false;
+ ExceptionEntry(EXC_PENDSV);
+ return true;
+ }
+
+ // Hardware IRQs
+ var pending = Registers.PendingInterrupts & Registers.EnabledInterrupts;
+ if (pending != 0 && Registers.PRIMASK == 0)
+ {
+ var irq = System.Numerics.BitOperations.TrailingZeroCount(pending);
+ Registers.PendingInterrupts &= ~(1u << irq);
+ Registers.Waiting = false;
+ ExceptionEntry((uint)(irq + 16)); // IRQ0 = Exception 16
+ return true;
+ }
+
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public void ExceptionReturn(uint excReturn)
+ {
+ var returnToThread = (excReturn & 8) != 0;
+ var usePsp = (excReturn & 4) != 0;
+
+ if (!returnToThread && usePsp)
+ {
+ usePsp = false;
+ }
+
+ if (returnToThread)
+ {
+ // ARMv6-M §B1.5.8: when returning to Thread mode, IPSR becomes 0.
+ Registers.IPSR = 0;
+
+ if (usePsp)
+ {
+ Registers.MSP_Storage = Registers.SP;
+ Registers.SP = Registers.PSP_Storage;
+ Registers.CONTROL |= 2;
+ }
+ else
+ {
+ Registers.CONTROL &= ~2u;
+ }
+ }
+
+ var framePtr = Registers.SP;
+
+ Registers.R0 = Bus.ReadWord(framePtr + 0x00);
+ Registers.R1 = Bus.ReadWord(framePtr + 0x04);
+ Registers.R2 = Bus.ReadWord(framePtr + 0x08);
+ Registers.R3 = Bus.ReadWord(framePtr + 0x0C);
+ Registers.R12 = Bus.ReadWord(framePtr + 0x10);
+ Registers.LR = Bus.ReadWord(framePtr + 0x14);
+ var retPC = Bus.ReadWord(framePtr + 0x18);
+ var xpsr = Bus.ReadWord(framePtr + 0x1C);
+
+ Registers.N = (xpsr & 0x80000000) != 0;
+ Registers.Z = (xpsr & 0x40000000) != 0;
+ Registers.C = (xpsr & 0x20000000) != 0;
+ Registers.V = (xpsr & 0x10000000) != 0;
+
+ // ARMv6-M §B1.5.8: when returning to Handler mode (nested interrupt),
+ // IPSR must be restored from the stacked xPSR (bits [5:0] = exception number).
+ if (!returnToThread)
+ Registers.IPSR = xpsr & 0x3Fu;
+
+ var alignAdjust = (xpsr & (1 << 9)) != 0;
+ var stackFree = 0x20u + (alignAdjust ? 4u : 0u);
+
+ Registers.SP += stackFree;
+ Registers.PC = retPC & 0xFFFFFFFE;
+
+ Cycles += 10;
+ // After returning from an ISR, re-check interrupts so that a still-pending
+ // higher-priority IRQ (e.g. USB after SysTick) fires immediately, AND signal
+ // that an event was registered so the next WFE consumes it instead of sleeping.
+ // Without `EventRegistered = true`, pico-sdk WFE-loops that expect to be woken
+ // by the very IRQ we just serviced will deadlock — the alarm fires once, the
+ // handler runs, but the WFE that follows sleeps forever waiting for an event
+ // that already happened. rp2040js cortex-m0-core.ts:339-341 sets both flags.
+ Registers.InterruptsUpdated = true;
+ Registers.EventRegistered = true;
+ }
+}
diff --git a/src/RP2040.Core/Cpu/InstructionDecoder.cs b/src/RP2040Sharp/Core/Cpu/InstructionDecoder.cs
similarity index 71%
rename from src/RP2040.Core/Cpu/InstructionDecoder.cs
rename to src/RP2040Sharp/Core/Cpu/InstructionDecoder.cs
index 454cea7..b52189b 100644
--- a/src/RP2040.Core/Cpu/InstructionDecoder.cs
+++ b/src/RP2040Sharp/Core/Cpu/InstructionDecoder.cs
@@ -6,12 +6,11 @@
namespace RP2040.Core.Cpu;
+// If I make this static make sure it is not disposable
public sealed unsafe class InstructionDecoder : IDisposable
{
public static InstructionDecoder Instance { get; } = new InstructionDecoder();
- private readonly InstructionHandler[] _lookupTable = new InstructionHandler[65536];
- private GCHandle _pinnedHandle;
private readonly InstructionHandler* _fastTablePtr;
bool _disposed;
@@ -25,14 +24,10 @@ private readonly struct OpcodeRule(ushort mask, ushort pattern, InstructionHandl
public InstructionDecoder()
{
- _pinnedHandle = GCHandle.Alloc(_lookupTable, GCHandleType.Pinned);
- _fastTablePtr = (InstructionHandler*)_pinnedHandle.AddrOfPinnedObject();
+ _fastTablePtr = (InstructionHandler*)NativeMemory.AllocZeroed(65536, (nuint)sizeof(InstructionHandler));
InstructionHandler undefinedPtr = &HandleUndefined;
- fixed (InstructionHandler* ptrToArr = _lookupTable)
- {
- new Span(ptrToArr, _lookupTable.Length).Fill((nuint)undefinedPtr);
- }
+ new Span(_fastTablePtr, 65536).Fill((nuint)undefinedPtr);
ReadOnlySpan rules =
[
@@ -44,11 +39,25 @@ public InstructionDecoder()
// DMB, DSB, ISB (F3BF)
// Mask: 1111 1111 1111 1111
new OpcodeRule(0xFFFF, 0xF3BF, &SystemOps.Barrier),
+ // CPSIE i (enable interrupts)
+ new OpcodeRule(0xFFFF, 0xB662, &SystemOps.Cpsie),
+ // CPSID i (disable interrupts)
+ new OpcodeRule(0xFFFF, 0xB672, &SystemOps.Cpsid),
+ // NOP — must be exact match to prevent CBNZ(mask 0xFB00) from overriding
+ new OpcodeRule(0xFFFF, 0xBF00, &SystemOps.Nop),
+ // SEV
+ new OpcodeRule(0xFFFF, 0xBF40, &SystemOps.Sev),
+ // WFE
+ new OpcodeRule(0xFFFF, 0xBF20, &SystemOps.Wfe),
+ // WFI
+ new OpcodeRule(0xFFFF, 0xBF30, &SystemOps.Wfi),
// ================================================================
// GROUP 2: Mask 0xFFF0
// ================================================================
// MSR spec_reg, Rn (F38x)
new OpcodeRule(0xFFF0, 0xF380, &SystemOps.Msr),
+ // CLZ Rd, Rm (Thumb-2 32-bit: first halfword 0xFABx)
+ new OpcodeRule(0xFFF0, 0xFAB0, &BitOps.Clz),
// ================================================================
// GROUP 3: Mask 0xFFC0 (10 bits significant)
// IMPORTANT: Must come before 0xFF00 to prevent generic instructions
@@ -92,6 +101,16 @@ public InstructionDecoder()
new OpcodeRule(0xFFC0, 0xBA00, &BitOps.Rev),
// SBCS (Rn, Rm)
new OpcodeRule(0xFFC0, 0x4180, &ArithmeticOps.Sbcs),
+ // ROR (register)
+ new OpcodeRule(0xFFC0, 0x41C0, &BitOps.Ror),
+ // SXTH Rd, Rm
+ new OpcodeRule(0xFFC0, 0xB200, &BitOps.Sxth),
+ // SXTB Rd, Rm
+ new OpcodeRule(0xFFC0, 0xB240, &BitOps.Sxtb),
+ // UXTH Rd, Rm
+ new OpcodeRule(0xFFC0, 0xB280, &BitOps.Uxth),
+ // UXTB Rd, Rm
+ new OpcodeRule(0xFFC0, 0xB2C0, &BitOps.Uxtb),
// ================================================================
// GROUP 4: Mask 0xFF87 (High Register Special Cases)
// CRITICAL: These are specific cases of the 0xFF00 generic group.
@@ -116,9 +135,29 @@ public InstructionDecoder()
new OpcodeRule(0xFF80, 0xB000, &ArithmeticOps.AddSpImmediate7),
// Sub (SP - imm)
new OpcodeRule(0xFF80, 0xB080, &ArithmeticOps.SubSp),
+ // Stack Operations — must be before CBZ/CBNZ (0xF900 mask) since 0xFF00 is more specific
+ // but CBZ/CBNZ would match 0xB5xx (PUSH with LR) due to broader mask 0xF900
+ new OpcodeRule(0xFF00, 0xBC00, &MemoryOps.Pop),
+ new OpcodeRule(0xFF00, 0xBD00, &MemoryOps.PopPc),
+ new OpcodeRule(0xFF00, 0xB400, &MemoryOps.Push),
+ new OpcodeRule(0xFF00, 0xB500, &MemoryOps.PushLr),
+ // ================================================================
+ // GROUP 5b: Mask 0xFB00 — CBZ / CBNZ
+ // NOTE: Hint instructions (NOP/WFE/WFI/SEV) overlap with CBNZ(i=1)
+ // but are already registered in Group 1 with exact match, so they
+ // take priority in the lookup table.
+ // ================================================================
+ // CBZ Rn, label (0xB1xx i=0, 0xB3xx i=1)
+ new OpcodeRule(0xF900, 0xB100, &FlowOps.Cbz),
+ // CBNZ Rn, label (0xB9xx i=0, 0xBBxx i=1)
+ new OpcodeRule(0xF900, 0xB900, &FlowOps.Cbnz),
// ================================================================
// GROUP 6: Mask 0xFF00 (8 bits significant - Broad Categories)
// ================================================================
+ // BKPT #imm8 — must be before NOP group (0xBF00) and hint range
+ new OpcodeRule(0xFF00, 0xBE00, &SystemOps.Bkpt),
+ // SVC #imm8 — must be before conditional branches (0xDF00 range)
+ new OpcodeRule(0xFF00, 0xDF00, &SystemOps.Svc),
// 3. Low Priority: ADD Generic (R0-R12, R14)
new OpcodeRule(0xFF00, 0x4400, &ArithmeticOps.AddHighToReg),
// CMP Rn, Rm (High Registers - Encoding T2)
@@ -131,8 +170,6 @@ public InstructionDecoder()
// Stack Operations
new OpcodeRule(0xFF00, 0xBC00, &MemoryOps.Pop),
new OpcodeRule(0xFF00, 0xBD00, &MemoryOps.PopPc),
- new OpcodeRule(0xFF00, 0xB400, &MemoryOps.Push),
- new OpcodeRule(0xFF00, 0xB500, &MemoryOps.PushLr),
// Conditional Branches (T1)
// SVC (0xDF00) is technically caught here if not handled separately.
// Ensure the handler filters 0xF (SVC) or add a specific SVC rule with higher priority.
@@ -163,6 +200,20 @@ public InstructionDecoder()
new OpcodeRule(0xFE00, 0x1A00, &ArithmeticOps.SubsRegister),
// LDR (register)
new OpcodeRule(0xFE00, 0x5800, &MemoryOps.LdrRegister),
+ // STR (register)
+ new OpcodeRule(0xFE00, 0x5000, &MemoryOps.StrRegister),
+ // STRH (register)
+ new OpcodeRule(0xFE00, 0x5200, &MemoryOps.StrhRegister),
+ // STRB (register)
+ new OpcodeRule(0xFE00, 0x5400, &MemoryOps.StrbRegister),
+ // LDRSB (register, sign-extend)
+ new OpcodeRule(0xFE00, 0x5600, &MemoryOps.Ldrsb),
+ // LDRH (register)
+ new OpcodeRule(0xFE00, 0x5A00, &MemoryOps.LdrhRegister),
+ // LDRB (register)
+ new OpcodeRule(0xFE00, 0x5C00, &MemoryOps.LdrbRegister),
+ // LDRSH (register, sign-extend)
+ new OpcodeRule(0xFE00, 0x5E00, &MemoryOps.Ldrsh),
// ================================================================
// GROUP 8: Mask 0xF800 (5 bits significant - Most Generic)
// ================================================================
@@ -186,12 +237,26 @@ public InstructionDecoder()
new OpcodeRule(0xF800, 0x2000, &BitOps.Movs),
// LDMIA (Load Multiple Increment After)
new OpcodeRule(0xF800, 0xC800, &MemoryOps.Ldmia),
+ // STMIA (Store Multiple Increment After)
+ new OpcodeRule(0xF800, 0xC000, &MemoryOps.Stmia),
// LDR (literal)
new OpcodeRule(0xF800, 0x4800, &MemoryOps.LdrLiteral),
// LDR (imm5)
new OpcodeRule(0xF800, 0x6800, &MemoryOps.LdrImmediate),
// LDR (SP, imm8)
new OpcodeRule(0xF800, 0x9800, &MemoryOps.LdrSpRelative),
+ // STR (imm5)
+ new OpcodeRule(0xF800, 0x6000, &MemoryOps.StrImmediate),
+ // STR (SP + imm8)
+ new OpcodeRule(0xF800, 0x9000, &MemoryOps.StrSpRelative),
+ // STRB (imm5)
+ new OpcodeRule(0xF800, 0x7000, &MemoryOps.StrbImmediate),
+ // STRH (imm5)
+ new OpcodeRule(0xF800, 0x8000, &MemoryOps.StrhImmediate),
+ // LDRB (imm5)
+ new OpcodeRule(0xF800, 0x7800, &MemoryOps.LdrbImmediate),
+ // LDRH (imm5)
+ new OpcodeRule(0xF800, 0x8800, &MemoryOps.LdrhImmediate),
// LSLS (Rd, Rm, imm5)
new OpcodeRule(0xF800, 0x0000, &BitOps.LslsImm5),
// LSRS (Rd, Rm, imm5)
@@ -235,7 +300,10 @@ public nuint GetHandler(ushort opcode)
private static void HandleUndefined(ushort opcode, CortexM0Plus cpu)
{
- throw new Exception($"Undefined Opcode: 0x{opcode:X4} PC={cpu.Registers.PC:X8}");
+ // ARMv6-M B1.5.6: executing an UNDEFINED encoding raises HardFault.
+ // Do not throw a C# exception — let the handler vector take over.
+ System.Console.Error.WriteLine($"Undefined instruction 0x{opcode:X4} at PC=0x{cpu.Registers.PC:X8}");
+ cpu.TriggerHardFault();
}
[ExcludeFromCodeCoverage]
@@ -245,6 +313,7 @@ public void Dispose()
GC.SuppressFinalize(this);
}
+ // Implement the virtual dispose for explicit dispose, it is a good practice to ensure resources are released
[ExcludeFromCodeCoverage]
private void Dispose(bool disposing)
{
@@ -252,10 +321,7 @@ private void Dispose(bool disposing)
if (_disposed)
return;
- if (_pinnedHandle.IsAllocated)
- {
- _pinnedHandle.Free();
- }
+ NativeMemory.Free(_fastTablePtr);
_disposed = true;
}
diff --git a/src/RP2040.Core/Cpu/Instructions/ArithmeticOps.cs b/src/RP2040Sharp/Core/Cpu/Instructions/ArithmeticOps.cs
similarity index 96%
rename from src/RP2040.Core/Cpu/Instructions/ArithmeticOps.cs
rename to src/RP2040Sharp/Core/Cpu/Instructions/ArithmeticOps.cs
index 2be500b..f98ac86 100644
--- a/src/RP2040.Core/Cpu/Instructions/ArithmeticOps.cs
+++ b/src/RP2040Sharp/Core/Cpu/Instructions/ArithmeticOps.cs
@@ -96,11 +96,13 @@ public static void AddHighToPc(ushort opcode, CortexM0Plus cpu)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AddHighToSp(ushort opcode, CortexM0Plus cpu)
{
+ // ARMv6-M §A6.7.2: ADD (SP plus register) — writes the raw sum to SP.
+ // The architecture does NOT mandate alignment for this encoding; only
+ // explicit stack operations (MSR SP, PUSH, POP) must be word-aligned.
var rm = (opcode >> 3) & 0xF;
var valRm = cpu.Registers[rm];
- ref var sp = ref cpu.Registers.SP;
- sp = (sp + valRm) & 0xFFFFFFFC;
+ cpu.Registers.SP += valRm;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/RP2040.Core/Cpu/Instructions/BitOps.cs b/src/RP2040Sharp/Core/Cpu/Instructions/BitOps.cs
similarity index 71%
rename from src/RP2040.Core/Cpu/Instructions/BitOps.cs
rename to src/RP2040Sharp/Core/Cpu/Instructions/BitOps.cs
index 7e35ba3..b2e6603 100644
--- a/src/RP2040.Core/Cpu/Instructions/BitOps.cs
+++ b/src/RP2040Sharp/Core/Cpu/Instructions/BitOps.cs
@@ -1,4 +1,5 @@
using System.Buffers.Binary;
+using System.Numerics;
using System.Runtime.CompilerServices;
namespace RP2040.Core.Cpu.Instructions;
@@ -147,10 +148,23 @@ public static void LslsRegister(ushort opcode, CortexM0Plus cpu)
return;
}
- var extended = (ulong)valRdn << shift;
- var result = shift >= 32 ? 0 : (uint)extended;
-
- var calcCarry = (extended & 0x1_0000_0000) != 0;
+ // ARMv6-M §A2.2.1: for LSL, if shift ≥ 33 the result and carry are both 0.
+ // C# ulong shifts are masked to mod-64, so shifts of 33–63 behave correctly
+ // via the (ulong)valRdn << shift expression. However shifts ≥ 64 would wrap
+ // back and produce wrong results; guard against that explicitly.
+ uint result;
+ bool calcCarry;
+ if (shift >= 33)
+ {
+ result = 0;
+ calcCarry = false;
+ }
+ else
+ {
+ var extended = (ulong)valRdn << shift;
+ result = (uint)extended;
+ calcCarry = (extended & 0x1_0000_0000UL) != 0;
+ }
ptrRdn = result;
@@ -284,13 +298,16 @@ public static void MovToPc(ushort opcode, CortexM0Plus cpu)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MovToSp(ushort opcode, CortexM0Plus cpu)
{
+ // ARMv6-M §A6.7.75: MOV (register) to SP — writes the raw register value.
+ // The architecture does NOT force word-alignment on this write; only the
+ // processor behavior on subsequent stack accesses is affected by misalignment.
var rm = (opcode >> 3) & 0xF;
ref var sp = ref cpu.Registers.SP;
var valRm = cpu.Registers[rm];
valRm += (uint)((rm + 1) >> 4) << 1;
- sp = valRm & 0xFFFFFFFC;
+ sp = valRm;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -369,4 +386,100 @@ public static void Tst(ushort opcode, CortexM0Plus cpu)
cpu.Registers.N = (int)result < 0;
cpu.Registers.Z = result == 0;
}
+
+ // ================================================================
+ // ROR (Rotate Right, register) mask=0xFFC0 pattern=0x41C0
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Ror(ushort opcode, CortexM0Plus cpu)
+ {
+ var rdn = opcode & 0x7;
+ var rm = (opcode >> 3) & 0x7;
+
+ ref var ptrRdn = ref cpu.Registers[rdn];
+ var val = ptrRdn;
+ var shift = cpu.Registers[rm] & 0xFF;
+
+ if (shift == 0)
+ {
+ cpu.Registers.N = (int)val < 0;
+ cpu.Registers.Z = val == 0;
+ return;
+ }
+
+ var shiftMod = (int)(shift & 0x1F);
+ uint result;
+ bool carry;
+
+ if (shiftMod == 0)
+ {
+ // shift is a multiple of 32: result = val, C = bit31
+ result = val;
+ carry = (val >> 31) != 0;
+ }
+ else
+ {
+ result = (val >> shiftMod) | (val << (32 - shiftMod));
+ carry = ((val >> (shiftMod - 1)) & 1) != 0;
+ }
+
+ ptrRdn = result;
+ cpu.Registers.N = (int)result < 0;
+ cpu.Registers.Z = result == 0;
+ cpu.Registers.C = carry;
+ }
+
+ // ================================================================
+ // Sign/Zero extend (mask=0xFFC0, 16-bit Thumb)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Sxth(ushort opcode, CortexM0Plus cpu)
+ {
+ var rm = (opcode >> 3) & 0x7;
+ var rd = opcode & 0x7;
+ cpu.Registers[rd] = (uint)(short)(ushort)cpu.Registers[rm];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Sxtb(ushort opcode, CortexM0Plus cpu)
+ {
+ var rm = (opcode >> 3) & 0x7;
+ var rd = opcode & 0x7;
+ cpu.Registers[rd] = (uint)(sbyte)(byte)cpu.Registers[rm];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Uxth(ushort opcode, CortexM0Plus cpu)
+ {
+ var rm = (opcode >> 3) & 0x7;
+ var rd = opcode & 0x7;
+ cpu.Registers[rd] = cpu.Registers[rm] & 0xFFFFu;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Uxtb(ushort opcode, CortexM0Plus cpu)
+ {
+ var rm = (opcode >> 3) & 0x7;
+ var rd = opcode & 0x7;
+ cpu.Registers[rd] = cpu.Registers[rm] & 0xFFu;
+ }
+
+ // ================================================================
+ // CLZ (Count Leading Zeros) — Thumb-2 32-bit, mask=0xFFF0 pattern=0xFAB0
+ // First halfword: 0xFABx (Rm in bits 3:0)
+ // Second halfword: 0xF08x (Rd in bits 11:8, Rm in bits 3:0)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Clz(ushort opcode, CortexM0Plus cpu)
+ {
+ var rm = opcode & 0xF;
+ var second = cpu.Bus.ReadHalfWord(cpu.Registers.PC);
+ cpu.Registers.PC += 2;
+
+ var rd = (second >> 8) & 0xF;
+ cpu.Registers[rd] = (uint)BitOperations.LeadingZeroCount(cpu.Registers[rm]);
+ }
}
diff --git a/src/RP2040.Core/Cpu/Instructions/FlowOps.cs b/src/RP2040Sharp/Core/Cpu/Instructions/FlowOps.cs
similarity index 78%
rename from src/RP2040.Core/Cpu/Instructions/FlowOps.cs
rename to src/RP2040Sharp/Core/Cpu/Instructions/FlowOps.cs
index 2027395..d1bb234 100644
--- a/src/RP2040.Core/Cpu/Instructions/FlowOps.cs
+++ b/src/RP2040Sharp/Core/Cpu/Instructions/FlowOps.cs
@@ -164,7 +164,7 @@ public static void Bx(ushort opcode, CortexM0Plus cpu)
{
var rm = (opcode >> 3) & 0xf;
var target = cpu.Registers[rm];
- if (target >= 0xFFFFFFF0 && cpu.Registers.IPSR != 0)
+ if (target >= 0xFFFFFFF0)
{
cpu.ExceptionReturn(target);
return;
@@ -173,6 +173,40 @@ public static void Bx(ushort opcode, CortexM0Plus cpu)
cpu.Cycles++;
}
+ // ================================================================
+ // CBZ / CBNZ (Compare and Branch if Zero/Non-Zero)
+ // mask=0xF500, CBZ=0xB100, CBNZ=0xB900
+ // Encoding: bit11=0(CBZ)/1(CBNZ), bit9=imm5[5], bits[7:3]=imm5[4:0]
+ // offset = imm5:0 (zero-extended, already bit1=0 so effective *2)
+ // Branch target = PC_after_fetch + offset (PC was already +2 at dispatch)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Cbz(ushort opcode, CortexM0Plus cpu)
+ {
+ if (cpu.Registers[opcode & 0x7] == 0)
+ TakeCbBranch(opcode, cpu);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Cbnz(ushort opcode, CortexM0Plus cpu)
+ {
+ if (cpu.Registers[opcode & 0x7] != 0)
+ TakeCbBranch(opcode, cpu);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void TakeCbBranch(ushort opcode, CortexM0Plus cpu)
+ {
+ // imm32 = ZeroExtend(i:imm5:0, 32)
+ // i = bit[10], imm5 = bits[7:3]
+ // combined: (imm5 | (i << 5)) << 1
+ var imm32 = (uint)((((opcode >> 3) & 0x1F) | ((opcode >> 5) & 0x20)) << 1);
+ // PC was already advanced by 2 (speculative fetch); add imm32 + 2 more
+ cpu.Registers.PC += imm32 + 2;
+ cpu.Cycles++;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void TakeBranch(ushort opcode, CortexM0Plus cpu)
{
diff --git a/src/RP2040Sharp/Core/Cpu/Instructions/MemoryOps.cs b/src/RP2040Sharp/Core/Cpu/Instructions/MemoryOps.cs
new file mode 100644
index 0000000..626460a
--- /dev/null
+++ b/src/RP2040Sharp/Core/Cpu/Instructions/MemoryOps.cs
@@ -0,0 +1,534 @@
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using RP2040.Core.Memory;
+
+namespace RP2040.Core.Cpu.Instructions;
+
+public static unsafe class MemoryOps
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LdrImmediate(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var imm5 = (uint)((opcode >> 6) & 0x1F) << 2;
+
+ cpu.Registers[rt] = ReadWordWithCycles(cpu, cpu.Registers[rn] + imm5);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LdrLiteral(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = (opcode >> 8) & 0x7;
+ var imm8 = (uint)(opcode & 0xFF) << 2;
+ var nextPc = cpu.Registers.PC + 2;
+ var addr = (nextPc & 0xFFFFFFFC) + imm8;
+ cpu.Registers[rt] = ReadWordWithCycles(cpu, addr);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LdrRegister(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var rm = (opcode >> 6) & 0x7;
+ cpu.Registers[rt] = ReadWordWithCycles(cpu, cpu.Registers[rn] + cpu.Registers[rm]);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LdrSpRelative(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = (opcode >> 8) & 0x7;
+ var imm8 = (uint)(opcode & 0xFF) << 2;
+ cpu.Registers[rt] = ReadWordWithCycles(cpu, cpu.Registers.SP + imm8);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Pop(ushort opcode, CortexM0Plus cpu)
+ {
+ var mask = (uint)(opcode & 0xFF);
+ var regCount = (uint)BitOperations.PopCount(mask);
+
+ var sp = cpu.Registers.SP;
+ var finalSp = sp + (regCount * 4);
+
+ if ((sp >> 28) == BusInterconnect.REGION_SRAM)
+ {
+ var rawPtr = cpu.Bus.PtrSram + (sp & BusInterconnect.MASK_SRAM);
+
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+ cpu.Registers[regIdx] = Unsafe.ReadUnaligned(rawPtr);
+
+ rawPtr += 4;
+ mask &= (mask - 1);
+ }
+ }
+ else
+ {
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+ cpu.Registers[regIdx] = cpu.Bus.ReadWord(sp);
+ sp += 4;
+ mask &= (mask - 1);
+ }
+ }
+
+ cpu.Registers.SP = finalSp;
+ cpu.Cycles += 1 + regCount;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void PopPc(ushort opcode, CortexM0Plus cpu)
+ {
+ var mask = (uint)(opcode & 0xFF);
+ var regCount = (uint)BitOperations.PopCount(mask);
+
+ var sp = cpu.Registers.SP;
+ var finalSp = sp + ((regCount + 1) * 4);
+
+ uint newPc;
+ if ((sp >> 28) == BusInterconnect.REGION_SRAM)
+ {
+ var rawPtr = cpu.Bus.PtrSram + (sp & BusInterconnect.MASK_SRAM);
+
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+ cpu.Registers[regIdx] = Unsafe.ReadUnaligned(rawPtr);
+ rawPtr += 4;
+ mask &= (mask - 1);
+ }
+ newPc = Unsafe.ReadUnaligned(rawPtr);
+ }
+ else
+ {
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+ cpu.Registers[regIdx] = cpu.Bus.ReadWord(sp);
+ sp += 4;
+ mask &= (mask - 1);
+ }
+ newPc = cpu.Bus.ReadWord(sp);
+ }
+
+ // SP must reflect the post-pop value before ExceptionReturn unstacks the
+ // architectural frame, otherwise the frame is read starting at the
+ // EXC_RETURN word itself (corrupting R0..xPSR).
+ cpu.Registers.SP = finalSp;
+
+ if (newPc >= 0xFFFFFFF0)
+ cpu.ExceptionReturn(newPc);
+ else
+ cpu.Registers.PC = newPc & 0xFFFFFFFE;
+
+ cpu.Cycles += 4 + regCount;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Push(ushort opcode, CortexM0Plus cpu)
+ {
+ var mask = (uint)(opcode & 0xFF);
+ var regCount = (uint)BitOperations.PopCount(mask);
+ var totalBytes = regCount * 4;
+
+ var oldSp = cpu.Registers.SP;
+ var newSp = oldSp - totalBytes;
+
+ if ((newSp >> 28) == BusInterconnect.REGION_SRAM)
+ {
+ var rawPtr = cpu.Bus.PtrSram + (newSp & BusInterconnect.MASK_SRAM);
+
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+
+ Unsafe.WriteUnaligned(rawPtr, cpu.Registers[regIdx]);
+
+ rawPtr += 4;
+ mask &= (mask - 1);
+ }
+ }
+ else
+ {
+ var writePtr = newSp;
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+ var val = cpu.Registers[regIdx];
+ cpu.Bus.WriteWord(writePtr, val);
+
+ writePtr += 4;
+ mask &= (mask - 1);
+ }
+ }
+
+ cpu.Registers.SP = newSp;
+ cpu.Cycles += regCount;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void PushLr(ushort opcode, CortexM0Plus cpu)
+ {
+ var mask = (uint)(opcode & 0xFF);
+ var regCount = (uint)BitOperations.PopCount(mask);
+ var totalBytes = (regCount + 1) * 4; // +1 because of LR
+
+ var oldSp = cpu.Registers.SP;
+ var newSp = oldSp - totalBytes;
+
+ if ((newSp >> 28) == BusInterconnect.REGION_SRAM)
+ {
+ var rawPtr = cpu.Bus.PtrSram + (newSp & BusInterconnect.MASK_SRAM);
+
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+ Unsafe.WriteUnaligned(rawPtr, cpu.Registers[regIdx]);
+ rawPtr += 4;
+ mask &= (mask - 1);
+ }
+ Unsafe.WriteUnaligned(rawPtr, cpu.Registers.LR);
+ }
+ else
+ {
+ var writePtr = newSp;
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+ cpu.Bus.WriteWord(writePtr, cpu.Registers[regIdx]);
+ writePtr += 4;
+ mask &= (mask - 1);
+ }
+ cpu.Bus.WriteWord(writePtr, cpu.Registers.LR);
+ }
+
+ cpu.Registers.SP = newSp;
+ cpu.Cycles += regCount + 1;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Ldmia(ushort opcode, CortexM0Plus cpu)
+ {
+ var rn = (opcode >> 8) & 0x7;
+ var mask = (uint)(opcode & 0xFF);
+
+ var regCount = (uint)BitOperations.PopCount(mask);
+ var baseAddr = cpu.Registers[rn];
+
+ var isRnInList = (mask >> rn) & 1;
+ var writeBackOffset = (regCount * 4) * (isRnInList ^ 1);
+
+ if ((baseAddr >> 28) == BusInterconnect.REGION_SRAM)
+ {
+ var ptr = cpu.Bus.PtrSram + (baseAddr & BusInterconnect.MASK_SRAM);
+
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+ cpu.Registers[regIdx] = Unsafe.ReadUnaligned(ptr);
+
+ ptr += 4;
+ mask &= (mask - 1);
+ }
+ }
+ else // SLOW PATH
+ {
+ var readPtr = baseAddr;
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+ cpu.Registers[regIdx] = cpu.Bus.ReadWord(readPtr);
+
+ readPtr += 4;
+ mask &= (mask - 1);
+ }
+ }
+
+ cpu.Registers[rn] += writeBackOffset;
+ cpu.Cycles += (int)regCount;
+ }
+
+ // ================================================================
+ // STR variants (Store Word)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void StrImmediate(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var imm5 = (uint)((opcode >> 6) & 0x1F) << 2;
+ var address = cpu.Registers[rn] + imm5;
+ WriteWordWithCycles(cpu, address, cpu.Registers[rt]);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void StrSpRelative(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = (opcode >> 8) & 0x7;
+ var imm8 = (uint)(opcode & 0xFF) << 2;
+ var address = cpu.Registers.SP + imm8;
+ WriteWordWithCycles(cpu, address, cpu.Registers[rt]);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void StrRegister(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var rm = (opcode >> 6) & 0x7;
+ var address = cpu.Registers[rn] + cpu.Registers[rm];
+ WriteWordWithCycles(cpu, address, cpu.Registers[rt]);
+ }
+
+ // ================================================================
+ // STRB variants (Store Byte)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void StrbImmediate(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var imm5 = (uint)((opcode >> 6) & 0x1F);
+ var address = cpu.Registers[rn] + imm5;
+ WriteByteWithCycles(cpu, address, (byte)cpu.Registers[rt]);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void StrbRegister(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var rm = (opcode >> 6) & 0x7;
+ var address = cpu.Registers[rn] + cpu.Registers[rm];
+ WriteByteWithCycles(cpu, address, (byte)cpu.Registers[rt]);
+ }
+
+ // ================================================================
+ // STRH variants (Store Halfword)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void StrhImmediate(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var imm5 = (uint)((opcode >> 6) & 0x1F) << 1;
+ var address = cpu.Registers[rn] + imm5;
+ WriteHalfWordWithCycles(cpu, address, (ushort)cpu.Registers[rt]);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void StrhRegister(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var rm = (opcode >> 6) & 0x7;
+ var address = cpu.Registers[rn] + cpu.Registers[rm];
+ WriteHalfWordWithCycles(cpu, address, (ushort)cpu.Registers[rt]);
+ }
+
+ // ================================================================
+ // LDRB variants (Load Byte, zero-extend)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LdrbImmediate(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var imm5 = (uint)((opcode >> 6) & 0x1F);
+ cpu.Registers[rt] = ReadByteWithCycles(cpu, cpu.Registers[rn] + imm5);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LdrbRegister(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var rm = (opcode >> 6) & 0x7;
+ cpu.Registers[rt] = ReadByteWithCycles(cpu, cpu.Registers[rn] + cpu.Registers[rm]);
+ }
+
+ // ================================================================
+ // LDRH variants (Load Halfword, zero-extend)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LdrhImmediate(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var imm5 = (uint)((opcode >> 6) & 0x1F) << 1;
+ cpu.Registers[rt] = ReadHalfWordWithCycles(cpu, cpu.Registers[rn] + imm5);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LdrhRegister(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var rm = (opcode >> 6) & 0x7;
+ cpu.Registers[rt] = ReadHalfWordWithCycles(cpu, cpu.Registers[rn] + cpu.Registers[rm]);
+ }
+
+ // ================================================================
+ // LDRSB / LDRSH (Load Signed Byte/Halfword, sign-extend to 32 bits)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Ldrsb(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var rm = (opcode >> 6) & 0x7;
+ cpu.Registers[rt] = (uint)(sbyte)ReadByteWithCycles(cpu, cpu.Registers[rn] + cpu.Registers[rm]);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Ldrsh(ushort opcode, CortexM0Plus cpu)
+ {
+ var rt = opcode & 0x7;
+ var rn = (opcode >> 3) & 0x7;
+ var rm = (opcode >> 6) & 0x7;
+ cpu.Registers[rt] = (uint)(short)ReadHalfWordWithCycles(cpu, cpu.Registers[rn] + cpu.Registers[rm]);
+ }
+
+ // ================================================================
+ // STMIA (Store Multiple Increment After)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Stmia(ushort opcode, CortexM0Plus cpu)
+ {
+ var rn = (opcode >> 8) & 0x7;
+ var mask = (uint)(opcode & 0xFF);
+
+ var regCount = (uint)BitOperations.PopCount(mask);
+ var baseAddr = cpu.Registers[rn];
+
+ if ((baseAddr >> 28) == BusInterconnect.REGION_SRAM)
+ {
+ var ptr = cpu.Bus.PtrSram + (baseAddr & BusInterconnect.MASK_SRAM);
+
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+ Unsafe.WriteUnaligned(ptr, cpu.Registers[regIdx]);
+
+ ptr += 4;
+ mask &= (mask - 1);
+ }
+ }
+ else
+ {
+ var writePtr = baseAddr;
+ while (mask != 0)
+ {
+ var regIdx = BitOperations.TrailingZeroCount(mask);
+ cpu.Bus.WriteWord(writePtr, cpu.Registers[regIdx]);
+
+ writePtr += 4;
+ mask &= (mask - 1);
+ }
+ }
+
+ // STMIA always writes back (unlike LDMIA which skips if Rn is in list)
+ cpu.Registers[rn] = baseAddr + (regCount * 4);
+ cpu.Cycles += (int)regCount;
+ }
+
+ // ================================================================
+ // Private helpers
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint ReadWordWithCycles(CortexM0Plus cpu, uint address)
+ {
+ var region = address >> 28;
+
+ switch (region)
+ {
+ case <= BusInterconnect.REGION_SRAM:
+ cpu.Cycles += 1;
+ break;
+ case 0x4: // APB/AHB
+ case 0x5:
+ cpu.Cycles += 2;
+ break;
+ // SIO (Single-cycle IO)
+ case 0xD:
+ break;
+ default:
+ cpu.Cycles += 1; // Fallback
+ break;
+ }
+
+ return cpu.Bus.ReadWord(address);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint ReadByteWithCycles(CortexM0Plus cpu, uint address)
+ {
+ var region = address >> 28;
+ switch (region)
+ {
+ case <= BusInterconnect.REGION_SRAM: cpu.Cycles += 1; break;
+ case 0x4: case 0x5: cpu.Cycles += 2; break;
+ }
+ return cpu.Bus.ReadByte(address);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint ReadHalfWordWithCycles(CortexM0Plus cpu, uint address)
+ {
+ var region = address >> 28;
+ switch (region)
+ {
+ case <= BusInterconnect.REGION_SRAM: cpu.Cycles += 1; break;
+ case 0x4: case 0x5: cpu.Cycles += 2; break;
+ }
+ return cpu.Bus.ReadHalfWord(address);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteWordWithCycles(CortexM0Plus cpu, uint address, uint value)
+ {
+ var region = address >> 28;
+ switch (region)
+ {
+ case <= BusInterconnect.REGION_SRAM: cpu.Cycles += 1; break;
+ case 0x4: case 0x5: cpu.Cycles += 2; break;
+ }
+ cpu.Bus.WriteWord(address, value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteByteWithCycles(CortexM0Plus cpu, uint address, byte value)
+ {
+ var region = address >> 28;
+ switch (region)
+ {
+ case <= BusInterconnect.REGION_SRAM: cpu.Cycles += 1; break;
+ case 0x4: case 0x5: cpu.Cycles += 2; break;
+ }
+ cpu.Bus.WriteByte(address, value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteHalfWordWithCycles(CortexM0Plus cpu, uint address, ushort value)
+ {
+ var region = address >> 28;
+ switch (region)
+ {
+ case <= BusInterconnect.REGION_SRAM: cpu.Cycles += 1; break;
+ case 0x4: case 0x5: cpu.Cycles += 2; break;
+ }
+ cpu.Bus.WriteHalfWord(address, value);
+ }
+}
diff --git a/src/RP2040.Core/Cpu/Instructions/SystemOps.cs b/src/RP2040Sharp/Core/Cpu/Instructions/SystemOps.cs
similarity index 62%
rename from src/RP2040.Core/Cpu/Instructions/SystemOps.cs
rename to src/RP2040Sharp/Core/Cpu/Instructions/SystemOps.cs
index c9d625e..8a4f47c 100644
--- a/src/RP2040.Core/Cpu/Instructions/SystemOps.cs
+++ b/src/RP2040Sharp/Core/Cpu/Instructions/SystemOps.cs
@@ -156,4 +156,83 @@ public static void Msr(ushort opcodeH1, CortexM0Plus cpu)
}
cpu.Cycles += 2;
}
+
+ // ================================================================
+ // CPS (Change Processor State) — exact opcodes, Group 1 (mask 0xFFFF)
+ // CPSIE i = 0xB662 → PRIMASK = 0 (interrupts enabled)
+ // CPSID i = 0xB672 → PRIMASK = 1 (interrupts disabled)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Cpsie(ushort opcode, CortexM0Plus cpu)
+ {
+ cpu.Registers.PRIMASK = 0;
+ cpu.Registers.InterruptsUpdated = true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Cpsid(ushort opcode, CortexM0Plus cpu)
+ {
+ cpu.Registers.PRIMASK = 1;
+ }
+
+ // ================================================================
+ // Hint instructions — exact opcodes, Group 1 (mask 0xFFFF)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Wfi(ushort opcode, CortexM0Plus cpu)
+ {
+ cpu.Registers.Waiting = true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Wfe(ushort opcode, CortexM0Plus cpu)
+ {
+ if (!cpu.Registers.EventRegistered)
+ cpu.Registers.Waiting = true;
+ else
+ cpu.Registers.EventRegistered = false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Sev(ushort opcode, CortexM0Plus cpu)
+ {
+ cpu.Registers.EventRegistered = true;
+ }
+
+ // ================================================================
+ // BKPT — mask=0xFF00, pattern=0xBE00
+ // ARMv6-M §C1.7.2: if a debug monitor is configured (OnBreakpoint handler is set),
+ // invoke it and continue. Otherwise, the processor raises a HardFault, as if no
+ // debug monitor is present — matching real hardware behaviour where a BKPT without
+ // a connected debugger faults the core.
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Bkpt(ushort opcode, CortexM0Plus cpu)
+ {
+ var imm8 = (byte)(opcode & 0xFF);
+ if (cpu.OnBreakpoint != null)
+ {
+ cpu.OnBreakpoint.Invoke(imm8);
+ }
+ else
+ {
+ // No debugger attached — escalate to HardFault per ARMv6-M §C1.7.2.
+ cpu.TriggerHardFault();
+ }
+ }
+
+ // ================================================================
+ // SVC (Supervisor Call) — mask=0xFF00, pattern=0xDF00
+ // Triggers exception entry for EXC_SVCALL (11)
+ // ================================================================
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Svc(ushort opcode, CortexM0Plus cpu)
+ {
+ cpu.Registers.PendingSVCall = true;
+ cpu.Registers.InterruptsUpdated = true;
+ }
}
diff --git a/src/RP2040.Core/Cpu/Registers.cs b/src/RP2040Sharp/Core/Cpu/Registers.cs
similarity index 60%
rename from src/RP2040.Core/Cpu/Registers.cs
rename to src/RP2040Sharp/Core/Cpu/Registers.cs
index 03bcb3c..1546ffd 100644
--- a/src/RP2040.Core/Cpu/Registers.cs
+++ b/src/RP2040Sharp/Core/Cpu/Registers.cs
@@ -48,6 +48,24 @@ public struct Registers
public bool C; // Carry
public bool V; // Overflow
+ // --- Interrupt / Exception State ---
+ public uint VTOR; // Vector Table Offset Register
+ public uint PendingInterrupts; // Bitmap of 26 hardware IRQs pending
+ public uint EnabledInterrupts; // Bitmap of 26 hardware IRQs enabled
+ public uint InterruptPriorities0; // Priority bucket 0 (highest)
+ public uint InterruptPriorities1;
+ public uint InterruptPriorities2;
+ public uint InterruptPriorities3; // Priority bucket 3 (lowest)
+ public uint SHPR2; // SVC priority (bits 31:24)
+ public uint SHPR3; // PendSV (bits 23:16) + SysTick (bits 31:24) priority
+ public bool PendingNMI;
+ public bool PendingPendSV;
+ public bool PendingSVCall;
+ public bool PendingSystick;
+ public bool InterruptsUpdated; // Signal Run() to call CheckForInterrupts
+ public bool EventRegistered; // SEV/WFE event flag
+ public bool Waiting; // WFI/WFE sleep state
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte GetC() => Unsafe.As(ref C);
@@ -68,6 +86,20 @@ public uint GetxPsr()
return apsr | 0x01000000 | (IPSR & 0x3F);
}
+ ///
+ /// Write the APSR (condition flags) and IPSR portions of xPSR. Used by the GDB stub
+ /// when a debugger writes the cpsr/xPSR register. The EPSR Thumb bit is fixed.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetxPsr(uint value)
+ {
+ N = (value & 0x80000000) != 0;
+ Z = (value & 0x40000000) != 0;
+ C = (value & 0x20000000) != 0;
+ V = (value & 0x10000000) != 0;
+ IPSR = value & 0x3F;
+ }
+
// Interrupt Status Register (IPSR) y Execution (EPSR) se pueden manejar aparte o implícitamente.
///
diff --git a/src/RP2040.Core/Helpers/InstructionEmiter.cs b/src/RP2040Sharp/Core/Helpers/InstructionEmiter.cs
similarity index 67%
rename from src/RP2040.Core/Helpers/InstructionEmiter.cs
rename to src/RP2040Sharp/Core/Helpers/InstructionEmiter.cs
index a22f0c3..5ae985b 100644
--- a/src/RP2040.Core/Helpers/InstructionEmiter.cs
+++ b/src/RP2040Sharp/Core/Helpers/InstructionEmiter.cs
@@ -428,4 +428,178 @@ public static ushort Tst(uint rn, uint rm)
throw new ArgumentException(LowRegisterIndexOutOfRange);
return (ushort)(0x4200 | ((rm & 7) << 3) | (rn & 7));
}
+
+ // ================================================================
+ // Store instructions
+ // ================================================================
+
+ public static ushort Str(uint rt, uint rn, uint imm5)
+ {
+ if (rt > 7 || rn > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ if (imm5 > 124 || (imm5 & 3) != 0) throw new ArgumentException("Immediate must be 0-124 and word-aligned");
+ return (ushort)(0x6000 | ((imm5 >> 2) << 6) | (rn << 3) | rt);
+ }
+
+ public static ushort StrSpRelative(uint rt, uint imm8)
+ {
+ if (rt > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ if (imm8 > 1020 || (imm8 & 3) != 0) throw new ArgumentException("Immediate must be 0-1020 and word-aligned");
+ return (ushort)(0x9000 | (rt << 8) | (imm8 >> 2));
+ }
+
+ public static ushort StrRegister(uint rt, uint rn, uint rm)
+ {
+ if (rt > 7 || rn > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0x5000 | (rm << 6) | (rn << 3) | rt);
+ }
+
+ public static ushort Strb(uint rt, uint rn, uint imm5)
+ {
+ if (rt > 7 || rn > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ if (imm5 > 31) throw new ArgumentException("Immediate must be 0-31");
+ return (ushort)(0x7000 | (imm5 << 6) | (rn << 3) | rt);
+ }
+
+ public static ushort StrbRegister(uint rt, uint rn, uint rm)
+ {
+ if (rt > 7 || rn > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0x5400 | (rm << 6) | (rn << 3) | rt);
+ }
+
+ public static ushort Strh(uint rt, uint rn, uint imm5)
+ {
+ if (rt > 7 || rn > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ if (imm5 > 62 || (imm5 & 1) != 0) throw new ArgumentException("Immediate must be 0-62 and halfword-aligned");
+ return (ushort)(0x8000 | ((imm5 >> 1) << 6) | (rn << 3) | rt);
+ }
+
+ public static ushort StrhRegister(uint rt, uint rn, uint rm)
+ {
+ if (rt > 7 || rn > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0x5200 | (rm << 6) | (rn << 3) | rt);
+ }
+
+ public static ushort Stmia(uint rn, uint regList)
+ {
+ if (rn > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ if (regList > 0xFF || regList == 0) throw new ArgumentException("Register list must be 1-8 low registers");
+ return (ushort)(0xC000 | (rn << 8) | regList);
+ }
+
+ // ================================================================
+ // Load byte/halfword instructions
+ // ================================================================
+
+ public static ushort Ldrb(uint rt, uint rn, uint imm5)
+ {
+ if (rt > 7 || rn > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ if (imm5 > 31) throw new ArgumentException("Immediate must be 0-31");
+ return (ushort)(0x7800 | (imm5 << 6) | (rn << 3) | rt);
+ }
+
+ public static ushort LdrbRegister(uint rt, uint rn, uint rm)
+ {
+ if (rt > 7 || rn > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0x5C00 | (rm << 6) | (rn << 3) | rt);
+ }
+
+ public static ushort Ldrh(uint rt, uint rn, uint imm5)
+ {
+ if (rt > 7 || rn > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ if (imm5 > 62 || (imm5 & 1) != 0) throw new ArgumentException("Immediate must be 0-62 and halfword-aligned");
+ return (ushort)(0x8800 | ((imm5 >> 1) << 6) | (rn << 3) | rt);
+ }
+
+ public static ushort LdrhRegister(uint rt, uint rn, uint rm)
+ {
+ if (rt > 7 || rn > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0x5A00 | (rm << 6) | (rn << 3) | rt);
+ }
+
+ public static ushort Ldrsb(uint rt, uint rn, uint rm)
+ {
+ if (rt > 7 || rn > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0x5600 | (rm << 6) | (rn << 3) | rt);
+ }
+
+ public static ushort Ldrsh(uint rt, uint rn, uint rm)
+ {
+ if (rt > 7 || rn > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0x5E00 | (rm << 6) | (rn << 3) | rt);
+ }
+
+ // ================================================================
+ // Bit operations
+ // ================================================================
+
+ public static ushort Ror(uint rdn, uint rm)
+ {
+ if (rdn > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0x41C0 | (rm << 3) | rdn);
+ }
+
+ public static ushort Sxth(uint rd, uint rm)
+ {
+ if (rd > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0xB200 | (rm << 3) | rd);
+ }
+
+ public static ushort Sxtb(uint rd, uint rm)
+ {
+ if (rd > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0xB240 | (rm << 3) | rd);
+ }
+
+ public static ushort Uxth(uint rd, uint rm)
+ {
+ if (rd > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0xB280 | (rm << 3) | rd);
+ }
+
+ public static ushort Uxtb(uint rd, uint rm)
+ {
+ if (rd > 7 || rm > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ return (ushort)(0xB2C0 | (rm << 3) | rd);
+ }
+
+ /// Returns the two halfwords for CLZ Rd, Rm (Thumb-2 32-bit).
+ public static (ushort h1, ushort h2) Clz(uint rd, uint rm)
+ {
+ if (rd > 15 || rm > 15) throw new ArgumentException(HighRegisterIndexOutOfRange);
+ return ((ushort)(0xFAB0 | rm), (ushort)(0xF080 | (rd << 8) | rm));
+ }
+
+ // ================================================================
+ // Control flow
+ // ================================================================
+
+ public static ushort Cbz(uint rn, uint offset)
+ {
+ if (rn > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ if (offset > 126 || (offset & 1) != 0) throw new ArgumentException("Offset must be 0-126 and even");
+ var i = (offset >> 6) & 1;
+ var imm5 = (offset >> 1) & 0x1F;
+ return (ushort)(0xB300 | (i << 10) | (imm5 << 3) | rn);
+ }
+
+ public static ushort Cbnz(uint rn, uint offset)
+ {
+ if (rn > 7) throw new ArgumentException(LowRegisterIndexOutOfRange);
+ if (offset > 126 || (offset & 1) != 0) throw new ArgumentException("Offset must be 0-126 and even");
+ var i = (offset >> 6) & 1;
+ var imm5 = (offset >> 1) & 0x1F;
+ return (ushort)(0xBB00 | (i << 10) | (imm5 << 3) | rn);
+ }
+
+ // ================================================================
+ // System
+ // ================================================================
+
+ public static ushort Bkpt(byte imm8) => (ushort)(0xBE00 | imm8);
+ public static ushort Svc(byte imm8) => (ushort)(0xDF00 | imm8);
+ public static ushort Cpsie => 0xB662;
+ public static ushort Cpsid => 0xB672;
+ public static ushort Wfi => 0xBF30;
+ public static ushort Wfe => 0xBF20;
+ public static ushort Sev => 0xBF40;
}
diff --git a/src/RP2040.Core/Memory/BusInterconnect.cs b/src/RP2040Sharp/Core/Memory/BusInterconnect.cs
similarity index 51%
rename from src/RP2040.Core/Memory/BusInterconnect.cs
rename to src/RP2040Sharp/Core/Memory/BusInterconnect.cs
index 279f3aa..767e420 100644
--- a/src/RP2040.Core/Memory/BusInterconnect.cs
+++ b/src/RP2040Sharp/Core/Memory/BusInterconnect.cs
@@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
namespace RP2040.Core.Memory;
@@ -9,9 +8,11 @@ public unsafe class BusInterconnect : IMemoryBus, IDisposable
public const uint REGION_FLASH = 0x1;
public const uint REGION_SRAM = 0x2;
- public const uint MASK_SRAM = 0x7FFFF; // 512KB (covers 264KB + mirrors)
- public const uint MASK_FLASH = 0x1FFFFF; // 2MB
- public const uint MASK_BOOTROM = 0x3FFF; // 16KB
+ public const uint MASK_SRAM = 0x7FFFF; // 512KB (covers 264KB + mirrors)
+ public const uint MASK_BOOTROM = 0x3FFF; // 16KB
+
+ public uint FlashSize { get; }
+ public uint MaskFlash { get; }
public const uint SRAM_START_ADDRESS = 0x20000000;
public const uint FLASH_START_ADDRESS = 0x10000000;
@@ -20,39 +21,32 @@ public unsafe class BusInterconnect : IMemoryBus, IDisposable
public readonly byte* PtrFlash;
public readonly byte* PtrBootRom;
- private readonly byte** _pageTable;
- private readonly uint* _maskTable;
-
private readonly IMemoryMappedDevice[] _memoryMap = new IMemoryMappedDevice[16];
+ // SSI peripheral lives inside the Flash region (0x18000000) — handled as a sub-device
+ // so the flash fast path continues to serve 0x10000000–0x17FFFFFF unchanged.
+ private IMemoryMappedDevice? _ssiDevice;
+ private const uint SSI_BASE_ADDRESS = 0x18000000;
+
private readonly RandomAccessMemory _sram;
private readonly RandomAccessMemory _bootRom;
private readonly RandomAccessMemory _flash;
private bool _disposed;
- public BusInterconnect()
+ public BusInterconnect(uint flashSizeBytes = 2 * 1024 * 1024)
{
- _pageTable = (byte**)NativeMemory.AllocZeroed(16, (nuint)sizeof(byte*));
- _maskTable = (uint*)NativeMemory.AllocZeroed(16, sizeof(uint));
+ FlashSize = flashSizeBytes;
+ MaskFlash = flashSizeBytes - 1;
_sram = new RandomAccessMemory(512 * 1024);
- _flash = new RandomAccessMemory(2 * 1024 * 1024);
+ _flash = new RandomAccessMemory((int)flashSizeBytes);
_bootRom = new RandomAccessMemory(16 * 1024);
PtrSram = _sram.BasePtr;
PtrFlash = _flash.BasePtr;
PtrBootRom = _bootRom.BasePtr;
- _pageTable[REGION_BOOTROM] = PtrBootRom;
- _maskTable[REGION_BOOTROM] = MASK_BOOTROM;
-
- _pageTable[REGION_FLASH] = PtrFlash;
- _maskTable[REGION_FLASH] = MASK_FLASH;
-
- _pageTable[REGION_SRAM] = PtrSram;
- _maskTable[REGION_SRAM] = MASK_SRAM;
-
MapDevice((int)REGION_BOOTROM, _bootRom);
MapDevice((int)REGION_FLASH, _flash);
MapDevice((int)REGION_SRAM, _sram);
@@ -65,35 +59,62 @@ public void MapDevice(int regionIndex, IMemoryMappedDevice device)
_memoryMap[regionIndex] = device;
}
+ ///
+ /// Register the SSI peripheral at 0x18000000 (within the XIP flash region).
+ /// Accesses to [0x18000000, 0x18FFFFFF] are forwarded to ;
+ /// the rest of the flash region continues to use the fast pointer path.
+ ///
+ public void RegisterSsi(IMemoryMappedDevice ssi) => _ssiDevice = ssi;
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte ReadByte(uint address)
{
var region = address >> 28;
- var basePtr = _pageTable[region];
-
- return basePtr != null ? basePtr[address & _maskTable[region]] : ReadByteDispatch(address);
+ if (region == REGION_SRAM)
+ return PtrSram[address & MASK_SRAM];
+ if (region == REGION_FLASH)
+ {
+ if (_ssiDevice != null && address >= SSI_BASE_ADDRESS)
+ return _ssiDevice.ReadByte(address - SSI_BASE_ADDRESS);
+ return PtrFlash[address & MaskFlash];
+ }
+ if (region == REGION_BOOTROM)
+ return PtrBootRom[address & MASK_BOOTROM];
+ return ReadByteDispatch(address);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ushort ReadHalfWord(uint address)
{
var region = address >> 28;
- var basePtr = _pageTable[region];
-
- return basePtr != null
- ? Unsafe.ReadUnaligned(basePtr + (address & _maskTable[region]))
- : ReadHalfWordDispatch(address);
+ if (region == REGION_SRAM)
+ return Unsafe.ReadUnaligned(PtrSram + (address & MASK_SRAM));
+ if (region == REGION_FLASH)
+ {
+ if (_ssiDevice != null && address >= SSI_BASE_ADDRESS)
+ return _ssiDevice.ReadHalfWord(address - SSI_BASE_ADDRESS);
+ return Unsafe.ReadUnaligned(PtrFlash + (address & MaskFlash));
+ }
+ if (region == REGION_BOOTROM)
+ return Unsafe.ReadUnaligned(PtrBootRom + (address & MASK_BOOTROM));
+ return ReadHalfWordDispatch(address);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint ReadWord(uint address)
{
var region = address >> 28;
- var basePtr = _pageTable[region];
-
- return basePtr != null
- ? Unsafe.ReadUnaligned(basePtr + (address & _maskTable[region]))
- : ReadWordDispatch(address);
+ if (region == REGION_SRAM)
+ return Unsafe.ReadUnaligned(PtrSram + (address & MASK_SRAM));
+ if (region == REGION_FLASH)
+ {
+ if (_ssiDevice != null && address >= SSI_BASE_ADDRESS)
+ return _ssiDevice.ReadWord(address - SSI_BASE_ADDRESS);
+ return Unsafe.ReadUnaligned(PtrFlash + (address & MaskFlash));
+ }
+ if (region == REGION_BOOTROM)
+ return Unsafe.ReadUnaligned(PtrBootRom + (address & MASK_BOOTROM));
+ return ReadWordDispatch(address);
}
// --- SLOW PATH DISPATCHERS ---
@@ -113,12 +134,17 @@ private uint ReadWordDispatch(uint address) =>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteWord(uint address, uint value)
{
- if ((address >> 28) == REGION_SRAM)
+ var region = address >> 28;
+ if (region == REGION_SRAM)
{
Unsafe.WriteUnaligned(PtrSram + (address & MASK_SRAM), value);
return;
}
- var region = address >> 28;
+ if (_ssiDevice != null && region == REGION_FLASH && address >= SSI_BASE_ADDRESS)
+ {
+ _ssiDevice.WriteWord(address - SSI_BASE_ADDRESS, value);
+ return;
+ }
if (region == REGION_FLASH || region == REGION_BOOTROM)
return;
@@ -128,27 +154,39 @@ public void WriteWord(uint address, uint value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteByte(uint address, byte value)
{
- if ((address >> 28) == REGION_SRAM)
+ var region = address >> 28;
+ if (region == REGION_SRAM)
{
PtrSram[address & MASK_SRAM] = value;
return;
}
- if ((address >> 28) <= REGION_FLASH)
+ if (_ssiDevice != null && region == REGION_FLASH && address >= SSI_BASE_ADDRESS)
+ {
+ _ssiDevice.WriteByte(address - SSI_BASE_ADDRESS, value);
+ return;
+ }
+ if (region <= REGION_FLASH)
return; // ROM(0) o FLASH(1)
- _memoryMap[address >> 28]?.WriteByte(address & 0x0FFFFFFF, value);
+ _memoryMap[region]?.WriteByte(address & 0x0FFFFFFF, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteHalfWord(uint address, ushort value)
{
- if ((address >> 28) == REGION_SRAM)
+ var region = address >> 28;
+ if (region == REGION_SRAM)
{
Unsafe.WriteUnaligned(PtrSram + (address & MASK_SRAM), value);
return;
}
- if ((address >> 28) <= REGION_FLASH)
+ if (_ssiDevice != null && region == REGION_FLASH && address >= SSI_BASE_ADDRESS)
+ {
+ _ssiDevice.WriteHalfWord(address - SSI_BASE_ADDRESS, value);
return;
- _memoryMap[address >> 28]?.WriteHalfWord(address & 0x0FFFFFFF, value);
+ }
+ if (region <= REGION_FLASH)
+ return;
+ _memoryMap[region]?.WriteHalfWord(address & 0x0FFFFFFF, value);
}
[MethodImpl(MethodImplOptions.NoInlining)]
@@ -175,11 +213,6 @@ protected virtual void Dispose(bool disposing)
_bootRom?.Dispose();
}
- if (_pageTable != null)
- NativeMemory.Free(_pageTable);
- if (_maskTable != null)
- NativeMemory.Free(_maskTable);
-
_disposed = true;
}
}
diff --git a/src/RP2040.Core/Memory/IMemoryBus.cs b/src/RP2040Sharp/Core/Memory/IMemoryBus.cs
similarity index 100%
rename from src/RP2040.Core/Memory/IMemoryBus.cs
rename to src/RP2040Sharp/Core/Memory/IMemoryBus.cs
diff --git a/src/RP2040Sharp/Core/Memory/IMemoryMappedDevice.cs b/src/RP2040Sharp/Core/Memory/IMemoryMappedDevice.cs
new file mode 100644
index 0000000..0f1dac8
--- /dev/null
+++ b/src/RP2040Sharp/Core/Memory/IMemoryMappedDevice.cs
@@ -0,0 +1,22 @@
+namespace RP2040.Core.Memory;
+
+public interface IMemoryMappedDevice
+{
+ uint Size { get; }
+
+ byte ReadByte(uint address);
+ ushort ReadHalfWord(uint address);
+ uint ReadWord(uint address);
+
+ void WriteByte(uint address, byte value);
+ void WriteHalfWord(uint address, ushort value);
+ void WriteWord(uint address, uint value);
+}
+
+///
+/// Marker interface: the device processes RP2040 atomic-alias addresses
+/// (bits 12–13 = XOR/SET/CLR) internally. The AHB bridge will pass the
+/// full, unmodified address and the raw firmware write value so the device
+/// can apply the correct per-register semantics (e.g. W1C vs R/W).
+///
+public interface IHandlesAtomicAliases { }
diff --git a/src/RP2040.Core/Memory/Ram.cs b/src/RP2040Sharp/Core/Memory/Ram.cs
similarity index 95%
rename from src/RP2040.Core/Memory/Ram.cs
rename to src/RP2040Sharp/Core/Memory/Ram.cs
index 4305043..66635f8 100644
--- a/src/RP2040.Core/Memory/Ram.cs
+++ b/src/RP2040Sharp/Core/Memory/Ram.cs
@@ -4,7 +4,7 @@
namespace RP2040.Core.Memory;
-public unsafe class RandomAccessMemory : IMemoryMappedDevice, IDisposable
+public sealed unsafe class RandomAccessMemory : IMemoryMappedDevice, IDisposable
{
readonly byte[] _memory;
GCHandle _pinnedHandle;
diff --git a/src/RP2040Sharp/Gdb/GdbConnection.cs b/src/RP2040Sharp/Gdb/GdbConnection.cs
new file mode 100644
index 0000000..82923d1
--- /dev/null
+++ b/src/RP2040Sharp/Gdb/GdbConnection.cs
@@ -0,0 +1,73 @@
+using static RP2040.Gdb.GdbUtils;
+
+namespace RP2040.Gdb;
+
+///
+/// A single GDB client connection: frames RSP packets out of an incoming byte stream,
+/// validates checksums, and dispatches to . Transport-agnostic —
+/// responses are delivered through the onResponse callback. Ported from rp2040js
+/// (src/gdb/gdb-connection.ts).
+///
+public sealed class GdbConnection
+{
+ private readonly GdbServer _server;
+ private readonly Action _onResponse;
+ private string _buf = "";
+
+ public GdbConnection(GdbServer server, Action onResponse)
+ {
+ _server = server;
+ _onResponse = onResponse;
+ server.AddConnection(this);
+ onResponse("+");
+ }
+
+ public void FeedData(string data)
+ {
+ if (data.Length > 0 && data[0] == 3) // Ctrl-C interrupt
+ {
+ _server.Target.Stop();
+ _onResponse(GdbMessage(GdbServer.StopReplySigint));
+ data = data[1..];
+ }
+
+ _buf += data;
+ while (true)
+ {
+ var dolla = _buf.IndexOf('$');
+ if (dolla < 0)
+ return;
+ var hash = _buf.IndexOf('#', dolla + 1);
+ if (hash < 0 || hash + 2 >= _buf.Length) // need both checksum chars after '#'
+ return;
+
+ var cmd = _buf.Substring(dolla + 1, hash - dolla - 1);
+ var cksum = _buf.Substring(hash + 1, 2);
+ _buf = _buf[(hash + 3)..];
+
+ if (GdbChecksum(cmd) != cksum)
+ {
+ _onResponse("-");
+ }
+ else
+ {
+ _onResponse("+");
+ var response = _server.ProcessGdbMessage(cmd);
+ if (response != null)
+ _onResponse(response);
+ }
+ }
+ }
+
+ public void OnBreakpoint()
+ {
+ try
+ {
+ _onResponse(GdbMessage(GdbServer.StopReplyTrap));
+ }
+ catch
+ {
+ _server.RemoveConnection(this);
+ }
+ }
+}
diff --git a/src/RP2040Sharp/Gdb/GdbServer.cs b/src/RP2040Sharp/Gdb/GdbServer.cs
new file mode 100644
index 0000000..063c9fe
--- /dev/null
+++ b/src/RP2040Sharp/Gdb/GdbServer.cs
@@ -0,0 +1,290 @@
+using RP2040.Core.Cpu;
+using static RP2040.Gdb.GdbUtils;
+
+namespace RP2040.Gdb;
+
+///
+/// RP2040 GDB Remote Serial Protocol server. Ported from rp2040js
+/// (src/gdb/gdb-server.ts © 2021 Uri Shaked). Debugs Core 0.
+///
+public class GdbServer
+{
+ public const string StopReplySigint = "S02";
+ public const string StopReplyTrap = "S05";
+
+ // SYSM values for MRS/MSR special registers (ARMv6-M).
+ private const uint SysmMsp = 8;
+ private const uint SysmPsp = 9;
+ private const uint SysmPrimask = 16;
+ private const uint SysmControl = 20;
+
+ /* string value: armv6m-none-unknown-eabi */
+ private const string LldbTriple = "61726d76366d2d6e6f6e652d756e6b6e6f776e2d65616269";
+
+ private static readonly string[] RegisterInfo =
+ [
+ "name:r0;bitsize:32;offset:0;encoding:int;format:hex;set:General Purpose Registers;generic:arg1;gcc:0;dwarf:0;",
+ "name:r1;bitsize:32;offset:4;encoding:int;format:hex;set:General Purpose Registers;generic:arg2;gcc:1;dwarf:1;",
+ "name:r2;bitsize:32;offset:8;encoding:int;format:hex;set:General Purpose Registers;generic:arg3;gcc:2;dwarf:2;",
+ "name:r3;bitsize:32;offset:12;encoding:int;format:hex;set:General Purpose Registers;generic:arg4;gcc:3;dwarf:3;",
+ "name:r4;bitsize:32;offset:16;encoding:int;format:hex;set:General Purpose Registers;gcc:4;dwarf:4;",
+ "name:r5;bitsize:32;offset:20;encoding:int;format:hex;set:General Purpose Registers;gcc:5;dwarf:5;",
+ "name:r6;bitsize:32;offset:24;encoding:int;format:hex;set:General Purpose Registers;gcc:6;dwarf:6;",
+ "name:r7;bitsize:32;offset:28;encoding:int;format:hex;set:General Purpose Registers;gcc:7;dwarf:7;",
+ "name:r8;bitsize:32;offset:32;encoding:int;format:hex;set:General Purpose Registers;gcc:8;dwarf:8;",
+ "name:r9;bitsize:32;offset:36;encoding:int;format:hex;set:General Purpose Registers;gcc:9;dwarf:9;",
+ "name:r10;bitsize:32;offset:40;encoding:int;format:hex;set:General Purpose Registers;gcc:10;dwarf:10;",
+ "name:r11;bitsize:32;offset:44;encoding:int;format:hex;set:General Purpose Registers;generic:fp;gcc:11;dwarf:11;",
+ "name:r12;bitsize:32;offset:48;encoding:int;format:hex;set:General Purpose Registers;gcc:12;dwarf:12;",
+ "name:sp;bitsize:32;offset:52;encoding:int;format:hex;set:General Purpose Registers;generic:sp;alt-name:r13;gcc:13;dwarf:13;",
+ "name:lr;bitsize:32;offset:56;encoding:int;format:hex;set:General Purpose Registers;generic:ra;alt-name:r14;gcc:14;dwarf:14;",
+ "name:pc;bitsize:32;offset:60;encoding:int;format:hex;set:General Purpose Registers;generic:pc;alt-name:r15;gcc:15;dwarf:15;",
+ "name:cpsr;bitsize:32;offset:64;encoding:int;format:hex;set:General Purpose Registers;generic:flags;alt-name:psr;gcc:16;dwarf:16;",
+ ];
+
+ private const string TargetXml = """
+
+
+
+arm
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+""";
+
+ public readonly IGdbTarget Target;
+ private readonly HashSet _connections = [];
+
+ public GdbServer(IGdbTarget target) => Target = target;
+
+ private CortexM0Plus Core => Target.Machine.Cpu;
+
+ public string? ProcessGdbMessage(string cmd)
+ {
+ var core = Core;
+
+ if (cmd == "Hg0")
+ return GdbMessage("OK");
+
+ switch (cmd[0])
+ {
+ case '?':
+ return GdbMessage(StopReplyTrap);
+
+ case 'q':
+ if (cmd.StartsWith("qSupported:"))
+ return GdbMessage("PacketSize=4000;vContSupported+;qXfer:features:read+");
+ if (cmd == "qAttached")
+ return GdbMessage("1");
+ if (cmd.StartsWith("qXfer:features:read:target.xml"))
+ return GdbMessage("l" + TargetXml);
+ if (cmd.StartsWith("qRegisterInfo"))
+ {
+ var index = Convert.ToInt32(cmd[13..], 16);
+ return index >= 0 && index < RegisterInfo.Length
+ ? GdbMessage(RegisterInfo[index])
+ : GdbMessage("E45");
+ }
+ if (cmd == "qHostInfo")
+ return GdbMessage($"triple:{LldbTriple};endian:little;ptrsize:4;");
+ if (cmd == "qProcessInfo")
+ return GdbMessage("pid:1;endian:little;ptrsize:4;");
+ return GdbMessage("");
+
+ case 'v':
+ if (cmd == "vCont?")
+ return GdbMessage("vCont;c;C;s;S");
+ if (cmd.StartsWith("vCont;c"))
+ {
+ if (!Target.Executing)
+ Target.Execute();
+ return null;
+ }
+ if (cmd.StartsWith("vCont;s"))
+ {
+ core.Step();
+ var status = new List(17);
+ for (var i = 0; i < 17; i++)
+ {
+ var value = i == 16 ? core.Registers.GetxPsr() : core.Registers[i];
+ status.Add($"{EncodeHexByte((byte)i)}:{EncodeHexUint32(value)}");
+ }
+ return GdbMessage($"T05{string.Join(';', status)};reason:trace;");
+ }
+ break;
+
+ case 'c':
+ if (!Target.Executing)
+ Target.Execute();
+ return GdbMessage("OK");
+
+ case 'D':
+ // Detach: the debugger is leaving, so resume free execution and acknowledge.
+ if (!Target.Executing)
+ Target.Execute();
+ return GdbMessage("OK");
+
+ case 'g':
+ {
+ Span buf = stackalloc byte[17 * 4];
+ for (var i = 0; i < 16; i++)
+ WriteUint32Le(buf[(i * 4)..], core.Registers[i]);
+ WriteUint32Le(buf[(16 * 4)..], core.Registers.GetxPsr());
+ return GdbMessage(EncodeHexBuf(buf));
+ }
+
+ case 'p':
+ {
+ var registerIndex = Convert.ToInt32(cmd[1..], 16);
+ if (registerIndex is >= 0 and <= 15)
+ return GdbMessage(EncodeHexUint32(core.Registers[registerIndex]));
+ switch (registerIndex)
+ {
+ case 0x10: return GdbMessage(EncodeHexUint32(core.Registers.GetxPsr()));
+ case 0x11: return GdbMessage(EncodeHexUint32(ReadSpecial(SysmMsp)));
+ case 0x12: return GdbMessage(EncodeHexUint32(ReadSpecial(SysmPsp)));
+ case 0x13: return GdbMessage(EncodeHexUint32(ReadSpecial(SysmPrimask)));
+ case 0x14: return GdbMessage(EncodeHexUint32(0)); // TODO BASEPRI
+ case 0x15: return GdbMessage(EncodeHexUint32(0)); // TODO faultmask
+ case 0x16: return GdbMessage(EncodeHexUint32(ReadSpecial(SysmControl)));
+ }
+ break;
+ }
+
+ case 'P':
+ {
+ var parts = cmd[1..].Split('=');
+ var registerIndex = Convert.ToInt32(parts[0], 16);
+ var registerValue = parts[1].Trim();
+ var registerBytes = registerIndex > 0x12 ? 1 : 4;
+ var decoded = DecodeHexBuf(registerValue);
+ if (registerIndex is < 0 or > 0x16 || decoded.Length != registerBytes)
+ return GdbMessage("E00");
+
+ uint value = 0;
+ for (var i = 0; i < decoded.Length && i < 4; i++)
+ value |= (uint)decoded[i] << (i * 8);
+
+ switch (registerIndex)
+ {
+ case 0x10: core.Registers.SetxPsr(value); break;
+ case 0x11: WriteSpecial(SysmMsp, value); break;
+ case 0x12: WriteSpecial(SysmPsp, value); break;
+ case 0x13: WriteSpecial(SysmPrimask, value); break;
+ case 0x14: break; // TODO BASEPRI
+ case 0x15: break; // TODO faultmask
+ case 0x16: WriteSpecial(SysmControl, value); break;
+ default: core.Registers[registerIndex] = value; break;
+ }
+ return GdbMessage("OK");
+ }
+
+ case 'm':
+ {
+ var parts = cmd[1..].Split(',');
+ var address = Convert.ToUInt32(parts[0], 16);
+ var length = Convert.ToInt32(parts[1], 16);
+ var bus = Target.Machine.Bus;
+ Span bytes = length <= 1024 ? stackalloc byte[length] : new byte[length];
+ for (var i = 0; i < length; i++)
+ bytes[i] = bus.ReadByte((uint)(address + i));
+ return GdbMessage(EncodeHexBuf(bytes));
+ }
+
+ case 'M':
+ {
+ var parts = cmd[1..].Split(',', ':');
+ var address = Convert.ToUInt32(parts[0], 16);
+ var length = Convert.ToInt32(parts[1], 16);
+ var data = DecodeHexBuf(parts[2][..(length * 2)]);
+ var bus = Target.Machine.Bus;
+ for (var i = 0; i < data.Length; i++)
+ bus.WriteByte((uint)(address + i), data[i]);
+ return GdbMessage("OK");
+ }
+ }
+
+ return GdbMessage("");
+ }
+
+ public void AddConnection(GdbConnection connection)
+ {
+ _connections.Add(connection);
+ Core.OnBreakpoint = _ =>
+ {
+ Target.Stop();
+ // Step() advanced PC past the 2-byte BKPT; rewind so GDB reports the BKPT address.
+ Core.Registers.PC -= 2;
+ foreach (var c in _connections)
+ c.OnBreakpoint();
+ };
+ }
+
+ public void RemoveConnection(GdbConnection connection) => _connections.Remove(connection);
+
+ // ── Special registers (ARMv6-M MRS/MSR semantics) ────────────────────────────
+
+ private uint ReadSpecial(uint sysm)
+ {
+ var r = Core.Registers;
+ var usePsp = r.IPSR == 0 && (r.CONTROL & 2) != 0;
+ return sysm switch
+ {
+ SysmMsp => usePsp ? r.MSP_Storage : r.SP,
+ SysmPsp => usePsp ? r.SP : r.PSP_Storage,
+ SysmPrimask => r.PRIMASK & 1,
+ SysmControl => r.CONTROL & 3,
+ _ => 0,
+ };
+ }
+
+ private void WriteSpecial(uint sysm, uint value)
+ {
+ ref var r = ref Core.Registers;
+ var usePsp = r.IPSR == 0 && (r.CONTROL & 2) != 0;
+ switch (sysm)
+ {
+ case SysmMsp:
+ if (usePsp) r.MSP_Storage = value; else r.SP = value;
+ break;
+ case SysmPsp:
+ if (usePsp) r.SP = value; else r.PSP_Storage = value;
+ break;
+ case SysmPrimask: r.PRIMASK = value & 1; break;
+ case SysmControl: r.CONTROL = value & 3; break;
+ }
+ }
+
+ private static void WriteUint32Le(Span dst, uint value)
+ {
+ dst[0] = (byte)(value & 0xFF);
+ dst[1] = (byte)((value >> 8) & 0xFF);
+ dst[2] = (byte)((value >> 16) & 0xFF);
+ dst[3] = (byte)((value >> 24) & 0xFF);
+ }
+}
diff --git a/src/RP2040Sharp/Gdb/GdbTcpServer.cs b/src/RP2040Sharp/Gdb/GdbTcpServer.cs
new file mode 100644
index 0000000..a31d06a
--- /dev/null
+++ b/src/RP2040Sharp/Gdb/GdbTcpServer.cs
@@ -0,0 +1,93 @@
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+namespace RP2040.Gdb;
+
+///
+/// Exposes a over TCP so arm-none-eabi-gdb can connect with
+/// target remote :3333. Ported from rp2040js (src/gdb/gdb-tcp-server.ts).
+/// One connection is served at a time, matching a typical debug session.
+///
+public sealed class GdbTcpServer : GdbServer, IDisposable
+{
+ private readonly TcpListener _listener;
+ private readonly CancellationTokenSource _cts = new();
+
+ public int Port { get; }
+
+ /// Optional sink for connection/lifecycle messages (connected, disconnected, errors).
+ public Action? OnLog;
+
+ public GdbTcpServer(IGdbTarget target, int port = 3333) : base(target)
+ {
+ Port = port;
+ _listener = new TcpListener(IPAddress.Loopback, port);
+ }
+
+ /// Begin accepting connections on a background task.
+ public void Start()
+ {
+ _listener.Start();
+ _ = AcceptLoopAsync(_cts.Token);
+ }
+
+ private async Task AcceptLoopAsync(CancellationToken ct)
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ TcpClient client;
+ try
+ {
+ client = await _listener.AcceptTcpClientAsync(ct);
+ }
+ catch (OperationCanceledException) { return; }
+ catch (Exception e) { OnLog?.Invoke($"GDB accept error: {e.Message}"); return; }
+
+ _ = HandleConnectionAsync(client, ct);
+ }
+ }
+
+ private async Task HandleConnectionAsync(TcpClient client, CancellationToken ct)
+ {
+ OnLog?.Invoke("GDB connected");
+ client.NoDelay = true;
+ var stream = client.GetStream();
+
+ var connection = new GdbConnection(this, data =>
+ {
+ var bytes = Encoding.ASCII.GetBytes(data);
+ lock (stream)
+ stream.Write(bytes, 0, bytes.Length);
+ });
+
+ var buffer = new byte[4096];
+ try
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ var read = await stream.ReadAsync(buffer, ct);
+ if (read == 0)
+ break;
+ connection.FeedData(Encoding.ASCII.GetString(buffer, 0, read));
+ }
+ }
+ catch (Exception e)
+ {
+ OnLog?.Invoke($"GDB socket error: {e.Message}");
+ }
+ finally
+ {
+ RemoveConnection(connection);
+ client.Dispose();
+ OnLog?.Invoke("GDB disconnected");
+ }
+ }
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _listener.Stop();
+ _cts.Dispose();
+ }
+}
diff --git a/src/RP2040Sharp/Gdb/GdbUtils.cs b/src/RP2040Sharp/Gdb/GdbUtils.cs
new file mode 100644
index 0000000..d67d337
--- /dev/null
+++ b/src/RP2040Sharp/Gdb/GdbUtils.cs
@@ -0,0 +1,80 @@
+using System.Text;
+
+namespace RP2040.Gdb;
+
+///
+/// Helpers for the GDB Remote Serial Protocol: hex encoding/decoding, checksums and
+/// packet framing. Ported from rp2040js (src/gdb/gdb-utils.ts).
+///
+public static class GdbUtils
+{
+ public static string EncodeHexByte(byte value)
+ {
+ Span chars = stackalloc char[2];
+ chars[0] = HexDigit(value >> 4);
+ chars[1] = HexDigit(value & 0xF);
+ return new string(chars);
+ }
+
+ public static string EncodeHexBuf(ReadOnlySpan buf)
+ {
+ var sb = new StringBuilder(buf.Length * 2);
+ foreach (var b in buf)
+ {
+ sb.Append(HexDigit(b >> 4));
+ sb.Append(HexDigit(b & 0xF));
+ }
+ return sb.ToString();
+ }
+
+ /// Encode a 32-bit value as 8 hex chars in little-endian byte order.
+ public static string EncodeHexUint32(uint value)
+ {
+ Span bytes =
+ [
+ (byte)(value & 0xFF),
+ (byte)((value >> 8) & 0xFF),
+ (byte)((value >> 16) & 0xFF),
+ (byte)((value >> 24) & 0xFF),
+ ];
+ return EncodeHexBuf(bytes);
+ }
+
+ public static byte[] DecodeHexBuf(string encoded)
+ {
+ var result = new byte[encoded.Length / 2];
+ for (var i = 0; i < result.Length; i++)
+ result[i] = (byte)((HexValue(encoded[i * 2]) << 4) | HexValue(encoded[i * 2 + 1]));
+ return result;
+ }
+
+ /// Decode 8 little-endian hex chars into a 32-bit value.
+ public static uint DecodeHexUint32(string encoded)
+ {
+ var buf = DecodeHexBuf(encoded);
+ uint value = 0;
+ for (var i = 0; i < buf.Length && i < 4; i++)
+ value |= (uint)buf[i] << (i * 8);
+ return value;
+ }
+
+ public static string GdbChecksum(string text)
+ {
+ var sum = 0;
+ foreach (var c in text)
+ sum += c;
+ return EncodeHexByte((byte)(sum & 0xFF));
+ }
+
+ public static string GdbMessage(string value) => $"${value}#{GdbChecksum(value)}";
+
+ private static char HexDigit(int nibble) => (char)(nibble < 10 ? '0' + nibble : 'a' + nibble - 10);
+
+ private static int HexValue(char c) => c switch
+ {
+ >= '0' and <= '9' => c - '0',
+ >= 'a' and <= 'f' => c - 'a' + 10,
+ >= 'A' and <= 'F' => c - 'A' + 10,
+ _ => 0,
+ };
+}
diff --git a/src/RP2040Sharp/Gdb/IGdbTarget.cs b/src/RP2040Sharp/Gdb/IGdbTarget.cs
new file mode 100644
index 0000000..2aa1245
--- /dev/null
+++ b/src/RP2040Sharp/Gdb/IGdbTarget.cs
@@ -0,0 +1,22 @@
+using RP2040.Peripherals;
+
+namespace RP2040.Gdb;
+
+///
+/// The execution target a drives. Ported from rp2040js
+/// (src/gdb/gdb-target.ts). GDB debugs Core 0 ().
+///
+public interface IGdbTarget
+{
+ /// The machine being debugged.
+ RP2040Machine Machine { get; }
+
+ /// True while the target is freely running (between continue and a stop).
+ bool Executing { get; }
+
+ /// Start free-running execution (GDB continue/vCont;c).
+ void Execute();
+
+ /// Halt free-running execution (GDB interrupt or breakpoint hit).
+ void Stop();
+}
diff --git a/src/RP2040Sharp/Peripherals/Adc/AdcPeripheral.cs b/src/RP2040Sharp/Peripherals/Adc/AdcPeripheral.cs
new file mode 100644
index 0000000..c68f43c
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Adc/AdcPeripheral.cs
@@ -0,0 +1,226 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Adc;
+
+///
+/// RP2040 ADC peripheral (base 0x4004C000).
+/// 4 external channels (GPIO26-29) + 1 internal temperature sensor.
+/// Conversion results are provided via injectable callbacks for simulation.
+/// START_MANY (free-running) mode is driven via ITickable.
+///
+public sealed class AdcPeripheral : IMemoryMappedDevice, ITickable
+{
+ private const uint ADC_CS = 0x000; // Control / Status
+ private const uint ADC_RESULT = 0x004; // Conversion result (12-bit, read-only)
+ private const uint ADC_FCS = 0x008; // FIFO control / status
+ private const uint ADC_FIFO = 0x00C; // FIFO result
+ private const uint ADC_DIV = 0x010; // Clock divisor
+ private const uint ADC_INTR = 0x014; // Raw interrupt status
+ private const uint ADC_INTE = 0x018; // Interrupt enable
+ private const uint ADC_INTF = 0x01C; // Force interrupt
+ private const uint ADC_INTS = 0x020; // Masked interrupt status
+
+ private const int CHANNEL_COUNT = 5;
+ private const int FIFO_DEPTH = 4;
+
+ private readonly CortexM0Plus _cpu;
+
+ private uint _cs; // Includes selected channel (bits 14:12), EN (bit 0), START_ONCE (bit 2)
+ private uint _result; // Latest 12-bit conversion result
+ private uint _fcs; // FIFO control/status
+ private uint _div;
+ private uint _inte;
+ private uint _intf;
+
+ private readonly Queue _adcFifo = new(FIFO_DEPTH);
+ private bool _fifoUnder; // underflow (read when empty)
+ private bool _fifoOver; // overflow (write when full)
+ private long _tickAccum; // accumulated CPU cycles for free-running mode
+
+ ///
+ /// Optional per-channel value provider. Return a 12-bit value (0-4095).
+ /// If null for a channel, returns 0.
+ ///
+ public Func? ReadChannel;
+
+ /// DREQ source for DMA: true when the ADC FIFO has data to read.
+ public bool HasFifoData => _adcFifo.Count > 0;
+
+ public uint Size => 0x100;
+
+ // ── ITickable (START_MANY free-running mode) ──────────────────────────
+
+ public void Tick(long deltaCycles)
+ {
+ if ((_cs & (1u << 3)) == 0) return; // START_MANY not set
+
+ // ADC clock = 48 MHz; CPU clock = 125 MHz; each conversion takes 96 ADC clocks.
+ // ADC_DIV: INT[27:8] + FRAC[7:0] (integer and fractional divisor of ADC clock).
+ var divInt = (long)((_div >> 8) & 0xFFFFF);
+ var divFrac = (long)(_div & 0xFF);
+ if (divInt == 0) divInt = 1;
+
+ // cycles_per_conversion = (divInt + divFrac/256) * 96 * (CPU_HZ / ADC_HZ)
+ // = (divInt*256 + divFrac) * 96 * 125 / (256 * 48)
+ const long num = 96L * 125;
+ const long den = 256L * 48;
+ var cyclesPerConv = (divInt * 256 + divFrac) * num / den;
+ if (cyclesPerConv < 1) cyclesPerConv = 1;
+
+ _tickAccum += deltaCycles;
+ while (_tickAccum >= cyclesPerConv)
+ {
+ _tickAccum -= cyclesPerConv;
+ PerformConversion();
+ }
+ }
+
+ public AdcPeripheral(CortexM0Plus cpu)
+ {
+ _cpu = cpu;
+ }
+
+ public uint ReadWord(uint address)
+ {
+ return address switch
+ {
+ ADC_CS => _cs | (1u << 8), // READY is always 1 in synchronous simulation
+ ADC_RESULT => _result & 0xFFF,
+ ADC_FCS => BuildFcs(),
+ ADC_FIFO => ReadFifo(),
+ ADC_DIV => _div,
+ ADC_INTR => BuildIntr(),
+ ADC_INTE => _inte,
+ ADC_INTF => _intf,
+ ADC_INTS => (BuildIntr() | _intf) & _inte,
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case ADC_CS:
+ _cs = value & ~(1u << 8); // READY is read-only HW; don't store it
+ if ((value & (1u << 2)) != 0) // START_ONCE
+ PerformConversion();
+ break;
+ case ADC_FCS:
+ // Writable bits: [3:0] and [27:24]; bits [10:9] are write-1-clear
+ _fcs = value & 0x0F00000Fu;
+ if ((value & (1u << 10)) != 0) _fifoUnder = false;
+ if ((value & (1u << 11)) != 0) _fifoOver = false;
+ if ((_fcs & 1) == 0) _adcFifo.Clear(); // clear FIFO when EN=0
+ break;
+ case ADC_DIV:
+ _div = value;
+ break;
+ case ADC_INTE:
+ _inte = value & 1;
+ break;
+ case ADC_INTF:
+ _intf = value & 1;
+ break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ private void PerformConversion()
+ {
+ var channel = (int)((_cs >> 12) & 0x7);
+ if (channel >= CHANNEL_COUNT) channel = 0;
+
+ _result = ReadChannel?.Invoke(channel) ?? 0;
+ _result &= 0xFFF;
+
+ // Clear START_ONCE (READY is always 1 in ReadWord, no need to set it here)
+ _cs &= ~(1u << 2);
+
+ AdvanceRoundRobin();
+
+ // Push to FIFO if enabled
+ if ((_fcs & 1) != 0)
+ {
+ if (_adcFifo.Count >= FIFO_DEPTH)
+ {
+ _fifoOver = true;
+ }
+ else
+ {
+ var sample = (ushort)(_result & 0xFFF);
+ if ((_fcs & (1u << 1)) != 0) sample >>= 4; // SHIFT
+ _adcFifo.Enqueue(sample);
+ }
+
+ // Fire ADC_IRQ_FIFO (IRQ 22) when FIFO level meets threshold
+ if (BuildIntr() != 0 && (_inte & 1) != 0)
+ _cpu.SetInterrupt(22, true);
+ }
+ }
+
+ private uint BuildFcs()
+ {
+ var level = (uint)_adcFifo.Count;
+ var thresh = (_fcs >> 24) & 0xF;
+ return (_fcs & 0x0F00000Fu)
+ | (level << 16)
+ | (_adcFifo.Count == 0 ? (1u << 8) : 0u) // EMPTY
+ | (_adcFifo.Count >= FIFO_DEPTH ? (1u << 9) : 0u) // FULL
+ | (_fifoUnder ? (1u << 10) : 0u)
+ | (_fifoOver ? (1u << 11) : 0u)
+ | (thresh << 24);
+ }
+
+ private uint ReadFifo()
+ {
+ if (_adcFifo.TryDequeue(out var v)) return v;
+ _fifoUnder = true;
+ return 0;
+ }
+
+ private uint BuildIntr()
+ {
+ var thresh = (int)((_fcs >> 24) & 0xF);
+ var effectiveThresh = thresh == 0 ? 1 : thresh; // threshold 0 behaves as 1 (matches hardware)
+ return ((_fcs & 1) != 0 && _adcFifo.Count >= effectiveThresh) ? 1u : 0u;
+ }
+
+ private void AdvanceRoundRobin()
+ {
+ // RROBIN bits [20:16]: 5-bit bitmask of channels participating in round-robin (channels 0–4)
+ var rrobin = (int)((_cs >> 16) & 0x1F);
+ if (rrobin == 0) return;
+
+ var current = (int)((_cs >> 12) & 0x7);
+ for (var i = 1; i <= 5; i++)
+ {
+ var next = (current + i) % 5; // 5 channels: 0–3 external + 4 temperature sensor
+ if ((rrobin & (1 << next)) != 0)
+ {
+ _cs = (_cs & ~(0x7u << 12)) | ((uint)next << 12);
+ return;
+ }
+ }
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Ahb/AhbBridge.cs b/src/RP2040Sharp/Peripherals/Ahb/AhbBridge.cs
new file mode 100644
index 0000000..85ee9a0
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Ahb/AhbBridge.cs
@@ -0,0 +1,95 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Ahb;
+
+///
+/// Sub-router for a 256 MB address region (e.g., region 0x5).
+/// Dispatches by 1 MB blocks: index = (address >> 20) & 0xFF.
+/// Devices receive the address unchanged.
+///
+public sealed class AhbBridge : IMemoryMappedDevice
+{
+ private readonly IMemoryMappedDevice?[] _devices = new IMemoryMappedDevice?[256];
+
+ public uint Size => 0x1000_0000; // full region
+
+ /// Register a device. baseAddress bits [27:20] determine the slot.
+ public void Register(uint baseAddress, IMemoryMappedDevice device)
+ {
+ var idx = (baseAddress >> 20) & 0xFF;
+ _devices[idx] = device;
+ }
+
+ public uint ReadWord(uint address)
+ => _devices[(address >> 20) & 0xFF]?.ReadWord(address) ?? 0;
+
+ public ushort ReadHalfWord(uint address)
+ => _devices[(address >> 20) & 0xFF]?.ReadHalfWord(address) ?? 0;
+
+ public byte ReadByte(uint address)
+ => _devices[(address >> 20) & 0xFF]?.ReadByte(address) ?? 0;
+
+ public void WriteWord(uint address, uint value)
+ {
+ var device = _devices[(address >> 20) & 0xFF];
+ if (device == null) return;
+
+ var atomicType = (address >> 12) & 0x3;
+ // Devices that handle atomic aliases themselves receive the raw address+value.
+ if (atomicType == 0 || device is IHandlesAtomicAliases) { device.WriteWord(address, value); return; }
+
+ var baseAddr = address & ~0x3000u;
+ var current = device.ReadWord(baseAddr);
+ device.WriteWord(baseAddr, atomicType switch
+ {
+ 1 => current ^ value,
+ 2 => current | value,
+ 3 => current & ~value,
+ _ => value,
+ });
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var device = _devices[(address >> 20) & 0xFF];
+ if (device == null) return;
+
+ var atomicType = (address >> 12) & 0x3;
+ if (atomicType == 0 || device is IHandlesAtomicAliases) { device.WriteHalfWord(address, value); return; }
+
+ var baseAddr = (address & ~0x3000u) & ~3u;
+ var shift = (int)((address & 2) << 3);
+ var current = device.ReadWord(baseAddr);
+ uint expanded = (uint)value << shift;
+ uint mask = 0xFFFFu << shift;
+ device.WriteWord(baseAddr, atomicType switch
+ {
+ 1 => (current & ~mask) | ((current ^ expanded) & mask),
+ 2 => current | expanded,
+ 3 => current & ~expanded,
+ _ => (current & ~mask) | expanded,
+ });
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var device = _devices[(address >> 20) & 0xFF];
+ if (device == null) return;
+
+ var atomicType = (address >> 12) & 0x3;
+ if (atomicType == 0 || device is IHandlesAtomicAliases) { device.WriteByte(address, value); return; }
+
+ var baseAddr = (address & ~0x3000u) & ~3u;
+ var shift = (int)((address & 3) << 3);
+ var current = device.ReadWord(baseAddr);
+ uint expanded = (uint)value << shift;
+ uint mask = 0xFFu << shift;
+ device.WriteWord(baseAddr, atomicType switch
+ {
+ 1 => (current & ~mask) | ((current ^ expanded) & mask),
+ 2 => current | expanded,
+ 3 => current & ~expanded,
+ _ => (current & ~mask) | expanded,
+ });
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Apb/ApbBridge.cs b/src/RP2040Sharp/Peripherals/Apb/ApbBridge.cs
new file mode 100644
index 0000000..a2e6c93
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Apb/ApbBridge.cs
@@ -0,0 +1,118 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Apb;
+
+///
+/// APB bridge for the 0x40xxxxxx peripheral bus (region 0x4).
+/// Routes using bits [21:14] of the local address (after & 0x0FFFFFFF),
+/// which groups each peripheral's four atomic-mirror windows (base, XOR, SET, CLR)
+/// into the same 16 KiB slot.
+/// The local address passed to each device is address & 0xFFF (4 KiB window).
+///
+public sealed class ApbBridge : IMemoryMappedDevice
+{
+ // 256 slots, each covering 16 KiB of the APB space
+ private readonly IMemoryMappedDevice?[] _devices = new IMemoryMappedDevice?[256];
+
+ public uint Size => 0x10000000;
+
+ ///
+ /// Register a device at its APB base address (full 32-bit address, e.g. 0x40034000).
+ ///
+ public void Register(uint baseAddress, IMemoryMappedDevice device)
+ {
+ // Mask off region nibble → local address, then extract 16 KiB slot index
+ var localBase = baseAddress & 0x0FFFFFFF;
+ _devices[(localBase >> 14) & 0xFF] = device;
+ }
+
+ public uint ReadWord(uint address)
+ {
+ var device = _devices[(address >> 14) & 0xFF];
+ return device?.ReadWord(address & 0xFFF) ?? 0;
+ }
+
+ public ushort ReadHalfWord(uint address)
+ {
+ var device = _devices[(address >> 14) & 0xFF];
+ return device?.ReadHalfWord(address & 0xFFF) ?? 0;
+ }
+
+ public byte ReadByte(uint address)
+ {
+ var device = _devices[(address >> 14) & 0xFF];
+ return device?.ReadByte(address & 0xFFF) ?? 0;
+ }
+
+ public void WriteWord(uint address, uint value)
+ {
+ var device = _devices[(address >> 14) & 0xFF];
+ if (device == null) return;
+
+ var atomicType = (address >> 12) & 0x3;
+ var offset = address & 0xFFF;
+
+ if (atomicType == 0)
+ {
+ device.WriteWord(offset, value);
+ return;
+ }
+
+ var current = device.ReadWord(offset);
+ device.WriteWord(offset, atomicType switch
+ {
+ 1 => current ^ value, // XOR (+0x1000)
+ 2 => current | value, // SET (+0x2000)
+ 3 => current & ~value, // CLR (+0x3000)
+ _ => value,
+ });
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var device = _devices[(address >> 14) & 0xFF];
+ if (device == null) return;
+
+ var atomicType = (address >> 12) & 0x3;
+ var offset = address & 0xFFF;
+
+ if (atomicType == 0) { device.WriteHalfWord(offset, value); return; }
+
+ var aligned = offset & ~3u;
+ var shift = (int)((offset & 2) << 3);
+ var current = device.ReadWord(aligned);
+ uint expanded = (uint)value << shift;
+ uint mask = 0xFFFFu << shift;
+ device.WriteWord(aligned, atomicType switch
+ {
+ 1 => (current & ~mask) | ((current ^ expanded) & mask),
+ 2 => current | expanded,
+ 3 => current & ~expanded,
+ _ => (current & ~mask) | expanded,
+ });
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var device = _devices[(address >> 14) & 0xFF];
+ if (device == null) return;
+
+ var atomicType = (address >> 12) & 0x3;
+ var offset = address & 0xFFF;
+
+ if (atomicType == 0) { device.WriteByte(offset, value); return; }
+
+ var aligned = offset & ~3u;
+ var shift = (int)((offset & 3) << 3);
+ var current = device.ReadWord(aligned);
+ uint expanded = (uint)value << shift;
+ uint mask = 0xFFu << shift;
+ device.WriteWord(aligned, atomicType switch
+ {
+ 1 => (current & ~mask) | ((current ^ expanded) & mask),
+ 2 => current | expanded,
+ 3 => current & ~expanded,
+ _ => (current & ~mask) | expanded,
+ });
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Busctrl/BusctrlPeripheral.cs b/src/RP2040Sharp/Peripherals/Busctrl/BusctrlPeripheral.cs
new file mode 100644
index 0000000..58dbf4b
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Busctrl/BusctrlPeripheral.cs
@@ -0,0 +1,74 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Busctrl;
+
+///
+/// Bus Fabric control peripheral (0x40030000).
+/// Controls bus priority and performance counters.
+/// In simulation buses have no contention, so all counters stay at 0.
+///
+public sealed class BusctrlPeripheral : IMemoryMappedDevice
+{
+ private const uint BUS_PRIORITY = 0x000;
+ private const uint BUS_PRIORITY_ACK = 0x004;
+ private const uint PERFCTR0 = 0x008;
+ private const uint PERFSEL0 = 0x00C;
+ private const uint PERFCTR1 = 0x010;
+ private const uint PERFSEL1 = 0x014;
+ private const uint PERFCTR2 = 0x018;
+ private const uint PERFSEL2 = 0x01C;
+ private const uint PERFCTR3 = 0x020;
+ private const uint PERFSEL3 = 0x024;
+
+ private uint _priority;
+ private readonly uint[] _perfsel = new uint[4];
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address) => address switch
+ {
+ BUS_PRIORITY => _priority,
+ BUS_PRIORITY_ACK => _priority, // ack mirrors priority in simulation
+ PERFCTR0 => 0,
+ PERFSEL0 => _perfsel[0],
+ PERFCTR1 => 0,
+ PERFSEL1 => _perfsel[1],
+ PERFCTR2 => 0,
+ PERFSEL2 => _perfsel[2],
+ PERFCTR3 => 0,
+ PERFSEL3 => _perfsel[3],
+ _ => 0,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case BUS_PRIORITY: _priority = value & 0xF; break;
+ case PERFSEL0: _perfsel[0] = value; break;
+ case PERFSEL1: _perfsel[1] = value; break;
+ case PERFSEL2: _perfsel[2] = value; break;
+ case PERFSEL3: _perfsel[3] = value; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Clocks/ClocksPeripheral.cs b/src/RP2040Sharp/Peripherals/Clocks/ClocksPeripheral.cs
new file mode 100644
index 0000000..3d90b9c
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Clocks/ClocksPeripheral.cs
@@ -0,0 +1,183 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Clocks;
+
+///
+/// Clocks peripheral (0x40008000).
+/// Manages 8 clock domains. In simulation all clocks run at their default
+/// frequencies — the peripheral stores register writes and returns SELECTED=1
+/// so firmware clock-init sequences complete without spinning.
+///
+public sealed class ClocksPeripheral : IMemoryMappedDevice
+{
+ // ── Register offsets ────────────────────────────────────────────────
+ // CLK_GPOUT0..3 (CTRL/DIV/SELECTED, stride 0x0C)
+ private const uint CLK_GPOUT0_CTRL = 0x000;
+ private const uint CLK_GPOUT0_DIV = 0x004;
+ private const uint CLK_GPOUT0_SELECTED = 0x008;
+ // ... up to GPOUT3 at 0x024/0x028/0x02C
+ private const uint CLK_REF_CTRL = 0x030;
+ private const uint CLK_REF_DIV = 0x034;
+ private const uint CLK_REF_SELECTED = 0x038;
+ private const uint CLK_SYS_CTRL = 0x03C;
+ private const uint CLK_SYS_DIV = 0x040;
+ private const uint CLK_SYS_SELECTED = 0x044;
+ private const uint CLK_PERI_CTRL = 0x048;
+ private const uint CLK_PERI_SELECTED = 0x050;
+ private const uint CLK_USB_CTRL = 0x054;
+ private const uint CLK_USB_DIV = 0x058;
+ private const uint CLK_USB_SELECTED = 0x05C;
+ private const uint CLK_ADC_CTRL = 0x060;
+ private const uint CLK_ADC_DIV = 0x064;
+ private const uint CLK_ADC_SELECTED = 0x068;
+ private const uint CLK_RTC_CTRL = 0x06C;
+ private const uint CLK_RTC_DIV = 0x070;
+ private const uint CLK_RTC_SELECTED = 0x074;
+ private const uint CLK_SYS_RESUS_CTRL = 0x078;
+ private const uint CLK_SYS_RESUS_STATUS = 0x07C;
+ private const uint FC0_REF_KHZ = 0x080;
+ private const uint FC0_MIN_KHZ = 0x084;
+ private const uint FC0_MAX_KHZ = 0x088;
+ private const uint FC0_DELAY = 0x08C;
+ private const uint FC0_INTERVAL = 0x090;
+ private const uint FC0_SRC = 0x094;
+ private const uint FC0_STATUS = 0x098;
+ private const uint FC0_RESULT = 0x09C;
+ private const uint WAKE_EN0 = 0x0A0;
+ private const uint WAKE_EN1 = 0x0A4;
+ private const uint SLEEP_EN0 = 0x0A8;
+ private const uint SLEEP_EN1 = 0x0AC;
+ private const uint ENABLED0 = 0x0B0;
+ private const uint ENABLED1 = 0x0B4;
+ private const uint INTR = 0x0B8;
+ private const uint INTE = 0x0BC;
+ private const uint INTF = 0x0C0;
+ private const uint INTS = 0x0C4;
+
+ // We store ctrl/div for each domain index (0=gpout0..3, 4=ref, 5=sys, 6=peri, 7=usb, 8=adc, 9=rtc)
+ private readonly uint[] _ctrl = new uint[10];
+ private readonly uint[] _div = new uint[10];
+ private uint _resusCtrl;
+ private uint _fc0Src;
+ private uint _wakeEn0 = 0xFFFFFFFF, _wakeEn1 = 0xFFFF;
+ private uint _sleepEn0 = 0xFFFFFFFF, _sleepEn1 = 0xFFFF;
+ private uint _inte;
+
+ // Default divider = 1.0 (integer=1, frac=0 → top byte = 0x01, rest 0 → 0x01000000)
+ private const uint DIV_DEFAULT = 0x01000000;
+
+ public ClocksPeripheral()
+ {
+ for (int i = 0; i < _div.Length; i++)
+ _div[i] = DIV_DEFAULT;
+ }
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address)
+ {
+ return address switch
+ {
+ // GPOUTn: stride 0x0C, base 0x000
+ var a when a >= 0x000 && a <= 0x02C =>
+ ReadClockDomain((a / 0x0C), (a % 0x0C)),
+ CLK_REF_CTRL => _ctrl[4],
+ CLK_REF_DIV => _div[4],
+ CLK_REF_SELECTED => 1u << (int)(_ctrl[4] & 0x3u), // SRC bits [1:0]: ROSC=0, AUX=1, XOSC=2
+ CLK_SYS_CTRL => _ctrl[5],
+ CLK_SYS_DIV => _div[5],
+ CLK_SYS_SELECTED => 1u << (int)(_ctrl[5] & 0x1u), // SRC bit [0]: CLK_REF=0, AUX=1
+ CLK_PERI_CTRL => _ctrl[6],
+ CLK_PERI_SELECTED => 1u,
+ CLK_USB_CTRL => _ctrl[7],
+ CLK_USB_DIV => _div[7],
+ CLK_USB_SELECTED => 1u,
+ CLK_ADC_CTRL => _ctrl[8],
+ CLK_ADC_DIV => _div[8],
+ CLK_ADC_SELECTED => 1u,
+ CLK_RTC_CTRL => _ctrl[9],
+ CLK_RTC_DIV => _div[9],
+ CLK_RTC_SELECTED => 1u,
+ CLK_SYS_RESUS_CTRL => _resusCtrl,
+ CLK_SYS_RESUS_STATUS => 0, // no resuscitation needed
+ FC0_SRC => _fc0Src,
+ FC0_STATUS => 0x10, // FC_DONE
+ FC0_RESULT => 125_000 << 5, // 125 MHz: KHZ field at bits[28:5], so 125000 << 5
+ WAKE_EN0 => _wakeEn0,
+ WAKE_EN1 => _wakeEn1,
+ SLEEP_EN0 => _sleepEn0,
+ SLEEP_EN1 => _sleepEn1,
+ ENABLED0 => 0xFFFFFFFF,
+ ENABLED1 => 0xFFFF,
+ INTR => 0,
+ INTE => _inte,
+ INTF => 0,
+ INTS => 0,
+ _ => 0,
+ };
+ }
+
+ private uint ReadClockDomain(uint domain, uint field) => field switch
+ {
+ 0x00 => _ctrl[domain],
+ 0x04 => _div[domain],
+ 0x08 => 1u, // SELECTED always 1
+ _ => 0,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case var a when a >= 0x000 && a <= 0x02C:
+ WriteClockDomain(a / 0x0Cu, a % 0x0Cu, value); break;
+ case CLK_REF_CTRL: _ctrl[4] = value; break;
+ case CLK_REF_DIV: _div[4] = value; break;
+ case CLK_SYS_CTRL: _ctrl[5] = value; break;
+ case CLK_SYS_DIV: _div[5] = value; break;
+ case CLK_PERI_CTRL: _ctrl[6] = value; break;
+ case CLK_USB_CTRL: _ctrl[7] = value; break;
+ case CLK_USB_DIV: _div[7] = value; break;
+ case CLK_ADC_CTRL: _ctrl[8] = value; break;
+ case CLK_ADC_DIV: _div[8] = value; break;
+ case CLK_RTC_CTRL: _ctrl[9] = value; break;
+ case CLK_RTC_DIV: _div[9] = value; break;
+ case CLK_SYS_RESUS_CTRL: _resusCtrl = value; break;
+ case FC0_SRC: _fc0Src = value; break;
+ case WAKE_EN0: _wakeEn0 = value; break;
+ case WAKE_EN1: _wakeEn1 = value; break;
+ case SLEEP_EN0: _sleepEn0 = value; break;
+ case SLEEP_EN1: _sleepEn1 = value; break;
+ case INTE: _inte = value; break;
+ }
+ }
+
+ private void WriteClockDomain(uint domain, uint field, uint value)
+ {
+ switch (field)
+ {
+ case 0x00: _ctrl[domain] = value; break;
+ case 0x04: _div[domain] = value; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Dma/DmaPeripheral.cs b/src/RP2040Sharp/Peripherals/Dma/DmaPeripheral.cs
new file mode 100644
index 0000000..5d83784
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Dma/DmaPeripheral.cs
@@ -0,0 +1,384 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Dma;
+
+///
+/// RP2040 DMA controller (base 0x50000000, region 0x5).
+/// 12 DMA channels. Transfers execute synchronously when CTRL_TRIG is written
+/// with EN=1, making simulation deterministic.
+///
+public sealed class DmaPeripheral : IMemoryMappedDevice
+{
+ private const int CHANNEL_COUNT = 12;
+ private const uint CHANNEL_SIZE = 0x40; // 64 bytes per channel
+
+ // Channel register offsets within each 64-byte block
+ private const uint OFF_READ_ADDR = 0x00;
+ private const uint OFF_WRITE_ADDR = 0x04;
+ private const uint OFF_TRANS_COUNT = 0x08;
+ private const uint OFF_CTRL_TRIG = 0x0C;
+
+ // System registers (above channel space)
+ private const uint REG_INTR = 0x400;
+ private const uint REG_INTE0 = 0x404;
+ private const uint REG_INTF0 = 0x408;
+ private const uint REG_INTS0 = 0x40C;
+ private const uint REG_INTE1 = 0x414;
+ private const uint REG_INTF1 = 0x418;
+ private const uint REG_INTS1 = 0x41C;
+ private const uint REG_TIMER0 = 0x420;
+ private const uint REG_TIMER1 = 0x424;
+ private const uint REG_TIMER2 = 0x428;
+ private const uint REG_TIMER3 = 0x42C;
+ private const uint REG_MULTI_CHAN = 0x430;
+ private const uint REG_SNIFF_CTRL = 0x434;
+ private const uint REG_SNIFF_DATA = 0x438;
+ private const uint REG_FIFO_LEVELS = 0x440;
+ private const uint REG_CHAN_ABORT = 0x444;
+ private const uint REG_N_CHANNELS = 0x448;
+
+ // AL1 alias offsets within channel block (+0x10): CTRL, READ, WRITE, TRANS_TRIG
+ private const uint AL1_OFF = 0x10;
+ // AL2 alias offsets within channel block (+0x20): CTRL, TRANS, READ, WRITE_TRIG
+ private const uint AL2_OFF = 0x20;
+ // AL3 alias offsets within channel block (+0x30): CTRL, WRITE, TRANS, READ_TRIG
+ private const uint AL3_OFF = 0x30;
+
+ // CTRL bit masks
+ private const uint CTRL_EN = 1u << 0;
+ private const uint CTRL_BUSY = 1u << 24;
+ private const uint CTRL_AHB_ERROR = 1u << 31;
+ private const uint CTRL_DATA_SIZE = 3u << 2; // bits 3:2
+ private const uint CTRL_INCR_READ = 1u << 4;
+ private const uint CTRL_INCR_WRITE = 1u << 5;
+ private const uint CTRL_BSWAP = 1u << 22;
+ private const uint CTRL_IRQ_QUIET = 1u << 21;
+ private const uint CTRL_CHAIN_TO = 0xFu << 11; // bits 14:11
+
+ private readonly BusInterconnect _bus;
+ private readonly CortexM0Plus _cpu;
+
+ // Per-channel state
+ private readonly uint[] _readAddr = new uint[CHANNEL_COUNT];
+ private readonly uint[] _writeAddr = new uint[CHANNEL_COUNT];
+ private readonly uint[] _transCount = new uint[CHANNEL_COUNT];
+ private readonly uint[] _ctrl = new uint[CHANNEL_COUNT];
+
+ // DREQ sources: 64 DREQ lines. Null = always ready (same as PERMANENT/TREQ=63).
+ // Returns true when the peripheral is ready for one data beat.
+ private readonly Func?[] _dreqSources = new Func?[64];
+
+ private const int TREQ_PERMANENT = 0x3F;
+
+ // System registers
+ private uint _intr; // pending channel complete flags
+ private uint _inte0; // IRQ0 enable mask
+ private uint _intf0; // IRQ0 force mask
+ private uint _inte1; // IRQ1 enable mask
+ private uint _intf1; // IRQ1 force mask
+ private uint _timer0, _timer1, _timer2, _timer3;
+ private uint _sniffCtrl;
+ private uint _sniffData;
+
+ public uint Size => 0x1000;
+
+ ///
+ /// Register a DREQ source for the given DREQ index (0–62).
+ /// The delegate returns true when the peripheral is ready for one beat.
+ /// DREQ 63 (PERMANENT) is always ready and cannot be overridden.
+ ///
+ public void RegisterDreq(int dreqIndex, Func ready)
+ {
+ if (dreqIndex is < 0 or >= TREQ_PERMANENT)
+ throw new ArgumentOutOfRangeException(nameof(dreqIndex));
+ _dreqSources[dreqIndex] = ready;
+ }
+
+ public DmaPeripheral(BusInterconnect bus, CortexM0Plus cpu)
+ {
+ _bus = bus;
+ _cpu = cpu;
+ // Default CHAIN_TO: each channel chains to itself (no chaining)
+ for (var i = 0; i < CHANNEL_COUNT; i++)
+ _ctrl[i] = (uint)i << 11;
+ }
+
+ // ── IMemoryMappedDevice ──────────────────────────────────────────
+
+ public uint ReadWord(uint address)
+ {
+ if (address < CHANNEL_COUNT * CHANNEL_SIZE)
+ {
+ var ch = (int)(address / CHANNEL_SIZE);
+ var off = address % CHANNEL_SIZE;
+ return off switch
+ {
+ OFF_READ_ADDR => _readAddr[ch],
+ OFF_WRITE_ADDR => _writeAddr[ch],
+ OFF_TRANS_COUNT => _transCount[ch],
+ OFF_CTRL_TRIG => _ctrl[ch],
+ // AL1: CTRL, READ, WRITE, TRANS (trigger)
+ 0x10 => _ctrl[ch],
+ 0x14 => _readAddr[ch],
+ 0x18 => _writeAddr[ch],
+ 0x1C => _transCount[ch],
+ // AL2: CTRL, TRANS, READ, WRITE (trigger)
+ 0x20 => _ctrl[ch],
+ 0x24 => _transCount[ch],
+ 0x28 => _readAddr[ch],
+ 0x2C => _writeAddr[ch],
+ // AL3: CTRL, WRITE, TRANS, READ (trigger)
+ 0x30 => _ctrl[ch],
+ 0x34 => _writeAddr[ch],
+ 0x38 => _transCount[ch],
+ 0x3C => _readAddr[ch],
+ _ => 0,
+ };
+ }
+
+ return address switch
+ {
+ REG_INTR => _intr,
+ REG_INTE0 => _inte0,
+ REG_INTF0 => _intf0,
+ REG_INTS0 => (_intr | _intf0) & _inte0,
+ REG_INTE1 => _inte1,
+ REG_INTF1 => _intf1,
+ REG_INTS1 => (_intr | _intf1) & _inte1,
+ REG_TIMER0 => _timer0,
+ REG_TIMER1 => _timer1,
+ REG_TIMER2 => _timer2,
+ REG_TIMER3 => _timer3,
+ REG_SNIFF_CTRL => _sniffCtrl,
+ REG_SNIFF_DATA => _sniffData,
+ REG_FIFO_LEVELS => 0,
+ REG_N_CHANNELS => CHANNEL_COUNT,
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ if (address < CHANNEL_COUNT * CHANNEL_SIZE)
+ {
+ WriteChannelWord(address, value);
+ return;
+ }
+
+ switch (address)
+ {
+ case REG_INTR: _intr &= ~value; break; // write 1 to clear
+ case REG_INTE0: _inte0 = value & 0xFFF; break;
+ case REG_INTF0: _intf0 = value & 0xFFF; break;
+ case REG_INTE1: _inte1 = value & 0xFFF; break;
+ case REG_INTF1: _intf1 = value & 0xFFF; break;
+ case REG_TIMER0: _timer0 = value; break;
+ case REG_TIMER1: _timer1 = value; break;
+ case REG_TIMER2: _timer2 = value; break;
+ case REG_TIMER3: _timer3 = value; break;
+ case REG_SNIFF_CTRL: _sniffCtrl = value; break;
+ case REG_SNIFF_DATA: _sniffData = value; break;
+ case REG_CHAN_ABORT:
+ // Abort in-flight channels — since transfers are synchronous they're
+ // already done, so just clear BUSY on matching channels
+ for (var i = 0; i < CHANNEL_COUNT; i++)
+ if ((value & (1u << i)) != 0)
+ _ctrl[i] &= ~CTRL_BUSY;
+ break;
+ case REG_MULTI_CHAN:
+ // Trigger multiple channels simultaneously
+ for (var i = 0; i < CHANNEL_COUNT; i++)
+ if ((value & (1u << i)) != 0 && (_ctrl[i] & CTRL_EN) != 0)
+ ExecuteChannel(i);
+ break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ var current = ReadWord(aligned);
+ WriteWord(aligned, (current & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ var current = ReadWord(aligned);
+ WriteWord(aligned, (current & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ // ── Private ──────────────────────────────────────────────────────
+
+ private void WriteChannelWord(uint address, uint value)
+ {
+ var ch = (int)(address / CHANNEL_SIZE);
+ var off = address % CHANNEL_SIZE;
+
+ switch (off)
+ {
+ case OFF_READ_ADDR:
+ _readAddr[ch] = value; break;
+ case OFF_WRITE_ADDR:
+ _writeAddr[ch] = value; break;
+ case OFF_TRANS_COUNT:
+ _transCount[ch] = value; break;
+ case OFF_CTRL_TRIG:
+ if (value == 0)
+ {
+ // Null trigger: signal completion without starting a transfer
+ NullTrigger(ch);
+ break;
+ }
+ _ctrl[ch] = value & ~CTRL_BUSY; // BUSY is HW-driven
+ if ((value & CTRL_EN) != 0 && _transCount[ch] > 0)
+ ExecuteChannel(ch);
+ break;
+ // AL1: CTRL, READ, WRITE, TRANS_TRIG (last triggers)
+ case 0x10: _ctrl[ch] = value & ~CTRL_BUSY; break;
+ case 0x14: _readAddr[ch] = value; break;
+ case 0x18: _writeAddr[ch] = value; break;
+ case 0x1C:
+ _transCount[ch] = value;
+ if ((_ctrl[ch] & CTRL_EN) != 0 && _transCount[ch] > 0) ExecuteChannel(ch);
+ else if (_transCount[ch] == 0) NullTrigger(ch);
+ break;
+ // AL2: CTRL, TRANS, READ, WRITE_TRIG (last triggers)
+ case 0x20: _ctrl[ch] = value & ~CTRL_BUSY; break;
+ case 0x24: _transCount[ch] = value; break;
+ case 0x28: _readAddr[ch] = value; break;
+ case 0x2C:
+ _writeAddr[ch] = value;
+ if ((_ctrl[ch] & CTRL_EN) != 0 && _transCount[ch] > 0) ExecuteChannel(ch);
+ else if (_transCount[ch] == 0) NullTrigger(ch);
+ break;
+ // AL3: CTRL, WRITE, TRANS, READ_TRIG (last triggers)
+ case 0x30: _ctrl[ch] = value & ~CTRL_BUSY; break;
+ case 0x34: _writeAddr[ch] = value; break;
+ case 0x38: _transCount[ch] = value; break;
+ case 0x3C:
+ _readAddr[ch] = value;
+ if ((_ctrl[ch] & CTRL_EN) != 0 && _transCount[ch] > 0) ExecuteChannel(ch);
+ else if (_transCount[ch] == 0) NullTrigger(ch);
+ break;
+ }
+ }
+
+ private void NullTrigger(int ch)
+ {
+ // Null trigger: signal completion without performing any transfer.
+ // Per RP2040 TRM §2.5.2 and rp2040js: fires when IRQ_QUIET IS SET.
+ // IRQ_QUIET suppresses normal end-of-transfer IRQs to allow chained sub-transfers;
+ // a null write (value=0) to the final trigger alias signals the logical transfer is done.
+ if ((_ctrl[ch] & CTRL_IRQ_QUIET) != 0)
+ {
+ _intr |= 1u << ch;
+ if ((_inte0 & (1u << ch)) != 0) _cpu.SetInterrupt(11, true);
+ if ((_inte1 & (1u << ch)) != 0) _cpu.SetInterrupt(12, true);
+ }
+ }
+
+ private void ExecuteChannel(int ch)
+ {
+ _ctrl[ch] |= CTRL_BUSY;
+
+ var dataSize = (int)((_ctrl[ch] & CTRL_DATA_SIZE) >> 2); // 0=byte, 1=half, 2=word
+ var incrRead = (_ctrl[ch] & CTRL_INCR_READ) != 0;
+ var incrWrite = (_ctrl[ch] & CTRL_INCR_WRITE) != 0;
+ var bswap = (_ctrl[ch] & CTRL_BSWAP) != 0;
+ var count = _transCount[ch];
+ var rAddr = _readAddr[ch];
+ var wAddr = _writeAddr[ch];
+ var stride = 1u << dataSize;
+
+ // DREQ: bits [20:15] of CTRL
+ var treqSel = (int)((_ctrl[ch] >> 15) & 0x3F);
+ var dreqSource = treqSel == TREQ_PERMANENT ? null : _dreqSources[treqSel];
+
+ // Ring buffer: RING_SIZE bits [9:6], RING_SEL bit 10
+ var ringSize = (int)((_ctrl[ch] >> 6) & 0xF);
+ var ringSel = ((_ctrl[ch] >> 10) & 1) != 0; // false=read ring, true=write ring
+ var ringMask = ringSize > 0 ? (1u << ringSize) - 1 : 0u;
+
+ var beatsExecuted = 0u;
+ for (var i = 0u; i < count; i++)
+ {
+ // Check DREQ: if source is registered and says not ready, stop
+ if (dreqSource != null && !dreqSource())
+ break;
+
+ uint data = dataSize switch
+ {
+ 0 => _bus.ReadByte(rAddr),
+ 1 => _bus.ReadHalfWord(rAddr),
+ _ => _bus.ReadWord(rAddr),
+ };
+
+ if (bswap)
+ data = dataSize switch
+ {
+ 0 => data,
+ 1 => ((data & 0xFF) << 8) | (data >> 8),
+ _ => ((data & 0xFF) << 24) | ((data & 0xFF00) << 8)
+ | ((data >> 8) & 0xFF00) | (data >> 24),
+ };
+
+ switch (dataSize)
+ {
+ case 0: _bus.WriteByte(wAddr, (byte)data); break;
+ case 1: _bus.WriteHalfWord(wAddr, (ushort)data); break;
+ default: _bus.WriteWord(wAddr, data); break;
+ }
+
+ if (incrRead)
+ {
+ if (ringSize > 0 && !ringSel)
+ rAddr = (rAddr & ~ringMask) | ((rAddr + stride) & ringMask);
+ else
+ rAddr += stride;
+ }
+ if (incrWrite)
+ {
+ if (ringSize > 0 && ringSel)
+ wAddr = (wAddr & ~ringMask) | ((wAddr + stride) & ringMask);
+ else
+ wAddr += stride;
+ }
+ beatsExecuted++;
+ }
+
+ _readAddr[ch] = rAddr;
+ _writeAddr[ch] = wAddr;
+ _transCount[ch] = count - beatsExecuted;
+
+ // If not all beats completed (DREQ not ready), stay BUSY
+ if (_transCount[ch] == 0)
+ {
+ // Hardware keeps EN=1 after transfer completes; only BUSY is cleared
+ _ctrl[ch] &= ~CTRL_BUSY;
+
+ // Signal completion
+ _intr |= 1u << ch;
+
+ // Fire CPU interrupt if unmasked — DMA_IRQ0=11, DMA_IRQ1=12
+ if ((_inte0 & (1u << ch)) != 0)
+ _cpu.SetInterrupt(11, true);
+ if ((_inte1 & (1u << ch)) != 0)
+ _cpu.SetInterrupt(12, true);
+
+ // Chain to another channel if configured
+ var chainTo = (int)((_ctrl[ch] & CTRL_CHAIN_TO) >> 11);
+ if (chainTo != ch && (_ctrl[chainTo] & CTRL_EN) != 0)
+ ExecuteChannel(chainTo);
+ }
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Gpio/GpioPin.cs b/src/RP2040Sharp/Peripherals/Gpio/GpioPin.cs
new file mode 100644
index 0000000..781d5d1
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Gpio/GpioPin.cs
@@ -0,0 +1,54 @@
+namespace RP2040.Peripherals.Gpio;
+
+///
+/// Represents a single GPIO pin on the RP2040.
+/// Direction and output value are driven by the SIO peripheral;
+/// input value is exposed here for external connection.
+///
+public sealed class GpioPin
+{
+ private readonly int _pinIndex;
+ private readonly Sio.SioPeripheral _sio;
+ private readonly IoBank0Peripheral? _ioBank0;
+
+ internal GpioPin(int pinIndex, Sio.SioPeripheral sio, IoBank0Peripheral? ioBank0 = null)
+ {
+ _pinIndex = pinIndex;
+ _sio = sio;
+ _ioBank0 = ioBank0;
+ }
+
+ /// Pin is configured as output (SIO GPIO_OE bit is set).
+ public bool IsOutput => (_sio.GpioOe & (1u << _pinIndex)) != 0;
+
+ ///
+ /// Pin is assigned to a PIO state machine (FUNCSEL = 6 for PIO0 or 7 for PIO1).
+ /// PIO-driven pins are configured via IO_BANK0 FUNCSEL, not SIO GPIO_OE, so
+ /// is false for PIO pins even when the SM drives them.
+ ///
+ public bool IsPioOutput => _ioBank0 is not null && (_ioBank0.GetFuncSel(_pinIndex) is 6 or 7);
+
+ /// Current output level driven by software (SIO GPIO_OUT).
+ public bool OutputValue => (_sio.GpioOut & (1u << _pinIndex)) != 0;
+
+ ///
+ /// Digital level seen by the processor (combines output + external input).
+ /// When the pin is an output this matches ;
+ /// when it is an input it reflects the value injected via .
+ ///
+ public bool DigitalValue => IsOutput
+ ? OutputValue
+ : ((_sio.GpioIn) & (1u << _pinIndex)) != 0;
+
+ ///
+ /// Inject an external signal level into this pin (simulates a physical connection).
+ /// Only effective when the pin is configured as an input.
+ /// Notifies IoBank0 to trigger edge/level GPIO interrupts.
+ ///
+ public void ForceInput(bool high)
+ {
+ var mask = 1u << _pinIndex;
+ _sio.GpioIn = high ? (_sio.GpioIn | mask) : (_sio.GpioIn & ~mask);
+ _ioBank0?.UpdatePinInput(_pinIndex, high);
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Gpio/IoBank0Peripheral.cs b/src/RP2040Sharp/Peripherals/Gpio/IoBank0Peripheral.cs
new file mode 100644
index 0000000..a133fa5
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Gpio/IoBank0Peripheral.cs
@@ -0,0 +1,254 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+using RP2040.Peripherals.Sio;
+
+namespace RP2040.Peripherals.Gpio;
+
+///
+/// IO_BANK0 peripheral (base 0x40014000).
+/// Each GPIO pin has a STATUS (RO) and CTRL (RW) register pair at offsets n*8 and n*8+4.
+/// FUNCSEL bits [4:0] of CTRL select the peripheral that drives/reads the pin.
+/// Supports IRQ edge/level detection and PROC0_INTE/INTF/INTS interrupt bank.
+///
+public sealed class IoBank0Peripheral : IMemoryMappedDevice
+{
+ private const int GPIO_COUNT = 30;
+
+ // Register layout offsets
+ private const uint GPIO_CTRL_LAST = 0x0EC; // last byte of GPIO pair area
+ private const uint INTR_BASE = 0x0F0; // INTR0-3 raw interrupt (write 1 to clear edge)
+ private const uint PROC0_INTE_BASE = 0x100; // PROC0_INTE0-3
+ private const uint PROC0_INTF_BASE = 0x110; // PROC0_INTF0-3
+ private const uint PROC0_INTS_BASE = 0x120; // PROC0_INTS0-3 (RO)
+ private const uint PROC1_INTE_BASE = 0x130; // PROC1 (single-core: store only)
+ private const uint PROC1_INTF_BASE = 0x140;
+ private const uint PROC1_INTS_BASE = 0x150;
+
+ // IRQ event bits per pin (4 bits per pin in INTR registers)
+ private const uint IRQ_LEVEL_LOW = 1u << 0;
+ private const uint IRQ_LEVEL_HIGH = 1u << 1;
+ private const uint IRQ_EDGE_LOW = 1u << 2;
+ private const uint IRQ_EDGE_HIGH = 1u << 3;
+
+ // CTRL field masks
+ private const uint FUNCSEL_MASK = 0x1F;
+ private const uint FUNCSEL_SIO = 5;
+
+ // IO_IRQ_BANK0 = hardware IRQ 13
+ private const int IO_IRQ_BANK0 = 13;
+
+ private readonly CortexM0Plus? _cpu;
+ private readonly SioPeripheral _sio;
+
+ private readonly uint[] _ctrl = new uint[GPIO_COUNT];
+ private readonly bool[] _gpioInput = new bool[GPIO_COUNT]; // current input state
+ private readonly uint[] _intrEdge = new uint[GPIO_COUNT]; // edge IRQ bits per pin (bits 2-3)
+
+ private readonly uint[] _proc0Inte = new uint[4];
+ private readonly uint[] _proc0Intf = new uint[4];
+ private readonly uint[] _proc1Inte = new uint[4];
+ private readonly uint[] _proc1Intf = new uint[4];
+
+ ///
+ /// Returns the FUNCSEL value [4:0] for .
+ /// Key values: 5 = SIO, 6 = PIO0, 7 = PIO1, 31 = NULL (hi-Z / default).
+ ///
+ public uint GetFuncSel(int pin)
+ {
+ if ((uint)pin >= GPIO_COUNT) return 31u;
+ return _ctrl[pin] & FUNCSEL_MASK;
+ }
+
+ public uint Size => 0x160;
+
+ public IoBank0Peripheral(SioPeripheral sio, CortexM0Plus? cpu = null)
+ {
+ _sio = sio;
+ _cpu = cpu;
+ // Default FUNCSEL=31 (NULL / hi-Z) for all pins
+ Array.Fill(_ctrl, 0x1Fu);
+ }
+
+ // ── GPIO input update ────────────────────────────────────────────
+
+ ///
+ /// Notify that a GPIO input pin changed value. This detects edges and
+ /// updates INTR edge bits, then fires the NVIC interrupt if enabled.
+ ///
+ public void UpdatePinInput(int pin, bool value)
+ {
+ if (pin < 0 || pin >= GPIO_COUNT) return;
+
+ var old = _gpioInput[pin];
+ _gpioInput[pin] = value;
+
+ if (!old && value) _intrEdge[pin] |= IRQ_EDGE_HIGH;
+ if (old && !value) _intrEdge[pin] |= IRQ_EDGE_LOW;
+
+ CheckInterrupts();
+ }
+
+ // ── IMemoryMappedDevice ──────────────────────────────────────────
+
+ public uint ReadWord(uint address)
+ {
+ if (address <= GPIO_CTRL_LAST)
+ {
+ var pinPair = address >> 3;
+ if (pinPair >= GPIO_COUNT) return 0;
+ return (address & 4) != 0 ? _ctrl[pinPair] : ReadStatus((int)pinPair);
+ }
+
+ if (address >= INTR_BASE && address < PROC0_INTE_BASE)
+ return BuildIntr((int)((address - INTR_BASE) >> 2));
+
+ if (address >= PROC0_INTE_BASE && address < PROC0_INTF_BASE)
+ return _proc0Inte[(address - PROC0_INTE_BASE) >> 2];
+
+ if (address >= PROC0_INTF_BASE && address < PROC0_INTS_BASE)
+ return _proc0Intf[(address - PROC0_INTF_BASE) >> 2];
+
+ if (address >= PROC0_INTS_BASE && address < PROC1_INTE_BASE)
+ {
+ var reg = (int)((address - PROC0_INTS_BASE) >> 2);
+ return (BuildIntr(reg) | _proc0Intf[reg]) & _proc0Inte[reg];
+ }
+
+ if (address >= PROC1_INTE_BASE && address < PROC1_INTF_BASE)
+ return _proc1Inte[(address - PROC1_INTE_BASE) >> 2];
+
+ if (address >= PROC1_INTF_BASE && address < PROC1_INTS_BASE)
+ return _proc1Intf[(address - PROC1_INTF_BASE) >> 2];
+
+ if (address >= PROC1_INTS_BASE && address < PROC1_INTS_BASE + 0x10)
+ {
+ var reg = (int)((address - PROC1_INTS_BASE) >> 2);
+ return (BuildIntr(reg) | _proc1Intf[reg]) & _proc1Inte[reg];
+ }
+
+ return 0;
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ if (address <= GPIO_CTRL_LAST)
+ {
+ var pinPair = address >> 3;
+ if (pinPair >= GPIO_COUNT) return;
+ if ((address & 4) != 0) _ctrl[pinPair] = value;
+ // STATUS is read-only
+ return;
+ }
+
+ if (address >= INTR_BASE && address < PROC0_INTE_BASE)
+ {
+ // Write 1 to clear edge IRQ bits
+ var reg = (int)((address - INTR_BASE) >> 2);
+ ClearEdgeBits(reg, value);
+ return;
+ }
+
+ if (address >= PROC0_INTE_BASE && address < PROC0_INTF_BASE)
+ {
+ _proc0Inte[(address - PROC0_INTE_BASE) >> 2] = value;
+ CheckInterrupts();
+ return;
+ }
+
+ if (address >= PROC0_INTF_BASE && address < PROC0_INTS_BASE)
+ {
+ _proc0Intf[(address - PROC0_INTF_BASE) >> 2] = value;
+ CheckInterrupts();
+ return;
+ }
+
+ if (address >= PROC1_INTE_BASE && address < PROC1_INTF_BASE)
+ {
+ _proc1Inte[(address - PROC1_INTE_BASE) >> 2] = value;
+ return;
+ }
+
+ if (address >= PROC1_INTF_BASE && address < PROC1_INTS_BASE)
+ {
+ _proc1Intf[(address - PROC1_INTF_BASE) >> 2] = value;
+ return;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ // ── Private helpers ──────────────────────────────────────────────
+
+ private uint ReadStatus(int pin)
+ {
+ var status = 0u;
+ var funcsel = _ctrl[pin] & FUNCSEL_MASK;
+ if (funcsel == FUNCSEL_SIO)
+ {
+ if ((_sio.GpioOe & (1u << pin)) != 0) status |= 1u << 13; // OETOPAD
+ if ((_sio.GpioOut & (1u << pin)) != 0) status |= 1u << 9; // OUTTOPAD
+ }
+ if (_gpioInput[pin]) status |= (1u << 17) | (1u << 19); // INFROMPAD + INTOPERI
+ return status;
+ }
+
+ ///
+ /// Build INTR register N (8 GPIOs per register, 4 bits each).
+ /// LEVEL bits computed from current input; EDGE bits from stored state.
+ ///
+ private uint BuildIntr(int reg)
+ {
+ var result = 0u;
+ for (var i = 0; i < 8; i++)
+ {
+ var pin = reg * 8 + i;
+ if (pin >= GPIO_COUNT) break;
+
+ uint bits = 0;
+ bits |= !_gpioInput[pin] ? IRQ_LEVEL_LOW : 0u;
+ bits |= _gpioInput[pin] ? IRQ_LEVEL_HIGH : 0u;
+ bits |= _intrEdge[pin] & (IRQ_EDGE_LOW | IRQ_EDGE_HIGH);
+ result |= bits << (i * 4);
+ }
+ return result;
+ }
+
+ private void ClearEdgeBits(int reg, uint mask)
+ {
+ for (var i = 0; i < 8; i++)
+ {
+ var pin = reg * 8 + i;
+ if (pin >= GPIO_COUNT) break;
+ var bits = (mask >> (i * 4)) & 0xF;
+ _intrEdge[pin] &= ~(bits & (IRQ_EDGE_LOW | IRQ_EDGE_HIGH));
+ }
+ CheckInterrupts();
+ }
+
+ private void CheckInterrupts()
+ {
+ if (_cpu is null) return;
+ var active = false;
+ for (var reg = 0; reg < 4 && !active; reg++)
+ active = ((BuildIntr(reg) | _proc0Intf[reg]) & _proc0Inte[reg]) != 0;
+ _cpu.SetInterrupt(IO_IRQ_BANK0, active);
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/I2c/I2cPeripheral.cs b/src/RP2040Sharp/Peripherals/I2c/I2cPeripheral.cs
new file mode 100644
index 0000000..fec1622
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/I2c/I2cPeripheral.cs
@@ -0,0 +1,378 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.I2c;
+
+///
+/// RP2040 I2C peripheral (DesignWare DW_apb_i2c).
+/// I2C0 base: 0x40044000, I2C1 base: 0x40048000.
+/// Transfer simulation via injectable callbacks.
+///
+public sealed class I2cPeripheral : IMemoryMappedDevice
+{
+ private const uint IC_CON = 0x000;
+ private const uint IC_TAR = 0x004;
+ private const uint IC_SAR = 0x008; // Slave address
+ private const uint IC_DATA_CMD = 0x010;
+ private const uint IC_SS_SCL_HCNT = 0x014;
+ private const uint IC_SS_SCL_LCNT = 0x018;
+ private const uint IC_FS_SCL_HCNT = 0x01C;
+ private const uint IC_FS_SCL_LCNT = 0x020;
+ private const uint IC_INTR_STAT = 0x02C;
+ private const uint IC_INTR_MASK = 0x030;
+ private const uint IC_RAW_INTR_STAT = 0x034;
+ private const uint IC_RX_TL = 0x038;
+ private const uint IC_TX_TL = 0x03C;
+ private const uint IC_CLR_INTR = 0x040;
+ private const uint IC_CLR_RX_UNDER = 0x044;
+ private const uint IC_CLR_RX_OVER = 0x048;
+ private const uint IC_CLR_TX_OVER = 0x04C;
+ private const uint IC_CLR_RD_REQ = 0x050;
+ private const uint IC_CLR_TX_ABRT = 0x054;
+ private const uint IC_CLR_RX_DONE = 0x058;
+ private const uint IC_CLR_ACTIVITY = 0x05C;
+ private const uint IC_CLR_STOP_DET = 0x060;
+ private const uint IC_CLR_START_DET = 0x064;
+ private const uint IC_CLR_GEN_CALL = 0x068;
+ private const uint IC_ENABLE = 0x06C;
+ private const uint IC_STATUS = 0x070;
+ private const uint IC_TXFLR = 0x074;
+ private const uint IC_RXFLR = 0x078;
+ private const uint IC_SDA_HOLD = 0x07C;
+ private const uint IC_TX_ABRT_SOURCE = 0x080;
+ private const uint IC_SLV_DATA_NACK_ONLY = 0x084;
+ private const uint IC_DMA_CR = 0x088;
+ private const uint IC_DMA_TDLR = 0x08C;
+ private const uint IC_DMA_RDLR = 0x090;
+ private const uint IC_SDA_SETUP = 0x094;
+ private const uint IC_ACK_GENERAL_CALL = 0x098;
+ private const uint IC_ENABLE_STATUS = 0x09C;
+ private const uint IC_FS_SPKLEN = 0x0A0;
+ private const uint IC_CLR_RESTART_DET = 0x0A8;
+ private const uint IC_COMP_PARAM_1 = 0x0F4;
+ private const uint IC_COMP_VERSION = 0x0F8;
+ private const uint IC_COMP_TYPE = 0x0FC;
+
+ // IC_STATUS bits
+ private const uint ST_ACTIVITY = 1u << 0;
+ private const uint ST_TFNF = 1u << 1; // TX FIFO not full
+ private const uint ST_TFE = 1u << 2; // TX FIFO empty
+ private const uint ST_RFNE = 1u << 3; // RX FIFO not empty
+ private const uint ST_RFF = 1u << 4; // RX FIFO full
+ private const uint ST_MST_ACTV = 1u << 5; // Master FSM active
+
+ private const int FIFO_DEPTH = 16;
+
+ private readonly CortexM0Plus? _cpu;
+ private readonly int _irq;
+
+ private uint _con = 0x65; // default: master, 7-bit, fast-mode enabled, restart enabled
+ private uint _tar;
+ private uint _sar = 0x55; // default slave address
+ private uint _ssSclHcnt, _ssSclLcnt;
+ private uint _fsSclHcnt = 0x06, _fsSclLcnt = 0x0D;
+ private uint _intrMask = 0x8FF;
+ private uint _rawIntr;
+ private uint _rxTl;
+ private uint _txTl;
+ private uint _enable;
+ private uint _sdaHold = 0x1;
+ private uint _slvDataNackOnly;
+ private uint _dmaCr;
+ private uint _dmaTdlr;
+ private uint _dmaRdlr;
+ private uint _sdaSetup = 0x64;
+ private uint _ackGeneralCall = 0x1;
+ private uint _fsSpklen = 0x7;
+
+ private readonly Queue _rxFifo = new(FIFO_DEPTH);
+
+ private bool _inSlaveTransmit;
+ private readonly Queue _slaveTxFifo = new(FIFO_DEPTH);
+
+ /// Called on each byte write: (targetAddress, data).
+ public Action? OnWrite;
+
+ /// Called on each byte read request: (targetAddress) → rx byte.
+ public Func? OnRead;
+
+ /// Called when the STOP bit is set in IC_DATA_CMD, signalling end of transaction.
+ public Action? OnStop;
+
+ /// Raised when firmware writes IC_SAR (slave address register). Argument is the new 7-bit address (0 = slave disabled).
+ public event Action? SlaveAddressChanged;
+
+ /// The 7-bit slave address currently written in IC_SAR.
+ public byte SlaveAddress => (byte)(_sar & 0x7F);
+
+ public uint Size => 0x1000;
+
+ public I2cPeripheral(CortexM0Plus? cpu = null, int irq = 0)
+ {
+ _cpu = cpu;
+ _irq = irq;
+ }
+
+ // ── IMemoryMappedDevice ──────────────────────────────────────────
+
+ public uint ReadWord(uint address)
+ {
+ return address switch
+ {
+ IC_CON => _con,
+ IC_TAR => _tar,
+ IC_SAR => _sar,
+ IC_DATA_CMD => PopRxFifo(),
+ IC_SS_SCL_HCNT => _ssSclHcnt,
+ IC_SS_SCL_LCNT => _ssSclLcnt,
+ IC_FS_SCL_HCNT => _fsSclHcnt,
+ IC_FS_SCL_LCNT => _fsSclLcnt,
+ IC_INTR_STAT => _rawIntr & _intrMask,
+ IC_INTR_MASK => _intrMask,
+ IC_RAW_INTR_STAT => _rawIntr,
+ IC_RX_TL => _rxTl,
+ IC_TX_TL => _txTl,
+ IC_CLR_INTR => ClearAllInterrupts(),
+ IC_CLR_RX_UNDER => ClearBit(0),
+ IC_CLR_RX_OVER => ClearBit(1),
+ IC_CLR_TX_OVER => ClearBit(3),
+ IC_CLR_RD_REQ => ClearBit(5),
+ IC_CLR_TX_ABRT => ClearBit(6),
+ IC_CLR_RX_DONE => ClearBit(7),
+ IC_CLR_ACTIVITY => ClearBit(8),
+ IC_CLR_STOP_DET => ClearBit(9),
+ IC_CLR_START_DET => ClearBit(10),
+ IC_CLR_GEN_CALL => ClearBit(11),
+ IC_ENABLE => _enable,
+ IC_STATUS => BuildStatus(),
+ IC_TXFLR => 0, // TX FIFO always drained in simulation
+ IC_RXFLR => (uint)_rxFifo.Count,
+ IC_SDA_HOLD => _sdaHold,
+ IC_TX_ABRT_SOURCE => 0,
+ IC_SLV_DATA_NACK_ONLY => _slvDataNackOnly,
+ IC_DMA_CR => _dmaCr,
+ IC_DMA_TDLR => _dmaTdlr,
+ IC_DMA_RDLR => _dmaRdlr,
+ IC_SDA_SETUP => _sdaSetup,
+ IC_ACK_GENERAL_CALL => _ackGeneralCall,
+ IC_ENABLE_STATUS => _enable & 1,
+ IC_FS_SPKLEN => _fsSpklen,
+ IC_CLR_RESTART_DET => ClearBit(12),
+ IC_COMP_PARAM_1 => 0,
+ IC_COMP_VERSION => 0x3230312A,
+ IC_COMP_TYPE => 0x44570140,
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case IC_CON: _con = value & 0x7FF; break;
+ case IC_TAR: _tar = value & 0x3FF; break;
+ case IC_SAR:
+ _sar = value & 0x3FF;
+ SlaveAddressChanged?.Invoke((byte)(_sar & 0x7F));
+ break;
+ case IC_DATA_CMD: HandleDataCmd(value); break;
+ case IC_SS_SCL_HCNT: _ssSclHcnt = value & 0xFFFF; break;
+ case IC_SS_SCL_LCNT: _ssSclLcnt = value & 0xFFFF; break;
+ case IC_FS_SCL_HCNT: _fsSclHcnt = value & 0xFFFF; break;
+ case IC_FS_SCL_LCNT: _fsSclLcnt = value & 0xFFFF; break;
+ case IC_INTR_MASK:
+ _intrMask = value & 0xFFF;
+ CheckInterrupts();
+ break;
+ case IC_RX_TL: _rxTl = value & 0xFF; break;
+ case IC_TX_TL: _txTl = value & 0xFF; break;
+ case IC_ENABLE:
+ _enable = value & 3;
+ // TX_EMPTY (bit 4): TX FIFO is at or below IC_TX_TL threshold.
+ // In simulation TX is always drained immediately, so set it when enabled.
+ if (IsEnabled) _rawIntr |= 1u << 4;
+ else _rawIntr &= ~(1u << 4);
+ CheckInterrupts();
+ break;
+ case IC_SDA_HOLD: _sdaHold = value & 0xFFFFFF; break;
+ case IC_SLV_DATA_NACK_ONLY: _slvDataNackOnly = value & 1; break;
+ case IC_DMA_CR: _dmaCr = value & 3; break;
+ case IC_DMA_TDLR: _dmaTdlr = value & 0xF; break;
+ case IC_DMA_RDLR: _dmaRdlr = value & 0xF; break;
+ case IC_SDA_SETUP: _sdaSetup = value & 0xFF; break;
+ case IC_ACK_GENERAL_CALL: _ackGeneralCall = value & 1; break;
+ case IC_FS_SPKLEN: _fsSpklen = value & 0xFF; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ // ── Private ──────────────────────────────────────────────────────
+
+ private bool IsEnabled => (_enable & 1) != 0;
+
+ private void HandleDataCmd(uint value)
+ {
+ if (!IsEnabled) return;
+
+ var isRead = (value & (1u << 8)) != 0;
+ var addr = (byte)(_tar & 0x7F);
+
+ if (isRead)
+ {
+ var rxByte = OnRead?.Invoke(addr) ?? 0;
+ if (_rxFifo.Count < FIFO_DEPTH)
+ _rxFifo.Enqueue(rxByte);
+ _rawIntr |= 1u << 2; // RX_FULL
+ CheckInterrupts();
+ }
+ else if (_inSlaveTransmit)
+ {
+ // Firmware is responding to RD_REQ in slave-transmit mode — capture the byte.
+ // The master may clock out several bytes before issuing STOP, so accumulate
+ // them rather than overwriting; the mode ends on SimulateStop().
+ if (_slaveTxFifo.Count < FIFO_DEPTH)
+ _slaveTxFifo.Enqueue((byte)(value & 0xFF));
+ _rawIntr |= 1u << 4; // TX_EMPTY
+ CheckInterrupts();
+ }
+ else
+ {
+ OnWrite?.Invoke(addr, (byte)(value & 0xFF));
+ // TX FIFO is always drained instantly in simulation
+ _rawIntr |= 1u << 4; // TX_EMPTY
+ CheckInterrupts();
+ }
+
+ // Signal STOP_DET when STOP bit set
+ if ((value & (1u << 9)) != 0)
+ {
+ OnStop?.Invoke();
+ _rawIntr |= 1u << 9;
+ CheckInterrupts();
+ }
+ }
+
+ private uint PopRxFifo()
+ {
+ if (_rxFifo.TryDequeue(out var data))
+ {
+ if (_rxFifo.Count == 0)
+ {
+ _rawIntr &= ~(1u << 2);
+ CheckInterrupts();
+ }
+ return data;
+ }
+ return 0;
+ }
+
+ private uint BuildStatus()
+ {
+ uint st = ST_TFE | ST_TFNF; // TX always ready in simulation
+ if (_rxFifo.Count > 0) st |= ST_RFNE;
+ if (_rxFifo.Count >= FIFO_DEPTH) st |= ST_RFF;
+ return st;
+ }
+
+ private uint ClearAllInterrupts()
+ {
+ _rawIntr = 0;
+ CheckInterrupts();
+ return 0;
+ }
+
+ private uint ClearBit(int bit)
+ {
+ _rawIntr &= ~(1u << bit);
+ CheckInterrupts();
+ return 0;
+ }
+
+ private void CheckInterrupts()
+ {
+ if (_cpu is null) return;
+ _cpu.SetInterrupt(_irq, (_rawIntr & _intrMask) != 0);
+ }
+
+ /// Inject a byte into the RX FIFO (simulates a slave device responding).
+ public void InjectByte(byte value)
+ {
+ if (_rxFifo.Count < FIFO_DEPTH)
+ _rxFifo.Enqueue(value);
+ }
+
+ // ── Slave simulation ─────────────────────────────────────────────────────────
+
+ ///
+ /// Simulate an external I2C master addressing this device as a slave.
+ /// Returns true when the address matches IC_SAR.
+ /// When the master wants to read ( = false), raises RD_REQ (bit 5)
+ /// so firmware can respond by writing to IC_DATA_CMD.
+ ///
+ public bool SimulateIncomingAddress(byte addr, bool isWrite)
+ {
+ if ((byte)(_sar & 0x7F) != addr)
+ return false;
+
+ if (!isWrite)
+ {
+ // Master wants to read from us — firmware must respond via IC_DATA_CMD.
+ _inSlaveTransmit = true;
+ _rawIntr |= 1u << 5; // RD_REQ
+ CheckInterrupts();
+ }
+
+ return true;
+ }
+
+ ///
+ /// Simulate a data byte delivered by an external master (slave-receive mode).
+ /// Places the byte in the RX FIFO and raises RX_FULL.
+ ///
+ public void SimulateIncomingData(byte data)
+ {
+ if (_rxFifo.Count < FIFO_DEPTH)
+ _rxFifo.Enqueue(data);
+ _rawIntr |= 1u << 2; // RX_FULL
+ CheckInterrupts();
+ }
+
+ ///
+ /// Simulate a STOP condition from an external master. Ends any slave-transmit phase
+ /// and raises STOP_DET (bit 9).
+ ///
+ public void SimulateStop()
+ {
+ _inSlaveTransmit = false;
+ _rawIntr |= 1u << 9; // STOP_DET
+ CheckInterrupts();
+ }
+
+ /// True while firmware still owes the master bytes captured for slave-transmit.
+ public bool HasSlaveTransmitByte => _slaveTxFifo.Count > 0;
+
+ ///
+ /// Dequeue the next byte firmware placed in IC_DATA_CMD for slave-transmit mode.
+ /// Returns 0 when no byte is pending.
+ ///
+ public byte ReadSlaveTransmitByte() => _slaveTxFifo.TryDequeue(out var b) ? b : (byte)0;
+}
diff --git a/src/RP2040Sharp/Peripherals/ITickable.cs b/src/RP2040Sharp/Peripherals/ITickable.cs
new file mode 100644
index 0000000..d0751d2
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/ITickable.cs
@@ -0,0 +1,10 @@
+namespace RP2040.Peripherals;
+
+///
+/// Peripheral that advances its simulation state by a given number of CPU cycles.
+/// Called by RP2040Machine at the end of each Run() batch.
+///
+public interface ITickable
+{
+ void Tick(long deltaCycles);
+}
diff --git a/src/RP2040Sharp/Peripherals/IoQspi/IoQspiPeripheral.cs b/src/RP2040Sharp/Peripherals/IoQspi/IoQspiPeripheral.cs
new file mode 100644
index 0000000..34fa7a8
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/IoQspi/IoQspiPeripheral.cs
@@ -0,0 +1,104 @@
+using RP2040.Core.Memory;
+using RP2040.Peripherals.Ssi;
+
+namespace RP2040.Peripherals.IoQspi;
+
+///
+/// IO_QSPI peripheral stub (0x40018000).
+/// Controls the QSPI GPIO pins (SCLK, SS, SD0-SD3). Stores FUNCSEL/CTRL
+/// registers but has no electrical simulation.
+///
+/// The SS pin (pin 1) OUTOVER field is monitored: OUTOVER=2 (drive low)
+/// asserts the QSPI chip-select and OUTOVER≠2 (after a drive-low) deasserts
+/// it. These transitions are forwarded to so
+/// the SSI can delimit flash command transactions.
+///
+public sealed class IoQspiPeripheral : IMemoryMappedDevice
+{
+ // 6 QSPI GPIO pins: SCLK, SS, SD0, SD1, SD2, SD3
+ // Each pin: STATUS (0, RO) + CTRL (4, R/W) → stride 8 bytes
+ private const int PIN_COUNT = 6;
+ private const int PIN_SS = 1; // SS = chip-select pin index
+
+ // IO_QSPI OUTOVER values (bits [9:8] of each pin's CTRL register)
+ private const uint OUTOVER_DRIVE_LOW = 2u;
+ private const int OUTOVER_SHIFT = 8; // shift count must be int in C#
+ private const uint OUTOVER_MASK = 3u << 8;
+
+ private readonly uint[] _ctrl = new uint[PIN_COUNT];
+
+ // SSI peripheral to notify on CS assert/deassert
+ private SsiPeripheral? _ssi;
+
+ public uint Size => 0x1000;
+
+ ///
+ /// Wire this peripheral to the SSI so SS CTRL OUTOVER changes propagate
+ /// as CS assert/deassert signals.
+ ///
+ public void AttachSsi(SsiPeripheral ssi) => _ssi = ssi;
+
+ public uint ReadWord(uint address)
+ {
+ var pin = (int)(address >> 3) & 0x1F;
+ if (pin >= PIN_COUNT) return 0;
+ var field = address & 7;
+ return field switch
+ {
+ // STATUS register: report INFROMPAD (bit 17) and INTOPERI (bit 19) as HIGH
+ // for all QSPI pins. Bit 17 is the critical one: the bootrom reads
+ // GPIO_QSPI_SS_STATUS.INFROMPAD to detect whether the BOOTSEL button is
+ // pressed (active-low). A zero would mean "BOOTSEL held → USB BOOTSEL mode".
+ 0 => 0x000A0000u, // INFROMPAD=1 (bit17), INTOPERI=1 (bit19)
+ 4 => _ctrl[pin],
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ var pin = (int)(address >> 3) & 0x1F;
+ if (pin >= PIN_COUNT) return;
+ if ((address & 7) == 4)
+ {
+ var prev = _ctrl[pin];
+ _ctrl[pin] = value;
+
+ // Monitor the SS pin (pin 1) OUTOVER field [9:8].
+ // OUTOVER=2 (DRIVE_LOW) → CS asserted (flash_cs_force(low)).
+ // Any other value after DRIVE_LOW → CS deasserted.
+ if (pin == PIN_SS && _ssi != null)
+ {
+ var prevOutover = (prev & OUTOVER_MASK) >> OUTOVER_SHIFT;
+ var newOutover = (value & OUTOVER_MASK) >> OUTOVER_SHIFT;
+ if (prevOutover != newOutover)
+ {
+ if (newOutover == OUTOVER_DRIVE_LOW)
+ _ssi.OnCsAssert();
+ else if (prevOutover == OUTOVER_DRIVE_LOW)
+ _ssi.OnCsDeassert();
+ }
+ }
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Pads/PadsPeripheral.cs b/src/RP2040Sharp/Peripherals/Pads/PadsPeripheral.cs
new file mode 100644
index 0000000..8b8122e
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Pads/PadsPeripheral.cs
@@ -0,0 +1,72 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Pads;
+
+///
+/// PADS_BANK0 peripheral (0x4001C000) and PADS_QSPI (0x40020000).
+/// Controls pad electrical characteristics: I/O enable, drive strength, pull,
+/// schmitt trigger, slew rate. In simulation these are stored but have no
+/// electrical effect — GPIO function is handled by IO_BANK0.
+///
+public sealed class PadsPeripheral : IMemoryMappedDevice
+{
+ private const uint VOLTAGE_SELECT = 0x000; // 0=3.3V, 1=1.8V
+ private const uint GPIO_FIRST = 0x004; // GPIO0
+ // GPIO_LAST = GPIO_FIRST + 29*4 = 0x078
+
+ // Store voltage select + up to 32 GPIO pads
+ private uint _voltageSelect;
+ private readonly uint[] _gpioRegs = new uint[32];
+
+ public uint Size => 0x1000;
+
+ // Default pad value: IE=1 (input enabled), DRIVE=4mA (bits[5:4]=01), PUE=0, PDE=0
+ private const uint DEFAULT_PAD = (1u << 6); // IE bit
+
+ public PadsPeripheral()
+ {
+ for (int i = 0; i < _gpioRegs.Length; i++)
+ _gpioRegs[i] = DEFAULT_PAD;
+ }
+
+ public uint ReadWord(uint address)
+ {
+ if (address == VOLTAGE_SELECT) return _voltageSelect;
+ if (address >= GPIO_FIRST && address < GPIO_FIRST + 32 * 4)
+ {
+ var idx = (address - GPIO_FIRST) >> 2;
+ return _gpioRegs[idx];
+ }
+ return 0;
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ if (address == VOLTAGE_SELECT) { _voltageSelect = value & 1; return; }
+ if (address >= GPIO_FIRST && address < GPIO_FIRST + 32 * 4)
+ {
+ var idx = (address - GPIO_FIRST) >> 2;
+ _gpioRegs[idx] = value & 0xFF;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Pio/PioPeripheral.cs b/src/RP2040Sharp/Peripherals/Pio/PioPeripheral.cs
new file mode 100644
index 0000000..232f0cd
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Pio/PioPeripheral.cs
@@ -0,0 +1,932 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Pio;
+
+///
+/// RP2040 PIO block.
+/// PIO0 base: 0x50200000, PIO1 base: 0x50300000.
+/// 4 state machines. 32 × 16-bit instruction words.
+/// Implements ITickable; tick at system clock granularity.
+///
+public sealed class PioPeripheral : IMemoryMappedDevice, ITickable
+{
+ private const int SM_COUNT = 4;
+ private const int INSTR_COUNT = 32;
+
+ // ── Register offsets ─────────────────────────────────────────────
+ private const uint REG_CTRL = 0x000;
+ private const uint REG_FSTAT = 0x004;
+ private const uint REG_FDEBUG = 0x008;
+ private const uint REG_FLEVEL = 0x00C;
+ private const uint REG_TXF_BASE = 0x010; // TXF0-TXF3, 4 bytes each
+ private const uint REG_RXF_BASE = 0x020; // RXF0-RXF3, 4 bytes each
+ private const uint REG_IRQ = 0x030;
+ private const uint REG_IRQ_FORCE = 0x034;
+ private const uint REG_INPUT_SYNC = 0x038;
+ private const uint REG_DBG_PADOUT = 0x03C;
+ private const uint REG_DBG_PADOE = 0x040;
+ private const uint REG_DBG_CFGINFO = 0x044;
+ private const uint REG_INSTR_MEM_BASE = 0x048; // 32 entries × 4 bytes = 0x048..0x0C4
+ private const uint REG_SM_BASE = 0x0C8; // SM0 starts here, each SM = 6 regs × 4 = 0x18
+ private const uint REG_INTR = 0x128;
+ private const uint REG_IRQ0_INTE = 0x12C;
+ private const uint REG_IRQ0_INTF = 0x130;
+ private const uint REG_IRQ0_INTS = 0x134;
+ private const uint REG_IRQ1_INTE = 0x138;
+ private const uint REG_IRQ1_INTF = 0x13C;
+ private const uint REG_IRQ1_INTS = 0x140;
+
+ private const uint SM_STRIDE = 0x18; // 6 registers × 4 bytes
+ private const uint SM_OFF_CLKDIV = 0x00;
+ private const uint SM_OFF_EXECCTRL = 0x04;
+ private const uint SM_OFF_SHIFTCTRL = 0x08;
+ private const uint SM_OFF_ADDR = 0x0C;
+ private const uint SM_OFF_INSTR = 0x10;
+ private const uint SM_OFF_PINCTRL = 0x14;
+
+ // PIO instruction opcodes (bits [15:13])
+ private const int OP_JMP = 0;
+ private const int OP_WAIT = 1;
+ private const int OP_IN = 2;
+ private const int OP_OUT = 3;
+ private const int OP_PUSH_PULL = 4;
+ private const int OP_MOV = 5;
+ private const int OP_IRQ = 6;
+ private const int OP_SET = 7;
+
+ private readonly CortexM0Plus _cpu;
+ private readonly uint _blockIndex; // 0=PIO0, 1=PIO1 (for IRQ routing)
+
+ private readonly ushort[] _instrMem = new ushort[INSTR_COUNT];
+ private readonly PioStateMachine[] _sm;
+
+ private uint _irq; // 8-bit IRQ flags
+ private uint _fdebug; // TXOVER/RXUNDER/TXSTALL/RXSTALL per SM
+ private uint _irq0Inte;
+ private uint _irq0Intf;
+ private uint _irq1Inte;
+ private uint _irq1Intf;
+
+ public uint Size => 0x100000; // up to 1 MB address space per block
+
+ /// Read current physical GPIO input levels (used by WAIT GPIO, IN PINS).
+ public Func? ReadGpioIn { get; set; }
+ /// Write physical GPIO output pins: (pinValue, pinMask).
+ public Action? WriteGpioPins { get; set; }
+ /// Write physical GPIO pin directions: (dirValue, pinMask).
+ public Action? WriteGpioDirs { get; set; }
+
+ public PioPeripheral(CortexM0Plus cpu, uint blockIndex)
+ {
+ _cpu = cpu;
+ _blockIndex = blockIndex;
+ _sm = new PioStateMachine[SM_COUNT];
+ for (var i = 0; i < SM_COUNT; i++)
+ {
+ _sm[i] = new PioStateMachine();
+ _sm[i].SmIndex = i;
+ // Default wrap: top=31, bottom=0
+ _sm[i].ExecCtrl = (31u << 12);
+ }
+ }
+
+ // ── ITickable ────────────────────────────────────────────────────
+
+ public void Tick(long deltaCycles)
+ {
+ for (var s = 0; s < SM_COUNT; s++)
+ {
+ var sm = _sm[s];
+ if (!sm.Enabled) continue;
+
+ // Clock divisor: bits[31:16]=integer (0=65536), bits[15:8]=frac
+ var divInt = (int)((sm.ClkDiv >> 16) & 0xFFFF);
+ var divFrac = (int)((sm.ClkDiv >> 8) & 0xFF);
+ if (divInt == 0) divInt = 65536;
+
+ var divisor = divInt * 256 + divFrac;
+ sm.FracAccum += deltaCycles * 256;
+ var steps = sm.FracAccum / divisor;
+ sm.FracAccum %= divisor;
+
+ for (var i = 0L; i < steps; i++)
+ ExecuteStep(sm, s);
+ }
+ CheckInterrupts();
+ }
+
+ // ── IMemoryMappedDevice ──────────────────────────────────────────
+
+ public uint ReadWord(uint address)
+ {
+ // Strip the base (top 20 bits may vary between PIO0/1)
+ var off = address & 0xFFFFF;
+
+ if (off >= REG_INSTR_MEM_BASE && off < REG_INSTR_MEM_BASE + INSTR_COUNT * 4)
+ return _instrMem[(off - REG_INSTR_MEM_BASE) / 4];
+
+ if (off >= REG_SM_BASE && off < REG_SM_BASE + SM_COUNT * SM_STRIDE)
+ return ReadSmReg(off);
+
+ if (off >= REG_TXF_BASE && off < REG_TXF_BASE + SM_COUNT * 4)
+ return 0; // TXF is write-only
+
+ if (off >= REG_RXF_BASE && off < REG_RXF_BASE + SM_COUNT * 4)
+ {
+ var smIdx = (int)((off - REG_RXF_BASE) / 4);
+ var sm = _sm[smIdx];
+ if (!sm.RxFifo.TryDequeue(out var v)) return 0;
+ // Clear RXSTALL for this SM now that RX FIFO has space
+ _fdebug &= ~(1u << smIdx);
+ // Wake a SM that was stalled waiting for space in the RX FIFO (PUSH block / autopush).
+ // rp2040js: readFIFO() → checkWait().
+ if (sm.Stalled)
+ CheckSmWait(sm, smIdx);
+ return v;
+ }
+
+ return off switch
+ {
+ REG_CTRL => BuildCtrl(),
+ REG_FSTAT => BuildFstat(),
+ REG_FDEBUG => _fdebug,
+ REG_FLEVEL => BuildFlevel(),
+ REG_IRQ => _irq,
+ REG_IRQ_FORCE => 0,
+ REG_DBG_CFGINFO => (SM_COUNT << 16) | (INSTR_COUNT << 8) | 2u,
+ REG_INTR => BuildIntr(),
+ REG_IRQ0_INTE => _irq0Inte,
+ REG_IRQ0_INTF => _irq0Intf,
+ REG_IRQ0_INTS => (BuildIntr() | _irq0Intf) & _irq0Inte,
+ REG_IRQ1_INTE => _irq1Inte,
+ REG_IRQ1_INTF => _irq1Intf,
+ REG_IRQ1_INTS => (BuildIntr() | _irq1Intf) & _irq1Inte,
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ var off = address & 0xFFFFF;
+
+ if (off >= REG_INSTR_MEM_BASE && off < REG_INSTR_MEM_BASE + INSTR_COUNT * 4)
+ {
+ _instrMem[(off - REG_INSTR_MEM_BASE) / 4] = (ushort)value;
+ return;
+ }
+
+ if (off >= REG_SM_BASE && off < REG_SM_BASE + SM_COUNT * SM_STRIDE)
+ {
+ WriteSmReg(off, value);
+ return;
+ }
+
+ if (off >= REG_TXF_BASE && off < REG_TXF_BASE + SM_COUNT * 4)
+ {
+ var smIdx = (int)((off - REG_TXF_BASE) / 4);
+ var sm = _sm[smIdx];
+ if (sm.TxFifo.Count < sm.TxDepth)
+ {
+ sm.TxFifo.Enqueue(value);
+ // Clear TXSTALL for this SM now that TX FIFO has data
+ _fdebug &= ~(1u << (24 + smIdx));
+ // Wake a SM that was stalled waiting for data in the TX FIFO (PULL block / autopull).
+ // rp2040js: writeFIFO() → checkWait().
+ if (sm.Stalled)
+ CheckSmWait(sm, smIdx);
+ }
+ else
+ {
+ _fdebug |= 1u << (8 + smIdx); // TXOVER bits [11:8]
+ }
+ return;
+ }
+
+ switch (off)
+ {
+ case REG_CTRL: WriteCtrl(value); break;
+ case REG_FDEBUG: _fdebug &= ~value; break; // write 1 to clear
+ case REG_IRQ: _irq &= ~value; IrqUpdated(); break; // write 1 to clear
+ case REG_IRQ_FORCE: _irq |= value & 0xFF; IrqUpdated(); break;
+ case REG_IRQ0_INTE: _irq0Inte = value & 0xFFF; CheckInterrupts(); break;
+ case REG_IRQ0_INTF: _irq0Intf = value & 0xFFF; CheckInterrupts(); break;
+ case REG_IRQ1_INTE: _irq1Inte = value & 0xFFF; CheckInterrupts(); break;
+ case REG_IRQ1_INTF: _irq1Intf = value & 0xFFF; CheckInterrupts(); break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ // ── Public helpers ───────────────────────────────────────────────
+
+ /// Read the current output pins for state machine .
+ public uint GetPins(int smIndex) => _sm[smIndex].GpioPins;
+
+ /// Returns true if RX FIFO of has data.
+ public bool RxFifoEmpty(int smIndex) => _sm[smIndex].RxFifo.Count == 0;
+ /// DREQ source for DMA TX: true when TX FIFO has space to accept data.
+ public bool TxFifoNotFull(int smIndex) => _sm[smIndex].TxFifo.Count < _sm[smIndex].TxDepth;
+
+ /// Inject a value directly into the RX FIFO of .
+ public void InjectRxData(int smIndex, uint value)
+ {
+ var sm = _sm[smIndex];
+ if (sm.RxFifo.Count < sm.RxDepth)
+ sm.RxFifo.Enqueue(value);
+ }
+
+ // ── Private: stall wake-up ───────────────────────────────────────
+
+ ///
+ /// Re-evaluate the stall condition for a SM that was blocked on a FIFO or IRQ wait.
+ /// Mirrors rp2040js StateMachine.checkWait(): the SM may immediately unstall and
+ /// advance its PC if the blocking condition has been resolved.
+ /// Called after TXF writes (may unblock PULL-stalled SM) and RXF reads (may unblock PUSH-stalled SM).
+ ///
+ private void CheckSmWait(PioStateMachine sm, int smIdx)
+ {
+ // Try to complete a blocked PULL (SM was stalled because TX FIFO was empty).
+ if (sm.TxFifo.Count > 0)
+ {
+ sm.OSR = sm.TxFifo.Dequeue();
+ sm.OsrCount = 32;
+ sm.Stalled = false;
+ AdvanceSmPc(sm);
+ }
+ // Try to complete a blocked PUSH (SM was stalled because RX FIFO was full).
+ else if (sm.RxFifo.Count < sm.RxDepth && sm.IsrCount >= (uint)sm.AutopushThreshold)
+ {
+ sm.RxFifo.Enqueue(sm.ISR);
+ sm.ISR = 0; sm.IsrCount = 0;
+ sm.Stalled = false;
+ AdvanceSmPc(sm);
+ }
+ }
+
+ ///
+ /// Re-check IRQ flag waits across all SMs after an IRQ flag change.
+ /// Called after IRQ register writes. Mirrors rp2040js RPPIO.irqUpdated().
+ ///
+ private void IrqUpdated()
+ {
+ for (var i = 0; i < SM_COUNT; i++)
+ {
+ var sm = _sm[i];
+ if (!sm.Stalled) continue;
+ // Check if this SM is stalled waiting for an IRQ flag to clear (IRQ WAIT instruction).
+ // The exact IRQ index being waited on is not stored separately; we re-evaluate by
+ // rescanning. In practice, very few SMs stall on IRQ simultaneously.
+ // A simple approach: let the next Tick() re-evaluate, which is safe because IRQ waits
+ // re-check every cycle. For immediate wake (matching rp2040js), we would need to store
+ // the wait type and index in PioStateMachine — deferred for a future improvement.
+ }
+ CheckInterrupts();
+ }
+
+ private void AdvanceSmPc(PioStateMachine sm)
+ {
+ if (!sm.PcJumped)
+ {
+ sm.PC++;
+ if (sm.PC > sm.WrapTop)
+ sm.PC = sm.WrapBottom;
+ }
+ sm.PcJumped = false;
+ }
+
+ private uint BuildCtrl()
+ {
+ uint ctrl = 0;
+ for (var i = 0; i < SM_COUNT; i++)
+ if (_sm[i].Enabled)
+ ctrl |= 1u << i;
+ return ctrl;
+ }
+
+ private void WriteCtrl(uint value)
+ {
+ // Bits [3:0]: SM_ENABLE
+ for (var i = 0; i < SM_COUNT; i++)
+ _sm[i].Enabled = (value & (1u << i)) != 0;
+
+ // Bits [7:4]: SM_RESTART — reset PC and shift state.
+ // rp2040js restart(): inputShiftCount=0, outputShiftCount=32 (full), waiting=false.
+ // TRM §3.7: RESTART clears ISR/ISR-count and OSR-count to "full" state.
+ for (var i = 0; i < SM_COUNT; i++)
+ if ((value & (1u << (4 + i))) != 0)
+ {
+ _sm[i].PC = _sm[i].WrapBottom;
+ _sm[i].ISR = 0; _sm[i].IsrCount = 0;
+ // OSR shift-count is reset such that autopull triggers on the first OUT
+ // (RP2040 datasheet §3.5.4.2.1: "After RESTART, the shift counter is set
+ // to a value that triggers an autopull"). OsrCount=0 → shifts-so-far=32,
+ // which always satisfies any PULL_THRESH ≤ 32.
+ _sm[i].OSR = 0; _sm[i].OsrCount = 0;
+ _sm[i].Stalled = false;
+ // Clear EXEC_STALLED status in EXECCTRL (bit 31)
+ _sm[i].ExecCtrl &= 0x7FFFFFFFu;
+ }
+
+ // Bits [11:8]: CLKDIV_RESTART — reset fractional accumulator
+ for (var i = 0; i < SM_COUNT; i++)
+ if ((value & (1u << (8 + i))) != 0)
+ _sm[i].FracAccum = 0;
+ }
+
+ // ── Private: FSTAT ───────────────────────────────────────────────
+
+ private uint BuildFstat()
+ {
+ // RP2040 TRM §3.7 / rp2040js reference:
+ // [27:24] = TXEMPTY (per SM), [19:16] = TXFULL, [11:8] = RXEMPTY, [3:0] = RXFULL
+ uint result = 0;
+ for (var i = 0; i < SM_COUNT; i++)
+ {
+ var sm = _sm[i];
+ if (sm.RxFifo.Count >= sm.RxDepth) result |= 1u << i; // RX full [3:0]
+ if (sm.RxFifo.Count == 0) result |= 1u << (8 + i); // RX empty [11:8]
+ if (sm.TxFifo.Count >= sm.TxDepth) result |= 1u << (16 + i); // TX full [19:16]
+ if (sm.TxFifo.Count == 0) result |= 1u << (24 + i); // TX empty [27:24]
+ }
+ return result;
+ }
+
+ // ── Private: FLEVEL ──────────────────────────────────────────────
+
+ private uint BuildFlevel()
+ {
+ uint result = 0;
+ for (var i = 0; i < SM_COUNT; i++)
+ {
+ var txLevel = (uint)(_sm[i].TxFifo.Count & 0xF);
+ var rxLevel = (uint)(_sm[i].RxFifo.Count & 0xF);
+ result |= (txLevel | (rxLevel << 4)) << (i * 8);
+ }
+ return result;
+ }
+
+ // ── Private: INTR (dynamic) ──────────────────────────────────────
+ // Bits [3:0]=RX not empty per SM, [7:4]=TX not full per SM, [11:8]=IRQ flags 0-3
+
+ private uint BuildIntr()
+ {
+ uint intr = _irq & 0xF; // IRQ flags 0-3 in bits [11:8] form, shifted to [11:8]
+ intr <<= 8;
+ for (var i = 0; i < SM_COUNT; i++)
+ {
+ var sm = _sm[i];
+ if (sm.RxFifo.Count > 0) intr |= 1u << i; // RX not empty [3:0]
+ if (sm.TxFifo.Count < sm.TxDepth) intr |= 1u << (4 + i); // TX not full [7:4]
+ }
+ return intr;
+ }
+
+ // ── Private: interrupt routing to NVIC ──────────────────────────
+ // PIO0_IRQ0=7, PIO0_IRQ1=8, PIO1_IRQ0=9, PIO1_IRQ1=10
+
+ private void CheckInterrupts()
+ {
+ var intr = BuildIntr();
+ var irq0Active = ((intr | _irq0Intf) & _irq0Inte) != 0;
+ var irq1Active = ((intr | _irq1Intf) & _irq1Inte) != 0;
+ _cpu.SetInterrupt((int)(7 + _blockIndex * 2), irq0Active);
+ _cpu.SetInterrupt((int)(7 + _blockIndex * 2 + 1), irq1Active);
+ }
+
+ // ── Private: SM register read/write ─────────────────────────────
+
+ private uint ReadSmReg(uint off)
+ {
+ var smIdx = (int)((off - REG_SM_BASE) / SM_STRIDE);
+ var reg = (off - REG_SM_BASE) % SM_STRIDE;
+ var sm = _sm[smIdx];
+ return reg switch
+ {
+ SM_OFF_CLKDIV => sm.ClkDiv,
+ // EXECCTRL bit 31 (EXEC_STALLED) is hardware read-only status; preserve it.
+ SM_OFF_EXECCTRL => sm.ExecCtrl,
+ SM_OFF_SHIFTCTRL => sm.ShiftCtrl,
+ SM_OFF_ADDR => sm.PC,
+ SM_OFF_INSTR => _instrMem[sm.PC & 0x1F],
+ SM_OFF_PINCTRL => sm.PinCtrl,
+ _ => 0,
+ };
+ }
+
+ private void WriteSmReg(uint off, uint value)
+ {
+ var smIdx = (int)((off - REG_SM_BASE) / SM_STRIDE);
+ var reg = (off - REG_SM_BASE) % SM_STRIDE;
+ var sm = _sm[smIdx];
+ switch (reg)
+ {
+ case SM_OFF_CLKDIV: sm.ClkDiv = value; break;
+ // EXECCTRL bit 31 (EXEC_STALLED) is read-only hardware status;
+ // writes must preserve it (rp2040js: execCtrl = (value & 0x7FFFFFFF) | (execCtrl & 0x80000000)).
+ case SM_OFF_EXECCTRL: sm.ExecCtrl = (value & 0x7FFFFFFFu) | (sm.ExecCtrl & 0x80000000u); break;
+ case SM_OFF_SHIFTCTRL: sm.ShiftCtrl = value; break;
+ case SM_OFF_ADDR: break; // read-only
+ case SM_OFF_INSTR:
+ // SM_INSTR must execute the instruction immediately (rp2040js: executeInstruction(value)),
+ // not defer it. Immediate execution is required for correct PC updates visible on the next
+ // SM_ADDR read (e.g. firmware writing a JMP and then reading SM_ADDR).
+ ExecuteInstr(sm, (ushort)value, (int)((value >> 13) & 7));
+ // Reflect stall in EXECCTRL bit 31 (EXEC_STALLED) per TRM §3.7.
+ if (sm.Stalled)
+ sm.ExecCtrl |= 0x80000000u;
+ else
+ sm.ExecCtrl &= 0x7FFFFFFFu;
+ break;
+ case SM_OFF_PINCTRL: sm.PinCtrl = value; break;
+ }
+ }
+
+ // ── Private: instruction execution ──────────────────────────────
+
+ private void ExecuteStep(PioStateMachine sm, int smIdx)
+ {
+ // Burn delay cycles
+ if (sm.DelayCounter > 0)
+ {
+ sm.DelayCounter--;
+ return;
+ }
+
+ if (sm.PC >= INSTR_COUNT) sm.PC = (uint)(sm.WrapBottom & 0x1F);
+ var instr = _instrMem[sm.PC];
+
+ var opcode = (instr >> 13) & 0x7;
+
+ sm.PcJumped = false;
+
+ ExecuteInstr(sm, instr, opcode);
+
+ // Update FDEBUG stall bits (sticky — cleared by writing 1 to FDEBUG).
+ // RP2040 TRM §3.7 / rp2040js reference bit layout:
+ // [27:24] = TXSTALL (SM stalled waiting to read from empty TX FIFO / autopull)
+ // [3:0] = RXSTALL (SM stalled waiting to write to full RX FIFO / autopush)
+ if (sm.Stalled && opcode == OP_PUSH_PULL)
+ {
+ if ((instr & 0x80) != 0)
+ _fdebug |= 1u << (24 + smIdx); // TXSTALL bits [27:24]
+ else
+ _fdebug |= 1u << smIdx; // RXSTALL bits [3:0]
+ }
+
+ // Sideset, delay, and PC advance only on completing cycle (not on stall)
+ if (!sm.Stalled)
+ {
+ // Apply sideset AFTER instruction completes (hardware fires sideset on completion)
+ ApplySideset(sm, instr);
+ // PINCTRL.SIDESET_COUNT includes enable bit when SIDE_EN is set → no +1 needed
+ var sidesetCount = (int)sm.SidesetCount;
+ var delayBits = 5 - sidesetCount;
+ if (delayBits > 0)
+ {
+ var delay = (int)((instr >> 8) & ((1 << delayBits) - 1));
+ sm.DelayCounter = delay;
+ }
+
+ if (!sm.PcJumped)
+ {
+ sm.PC++;
+ if (sm.PC > sm.WrapTop)
+ sm.PC = sm.WrapBottom;
+ }
+ }
+ }
+
+ // Apply sideset bits from instruction field [12:8]
+ private void ApplySideset(PioStateMachine sm, ushort instr)
+ {
+ var sidesetCount = (int)sm.SidesetCount;
+ if (sidesetCount == 0) return;
+
+ var field = (instr >> 8) & 0x1F; // 5 bits: delay+sideset
+
+ // If sideEn bit is set in EXECCTRL, MSB of the field is the enable
+ // The enable is always field bit 4 (0x10) — MSB of the 5-bit field — regardless of
+ // sidesetCount. PINCTRL.SIDESET_COUNT is inclusive of the enable bit per the TRM.
+ var sideEn = sm.SideEn != 0;
+ int sideValue;
+ int pinCount;
+ if (sideEn)
+ {
+ if ((field & 0x10) == 0) return; // field bit 4 is the enable gate; 0 → no sideset
+ sideValue = (field >> (5 - sidesetCount)) & ((1 << (sidesetCount - 1)) - 1);
+ pinCount = sidesetCount - 1; // one bit consumed by enable
+ }
+ else
+ {
+ sideValue = (field >> (5 - sidesetCount)) & ((1 << sidesetCount) - 1);
+ pinCount = sidesetCount;
+ }
+
+ if (pinCount <= 0) return;
+
+ var sideBase = (int)sm.SidesetBase;
+ var sidePinDir = sm.SidePinDir != 0;
+ var pinMask = pinCount < 32 ? ((1u << pinCount) - 1) << sideBase : 0xFFFFFFFFu;
+
+ for (var bit = 0; bit < pinCount; bit++)
+ {
+ var pin = (sideBase + bit) & 0x1F;
+ var v = (sideValue >> bit) & 1;
+ if (sidePinDir)
+ sm.GpioPinDirs = (sm.GpioPinDirs & ~(1u << pin)) | ((uint)v << pin);
+ else
+ sm.GpioPins = (sm.GpioPins & ~(1u << pin)) | ((uint)v << pin);
+ }
+
+ // Propagate to physical GPIO (same as SET/OUT pin operations)
+ if (sidePinDir)
+ WriteGpioDirs?.Invoke(sm.GpioPinDirs, pinMask);
+ else
+ WriteGpioPins?.Invoke(sm.GpioPins, pinMask);
+ }
+
+ private void ExecuteInstr(PioStateMachine sm, ushort instr, int opcode)
+ {
+ switch (opcode)
+ {
+ case OP_JMP: ExecJmp(sm, instr); break;
+ case OP_WAIT: ExecWait(sm, instr); break;
+ case OP_IN: ExecIn(sm, instr); break;
+ case OP_OUT: ExecOut(sm, instr); break;
+ case OP_PUSH_PULL:
+ if ((instr & 0x80) == 0) ExecPush(sm, instr);
+ else ExecPull(sm, instr);
+ break;
+ case OP_MOV: ExecMov(sm, instr); break;
+ case OP_IRQ: ExecIrq(sm, instr); break;
+ case OP_SET: ExecSet(sm, instr); break;
+ }
+ }
+
+ // JMP: bits [7:5]=condition, [4:0]=target
+ private void ExecJmp(PioStateMachine sm, ushort instr)
+ {
+ var cond = (instr >> 5) & 0x7;
+ var target = (uint)(instr & 0x1F);
+ bool taken = cond switch
+ {
+ 0 => true, // always
+ 1 => sm.X == 0, // !X
+ 2 => sm.X-- != 0, // X-- (post-dec, take if was non-zero)
+ 3 => sm.Y == 0, // !Y
+ 4 => sm.Y-- != 0, // Y--
+ 5 => sm.X != sm.Y, // X!=Y
+ 6 => ((ReadGpioIn?.Invoke() ?? sm.GpioPins) >> (int)sm.JmpPin & 1) != 0, // PIN (input, not output)
+ 7 => sm.OsrCount > 0, // !OSRE: jump when OSR is not empty
+ _ => false,
+ };
+ if (taken)
+ {
+ sm.PC = target;
+ sm.PcJumped = true;
+ sm.Stalled = false;
+ return;
+ }
+ sm.Stalled = false;
+ }
+
+ // WAIT: bits [7]=polarity, [6:5]=source, [4:0]=index
+ private void ExecWait(PioStateMachine sm, ushort instr)
+ {
+ var polarity = (instr >> 7) & 1;
+ var source = (instr >> 5) & 3;
+ var index = (uint)(instr & 0x1F);
+
+ // For IRQ source: bit 4 of index is the REL flag — same computation as ExecIrq
+ var irqFlagIdx = (index & 0x10) != 0
+ ? (int)((index & 0xC) | (((index & 3) + (uint)sm.SmIndex) & 3))
+ : (int)(index & 0x7);
+
+ bool condition = source switch
+ {
+ 0 => (((ReadGpioIn?.Invoke() ?? sm.GpioPins) >> (int)index) & 1) == polarity, // GPIO (absolute)
+ 1 => (((ReadGpioIn?.Invoke() ?? sm.GpioPins) >> (int)((index + sm.InBase) & 0x1F)) & 1) == polarity, // PIN relative to IN_BASE
+ 2 => ((_irq >> irqFlagIdx) & 1) == polarity, // IRQ flag
+ _ => true,
+ };
+
+ sm.Stalled = !condition;
+ if (condition && source == 2 && polarity == 1)
+ _irq &= ~(1u << irqFlagIdx); // clear IRQ on successful WAIT IRQ
+ }
+
+ // IN: bits [7:5]=source, [4:0]=bit count (0=32)
+ private void ExecIn(PioStateMachine sm, ushort instr)
+ {
+ var source = (instr >> 5) & 0x7;
+ var bitCount = (int)(instr & 0x1F);
+ if (bitCount == 0) bitCount = 32;
+
+ uint data = source switch
+ {
+ 0 => (ReadGpioIn?.Invoke() ?? sm.GpioPins) >> (int)sm.InBase, // PINS: read from InBase
+ 1 => sm.X,
+ 2 => sm.Y,
+ 3 => 0, // NULL
+ 6 => sm.ISR,
+ 7 => sm.OSR,
+ _ => 0,
+ };
+
+ if (sm.IsrShiftRight)
+ {
+ // When bitCount=32: C# shift is mod-32 (>>32 = >>0 = no-op), so handle explicitly
+ sm.ISR = bitCount == 32 ? data : (sm.ISR >> bitCount) | (data << (32 - bitCount));
+ }
+ else
+ {
+ // When bitCount=32: (1u<<32)-1 = 0 in C# (mod-32 shift), so handle explicitly
+ sm.ISR = bitCount == 32 ? data : (sm.ISR << bitCount) | (data & ((1u << bitCount) - 1));
+ }
+ sm.IsrCount += (uint)bitCount;
+
+ if (sm.AutopushEnabled && sm.IsrCount >= (uint)sm.AutopushThreshold)
+ DoPush(sm, false);
+ }
+
+ // OUT: bits [7:5]=destination, [4:0]=bit count (0=32)
+ private void ExecOut(PioStateMachine sm, ushort instr)
+ {
+ var dest = (instr >> 5) & 0x7;
+ var bitCount = (int)(instr & 0x1F);
+ if (bitCount == 0) bitCount = 32;
+
+ // Autopull is checked at the START of each OUT cycle (RP2040 datasheet §3.5.4.4):
+ // when shifts-so-far ≥ PULL_THRESH, the OSR is considered "empty"; refill it from
+ // TX FIFO before extracting bits, or stall if the FIFO is empty. This is what makes
+ // autopull-driven OUT sequences see fresh data on every threshold-aligned step,
+ // including the very first OUT after RESTART (where OSR is initialised stale).
+ if (sm.AutopullEnabled && (32u - sm.OsrCount) >= (uint)sm.AutopullThreshold)
+ {
+ if (sm.TxFifo.Count == 0)
+ {
+ sm.Stalled = true;
+ _fdebug |= 1u << (24 + sm.SmIndex); // TXSTALL
+ return;
+ }
+ sm.OSR = sm.TxFifo.Dequeue();
+ sm.OsrCount = 32;
+ }
+
+ uint data;
+ if (sm.OsrShiftRight)
+ {
+ // When bitCount=32: (1u<<32)-1 = 0 and >>=32 is no-op in C# (mod-32 shift)
+ data = bitCount == 32 ? sm.OSR : (sm.OSR & ((1u << bitCount) - 1));
+ sm.OSR = bitCount == 32 ? 0u : (sm.OSR >> bitCount);
+ }
+ else
+ {
+ // When bitCount=32: <<32 is no-op in C# (mod-32 shift)
+ data = sm.OSR >> (32 - bitCount); // bitCount=32 → >>0 = full OSR ✓
+ sm.OSR = bitCount == 32 ? 0u : (sm.OSR << bitCount);
+ }
+ unchecked { sm.OsrCount -= (uint)bitCount; }
+
+ switch (dest)
+ {
+ case 0: { // PINS: write bitCount pins at OutBase
+ var outBase = (int)sm.OutBase;
+ var pinMask = bitCount < 32 ? ((1u << bitCount) - 1) << outBase : 0xFFFFFFFFu;
+ var pinValue = (data & (bitCount < 32 ? (1u << bitCount) - 1 : 0xFFFFFFFFu)) << outBase;
+ sm.GpioPins = (sm.GpioPins & ~pinMask) | pinValue;
+ WriteGpioPins?.Invoke(pinValue, pinMask);
+ break;
+ }
+ case 1: sm.X = data; break;
+ case 2: sm.Y = data; break;
+ case 3: break; // NULL
+ case 4: { // PINDIRS: write bitCount dirs at OutBase
+ var outBase = (int)sm.OutBase;
+ var pinMask = bitCount < 32 ? ((1u << bitCount) - 1) << outBase : 0xFFFFFFFFu;
+ var pinValue = (data & (bitCount < 32 ? (1u << bitCount) - 1 : 0xFFFFFFFFu)) << outBase;
+ sm.GpioPinDirs = (sm.GpioPinDirs & ~pinMask) | pinValue;
+ WriteGpioDirs?.Invoke(pinValue, pinMask);
+ break;
+ }
+ case 5: sm.PC = data & 0x1F; sm.PcJumped = true; sm.Stalled = false; return; // PC
+ case 6: sm.ISR = data; sm.IsrCount = (uint)bitCount; break;
+ case 7: ExecuteInstr(sm, (ushort)data, (int)((data >> 13) & 7)); return; // EXEC
+ }
+
+ // Autopull is checked at the START of the next OUT (above), so no post-OUT pull
+ // is needed here. A non-blocking post-pull would also subtly change semantics by
+ // refilling on threshold-exact extractions even when no further OUT is pending.
+ }
+
+ // PUSH: bits [6]=IfFull, [5]=Block
+ private void ExecPush(PioStateMachine sm, ushort instr)
+ {
+ var ifFull = (instr & 0x40) != 0;
+ var block = (instr & 0x20) != 0;
+
+ if (ifFull && sm.IsrCount < (uint)sm.AutopushThreshold)
+ {
+ sm.Stalled = false;
+ return;
+ }
+
+ DoPush(sm, block);
+ }
+
+ private void DoPush(PioStateMachine sm, bool block)
+ {
+ if (sm.RxFifo.Count >= sm.RxDepth)
+ {
+ sm.Stalled = block;
+ // NOBLOCK: data is discarded but ISR must still be cleared (datasheet §3.5.4.2)
+ if (!block) { sm.ISR = 0; sm.IsrCount = 0; }
+ return;
+ }
+ sm.RxFifo.Enqueue(sm.ISR);
+ sm.ISR = 0;
+ sm.IsrCount = 0;
+ sm.Stalled = false;
+ }
+
+ // PULL: bits [6]=IfEmpty, [5]=Block
+ private void ExecPull(PioStateMachine sm, ushort instr)
+ {
+ var ifEmpty = (instr & 0x40) != 0;
+ var block = (instr & 0x20) != 0;
+
+ if (ifEmpty && sm.OsrCount > 0)
+ {
+ sm.Stalled = false;
+ return;
+ }
+
+ DoPull(sm, block);
+ }
+
+ private void DoPull(PioStateMachine sm, bool block)
+ {
+ if (sm.TxFifo.Count == 0)
+ {
+ if (block) { sm.Stalled = true; return; }
+ sm.OSR = sm.X; // refill with X when non-blocking
+ }
+ else
+ {
+ sm.OSR = sm.TxFifo.Dequeue();
+ }
+ sm.OsrCount = 32;
+ sm.Stalled = false;
+ }
+
+ // MOV: bits [7:5]=dest, [4:3]=op, [2:0]=source
+ private void ExecMov(PioStateMachine sm, ushort instr)
+ {
+ var dest = (instr >> 5) & 0x7;
+ var op = (instr >> 3) & 0x3; // 0=none, 1=invert, 2=bit-reverse, 3=reserved
+ var source = instr & 0x7;
+
+ uint data = source switch
+ {
+ 0 => ReadGpioIn?.Invoke() ?? sm.GpioPins, // PINS: read physical GPIO
+ 1 => sm.X,
+ 2 => sm.Y,
+ 3 => 0,
+ 5 => ComputeStatus(sm),
+ 6 => sm.ISR,
+ 7 => sm.OSR,
+ _ => 0,
+ };
+
+ data = op switch
+ {
+ 1 => ~data,
+ 2 => BitReverse(data),
+ _ => data,
+ };
+
+ switch (dest)
+ {
+ case 0: { // PINS: write via OutBase/OutCount
+ var outBase = (int)sm.OutBase;
+ var outCount = (int)sm.OutCount;
+ var pinMask = outCount > 0 ? ((1u << outCount) - 1) << outBase : 0xFFFFFFFFu;
+ var pinValue = outCount > 0 ? (data & ((1u << outCount) - 1)) << outBase : data;
+ sm.GpioPins = (sm.GpioPins & ~pinMask) | pinValue;
+ WriteGpioPins?.Invoke(pinValue, pinMask);
+ break;
+ }
+ case 1: sm.X = data; break;
+ case 2: sm.Y = data; break;
+ case 4: ExecuteInstr(sm, (ushort)data, (int)((data >> 13) & 7)); return; // EXEC
+ case 5: sm.PC = data & 0x1F; sm.PcJumped = true; sm.Stalled = false; return; // PC
+ case 6:
+ // MOV ISR: rp2040js §setMovDestination / TRM §3.4.3 —
+ // "The ISR shift count is set to 0 (empty)."
+ sm.ISR = data; sm.IsrCount = 0; break;
+ case 7:
+ // MOV OSR: rp2040js §setMovDestination / TRM §3.4.3 —
+ // "The OSR shift count is set to 0 (full, i.e. 32 bits remain)."
+ sm.OSR = data; sm.OsrCount = 32; break;
+ }
+ sm.Stalled = false;
+ }
+
+ // IRQ: bits [6]=clear, [5]=wait, [4:0]=index (bit 4 = REL flag)
+ private void ExecIrq(PioStateMachine sm, ushort instr)
+ {
+ var smIdx = Array.IndexOf(_sm, sm);
+ var doClear = (instr & 0x40) != 0;
+ var doWait = (instr & 0x20) != 0;
+ var index = (uint)(instr & 0x1F);
+ var rel = (index & 0x10) != 0;
+ // If REL: bits[3:2] unchanged, bits[1:0] = (index + smIdx) mod 4
+ // REL flag is bit 4 of index; must NOT be included in the computed flag index
+ var flagIdx = rel ? (int)((index & 0xC) | (((index & 3) + (uint)smIdx) & 3))
+ : (int)(index & 0x7);
+
+ if (doClear)
+ {
+ _irq &= ~(1u << flagIdx);
+ }
+ else
+ {
+ _irq |= 1u << flagIdx;
+ }
+
+ if (doWait && !doClear)
+ sm.Stalled = (_irq & (1u << flagIdx)) != 0; // wait for flag to be cleared
+ else
+ sm.Stalled = false;
+ }
+
+ // SET: bits [7:5]=dest, [4:0]=data
+ private void ExecSet(PioStateMachine sm, ushort instr)
+ {
+ var dest = (instr >> 5) & 0x7;
+ var data = (uint)(instr & 0x1F);
+
+ switch (dest)
+ {
+ case 0: { // PINS: SET_COUNT pins at SET_BASE
+ var setBase = (int)sm.SetBase;
+ var setCount = (int)sm.SetCount;
+ var pinMask = setCount > 0 ? ((1u << setCount) - 1) << setBase : 0u;
+ var pinValue = setCount > 0 ? (data & ((1u << setCount) - 1)) << setBase : 0u;
+ sm.GpioPins = (sm.GpioPins & ~pinMask) | pinValue;
+ WriteGpioPins?.Invoke(pinValue, pinMask);
+ break;
+ }
+ case 1: sm.X = data; break;
+ case 2: sm.Y = data; break;
+ case 4: { // PINDIRS: SET_COUNT dirs at SET_BASE
+ var setBase = (int)sm.SetBase;
+ var setCount = (int)sm.SetCount;
+ var pinMask = setCount > 0 ? ((1u << setCount) - 1) << setBase : 0u;
+ var pinValue = setCount > 0 ? (data & ((1u << setCount) - 1)) << setBase : 0u;
+ sm.GpioPinDirs = (sm.GpioPinDirs & ~pinMask) | pinValue;
+ WriteGpioDirs?.Invoke(pinValue, pinMask);
+ break;
+ }
+ }
+ sm.Stalled = false;
+ }
+
+ private static uint BitReverse(uint v)
+ {
+ v = ((v >> 1) & 0x55555555u) | ((v & 0x55555555u) << 1);
+ v = ((v >> 2) & 0x33333333u) | ((v & 0x33333333u) << 2);
+ v = ((v >> 4) & 0x0F0F0F0Fu) | ((v & 0x0F0F0F0Fu) << 4);
+ v = ((v >> 8) & 0x00FF00FFu) | ((v & 0x00FF00FFu) << 8);
+ return (v >> 16) | (v << 16);
+ }
+
+ // STATUS source for MOV: 0xFFFFFFFF if FIFO count < STATUS_N, else 0
+ private static uint ComputeStatus(PioStateMachine sm)
+ {
+ var n = (int)sm.StatusN;
+ var count = sm.StatusSel == 0
+ ? sm.TxFifo.Count // TX FIFO level
+ : sm.RxFifo.Count; // RX FIFO level
+ return (uint)(count < n ? 0xFFFFFFFF : 0);
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Pio/PioStateMachine.cs b/src/RP2040Sharp/Peripherals/Pio/PioStateMachine.cs
new file mode 100644
index 0000000..a9ed843
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Pio/PioStateMachine.cs
@@ -0,0 +1,99 @@
+namespace RP2040.Peripherals.Pio;
+
+///
+/// Internal state for one PIO state machine.
+/// Carries shift registers, scratch X/Y, FIFO references, and execution state.
+///
+internal sealed class PioStateMachine
+{
+ private const int FIFO_DEPTH = 4;
+
+ // ── Registers ────────────────────────────────────────────────────
+ public uint PC; // Program counter (0-31)
+ public uint X; // Scratch X
+ public uint Y; // Scratch Y
+ public uint ISR; // Input shift register
+ public uint OSR; // Output shift register
+ public uint IsrCount; // How many bits shifted into ISR
+ public uint OsrCount; // How many bits remain in OSR (32 when full)
+
+ public uint ClkDiv; // CLKDIV register (8.8 integer+frac)
+ public uint ExecCtrl; // EXECCTRL
+ public uint ShiftCtrl; // SHIFTCTRL
+ public uint PinCtrl; // PINCTRL
+
+ // ── FIFOs ────────────────────────────────────────────────────────
+ internal readonly Queue TxFifo = new(FIFO_DEPTH);
+ internal readonly Queue RxFifo = new(FIFO_DEPTH);
+
+ // ── Execution state ──────────────────────────────────────────────
+ public bool Enabled;
+ public bool Stalled; // waiting for FIFO or condition
+ internal bool PcJumped; // JMP or MOV PC set a new PC directly (skip auto-increment)
+ internal long FracAccum; // for sub-cycle fractional clock divisor
+ internal int DelayCounter; // instruction delay cycles remaining
+ public int SmIndex; // index of this SM within its PIO block (0-3); set by PioPeripheral
+
+ // ── GPIO state (driven by this SM) ───────────────────────────────
+ public uint GpioPins; // current SET/OUT output value
+ public uint GpioPinDirs; // direction bits (1=output)
+
+ public void Reset()
+ {
+ PC = 0; X = 0; Y = 0; ISR = 0; OSR = 0;
+ IsrCount = 0; OsrCount = 0;
+ Stalled = false; PcJumped = false; FracAccum = 0; DelayCounter = 0;
+ TxFifo.Clear(); RxFifo.Clear();
+ }
+
+ // ── SHIFTCTRL helpers ─────────────────────────────────────────────
+ /// ISR shift direction: false=left, true=right (SHIFTCTRL bit 18).
+ public bool IsrShiftRight => (ShiftCtrl & (1u << 18)) != 0;
+ /// OSR shift direction: false=left, true=right (SHIFTCTRL bit 19).
+ public bool OsrShiftRight => (ShiftCtrl & (1u << 19)) != 0;
+ /// Autopush threshold 0=32: SHIFTCTRL bits [24:20].
+ public int AutopushThreshold => (int)((ShiftCtrl >> 20) & 0x1F) is 0 ? 32 : (int)((ShiftCtrl >> 20) & 0x1F);
+ /// Autopull threshold 0=32: SHIFTCTRL bits [29:25].
+ public int AutopullThreshold => (int)((ShiftCtrl >> 25) & 0x1F) is 0 ? 32 : (int)((ShiftCtrl >> 25) & 0x1F);
+ public bool AutopushEnabled => (ShiftCtrl & (1u << 16)) != 0;
+ public bool AutopullEnabled => (ShiftCtrl & (1u << 17)) != 0;
+ /// FJOIN_TX (bit 31): double TX FIFO to 8 entries (RX disabled).
+ public bool FifoJoinTx => (ShiftCtrl & (1u << 31)) != 0;
+ /// FJOIN_RX (bit 30): double RX FIFO to 8 entries (TX disabled).
+ public bool FifoJoinRx => (ShiftCtrl & (1u << 30)) != 0;
+ /// Effective TX FIFO depth: 8 when FJOIN_TX, 0 when FJOIN_RX, else 4.
+ public int TxDepth => FifoJoinTx ? 8 : FifoJoinRx ? 0 : 4;
+ /// Effective RX FIFO depth: 0 when FJOIN_TX, 8 when FJOIN_RX, else 4.
+ public int RxDepth => FifoJoinTx ? 0 : FifoJoinRx ? 8 : 4;
+
+ // ── EXECCTRL helpers ──────────────────────────────────────────────
+ /// Wrap top (inclusive): EXECCTRL bits [16:12].
+ public uint WrapTop => (ExecCtrl >> 12) & 0x1F;
+ /// Wrap bottom: EXECCTRL bits [11:7].
+ public uint WrapBottom => (ExecCtrl >> 7) & 0x1F;
+ public uint JmpPin => (ExecCtrl >> 24) & 0x1F;
+ /// STATUS_SEL: 0=TX FIFO, 1=RX FIFO — EXECCTRL bit 4.
+ public uint StatusSel => (ExecCtrl >> 4) & 1;
+ /// STATUS_N: FIFO level threshold — EXECCTRL bits [3:0].
+ public uint StatusN => ExecCtrl & 0xF;
+ /// Side-set enable (from program): EXECCTRL bit 30.
+ public uint SideEn => (ExecCtrl >> 30) & 1;
+ /// Side-set pin dir (1=sets PINDIRS): EXECCTRL bit 29.
+ public uint SidePinDir => (ExecCtrl >> 29) & 1;
+
+ // ── PINCTRL helpers ───────────────────────────────────────────────
+ /// Number of side-set bits: PINCTRL bits [31:29].
+ public uint SidesetCount => (PinCtrl >> 29) & 7;
+ /// Side-set base pin: PINCTRL bits [14:10].
+ public uint SidesetBase => (PinCtrl >> 10) & 0x1F;
+ /// OUT pin count: PINCTRL bits [25:20].
+ public uint OutCount => (PinCtrl >> 20) & 0x3F;
+ /// SET pin count: PINCTRL bits [28:26].
+ public uint SetCount => (PinCtrl >> 26) & 0x7;
+ /// IN base pin: PINCTRL bits [19:15].
+ public uint InBase => (PinCtrl >> 15) & 0x1F;
+ /// OUT base pin: PINCTRL bits [4:0].
+ public uint OutBase => PinCtrl & 0x1F;
+ /// SET base pin: PINCTRL bits [9:5].
+ public uint SetBase => (PinCtrl >> 5) & 0x1F;
+}
diff --git a/src/RP2040Sharp/Peripherals/Pll/PllPeripheral.cs b/src/RP2040Sharp/Peripherals/Pll/PllPeripheral.cs
new file mode 100644
index 0000000..353886e
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Pll/PllPeripheral.cs
@@ -0,0 +1,62 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Pll;
+
+///
+/// PLL peripheral stub (PLL_SYS at 0x40028000, PLL_USB at 0x4002C000).
+/// Reports CS.LOCK=1 (bit 31) always so firmware PLL lock-wait loops complete.
+///
+public sealed class PllPeripheral : IMemoryMappedDevice
+{
+ private const uint CS = 0x00; // bit31=LOCK, bit0=BYPASS
+ private const uint PWR = 0x04; // power control
+ private const uint FBDIV_INT = 0x08; // feedback divisor (integer)
+ private const uint PRIM = 0x0C; // post dividers
+
+ private const uint CS_LOCK = 1u << 31;
+
+ private uint _pwr = 0x2D; // default: VCOPD=0, POSTDIVPD=0, DSMPD=1, PD=1 (powered)
+ private uint _fbdiv = 100;
+ private uint _prim = 0x00062000; // POSTDIV1=6, POSTDIV2=2 — gives 125 MHz from 12 MHz XOSC
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address) => address switch
+ {
+ CS => CS_LOCK, // always locked in simulation
+ PWR => _pwr,
+ FBDIV_INT => _fbdiv,
+ PRIM => _prim,
+ _ => 0,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case PWR: _pwr = value; break;
+ case FBDIV_INT: _fbdiv = value & 0xFFF; break;
+ case PRIM: _prim = value; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Ppb/PpbPeripheral.cs b/src/RP2040Sharp/Peripherals/Ppb/PpbPeripheral.cs
new file mode 100644
index 0000000..f624bf7
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Ppb/PpbPeripheral.cs
@@ -0,0 +1,252 @@
+using System.Numerics;
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Ppb;
+
+///
+/// Private Peripheral Bus (PPB) — NVIC, SysTick, and System Control Block (SCB).
+/// Base address: 0xE000E000. Register with BusInterconnect via MapDevice(0xE, ppb).
+/// Addresses received from the bus are already masked (address & 0x0FFFFFFF),
+/// so 0xE000Exyz arrives as 0x0000Exyz; local offset = address & 0xFFF.
+///
+public sealed class PpbPeripheral : IMemoryMappedDevice, ITickable
+{
+ ///
+ /// Fired when NVIC_ISER enables new IRQ bits. Subscribers should re-check
+ /// their interrupt state (level-triggered IRQs may have been cleared by ICPR).
+ ///
+ public Action? OnInterruptEnable;
+ // ── SysTick offsets ──────────────────────────────────────────────
+ private const uint SYST_CSR = 0x010; // Control / Status
+ private const uint SYST_RVR = 0x014; // Reload Value
+ private const uint SYST_CVR = 0x018; // Current Value (write clears)
+ private const uint SYST_CALIB = 0x01C; // Calibration (RO, no data)
+
+ // ── NVIC offsets ─────────────────────────────────────────────────
+ private const uint NVIC_ISER = 0x100; // Set-Enable
+ private const uint NVIC_ICER = 0x180; // Clear-Enable
+ private const uint NVIC_ISPR = 0x200; // Set-Pending
+ private const uint NVIC_ICPR = 0x280; // Clear-Pending
+ private const uint NVIC_IPR0 = 0x400; // Priority R0 (IPR0-IPR7)
+ private const uint NVIC_IPR7 = 0x41C; // Priority R7
+
+ // ── SCB offsets ──────────────────────────────────────────────────
+ private const uint SCB_CPUID = 0xD00; // Processor ID (RO)
+ private const uint SCB_ICSR = 0xD04; // Interrupt Control / State
+ private const uint SCB_VTOR = 0xD08; // Vector Table Offset
+ private const uint SCB_AIRCR = 0xD0C; // Application Interrupt / Reset Control
+ private const uint SCB_SHPR2 = 0xD1C; // System Handler Priority 2 (SVC bits 31:24)
+ private const uint SCB_SHPR3 = 0xD20; // System Handler Priority 3 (PendSV[23:16] / SysTick[31:24])
+
+ private readonly CortexM0Plus _cpu;
+
+ // SysTick state
+ private uint _systCsr;
+ private uint _systRvr;
+ private long _systCvr; // kept as long to handle large delta gracefully
+
+ // NVIC priority registers — 8 × uint → 32 IRQs, 2 priority bits each (bits 7:6)
+ private readonly uint[] _nvicIpr = new uint[8];
+
+ public uint Size => 0x1000;
+
+ public PpbPeripheral(CortexM0Plus cpu)
+ {
+ _cpu = cpu;
+ }
+
+ // ── ITickable ────────────────────────────────────────────────────
+
+ /// Advance SysTick by cycles.
+ public void Tick(long deltaCycles)
+ {
+ if ((_systCsr & 1) == 0) return; // SysTick not enabled
+
+ _systCvr -= deltaCycles;
+
+ // Handle one or more rollovers (usually 0 or 1 per Tick call)
+ while (_systCvr <= 0)
+ {
+ _systCsr |= 1u << 16; // COUNTFLAG
+
+ long reload = _systRvr > 0 ? (long)_systRvr : 0xFFFFFF;
+ _systCvr += reload;
+
+ if ((_systCsr & 2) != 0) // TICKINT
+ _cpu.TriggerSysTick();
+ }
+ }
+
+ // ── IMemoryMappedDevice — reads ──────────────────────────────────
+
+ public uint ReadWord(uint address)
+ {
+ var offset = address & 0xFFF;
+
+ if (offset >= NVIC_IPR0 && offset <= NVIC_IPR7)
+ return _nvicIpr[(offset - NVIC_IPR0) >> 2];
+
+ return offset switch
+ {
+ SYST_CSR => _systCsr,
+ SYST_RVR => _systRvr,
+ SYST_CVR => (uint)(_systCvr & 0xFFFFFF),
+ SYST_CALIB => 0,
+ NVIC_ISER => _cpu.Registers.EnabledInterrupts,
+ NVIC_ICER => _cpu.Registers.EnabledInterrupts,
+ NVIC_ISPR => _cpu.Registers.PendingInterrupts,
+ NVIC_ICPR => _cpu.Registers.PendingInterrupts,
+ SCB_CPUID => 0x410CC601, // Cortex-M0+, r0p1
+ SCB_ICSR => BuildIcsr(),
+ SCB_VTOR => _cpu.Registers.VTOR,
+ SCB_AIRCR => 0xFA050000, // VECTKEY read value, no reset pending
+ SCB_SHPR2 => _cpu.Registers.SHPR2,
+ SCB_SHPR3 => _cpu.Registers.SHPR3,
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ // ── IMemoryMappedDevice — writes ─────────────────────────────────
+
+ public void WriteWord(uint address, uint value)
+ {
+ var offset = address & 0xFFF;
+
+ if (offset >= NVIC_IPR0 && offset <= NVIC_IPR7)
+ {
+ var idx = (int)((offset - NVIC_IPR0) >> 2);
+ _nvicIpr[idx] = value & 0xC0C0C0C0; // only top 2 bits per byte
+ UpdatePriorityBucket(idx, _nvicIpr[idx]);
+ return;
+ }
+
+ switch (offset)
+ {
+ case SYST_CSR:
+ _systCsr = value & 0x7; // ENABLE | TICKINT | CLKSOURCE
+ break;
+
+ case SYST_RVR:
+ _systRvr = value & 0x00FFFFFF;
+ break;
+
+ case SYST_CVR:
+ _systCvr = 0;
+ _systCsr &= ~(1u << 16); // clear COUNTFLAG
+ break;
+
+ case NVIC_ISER:
+ _cpu.Registers.EnabledInterrupts |= value;
+ _cpu.Registers.InterruptsUpdated = true;
+ OnInterruptEnable?.Invoke();
+ break;
+
+ case NVIC_ICER:
+ _cpu.Registers.EnabledInterrupts &= ~value;
+ break;
+
+ case NVIC_ISPR:
+ SetPendingBits(value & 0x3FFFFFF);
+ break;
+
+ case NVIC_ICPR:
+ _cpu.Registers.PendingInterrupts &= ~value;
+ break;
+
+ case SCB_ICSR:
+ if ((value & (1u << 31)) != 0) _cpu.TriggerNmi();
+ if ((value & (1u << 28)) != 0) _cpu.TriggerPendSv();
+ if ((value & (1u << 27)) != 0)
+ {
+ _cpu.Registers.PendingPendSV = false;
+ _cpu.Registers.InterruptsUpdated = true;
+ }
+ if ((value & (1u << 26)) != 0) _cpu.TriggerSysTick();
+ if ((value & (1u << 25)) != 0)
+ {
+ _cpu.Registers.PendingSystick = false;
+ _cpu.Registers.InterruptsUpdated = true;
+ }
+ break;
+
+ case SCB_VTOR:
+ _cpu.Registers.VTOR = value & 0xFFFFFF00;
+ break;
+
+ case SCB_AIRCR:
+ // SYSRESETREQ (bit2) could trigger board-level reset; ignored here
+ break;
+
+ case SCB_SHPR2:
+ _cpu.Registers.SHPR2 = value;
+ break;
+
+ case SCB_SHPR3:
+ _cpu.Registers.SHPR3 = value;
+ break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ var current = ReadWord(aligned);
+ WriteWord(aligned, (current & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ var current = ReadWord(aligned);
+ WriteWord(aligned, (current & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ // ── Private helpers ──────────────────────────────────────────────
+
+ private uint BuildIcsr()
+ {
+ ref readonly var regs = ref _cpu.Registers;
+ var icsr = regs.IPSR & 0x3Fu;
+ if (regs.PendingNMI) icsr |= 1u << 31;
+ if (regs.PendingPendSV) icsr |= 1u << 28;
+ if (regs.PendingSystick) icsr |= 1u << 26;
+ return icsr;
+ }
+
+ private void SetPendingBits(uint mask)
+ {
+ while (mask != 0)
+ {
+ var irq = BitOperations.TrailingZeroCount(mask);
+ _cpu.SetInterrupt(irq, true);
+ mask &= mask - 1; // clear lowest set bit
+ }
+ }
+
+ private void UpdatePriorityBucket(int iprIdx, uint iprValue)
+ {
+ // Each InterruptPrioritiesN field holds 8 priority bytes (2 IPR registers).
+ // iprIdx 0-1 → InterruptPriorities0, 2-3 → InterruptPriorities1, etc.
+ var inBucket = (iprIdx & 1) << 4; // 0 or 16 bit shift within the 32-bit bucket
+ var mask = 0xFFFFu << inBucket;
+ var twoBytes = (iprValue & 0xC0C0u) << inBucket;
+
+ if (iprIdx < 2)
+ _cpu.Registers.InterruptPriorities0 = (_cpu.Registers.InterruptPriorities0 & ~(uint)mask) | (uint)twoBytes;
+ else if (iprIdx < 4)
+ _cpu.Registers.InterruptPriorities1 = (_cpu.Registers.InterruptPriorities1 & ~(uint)mask) | (uint)twoBytes;
+ else if (iprIdx < 6)
+ _cpu.Registers.InterruptPriorities2 = (_cpu.Registers.InterruptPriorities2 & ~(uint)mask) | (uint)twoBytes;
+ else
+ _cpu.Registers.InterruptPriorities3 = (_cpu.Registers.InterruptPriorities3 & ~(uint)mask) | (uint)twoBytes;
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Psm/PsmPeripheral.cs b/src/RP2040Sharp/Peripherals/Psm/PsmPeripheral.cs
new file mode 100644
index 0000000..9d48d26
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Psm/PsmPeripheral.cs
@@ -0,0 +1,63 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Psm;
+
+///
+/// Power-on State Machine peripheral (0x40010000).
+/// In simulation all subsystems are always ready (DONE = all bits set).
+///
+public sealed class PsmPeripheral : IMemoryMappedDevice
+{
+ private const uint FRCE_ON = 0x00;
+ private const uint FRCE_OFF = 0x04;
+ private const uint WDSEL = 0x08;
+ private const uint DONE = 0x0C;
+
+ // All 17 subsystem bits (proc0..spi1)
+ private const uint ALL_BITS = 0x0001FFFF;
+
+ private uint _frceOn;
+ private uint _frceOff;
+ private uint _wdsel;
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address) => address switch
+ {
+ FRCE_ON => _frceOn,
+ FRCE_OFF => _frceOff,
+ WDSEL => _wdsel,
+ DONE => ALL_BITS, // all subsystems always done in simulation
+ _ => 0,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case FRCE_ON: _frceOn = value & ALL_BITS; break;
+ case FRCE_OFF: _frceOff = value & ALL_BITS; break;
+ case WDSEL: _wdsel = value & ALL_BITS; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Pwm/PwmPeripheral.cs b/src/RP2040Sharp/Peripherals/Pwm/PwmPeripheral.cs
new file mode 100644
index 0000000..f03c8d1
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Pwm/PwmPeripheral.cs
@@ -0,0 +1,246 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Pwm;
+
+///
+/// RP2040 PWM peripheral (base 0x40050000).
+/// 8 slices (A/B channels each). Supports:
+/// - Free-running mode (default): counter wraps at TOP
+/// - Level-sensitive: counter resets when input goes low
+/// Provides ITickable to advance the counter.
+///
+public sealed class PwmPeripheral : IMemoryMappedDevice, ITickable
+{
+ private const int SLICE_COUNT = 8;
+
+ // Per-slice offsets within each 0x14-byte block
+ private const uint OFF_CSR = 0x00; // Control / Status
+ private const uint OFF_DIV = 0x04; // Clock divisor (8.4 fixed-point)
+ private const uint OFF_CTR = 0x08; // Counter value
+ private const uint OFF_CC = 0x0C; // Compare values (B[31:16], A[15:0])
+ private const uint OFF_TOP = 0x10; // Wrap value
+
+ private const uint SLICE_BYTES = 0x14;
+
+ // System registers
+ private const uint REG_EN = 0xA0; // enable bitfield (bit N = enable slice N)
+ private const uint REG_INTR = 0xA4; // raw interrupt (write 1 to clear)
+ private const uint REG_INTE = 0xA8; // interrupt enable
+ private const uint REG_INTF = 0xAC; // interrupt force
+ private const uint REG_INTS = 0xB0; // interrupt status
+
+ private readonly CortexM0Plus _cpu;
+
+ private readonly uint[] _csr = new uint[SLICE_COUNT];
+ private readonly uint[] _div = new uint[SLICE_COUNT];
+ private readonly uint[] _ctr = new uint[SLICE_COUNT];
+ private readonly uint[] _cc = new uint[SLICE_COUNT];
+ private readonly uint[] _top = new uint[SLICE_COUNT];
+
+ private long[] _fracAccum = new long[SLICE_COUNT];
+ private bool[] _phaseDir = new bool[SLICE_COUNT]; // true=counting up (phase-correct)
+
+ private uint _enable; // slice enable bitfield (mirrors CSR.EN per slice)
+ private uint _intr;
+ private uint _inte;
+ private uint _intf;
+
+ // CSR bit definitions
+ private const uint CSR_EN = 1u << 0;
+ private const uint CSR_PH_CORRECT = 1u << 1;
+ private const uint CSR_A_INV = 1u << 2;
+ private const uint CSR_B_INV = 1u << 3;
+ private const uint CSR_DIVMODE = 3u << 4; // bits [5:4]
+ private const uint CSR_PH_RET = 1u << 6; // strobe
+ private const uint CSR_PH_ADV = 1u << 7; // strobe
+ private const uint CSR_PH_STALLED = 1u << 8; // read-only
+
+ public uint Size => 0x1000;
+
+ public PwmPeripheral(CortexM0Plus cpu)
+ {
+ _cpu = cpu;
+ for (var i = 0; i < SLICE_COUNT; i++)
+ {
+ _top[i] = 0xFFFF; // default wrap at 0xFFFF
+ _div[i] = 0x10; // reset: integer=1, frac=0
+ }
+ }
+
+ // ── ITickable ────────────────────────────────────────────────────
+
+ public void Tick(long deltaCycles)
+ {
+ for (var s = 0; s < SLICE_COUNT; s++)
+ {
+ if ((_csr[s] & CSR_EN) == 0) continue; // slice not enabled
+
+ // DIV = integer (bits 11:4) + fraction (bits 3:0) in 8.4 format
+ var divInt = (int)((_div[s] >> 4) & 0xFF);
+ var divFrac = (int)(_div[s] & 0xF);
+ if (divInt == 0) divInt = 1;
+
+ // Fixed-point divisor in 1/16 units
+ var divisor = divInt * 16 + divFrac;
+
+ _fracAccum[s] += deltaCycles * 16;
+ var steps = _fracAccum[s] / divisor;
+ _fracAccum[s] %= divisor;
+
+ var phCorrect = (_csr[s] & CSR_PH_CORRECT) != 0;
+
+ for (var i = 0L; i < steps; i++)
+ {
+ if (phCorrect)
+ {
+ // Phase-correct: count up to TOP then back down to 0
+ if (_phaseDir[s])
+ {
+ _ctr[s]++;
+ if (_ctr[s] >= _top[s])
+ {
+ _ctr[s] = _top[s];
+ _phaseDir[s] = false;
+ }
+ }
+ else
+ {
+ if (_ctr[s] == 0)
+ {
+ _phaseDir[s] = true;
+ _intr |= 1u << s;
+ if ((_inte & (1u << s)) != 0)
+ _cpu.SetInterrupt(4, true); // PWM_IRQ_WRAP is single shared IRQ
+ }
+ else _ctr[s]--;
+ }
+ }
+ else
+ {
+ _ctr[s]++;
+ if (_ctr[s] > _top[s])
+ {
+ _ctr[s] = 0;
+ _intr |= 1u << s;
+ if ((_inte & (1u << s)) != 0)
+ _cpu.SetInterrupt(4, true); // PWM_IRQ_WRAP is single shared IRQ
+ }
+ }
+ }
+ }
+ }
+
+ // ── IMemoryMappedDevice ──────────────────────────────────────────
+
+ public uint ReadWord(uint address)
+ {
+ if (address < SLICE_COUNT * SLICE_BYTES)
+ {
+ var s = (int)(address / SLICE_BYTES);
+ return (address % SLICE_BYTES) switch
+ {
+ OFF_CSR => _csr[s],
+ OFF_DIV => _div[s],
+ OFF_CTR => _ctr[s],
+ OFF_CC => _cc[s],
+ OFF_TOP => _top[s],
+ _ => 0,
+ };
+ }
+
+ return address switch
+ {
+ REG_EN => _enable,
+ REG_INTR => _intr,
+ REG_INTE => _inte,
+ REG_INTF => _intf,
+ REG_INTS => (_intr | _intf) & _inte,
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ if (address < SLICE_COUNT * SLICE_BYTES)
+ {
+ var s = (int)(address / SLICE_BYTES);
+ switch (address % SLICE_BYTES)
+ {
+ case OFF_CSR:
+ // PH_ADV / PH_RET are strobe bits — apply immediately, don't store
+ if ((value & CSR_PH_ADV) != 0 && _ctr[s] < _top[s]) _ctr[s]++;
+ if ((value & CSR_PH_RET) != 0 && _ctr[s] > 0) _ctr[s]--;
+ _csr[s] = value & ~(CSR_PH_ADV | CSR_PH_RET | CSR_PH_STALLED);
+ if ((value & CSR_EN) != 0)
+ {
+ _enable |= 1u << s;
+ // When first enabling in phase-correct mode with counter at 0,
+ // start counting UP to prevent a spurious wrap-interrupt at t=0.
+ if ((value & CSR_PH_CORRECT) != 0 && _ctr[s] == 0)
+ _phaseDir[s] = true;
+ }
+ else _enable &= ~(1u << s);
+ break;
+ case OFF_DIV: _div[s] = value & 0xFFF; break;
+ case OFF_CTR: _ctr[s] = value & 0xFFFF; break;
+ case OFF_CC: _cc[s] = value; break;
+ case OFF_TOP: _top[s] = value & 0xFFFF; break;
+ }
+ return;
+ }
+
+ switch (address)
+ {
+ case REG_EN:
+ _enable = value & 0xFF;
+ // Mirror enable bits into per-slice CSR so Tick() sees the change
+ for (var i = 0; i < SLICE_COUNT; i++)
+ {
+ if ((_enable & (1u << i)) != 0) _csr[i] |= CSR_EN;
+ else _csr[i] &= ~CSR_EN;
+ }
+ break;
+ case REG_INTR: _intr &= ~value; break; // write 1 to clear
+ case REG_INTE: _inte = value & 0xFF; break;
+ case REG_INTF: _intf = value & 0xFF; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ /// Read Channel A duty cycle (0-65535). Applies A_INV if set.
+ public ushort GetDutyA(int slice)
+ {
+ var raw = (ushort)(_cc[slice] & 0xFFFF);
+ return (_csr[slice] & CSR_A_INV) != 0 ? (ushort)(~raw) : raw;
+ }
+
+ /// Read Channel B duty cycle (0-65535). Applies B_INV if set.
+ public ushort GetDutyB(int slice)
+ {
+ var raw = (ushort)(_cc[slice] >> 16);
+ return (_csr[slice] & CSR_B_INV) != 0 ? (ushort)(~raw) : raw;
+ }
+
+ /// True if slice counter is currently counting up (phase-correct mode).
+ public bool IsCountingUp(int slice) => _phaseDir[slice];
+}
diff --git a/src/RP2040Sharp/Peripherals/RP2040Machine.cs b/src/RP2040Sharp/Peripherals/RP2040Machine.cs
new file mode 100644
index 0000000..0dfa055
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/RP2040Machine.cs
@@ -0,0 +1,908 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+using RP2040.Peripherals.Adc;
+using RP2040.Peripherals.Ahb;
+using RP2040.Peripherals.Apb;
+using RP2040.Peripherals.Busctrl;
+using RP2040.Peripherals.Clocks;
+using RP2040.Peripherals.Dma;
+using RP2040.Peripherals.Gpio;
+using RP2040.Peripherals.I2c;
+using RP2040.Peripherals.IoQspi;
+using RP2040.Peripherals.Pads;
+using RP2040.Peripherals.Pio;
+using RP2040.Peripherals.Pll;
+using RP2040.Peripherals.Ppb;
+using RP2040.Peripherals.Psm;
+using RP2040.Peripherals.Pwm;
+using RP2040.Peripherals.Resets;
+using RP2040.Peripherals.Rosc;
+using RP2040.Peripherals.Rtc;
+using RP2040.Peripherals.Sio;
+using RP2040.Peripherals.Spi;
+using RP2040.Peripherals.Ssi;
+using RP2040.Peripherals.SysCfg;
+using RP2040.Peripherals.SysInfo;
+using RP2040.Peripherals.Tbman;
+using RP2040.Peripherals.Timer;
+using RP2040.Peripherals.Uart;
+using RP2040.Peripherals.Usb;
+using RP2040.Peripherals.Vreg;
+using RP2040.Peripherals.Watchdog;
+using RP2040.Peripherals.Xosc;
+
+namespace RP2040.Peripherals;
+
+///
+/// Root class that wires all RP2040 peripherals together.
+/// Typical usage:
+///
+/// var machine = new RP2040Machine();
+/// machine.LoadFlash(bytes);
+/// machine.Run(1_000_000);
+///
+///
+public sealed class RP2040Machine : IDisposable
+{
+ public const uint CLK_HZ = 125_000_000;
+
+ // ── Core ────────────────────────────────────────────────────────────
+ public BusInterconnect Bus { get; }
+ /// Core 0 (the primary CPU).
+ public CortexM0Plus Cpu { get; }
+ /// Core 1 (launched by multicore handshake via SIO FIFO).
+ public CortexM0Plus Cpu1 { get; }
+
+ // ── System peripherals ──────────────────────────────────────────────
+ /// Private Peripheral Bus for Core 0.
+ public PpbPeripheral Ppb { get; }
+ /// Private Peripheral Bus for Core 1.
+ public PpbPeripheral Ppb1 { get; }
+ public SioPeripheral Sio { get; }
+ public SysInfoPeripheral SysInfo { get; }
+ public SysCfgPeripheral SysCfg { get; }
+ public PsmPeripheral Psm { get; }
+ public ResetsPeripheral Resets { get; }
+ public ClocksPeripheral Clocks { get; }
+ public XoscPeripheral Xosc { get; }
+ public WatchdogPeripheral Watchdog { get; }
+ public BusctrlPeripheral Busctrl { get; }
+ public TbmanPeripheral Tbman { get; }
+ public PllPeripheral PllSys { get; }
+ public PllPeripheral PllUsb { get; }
+ public RoscPeripheral Rosc { get; }
+ public VregPeripheral Vreg { get; }
+ public SsiPeripheral Ssi { get; }
+ public IoQspiPeripheral IoQspi { get; }
+
+ // ── I/O peripherals ─────────────────────────────────────────────────
+ public IoBank0Peripheral IoBank0 { get; }
+ public PadsPeripheral PadsBank0 { get; }
+ public PadsPeripheral PadsQspi { get; }
+ public TimerPeripheral Timer { get; }
+ public UartPeripheral Uart0 { get; }
+ public UartPeripheral Uart1 { get; }
+ public SpiPeripheral Spi0 { get; }
+ public SpiPeripheral Spi1 { get; }
+ public I2cPeripheral I2c0 { get; }
+ public I2cPeripheral I2c1 { get; }
+ public AdcPeripheral Adc { get; }
+ public PwmPeripheral Pwm { get; }
+ public RtcPeripheral Rtc { get; }
+ public DmaPeripheral Dma { get; }
+ public PioPeripheral Pio0 { get; }
+ public PioPeripheral Pio1 { get; }
+ public UsbPeripheral Usb { get; }
+ public IReadOnlyList Gpio { get; }
+
+ private readonly ITickable[] _tickables;
+ private bool _core1Launched;
+ private int _activeCoreId; // 0 = Core0, 1 = Core1 (set before each Run slice)
+
+ public RP2040Machine(uint flashSize = 2 * 1024 * 1024)
+ {
+ Bus = new BusInterconnect(flashSize);
+ Cpu = new CortexM0Plus(Bus) { CoreId = 0 };
+ Cpu1 = new CortexM0Plus(Bus) { CoreId = 1 };
+
+ // ── PPB (0xE) ────────────────────────────────────────────────────
+ Ppb = new PpbPeripheral(Cpu);
+ Ppb1 = new PpbPeripheral(Cpu1);
+ // Route PPB accesses to the correct per-core PPB based on the active core.
+ var ppbRouter = new PerCorePpbRouter(Ppb, Ppb1, () => _activeCoreId);
+ Bus.MapDevice(0xE, ppbRouter);
+
+ // ── SIO (0xD) ────────────────────────────────────────────────────
+ Sio = new SioPeripheral(Cpu);
+ Sio.GetActiveCoreId = () => _activeCoreId;
+ Sio.SetCpu1(Cpu1);
+ Sio.OnLaunchCore1 = LaunchCore1;
+ Bus.MapDevice(0xD, Sio);
+
+ // ── APB bridge (0x4) ─────────────────────────────────────────────
+ var apb = new ApbBridge();
+ Bus.MapDevice(4, apb);
+
+ // System info / config (slots 0–1)
+ SysInfo = new SysInfoPeripheral();
+ apb.Register(0x40000000, SysInfo);
+
+ SysCfg = new SysCfgPeripheral();
+ apb.Register(0x40004000, SysCfg);
+
+ // Clocks @ 0x40008000 (slot 2)
+ Clocks = new ClocksPeripheral();
+ apb.Register(0x40008000, Clocks);
+
+ // RESETS @ 0x4000C000 (slot 3)
+ Resets = new ResetsPeripheral();
+ apb.Register(0x4000C000, Resets);
+
+ // PSM @ 0x40010000 (slot 4)
+ Psm = new PsmPeripheral();
+ apb.Register(0x40010000, Psm);
+
+ // IO_BANK0 @ 0x40014000 (slot 5)
+ IoBank0 = new IoBank0Peripheral(Sio, Cpu);
+ apb.Register(0x40014000, IoBank0);
+
+ // PADS_BANK0 @ 0x4001C000 (slot 7), PADS_QSPI @ 0x40020000 (slot 8)
+ PadsBank0 = new PadsPeripheral();
+ apb.Register(0x4001C000, PadsBank0);
+
+ PadsQspi = new PadsPeripheral();
+ apb.Register(0x40020000, PadsQspi);
+
+ // XOSC @ 0x40024000 (slot 9)
+ Xosc = new XoscPeripheral();
+ apb.Register(0x40024000, Xosc);
+
+ // PLL_SYS @ 0x40028000 (slot 10), PLL_USB @ 0x4002C000 (slot 11)
+ PllSys = new PllPeripheral();
+ apb.Register(0x40028000, PllSys);
+
+ PllUsb = new PllPeripheral();
+ apb.Register(0x4002C000, PllUsb);
+
+ // IO_QSPI @ 0x40018000 (slot 6)
+ IoQspi = new IoQspiPeripheral();
+ apb.Register(0x40018000, IoQspi);
+
+ // BUSCTRL @ 0x40030000 (slot 12)
+ Busctrl = new BusctrlPeripheral();
+ apb.Register(0x40030000, Busctrl);
+
+ // UART0 @ 0x40034000 (slot 13), UART1 @ 0x40038000 (slot 14)
+ Uart0 = new UartPeripheral(Cpu, irq: 20);
+ Uart1 = new UartPeripheral(Cpu, irq: 21);
+ apb.Register(0x40034000, Uart0);
+ apb.Register(0x40038000, Uart1);
+
+ // SPI0 @ 0x4003C000 (slot 15), SPI1 @ 0x40040000 (slot 16)
+ Spi0 = new SpiPeripheral(Cpu, irq: 18);
+ Spi1 = new SpiPeripheral(Cpu, irq: 19);
+ apb.Register(0x4003C000, Spi0);
+ apb.Register(0x40040000, Spi1);
+
+ // I2C0 @ 0x40044000 (slot 17), I2C1 @ 0x40048000 (slot 18)
+ I2c0 = new I2cPeripheral(Cpu, irq: 23);
+ I2c1 = new I2cPeripheral(Cpu, irq: 24);
+ apb.Register(0x40044000, I2c0);
+ apb.Register(0x40048000, I2c1);
+
+ // ADC @ 0x4004C000 (slot 19)
+ Adc = new AdcPeripheral(Cpu);
+ apb.Register(0x4004C000, Adc);
+
+ // PWM @ 0x40050000 (slot 20)
+ Pwm = new PwmPeripheral(Cpu);
+ apb.Register(0x40050000, Pwm);
+
+ // Timer @ 0x40054000 (slot 21)
+ Timer = new TimerPeripheral(Cpu, CLK_HZ);
+ apb.Register(0x40054000, Timer);
+
+ // Watchdog @ 0x40058000 (slot 22)
+ Watchdog = new WatchdogPeripheral();
+ apb.Register(0x40058000, Watchdog);
+
+ // RTC @ 0x4005C000 (slot 23)
+ Rtc = new RtcPeripheral(Cpu);
+ apb.Register(0x4005C000, Rtc);
+
+ // TBMAN @ 0x4006C000 (slot 27)
+ Tbman = new TbmanPeripheral();
+ apb.Register(0x4006C000, Tbman);
+
+ // ROSC @ 0x40060000 (slot 24), VREG @ 0x40064000 (slot 25)
+ Rosc = new RoscPeripheral();
+ apb.Register(0x40060000, Rosc);
+
+ Vreg = new VregPeripheral();
+ apb.Register(0x40064000, Vreg);
+
+ // SSI at 0x18000000 is within XIP Flash region — registered as sub-device so
+ // all accesses to [0x18000000, 0x18FFFFFF] route to SSI registers while
+ // [0x10000000, 0x17FFFFFF] continues to use the flash pointer fast path.
+ Ssi = new SsiPeripheral();
+ Bus.RegisterSsi(Ssi);
+
+ // Wire the SSI flash command engine to the flash memory and to the IO_QSPI
+ // SS pin so CS assert/deassert signals from flash_cs_force() reach the SSI.
+ unsafe { Ssi.AttachFlash(Bus.PtrFlash, Bus.FlashSize); }
+ IoQspi.AttachSsi(Ssi);
+
+ // ── AHB bridge (0x5): DMA + PIO ──────────────────────────────────
+ var ahb = new AhbBridge();
+ Bus.MapDevice(5, ahb);
+
+ // DMA @ 0x50000000 (slot 0)
+ Dma = new DmaPeripheral(Bus, Cpu);
+ ahb.Register(0x50000000, Dma);
+
+ // USB @ 0x50100000 (slot 1, covers DPRAM + REGS at 0x50110000)
+ Usb = new UsbPeripheral(Cpu);
+ ahb.Register(0x50100000, Usb);
+
+ // Wire PPB's OnInterruptEnable to USB.RecheckInterrupts so that when
+ // pico-sdk's irq_set_enabled does ICPR then ISER, the USB level-triggered
+ // IRQ is re-asserted correctly (see: NVIC_ICPR clears pending bit, but
+ // hardware IRQ line stays asserted — we simulate this via RecheckInterrupts).
+ Ppb.OnInterruptEnable += Usb.RecheckInterrupts;
+
+ // When firmware resets the USBCTRL block (rp2040_usb_init → reset_block/unreset_block),
+ // reset the USB peripheral emulator state so the next CONTROLLER_EN write re-triggers
+ // enumeration (OnUsbEnabled). Bit 24 = USBCTRL in RESETS.RESET.
+ const uint USBCTRL_BIT = 1u << 24;
+ Resets.OnUnreset += released => { if ((released & USBCTRL_BIT) != 0) Usb.Reset(); };
+
+ // PIO0 @ 0x50200000 (slot 2), PIO1 @ 0x50300000 (slot 3)
+ Pio0 = new PioPeripheral(Cpu, 0);
+ Pio1 = new PioPeripheral(Cpu, 1);
+ ahb.Register(0x50200000, Pio0);
+ ahb.Register(0x50300000, Pio1);
+
+ // ── GPIO pins ─────────────────────────────────────────────────────
+ var pins = new GpioPin[30];
+ for (var i = 0; i < 30; i++)
+ pins[i] = new GpioPin(i, Sio, IoBank0);
+ Gpio = pins;
+
+ // ── Tickable list ─────────────────────────────────────────────────
+ // ppbRouter.Tick() internally ticks both Ppb (Core0) and Ppb1 (Core1),
+ // so Ppb1 does not need a separate entry here.
+ _tickables = [ppbRouter, Timer, Pwm, Pio0, Pio1, Rtc, Watchdog, Usb];
+
+ // ── DMA DREQ sources ──────────────────────────────────────────────
+ // PIO0 TX/RX SM0-3: DREQ 0-3 (TX), 4-7 (RX)
+ // PIO1 TX/RX SM0-3: DREQ 8-11 (TX), 12-15 (RX)
+ for (var i = 0; i < 4; i++)
+ {
+ var sm = i;
+ Dma.RegisterDreq( 0 + sm, () => Pio0.TxFifoNotFull(sm));
+ Dma.RegisterDreq( 4 + sm, () => !Pio0.RxFifoEmpty(sm));
+ Dma.RegisterDreq( 8 + sm, () => Pio1.TxFifoNotFull(sm));
+ Dma.RegisterDreq(12 + sm, () => !Pio1.RxFifoEmpty(sm));
+ }
+ // SPI0 TX(16), RX(17), SPI1 TX(18), RX(19)
+ Dma.RegisterDreq(16, () => true); // SPI0 TX always ready
+ Dma.RegisterDreq(17, () => Spi0.RxDataAvailable);
+ Dma.RegisterDreq(18, () => true); // SPI1 TX always ready
+ Dma.RegisterDreq(19, () => Spi1.RxDataAvailable);
+ // UART0 TX(20), RX(21), UART1 TX(22), RX(23)
+ Dma.RegisterDreq(20, () => true); // UART0 TX always ready
+ Dma.RegisterDreq(21, () => Uart0.RxDataAvailable);
+ Dma.RegisterDreq(22, () => true); // UART1 TX always ready
+ Dma.RegisterDreq(23, () => Uart1.RxDataAvailable);
+ // ADC DREQ 36: RX FIFO has data
+ Dma.RegisterDreq(36, () => Adc.HasFifoData);
+
+ // ── PIO GPIO integration ───────────────────────────────────────────
+ // Shared helpers: read physical GPIO levels; update SIO output and notify IoBank0
+ uint ReadGpio() => Sio.GpioIn | Sio.GpioOut;
+
+ void ApplyPins(uint value, uint mask)
+ {
+ // PIO output: update SIO GpioIn so physical level is visible to CPU reads
+ Sio.GpioIn = (Sio.GpioIn & ~mask) | (value & mask);
+ // Notify IoBank0 for edge/level interrupt detection on each changed pin
+ for (var pin = 0; pin < 30; pin++)
+ if ((mask & (1u << pin)) != 0)
+ IoBank0.UpdatePinInput(pin, (value & (1u << pin)) != 0);
+ }
+
+ Pio0.ReadGpioIn = ReadGpio;
+ Pio0.WriteGpioPins = ApplyPins;
+ Pio0.WriteGpioDirs = (value, mask) => { /* dir changes tracked in SM only */ };
+
+ Pio1.ReadGpioIn = ReadGpio;
+ Pio1.WriteGpioPins = ApplyPins;
+ Pio1.WriteGpioDirs = (value, mask) => { };
+ }
+
+ /// Load a binary image into Flash starting at 0x10000000.
+ public unsafe void LoadFlash(ReadOnlySpan image)
+ {
+ if (image.Length > Bus.FlashSize)
+ throw new ArgumentException($"Flash image exceeds configured flash size ({Bus.FlashSize / 1024} KB)");
+
+ image.CopyTo(new Span(Bus.PtrFlash, image.Length));
+
+ // If no BootROM has been loaded, install the real RP2040 B1 BootROM binary.
+ // The real bootrom implements rom_table_lookup, memcpy44, memset4 and all
+ // bit-manipulation helpers correctly in native Thumb code.
+ // Flash-hardware-accessing functions (connect_internal_flash, flash_exit_xip,
+ // flash_flush_cache, flash_enter_cmd_xip) are patched to BX LR so they return
+ // immediately without touching SSI registers.
+ // flash_range_erase and flash_range_program are intercepted by C# native hooks.
+ if (*(uint*)Bus.PtrBootRom == 0 && *(uint*)(Bus.PtrBootRom + 4) == 0)
+ {
+ LoadRealBootRom(Bus.PtrBootRom);
+
+ if (TryFindVectorTable(Bus.PtrFlash, (int)image.Length, out var sp, out var resetPc,
+ out var vectorTableOffset))
+ {
+ // Real BootROM sets VTOR to point at the firmware's own vector table
+ // before branching to the Reset handler. pico-sdk code checks VTOR
+ // during spinlock initialisation, so this must be done before Reset().
+ Cpu.Registers.VTOR = 0x10000000u + (uint)vectorTableOffset;
+ }
+
+ // Register C# hooks only for flash erase/program at their real bootrom
+ // addresses so MicroPython's LittleFS formatter can modify emulated flash.
+ Cpu.RegisterNativeHook(0x237C, FlashEraseHook);
+ Cpu.RegisterNativeHook(0x23C4, FlashProgramHook);
+ }
+
+ Cpu.Reset();
+
+ // rp2040js-compatible boot: bypass the bootrom reset handler (which tries to
+ // configure SSI/QSPI hardware that is not fully emulated) and start execution
+ // directly at the flash start address 0x10000000, where boot2 lives.
+ // The bootrom is still resident and handles ROM API calls (rom_table_lookup, etc.)
+ // The firmware's own SP comes from the vector table entry we found above.
+ if (TryFindVectorTable(Bus.PtrFlash, (int)image.Length, out var firmwareSp, out _,
+ out _))
+ {
+ Cpu.Registers.SP = firmwareSp;
+ }
+ Cpu.Registers.PC = BusInterconnect.FLASH_START_ADDRESS;
+ }
+
+ // UF2 format constants (https://github.com/microsoft/uf2)
+ private const uint Uf2MagicStart0 = 0x0A324655u; // "UF2\n"
+ private const uint Uf2MagicStart1 = 0x9E5D5157u;
+ private const uint Uf2MagicEnd = 0x0AB16F30u;
+ private const int Uf2BlockSize = 512;
+ private const uint FlashBase = BusInterconnect.FLASH_START_ADDRESS;
+
+ ///
+ /// Parses a UF2 firmware file and loads its payload into Flash via .
+ /// Only blocks targeting the RP2040 flash region (≥ 0x10000000) are copied; blocks with
+ /// the "not main flash" flag (bit 0) are skipped.
+ ///
+ /// Raw UF2 file bytes.
+ /// Data is not a valid UF2 size.
+ /// No valid data blocks or target address below flash base.
+ public void LoadUf2(ReadOnlySpan uf2) => LoadFlash(Uf2ToFlash(uf2));
+
+ ///
+ /// Parses a UF2 file into a flat binary flash image starting at offset 0 (relative to 0x10000000).
+ /// Erased bytes (not covered by any UF2 block) are set to 0xFF.
+ /// Returns null if the data is not a valid UF2 file (wrong magic or invalid block structure).
+ ///
+ /// Raw UF2 file bytes.
+ public static byte[]? Uf2ToFlash(ReadOnlySpan uf2)
+ {
+ if (uf2.IsEmpty || uf2.Length < Uf2BlockSize || uf2.Length % Uf2BlockSize != 0)
+ return null;
+
+ int blockCount = uf2.Length / Uf2BlockSize;
+ uint flashMin = uint.MaxValue;
+ uint flashMax = 0;
+
+ // First pass: validate blocks and find address range.
+ for (int b = 0; b < blockCount; b++)
+ {
+ int off = b * Uf2BlockSize;
+ uint magic0 = System.Runtime.InteropServices.MemoryMarshal.Read(uf2[off..]);
+ uint magic1 = System.Runtime.InteropServices.MemoryMarshal.Read(uf2[(off + 4)..]);
+ uint magicE = System.Runtime.InteropServices.MemoryMarshal.Read(uf2[(off + 508)..]);
+ if (magic0 != Uf2MagicStart0 || magic1 != Uf2MagicStart1 || magicE != Uf2MagicEnd)
+ return null;
+
+ uint flags = System.Runtime.InteropServices.MemoryMarshal.Read(uf2[(off + 8)..]);
+ if ((flags & 0x00000001u) != 0) continue; // not main flash — skip
+
+ uint targetAddr = System.Runtime.InteropServices.MemoryMarshal.Read(uf2[(off + 12)..]);
+ uint payloadSize = System.Runtime.InteropServices.MemoryMarshal.Read(uf2[(off + 16)..]);
+ if (payloadSize == 0 || payloadSize > 476)
+ return null;
+
+ if (targetAddr < flashMin) flashMin = targetAddr;
+ uint end = targetAddr + payloadSize;
+ if (end > flashMax) flashMax = end;
+ }
+
+ if (flashMin == uint.MaxValue || flashMax <= flashMin)
+ return null;
+ if (flashMin < FlashBase)
+ return null;
+
+ // Allocate a flash image from FlashBase, initialized to 0xFF (erased flash).
+ var image = new byte[flashMax - FlashBase];
+ image.AsSpan().Fill(0xFF);
+
+ // Second pass: copy payloads.
+ for (int b = 0; b < blockCount; b++)
+ {
+ int off = b * Uf2BlockSize;
+ uint flags = System.Runtime.InteropServices.MemoryMarshal.Read(uf2[(off + 8)..]);
+ if ((flags & 0x00000001u) != 0) continue;
+
+ uint targetAddr = System.Runtime.InteropServices.MemoryMarshal.Read(uf2[(off + 12)..]);
+ uint payloadSize = System.Runtime.InteropServices.MemoryMarshal.Read(uf2[(off + 16)..]);
+ uf2.Slice(off + 32, (int)payloadSize).CopyTo(image.AsSpan((int)(targetAddr - FlashBase)));
+ }
+
+ return image;
+ }
+
+ ///
+ /// Scans the flash image for an ARM Cortex-M vector table by looking for a word
+ /// whose upper byte places it in SRAM (0x20xxxxxx) followed by a Thumb-mode pointer
+ /// into Flash (0x1xxxxxxx with LSB set).
+ ///
+ private static unsafe bool TryFindVectorTable(byte* flash, int size,
+ out uint sp, out uint resetPc, out int vectorTableOffset)
+ {
+ // RP2040 SDK firmware: main vector table at offset 0x100 (after 256-byte boot2).
+ // Bare Cortex-M firmware (no boot2): vector table at offset 0.
+ // Also try 0x200 for exotic layouts.
+ ReadOnlySpan offsets = [0x100, 0, 0x200];
+
+ foreach (var off in offsets)
+ {
+ if (off + 8 > size) continue;
+
+ var candidateSp = *(uint*)(flash + off);
+ var candidatePc = *(uint*)(flash + off + 4);
+
+ // SP must be within RP2040 SRAM (0x20000000 – 0x2007FFFF), 4-byte aligned.
+ if ((candidateSp >> 19) != (0x20000000u >> 19)) continue;
+ if ((candidateSp & 3) != 0) continue;
+
+ // Reset PC must be a Thumb pointer (LSB = 1) into Flash (0x10xxxxxx).
+ if ((candidatePc & 1) == 0) continue;
+ if ((candidatePc >> 24) != 0x10) continue;
+
+ sp = candidateSp;
+ resetPc = candidatePc;
+ vectorTableOffset = off;
+ return true;
+ }
+
+ sp = 0;
+ resetPc = 0;
+ vectorTableOffset = 0;
+ return false;
+ }
+
+ // ── Native hook: ROM function lookup ─────────────────────────────────────
+
+ ///
+ /// Function codes for the ROM function lookup table, indexed for fast access.
+ /// Key = 16-bit ROM code, Value = BootROM address (even, Thumb bit NOT included).
+ ///
+ private static readonly Dictionary RomFuncTable = new()
+ {
+ [0x434D] = 0x0100, // 'MC' = memcpy44
+ [0x534D] = 0x0120, // 'MS' = memset4
+ [0x3443] = 0x0100, // 'C4' = memcpy4 (alias)
+ [0x3453] = 0x0120, // 'S4' = memset4 (alias)
+ [0x3350] = 0x01C0, // 'P3' = popcount32 (native hook at 0x01C0)
+ [0x3352] = 0x01D0, // 'R3' = reverse32 (native hook at 0x01D0)
+ [0x334C] = 0x01E0, // 'L3' = clz32 (native hook at 0x01E0)
+ [0x3354] = 0x01F0, // 'T3' = ctz32 (native hook at 0x01F0)
+ [0x4649] = 0x0180, // 'IF' = connect_internal_flash (no-op)
+ [0x5845] = 0x0180, // 'EX' = flash_exit_xip (no-op)
+ [0x4552] = 0x0190, // 'RE' = flash_range_erase (native hook)
+ [0x5052] = 0x01A0, // 'RP' = flash_range_program (native hook)
+ [0x4346] = 0x0180, // 'FC' = flash_flush_cache (no-op)
+ [0x5843] = 0x0180, // 'CX' = flash_enter_cmd_xip (no-op)
+ // Soft-float data table: 'SF' returns pointer to an empty table (terminator only at 0x0250)
+ [0x4653] = 0x0250, // 'SF' = soft_float_table stub
+ };
+
+ private static void RomTableLookupHook(Core.Cpu.CortexM0Plus cpu)
+ {
+ // r0 = table ptr (uint16_t*), r1 = code → r0 = func addr with Thumb bit, or 0
+ var code = cpu.Registers.R1 & 0xFFFF;
+ if (RomFuncTable.TryGetValue(code, out var addr))
+ {
+ cpu.Registers.R0 = addr | 1u;
+ }
+ else
+ {
+ System.Console.Error.WriteLine($"Unknown ROM function code=0x{code:X4} ('{(char)(code & 0xFF)}{(char)((code >> 8) & 0xFF)}') at LR=0x{cpu.Registers.LR:X8}");
+ cpu.Registers.R0 = 0x0181u; // BX LR (safe no-op instead of NULL)
+ }
+ }
+
+ ///
+ /// Native hook for flash_range_erase(uint32_t flash_offs, size_t count, ...).
+ /// Fills the specified flash region with 0xFF (erased state).
+ /// Called by the CPU when PC = 0x0190 (registered in ).
+ ///
+ private unsafe void FlashEraseHook(Core.Cpu.CortexM0Plus cpu)
+ {
+ var offset = (int)(cpu.Registers.R0 & (Bus.FlashSize - 1));
+ var count = (int)cpu.Registers.R1;
+ if (count < 0 || offset + count > (int)Bus.FlashSize) count = (int)Bus.FlashSize - offset;
+ if (count > 0)
+ new Span(Bus.PtrFlash + offset, count).Fill(0xFF);
+ }
+
+ ///
+ /// Native hook for flash_range_program(uint32_t flash_offs, const uint8_t* data, size_t count).
+ /// Copies bytes from SRAM (or anywhere in the address space) into the emulated flash.
+ /// Called by the CPU when PC = 0x01A0 (registered in ).
+ ///
+ private unsafe void FlashProgramHook(Core.Cpu.CortexM0Plus cpu)
+ {
+ var flashOffset = (int)(cpu.Registers.R0 & (Bus.FlashSize - 1));
+ var srcAddr = cpu.Registers.R1;
+ var count = (int)cpu.Registers.R2;
+ if (count < 0 || flashOffset + count > (int)Bus.FlashSize)
+ count = (int)Bus.FlashSize - flashOffset;
+ for (var i = 0; i < count; i++)
+ Bus.PtrFlash[flashOffset + i] = Bus.ReadByte(srcAddr + (uint)i);
+ }
+
+ ///
+ /// Native hook for bootrom memcpy44: copies n bytes (arbitrary count) from src to dst.
+ /// Signature: void* memcpy44(void* dst, const void* src, size_t n) → R0=dst
+ ///
+ private unsafe void Memcpy44Hook(Core.Cpu.CortexM0Plus cpu)
+ {
+ var dst = cpu.Registers.R0;
+ var src = cpu.Registers.R1;
+ var n = (int)cpu.Registers.R2;
+ for (var i = 0; i < n; i++)
+ Bus.WriteByte(dst + (uint)i, Bus.ReadByte(src + (uint)i));
+ // R0 = original dst (already set, unchanged)
+ }
+
+ ///
+ /// Native hook for bootrom memset4: fills n bytes with value c.
+ /// Signature: void* memset4(void* dst, uint8_t c, size_t n) → R0=dst
+ /// The real RP2040 bootrom 'MS' function handles arbitrary n.
+ ///
+ private unsafe void Memset4Hook(Core.Cpu.CortexM0Plus cpu)
+ {
+ var dst = cpu.Registers.R0;
+ var val = (byte)(cpu.Registers.R1 & 0xFF);
+ var n = (int)cpu.Registers.R2;
+ for (var i = 0; i < n; i++)
+ Bus.WriteByte(dst + (uint)i, val);
+ // R0 = original dst (already set, unchanged)
+ }
+
+ private static void Popcount32Hook(Core.Cpu.CortexM0Plus cpu)
+ => cpu.Registers.R0 = (uint)System.Numerics.BitOperations.PopCount(cpu.Registers.R0);
+
+ private static void Reverse32Hook(Core.Cpu.CortexM0Plus cpu)
+ {
+ var v = cpu.Registers.R0;
+ v = ((v & 0xFFFF0000u) >> 16) | ((v & 0x0000FFFFu) << 16);
+ v = ((v & 0xFF00FF00u) >> 8) | ((v & 0x00FF00FFu) << 8);
+ v = ((v & 0xF0F0F0F0u) >> 4) | ((v & 0x0F0F0F0Fu) << 4);
+ v = ((v & 0xCCCCCCCCu) >> 2) | ((v & 0x33333333u) << 2);
+ v = ((v & 0xAAAAAAAAu) >> 1) | ((v & 0x55555555u) << 1);
+ cpu.Registers.R0 = v;
+ }
+
+ private static void Clz32Hook(Core.Cpu.CortexM0Plus cpu)
+ => cpu.Registers.R0 = (uint)System.Numerics.BitOperations.LeadingZeroCount(cpu.Registers.R0);
+
+ private static void Ctz32Hook(Core.Cpu.CortexM0Plus cpu)
+ => cpu.Registers.R0 = (uint)System.Numerics.BitOperations.TrailingZeroCount(cpu.Registers.R0);
+
+ ///
+ /// Loads the real RP2040 B1 bootrom binary (embedded as a resource) into bootrom
+ /// memory, then patches flash hardware-accessing functions to BX LR so they return
+ /// without touching SSI/QSPI registers that are not fully emulated.
+ ///
+ private static unsafe void LoadRealBootRom(byte* rom)
+ {
+ // Load binary from embedded resource
+ var asm = System.Reflection.Assembly.GetExecutingAssembly();
+ using var stream = asm.GetManifestResourceStream("RP2040Sharp.bootrom_b1.bin")
+ ?? throw new InvalidOperationException(
+ "Embedded resource 'RP2040Sharp.bootrom_b1.bin' not found. " +
+ "Ensure bootrom_b1.bin is included as an EmbeddedResource in the project.");
+ stream.ReadExactly(new Span(rom, 16384));
+
+ // Patch flash hardware-accessing bootrom functions to 'BX LR' (0x4770).
+ // These functions talk directly to the SSI/QSPI peripheral, which is not
+ // fully emulated. They are called by MicroPython's LittleFS flash trampoline
+ // (which runs from SRAM) to set up/tear down XIP mode around erase/program ops.
+ // Making them no-ops is safe: our C# hooks handle the actual flash data.
+ // 0x24A0 = connect_internal_flash
+ // 0x23F4 = flash_exit_xip
+ // 0x2360 = flash_flush_cache
+ // 0x2330 = flash_enter_cmd_xip
+ static void PatchBxLr(byte* p, int addr) { p[addr] = 0x70; p[addr + 1] = 0x47; }
+ PatchBxLr(rom, 0x24A0);
+ PatchBxLr(rom, 0x23F4);
+ PatchBxLr(rom, 0x2360);
+ PatchBxLr(rom, 0x2330);
+ }
+
+ ///
+ /// The stub implements the ROM API (rom_table_lookup, memcpy44, memset4) using
+ /// hand-assembled ARM Thumb opcodes. Entry [0] (initial SP) and entry [1]
+ /// (reset PC) are left at zero and must be patched by the caller after
+ /// locating the firmware's own vector table.
+ ///
+ private static unsafe void WriteBootRomStub(byte* rom)
+ {
+ // ── helpers ─────────────────────────────────────────────────────────
+ static void W16(byte* p, int off, ushort v)
+ {
+ p[off] = (byte)(v & 0xFF);
+ p[off + 1] = (byte)(v >> 8);
+ }
+ static void W32(byte* p, int off, uint v)
+ {
+ p[off] = (byte)( v & 0xFF);
+ p[off + 1] = (byte)((v >> 8) & 0xFF);
+ p[off + 2] = (byte)((v >> 16) & 0xFF);
+ p[off + 3] = (byte)( v >> 24);
+ }
+
+ // ── Exception vector table (0x0000 – 0x003F + IRQs) ─────────────────
+ // Entry [0] = Initial SP ← patched later by LoadFlash
+ // Entry [1] = Reset PC ← patched later by LoadFlash
+ // All others → default_handler (BX LR at 0x0180) with Thumb bit
+ const uint defaultHandler = 0x0181u;
+ W32(rom, 0x0000, 0x20041000); // BootROM initial SP (overwritten later)
+ for (int i = 1; i < 16; i++)
+ W32(rom, i * 4, defaultHandler);
+ for (int i = 0; i < 26; i++) // RP2040 has 26 external IRQs
+ W32(rom, 0x0040 + i * 4, defaultHandler);
+
+ // ── ROM API infrastructure (in reserved Cortex-M0+ vector slots) ─────
+ // 0x0010 – ROM code magic, 0x0012 – version, 0x0014 – func_table_ptr,
+ // 0x0016 – data_table_ptr, 0x0018 – rom_table_lookup fn ptr
+ W16(rom, 0x0010, 0x0210); // ROM code magic (matches real RP2040 BootROM)
+ W16(rom, 0x0012, 0x02); // ROM version 2
+ W16(rom, 0x0014, 0x0200); // function table at 0x0200
+ W16(rom, 0x0016, 0x0250); // data table at 0x0250 (just a terminator)
+ W16(rom, 0x0018, 0x0061); // rom_table_lookup at 0x0060 (Thumb bit = 0x0061)
+
+ // ── default_handler at 0x0180: BX LR ─────────────────────────────────
+ W16(rom, 0x0180, 0x4770); // BX LR
+
+ // ── rom_table_lookup at 0x0060 ────────────────────────────────────────
+ // r0 = table (uint16_t*), r1 = code → r0 = func addr (with Thumb bit) or 0
+ // Branch offsets: ARMv6-M PC = instruction_address + 4 when computing branch target.
+ // loop(0x60): ldrh r2,[r0]; cbz r2,not_found(0x74); uxth r3,r1; cmp r2,r3
+ // beq found(0x6E); adds r0,#4; b loop(0x60)
+ // found(0x6E): ldrh r0,[r0,#2]; bx lr
+ // not_found(0x74): movs r0,#0; bx lr
+ ReadOnlySpan lookup =
+ [
+ 0x8802, // 0x0060 LDRH r2, [r0, #0] ; loop:
+ 0xB13A, // 0x0062 CBZ r2, not_found ; PC=0x0066, +14 → 0x0074
+ 0xB28B, // 0x0064 UXTH r3, r1
+ 0x429A, // 0x0066 CMP r2, r3
+ 0xD001, // 0x0068 BEQ found ; PC=0x006C, +1×2=2 → 0x006E
+ 0x3004, // 0x006A ADDS r0, r0, #4
+ 0xE7F8, // 0x006C B loop ; PC=0x0070, -8×2=-16 → 0x0060
+ 0x8840, // 0x006E LDRH r0, [r0, #2] ; found:
+ 0x4770, // 0x0070 BX LR
+ 0x2000, // 0x0072 MOVS r0, #0 ; not_found:
+ 0x4770, // 0x0074 BX LR
+ ];
+ for (int i = 0; i < lookup.Length; i++) W16(rom, 0x0060 + i * 2, lookup[i]);
+
+ // ── memcpy44 at 0x0100 ────────────────────────────────────────────────
+ // void *memcpy44(void *dst, const void *src, uint n) -- n bytes (multiple of 4)
+ // Uses CBZ up-front guard so n=0 returns immediately without corrupting memory.
+ // Layout: 0x0100 – 0x0110 (9 halfwords = 18 bytes)
+ ReadOnlySpan memcpy44 =
+ [
+ 0xB510, // 0x0100 PUSH {r4, lr}
+ 0x4604, // 0x0102 MOV r4, r0 ; save original dst
+ 0xB11A, // 0x0104 CBZ r2, done (+6) ; PC=0x0108, +6 → 0x010E
+ 0xC908, // 0x0106 LDMIA r1!, {r3} ; loop: r3 = *src++
+ 0xC008, // 0x0108 STMIA r0!, {r3} ; *dst++ = r3
+ 0x3A04, // 0x010A SUBS r2, r2, #4
+ 0xD1FB, // 0x010C BNE loop (-10) ; PC=0x0110, -10 → 0x0106
+ 0x4620, // 0x010E MOV r0, r4 ; done: return original dst
+ 0xBD10, // 0x0110 POP {r4, pc}
+ ];
+ for (int i = 0; i < memcpy44.Length; i++) W16(rom, 0x0100 + i * 2, memcpy44[i]);
+
+ // ── memset4 at 0x0120 ────────────────────────────────────────────────
+ // void *memset4(void *dst, uint8_t c, uint n)
+ // Fills n bytes (multiple of 4) with word pattern (c,c,c,c); returns dst.
+ // Uses CBZ up-front guard: decrements n AFTER each store (no off-by-one).
+ // Layout: 0x0120 – 0x0138 (13 halfwords = 26 bytes)
+ ReadOnlySpan memset4 =
+ [
+ 0xB510, // 0x0120 PUSH {r4, lr}
+ 0x4604, // 0x0122 MOV r4, r0 ; save original dst
+ 0xB2C9, // 0x0124 UXTB r1, r1 ; r1 = c & 0xFF (zero-extend)
+ 0x020B, // 0x0126 LSLS r3, r1, #8
+ 0x4319, // 0x0128 ORRS r1, r3 ; r1 = c | (c<<8)
+ 0x040B, // 0x012A LSLS r3, r1, #16
+ 0x4319, // 0x012C ORRS r1, r3 ; r1 = 4-byte word pattern
+ 0xB112, // 0x012E CBZ r2, done (+4) ; PC=0x0132, +4 → 0x0136
+ 0xC002, // 0x0130 STMIA r0!, {r1} ; loop: *dst++ = word
+ 0x3A04, // 0x0132 SUBS r2, r2, #4
+ 0xD1FC, // 0x0134 BNE loop (-8) ; PC=0x0138, -8 → 0x0130
+ 0x4620, // 0x0136 MOV r0, r4 ; done: return original dst
+ 0xBD10, // 0x0138 POP {r4, pc}
+ ];
+ for (int i = 0; i < memset4.Length; i++) W16(rom, 0x0120 + i * 2, memset4[i]);
+
+ // ── Native-hook stubs ─────────────────────────────────────────────────
+ // 0x0190: flash_range_erase hook — BX LR fallback (hook fires first)
+ // 0x01A0: flash_range_program hook — BX LR fallback
+ // 0x01C0: popcount32, 0x01D0: reverse32, 0x01E0: clz32, 0x01F0: ctz32
+ W16(rom, 0x0190, 0x4770); // BX LR
+ W16(rom, 0x01A0, 0x4770); // BX LR
+ W16(rom, 0x01C0, 0x4770); // BX LR (popcount32 — native hook)
+ W16(rom, 0x01D0, 0x4770); // BX LR (reverse32 — native hook)
+ W16(rom, 0x01E0, 0x4770); // BX LR (clz32 — native hook)
+ W16(rom, 0x01F0, 0x4770); // BX LR (ctz32 — native hook)
+
+ // ── Function lookup table at 0x0200 ───────────────────────────────────
+ // Format: pairs of uint16_t {code, func_ptr}, terminated by {0, 0}.
+ // 'RE' and 'RP' point to native-hook stubs so C# code can modify flash.
+ ReadOnlySpan funcTable =
+ [
+ 0x434D, 0x0101, // 'MC' = MEMCPY / MEMCPY44 (Thumb bit: 0x0100|1)
+ 0x534D, 0x0121, // 'MS' = MEMSET / MEMSET4 (Thumb bit: 0x0120|1)
+ 0x4649, 0x0181, // 'IF' = connect_internal_flash (no-op BX LR)
+ 0x5845, 0x0181, // 'EX' = flash_exit_xip (no-op BX LR)
+ 0x4552, 0x0191, // 'RE' = flash_range_erase → native hook at 0x0190
+ 0x5052, 0x01A1, // 'RP' = flash_range_program → native hook at 0x01A0
+ 0x4346, 0x0181, // 'FC' = flash_flush_cache (no-op BX LR)
+ 0x5843, 0x0181, // 'CX' = flash_enter_cmd_xip (no-op BX LR)
+ 0x0000, 0x0000, // terminator
+ ];
+ for (int i = 0; i < funcTable.Length; i++) W16(rom, 0x0200 + i * 2, funcTable[i]);
+
+ // Data table at 0x0250: just a terminator
+ W16(rom, 0x0250, 0x0000);
+ }
+
+ /// Load a binary image into BootROM at 0x00000000 (max 16 KB).
+ public unsafe void LoadBootRom(ReadOnlySpan image)
+ {
+ if (image.Length > 0x4000)
+ throw new ArgumentException("BootROM image exceeds 16 KB");
+
+ image.CopyTo(new Span(Bus.PtrBootRom, image.Length));
+ }
+
+ /// Total instructions executed by Core 0 since reset.
+ public long InstructionCount => Cpu.Cycles;
+
+ /// True once Core 1 has been launched via the SIO FIFO multicore handshake.
+ public bool Core1Launched => _core1Launched;
+
+ ///
+ /// Wall-clock cycles elapsed during the most recent call.
+ /// When Core 1 is launched this is max(core0, core1) — both cores run in
+ /// parallel on real hardware — never the sum.
+ ///
+ public long LastElapsedCycles { get; private set; }
+
+ ///
+ /// Run both cores for approximately instructions each,
+ /// then tick all time-aware peripherals.
+ /// Core 0 always runs; Core 1 only runs after it has been launched by the firmware
+ /// via the SIO FIFO multicore handshake (RP2040 datasheet §2.8.3).
+ ///
+ public void Run(int instructions)
+ {
+ // ── Core 0 ────────────────────────────────────────────────────
+ _activeCoreId = 0;
+ var before0 = Cpu.Cycles;
+ Cpu.Run(instructions);
+ var delta = Cpu.Cycles - before0;
+
+ // ── Core 1 (if launched) ───────────────────────────────────────
+ if (_core1Launched)
+ {
+ _activeCoreId = 1;
+ var before1 = Cpu1.Cycles;
+ Cpu1.Run(instructions);
+ _activeCoreId = 0;
+ // Both cores run in parallel on real hardware; wall-clock elapsed
+ // is the maximum of the two cycle counts.
+ delta = Math.Max(delta, (int)(Cpu1.Cycles - before1));
+ }
+
+ LastElapsedCycles = delta;
+
+ foreach (var t in _tickables)
+ t.Tick(delta);
+ }
+
+ /// Reset Core 0. Core 1 is also reset and its launched state is cleared.
+ public void Reset()
+ {
+ _core1Launched = false;
+ _activeCoreId = 0;
+ Sio.ResetMulticoreLaunch();
+ Cpu.Reset();
+ Cpu1.Reset();
+ }
+
+ public void Dispose() => Bus.Dispose();
+
+ // ── Multicore launch ──────────────────────────────────────────────
+
+ ///
+ /// Called by when Core 0 completes the RP2040 §2.8.3
+ /// multicore launch handshake. Configures Core 1's registers (VTOR, SP, PC) and
+ /// marks it as runnable so subsequent calls execute it.
+ ///
+ private void LaunchCore1(uint vtor, uint sp, uint entry)
+ {
+ // Reset clears the lockup flag (handles re-launch of a previously faulted core).
+ Cpu1.Reset();
+ Cpu1.Registers.VTOR = vtor;
+ Cpu1.Registers.SP = sp;
+ Cpu1.Registers.PC = entry & 0xFFFFFFFEu; // strip Thumb bit
+ // The Run() loop will auto-update the fetch cache on the first instruction.
+ _core1Launched = true;
+ }
+
+ // ── Per-core PPB router ───────────────────────────────────────────
+
+ ///
+ /// Routes PPB (0xE000xxxx) bus accesses to Core 0's or Core 1's
+ /// based on the currently-active core ID.
+ /// Each core has its own private NVIC, SysTick and SCB in the real RP2040.
+ ///
+ private sealed class PerCorePpbRouter : IMemoryMappedDevice, ITickable
+ {
+ private readonly PpbPeripheral _ppb0;
+ private readonly PpbPeripheral _ppb1;
+ private readonly Func _getActiveCoreId;
+
+ public PerCorePpbRouter(PpbPeripheral ppb0, PpbPeripheral ppb1,
+ Func getActiveCoreId)
+ {
+ _ppb0 = ppb0;
+ _ppb1 = ppb1;
+ _getActiveCoreId = getActiveCoreId;
+ }
+
+ private PpbPeripheral Active =>
+ _getActiveCoreId() == 1 ? _ppb1 : _ppb0;
+
+ public uint Size => 0x10000000; // covers the full 0xE region
+
+ public uint ReadWord(uint address) => Active.ReadWord(address);
+ public ushort ReadHalfWord(uint address) => Active.ReadHalfWord(address);
+ public byte ReadByte(uint address) => Active.ReadByte(address);
+ public void WriteWord(uint address, uint value) => Active.WriteWord(address, value);
+ public void WriteHalfWord(uint address, ushort value) =>
+ Active.WriteHalfWord(address, value);
+ public void WriteByte(uint address, byte value) => Active.WriteByte(address, value);
+
+ /// Tick both PPBs (SysTick, etc.) by the same delta cycles.
+ public void Tick(long deltaCycles)
+ {
+ _ppb0.Tick(deltaCycles);
+ _ppb1.Tick(deltaCycles);
+ }
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Resets/ResetsPeripheral.cs b/src/RP2040Sharp/Peripherals/Resets/ResetsPeripheral.cs
new file mode 100644
index 0000000..c28c3b9
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Resets/ResetsPeripheral.cs
@@ -0,0 +1,74 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Resets;
+
+///
+/// RESETS peripheral (0x4000C000).
+/// Controls reset state of each RP2040 subsystem.
+/// Firmware writes RESET bits to hold subsystems in reset, then clears bits
+/// to bring them out. RESET_DONE returns the complement — polled by SDK init.
+///
+public sealed class ResetsPeripheral : IMemoryMappedDevice
+{
+ private const uint RESET = 0x00;
+ private const uint WDSEL = 0x04;
+ private const uint RESET_DONE = 0x08;
+
+ // 25 subsystem bits
+ private const uint ALL_BITS = 0x01FFFFFF;
+ private const uint USBCTRL_BIT = 1u << 24;
+
+ // Start with nothing in reset so RESET_DONE = ALL_BITS from power-on.
+ // Firmware reset/unreset sequences will still work correctly because after
+ // firmware writes RESET then clears it, RESET_DONE returns that bit set.
+ private uint _reset = 0;
+ private uint _wdsel;
+
+ /// Fired when a peripheral is released from reset (bit was set, now cleared).
+ public Action? OnUnreset;
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address) => address switch
+ {
+ RESET => _reset,
+ WDSEL => _wdsel,
+ RESET_DONE => (~_reset) & ALL_BITS,
+ _ => 0,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case RESET:
+ var prev = _reset;
+ _reset = value & ALL_BITS;
+ // Fire OnUnreset for any bits that transitioned from set to clear
+ var released = prev & ~_reset;
+ if (released != 0) OnUnreset?.Invoke(released);
+ break;
+ case WDSEL: _wdsel = value & ALL_BITS; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Rosc/RoscPeripheral.cs b/src/RP2040Sharp/Peripherals/Rosc/RoscPeripheral.cs
new file mode 100644
index 0000000..92ed709
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Rosc/RoscPeripheral.cs
@@ -0,0 +1,80 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Rosc;
+
+///
+/// Ring Oscillator peripheral stub (0x40060000).
+/// Reports STATUS.STABLE=1 (bit 31) always so firmware ROSC checks pass.
+///
+public sealed class RoscPeripheral : IMemoryMappedDevice
+{
+ private const uint CTRL = 0x00;
+ private const uint FREQA = 0x04;
+ private const uint FREQB = 0x08;
+ private const uint DORMANT = 0x0C;
+ private const uint DIV = 0x10;
+ private const uint PHASE = 0x14;
+ private const uint STATUS = 0x18;
+ private const uint RANDOMBIT = 0x1C;
+ private const uint COUNT = 0x20;
+
+ private const uint STATUS_STABLE = 1u << 31;
+ private const uint STATUS_ENABLED = 1u << 12;
+ private const uint STATUS_BADWRITE = 1u << 24;
+
+ private uint _ctrl = 0xFAB; // enabled (ENABLE field = 0xFAB)
+ private uint _freqa;
+ private uint _freqb;
+ private uint _div = 0xAA0; // default divisor
+ private uint _phase;
+
+ private static uint _randomBit; // simple pseudo-random bit
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address) => address switch
+ {
+ CTRL => _ctrl,
+ FREQA => _freqa,
+ FREQB => _freqb,
+ DORMANT => 0,
+ DIV => _div,
+ PHASE => _phase,
+ STATUS => STATUS_STABLE | STATUS_ENABLED,
+ RANDOMBIT => (++_randomBit) & 1, // alternate 0/1 as pseudo-random bit
+ COUNT => 0,
+ _ => 0,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case CTRL: _ctrl = value & 0xFFF; break;
+ case FREQA: _freqa = value; break;
+ case FREQB: _freqb = value; break;
+ case DIV: _div = value; break;
+ case PHASE: _phase = value; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Rtc/RtcPeripheral.cs b/src/RP2040Sharp/Peripherals/Rtc/RtcPeripheral.cs
new file mode 100644
index 0000000..7758576
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Rtc/RtcPeripheral.cs
@@ -0,0 +1,206 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Rtc;
+
+///
+/// Real-Time Clock peripheral (0x4005C000).
+/// Advances 1 second per 125M CPU cycles (CLK_SYS = 125 MHz).
+/// Fires IRQ 25 (RTC_IRQ) when the enabled alarm matches the current time.
+///
+public sealed class RtcPeripheral : IMemoryMappedDevice, ITickable
+{
+ private const uint RTC_SETUP0 = 0x04; // YEAR[27:16], MONTH[11:8], DAY[4:0]
+ private const uint RTC_SETUP1 = 0x08; // DOTW[26:24], HOUR[20:16], MIN[13:8], SEC[5:0]
+ private const uint RTC_CTRL = 0x0C; // ENABLE[0], ACTIVE[1], LOAD[4]
+ private const uint IRQ_SETUP_0 = 0x10;
+ private const uint IRQ_SETUP_1 = 0x14;
+ private const uint RTC_RTC1 = 0x18; // DOTW/HOUR/MIN/SEC (same layout as SETUP1 bits)
+ private const uint RTC_RTC0 = 0x1C; // YEAR/MONTH/DAY
+
+ private const uint CTRL_ENABLE = 1u;
+ private const uint CTRL_ACTIVE = 1u << 1;
+ private const uint CTRL_LOAD = 1u << 4;
+
+ // IRQ_SETUP_0 bit masks
+ // ENA bits are one position above the MSB of each value field to avoid overlap
+ private const uint IRQ0_MATCH_ENA = 1u << 31;
+ private const uint IRQ0_YEAR_ENA = 1u << 28; // YEAR [27:16] — bit above field OK
+ private const uint IRQ0_MONTH_ENA = 1u << 12; // MONTH [11:8] — ENA at 12, not 11
+ private const uint IRQ0_DAY_ENA = 1u << 5; // DAY [4:0] — ENA at 5, not 4
+
+ // IRQ_SETUP_1 bit masks
+ private const uint IRQ1_MATCH_ACTIVE = 1u << 31;
+ private const uint IRQ1_DOTW_ENA = 1u << 28; // DOTW [26:24] — bit 28 OK (gap at 27)
+ private const uint IRQ1_HOUR_ENA = 1u << 21; // HOUR [20:16] — ENA at 21, not 20
+ private const uint IRQ1_MIN_ENA = 1u << 14; // MIN [13:8] — ENA at 14, not 13
+ private const uint IRQ1_SEC_ENA = 1u << 6; // SEC [5:0] — ENA at 6, not 5
+
+ private const int RTC_IRQ = 25;
+ private const long CLK_HZ = 125_000_000; // 125 MHz
+
+ private readonly CortexM0Plus? _cpu;
+
+ private uint _setup0;
+ private uint _setup1;
+ private uint _ctrl;
+ private uint _irqSetup0;
+ private uint _irqSetup1;
+ // Running time registers
+ private uint _rtc0; // YEAR[27:16] MONTH[11:8] DAY[4:0]
+ private uint _rtc1; // DOTW[26:24] HOUR[20:16] MIN[13:8] SEC[5:0]
+
+ private long _accumCycles;
+
+ public uint Size => 0x1000;
+
+ public RtcPeripheral(CortexM0Plus? cpu = null)
+ {
+ _cpu = cpu;
+ // Default to 2024-01-01 Monday 00:00:00
+ _rtc0 = (2024u << 16) | (1u << 8) | 1u;
+ _rtc1 = (1u << 24); // Monday
+ }
+
+ /// Inject a specific date/time into the RTC.
+ public void SetDateTime(int year, int month, int day, int dayOfWeek, int hour, int min, int sec)
+ {
+ _rtc0 = ((uint)year << 16) | ((uint)month << 8) | (uint)day;
+ _rtc1 = ((uint)dayOfWeek << 24) | ((uint)hour << 16) | ((uint)min << 8) | (uint)sec;
+ }
+
+ // ── ITickable ─────────────────────────────────────────────────────────
+
+ public void Tick(long deltaCycles)
+ {
+ if ((_ctrl & CTRL_ENABLE) == 0) return;
+
+ _accumCycles += deltaCycles;
+ while (_accumCycles >= CLK_HZ)
+ {
+ _accumCycles -= CLK_HZ;
+ AdvanceSecond();
+ CheckAlarm();
+ }
+ }
+
+ // ── IMemoryMappedDevice ───────────────────────────────────────────────
+
+ public uint ReadWord(uint address) => address switch
+ {
+ RTC_SETUP0 => _setup0,
+ RTC_SETUP1 => _setup1,
+ RTC_CTRL => (_ctrl & CTRL_ENABLE) != 0 ? (_ctrl | CTRL_ACTIVE) : _ctrl,
+ IRQ_SETUP_0 => _irqSetup0,
+ IRQ_SETUP_1 => _irqSetup1,
+ RTC_RTC1 => _rtc1,
+ RTC_RTC0 => _rtc0,
+ _ => 0,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case RTC_SETUP0:
+ _setup0 = value;
+ break;
+ case RTC_SETUP1:
+ _setup1 = value;
+ break;
+ case RTC_CTRL:
+ if ((value & CTRL_LOAD) != 0)
+ {
+ _rtc0 = _setup0;
+ _rtc1 = _setup1;
+ _accumCycles = 0;
+ }
+ _ctrl = value & CTRL_ENABLE; // LOAD is strobe, ACTIVE is read-only
+ break;
+ case IRQ_SETUP_0: _irqSetup0 = value; break;
+ case IRQ_SETUP_1:
+ // bit 31 (MATCH_ACTIVE) is write-1-to-clear
+ if ((value & IRQ1_MATCH_ACTIVE) != 0) _irqSetup1 &= ~IRQ1_MATCH_ACTIVE;
+ _irqSetup1 = (_irqSetup1 & IRQ1_MATCH_ACTIVE) | (value & ~IRQ1_MATCH_ACTIVE);
+ break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ // ── Private helpers ───────────────────────────────────────────────────
+
+ private void AdvanceSecond()
+ {
+ var sec = (int)(_rtc1 & 0x3F);
+ var min = (int)((_rtc1 >> 8) & 0x3F);
+ var hour = (int)((_rtc1 >> 16) & 0x1F);
+ var dotw = (int)((_rtc1 >> 24) & 0x7);
+ var day = (int)(_rtc0 & 0x1F);
+ var month = (int)((_rtc0 >> 8) & 0xF);
+ var year = (int)((_rtc0 >> 16) & 0xFFF);
+
+ sec++;
+ if (sec >= 60) { sec = 0; min++; }
+ if (min >= 60) { min = 0; hour++; }
+ if (hour >= 24)
+ {
+ hour = 0;
+ dotw = (dotw + 1) % 7;
+ day++;
+ var daysInMonth = year > 0 && month is >= 1 and <= 12
+ ? DateTime.DaysInMonth(year, month) : 31;
+ if (day > daysInMonth) { day = 1; month++; }
+ if (month > 12) { month = 1; year++; }
+ }
+
+ _rtc0 = ((uint)year << 16) | ((uint)month << 8) | (uint)day;
+ _rtc1 = ((uint)dotw << 24) | ((uint)hour << 16) | ((uint)min << 8) | (uint)sec;
+ }
+
+ private void CheckAlarm()
+ {
+ if ((_irqSetup0 & IRQ0_MATCH_ENA) == 0) return;
+
+ var sec = _rtc1 & 0x3F;
+ var min = (_rtc1 >> 8) & 0x3F;
+ var hour = (_rtc1 >> 16) & 0x1F;
+ var dotw = (_rtc1 >> 24) & 0x7;
+ var day = _rtc0 & 0x1F;
+ var month = (_rtc0 >> 8) & 0xF;
+ var year = (_rtc0 >> 16) & 0xFFF;
+
+ var matched = true;
+ if ((_irqSetup0 & IRQ0_YEAR_ENA) != 0) matched &= ((_irqSetup0 >> 16) & 0xFFF) == year;
+ if ((_irqSetup0 & IRQ0_MONTH_ENA) != 0) matched &= ((_irqSetup0 >> 8) & 0xF) == month;
+ if ((_irqSetup0 & IRQ0_DAY_ENA) != 0) matched &= (_irqSetup0 & 0x1F) == day;
+ if ((_irqSetup1 & IRQ1_DOTW_ENA) != 0) matched &= ((_irqSetup1 >> 24) & 0x7) == dotw;
+ if ((_irqSetup1 & IRQ1_HOUR_ENA) != 0) matched &= ((_irqSetup1 >> 16) & 0x1F) == hour;
+ if ((_irqSetup1 & IRQ1_MIN_ENA) != 0) matched &= ((_irqSetup1 >> 8) & 0x3F) == min;
+ if ((_irqSetup1 & IRQ1_SEC_ENA) != 0) matched &= (_irqSetup1 & 0x3F) == sec;
+
+ if (matched)
+ {
+ _irqSetup1 |= IRQ1_MATCH_ACTIVE; // set MATCH_ACTIVE flag
+ _cpu?.SetInterrupt(RTC_IRQ, true);
+ }
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Sio/SioPeripheral.cs b/src/RP2040Sharp/Peripherals/Sio/SioPeripheral.cs
new file mode 100644
index 0000000..4e6bc26
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Sio/SioPeripheral.cs
@@ -0,0 +1,705 @@
+using System.Runtime.CompilerServices;
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Sio;
+
+///
+/// Single-Cycle I/O (SIO) peripheral.
+/// Base address: 0xD0000000. Register with BusInterconnect via MapDevice(0xD, sio).
+/// Addresses received are already masked (address & 0x0FFFFFFF); since SIO is the
+/// only device in region 0xD, local offset = address directly.
+///
+public sealed class SioPeripheral : IMemoryMappedDevice
+{
+ // ── CPUID ────────────────────────────────────────────────────────────
+ private const uint CPUID = 0x000; // 0 = Core0, 1 = Core1
+
+ // ── GPIO (offsets from SIO base) ─────────────────────────────────
+ private const uint GPIO_IN = 0x004;
+ private const uint GPIO_HI_IN = 0x008; // QSPI GPIO input
+ private const uint GPIO_OUT = 0x010;
+ private const uint GPIO_OUT_SET = 0x014;
+ private const uint GPIO_OUT_CLR = 0x018;
+ private const uint GPIO_OUT_XOR = 0x01C;
+ private const uint GPIO_OE = 0x020;
+ private const uint GPIO_OE_SET = 0x024;
+ private const uint GPIO_OE_CLR = 0x028;
+ private const uint GPIO_OE_XOR = 0x02C;
+ private const uint GPIO_HI_OUT = 0x030; // QSPI output
+ private const uint GPIO_HI_OUT_SET = 0x034;
+ private const uint GPIO_HI_OUT_CLR = 0x038;
+ private const uint GPIO_HI_OUT_XOR = 0x03C;
+ private const uint GPIO_HI_OE = 0x040;
+ private const uint GPIO_HI_OE_SET = 0x044;
+ private const uint GPIO_HI_OE_CLR = 0x048;
+ private const uint GPIO_HI_OE_XOR = 0x04C;
+
+ // ── Multicore FIFO ────────────────────────────────────────────────
+ private const uint FIFO_ST = 0x050; // FIFO status
+ private const uint FIFO_WR = 0x054; // write to TX FIFO (to other core)
+ private const uint FIFO_RD = 0x058; // read from RX FIFO (from other core)
+
+ // ── Spinlock status ───────────────────────────────────────────────
+ private const uint SPINLOCK_ST = 0x05C; // bitmask of claimed spinlocks
+
+ // ── Hardware divider ─────────────────────────────────────────────
+ private const uint DIV_UDIVIDEND = 0x060;
+ private const uint DIV_UDIVISOR = 0x064;
+ private const uint DIV_SDIVIDEND = 0x068;
+ private const uint DIV_SDIVISOR = 0x06C;
+ private const uint DIV_QUOTIENT = 0x070;
+ private const uint DIV_REMAINDER = 0x074;
+ private const uint DIV_CSR = 0x078; // bit0=DIRTY, bit1=READY
+
+ // ── Interpolators (INTERP0: 0x080, INTERP1: 0x0C0, stride 0x40) ───
+ private const uint INTERP0_BASE = 0x080;
+ private const uint INTERP1_BASE = 0x0C0;
+
+ // Per-interp offsets
+ private const uint INTERP_ACCUM0 = 0x00;
+ private const uint INTERP_ACCUM1 = 0x04;
+ private const uint INTERP_BASE0 = 0x08;
+ private const uint INTERP_BASE1 = 0x0C;
+ private const uint INTERP_BASE2 = 0x10;
+ private const uint INTERP_POP_LANE0 = 0x14; // read+update
+ private const uint INTERP_POP_LANE1 = 0x18;
+ private const uint INTERP_POP_FULL = 0x1C;
+ private const uint INTERP_PEEK_LANE0 = 0x20; // read-only
+ private const uint INTERP_PEEK_LANE1 = 0x24;
+ private const uint INTERP_PEEK_FULL = 0x28;
+ private const uint INTERP_CTRL_LANE0 = 0x2C;
+ private const uint INTERP_CTRL_LANE1 = 0x30;
+ private const uint INTERP_ACCUM0_ADD = 0x34;
+ private const uint INTERP_ACCUM1_ADD = 0x38;
+ private const uint INTERP_BASE_1AND0 = 0x3C;
+
+ // ── Spinlocks ────────────────────────────────────────────────────
+ private const uint SPINLOCK_BASE = 0x100;
+ private const uint SPINLOCK_END = 0x17C;
+
+ private readonly CortexM0Plus _cpu;
+ private CortexM0Plus? _cpu1; // Core1 CPU; set by RP2040Machine after construction
+ private bool _core1Launched; // true once the §2.8.3 launch handshake has completed
+
+ ///
+ /// Returns the ID of the core currently performing the bus access (0 or 1).
+ /// Set by RP2040Machine before each CPU's Run() call.
+ ///
+ public Func? GetActiveCoreId;
+
+ // GPIO state
+ private uint _gpioOut;
+ private uint _gpioOe;
+ private uint _gpioIn;
+ private uint _gpioHiOut;
+ private uint _gpioHiOe;
+
+ // Divider state
+ private uint _divUdividend, _divUdivisor;
+ private int _divSdividend, _divSdivisor;
+ private uint _divQuotient, _divRemainder;
+ private uint _divCsr;
+ private bool _divSigned;
+
+ // Spinlocks
+ private uint _spinLocks;
+
+ // Multicore FIFO — two one-directional queues
+ // _fifo01: Core0 → Core1 (Core0 writes here, Core1 reads here)
+ // _fifo10: Core1 → Core0 (Core1 writes here, Core0 reads here)
+ // FIFO_ST bits: VLD[0]=RX not empty, RDY[1]=TX not full, WOF[2], ROE[3]
+ private const int FIFO_DEPTH = 8;
+ private const uint FIFO_ST_VLD = 1u; // RX has data
+ private const uint FIFO_ST_RDY = 1u << 1; // TX has space
+ private const uint FIFO_ST_WOF = 1u << 2; // write-overflow (TX write when full)
+ private const uint FIFO_ST_ROE = 1u << 3; // read-underflow (RX read when empty)
+
+ private readonly Queue _fifo01 = new(FIFO_DEPTH); // Core0→Core1
+ private readonly Queue _fifo10 = new(FIFO_DEPTH); // Core1→Core0
+ private bool _wof0, _roe0; // Core0's WOF/ROE flags
+ private bool _wof1, _roe1; // Core1's WOF/ROE flags
+
+ // Multicore launch handshake state (RP2040 datasheet §2.8.3)
+ // Core0 sends the 6-word sequence: 0, 0, 1, VTOR, SP, Entry.
+ // Before Core1 is running we echo each word back so Core0's blocking
+ // pop returns immediately, and we configure + launch Core1 on the 6th word.
+ private int _launchSeqPos;
+ private uint _launchVtor;
+ private uint _launchSp;
+ private uint _launchEntry;
+
+ ///
+ /// Fired when Core0 completes the multicore launch sequence (RP2040 §2.8.3).
+ /// Parameters: (vtor, sp, entry). RP2040Machine sets this callback to configure
+ /// and start Core1.
+ ///
+ public Action? OnLaunchCore1;
+
+ // Interpolators
+ private InterpState _interp0;
+ private InterpState _interp1;
+
+ public uint Size => 0x10000; // wide enough to cover spinlocks at 0x100–0x17C
+
+ /// Optionally feed current GPIO input state from IO_BANK0.
+ public uint GpioIn
+ {
+ get => _gpioIn;
+ set => _gpioIn = value;
+ }
+
+ public uint GpioOe => _gpioOe;
+ public uint GpioOut => _gpioOut;
+
+ public void SetGpioExternalIn(int pin, bool high)
+ {
+ if (high) _gpioIn |= (1u << pin);
+ else _gpioIn &= ~(1u << pin);
+ }
+
+ public bool GetGpioOutputEnable(int pin) => (_gpioOe & (1u << pin)) != 0;
+ public bool GetGpioOut(int pin) => (_gpioOut & (1u << pin)) != 0;
+
+ public SioPeripheral(CortexM0Plus cpu)
+ {
+ _cpu = cpu;
+ }
+
+ /// Register Core1's CPU so FIFO writes can signal it.
+ public void SetCpu1(CortexM0Plus cpu1) => _cpu1 = cpu1;
+
+ // ── IMemoryMappedDevice — reads ──────────────────────────────────
+
+ public uint ReadWord(uint address)
+ {
+ // Spinlocks 0–31
+ if (address >= SPINLOCK_BASE && address <= SPINLOCK_END)
+ return ReadSpinlock((int)((address - SPINLOCK_BASE) >> 2));
+
+ // Interpolator 0
+ if (address >= INTERP0_BASE && address < INTERP0_BASE + 0x40)
+ return ReadInterp(ref _interp0, address - INTERP0_BASE);
+
+ // Interpolator 1
+ if (address >= INTERP1_BASE && address < INTERP1_BASE + 0x40)
+ return ReadInterp(ref _interp1, address - INTERP1_BASE);
+
+ return address switch
+ {
+ CPUID => (uint)(GetActiveCoreId?.Invoke() ?? 0),
+ GPIO_IN => _gpioIn,
+ // GPIO_HI_IN: QSPI GPIO inputs. Bit 1 = QSPI_SS_N (active-low flash select / BOOTSEL).
+ // It must read HIGH (1) so the bootrom BOOTSEL check sees "button not pressed" and
+ // proceeds to flash boot instead of USB BOOTSEL mode.
+ // Other data lines (SD0-SD3, bits 2-5) are HIGH at idle; SCLK (bit 0) is LOW.
+ GPIO_HI_IN => 0b111110u, // SS_N=1 (bit1), SD0-SD3=1 (bits2-5), SCLK=0 (bit0)
+ GPIO_OUT => _gpioOut,
+ GPIO_HI_OUT => _gpioHiOut,
+ GPIO_OE => _gpioOe,
+ GPIO_HI_OE => _gpioHiOe,
+ DIV_UDIVIDEND => _divUdividend,
+ DIV_UDIVISOR => _divUdivisor,
+ DIV_SDIVIDEND => (uint)_divSdividend,
+ DIV_SDIVISOR => (uint)_divSdivisor,
+ DIV_QUOTIENT => _divQuotient,
+ DIV_REMAINDER => _divRemainder,
+ DIV_CSR => _divCsr,
+ FIFO_ST => BuildFifoStatus(GetActiveCoreId?.Invoke() ?? 0),
+ FIFO_RD => ReadFifoForCore(GetActiveCoreId?.Invoke() ?? 0),
+ SPINLOCK_ST => _spinLocks,
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ // ── IMemoryMappedDevice — writes ─────────────────────────────────
+
+ public void WriteWord(uint address, uint value)
+ {
+ // Spinlocks — any write releases
+ if (address >= SPINLOCK_BASE && address <= SPINLOCK_END)
+ {
+ _spinLocks &= ~(1u << (int)((address - SPINLOCK_BASE) >> 2));
+ return;
+ }
+
+ // Interpolator 0
+ if (address >= INTERP0_BASE && address < INTERP0_BASE + 0x40)
+ {
+ WriteInterp(ref _interp0, address - INTERP0_BASE, value);
+ return;
+ }
+
+ // Interpolator 1
+ if (address >= INTERP1_BASE && address < INTERP1_BASE + 0x40)
+ {
+ WriteInterp(ref _interp1, address - INTERP1_BASE, value);
+ return;
+ }
+
+ switch (address)
+ {
+ case GPIO_OUT: _gpioOut = value; break;
+ case GPIO_OUT_SET: _gpioOut |= value; break;
+ case GPIO_OUT_CLR: _gpioOut &= ~value; break;
+ case GPIO_OUT_XOR: _gpioOut ^= value; break;
+ case GPIO_OE: _gpioOe = value; break;
+ case GPIO_OE_SET: _gpioOe |= value; break;
+ case GPIO_OE_CLR: _gpioOe &= ~value; break;
+ case GPIO_OE_XOR: _gpioOe ^= value; break;
+ case GPIO_HI_OUT: _gpioHiOut = value; break;
+ case GPIO_HI_OUT_SET: _gpioHiOut |= value; break;
+ case GPIO_HI_OUT_CLR: _gpioHiOut &= ~value; break;
+ case GPIO_HI_OUT_XOR: _gpioHiOut ^= value; break;
+ case GPIO_HI_OE: _gpioHiOe = value; break;
+ case GPIO_HI_OE_SET: _gpioHiOe |= value; break;
+ case GPIO_HI_OE_CLR: _gpioHiOe &= ~value; break;
+ case GPIO_HI_OE_XOR: _gpioHiOe ^= value; break;
+
+ case FIFO_WR:
+ {
+ var coreId = GetActiveCoreId?.Invoke() ?? 0;
+ if (coreId == 0)
+ {
+ if (!_core1Launched)
+ {
+ // Core1 not yet running: handle the RP2040 §2.8.3 launch handshake
+ // natively by echoing each word back so Core0's pop returns immediately.
+ // (_cpu1 is always wired up at construction, so gate on the launch
+ // state — not on a null reference — to actually enter the handshake.)
+ HandleLaunchHandshake(value);
+ }
+ else
+ {
+ // Core0 sends to Core1 (normal FIFO operation after Core1 is live)
+ if (_fifo01.Count < FIFO_DEPTH)
+ {
+ _fifo01.Enqueue(value);
+ _cpu1!.SetInterrupt(16, true); // SIO_IRQ_PROC1 on Core1
+ _cpu1.Registers.EventRegistered = true; // wake WFE
+ }
+ else _wof0 = true;
+ }
+ }
+ else
+ {
+ // Core1 sends to Core0
+ if (_fifo10.Count < FIFO_DEPTH)
+ {
+ _fifo10.Enqueue(value);
+ _cpu.SetInterrupt(15, true); // SIO_IRQ_PROC0 on Core0
+ _cpu.Registers.EventRegistered = true; // wake WFE
+ }
+ else _wof1 = true;
+ }
+ break;
+ }
+ case FIFO_ST:
+ {
+ // Write 1 to clear WOF and ROE
+ var coreId = GetActiveCoreId?.Invoke() ?? 0;
+ if (coreId == 0)
+ {
+ if ((value & FIFO_ST_WOF) != 0) _wof0 = false;
+ if ((value & FIFO_ST_ROE) != 0) _roe0 = false;
+ }
+ else
+ {
+ if ((value & FIFO_ST_WOF) != 0) _wof1 = false;
+ if ((value & FIFO_ST_ROE) != 0) _roe1 = false;
+ }
+ break;
+ }
+
+ case DIV_UDIVIDEND:
+ _divUdividend = value;
+ break;
+ case DIV_UDIVISOR:
+ _divUdivisor = value;
+ _divSigned = false;
+ PerformDivide();
+ break;
+ case DIV_SDIVIDEND:
+ _divSdividend = (int)value;
+ break;
+ case DIV_SDIVISOR:
+ _divSdivisor = (int)value;
+ _divSigned = true;
+ PerformDivide();
+ break;
+ case DIV_QUOTIENT:
+ _divQuotient = value;
+ _divCsr |= 1;
+ break;
+ case DIV_REMAINDER:
+ _divRemainder = value;
+ _divCsr |= 1;
+ break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ // ── Interpolator implementation ──────────────────────────────────
+
+ private static uint ReadInterp(ref InterpState st, uint offset)
+ {
+ return offset switch
+ {
+ INTERP_ACCUM0 => st.Accum0,
+ INTERP_ACCUM1 => st.Accum1,
+ INTERP_BASE0 => st.Base0,
+ INTERP_BASE1 => st.Base1,
+ INTERP_BASE2 => st.Base2,
+ INTERP_CTRL_LANE0 => st.Ctrl0,
+ INTERP_CTRL_LANE1 => st.Ctrl1,
+ INTERP_PEEK_LANE0 => ComputeLane(ref st, 0),
+ INTERP_PEEK_LANE1 => ComputeLane(ref st, 1),
+ INTERP_PEEK_FULL => ComputeFull(ref st),
+ INTERP_POP_LANE0 => PopLane(ref st, 0),
+ INTERP_POP_LANE1 => PopLane(ref st, 1),
+ INTERP_POP_FULL => PopFull(ref st),
+ INTERP_ACCUM0_ADD => st.Accum0,
+ INTERP_ACCUM1_ADD => st.Accum1,
+ _ => 0,
+ };
+ }
+
+ private static void WriteInterp(ref InterpState st, uint offset, uint value)
+ {
+ switch (offset)
+ {
+ case INTERP_ACCUM0: st.Accum0 = value; break;
+ case INTERP_ACCUM1: st.Accum1 = value; break;
+ case INTERP_BASE0: st.Base0 = value; break;
+ case INTERP_BASE1: st.Base1 = value; break;
+ case INTERP_BASE2: st.Base2 = value; break;
+ case INTERP_CTRL_LANE0: st.Ctrl0 = value; break;
+ case INTERP_CTRL_LANE1: st.Ctrl1 = value; break;
+ case INTERP_ACCUM0_ADD: st.Accum0 += value; break;
+ case INTERP_ACCUM1_ADD: st.Accum1 += value; break;
+ case INTERP_BASE_1AND0:
+ // sets BASE0 and BASE1 from a combined 32-bit write:
+ // BASE0 = bits [15:0], BASE1 = bits [31:16]
+ st.Base0 = (ushort)value;
+ st.Base1 = value >> 16;
+ break;
+ }
+ }
+
+ ///
+ /// Compute the primary (pre-CROSS_RESULT) result for one lane.
+ /// Implements the full RP2040 TRM §2.3.1 interpolator pipeline:
+ /// shift → mask → sign-extend → +BASE, with ADD_RAW, BLEND, CLAMP modes.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint ComputeLane(ref InterpState st, int lane)
+ {
+ var ctrl = lane == 0 ? st.Ctrl0 : st.Ctrl1;
+ var shift = (int)(ctrl & 0x1F);
+ var maskLsb = (int)((ctrl >> 5) & 0x1F);
+ var maskMsb = (int)((ctrl >> 10) & 0x1F);
+ var signed = (ctrl & (1u << 15)) != 0;
+ var crossIn = (ctrl & (1u << 16)) != 0;
+ var addRaw = (ctrl & (1u << 20)) != 0; // ADD_RAW: add raw (unshifted) accum to result
+ var blend = (ctrl & (1u << 21)) != 0; // BLEND: linear interpolation (lane 0 ctrl only)
+ var clamp = (ctrl & (1u << 22)) != 0; // CLAMP: clamp result to [BASE0, BASE1]
+
+ uint accum = crossIn
+ ? (lane == 0 ? st.Accum1 : st.Accum0)
+ : (lane == 0 ? st.Accum0 : st.Accum1);
+
+ // BLEND mode (RP2040 TRM §2.3.1.4): only used for lane 0; result = Base0 + alpha*(Base1-Base0)/256
+ // where alpha = accum0[7:0]. Ignores shift/mask/sign.
+ if (blend && lane == 0)
+ {
+ uint alpha = st.Accum0 & 0xFF;
+ // Arithmetic on signed Base values to handle Base1 < Base0 wrap
+ int blended = (int)st.Base0 + (int)((alpha * ((long)(int)st.Base1 - (int)st.Base0)) / 256);
+ return (uint)blended;
+ }
+
+ uint shifted = signed
+ ? (uint)((int)accum >> shift)
+ : accum >> shift;
+
+ uint mask = BuildMask(maskLsb, maskMsb);
+ uint masked = shifted & mask;
+
+ // Sign-extend at maskMsb when SIGNED
+ if (signed && maskMsb < 31)
+ {
+ uint signBit = 1u << maskMsb;
+ if ((masked & signBit) != 0)
+ masked |= ~mask;
+ }
+
+ uint baseVal = lane == 0 ? st.Base0 : st.Base1;
+
+ uint result;
+ if (addRaw)
+ // ADD_RAW: skip mask, add the raw shifted (but not masked) accumulator to BASE
+ result = shifted + baseVal;
+ else
+ result = masked + baseVal;
+
+ // CLAMP (RP2040 TRM §2.3.1.5): only applies to lane 0; clamp to [BASE0, BASE1].
+ // Base0 is the lower bound, Base1 the upper bound (unsigned comparison).
+ if (clamp && lane == 0)
+ {
+ if (result < st.Base0) result = st.Base0;
+ if (result > st.Base1) result = st.Base1;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Compute the FULL result: applies CROSS_RESULT routing and adds BASE2.
+ /// CROSS_RESULT on a lane swaps which lane's primary result feeds into the full output.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint ComputeFull(ref InterpState st)
+ {
+ uint r0 = ComputeLane(ref st, 0);
+ uint r1 = ComputeLane(ref st, 1);
+
+ // CROSS_RESULT (bit 17): if set for lane 0, lane 0's contribution to FULL uses lane 1's result.
+ // If set for lane 1, lane 1's contribution is ignored for FULL (the full result uses lane 0).
+ // Per TRM: FULL = RESULT0 + BASE2, with CROSS_RESULT_0 swapping RESULT0 ↔ RESULT1 for that slot.
+ var crossResult0 = (st.Ctrl0 & (1u << 17)) != 0;
+ uint fullBase = crossResult0 ? r1 : r0;
+ return fullBase + st.Base2;
+ }
+
+ private static uint PopLane(ref InterpState st, int lane)
+ {
+ uint r0 = ComputeLane(ref st, 0);
+ uint r1 = ComputeLane(ref st, 1);
+ // POP writes results back to accumulators (advances the pipeline)
+ st.Accum0 = r0;
+ st.Accum1 = r1;
+ return lane == 0 ? r0 : r1;
+ }
+
+ private static uint PopFull(ref InterpState st)
+ {
+ uint r0 = ComputeLane(ref st, 0);
+ uint r1 = ComputeLane(ref st, 1);
+ st.Accum0 = r0;
+ st.Accum1 = r1;
+ var crossResult0 = (st.Ctrl0 & (1u << 17)) != 0;
+ uint fullBase = crossResult0 ? r1 : r0;
+ return fullBase + st.Base2;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint BuildMask(int lsb, int msb)
+ {
+ if (msb < lsb) return 0;
+ int bits = msb - lsb + 1;
+ uint mask = bits >= 32 ? 0xFFFFFFFF : (1u << bits) - 1;
+ return mask << lsb;
+ }
+
+ // ── Divider ──────────────────────────────────────────────────────
+
+ private void PerformDivide()
+ {
+ _cpu.Cycles += 8;
+ _divCsr = 0x2; // READY, not DIRTY
+
+ if (_divSigned)
+ {
+ if (_divSdivisor == 0)
+ {
+ // RP2040 TRM §2.3.1.6: for signed div-by-zero:
+ // quotient = +1 when dividend >= 0 (0x00000001)
+ // quotient = -1 when dividend < 0 (0xFFFFFFFF)
+ // remainder = dividend in both cases.
+ _divQuotient = _divSdividend >= 0 ? 1u : 0xFFFFFFFF;
+ _divRemainder = (uint)_divSdividend;
+ }
+ else
+ {
+ _divQuotient = (uint)(_divSdividend / _divSdivisor);
+ _divRemainder = (uint)(_divSdividend % _divSdivisor);
+ }
+ }
+ else
+ {
+ if (_divUdivisor == 0)
+ {
+ _divQuotient = 0xFFFFFFFF;
+ _divRemainder = _divUdividend;
+ }
+ else
+ {
+ _divQuotient = _divUdividend / _divUdivisor;
+ _divRemainder = _divUdividend % _divUdivisor;
+ }
+ }
+ }
+
+ // ── Multicore launch handshake ────────────────────────────────────
+
+ ///
+ /// Processes one word of the RP2040 §2.8.3 multicore launch sequence sent by Core0
+ /// while Core1 is not yet running. Each word is echoed back into Core0's RX FIFO
+ /// so Core0's blocking pop returns immediately. When all 6 words of the sequence
+ /// (0, 0, 1, VTOR, SP, Entry) have been received, is
+ /// invoked and the sequence position is reset to allow re-launch if needed.
+ ///
+ private void HandleLaunchHandshake(uint value)
+ {
+ // Validate sequence position (§2.8.3: 0, 0, 1, VTOR, SP, Entry).
+ // Positions 0–2 carry constant magic values that identify a genuine launch attempt;
+ // positions 3–5 carry firmware-specific addresses (VTOR, SP, Entry) that we accept
+ // unconditionally. A mismatch at positions 0–2 resets the counter to let Core0 retry.
+ bool valid = _launchSeqPos switch
+ {
+ 0 or 1 => value == 0, // sync/flush words
+ 2 => value == 1, // magic "ready" sentinel
+ _ => true, // positions 3,4,5: VTOR, SP, Entry (any value valid)
+ };
+
+ if (!valid)
+ {
+ // Mismatch: Core0 is retrying, restart from position 0.
+ _launchSeqPos = 0;
+ // If the new value is 0 it matches position 0; process it.
+ if (value != 0) return;
+ }
+
+ // Store payload words for positions 3–5.
+ switch (_launchSeqPos)
+ {
+ case 3: _launchVtor = value; break;
+ case 4: _launchSp = value; break;
+ case 5: _launchEntry = value; break;
+ }
+
+ // Echo value back to Core0's RX FIFO (simulates Core1's response).
+ if (_fifo10.Count < FIFO_DEPTH)
+ {
+ _fifo10.Enqueue(value);
+ _cpu.SetInterrupt(15, true); // SIO_IRQ_PROC0 on Core0
+ _cpu.Registers.EventRegistered = true; // wake Core0's WFE
+ }
+
+ _launchSeqPos++;
+
+ if (_launchSeqPos == 6)
+ {
+ _launchSeqPos = 0; // reset sequence tracking
+ _core1Launched = true; // subsequent FIFO writes are normal Core0→Core1 traffic
+ OnLaunchCore1?.Invoke(_launchVtor, _launchSp, _launchEntry);
+ }
+ }
+
+ ///
+ /// Clear the multicore launch state so a subsequent §2.8.3 handshake re-launches Core1.
+ /// Called by .
+ ///
+ public void ResetMulticoreLaunch()
+ {
+ _core1Launched = false;
+ _launchSeqPos = 0;
+ }
+
+ // ── FIFO helpers ──────────────────────────────────────────────────
+
+ private uint BuildFifoStatus(int coreId)
+ {
+ if (coreId == 0)
+ {
+ // Core0: RX = fifo10 (Core1→Core0), TX = fifo01 (Core0→Core1)
+ return (_fifo10.Count > 0 ? FIFO_ST_VLD : 0u)
+ | (_fifo01.Count < FIFO_DEPTH ? FIFO_ST_RDY : 0u)
+ | (_wof0 ? FIFO_ST_WOF : 0u)
+ | (_roe0 ? FIFO_ST_ROE : 0u);
+ }
+ else
+ {
+ // Core1: RX = fifo01 (Core0→Core1), TX = fifo10 (Core1→Core0)
+ return (_fifo01.Count > 0 ? FIFO_ST_VLD : 0u)
+ | (_fifo10.Count < FIFO_DEPTH ? FIFO_ST_RDY : 0u)
+ | (_wof1 ? FIFO_ST_WOF : 0u)
+ | (_roe1 ? FIFO_ST_ROE : 0u);
+ }
+ }
+
+ private uint ReadFifoForCore(int coreId)
+ {
+ if (coreId == 0)
+ {
+ if (_fifo10.TryDequeue(out var v)) return v;
+ _roe0 = true;
+ return 0;
+ }
+ else
+ {
+ if (_fifo01.TryDequeue(out var v)) return v;
+ _roe1 = true;
+ return 0;
+ }
+ }
+
+ private uint ReadFifoRx() => ReadFifoForCore(0); // legacy alias for Core0
+
+ ///
+ /// Push a value into Core0's RX FIFO as if Core1 sent it.
+ /// Used by tests and simulated multicore scenarios.
+ ///
+ public void InjectFifoRx(uint value)
+ {
+ if (_fifo10.Count < FIFO_DEPTH)
+ {
+ _fifo10.Enqueue(value);
+ _cpu.SetInterrupt(15, true); // SIO_IRQ_PROC0: notify Core0 data is available
+ }
+ }
+
+ ///
+ /// Drain the TX FIFO (values written by Core0 and "sent" to Core1).
+ ///
+ public bool TryDequeueTx(out uint value) => _fifo01.TryDequeue(out value);
+
+ // ── Spinlocks ─────────────────────────────────────────────────────
+
+ private uint ReadSpinlock(int index)
+ {
+ var bit = 1u << index;
+ if ((_spinLocks & bit) != 0)
+ return 0; // already taken
+
+ _spinLocks |= bit;
+ return bit;
+ }
+}
+
+// ── Interpolator state ────────────────────────────────────────────────────────
+internal struct InterpState
+{
+ public uint Accum0, Accum1;
+ public uint Base0, Base1, Base2;
+ public uint Ctrl0, Ctrl1;
+}
diff --git a/src/RP2040Sharp/Peripherals/Spi/SpiPeripheral.cs b/src/RP2040Sharp/Peripherals/Spi/SpiPeripheral.cs
new file mode 100644
index 0000000..8ef595a
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Spi/SpiPeripheral.cs
@@ -0,0 +1,215 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Spi;
+
+///
+/// RP2040 SPI peripheral (PL022).
+/// SPI0 base: 0x4003C000, SPI1 base: 0x40040000.
+/// TX/RX FIFOs have capacity 8 each. Transfer simulation via injectable callback.
+///
+public sealed class SpiPeripheral : IMemoryMappedDevice
+{
+ private const uint SSPCR0 = 0x000; // Control 0: SCR, SPH, SPO, FRF, DSS
+ private const uint SSPCR1 = 0x004; // Control 1: SOD, MS, SSE, LBM
+ private const uint SSPDR = 0x008; // Data register (FIFO)
+ private const uint SSPSR = 0x00C; // Status
+ private const uint SSPCPSR = 0x010; // Clock prescaler
+ private const uint SSPIMSC = 0x014; // Interrupt mask set/clear
+ private const uint SSPRIS = 0x018; // Raw interrupt status
+ private const uint SSPMIS = 0x01C; // Masked interrupt status
+ private const uint SSPICR = 0x020; // Interrupt clear
+ private const uint SSPDMACR= 0x024; // DMA control
+
+ // PL022 Peripheral ID registers (read-only)
+ private const uint SSPPERIPHID0 = 0xFE0;
+ private const uint SSPPERIPHID1 = 0xFE4;
+ private const uint SSPPERIPHID2 = 0xFE8;
+ private const uint SSPPERIPHID3 = 0xFEC;
+ private const uint SSPPCELLID0 = 0xFF0;
+ private const uint SSPPCELLID1 = 0xFF4;
+ private const uint SSPPCELLID2 = 0xFF8;
+ private const uint SSPPCELLID3 = 0xFFC;
+
+ // SSPSR bits
+ private const uint SR_TFE = 1u << 0; // TX FIFO empty
+ private const uint SR_TNF = 1u << 1; // TX FIFO not full
+ private const uint SR_RNE = 1u << 2; // RX FIFO not empty
+ private const uint SR_RFF = 1u << 3; // RX FIFO full
+ private const uint SR_BSY = 1u << 4; // Busy
+
+ // SSPCR1 bits
+ private const uint CR1_LBM = 1u << 0; // Loopback mode
+ private const uint CR1_SSE = 1u << 1; // SSP enable
+
+ private const int FIFO_DEPTH = 8;
+
+ private readonly CortexM0Plus? _cpu;
+ private readonly int _irq;
+
+ private uint _cr0;
+ private uint _cr1;
+ private uint _cpsr;
+ private uint _imsc;
+ private uint _ris;
+ private uint _dmacr;
+
+ private readonly Queue _txFifo = new(FIFO_DEPTH);
+ private readonly Queue _rxFifo = new(FIFO_DEPTH);
+
+ ///
+ /// Transfer callback. Called with the TX byte/halfword; return value is the RX data.
+ /// If null, RX data is 0.
+ ///
+ public Func? OnTransfer;
+
+ /// DREQ source for DMA RX: true when RX FIFO has data to read.
+ public bool RxDataAvailable => _rxFifo.Count > 0;
+
+ public uint Size => 0x1000;
+
+ public SpiPeripheral(CortexM0Plus? cpu = null, int irq = 0)
+ {
+ _cpu = cpu;
+ _irq = irq;
+ }
+
+ // ── IMemoryMappedDevice ──────────────────────────────────────────
+
+ public uint ReadWord(uint address)
+ {
+ return address switch
+ {
+ SSPCR0 => _cr0,
+ SSPCR1 => _cr1,
+ SSPDR => ReadData(),
+ SSPSR => BuildStatus(),
+ SSPCPSR => _cpsr,
+ SSPIMSC => _imsc,
+ SSPRIS => _ris,
+ SSPMIS => _ris & _imsc,
+ SSPDMACR => _dmacr,
+ SSPPERIPHID0 => 0x22,
+ SSPPERIPHID1 => 0x10,
+ SSPPERIPHID2 => 0x04,
+ SSPPERIPHID3 => 0x00,
+ SSPPCELLID0 => 0x0D,
+ SSPPCELLID1 => 0xF0,
+ SSPPCELLID2 => 0x05,
+ SSPPCELLID3 => 0xB1,
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case SSPCR0: _cr0 = value; break;
+ case SSPCR1:
+ _cr1 = value & 0xF;
+ // TXRIS (bit 3): TX FIFO is always ≤ half full in synchronous simulation.
+ // Set when SSP is enabled; clear when disabled.
+ if (IsEnabled) _ris |= (1u << 3);
+ else _ris &= ~(1u << 3);
+ CheckInterrupts();
+ break;
+ case SSPDR: WriteData((ushort)value); break;
+ case SSPCPSR: _cpsr = value & 0xFE; break; // even values only, bits[7:0]
+ case SSPIMSC:
+ _imsc = value & 0xF;
+ CheckInterrupts();
+ break;
+ case SSPICR:
+ _ris &= ~(value & 0x3); // clear RORIC and RTIC
+ CheckInterrupts();
+ break;
+ case SSPDMACR: _dmacr = value & 0x3; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ // ── Private ──────────────────────────────────────────────────────
+
+ private bool IsEnabled => (_cr1 & CR1_SSE) != 0;
+ private bool IsLoopback => (_cr1 & CR1_LBM) != 0;
+
+ private void WriteData(ushort txData)
+ {
+ if (!IsEnabled || _txFifo.Count >= FIFO_DEPTH)
+ return;
+
+ ushort rxData;
+ if (IsLoopback)
+ {
+ // Loopback: TX data loops back into RX FIFO directly
+ rxData = txData;
+ }
+ else
+ {
+ rxData = OnTransfer?.Invoke(txData) ?? 0;
+ }
+
+ if (_rxFifo.Count < FIFO_DEPTH)
+ _rxFifo.Enqueue(rxData);
+
+ _ris |= 0x4; // RXRIS — RX not empty
+ _ris |= 0x8; // TXRIS — TX FIFO ≤ half full (always true after immediate transfer)
+ CheckInterrupts();
+ }
+
+ private uint ReadData()
+ {
+ if (_rxFifo.TryDequeue(out var data))
+ {
+ if (_rxFifo.Count == 0)
+ {
+ _ris &= ~0x4u; // clear RXRIS
+ CheckInterrupts();
+ }
+ return data;
+ }
+ return 0;
+ }
+
+ private uint BuildStatus()
+ {
+ uint sr = SR_TFE; // TX FIFO always appears empty in simulation (immediate transfer)
+ if (_txFifo.Count < FIFO_DEPTH) sr |= SR_TNF;
+ if (_rxFifo.Count > 0) sr |= SR_RNE;
+ if (_rxFifo.Count >= FIFO_DEPTH) sr |= SR_RFF;
+ return sr;
+ }
+
+ private void CheckInterrupts()
+ {
+ if (_cpu is null) return;
+ _cpu.SetInterrupt(_irq, (_ris & _imsc) != 0);
+ }
+
+ /// Inject a byte into the RX FIFO (simulates incoming data).
+ public void InjectByte(byte value)
+ {
+ if (_rxFifo.Count < FIFO_DEPTH)
+ _rxFifo.Enqueue(value);
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Ssi/SsiPeripheral.cs b/src/RP2040Sharp/Peripherals/Ssi/SsiPeripheral.cs
new file mode 100644
index 0000000..046bef1
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Ssi/SsiPeripheral.cs
@@ -0,0 +1,325 @@
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Ssi;
+
+///
+/// XIP SSI peripheral (0x18000000) with QSPI flash command emulation.
+///
+/// Handles the W25Q/W25X flash command set used by pico-sdk's
+/// flash_range_erase() / flash_range_program() routines:
+///
+/// - 0x06 WRITE_ENABLE
+/// - 0x04 WRITE_DISABLE
+/// - 0x05 READ_STATUS_1→ returns 0x00 (WIP=0, always idle)
+/// - 0x35 READ_STATUS_2→ returns 0x00
+/// - 0x20 SECTOR_ERASE 4 KBfills target sector with 0xFF
+/// - 0x52 BLOCK_ERASE 32 KBfills target block with 0xFF
+/// - 0xD8 BLOCK_ERASE 64 KBfills target block with 0xFF
+/// - 0xC7 / 0x60 CHIP_ERASEfills entire flash with 0xFF
+/// - 0x02 PAGE_PROGRAMwrites up to 256 bytes to flash
+/// - 0x03 READ_DATAstreams flash bytes into the RX FIFO
+/// - 0x0B FAST_READsame with one dummy byte after address
+///
+///
+/// Transaction boundaries are signalled by via
+/// / when the SS OUTOVER
+/// field in IO_QSPI SS CTRL changes. The SER register is also monitored
+/// as a fallback CS source for bootrom / stage-2 code.
+///
+/// The peripheral always reports SR.TFNF | SR.TFE | SR.RFNE so firmware
+/// polling loops complete immediately without timing simulation.
+///
+public sealed unsafe class SsiPeripheral : IMemoryMappedDevice
+{
+ // ── Register offsets ──────────────────────────────────────────────────────
+ private const uint SSI_CTRLR0 = 0x000;
+ private const uint SSI_CTRLR1 = 0x004;
+ private const uint SSI_SSIENR = 0x008;
+ private const uint SSI_MWCR = 0x00C;
+ private const uint SSI_SER = 0x010;
+ private const uint SSI_BAUDR = 0x014;
+ private const uint SSI_TXFTLR = 0x018;
+ private const uint SSI_RXFTLR = 0x01C;
+ private const uint SSI_TXFLR = 0x020;
+ private const uint SSI_RXFLR = 0x024;
+ private const uint SSI_SR = 0x028;
+ private const uint SSI_IMR = 0x02C;
+ private const uint SSI_ISR = 0x030;
+ private const uint SSI_RISR = 0x034;
+ private const uint SSI_ICR = 0x048;
+ private const uint SSI_IDR = 0x058;
+ private const uint SSI_VERSION_ID = 0x05C;
+ private const uint SSI_DR0 = 0x060;
+ private const uint SSI_RX_SAMPLE_DLY = 0x0F0;
+ private const uint SSI_SPI_CTRL_R0 = 0x0F4;
+ private const uint SSI_TXD_DRIVE_EDGE = 0x0F8;
+
+ // ── SR bits ───────────────────────────────────────────────────────────────
+ private const uint SR_TFNF = 1u << 1; // TX FIFO not full
+ private const uint SR_TFE = 1u << 2; // TX FIFO empty
+ private const uint SR_RFNE = 1u << 3; // RX FIFO not empty
+
+ // ── Flash command opcodes ─────────────────────────────────────────────────
+ private const byte CMD_WRITE_ENABLE = 0x06;
+ private const byte CMD_WRITE_DISABLE = 0x04;
+ private const byte CMD_READ_STATUS1 = 0x05;
+ private const byte CMD_READ_STATUS2 = 0x35;
+ private const byte CMD_SECTOR_ERASE = 0x20; // 4 KB
+ private const byte CMD_BLOCK_ERASE32 = 0x52; // 32 KB
+ private const byte CMD_BLOCK_ERASE64 = 0xD8; // 64 KB
+ private const byte CMD_CHIP_ERASE = 0xC7;
+ private const byte CMD_CHIP_ERASE2 = 0x60;
+ private const byte CMD_PAGE_PROGRAM = 0x02;
+ private const byte CMD_READ_DATA = 0x03;
+ private const byte CMD_FAST_READ = 0x0B;
+
+ // ── Registers ─────────────────────────────────────────────────────────────
+ private uint _ctrlr0;
+ private uint _ctrlr1;
+ private uint _ssienr;
+ private uint _ser;
+ private uint _baudr = 2;
+ private uint _spiCtrlr0;
+ private uint _txDriveEdge;
+ private uint _rxSampleDly;
+ private uint _imr;
+
+ // ── Flash reference ───────────────────────────────────────────────────────
+ private byte* _flashPtr;
+ private uint _flashSize;
+
+ // ── Transaction state ─────────────────────────────────────────────────────
+ private bool _csAsserted;
+ private bool _writeEnabled;
+ private readonly List _txBuf = new(260);
+ private readonly Queue _rxQueue = new(260);
+
+ public uint Size => 0x1000;
+
+ // ── Wiring API ────────────────────────────────────────────────────────────
+
+ ///
+ /// Attach the flash memory so write/erase commands are applied in-place.
+ /// Must be called after construction and before any firmware runs.
+ ///
+ public void AttachFlash(byte* flashPtr, uint flashSize)
+ {
+ _flashPtr = flashPtr;
+ _flashSize = flashSize;
+ }
+
+ ///
+ /// Called by when the SS OUTOVER field
+ /// transitions to DRIVE_LOW (2), asserting the active-low chip select.
+ ///
+ public void OnCsAssert()
+ {
+ if (_csAsserted) return; // guard against double-assert
+ _csAsserted = true;
+ _txBuf.Clear();
+ }
+
+ ///
+ /// Called by when SS OUTOVER leaves
+ /// DRIVE_LOW, deasserting the chip select and completing the transaction.
+ ///
+ public void OnCsDeassert()
+ {
+ if (!_csAsserted) return;
+ _csAsserted = false;
+ ProcessTransaction();
+ _txBuf.Clear();
+ }
+
+ // ── IMemoryMappedDevice ───────────────────────────────────────────────────
+
+ public uint ReadWord(uint address) => address switch
+ {
+ SSI_CTRLR0 => _ctrlr0,
+ SSI_CTRLR1 => _ctrlr1,
+ SSI_SSIENR => _ssienr,
+ SSI_SER => _ser,
+ SSI_BAUDR => _baudr,
+ SSI_SR => SR_TFE | SR_RFNE | SR_TFNF, // always ready
+ SSI_RXFLR => (uint)_rxQueue.Count,
+ SSI_IDR => 0x51535049u, // "QSPI" identifier
+ SSI_VERSION_ID => 0x3430312Au,
+ SSI_DR0 => _rxQueue.Count > 0 ? _rxQueue.Dequeue() : 0u,
+ SSI_SPI_CTRL_R0 => _spiCtrlr0,
+ SSI_TXD_DRIVE_EDGE => _txDriveEdge,
+ SSI_RX_SAMPLE_DLY => _rxSampleDly,
+ _ => 0u,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case SSI_CTRLR0: _ctrlr0 = value; break;
+ case SSI_CTRLR1: _ctrlr1 = value; break;
+ case SSI_SSIENR: _ssienr = value; break;
+ case SSI_BAUDR: _baudr = value; break;
+ case SSI_IMR: _imr = value; break;
+ case SSI_SPI_CTRL_R0: _spiCtrlr0 = value; break;
+ case SSI_TXD_DRIVE_EDGE: _txDriveEdge = value; break;
+ case SSI_RX_SAMPLE_DLY: _rxSampleDly = value; break;
+
+ case SSI_SER:
+ {
+ var prev = _ser;
+ _ser = value;
+ // Treat SER 0→non-0 as CS assert and non-0→0 as deassert.
+ // Handles bootrom / stage-2 code that drives CS via SER rather
+ // than IO_QSPI flash_cs_force.
+ if (value != 0 && prev == 0)
+ OnCsAssert();
+ else if (value == 0 && prev != 0)
+ OnCsDeassert();
+ break;
+ }
+
+ case SSI_DR0:
+ // Only accumulate bytes when a transaction is active (CS asserted).
+ if (_csAsserted)
+ {
+ _txBuf.Add((byte)value);
+ _rxQueue.Enqueue(ComputeRxByte());
+ }
+ break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ // ── Transaction helpers ───────────────────────────────────────────────────
+
+ ///
+ /// Compute the RX byte corresponding to the most-recently-added TX byte.
+ /// For read commands (READ_DATA, FAST_READ, READ_STATUS) this returns real data;
+ /// for all other commands firmware ignores the RX so 0x00 is returned.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private byte ComputeRxByte()
+ {
+ if (_txBuf.Count == 0) return 0;
+
+ var pos = _txBuf.Count - 1; // 0-based index of the byte just added
+ var cmd = _txBuf[0];
+
+ switch (cmd)
+ {
+ case CMD_READ_STATUS1:
+ case CMD_READ_STATUS2:
+ // All positions: 0x00 → WIP=0, always idle
+ return 0x00;
+
+ case CMD_READ_DATA when pos >= 4 && _flashPtr != null:
+ {
+ // Layout: [0x03][A2][A1][A0][D0][D1]…
+ var flashAddr = GetAddress24() + (uint)(pos - 4);
+ return flashAddr < _flashSize ? _flashPtr[flashAddr] : (byte)0xFF;
+ }
+
+ case CMD_FAST_READ when pos >= 5 && _flashPtr != null:
+ {
+ // Layout: [0x0B][A2][A1][A0][dummy][D0][D1]…
+ var flashAddr = GetAddress24() + (uint)(pos - 5);
+ return flashAddr < _flashSize ? _flashPtr[flashAddr] : (byte)0xFF;
+ }
+
+ default:
+ return 0x00;
+ }
+ }
+
+ ///
+ /// Apply write/erase operations accumulated in .
+ /// Called when CS is deasserted (end of transaction).
+ /// Read commands have already enqueued their RX bytes via
+ /// ; no additional action is needed for them here.
+ ///
+ private void ProcessTransaction()
+ {
+ if (_txBuf.Count == 0 || _flashPtr == null) return;
+
+ var cmd = _txBuf[0];
+ switch (cmd)
+ {
+ case CMD_WRITE_ENABLE:
+ _writeEnabled = true;
+ break;
+
+ case CMD_WRITE_DISABLE:
+ _writeEnabled = false;
+ break;
+
+ case CMD_SECTOR_ERASE when _writeEnabled && _txBuf.Count >= 4:
+ FlashErase(GetAddress24(), 4u * 1024);
+ _writeEnabled = false;
+ break;
+
+ case CMD_BLOCK_ERASE32 when _writeEnabled && _txBuf.Count >= 4:
+ FlashErase(GetAddress24(), 32u * 1024);
+ _writeEnabled = false;
+ break;
+
+ case CMD_BLOCK_ERASE64 when _writeEnabled && _txBuf.Count >= 4:
+ FlashErase(GetAddress24(), 64u * 1024);
+ _writeEnabled = false;
+ break;
+
+ case CMD_CHIP_ERASE when _writeEnabled:
+ case CMD_CHIP_ERASE2 when _writeEnabled:
+ FlashErase(0, _flashSize);
+ _writeEnabled = false;
+ break;
+
+ case CMD_PAGE_PROGRAM when _writeEnabled && _txBuf.Count >= 4:
+ {
+ var baseAddr = GetAddress24();
+ for (var i = 4; i < _txBuf.Count; i++)
+ {
+ var offset = baseAddr + (uint)(i - 4);
+ if (offset < _flashSize)
+ _flashPtr[offset] = _txBuf[i];
+ }
+ _writeEnabled = false;
+ break;
+ }
+ // READ_DATA / FAST_READ / READ_STATUS: data already enqueued via ComputeRxByte.
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private uint GetAddress24() =>
+ ((uint)_txBuf[1] << 16) | ((uint)_txBuf[2] << 8) | _txBuf[3];
+
+ private void FlashErase(uint addr, uint size)
+ {
+ // Align the start address down to the erase-unit boundary (size must be a power of 2)
+ var start = addr & ~(size - 1);
+ var end = start + size;
+ if (end > _flashSize) end = _flashSize;
+ Unsafe.InitBlock(_flashPtr + start, 0xFF, end - start);
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/SysCfg/SysCfgPeripheral.cs b/src/RP2040Sharp/Peripherals/SysCfg/SysCfgPeripheral.cs
new file mode 100644
index 0000000..47b3537
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/SysCfg/SysCfgPeripheral.cs
@@ -0,0 +1,74 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.SysCfg;
+
+///
+/// SysCfg peripheral (0x40004000).
+/// System configuration registers (NMI masks, processor config, etc.).
+///
+public sealed class SysCfgPeripheral : IMemoryMappedDevice
+{
+ private const uint PROC0_NMI_MASK = 0x00;
+ private const uint PROC1_NMI_MASK = 0x04;
+ private const uint PROC_CONFIG = 0x08;
+ private const uint PROC_IN_SYNC_BYPASS = 0x0C;
+ private const uint PROC_IN_SYNC_BYPASS_HI = 0x10;
+ private const uint DBGFORCE = 0x14;
+ private const uint MEMPOWERDOWN = 0x18;
+
+ private uint _proc0NmiMask;
+ private uint _proc1NmiMask;
+ private uint _procConfig;
+ private uint _procInSyncBypass;
+ private uint _procInSyncBypassHi;
+ private uint _dbgforce;
+ private uint _mempowerdown;
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address) => address switch
+ {
+ PROC0_NMI_MASK => _proc0NmiMask,
+ PROC1_NMI_MASK => _proc1NmiMask,
+ PROC_CONFIG => _procConfig,
+ PROC_IN_SYNC_BYPASS => _procInSyncBypass,
+ PROC_IN_SYNC_BYPASS_HI => _procInSyncBypassHi,
+ DBGFORCE => _dbgforce,
+ MEMPOWERDOWN => _mempowerdown,
+ _ => 0,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case PROC0_NMI_MASK: _proc0NmiMask = value; break;
+ case PROC1_NMI_MASK: _proc1NmiMask = value; break;
+ case PROC_CONFIG: _procConfig = value; break;
+ case PROC_IN_SYNC_BYPASS: _procInSyncBypass = value; break;
+ case PROC_IN_SYNC_BYPASS_HI: _procInSyncBypassHi = value; break;
+ case DBGFORCE: _dbgforce = value; break;
+ case MEMPOWERDOWN: _mempowerdown = value; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/SysInfo/SysInfoPeripheral.cs b/src/RP2040Sharp/Peripherals/SysInfo/SysInfoPeripheral.cs
new file mode 100644
index 0000000..4a46130
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/SysInfo/SysInfoPeripheral.cs
@@ -0,0 +1,37 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.SysInfo;
+
+///
+/// SysInfo peripheral (0x40000000).
+/// Read-only chip identification registers.
+///
+public sealed class SysInfoPeripheral : IMemoryMappedDevice
+{
+ private const uint CHIP_ID = 0x00; // RP2040 chip ID
+ private const uint PLATFORM = 0x04; // 0=FPGA, 1=ASIC, 2=SIMULATION
+ private const uint GITREF_RP2040 = 0x40; // ROM git ref
+
+ // RP2040-B2 chip ID: MANUFACTURER=0x927, PART=0x2, REVISION=2 → 0x10029927
+ private const uint RP2040_CHIP_ID = 0x10029927;
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address) => address switch
+ {
+ CHIP_ID => RP2040_CHIP_ID,
+ PLATFORM => 1, // ASIC
+ GITREF_RP2040 => 0,
+ _ => 0,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value) { }
+ public void WriteHalfWord(uint address, ushort value) { }
+ public void WriteByte(uint address, byte value) { }
+}
diff --git a/src/RP2040Sharp/Peripherals/Tbman/TbmanPeripheral.cs b/src/RP2040Sharp/Peripherals/Tbman/TbmanPeripheral.cs
new file mode 100644
index 0000000..ceba146
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Tbman/TbmanPeripheral.cs
@@ -0,0 +1,29 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Tbman;
+
+///
+/// Testbench Manager peripheral (0x4006C000).
+/// Allows firmware to detect whether it is running on ASIC, FPGA, or simulation.
+///
+public sealed class TbmanPeripheral : IMemoryMappedDevice
+{
+ private const uint PLATFORM = 0x00;
+
+ // ASIC bit is bit 0 (0x1); FPGA bit is bit 1 (0x2). Return ASIC only.
+ private const uint PLATFORM_ASIC = 0x1;
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address) => address == PLATFORM ? PLATFORM_ASIC : 0;
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value) { }
+ public void WriteHalfWord(uint address, ushort value) { }
+ public void WriteByte(uint address, byte value) { }
+}
diff --git a/src/RP2040Sharp/Peripherals/Timer/TimerPeripheral.cs b/src/RP2040Sharp/Peripherals/Timer/TimerPeripheral.cs
new file mode 100644
index 0000000..93a1d89
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Timer/TimerPeripheral.cs
@@ -0,0 +1,175 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Timer;
+
+///
+/// RP2040 Timer peripheral (base 0x40054000).
+/// Maintains a 64-bit microsecond counter driven by .
+/// Four alarms fire when the lower 32 bits of the counter match their values.
+///
+public sealed class TimerPeripheral : IMemoryMappedDevice, ITickable
+{
+ private const uint TIMEHW = 0x000;
+ private const uint TIMELW = 0x004;
+ private const uint TIMEHR = 0x008;
+ private const uint TIMELR = 0x00C;
+ private const uint ALARM0 = 0x010;
+ private const uint ALARM1 = 0x014;
+ private const uint ALARM2 = 0x018;
+ private const uint ALARM3 = 0x01C;
+ private const uint ARMED = 0x020;
+ private const uint TIMERAWH = 0x024;
+ private const uint TIMERAWL = 0x028;
+ private const uint DBGPAUSE = 0x02C;
+ private const uint PAUSE = 0x030;
+ private const uint INTR = 0x034;
+ private const uint INTE = 0x038;
+ private const uint INTF = 0x03C;
+ private const uint INTS = 0x040;
+
+ private readonly CortexM0Plus _cpu;
+ private readonly uint _clkHz;
+
+ // 64-bit microsecond counter (fractional accumulator for sub-us cycles)
+ private long _cycleAccum;
+ private ulong _timeMicros;
+
+ // Latched high word when timelr is read (for consistent 64-bit reads)
+ private uint _latchedHigh;
+
+ private readonly uint[] _alarm = new uint[4];
+ private uint _armed; // bit N = 1 means alarm N is enabled
+ private uint _intr; // raw interrupt status (written 1 to clear)
+ private uint _inte; // interrupt enable
+ private uint _intf; // forced interrupt
+
+ public uint Size => 0x1000;
+
+ public TimerPeripheral(CortexM0Plus cpu, uint clkHz = 125_000_000)
+ {
+ _cpu = cpu;
+ _clkHz = clkHz;
+ }
+
+ // ── ITickable ────────────────────────────────────────────────────
+
+ public void Tick(long deltaCycles)
+ {
+ _cycleAccum += deltaCycles;
+
+ // Convert accumulated cycles to microseconds
+ var us = _cycleAccum * 1_000_000 / _clkHz;
+ if (us <= 0) return;
+
+ _cycleAccum -= us * _clkHz / 1_000_000;
+ _timeMicros += (ulong)us;
+
+ // Check alarms (compare lower 32 bits with wrap-around).
+ // Use unsigned elapsed distance: elapsed = (low - alarm) as uint.
+ // If elapsed < 2^31 the alarm is in the past or at the current moment (fire it).
+ // This correctly handles 32-bit counter wrap-around without false-fires or missed alarms.
+ var low = (uint)_timeMicros;
+ for (var i = 0; i < 4; i++)
+ {
+ if ((_armed & (1u << i)) == 0) continue;
+ if (unchecked(low - _alarm[i]) < 0x80000000u)
+ {
+ _armed &= ~(1u << i);
+ _intr |= (1u << i);
+ if ((_inte & (1u << i)) != 0)
+ _cpu.SetInterrupt(i, true); // Timer IRQ 0-3 = hardware IRQ 0-3
+ }
+ }
+ }
+
+ // ── IMemoryMappedDevice ──────────────────────────────────────────
+
+ public uint ReadWord(uint address)
+ {
+ return address switch
+ {
+ TIMEHW => 0,
+ TIMELW => 0,
+ TIMEHR => _latchedHigh, // returns value latched when TIMELR was read
+ TIMELR =>
+ // Reading TIMELR latches TIMEHR for a coherent 64-bit read
+ (_ = (_latchedHigh = (uint)(_timeMicros >> 32)),
+ (uint)_timeMicros).Item2,
+ TIMERAWH => (uint)(_timeMicros >> 32),
+ TIMERAWL => (uint)_timeMicros,
+ ALARM0 => _alarm[0],
+ ALARM1 => _alarm[1],
+ ALARM2 => _alarm[2],
+ ALARM3 => _alarm[3],
+ ARMED => _armed,
+ INTR => _intr,
+ INTE => _inte,
+ INTF => _intf,
+ INTS => (_intr | _intf) & _inte,
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case TIMEHW:
+ // High word written first; no-op in sim (low write triggers)
+ break;
+ case TIMELW:
+ // Setting timer (e.g. during boot); snap to a specific time
+ _timeMicros = ((ulong)_latchedHigh << 32) | value;
+ break;
+ case ALARM0: WriteAlarm(0, value); break;
+ case ALARM1: WriteAlarm(1, value); break;
+ case ALARM2: WriteAlarm(2, value); break;
+ case ALARM3: WriteAlarm(3, value); break;
+ case ARMED:
+ _armed &= ~value; // write 1 to disarm
+ break;
+ case INTR:
+ _intr &= ~value; // write 1 to clear raw IRQ
+ break;
+ case INTE:
+ _inte = value & 0xF;
+ break;
+ case INTF:
+ _intf = value & 0xF;
+ // Force-trigger interrupts for set bits
+ for (var i = 0; i < 4; i++)
+ if ((_intf & (1u << i)) != 0 && (_inte & (1u << i)) != 0)
+ _cpu.SetInterrupt(i, true);
+ break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ var current = ReadWord(aligned);
+ WriteWord(aligned, (current & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ var current = ReadWord(aligned);
+ WriteWord(aligned, (current & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ private void WriteAlarm(int idx, uint value)
+ {
+ _alarm[idx] = value;
+ _armed |= 1u << idx; // arming happens automatically on alarm write
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Uart/UartPeripheral.cs b/src/RP2040Sharp/Peripherals/Uart/UartPeripheral.cs
new file mode 100644
index 0000000..a592ecf
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Uart/UartPeripheral.cs
@@ -0,0 +1,181 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Uart;
+
+///
+/// PL011 UART peripheral (UART0 base 0x40034000, UART1 base 0x40038000).
+/// For simulation: TX bytes are immediately forwarded to ;
+/// RX bytes come from via a 32-byte FIFO.
+///
+public sealed class UartPeripheral : IMemoryMappedDevice
+{
+ // PL011 register offsets (local addresses from ApbBridge, i.e., address & 0xFFF)
+ private const uint UARTDR = 0x000;
+ private const uint UARTRSR = 0x004; // Receive Status / Error Clear
+ private const uint UARTFR = 0x018; // Flag Register
+ private const uint UARTIBRD = 0x024; // Integer baud-rate divisor
+ private const uint UARTFBRD = 0x028; // Fractional baud-rate divisor
+ private const uint UARTLCR_H = 0x02C; // Line Control
+ private const uint UARTCR = 0x030; // Control Register
+ private const uint UARTIFLS = 0x034; // FIFO level select
+ private const uint UARTIMSC = 0x038; // Interrupt mask set/clear
+ private const uint UARTRIS = 0x03C; // Raw interrupt status
+ private const uint UARTMIS = 0x040; // Masked interrupt status
+ private const uint UARTICR = 0x044; // Interrupt clear
+ private const uint UARTDMACR = 0x048; // DMA control
+
+ // PL011 Peripheral ID registers (read-only, return PL011 signature)
+ private const uint UARTPERIPHID0 = 0xFE0;
+ private const uint UARTPERIPHID1 = 0xFE4;
+ private const uint UARTPERIPHID2 = 0xFE8;
+ private const uint UARTPERIPHID3 = 0xFEC;
+ private const uint UARTPCELLID0 = 0xFF0;
+ private const uint UARTPCELLID1 = 0xFF4;
+ private const uint UARTPCELLID2 = 0xFF8;
+ private const uint UARTPCELLID3 = 0xFFC;
+
+ // UARTFR bits
+ private const uint FR_TXFE = 1u << 7; // TX FIFO empty (1 = idle, buffer empty)
+ private const uint FR_RXFF = 1u << 6; // RX FIFO full
+ private const uint FR_TXFF = 1u << 5; // TX FIFO full
+ private const uint FR_RXFE = 1u << 4; // RX FIFO empty
+ private const uint FR_BUSY = 1u << 3; // UART transmitting
+
+ private readonly CortexM0Plus? _cpu;
+ private readonly int _irq;
+
+ private readonly Queue _rxFifo = new(32);
+ private uint _ibrd, _fbrd, _lcrH, _cr, _imsc, _ifls, _dmacr;
+ private uint _ris; // raw interrupt status
+
+ public uint Size => 0x1000;
+
+ /// Called when a byte is written to UARTDR (TX).
+ public Action? OnByteTransmit;
+
+ public UartPeripheral(CortexM0Plus? cpu = null, int irq = 0)
+ {
+ _cpu = cpu;
+ _irq = irq;
+ _ifls = 0x12; // default: TX at 1/2 full, RX at 1/2 full
+ }
+
+ /// Inject a byte into the RX FIFO (simulates remote device sending data).
+ public void InjectByte(byte value)
+ {
+ if (_rxFifo.Count < 32)
+ {
+ _rxFifo.Enqueue(value);
+ _ris |= (1u << 4); // RXRIS — RX interrupt raw
+ CheckInterrupts();
+ }
+ }
+
+ /// DREQ source for DMA RX: true when RX FIFO has data to read.
+ public bool RxDataAvailable => _rxFifo.Count > 0;
+
+ public uint ReadWord(uint address)
+ {
+ return address switch
+ {
+ UARTDR => ReadData(),
+ UARTRSR => 0, // no errors
+ UARTFR => BuildFr(),
+ UARTIBRD => _ibrd,
+ UARTFBRD => _fbrd,
+ UARTLCR_H => _lcrH,
+ UARTCR => _cr,
+ UARTIFLS => _ifls,
+ UARTIMSC => _imsc,
+ UARTRIS => _ris,
+ UARTMIS => _ris & _imsc,
+ UARTDMACR => _dmacr,
+ UARTPERIPHID0 => 0x11,
+ UARTPERIPHID1 => 0x10,
+ UARTPERIPHID2 => 0x34,
+ UARTPERIPHID3 => 0x00,
+ UARTPCELLID0 => 0x0D,
+ UARTPCELLID1 => 0xF0,
+ UARTPCELLID2 => 0x05,
+ UARTPCELLID3 => 0xB1,
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case UARTDR:
+ OnByteTransmit?.Invoke((byte)(value & 0xFF));
+ _ris |= (1u << 5); // TXRIS — TX interrupt (ready for more data)
+ CheckInterrupts();
+ break;
+ case UARTRSR:
+ // Write any value to clear error flags
+ break;
+ case UARTIBRD: _ibrd = value & 0xFFFF; break;
+ case UARTFBRD: _fbrd = value & 0x3F; break;
+ case UARTLCR_H: _lcrH = value & 0xFF; break;
+ case UARTCR: _cr = value & 0xFFFF; break;
+ case UARTIFLS: _ifls = value & 0x3F; break;
+ case UARTIMSC:
+ _imsc = value & 0x7FF;
+ CheckInterrupts();
+ break;
+ case UARTICR:
+ _ris &= ~value;
+ CheckInterrupts();
+ break;
+ case UARTDMACR: _dmacr = value & 0x7; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ var current = ReadWord(aligned);
+ WriteWord(aligned, (current & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ var current = ReadWord(aligned);
+ WriteWord(aligned, (current & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ private uint ReadData()
+ {
+ if (_rxFifo.Count == 0)
+ return 0;
+ var b = _rxFifo.Dequeue();
+ if (_rxFifo.Count == 0)
+ _ris &= ~(1u << 4); // clear RXRIS when FIFO empties
+ CheckInterrupts();
+ return b;
+ }
+
+ private uint BuildFr()
+ {
+ var fr = FR_TXFE; // TX always idle (immediate transmit in sim)
+ if (_rxFifo.Count == 0) fr |= FR_RXFE;
+ if (_rxFifo.Count >= 32) fr |= FR_RXFF;
+ return fr;
+ }
+
+ private void CheckInterrupts()
+ {
+ if (_cpu is null) return;
+ _cpu.SetInterrupt(_irq, (_ris & _imsc) != 0);
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Usb/UsbCdcHost.cs b/src/RP2040Sharp/Peripherals/Usb/UsbCdcHost.cs
new file mode 100644
index 0000000..d937bb6
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Usb/UsbCdcHost.cs
@@ -0,0 +1,245 @@
+namespace RP2040.Peripherals.Usb;
+
+///
+/// Minimal host-side driver that walks an RP2040 device through USB
+/// enumeration and CDC-ACM activation. Equivalent to rp2040js
+/// src/usb/cdc.ts (USBCDC). Once the device is configured, bytes pushed
+/// via are delivered to the device's bulk-OUT
+/// endpoint, and bytes the firmware writes to the bulk-IN endpoint are
+/// surfaced through .
+///
+/// is fired once SET_CONFIGURATION is
+/// acknowledged.
+///
+public sealed class UsbCdcHost
+{
+ private const byte CDC_REQUEST_SET_CONTROL_LINE_STATE = 0x22;
+ private const byte CDC_DTR = 1 << 0;
+ private const byte CDC_RTS = 1 << 1;
+ private const byte CDC_DATA_CLASS = 10;
+ private const byte ENDPOINT_BULK = 2;
+
+ private const int ENDPOINT_ZERO = 0;
+ private const int CONFIGURATION_DESCRIPTOR_SIZE = 9;
+ private const int TX_FIFO_SIZE = 512;
+
+ private enum DataDirection : byte { HostToDevice = 0, DeviceToHost = 1 }
+ private enum SetupType : byte { Standard = 0, Class = 1, Vendor = 2 }
+ private enum SetupRecipient : byte { Device = 0, Interface = 1, Endpoint = 2 }
+ private enum SetupRequest : byte
+ {
+ SetAddress = 5,
+ GetDescriptor = 6,
+ SetDeviceConfiguration = 9,
+ }
+ private enum DescriptorType : byte
+ {
+ Device = 1,
+ Configuration = 2,
+ Interface = 4,
+ Endpoint = 5,
+ }
+
+ private readonly UsbPeripheral _usb;
+ private readonly Queue _txFifo = new(TX_FIFO_SIZE);
+
+ private bool _initialized;
+ private bool _resumeSignaled;
+ private int? _descriptorsSize;
+ private readonly List _descriptors = new();
+ private int _inEndpoint = -1;
+ private int _outEndpoint = -1;
+
+ /// Raised whenever the device transmits bytes on the CDC bulk-IN endpoint.
+ public Action? OnSerialData;
+ /// Raised once the host has issued SET_CONTROL_LINE_STATE (device is "open").
+ public Action? OnDeviceConnected;
+ /// Raised after SET_CONFIGURATION is acknowledged and CDC line-state is set.
+ public Action? OnConfigurationComplete;
+
+ /// The underlying USB peripheral.
+ public UsbPeripheral Usb => _usb;
+
+ public bool IsConnected => _initialized;
+ public int InEndpoint => _inEndpoint;
+ public int OutEndpoint => _outEndpoint;
+ public int TxFifoCount => _txFifo.Count;
+
+ public UsbCdcHost(UsbPeripheral usb)
+ {
+ _usb = usb;
+ _usb.OnUsbEnabled += HandleUsbEnabled;
+ _usb.OnResetReceived += HandleResetReceived;
+ _usb.OnEndpointWrite += HandleEndpointWrite;
+ _usb.OnEndpointRead += HandleEndpointRead;
+ _usb.OnSof += HandleSof;
+ }
+
+ /// Queue a byte to be delivered to the device on the next bulk-OUT poll.
+ public void SendSerialByte(byte data) => _txFifo.Enqueue(data);
+
+ public void SendSerialBytes(ReadOnlySpan data)
+ {
+ foreach (var b in data) _txFifo.Enqueue(b);
+ }
+
+ private void HandleUsbEnabled() => _usb.SignalBusReset();
+
+ private void HandleResetReceived()
+ {
+ _resumeSignaled = false;
+ _usb.SendSetupPacket(SetDeviceAddressPacket(1));
+ }
+
+ private void HandleEndpointWrite(int endpoint, byte[] buffer)
+ {
+ if (endpoint == ENDPOINT_ZERO && buffer.Length == 0)
+ {
+ if (_descriptorsSize == null)
+ {
+ _usb.SendSetupPacket(GetDescriptorPacket(DescriptorType.Configuration, CONFIGURATION_DESCRIPTOR_SIZE));
+ }
+ else if (!_initialized)
+ {
+ CdcSetControlLineState();
+ OnDeviceConnected?.Invoke();
+ OnConfigurationComplete?.Invoke();
+ // Trigger MicroPython REPL prompt (mirrors rp2040js micropython-run.ts onDeviceConnected)
+ SendSerialByte((byte)'\r');
+ SendSerialByte((byte)'\n');
+ }
+ else if (!_resumeSignaled)
+ {
+ // STATUS ACK for SET_CONTROL_LINE_STATE — signal resume so TinyUSB clears _usbd_dev.suspended.
+ _resumeSignaled = true;
+ _usb.SignalResume();
+ }
+ return;
+ }
+
+ if (endpoint == ENDPOINT_ZERO && buffer.Length > 1)
+ {
+ if (buffer.Length == CONFIGURATION_DESCRIPTOR_SIZE
+ && buffer[1] == (byte)DescriptorType.Configuration
+ && _descriptorsSize == null)
+ {
+ _descriptorsSize = (buffer[3] << 8) | buffer[2];
+ _usb.SendSetupPacket(GetDescriptorPacket(DescriptorType.Configuration, _descriptorsSize.Value));
+ }
+ else if (_descriptorsSize != null && _descriptors.Count < _descriptorsSize)
+ {
+ _descriptors.AddRange(buffer);
+ }
+
+ if (_descriptorsSize == _descriptors.Count)
+ {
+ ExtractEndpointNumbers(_descriptors, out _inEndpoint, out _outEndpoint);
+ _usb.SendSetupPacket(SetDeviceConfigurationPacket(1));
+ }
+ return;
+ }
+
+ if (endpoint == _inEndpoint && buffer.Length > 0)
+ OnSerialData?.Invoke(buffer);
+ }
+
+ private void HandleSof(uint frameNumber)
+ {
+ // SOF fires every 1 ms; periodically re-signal resume (every ~128 ms) so TinyUSB
+ // stays awake if it somehow suspended after the initial RESUME handshake.
+ if (_initialized && (frameNumber & 0x7F) == 0)
+ _usb.SignalResume();
+ }
+
+ private void HandleEndpointRead(int endpoint, int size)
+ {
+ if (endpoint != _outEndpoint) return;
+ var n = Math.Min(size, _txFifo.Count);
+ if (n == 0)
+ {
+ // No data ready — leave the buffer armed; we'll fulfil it next time the
+ // firmware re-arms or whenever bytes become available via SendSerialByte.
+ // Submit a zero-length completion so TinyUSB doesn't stall.
+ _usb.EndpointReadDone(endpoint, ReadOnlySpan.Empty);
+ return;
+ }
+ var buffer = new byte[n];
+ for (var i = 0; i < n; i++) buffer[i] = _txFifo.Dequeue();
+ _usb.EndpointReadDone(endpoint, buffer);
+ }
+
+ private void CdcSetControlLineState(ushort value = CDC_DTR | CDC_RTS, ushort interfaceNumber = 0)
+ {
+ // bmRequestType = 0x21: HostToDevice | Class | Interface (not Device)
+ _usb.SendSetupPacket(CreateSetupPacket(
+ DataDirection.HostToDevice, SetupType.Class, SetupRecipient.Interface,
+ CDC_REQUEST_SET_CONTROL_LINE_STATE, value, interfaceNumber, 0));
+ _initialized = true;
+ }
+
+ // ── Descriptor parsing ───────────────────────────────────────────────
+
+ ///
+ /// Scans the configuration descriptor blob for the CDC data interface and returns its
+ /// bulk IN/OUT endpoint numbers. Each output is -1 when no CDC data endpoint is found.
+ ///
+ public static void ExtractEndpointNumbers(IReadOnlyList descriptors, out int inEp, out int outEp)
+ {
+ inEp = outEp = -1;
+ var index = 0;
+ var curClass = -1;
+ while (index < descriptors.Count)
+ {
+ var len = descriptors[index];
+ if (len < 2 || index + len > descriptors.Count) break;
+ var type = descriptors[index + 1];
+
+ if (type == (byte)DescriptorType.Interface && len >= 9)
+ curClass = descriptors[index + 5];
+
+ if (type == (byte)DescriptorType.Endpoint && len == 7)
+ {
+ var addr = descriptors[index + 2];
+ var attr = descriptors[index + 3];
+ var isIn = (addr & 0x80) != 0;
+ var epNum = addr & 0x0F;
+ var isBulk = (attr & 0x03) == ENDPOINT_BULK;
+ if (curClass == CDC_DATA_CLASS && isBulk)
+ {
+ if (isIn) inEp = epNum; else outEp = epNum;
+ }
+ }
+ index += len;
+ }
+ }
+
+ // ── SETUP packet helpers ─────────────────────────────────────────────
+
+ private static byte[] CreateSetupPacket(
+ DataDirection dir, SetupType type, SetupRecipient recipient,
+ byte bRequest, ushort wValue, ushort wIndex, ushort wLength)
+ {
+ var p = new byte[8];
+ p[0] = (byte)(((byte)dir << 7) | ((byte)type << 5) | (byte)recipient);
+ p[1] = bRequest;
+ p[2] = (byte)(wValue & 0xFF);
+ p[3] = (byte)(wValue >> 8);
+ p[4] = (byte)(wIndex & 0xFF);
+ p[5] = (byte)(wIndex >> 8);
+ p[6] = (byte)(wLength & 0xFF);
+ p[7] = (byte)(wLength >> 8);
+ return p;
+ }
+
+ private static byte[] SetDeviceAddressPacket(ushort address)
+ => CreateSetupPacket(DataDirection.HostToDevice, SetupType.Standard, SetupRecipient.Device,
+ (byte)SetupRequest.SetAddress, address, 0, 0);
+
+ private static byte[] GetDescriptorPacket(DescriptorType type, int length, ushort index = 0)
+ => CreateSetupPacket(DataDirection.DeviceToHost, SetupType.Standard, SetupRecipient.Device,
+ (byte)SetupRequest.GetDescriptor, (ushort)((byte)type << 8), index, (ushort)length);
+
+ private static byte[] SetDeviceConfigurationPacket(ushort configurationNumber)
+ => CreateSetupPacket(DataDirection.HostToDevice, SetupType.Standard, SetupRecipient.Device,
+ (byte)SetupRequest.SetDeviceConfiguration, configurationNumber, 0, 0);
+}
diff --git a/src/RP2040Sharp/Peripherals/Usb/UsbPeripheral.cs b/src/RP2040Sharp/Peripherals/Usb/UsbPeripheral.cs
new file mode 100644
index 0000000..46c07c3
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Usb/UsbPeripheral.cs
@@ -0,0 +1,628 @@
+using RP2040.Core.Cpu;
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Usb;
+
+///
+/// RP2040 USB Controller (USBCTRL) — device mode + CDC enumeration support.
+/// Memory map (AHB slot 1, bits [27:20] = 0x01):
+/// 0x50100000 - 0x50100FFF : DPRAM (4 KB)
+/// 0x50110000 - 0x50110FFF : Controller registers (USBCTRL_REGS)
+///
+/// The device-side endpoint FSM is sufficient to drive TinyUSB through enumeration
+/// and bulk CDC-ACM transfers. Companion host driver lives in .
+/// Equivalent to rp2040js: src/peripherals/usb.ts (device mode subset).
+///
+public sealed class UsbPeripheral : IMemoryMappedDevice, IHandlesAtomicAliases, ITickable
+{
+ private const int USB_IRQ = 5;
+
+ // ── DPRAM (4 KB) ──────────────────────────────────────────────────────
+ private const uint DPRAM_BASE = 0x50100000u;
+ private const uint DPRAM_SIZE = 0x1000u;
+
+ // ── REGS base offset from the AHB slot base ───────────────────────────
+ private const uint REGS_OFFSET = 0x10000u;
+
+ // ── DPRAM offsets ─────────────────────────────────────────────────────
+ private const uint EP1_IN_CONTROL = 0x008;
+ private const uint EP0_IN_BUFFER_CONTROL = 0x080;
+ private const uint EP0_OUT_BUFFER_CONTROL = 0x084;
+ private const uint EP15_OUT_BUFFER_CONTROL = 0x0FC;
+ private const uint EP0_BUFFER = 0x100;
+
+ // EP buffer-control bits
+ private const uint USB_BUF_CTRL_AVAILABLE = 1u << 10;
+ private const uint USB_BUF_CTRL_FULL = 1u << 15;
+ private const uint USB_BUF_CTRL_LEN_MASK = 0x3FFu;
+
+ // INTR bits (subset)
+ private const uint INTR_BUFF_STATUS = 1u << 4;
+ private const uint INTR_BUS_RESET = 1u << 12;
+ private const uint INTR_DEV_CONN_DIS = 1u << 13;
+ private const uint INTR_DEV_SUSPEND = 1u << 14;
+ private const uint INTR_DEV_RESUME = 1u << 15;
+ private const uint INTR_DEV_SOF = 1u << 17;
+ private const uint INTR_SETUP_REQ = 1u << 16;
+
+ // SIE_STATUS bits (subset)
+ private const uint SIE_VBUS_DETECTED = 1u << 0;
+ private const uint SIE_RESUME = 1u << 11;
+ private const uint SIE_CONNECTED = 1u << 16;
+ private const uint SIE_SETUP_REC = 1u << 17;
+ private const uint SIE_BUS_RESET = 1u << 19;
+
+ // MAIN_CTRL bits
+ private const uint MAIN_CTRL_CONTROLLER_EN = 1u << 0;
+ private const uint MAIN_CTRL_HOST_NDEVICE = 1u << 1;
+
+ // ── Register offsets within REGS region ──────────────────────────────
+ private const uint R_ADDR_ENDP0 = 0x000;
+ private const uint R_MAIN_CTRL = 0x040;
+ private const uint R_SOF_RW = 0x044;
+ private const uint R_SOF_RD = 0x048;
+ private const uint R_SIE_CTRL = 0x04C;
+ private const uint R_SIE_STATUS = 0x050;
+ private const uint R_INT_EP_CTRL = 0x054;
+ private const uint R_BUFF_STATUS = 0x058;
+ private const uint R_BUFF_CPU_SHOULD_HANDLE = 0x05C;
+ private const uint R_EP_ABORT = 0x060;
+ private const uint R_EP_ABORT_DONE = 0x064;
+ private const uint R_EP_STALL_ARM = 0x068;
+ private const uint R_NAK_POLL = 0x06C;
+ private const uint R_EP_STATUS_STALL_NAK = 0x070;
+ private const uint R_USB_MUXING = 0x074;
+ private const uint R_USB_PWR = 0x078;
+ private const uint R_USBPHY_DIRECT = 0x07C;
+ private const uint R_USBPHY_DIRECT_OVERRIDE = 0x080;
+ private const uint R_USBPHY_TRIM = 0x084;
+ private const uint R_INTR = 0x08C;
+ private const uint R_INTE = 0x090;
+ private const uint R_INTF = 0x094;
+ private const uint R_INTS = 0x098;
+
+ // ── Fields ────────────────────────────────────────────────────────────
+ private readonly CortexM0Plus? _cpu;
+ private readonly byte[] _dpram = new byte[DPRAM_SIZE];
+
+ private readonly uint[] _addrEndp = new uint[16];
+
+ private uint _mainCtrl;
+ private uint _sofRw;
+ private uint _sofRd;
+ private uint _sieCtrl;
+ private uint _sieStatus;
+ private uint _intEpCtrl;
+ private uint _buffStatus;
+ private uint _buffCpuShouldHandle;
+ private uint _epAbort;
+ private uint _epAbortDone;
+ private uint _epStallArm;
+ private uint _nakPoll;
+ private uint _epStatusStallNak;
+ private uint _usbMuxing;
+ private uint _usbPwr;
+ private uint _usbphyDirect;
+ private uint _usbphyDirectOverride;
+ private uint _usbphyTrim = 0x04040000u;
+ private uint _intr;
+ private uint _inte;
+ private uint _intf;
+
+ private bool _controllerEnabled;
+ private bool _hostMode;
+ private bool _prevSieConnected;
+
+ public uint Size => 0x20000u;
+
+ // ── Device-mode callbacks ────────────────────────────────────────────
+ /// Fired the first time the firmware sets MAIN_CTRL.CONTROLLER_EN in device mode.
+ public Action? OnUsbEnabled;
+ /// Fired when firmware acknowledges a bus reset (writes SIE_BUS_RESET as W1C).
+ public Action? OnResetReceived;
+ /// Fired when firmware completes an IN transfer (data flowing device → host).
+ public Action? OnEndpointWrite;
+ /// Fired when firmware arms an OUT endpoint (host → device); host should call .
+ public Action? OnEndpointRead;
+ /// Fired on every simulated Start-of-Frame (1 ms intervals when controller enabled in device mode).
+ public Action? OnSof;
+
+ public UsbPeripheral(CortexM0Plus? cpu = null)
+ {
+ _cpu = cpu;
+ }
+
+ ///
+ /// Reset USB peripheral state (called when RESETS.RESET.USBCTRL is cycled).
+ /// Clears all registers and re-arms the controller-enable trigger so that
+ /// the next dcd_init() call fires OnUsbEnabled and restarts enumeration.
+ ///
+ public void Reset()
+ {
+ Array.Clear(_dpram);
+ Array.Clear(_addrEndp);
+ _mainCtrl = 0;
+ _sofRw = 0;
+ _sieCtrl = 0;
+ _sieStatus = 0;
+ _intEpCtrl = 0;
+ _buffStatus = 0;
+ _buffCpuShouldHandle = 0;
+ _epAbort = 0;
+ _epAbortDone = 0;
+ _epStallArm = 0;
+ _nakPoll = 0;
+ _epStatusStallNak = 0;
+ _usbMuxing = 0;
+ _usbPwr = 0;
+ _usbphyDirect = 0;
+ _usbphyDirectOverride = 0;
+ _usbphyTrim = 0x04040000u;
+ _intr = 0;
+ _inte = 0;
+ _intf = 0;
+ _controllerEnabled = false;
+ _hostMode = false;
+ _prevSieConnected = false;
+ _pendingWrites.Clear();
+ _pendingReads.Clear();
+ }
+
+ // ── IMemoryMappedDevice ───────────────────────────────────────────────
+
+ public uint ReadWord(uint address)
+ {
+ var offset = address & 0x1FFFFu;
+
+ if (offset < DPRAM_SIZE)
+ return ReadDpramWord(offset);
+
+ // Strip atomic-alias bits (12–13) — reads always return the base register value.
+ var reg = (offset & ~0x3000u) - REGS_OFFSET;
+ return reg switch
+ {
+ var r when r < 0x040 => _addrEndp[r >> 2],
+
+ R_MAIN_CTRL => _mainCtrl,
+ R_SOF_RW => _sofRw,
+ R_SOF_RD => _sofRd,
+ R_SIE_CTRL => _sieCtrl,
+ R_SIE_STATUS => _sieStatus,
+ R_INT_EP_CTRL => _intEpCtrl,
+ R_BUFF_STATUS => _buffStatus,
+ R_BUFF_CPU_SHOULD_HANDLE => _buffCpuShouldHandle,
+ R_EP_ABORT => _epAbort,
+ R_EP_ABORT_DONE => _epAbortDone,
+ R_EP_STALL_ARM => _epStallArm,
+ R_NAK_POLL => _nakPoll,
+ R_EP_STATUS_STALL_NAK => _epStatusStallNak,
+ R_USB_MUXING => _usbMuxing,
+ R_USB_PWR => _usbPwr,
+ R_USBPHY_DIRECT => _usbphyDirect,
+ R_USBPHY_DIRECT_OVERRIDE => _usbphyDirectOverride,
+ R_USBPHY_TRIM => _usbphyTrim,
+ R_INTR => _intr,
+ R_INTE => _inte,
+ R_INTF => _intf,
+ R_INTS => (_intr | _intf) & _inte,
+ _ => 0u,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address)
+ {
+ var offset = address & 0x1FFFFu;
+ if (offset < DPRAM_SIZE)
+ {
+ var aligned = offset & ~1u;
+ var shift = (int)(offset & 1) << 3;
+ return (ushort)(ReadDpramWord(aligned & ~3u) >> shift);
+ }
+ return (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+ }
+
+ public byte ReadByte(uint address)
+ {
+ var offset = address & 0x1FFFFu;
+ if (offset < DPRAM_SIZE)
+ return _dpram[offset];
+ return (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+ }
+
+ public void WriteWord(uint address, uint value)
+ {
+ var offset = address & 0x1FFFFu;
+
+ if (offset < DPRAM_SIZE)
+ {
+ WriteDpramWord(offset & ~3u, value);
+ if (offset >= EP0_IN_BUFFER_CONTROL && offset <= EP15_OUT_BUFFER_CONTROL)
+ DpramUpdated(offset & ~3u, value);
+ return;
+ }
+
+ // Extract and strip atomic-alias type (bits 12–13) from the offset.
+ // UsbPeripheral implements IHandlesAtomicAliases, so the AHB bridge passes the
+ // raw firmware value. We must apply the transform ourselves for R/W registers,
+ // while W1C registers treat `value` as the bitmask of bits to clear regardless
+ // of atomic type (both direct writes and atomic-clear alias use the same mask).
+ var atomicType = (offset >> 12) & 0x3u;
+ offset &= ~0x3000u; // strip alias bits to get the base register offset
+
+ var reg = offset - REGS_OFFSET;
+ switch (reg)
+ {
+ case var r when r < 0x040:
+ // R/W — apply atomic transform
+ _addrEndp[r >> 2] = ApplyAtomic(_addrEndp[r >> 2], value, atomicType) & (0x07FF_0000u | 0xFFu);
+ break;
+
+ case R_MAIN_CTRL:
+ {
+ var v = ApplyAtomic(_mainCtrl, value, atomicType) & 0xC0000003u;
+ _mainCtrl = v;
+ _hostMode = (v & MAIN_CTRL_HOST_NDEVICE) != 0;
+ if ((v & MAIN_CTRL_CONTROLLER_EN) != 0 && !_controllerEnabled)
+ {
+ _controllerEnabled = true;
+ if (!_hostMode) OnUsbEnabled?.Invoke();
+ }
+ }
+ break;
+ case R_SOF_RW: _sofRw = ApplyAtomic(_sofRw, value, atomicType) & 0x7FFu; break;
+ case R_SIE_CTRL: _sieCtrl = ApplyAtomic(_sieCtrl, value, atomicType); break;
+
+ case R_SIE_STATUS:
+ {
+ // W1C register: `value` is the raw firmware-written bitmask of bits to clear.
+ // Both direct writes and atomic-clear alias use the same semantics here.
+ var clearMask = value;
+ if ((clearMask & SIE_BUS_RESET) != 0 && !_hostMode)
+ OnResetReceived?.Invoke();
+ _sieStatus &= ~clearMask;
+ SieStatusUpdated();
+ }
+ break;
+
+ case R_INT_EP_CTRL: _intEpCtrl = ApplyAtomic(_intEpCtrl, value, atomicType); break;
+ // W1C registers — value is always the bitmask of bits to clear.
+ case R_BUFF_STATUS: _buffStatus &= ~value; BuffStatusUpdated(); break;
+ case R_BUFF_CPU_SHOULD_HANDLE: _buffCpuShouldHandle &= ~value; break;
+ case R_EP_ABORT:
+ {
+ var v = ApplyAtomic(_epAbort, value, atomicType);
+ _epAbort = v; _epAbortDone |= v;
+ }
+ break;
+ case R_EP_ABORT_DONE: _epAbortDone &= ~value; break; // W1C
+ case R_EP_STALL_ARM: _epStallArm = ApplyAtomic(_epStallArm, value, atomicType); break;
+ case R_NAK_POLL: _nakPoll = ApplyAtomic(_nakPoll, value, atomicType); break;
+ case R_EP_STATUS_STALL_NAK: _epStatusStallNak &= ~value; break; // W1C
+ case R_USB_MUXING:
+ {
+ var v = ApplyAtomic(_usbMuxing, value, atomicType);
+ _usbMuxing = v;
+ // pico-sdk hw_enumeration_fix waits for SIE_CONNECTED after rerouting muxing
+ if ((v & 0b0100) != 0 && (v & 0b0001) == 0)
+ _sieStatus |= SIE_CONNECTED;
+ }
+ break;
+ case R_USB_PWR:
+ {
+ var v = ApplyAtomic(_usbPwr, value, atomicType);
+ _usbPwr = v;
+ // VBUS detect override
+ if ((v & (1u << 2)) != 0)
+ {
+ if ((v & (1u << 3)) != 0) _sieStatus |= SIE_VBUS_DETECTED;
+ else _sieStatus &= ~SIE_VBUS_DETECTED;
+ }
+ }
+ break;
+ case R_USBPHY_DIRECT: _usbphyDirect = ApplyAtomic(_usbphyDirect, value, atomicType); break;
+ case R_USBPHY_DIRECT_OVERRIDE: _usbphyDirectOverride = ApplyAtomic(_usbphyDirectOverride, value, atomicType); break;
+ case R_USBPHY_TRIM: _usbphyTrim = ApplyAtomic(_usbphyTrim, value, atomicType); break;
+ case R_INTR: _intr &= ~value; CheckInterrupts(); break; // W1C
+ case R_INTE: _inte = ApplyAtomic(_inte, value, atomicType); CheckInterrupts(); break;
+ case R_INTF: _intf = ApplyAtomic(_intf, value, atomicType); CheckInterrupts(); break;
+ }
+ }
+
+ private static uint ApplyAtomic(uint current, uint value, uint atomicType) => atomicType switch
+ {
+ 1u => current ^ value, // XOR
+ 2u => current | value, // SET
+ 3u => current & ~value, // CLR
+ _ => value, // normal write
+ };
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var offset = address & 0x1FFFFu;
+ if (offset < DPRAM_SIZE)
+ {
+ _dpram[offset & ~1u] = (byte)(value & 0xFF);
+ _dpram[(offset & ~1u)+1] = (byte)(value >> 8);
+ // EP buffer-control writes can be 16-bit too; route through the 32-bit path
+ var alignedW = offset & ~3u;
+ DpramUpdated(alignedW, ReadDpramWord(alignedW));
+ return;
+ }
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ var current = ReadWord(aligned);
+ WriteWord(aligned, (current & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var offset = address & 0x1FFFFu;
+ if (offset < DPRAM_SIZE)
+ {
+ _dpram[offset] = value;
+ var alignedW = offset & ~3u;
+ DpramUpdated(alignedW, ReadDpramWord(alignedW));
+ return;
+ }
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ var current = ReadWord(aligned);
+ WriteWord(aligned, (current & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ // ── Public host-side helpers ─────────────────────────────────────────
+
+ /// Simulate a bus reset received by the device. Sets BUS_RESET in SIE_STATUS.
+ public void SignalBusReset()
+ {
+ _sieStatus |= SIE_BUS_RESET | SIE_CONNECTED;
+ SieStatusUpdated();
+ }
+
+ ///
+ /// Signal a host-initiated resume to the device. Sets SIE_STATUS.RESUME which maps to
+ /// INTR.DEV_RESUME_FROM_HOST, clearing _usbd_dev.suspended in TinyUSB.
+ ///
+ public void SignalResume()
+ {
+ _sieStatus |= SIE_RESUME;
+ SieStatusUpdated();
+ }
+
+ ///
+ /// Signal a USB Start-of-Frame. Sets INTR.DEV_SOF directly (not via SIE_STATUS).
+ /// Used to decrement TinyUSB's cdc_connected_flush_delay counter.
+ ///
+ public void SignalSof(uint frameNumber)
+ {
+ // SOF frame number is in SOF_RD register (R_SOF_RD offset 0x048)
+ _sofRd = frameNumber & 0x7FF;
+ _intr |= INTR_DEV_SOF;
+ CheckInterrupts();
+ OnSof?.Invoke(frameNumber);
+ }
+
+ /// Simulate an isolated SETUP_REC bit assertion (no payload).
+ public void SignalSetupPacket()
+ {
+ _sieStatus |= SIE_SETUP_REC;
+ SieStatusUpdated();
+ }
+
+ /// Inject an 8-byte SETUP packet into DPRAM[0..8] and raise SETUP_REC.
+ public void SendSetupPacket(ReadOnlySpan setup)
+ {
+ if (setup.Length != 8) throw new ArgumentException("SETUP packet must be 8 bytes", nameof(setup));
+ setup.CopyTo(_dpram.AsSpan(0, 8));
+ _sieStatus |= SIE_SETUP_REC;
+ SieStatusUpdated();
+ }
+
+ /// Provide data for an OUT endpoint that the firmware previously armed.
+ public void EndpointReadDone(int endpoint, ReadOnlySpan data)
+ {
+ // Defer DPRAM write + IndicateBufferReady to Tick() after READ_DELAY_CYCLES.
+ // Matches rp2040js endpointReadAlarms: without this delay, TinyUSB immediately re-arms
+ // the endpoint from within the BUFF_STATUS ISR, creating a tight loop that eventually
+ // fires BUFF_STATUS with ep->active=false and panics.
+ var buffBit = endpoint * 2 + 1; // OUT = odd bit
+ if (!_pendingReads.TryGetValue(buffBit, out var q))
+ _pendingReads[buffBit] = q = new Queue<(int, byte[], long)>();
+ q.Enqueue((endpoint, data.ToArray(), _totalCycles + READ_DELAY_CYCLES));
+ }
+
+ /// Copy into DPRAM at .
+ public void WriteDpram(uint dpramOffset, ReadOnlySpan data)
+ {
+ if (dpramOffset + data.Length > DPRAM_SIZE)
+ throw new ArgumentOutOfRangeException(nameof(dpramOffset));
+ data.CopyTo(_dpram.AsSpan((int)dpramOffset));
+ }
+
+ /// Read bytes from DPRAM at .
+ public byte[] ReadDpram(uint dpramOffset, int length)
+ {
+ var result = new byte[length];
+ _dpram.AsSpan((int)dpramOffset, length).CopyTo(result);
+ return result;
+ }
+
+ // Pending IN-transfer completions: keyed by BUFF_STATUS bit, queued to preserve ordering
+ // when firmware sends multiple packets in the same CPU batch (e.g. 64-byte + 11-byte for a
+ // 75-byte descriptor). We defer OnEndpointWrite until Tick() delivers after a short delay,
+ // mimicking rp2040js's writeDelayMicroseconds alarm mechanism.
+ private readonly Dictionary> _pendingWrites = new();
+ private const long WRITE_DELAY_CYCLES = 625L; // ~5 µs at 125 MHz, matching rp2040js default
+
+ // Pending OUT-transfer completions: deferred to Tick() so IndicateBufferReady fires after
+ // a delay rather than synchronously inside DpramUpdated. Matches rp2040js's readDelayMicroseconds
+ // alarm, which prevents a tight re-arm loop that would cause ep->active=false panics in TinyUSB.
+ private readonly Dictionary> _pendingReads = new();
+ private const long READ_DELAY_CYCLES = 625L; // ~5 µs at 125 MHz, matching rp2040js default
+
+ private long _totalCycles;
+
+ // ── DPRAM endpoint FSM ───────────────────────────────────────────────
+
+ private void DpramUpdated(uint offset, uint value)
+ {
+ if (_hostMode) return;
+ if ((value & USB_BUF_CTRL_AVAILABLE) == 0) return;
+ if (offset < EP0_IN_BUFFER_CONTROL || offset > EP15_OUT_BUFFER_CONTROL) return;
+
+ var endpoint = (int)((offset - EP0_IN_BUFFER_CONTROL) >> 3);
+ var isOut = (offset & 4) != 0;
+ var bufLen = (int)(value & USB_BUF_CTRL_LEN_MASK);
+ var bufferOffset = GetEndpointBufferOffset(endpoint, isOut);
+
+ // Consume AVAILABLE flag
+ value &= ~USB_BUF_CTRL_AVAILABLE;
+
+ if (isOut)
+ {
+ WriteDpramWord(offset, value);
+ OnEndpointRead?.Invoke(endpoint, bufLen);
+ }
+ else
+ {
+ // IN: data flows device → host. Capture buffer, clear FULL, indicate ready.
+ value &= ~USB_BUF_CTRL_FULL;
+ WriteDpramWord(offset, value);
+ var buffer = new byte[bufLen];
+ _dpram.AsSpan((int)bufferOffset, bufLen).CopyTo(buffer);
+ // Store pending write to be delivered by Tick() after WRITE_DELAY_CYCLES.
+ // This ensures the firmware ISR returns before the host responds with next SETUP.
+ var buffBit = endpoint * 2; // IN = even bit
+ if (!_pendingWrites.TryGetValue(buffBit, out var q))
+ _pendingWrites[buffBit] = q = new Queue<(int, byte[], long)>();
+ q.Enqueue((endpoint, buffer, _totalCycles + WRITE_DELAY_CYCLES));
+ IndicateBufferReady(endpoint, isOut: false);
+ }
+ }
+
+ private uint GetEndpointBufferOffset(int endpoint, bool out_)
+ {
+ if (endpoint == 0) return EP0_BUFFER;
+ var ctrlOffset = EP1_IN_CONTROL + 8u * (uint)(endpoint - 1) + (out_ ? 4u : 0u);
+ return ReadDpramWord(ctrlOffset) & 0xFFC0u;
+ }
+
+ private void IndicateBufferReady(int endpoint, bool isOut)
+ {
+ _buffStatus |= 1u << (endpoint * 2 + (isOut ? 1 : 0));
+ BuffStatusUpdated();
+ }
+
+ private void BuffStatusUpdated()
+ {
+ if (_buffStatus != 0) _intr |= INTR_BUFF_STATUS;
+ else _intr &= ~INTR_BUFF_STATUS;
+ CheckInterrupts();
+ }
+
+ private void SieStatusUpdated()
+ {
+ // Map SIE_STATUS bits to INTR bits (device-mode subset).
+ SyncIntrBit(SIE_SETUP_REC, INTR_SETUP_REQ);
+ SyncIntrBit(SIE_BUS_RESET, INTR_BUS_RESET);
+ // DEV_CONN_DIS is edge-triggered: pulse INTR only on the 0→1 transition of SIE_CONNECTED.
+ // A level mapping causes the bit to re-assert after every W1C, creating an IRQ storm.
+ var sieConnected = (_sieStatus & SIE_CONNECTED) != 0;
+ if (sieConnected && !_prevSieConnected)
+ _intr |= INTR_DEV_CONN_DIS;
+ _prevSieConnected = sieConnected;
+ SyncIntrBit(SIE_RESUME, INTR_DEV_RESUME);
+ CheckInterrupts();
+ }
+
+ private void SyncIntrBit(uint sieBit, uint intrBit)
+ {
+ if ((_sieStatus & sieBit) != 0) _intr |= intrBit;
+ else _intr &= ~intrBit;
+ }
+
+ private uint ReadDpramWord(uint byteOffset)
+ {
+ var i = (int)(byteOffset & ~3u);
+ return (uint)(_dpram[i] | (_dpram[i+1] << 8) | (_dpram[i+2] << 16) | (_dpram[i+3] << 24));
+ }
+
+ private void WriteDpramWord(uint byteOffset, uint value)
+ {
+ var i = (int)(byteOffset & ~3u);
+ _dpram[i] = (byte) value;
+ _dpram[i+1] = (byte)(value >> 8);
+ _dpram[i+2] = (byte)(value >> 16);
+ _dpram[i+3] = (byte)(value >> 24);
+ }
+
+ private void CheckInterrupts()
+ {
+ if (_cpu == null) return;
+ var pending = ((_intr | _intf) & _inte) != 0;
+ _cpu.SetInterrupt(USB_IRQ, pending);
+ }
+
+ ///
+ /// Re-check interrupt state (called after NVIC_ICPR cleared the pending bit
+ /// so that level-triggered USB IRQ can re-assert via NVIC_ISER enable).
+ ///
+ public void RecheckInterrupts() => CheckInterrupts();
+
+ private const long SOF_PERIOD_CYCLES = 125_000L; // 1ms at 125 MHz (USB full-speed SOF rate)
+ private long _lastSofCycles = long.MinValue / 2;
+ private uint _sofFrameCount;
+
+ /// Advance cycle counter; deliver any pending IN-transfer callbacks whose delay has elapsed.
+ public void Tick(long deltaCycles)
+ {
+ _totalCycles += deltaCycles;
+
+ // Auto-clear SOF bit after one tick so it doesn't re-trigger indefinitely.
+ _intr &= ~INTR_DEV_SOF;
+
+ // Emit periodic SOF when USB controller is enabled in device mode.
+ // The host generates SOFs unconditionally regardless of whether the device has the SOF interrupt enabled.
+ if (_controllerEnabled && !_hostMode)
+ {
+ if (_totalCycles - _lastSofCycles >= SOF_PERIOD_CYCLES)
+ {
+ _lastSofCycles = _totalCycles;
+ _sofFrameCount = (_sofFrameCount + 1) & 0x7FF;
+ SignalSof(_sofFrameCount);
+ }
+ }
+
+ if (_pendingWrites.Count == 0 && _pendingReads.Count == 0) return;
+
+ foreach (var (bit, queue) in _pendingWrites.ToArray())
+ {
+ while (queue.Count > 0 && _totalCycles >= queue.Peek().deliverAt)
+ {
+ var (ep, buf, _) = queue.Dequeue();
+ if (queue.Count == 0) _pendingWrites.Remove(bit);
+ OnEndpointWrite?.Invoke(ep, buf);
+ }
+ }
+
+ foreach (var (bit, queue) in _pendingReads.ToArray())
+ {
+ while (queue.Count > 0 && _totalCycles >= queue.Peek().deliverAt)
+ {
+ var (ep, buf, _) = queue.Dequeue();
+ if (queue.Count == 0) _pendingReads.Remove(bit);
+ var bufCtrlReg = EP0_OUT_BUFFER_CONTROL + (uint)ep * 8;
+ var bufCtrl = ReadDpramWord(bufCtrlReg);
+ var requestedLen = (int)(bufCtrl & USB_BUF_CTRL_LEN_MASK);
+ var newLen = Math.Min(buf.Length, requestedLen);
+ var bufferOffset = GetEndpointBufferOffset(ep, out_: true);
+ buf.AsSpan(0, newLen).CopyTo(_dpram.AsSpan((int)bufferOffset, newLen));
+ bufCtrl |= USB_BUF_CTRL_FULL;
+ bufCtrl = (bufCtrl & ~USB_BUF_CTRL_LEN_MASK) | ((uint)newLen & USB_BUF_CTRL_LEN_MASK);
+ WriteDpramWord(bufCtrlReg, bufCtrl);
+ IndicateBufferReady(ep, isOut: true);
+ }
+ }
+ }
+}
+
diff --git a/src/RP2040Sharp/Peripherals/Vreg/VregPeripheral.cs b/src/RP2040Sharp/Peripherals/Vreg/VregPeripheral.cs
new file mode 100644
index 0000000..a9d34e2
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Vreg/VregPeripheral.cs
@@ -0,0 +1,57 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Vreg;
+
+///
+/// Voltage Regulator and Chip Reset peripheral stub (0x40064000).
+/// Reports VREG voltage at default 1.1V. Chip reset reason returns 0.
+///
+public sealed class VregPeripheral : IMemoryMappedDevice
+{
+ private const uint VREG = 0x00; // voltage selection
+ private const uint BOD = 0x04; // brownout detection
+ private const uint CHIP_RESET = 0x08; // chip reset reason
+
+ // VREG default: 1.1V (VSEL=0b1011)
+ private uint _vreg = 0x0B_00; // VSEL=11, EN=1 in bits [8:4]
+ private uint _bod = 0x0091; // brownout enabled at ~1.0V
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address) => address switch
+ {
+ VREG => _vreg,
+ BOD => _bod,
+ CHIP_RESET => 0, // no reset reason
+ _ => 0,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case VREG: _vreg = value; break;
+ case BOD: _bod = value; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Watchdog/WatchdogPeripheral.cs b/src/RP2040Sharp/Peripherals/Watchdog/WatchdogPeripheral.cs
new file mode 100644
index 0000000..93570c0
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Watchdog/WatchdogPeripheral.cs
@@ -0,0 +1,157 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Watchdog;
+
+///
+/// Watchdog peripheral (0x40058000).
+/// Implements SCRATCH registers (used by pico-sdk to pass boot info),
+/// TICK generator, and watchdog countdown timer.
+/// When CTRL_ENABLE (bit 30) is set, the timer counts down from LOAD.
+/// Fires (and sets REASON_TIMER) when it reaches zero.
+///
+public sealed class WatchdogPeripheral : IMemoryMappedDevice, ITickable
+{
+ private const uint CTRL = 0x00;
+ private const uint LOAD = 0x04;
+ private const uint REASON = 0x08;
+ private const uint SCRATCH0 = 0x0C;
+ private const uint SCRATCH7 = 0x28; // SCRATCH0 + 7*4
+ private const uint TICK = 0x2C;
+
+ // CTRL bits
+ private const uint CTRL_TRIGGER = 1u << 31;
+ private const uint CTRL_ENABLE = 1u << 30;
+ private const uint CTRL_PAUSE_DBG0 = 1u << 26;
+ private const uint CTRL_PAUSE_DBG1 = 1u << 25;
+ private const uint CTRL_PAUSE_JTAG = 1u << 24;
+ private const uint CTRL_TIME_MASK = 0x00FFFFFF; // bits [23:0] = remaining time (µs × 2)
+
+ // REASON bits (per RP2040 TRM §4.7.6 and rp2040js)
+ private const uint REASON_TIMER = 1u << 0; // bit 0: watchdog countdown reached zero
+ private const uint REASON_FORCE = 1u << 1; // bit 1: CTRL[TRIGGER] was written
+
+ // TICK bits: [8:0] = CYCLES (divider), [9] = ENABLE, [10] = RUNNING, [19:11] = COUNT
+ private const uint TICK_RUNNING = 1u << 10;
+ private const uint TICK_ENABLE = 1u << 9;
+
+ // Per RP2040-E1 errata: the watchdog timer decrements at 2 MHz (twice per expected tick).
+ // 125 MHz / 2 MHz = 62.5 cycles per decrement tick → use 62 (slight fast-side bias is safe).
+ // Note: simulation keeps the logical rate at 1 tick = 1 µs (125 CPU cycles) so that firmware
+ // load values map intuitively to microseconds. The errata doubles the real-hardware rate but
+ // pico-sdk compensates internally; MicroPython integration tests validate end-to-end timing.
+ private const long CYCLES_PER_WDOG_TICK = 125;
+
+ private uint _ctrl;
+ private uint _load;
+ private uint _reason;
+ private uint _tick = TICK_ENABLE | TICK_RUNNING | 12; // enabled, running, 12 cycles (default)
+ private readonly uint[] _scratch = new uint[8];
+
+ private long _accumUs; // accumulated sub-microsecond cycles
+ private uint _countDown; // current countdown in µs×2 (mirrors CTRL[23:0])
+
+ /// Invoked when the watchdog timer expires. Simulate a system reset here.
+ public Action? OnReset { get; set; }
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address)
+ {
+ if (address >= SCRATCH0 && address <= SCRATCH7)
+ return _scratch[(address - SCRATCH0) >> 2];
+
+ return address switch
+ {
+ CTRL => (_ctrl & ~CTRL_TIME_MASK) | (_countDown & CTRL_TIME_MASK),
+ LOAD => _load,
+ REASON => _reason,
+ TICK => _tick,
+ _ => 0,
+ };
+ }
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ if (address >= SCRATCH0 && address <= SCRATCH7)
+ {
+ _scratch[(address - SCRATCH0) >> 2] = value;
+ return;
+ }
+
+ switch (address)
+ {
+ case CTRL:
+ // TRIGGER is a write-only strobe — writing it forces an immediate reset
+ if ((value & CTRL_TRIGGER) != 0)
+ {
+ _reason = REASON_FORCE;
+ OnReset?.Invoke();
+ }
+ _ctrl = value & ~(CTRL_TRIGGER | CTRL_TIME_MASK); // TRIGGER and TIME are not stored
+ // Loading a new CTRL with ENABLE: reload countdown from LOAD
+ if ((value & CTRL_ENABLE) != 0)
+ {
+ _countDown = _load & CTRL_TIME_MASK;
+ _accumUs = 0;
+ }
+ break;
+ case LOAD:
+ _load = value & CTRL_TIME_MASK;
+ // Writing LOAD also reloads the running countdown (matches hardware)
+ _countDown = _load;
+ _accumUs = 0;
+ break;
+ case TICK:
+ // ENABLE bit and CYCLES field; RUNNING and COUNT are read-only status
+ _tick = (_tick & ~0x3FFu) | (value & 0x3FF);
+ _tick = (_tick & ~TICK_RUNNING) | ((value & TICK_ENABLE) != 0 ? TICK_RUNNING : 0u);
+ break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+
+ // ── ITickable ─────────────────────────────────────────────────────────
+
+ public void Tick(long deltaCycles)
+ {
+ if ((_ctrl & CTRL_ENABLE) == 0) return;
+ if (_countDown == 0) return;
+
+ _accumUs += deltaCycles;
+ var ticks = _accumUs / CYCLES_PER_WDOG_TICK; // decrement ticks at 2 MHz per RP2040-E1
+ _accumUs %= CYCLES_PER_WDOG_TICK;
+
+ if (ticks <= 0) return;
+
+ if (ticks >= _countDown)
+ {
+ _countDown = 0;
+ _reason = REASON_TIMER;
+ _ctrl &= ~CTRL_ENABLE; // hardware clears ENABLE on reset
+ OnReset?.Invoke();
+ }
+ else
+ {
+ _countDown -= (uint)ticks;
+ }
+ }
+}
diff --git a/src/RP2040Sharp/Peripherals/Xosc/XoscPeripheral.cs b/src/RP2040Sharp/Peripherals/Xosc/XoscPeripheral.cs
new file mode 100644
index 0000000..98d0965
--- /dev/null
+++ b/src/RP2040Sharp/Peripherals/Xosc/XoscPeripheral.cs
@@ -0,0 +1,68 @@
+using RP2040.Core.Memory;
+
+namespace RP2040.Peripherals.Xosc;
+
+///
+/// Crystal Oscillator peripheral (0x40024000).
+/// In simulation the crystal is always stable and enabled.
+///
+public sealed class XoscPeripheral : IMemoryMappedDevice
+{
+ private const uint XOSC_CTRL = 0x00; // FREQ_RANGE, ENABLE
+ private const uint XOSC_STATUS = 0x04; // STABLE(31), ENABLED(12), FREQ_RANGE(1:0)
+ private const uint XOSC_DORMANT = 0x08; // dormancy control
+ private const uint XOSC_STARTUP = 0x0C; // startup delay
+ private const uint XOSC_COUNT = 0x1C; // countdown
+
+ private const uint CTRL_ENABLE_VALUE = 0xFAB000; // enable magic value (bits [23:12])
+ private const uint CTRL_DISABLE_VALUE = 0xD1E000;
+
+ private const uint STATUS_STABLE = 1u << 31;
+ private const uint STATUS_ENABLED = 1u << 12;
+
+ private uint _ctrl = CTRL_ENABLE_VALUE | 0xAA0; // enabled, 1–15 MHz range
+ private uint _dormant = 0;
+ private uint _startup = 0xC4; // default startup delay
+
+ public uint Size => 0x1000;
+
+ public uint ReadWord(uint address) => address switch
+ {
+ XOSC_CTRL => _ctrl,
+ XOSC_STATUS => STATUS_STABLE | STATUS_ENABLED | 0xAA0, // always stable
+ XOSC_DORMANT => _dormant,
+ XOSC_STARTUP => _startup,
+ XOSC_COUNT => 0,
+ _ => 0,
+ };
+
+ public ushort ReadHalfWord(uint address) =>
+ (ushort)(ReadWord(address & ~3u) >> (int)((address & 2) << 3));
+
+ public byte ReadByte(uint address) =>
+ (byte)(ReadWord(address & ~3u) >> (int)((address & 3) << 3));
+
+ public void WriteWord(uint address, uint value)
+ {
+ switch (address)
+ {
+ case XOSC_CTRL: _ctrl = value; break;
+ case XOSC_DORMANT: _dormant = value; break; // dormancy ignored in sim
+ case XOSC_STARTUP: _startup = value; break;
+ }
+ }
+
+ public void WriteHalfWord(uint address, ushort value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 2) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFFFu << shift)) | ((uint)value << shift));
+ }
+
+ public void WriteByte(uint address, byte value)
+ {
+ var aligned = address & ~3u;
+ var shift = (int)((address & 3) << 3);
+ WriteWord(aligned, (ReadWord(aligned) & ~(0xFFu << shift)) | ((uint)value << shift));
+ }
+}
diff --git a/src/RP2040.Core/RP2040.Core.csproj b/src/RP2040Sharp/RP2040Sharp.csproj
similarity index 51%
rename from src/RP2040.Core/RP2040.Core.csproj
rename to src/RP2040Sharp/RP2040Sharp.csproj
index 487ab0f..59fee5a 100644
--- a/src/RP2040.Core/RP2040.Core.csproj
+++ b/src/RP2040Sharp/RP2040Sharp.csproj
@@ -1,13 +1,17 @@
-
+
+ RP2040Sharp
+ Cycle-accurate RP2040 emulator: Cortex-M0+ CPU, BusInterconnect, and all peripherals. Single-package, AOT-compatible.
net10.0
enable
- enable
- true
true
true
true
true
embedded
+
+
+
+
diff --git a/src/RP2040Sharp/bootrom_b1.bin b/src/RP2040Sharp/bootrom_b1.bin
new file mode 100644
index 0000000..603b253
Binary files /dev/null and b/src/RP2040Sharp/bootrom_b1.bin differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/PicoExamplesFirmware.cs b/tests/RP2040Sharp.IntegrationTests/Firmware/PicoExamplesFirmware.cs
new file mode 100644
index 0000000..0ace86f
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Firmware/PicoExamplesFirmware.cs
@@ -0,0 +1,140 @@
+namespace RP2040Sharp.IntegrationTests.Firmware;
+
+///
+/// Pre-compiled RP2040 firmware images (raw UF2 bytes) embedded as assembly resources.
+/// These binaries were compiled with pico-sdk 2.1.0 for the Pico board.
+///
+/// Use to convert to
+/// raw flash bytes before loading into a simulation.
+///
+internal static class PicoExamplesFirmware
+{
+ // --- GPIO / Blink ---
+
+ /// blink/blink.uf2 — GPIO 25 blinks at 1 Hz (500 ms ON / 500 ms OFF).
+ public static byte[] Blink => Load("Firmware.gpio.blink.uf2");
+
+ /// blink_simple/blink_simple.uf2 — Minimal GPIO 25 blink.
+ public static byte[] BlinkSimple => Load("Firmware.gpio.blink_simple.uf2");
+
+ /// gpio/hello_gpio_irq — GPIO interrupt-driven button example.
+ public static byte[] HelloGpioIrq => Load("Firmware.gpio.hello_gpio_irq.uf2");
+
+ // --- UART ---
+
+ /// hello_world/serial — "Hello, World!" over UART0.
+ public static byte[] HelloSerial => Load("Firmware.uart.hello_serial.uf2");
+
+ /// uart/hello_uart — UART peripheral demo with configurable baud rate.
+ public static byte[] HelloUart => Load("Firmware.uart.hello_uart.uf2");
+
+ // --- USB ---
+
+ /// hello_world/usb — "Hello, World!" over USB CDC.
+ public static byte[] HelloUsb => Load("Firmware.usb.hello_usb.uf2");
+
+ // --- Timer ---
+
+ /// timer/hello_timer — Repeating timer callbacks via SDK timer API.
+ public static byte[] HelloTimer => Load("Firmware.timer.hello_timer.uf2");
+
+ /// timer/timer_lowlevel — Direct hardware timer register access.
+ public static byte[] TimerLowlevel => Load("Firmware.timer.timer_lowlevel.uf2");
+
+ // --- PWM ---
+
+ /// pwm/hello_pwm — Basic PWM output on GPIO.
+ public static byte[] HelloPwm => Load("Firmware.pwm.hello_pwm.uf2");
+
+ /// pwm/led_fade — PWM LED brightness fade using slice wrap/level.
+ public static byte[] PwmLedFade => Load("Firmware.pwm.pwm_led_fade.uf2");
+
+ // --- DMA ---
+
+ /// dma/hello_dma — Basic DMA memory-to-memory copy.
+ public static byte[] HelloDma => Load("Firmware.dma.hello_dma.uf2");
+
+ /// dma/channel_irq — DMA transfer completion interrupt.
+ public static byte[] DmaChannelIrq => Load("Firmware.dma.dma_channel_irq.uf2");
+
+ // --- Watchdog ---
+
+ /// watchdog/hello_watchdog — Watchdog timer scratch/reboot demo.
+ public static byte[] HelloWatchdog => Load("Firmware.watchdog.hello_watchdog.uf2");
+
+ // --- RTC ---
+
+ /// rtc/hello_rtc — RTC time/date set and read via UART.
+ public static byte[] HelloRtc => Load("Firmware.rtc.hello_rtc.uf2");
+
+ /// rtc/rtc_alarm — RTC one-shot alarm callback.
+ public static byte[] RtcAlarm => Load("Firmware.rtc.rtc_alarm.uf2");
+
+ // --- Multicore ---
+
+ /// multicore/hello_multicore — Launch code on core 1 via SIO FIFO.
+ public static byte[] HelloMulticore => Load("Firmware.multicore.hello_multicore.uf2");
+
+ /// multicore/multicore_fifo_irqs — Inter-core FIFO IRQ communication.
+ public static byte[] MulticoreFifoIrqs => Load("Firmware.multicore.multicore_fifo_irqs.uf2");
+
+ // --- PIO ---
+
+ /// pio/hello_pio — Minimal PIO blink program, state machine setup.
+ public static byte[] HelloPio => Load("Firmware.pio.hello_pio.uf2");
+
+ /// pio/pio_blink — PIO-driven GPIO blink with configurable period.
+ public static byte[] PioBlink => Load("Firmware.pio.pio_blink.uf2");
+
+ /// pio/uart_tx — PIO UART transmitter (8N1).
+ public static byte[] PioUartTx => Load("Firmware.pio.pio_uart_tx.uf2");
+
+ // --- Interpolator ---
+
+ /// interp/hello_interp — SIO interpolator lanes, accumulators and base offsets.
+ public static byte[] HelloInterp => Load("Firmware.interp.hello_interp.uf2");
+
+ // --- Hardware Divider ---
+
+ /// divider/hello_divider — SIO hardware divider (signed/unsigned).
+ public static byte[] HelloDivider => Load("Firmware.divider.hello_divider.uf2");
+
+ // --- ADC ---
+
+ /// adc/hello_adc — ADC channel read and print via UART.
+ public static byte[] HelloAdc => Load("Firmware.adc.hello_adc.uf2");
+
+ /// adc/onboard_temperature — Internal temperature sensor via ADC4.
+ public static byte[] OnboardTemperature => Load("Firmware.adc.onboard_temperature.uf2");
+
+ // --- Clocks ---
+
+ /// clocks/hello_48MHz — Reconfigure system clock to 48 MHz.
+ public static byte[] Hello48MHz => Load("Firmware.clocks.hello_48MHz.uf2");
+
+ // --- Reset ---
+
+ /// reset/hello_reset — Peripheral reset subsystem demo.
+ public static byte[] HelloReset => Load("Firmware.reset.hello_reset.uf2");
+
+ // --- System ---
+
+ /// system/unique_board_id — Read unique board ID from flash via SSI/DMA.
+ public static byte[] UniqueBoardId => Load("Firmware.system.unique_board_id.uf2");
+
+ // ── private loader ────────────────────────────────────────────────────────
+
+ private static byte[] Load(string resourceSuffix)
+ {
+ var asm = typeof(PicoExamplesFirmware).Assembly;
+ var name = $"RP2040Sharp.IntegrationTests.{resourceSuffix}";
+ using var stream = asm.GetManifestResourceStream(name)
+ ?? throw new InvalidOperationException(
+ $"Embedded firmware not found: '{name}'. " +
+ "Ensure the .uf2 file is present in the Firmware/ directory and " +
+ "the project has .");
+ var buf = new byte[stream.Length];
+ _ = stream.Read(buf, 0, buf.Length);
+ return buf;
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/adc/hello_adc.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/adc/hello_adc.uf2
new file mode 100644
index 0000000..54030ea
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/adc/hello_adc.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/adc/onboard_temperature.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/adc/onboard_temperature.uf2
new file mode 100644
index 0000000..a370e25
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/adc/onboard_temperature.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/clocks/hello_48MHz.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/clocks/hello_48MHz.uf2
new file mode 100644
index 0000000..f4970e3
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/clocks/hello_48MHz.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/divider/hello_divider.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/divider/hello_divider.uf2
new file mode 100644
index 0000000..b501c7a
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/divider/hello_divider.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/dma/dma_channel_irq.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/dma/dma_channel_irq.uf2
new file mode 100644
index 0000000..6034949
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/dma/dma_channel_irq.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/dma/hello_dma.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/dma/hello_dma.uf2
new file mode 100644
index 0000000..b2567c6
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/dma/hello_dma.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/gpio/blink.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/gpio/blink.uf2
new file mode 100644
index 0000000..6075379
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/gpio/blink.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/gpio/blink_simple.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/gpio/blink_simple.uf2
new file mode 100644
index 0000000..0ed546f
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/gpio/blink_simple.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/gpio/hello_gpio_irq.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/gpio/hello_gpio_irq.uf2
new file mode 100644
index 0000000..73c0478
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/gpio/hello_gpio_irq.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/interp/hello_interp.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/interp/hello_interp.uf2
new file mode 100644
index 0000000..afebc6a
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/interp/hello_interp.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/multicore/hello_multicore.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/multicore/hello_multicore.uf2
new file mode 100644
index 0000000..e42a06a
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/multicore/hello_multicore.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/multicore/multicore_fifo_irqs.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/multicore/multicore_fifo_irqs.uf2
new file mode 100644
index 0000000..7a2183f
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/multicore/multicore_fifo_irqs.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/pio/hello_pio.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/pio/hello_pio.uf2
new file mode 100644
index 0000000..1093662
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/pio/hello_pio.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/pio/pio_blink.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/pio/pio_blink.uf2
new file mode 100644
index 0000000..4dd4318
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/pio/pio_blink.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/pio/pio_uart_tx.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/pio/pio_uart_tx.uf2
new file mode 100644
index 0000000..45d35fb
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/pio/pio_uart_tx.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/pwm/hello_pwm.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/pwm/hello_pwm.uf2
new file mode 100644
index 0000000..5b8ca07
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/pwm/hello_pwm.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/pwm/pwm_led_fade.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/pwm/pwm_led_fade.uf2
new file mode 100644
index 0000000..133d5c0
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/pwm/pwm_led_fade.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/reset/hello_reset.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/reset/hello_reset.uf2
new file mode 100644
index 0000000..4383669
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/reset/hello_reset.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/rtc/hello_rtc.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/rtc/hello_rtc.uf2
new file mode 100644
index 0000000..e8b1b42
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/rtc/hello_rtc.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/rtc/rtc_alarm.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/rtc/rtc_alarm.uf2
new file mode 100644
index 0000000..091d07b
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/rtc/rtc_alarm.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/system/unique_board_id.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/system/unique_board_id.uf2
new file mode 100644
index 0000000..d965b15
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/system/unique_board_id.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/timer/hello_timer.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/timer/hello_timer.uf2
new file mode 100644
index 0000000..50a0edc
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/timer/hello_timer.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/timer/timer_lowlevel.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/timer/timer_lowlevel.uf2
new file mode 100644
index 0000000..100a9c9
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/timer/timer_lowlevel.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/uart/hello_serial.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/uart/hello_serial.uf2
new file mode 100644
index 0000000..fbd1f6a
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/uart/hello_serial.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/uart/hello_uart.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/uart/hello_uart.uf2
new file mode 100644
index 0000000..649ca82
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/uart/hello_uart.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/usb/hello_usb.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/usb/hello_usb.uf2
new file mode 100644
index 0000000..f92f738
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/usb/hello_usb.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/watchdog/hello_watchdog.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/watchdog/hello_watchdog.uf2
new file mode 100644
index 0000000..72de544
Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/watchdog/hello_watchdog.uf2 differ
diff --git a/tests/RP2040Sharp.IntegrationTests/Infrastructure/CircuitPythonRunner.cs b/tests/RP2040Sharp.IntegrationTests/Infrastructure/CircuitPythonRunner.cs
new file mode 100644
index 0000000..e00d3c0
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Infrastructure/CircuitPythonRunner.cs
@@ -0,0 +1,466 @@
+using RP2040.TestKit;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Probes;
+
+namespace RP2040Sharp.IntegrationTests.Infrastructure;
+
+///
+/// High-level runner that boots CircuitPython on the RP2040 emulator and exposes a REPL
+/// interface for driving tests via UART injection.
+///
+/// CircuitPython routes its REPL through USB-CDC (TinyUSB) when USB is available,
+/// falling back to the hardware UART otherwise. The runner detects which transport
+/// the REPL prompt appears on and routes all subsequent I/O through the same channel.
+///
+/// Usage:
+///
+/// await using var cp = await CircuitPythonRunner.CreateAsync("9.2.1");
+/// cp.Should().NotBeNull("firmware should be available");
+///
+/// bool booted = cp.WaitForPrompt();
+/// booted.Should().BeTrue();
+///
+/// cp.Execute("print('hello')");
+/// cp.WaitForOutput("hello").Should().BeTrue();
+///
+///
+public sealed class CircuitPythonRunner : IAsyncDisposable
+{
+ private readonly PicoSimulation _sim;
+
+ // CircuitPython also routes its REPL through USB-CDC when available.
+ private bool _replViaUsbCdc;
+
+ public UartProbe Uart => _sim.Uart0;
+ public UsbCdcProbe UsbCdc => _sim.UsbCdc;
+ public PicoSimulation Simulation => _sim;
+
+ private CircuitPythonRunner(PicoSimulation sim)
+ {
+ _sim = sim;
+ }
+
+ ///
+ /// Create a runner loaded with CircuitPython (e.g. "9.2.1").
+ /// Returns null when the firmware is not available (no network / not cached).
+ ///
+ /// CircuitPython version string.
+ ///
+ /// When true (default) the simulation includes a USB-CDC host, giving access to
+ /// the USB REPL but also causing CircuitPython to lock the CIRCUITPY filesystem
+ /// read-only (USB-MSC prevents Python code from writing).
+ /// Pass false to run without a USB host: CircuitPython falls back to the
+ /// UART0 REPL and the filesystem remains writable from Python code.
+ ///
+ public static async Task CreateAsync(string version, bool withUsbCdc = true)
+ {
+ var uf2Path = await FirmwareCache.GetCircuitPythonAsync(version);
+ if (uf2Path is null)
+ return null;
+
+ var uf2Bytes = await File.ReadAllBytesAsync(uf2Path);
+ var flashImage = Uf2Reader.ToFlashImage(uf2Bytes);
+
+ var sim = new PicoSimulation(withUsbCdc);
+ sim.LoadFlash(flashImage);
+ return new CircuitPythonRunner(sim);
+ }
+
+ // ── REPL helpers ──────────────────────────────────────────────────────
+
+ ///
+ /// Run the simulation until the CircuitPython REPL prompt (>>> ) appears on
+ /// UART or USB-CDC, or until elapses.
+ /// CircuitPython may also emit a "Press any key to enter the REPL" message before the
+ /// prompt; this helper sends a key press automatically when that message is detected.
+ ///
+ public bool WaitForPrompt(double timeoutMs = 20_000)
+ {
+ const double batchMs = 100.0;
+ var elapsed = 0.0;
+ var keySent = false;
+ while (elapsed < timeoutMs)
+ {
+ _sim.RunMilliseconds(batchMs);
+ elapsed += batchMs;
+
+ // CircuitPython may ask for a keypress to enter the REPL when no code.py exists.
+ // Only send it once — the text is accumulated so "Press any key" stays visible
+ // across iterations and we must not flood the fifo with extra keypresses.
+ if (!keySent)
+ {
+ if (UsbCdc.Text.Contains("Press any key", StringComparison.OrdinalIgnoreCase))
+ {
+ UsbCdc.InjectString("\r");
+ keySent = true;
+ }
+ else if (Uart.Text.Contains("Press any key", StringComparison.OrdinalIgnoreCase))
+ {
+ Uart.InjectString("\r");
+ keySent = true;
+ }
+ }
+
+ if (Uart.Text.Contains(">>> ", StringComparison.Ordinal))
+ {
+ _replViaUsbCdc = false;
+ // Run one extra batch so any pending USB endpoint reads that
+ // accumulated during the wait are drained before the caller
+ // injects the next command. Without this drain, the first
+ // Execute() after WaitForPrompt can be partially swallowed by
+ // a stale ZLP→re-arm cycle in the USB peripheral.
+ _sim.RunMilliseconds(100);
+ return true;
+ }
+ if (UsbCdc.Text.Contains(">>> ", StringComparison.Ordinal))
+ {
+ _replViaUsbCdc = true;
+ _sim.RunMilliseconds(100); // same drain on the CDC path
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Inject a line of Python code into the REPL (appends \r\n).
+ /// Call first to ensure the REPL is ready.
+ ///
+ public void Execute(string pythonLine)
+ {
+ if (_replViaUsbCdc)
+ {
+ UsbCdc.Clear();
+ UsbCdc.InjectString(pythonLine + "\r\n");
+ }
+ else
+ {
+ Uart.Clear();
+ Uart.InjectString(pythonLine + "\r\n");
+ }
+ }
+
+ ///
+ /// Run the simulation until appears in the output
+ /// captured since the last call.
+ ///
+ public bool WaitForOutput(string expectedText, double timeoutMs = 5_000) =>
+ _replViaUsbCdc
+ ? _sim.RunUntilOutput(UsbCdc, expectedText, timeoutMs)
+ : _sim.RunUntilOutput(Uart, expectedText, timeoutMs);
+
+ ///
+ /// Inject a Python line and wait for .
+ /// Returns true if the expected text appeared before the timeout.
+ ///
+ public bool ExecuteAndWait(string pythonLine, string expectedOutput, double timeoutMs = 5_000)
+ {
+ Execute(pythonLine);
+ return WaitForOutput(expectedOutput, timeoutMs);
+ }
+
+ ///
+ /// Run simulation in batches until over the output text returns true.
+ ///
+ public bool WaitForOutput(Func predicate, double timeoutMs = 5_000) =>
+ _replViaUsbCdc
+ ? _sim.RunUntilOutput(UsbCdc, predicate, timeoutMs)
+ : _sim.RunUntilOutput(Uart, predicate, timeoutMs);
+
+ ///
+ /// Inject a compound statement (def, for, class, if, etc.) into
+ /// the REPL. Sends the statement, waits for the ... continuation prompt, then sends
+ /// a blank line to execute it, and finally waits for the next >>> prompt.
+ /// Returns true if the REPL prompt was seen before the timeout.
+ ///
+ public bool ExecuteCompound(string pythonLine, double timeoutMs = 15_000)
+ {
+ if (_replViaUsbCdc)
+ {
+ UsbCdc.Clear();
+ UsbCdc.InjectString(pythonLine + "\r\n");
+ }
+ else
+ {
+ Uart.Clear();
+ Uart.InjectString(pythonLine + "\r\n");
+ }
+
+ const double batchMs = 100.0;
+ var elapsed = 0.0;
+ var gotContinuation = false;
+ while (elapsed < timeoutMs)
+ {
+ _sim.RunMilliseconds(batchMs);
+ var text = _replViaUsbCdc ? UsbCdc.Text : Uart.Text;
+ if (text.Contains(">>> ", StringComparison.Ordinal)) return true;
+ if (text.Contains("... ", StringComparison.Ordinal)) { gotContinuation = true; break; }
+ elapsed += batchMs;
+ }
+
+ if (!gotContinuation) return false;
+
+ if (_replViaUsbCdc)
+ UsbCdc.InjectString("\r\n");
+ else
+ Uart.InjectString("\r\n");
+
+ return WaitForPrompt(timeoutMs - elapsed);
+ }
+
+ // ── Filesystem helpers ────────────────────────────────────────────────────
+
+ ///
+ /// Write to on the CircuitPython
+ /// virtual filesystem (FAT, exposed as the CIRCUITPY drive, mounted after boot).
+ ///
+ /// The primary entry point for CircuitPython is code.py (with main.py,
+ /// code.txt, and main.txt as fall-backs in that order).
+ ///
+ /// The file is written via REPL injection. Call first
+ /// to ensure the REPL is ready. Content is sent in chunks of 150 escaped characters
+ /// so it always fits within the REPL line buffer.
+ ///
+ /// true if the file was written successfully before the timeout.
+ public bool WriteFile(string path, string content, double timeoutMs = 5_000)
+ {
+ const int chunkSize = 150;
+
+ var escapedPath = EscapePythonString(path);
+ var escapedContent = EscapePythonString(content);
+
+ if (!ExecuteAndWait($"_wf=open('{escapedPath}','w')", ">>> ", timeoutMs))
+ return false;
+
+ var pos = 0;
+ while (pos < escapedContent.Length)
+ {
+ var end = Math.Min(pos + chunkSize, escapedContent.Length);
+
+ // Don't split in the middle of a \-escape sequence.
+ while (end > pos + 1)
+ {
+ var slashes = 0;
+ for (var k = end - 1; k >= pos && escapedContent[k] == '\\'; k--)
+ slashes++;
+ if (slashes % 2 == 0) break;
+ end--;
+ }
+
+ var chunk = escapedContent.Substring(pos, end - pos);
+ if (!ExecuteAndWait($"_wf.write('{chunk}')", ">>> ", timeoutMs))
+ return false;
+
+ pos = end;
+ }
+
+ return ExecuteAndWait("_wf.close()", ">>> ", timeoutMs);
+ }
+
+ ///
+ /// Perform a CircuitPython soft reset (CTRL+D). The VM re-runs code.py
+ /// (or the first available fall-back: main.py, code.txt,
+ /// main.txt). Any output is captured in /
+ /// . Returns true when the >>>
+ /// prompt reappears within .
+ ///
+ public bool SoftReset(double timeoutMs = 20_000)
+ {
+ if (_replViaUsbCdc)
+ {
+ UsbCdc.Clear();
+ UsbCdc.InjectString("\x04");
+ }
+ else
+ {
+ Uart.Clear();
+ Uart.InjectString("\x04");
+ }
+ return WaitForPrompt(timeoutMs);
+ }
+
+ // ── Writable-FS factory ───────────────────────────────────────────────────
+
+ ///
+ /// Create a runner where the CircuitPython filesystem is writable from Python code.
+ ///
+ /// Strategy (two-phase):
+ ///
+ /// - Boot CircuitPython once so it initialises (or mounts) the FAT filesystem.
+ /// - Copy the full 2 MB flash image, inject a boot.py into the copy,
+ /// then restart with the modified image.
+ ///
+ /// boot.py runs on every hard reset (power-up / LoadFlash),
+ /// before TinyUSB is initialised. Calling storage.disable_usb_drive() there
+ /// suppresses the USB-MSC descriptor and leaves the CIRCUITPY drive writable from
+ /// Python code.
+ ///
+ /// Note: a soft reset (CTRL-D) does NOT re-run boot.py; that is why the
+ /// second phase creates a fresh simulation rather than calling SoftReset().
+ ///
+ public static async Task CreateWithWritableFsAsync(string version)
+ {
+ // ── Phase 1: boot once so the FAT is initialised ──────────────────────
+ var stage1 = await CreateAsync(version);
+ if (stage1 is null) return null;
+
+ if (!stage1.WaitForPrompt(timeoutMs: 20_000))
+ {
+ await stage1.DisposeAsync();
+ return null;
+ }
+
+ // ── Phase 2: snapshot flash, inject boot.py, discard first simulation ─
+ var flashSize = (int)stage1.Simulation.Rp2040.Bus.FlashSize;
+ var fullFlash = new byte[flashSize];
+ unsafe
+ {
+ new ReadOnlySpan(stage1.Simulation.Rp2040.Bus.PtrFlash, flashSize)
+ .CopyTo(fullFlash);
+ fixed (byte* ptr = fullFlash)
+ InjectBootPy(ptr, "import storage\nstorage.disable_usb_drive()\n");
+ }
+ await stage1.DisposeAsync();
+
+ // ── Phase 3: restart with modified flash ──────────────────────────────
+ // boot.py is run on the very first hard reset, before TinyUSB initialises,
+ // so storage.disable_usb_drive() takes effect and the FS is writable.
+ var sim2 = new PicoSimulation(withUsbCdc: true);
+ sim2.LoadFlash(fullFlash);
+ var runner = new CircuitPythonRunner(sim2);
+
+ if (!runner.WaitForPrompt(timeoutMs: 20_000))
+ return null;
+
+ runner.Simulation.RunMilliseconds(200);
+ runner.UsbCdc.Clear();
+ return runner;
+ }
+
+ // ── FAT12 boot.py injection ───────────────────────────────────────────────
+
+ ///
+ /// Writes a boot.py file into the CIRCUITPY FAT12 partition inside a raw
+ /// flash image. Works on the raw byte[] (via a pinned pointer) or on the
+ /// live emulated flash — either way must point to
+ /// the base of the 2 MB flash image.
+ ///
+ private static unsafe void InjectBootPy(byte* flashPtr, string content)
+ {
+ // BPB parameters discovered empirically from a live CircuitPython 9.2.1 image.
+ const uint FatFlashOffset = 0x100000u; // CIRCUITPY FAT partition at 1 MB in flash
+ const int Bps = 512; // BPB_BytsPerSec
+ const int Spc = 1; // BPB_SecPerClus
+ const int RsvdSectors = 1; // BPB_RsvdSecCnt
+ const int NumFats = 1; // BPB_NumFATs
+ const int SectorsPerFat = 7; // BPB_FATSz16
+ const int RootDirEntries = 512; // BPB_RootEntCnt
+
+ // Sector layout (all offsets relative to the start of the FAT partition):
+ // Sector 1 : FAT1 (7 sectors)
+ // Sector 8 : root directory (512 entries × 32 B / 512 B/sector = 32 sectors)
+ // Sector 40 : data area (cluster 2 = first data cluster)
+ const int FatSector = RsvdSectors;
+ const int RootSector = RsvdSectors + NumFats * SectorsPerFat;
+ const int DataSector = RootSector + RootDirEntries * 32 / Bps;
+
+ byte* bpb = flashPtr + FatFlashOffset;
+ byte* fat = bpb + FatSector * Bps;
+ byte* root = bpb + RootSector * Bps;
+ byte* data = bpb + DataSector * Bps;
+
+ // Find the first free cluster (FAT12 allocatable entries start at cluster 2).
+ int freeClu = -1;
+ for (int clu = 2; clu < 2048; clu++)
+ {
+ if (Fat12Get(fat, clu) == 0x000u) { freeClu = clu; break; }
+ }
+ if (freeClu < 0) return; // FAT full — cannot inject
+
+ // Write file content into the allocated cluster (clear first, then copy).
+ byte[] contentBytes = System.Text.Encoding.ASCII.GetBytes(content);
+ byte* clusterData = data + (freeClu - 2) * Spc * Bps;
+ new Span(clusterData, Spc * Bps).Clear();
+ contentBytes.AsSpan().CopyTo(new Span(clusterData, contentBytes.Length));
+
+ // Mark cluster as end-of-chain in FAT12.
+ Fat12Set(fat, freeClu, 0xFFFu);
+
+ // Add a root-directory entry for BOOT.PY (8.3 short name).
+ for (int e = 0; e < RootDirEntries - 1; e++)
+ {
+ byte* entry = root + e * 32;
+ bool isEnd = entry[0] == 0x00;
+ bool isDeleted = entry[0] == 0xE5;
+ if (!isEnd && !isDeleted) continue;
+
+ // 8.3 name: "BOOT " + "PY "
+ "BOOT "u8.CopyTo(new Span(entry, 8));
+ "PY "u8.CopyTo(new Span(entry + 8, 3));
+ entry[11] = 0x20; // ATTR_ARCHIVE
+ new Span(entry + 12, 14).Clear(); // reserved / time / date
+ *(ushort*)(entry + 26) = (ushort)freeClu; // first cluster (low word)
+ *(uint*) (entry + 28) = (uint)contentBytes.Length; // file size
+
+ // When overwriting the end-of-directory marker the next slot must also
+ // be marked as end-of-directory (entry+32 is the first byte of entry e+1).
+ if (isEnd)
+ *(entry + 32) = 0x00;
+
+ break;
+ }
+ }
+
+ private static unsafe uint Fat12Get(byte* fat, int cluster)
+ {
+ int byteOff = cluster * 3 / 2;
+ uint raw = (uint)fat[byteOff] | ((uint)fat[byteOff + 1] << 8);
+ return (cluster & 1) == 0 ? raw & 0xFFFu : (raw >> 4) & 0xFFFu;
+ }
+
+ private static unsafe void Fat12Set(byte* fat, int cluster, uint value)
+ {
+ int byteOff = cluster * 3 / 2;
+ if ((cluster & 1) == 0)
+ {
+ fat[byteOff] = (byte)(value & 0xFF);
+ fat[byteOff + 1] = (byte)(((uint)fat[byteOff + 1] & 0xF0u) | ((value >> 8) & 0x0Fu));
+ }
+ else
+ {
+ fat[byteOff] = (byte)(((uint)fat[byteOff] & 0x0Fu) | ((value << 4) & 0xF0u));
+ fat[byteOff + 1] = (byte)((value >> 4) & 0xFF);
+ }
+ }
+
+ // ── Private helpers ───────────────────────────────────────────────────────
+
+ private static string EscapePythonString(string s)
+ {
+ var sb = new System.Text.StringBuilder(s.Length + 8);
+ foreach (var c in s)
+ {
+ switch (c)
+ {
+ case '\\': sb.Append("\\\\"); break;
+ case '\'': sb.Append("\\'"); break;
+ case '\n': sb.Append("\\n"); break;
+ case '\r': sb.Append("\\r"); break;
+ case '\t': sb.Append("\\t"); break;
+ default:
+ if (c < 0x20 || c > 0x7E)
+ sb.Append($"\\x{(int)c:x2}");
+ else
+ sb.Append(c);
+ break;
+ }
+ }
+ return sb.ToString();
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ _sim.Dispose();
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Infrastructure/FatVolume.cs b/tests/RP2040Sharp.IntegrationTests/Infrastructure/FatVolume.cs
new file mode 100644
index 0000000..705ba24
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Infrastructure/FatVolume.cs
@@ -0,0 +1,365 @@
+namespace RP2040Sharp.IntegrationTests.Infrastructure;
+
+///
+/// Minimal FAT12/FAT16 volume reader/writer for use with the USB MSC host probe.
+///
+/// Provides just enough FAT support to create or overwrite a single file by name
+/// in the root directory — sufficient to write code.py to the
+/// CircuitPython CIRCUITPY drive via the emulated MSC transport.
+///
+/// Design notes:
+///
+/// - All sectors are 512 bytes.
+/// - Supports FAT12 and FAT16 (CircuitPython uses FAT12 on a 2 MB Pico).
+/// - Does not support subdirectories; only the root directory.
+/// - Sector I/O is deferred to caller-supplied delegates so the implementation
+/// is independent of the USB transport.
+///
+///
+internal sealed class FatVolume
+{
+ private const int SECTOR_SIZE = 512;
+ private const int DIR_ENTRY_SIZE = 32;
+ private const byte ATTR_ARCHIVE = 0x20;
+ private const byte ATTR_DELETED = 0xE5;
+ private const int FAT12_EOC = 0xFF8;
+ private const ushort FAT16_EOC = 0xFFF8;
+
+ private readonly Func _readSector;
+ private readonly Action _writeSector;
+
+ // Populated from the VBR (Volume Boot Record / BPB).
+ private int _bytesPerSector;
+ private int _sectorsPerCluster;
+ private int _reservedSectors;
+ private int _numFats;
+ private int _rootEntryCount;
+ private int _totalSectors16;
+ private int _sectorsPerFat;
+ private bool _isFat12;
+
+ // Derived sector offsets.
+ private int _fatStart;
+ private int _rootDirStart;
+ private int _dataStart;
+ private int _dataClusterCount;
+
+ public bool IsValid { get; private set; }
+
+ private FatVolume(Func read, Action write)
+ {
+ _readSector = read;
+ _writeSector = write;
+ }
+
+ ///
+ /// Create a by reading the VBR from sector 0 via
+ /// . Returns null if the VBR is not valid.
+ ///
+ public static FatVolume? Open(Func readSector, Action writeSector)
+ {
+ var vbr = readSector(0);
+ if (vbr.Length < SECTOR_SIZE) return null;
+
+ var v = new FatVolume(readSector, writeSector);
+ if (!v.ParseVbr(vbr)) return null;
+ return v;
+ }
+
+ ///
+ /// Write bytes to (8.3 format,
+ /// e.g. "CODE PY ") in the root directory. Creates the file if it does not exist,
+ /// or overwrites the data area if it does (directory entry is updated in-place).
+ /// Returns true on success.
+ ///
+ public bool WriteFile(string name83, byte[] content)
+ {
+ // Find or create the directory entry.
+ if (!FindOrCreateDirEntry(name83, out var dirSector, out var dirOffset, out var existingFirstCluster))
+ return false;
+
+ // Free any existing cluster chain.
+ if (existingFirstCluster >= 2) FreeChain((uint)existingFirstCluster);
+
+ // Allocate clusters for the new content.
+ uint firstCluster = 0;
+ if (content.Length > 0)
+ {
+ var clusters = AllocateClusters(content, out firstCluster);
+ if (clusters == 0) return false;
+ }
+
+ // Update the directory entry.
+ var dirSectorData = _readSector((uint)dirSector);
+ WriteDirectoryEntry(dirSectorData, dirOffset, name83, (uint)content.Length, (ushort)firstCluster);
+ _writeSector((uint)dirSector, dirSectorData);
+ return true;
+ }
+
+ // ── VBR parsing ──────────────────────────────────────────────────────────
+
+ private bool ParseVbr(byte[] vbr)
+ {
+ _bytesPerSector = Le16(vbr, 11);
+ _sectorsPerCluster = vbr[13];
+ _reservedSectors = Le16(vbr, 14);
+ _numFats = vbr[16];
+ _rootEntryCount = Le16(vbr, 17);
+ _totalSectors16 = Le16(vbr, 19);
+ _sectorsPerFat = Le16(vbr, 22);
+
+ if (_bytesPerSector != SECTOR_SIZE || _sectorsPerCluster == 0 ||
+ _reservedSectors == 0 || _numFats == 0 || _sectorsPerFat == 0)
+ return false;
+
+ _fatStart = _reservedSectors;
+ _rootDirStart = _fatStart + _numFats * _sectorsPerFat;
+ var rootDirSectors = (_rootEntryCount * DIR_ENTRY_SIZE + SECTOR_SIZE - 1) / SECTOR_SIZE;
+ _dataStart = _rootDirStart + rootDirSectors;
+ var totalSectors = (uint)(_totalSectors16 != 0 ? _totalSectors16 : (int)Le32(vbr, 32));
+ _dataClusterCount = (int)((totalSectors - (uint)_dataStart) / (uint)_sectorsPerCluster);
+ _isFat12 = _dataClusterCount < 4085;
+ IsValid = true;
+ return true;
+ }
+
+ // ── Directory helpers ─────────────────────────────────────────────────────
+
+ private bool FindOrCreateDirEntry(string name83, out int sector, out int offset, out int firstCluster)
+ {
+ sector = 0; offset = 0; firstCluster = 0;
+ var rootSectors = (_rootEntryCount * DIR_ENTRY_SIZE + SECTOR_SIZE - 1) / SECTOR_SIZE;
+ int? freeSlotSector = null;
+ int freeSlotOffset = 0;
+
+ for (var s = 0; s < rootSectors; s++)
+ {
+ var sectorData = _readSector((uint)(_rootDirStart + s));
+ for (var o = 0; o < SECTOR_SIZE; o += DIR_ENTRY_SIZE)
+ {
+ var firstByte = sectorData[o];
+ if (firstByte == 0x00) goto NotFound; // end of directory
+ if (firstByte == ATTR_DELETED)
+ {
+ freeSlotSector ??= _rootDirStart + s;
+ freeSlotOffset = o;
+ continue;
+ }
+ var attr = sectorData[o + 11];
+ if ((attr & 0x08) != 0) continue; // volume label
+ if ((attr & 0x10) != 0) continue; // subdirectory
+ var entryName = System.Text.Encoding.ASCII.GetString(sectorData, o, 11);
+ if (string.Equals(entryName, PadName83(name83), StringComparison.OrdinalIgnoreCase))
+ {
+ sector = _rootDirStart + s;
+ offset = o;
+ firstCluster = Le16(sectorData, o + 26);
+ return true;
+ }
+ }
+ }
+
+ NotFound:
+ // Use a free slot or return the free-slot position.
+ if (freeSlotSector.HasValue)
+ {
+ sector = freeSlotSector.Value;
+ offset = freeSlotOffset;
+ return true;
+ }
+ // Find the first free entry by scanning for first-byte == 0x00.
+ for (var s = 0; s < rootSectors; s++)
+ {
+ var sectorData = _readSector((uint)(_rootDirStart + s));
+ for (var o = 0; o < SECTOR_SIZE; o += DIR_ENTRY_SIZE)
+ {
+ if (sectorData[o] == 0x00)
+ {
+ sector = _rootDirStart + s;
+ offset = o;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static void WriteDirectoryEntry(byte[] sectorData, int offset, string name83,
+ uint fileSize, ushort firstCluster)
+ {
+ var padded = PadName83(name83);
+ System.Text.Encoding.ASCII.GetBytes(padded, 0, 11, sectorData, offset);
+ sectorData[offset + 11] = ATTR_ARCHIVE;
+ // Time/date: use a fixed stamp (2024-01-01 00:00:00) so tests are deterministic.
+ sectorData[offset + 22] = 0x00; // write time
+ sectorData[offset + 23] = 0x00;
+ sectorData[offset + 24] = 0x21; // write date: 2024-01-01
+ sectorData[offset + 25] = 0x58;
+ sectorData[offset + 26] = (byte)(firstCluster & 0xFF);
+ sectorData[offset + 27] = (byte)(firstCluster >> 8);
+ sectorData[offset + 28] = (byte)(fileSize & 0xFF);
+ sectorData[offset + 29] = (byte)((fileSize >> 8) & 0xFF);
+ sectorData[offset + 30] = (byte)((fileSize >> 16) & 0xFF);
+ sectorData[offset + 31] = (byte)((fileSize >> 24) & 0xFF);
+ }
+
+ // ── FAT chain allocation / freeing ────────────────────────────────────────
+
+ private int AllocateClusters(byte[] content, out uint firstCluster)
+ {
+ firstCluster = 0;
+ var bytesPerCluster = _sectorsPerCluster * SECTOR_SIZE;
+ var clusterCount = (content.Length + bytesPerCluster - 1) / bytesPerCluster;
+
+ // Find free clusters.
+ var freeList = new List();
+ for (uint c = 2; c < _dataClusterCount + 2 && freeList.Count < clusterCount; c++)
+ {
+ if (GetFatEntry(c) == 0) freeList.Add(c);
+ }
+ if (freeList.Count < clusterCount) return 0;
+
+ // Link clusters and write data.
+ for (var i = 0; i < freeList.Count; i++)
+ {
+ var cluster = freeList[i];
+ var nextVal = i + 1 < freeList.Count ? freeList[i + 1] : (uint)(_isFat12 ? FAT12_EOC : FAT16_EOC);
+ SetFatEntry(cluster, (uint)nextVal);
+
+ // Write sector data for this cluster.
+ var clusterSectorBase = _dataStart + (int)(cluster - 2) * _sectorsPerCluster;
+ for (var s = 0; s < _sectorsPerCluster; s++)
+ {
+ var byteOffset = (i * _sectorsPerCluster + s) * SECTOR_SIZE;
+ var sectorBuf = new byte[SECTOR_SIZE];
+ if (byteOffset < content.Length)
+ {
+ var toCopy = Math.Min(SECTOR_SIZE, content.Length - byteOffset);
+ Array.Copy(content, byteOffset, sectorBuf, 0, toCopy);
+ }
+ _writeSector((uint)(clusterSectorBase + s), sectorBuf);
+ }
+ }
+
+ firstCluster = freeList[0];
+ return freeList.Count;
+ }
+
+ private void FreeChain(uint firstCluster)
+ {
+ var c = firstCluster;
+ while (c >= 2 && c < (uint)(_dataClusterCount + 2))
+ {
+ var next = GetFatEntry(c);
+ SetFatEntry(c, 0);
+ if (next >= (_isFat12 ? (uint)FAT12_EOC : (uint)FAT16_EOC)) break;
+ c = next;
+ }
+ }
+
+ // ── FAT I/O ───────────────────────────────────────────────────────────────
+
+ private uint GetFatEntry(uint cluster)
+ {
+ if (_isFat12)
+ {
+ var byteOffset = cluster + cluster / 2; // 12-bit: 1.5 bytes per entry
+ var secIdx = (int)(byteOffset / SECTOR_SIZE);
+ var secOff = (int)(byteOffset % SECTOR_SIZE);
+ var s = _readSector((uint)(_fatStart + secIdx));
+ uint lo = s[secOff];
+ uint hi = secOff + 1 < SECTOR_SIZE ? s[secOff + 1]
+ : _readSector((uint)(_fatStart + secIdx + 1))[0];
+ var raw = lo | (hi << 8);
+ return (cluster & 1) != 0 ? (raw >> 4) & 0xFFF : raw & 0xFFF;
+ }
+ else
+ {
+ var byteOffset = cluster * 2;
+ var sec = _readSector((uint)(_fatStart + byteOffset / SECTOR_SIZE));
+ return (uint)Le16(sec, (int)(byteOffset % SECTOR_SIZE));
+ }
+ }
+
+ private void SetFatEntry(uint cluster, uint value)
+ {
+ // Write to all FAT copies.
+ for (var fatIdx = 0; fatIdx < _numFats; fatIdx++)
+ {
+ var fatBase = _fatStart + fatIdx * _sectorsPerFat;
+ if (_isFat12)
+ {
+ var byteOffset = cluster + cluster / 2;
+ var secIdx = (int)(byteOffset / SECTOR_SIZE);
+ var secOff = (int)(byteOffset % SECTOR_SIZE);
+ var s = _readSector((uint)(fatBase + secIdx));
+ if ((cluster & 1) != 0)
+ {
+ s[secOff] = (byte)((s[secOff] & 0x0F) | (byte)((value & 0x0F) << 4));
+ if (secOff + 1 < SECTOR_SIZE)
+ s[secOff + 1] = (byte)((value >> 4) & 0xFF);
+ else
+ {
+ _writeSector((uint)(fatBase + secIdx), s);
+ var s2 = _readSector((uint)(fatBase + secIdx + 1));
+ s2[0] = (byte)((value >> 4) & 0xFF);
+ _writeSector((uint)(fatBase + secIdx + 1), s2);
+ continue;
+ }
+ }
+ else
+ {
+ s[secOff] = (byte)(value & 0xFF);
+ if (secOff + 1 < SECTOR_SIZE)
+ s[secOff + 1] = (byte)((s[secOff + 1] & 0xF0) | (byte)((value >> 8) & 0x0F));
+ else
+ {
+ _writeSector((uint)(fatBase + secIdx), s);
+ var s2 = _readSector((uint)(fatBase + secIdx + 1));
+ s2[0] = (byte)((s2[0] & 0xF0) | (byte)((value >> 8) & 0x0F));
+ _writeSector((uint)(fatBase + secIdx + 1), s2);
+ continue;
+ }
+ }
+ _writeSector((uint)(fatBase + secIdx), s);
+ }
+ else // FAT16
+ {
+ var byteOffset = cluster * 2;
+ var sec = _readSector((uint)(fatBase + byteOffset / SECTOR_SIZE));
+ var off = (int)(byteOffset % SECTOR_SIZE);
+ sec[off] = (byte)(value & 0xFF);
+ sec[off + 1] = (byte)((value >> 8) & 0xFF);
+ _writeSector((uint)(fatBase + byteOffset / SECTOR_SIZE), sec);
+ }
+ }
+ }
+
+ // ── Utility ───────────────────────────────────────────────────────────────
+
+ /// Convert a short filename ("code.py") to a padded 11-char 8.3 name ("CODE PY ").
+ internal static string PadName83(string name)
+ {
+ // If already 11 chars, return as-is.
+ if (name.Length == 11) return name.ToUpperInvariant();
+
+ var dot = name.LastIndexOf('.');
+ string baseName, ext;
+ if (dot < 0)
+ {
+ baseName = name;
+ ext = "";
+ }
+ else
+ {
+ baseName = name[..dot];
+ ext = name[(dot + 1)..];
+ }
+ var b = baseName.ToUpperInvariant().PadRight(8).Substring(0, 8);
+ var e = ext.ToUpperInvariant().PadRight(3).Substring(0, 3);
+ return b + e;
+ }
+
+ private static int Le16(byte[] b, int o) => b[o] | (b[o + 1] << 8);
+ private static uint Le32(byte[] b, int o) => (uint)b[o] | ((uint)b[o+1] << 8) | ((uint)b[o+2] << 16) | ((uint)b[o+3] << 24);
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Infrastructure/FirmwareCache.cs b/tests/RP2040Sharp.IntegrationTests/Infrastructure/FirmwareCache.cs
new file mode 100644
index 0000000..f416fc1
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Infrastructure/FirmwareCache.cs
@@ -0,0 +1,126 @@
+namespace RP2040Sharp.IntegrationTests.Infrastructure;
+
+///
+/// Downloads and caches MicroPython and CircuitPython UF2 firmware images.
+/// Firmware is stored in a local cache directory so subsequent test runs are offline-capable.
+///
+public static class FirmwareCache
+{
+ private static readonly string CacheDir =
+ Path.Combine(Path.GetTempPath(), "rp2040sharp-firmware-cache");
+
+ ///
+ /// Returns the local path to the MicroPython UF2 image for
+ /// (e.g. "v1.21.0"), downloading it from GitHub Releases if not already cached.
+ /// Returns null if the download fails (network unavailable, etc.).
+ ///
+ public static async Task GetMicroPythonAsync(string version)
+ {
+ Directory.CreateDirectory(CacheDir);
+
+ var path = Path.Combine(CacheDir, $"micropython-{version}.uf2");
+ if (File.Exists(path) && new FileInfo(path).Length > 0)
+ return path;
+
+ try
+ {
+ using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };
+ http.DefaultRequestHeaders.UserAgent.ParseAdd("RP2040Sharp-IntegrationTests/1.0");
+
+ // Resolve the exact filename (includes build date) from the download index
+ var url = await ResolveMicroPythonUrlAsync(http, version);
+ if (url is null) return null;
+
+ var bytes = await http.GetByteArrayAsync(url);
+ await File.WriteAllBytesAsync(path, bytes);
+ return path;
+ }
+ catch
+ {
+ // Network unavailable or release doesn't exist — tests will be skipped
+ if (File.Exists(path))
+ File.Delete(path);
+ return null;
+ }
+ }
+
+ private static async Task ResolveMicroPythonUrlAsync(HttpClient http, string version)
+ {
+ // Firmware listed at https://micropython.org/download/RPI_PICO/
+ // Entries look like: /resources/firmware/RPI_PICO-{date}-{version}.uf2
+ var page = await http.GetStringAsync("https://micropython.org/download/RPI_PICO/");
+ // Filenames are RPI_PICO-{date}-v{semver}.uf2 — keep the v prefix
+ var tag = version.StartsWith('v') ? version : "v" + version;
+ const string needle = "/resources/firmware/RPI_PICO-";
+ var search = $"-{tag}.uf2"; // e.g. "-v1.21.0.uf2" (no trailing quote — rel slice excludes it)
+
+ var start = page.IndexOf(needle, StringComparison.Ordinal);
+ while (start >= 0)
+ {
+ var end = page.IndexOf('"', start + 1);
+ if (end < 0) break;
+ var rel = page[start..end];
+ if (rel.EndsWith(search, StringComparison.OrdinalIgnoreCase))
+ return "https://micropython.org" + rel;
+ start = page.IndexOf(needle, start + 1, StringComparison.Ordinal);
+ }
+ return null;
+ }
+
+ ///
+ /// Returns the local path to the CircuitPython UF2 image for
+ /// (e.g. "9.2.1"), downloading it from downloads.circuitpython.org if not already cached.
+ /// Returns null if the download fails (network unavailable, etc.).
+ ///
+ public static async Task GetCircuitPythonAsync(string version)
+ {
+ Directory.CreateDirectory(CacheDir);
+
+ var path = Path.Combine(CacheDir, $"circuitpython-{version}.uf2");
+ if (File.Exists(path) && new FileInfo(path).Length > 0)
+ return path;
+
+ try
+ {
+ using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };
+ http.DefaultRequestHeaders.UserAgent.ParseAdd("RP2040Sharp-IntegrationTests/1.0");
+
+ // Direct stable URL — no scraping needed; version has no 'v' prefix
+ var tag = version.StartsWith('v') ? version[1..] : version;
+ var url = $"https://downloads.circuitpython.org/bin/raspberry_pi_pico/en_US/" +
+ $"adafruit-circuitpython-raspberry_pi_pico-en_US-{tag}.uf2";
+
+ var bytes = await http.GetByteArrayAsync(url);
+ await File.WriteAllBytesAsync(path, bytes);
+ return path;
+ }
+ catch
+ {
+ // Network unavailable or release doesn't exist — tests will be skipped
+ if (File.Exists(path))
+ File.Delete(path);
+ return null;
+ }
+ }
+
+ ///
+ /// Returns the local path to the MicroPython firmware if already cached, without attempting
+ /// a download. Useful for offline CI environments where firmware is pre-seeded.
+ ///
+ public static string? GetCachedPath(string version)
+ {
+ var path = Path.Combine(CacheDir, $"micropython-{version}.uf2");
+ return File.Exists(path) && new FileInfo(path).Length > 0 ? path : null;
+ }
+
+ ///
+ /// Returns the local path to the CircuitPython firmware if already cached, without
+ /// attempting a download.
+ ///
+ public static string? GetCachedCircuitPythonPath(string version)
+ {
+ var tag = version.StartsWith('v') ? version[1..] : version;
+ var path = Path.Combine(CacheDir, $"circuitpython-{tag}.uf2");
+ return File.Exists(path) && new FileInfo(path).Length > 0 ? path : null;
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Infrastructure/MicroPythonRunner.cs b/tests/RP2040Sharp.IntegrationTests/Infrastructure/MicroPythonRunner.cs
new file mode 100644
index 0000000..5c881d8
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Infrastructure/MicroPythonRunner.cs
@@ -0,0 +1,284 @@
+using RP2040.TestKit;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Probes;
+
+namespace RP2040Sharp.IntegrationTests.Infrastructure;
+
+///
+/// High-level runner that boots MicroPython on the RP2040 emulator and exposes a REPL
+/// interface for driving tests via UART injection.
+///
+/// Usage:
+///
+/// await using var mp = await MicroPythonRunner.CreateAsync("v1.21.0");
+/// mp.Should().NotBeNull("firmware should be available");
+///
+/// bool booted = mp.WaitForPrompt();
+/// booted.Should().BeTrue();
+///
+/// mp.Execute("print('hello')");
+/// mp.WaitForOutput("hello").Should().BeTrue();
+///
+///
+public sealed class MicroPythonRunner : IAsyncDisposable
+{
+ private readonly PicoSimulation _sim;
+
+ // MicroPython routes its REPL through USB-CDC (TinyUSB) when USB is available.
+ // Track which transport the REPL prompt appeared on so Execute/WaitForOutput
+ // use the correct channel.
+ private bool _replViaUsbCdc;
+
+ public UartProbe Uart => _sim.Uart0;
+ public UsbCdcProbe UsbCdc => _sim.UsbCdc;
+ public PicoSimulation Simulation => _sim;
+
+ private MicroPythonRunner(PicoSimulation sim)
+ {
+ _sim = sim;
+ }
+
+ ///
+ /// Create a runner loaded with MicroPython .
+ /// Returns null when the firmware is not available (no network / not cached).
+ ///
+ public static async Task CreateAsync(string version)
+ {
+ var uf2Path = await FirmwareCache.GetMicroPythonAsync(version);
+ if (uf2Path is null)
+ return null;
+
+ var uf2Bytes = await File.ReadAllBytesAsync(uf2Path);
+ var flashImage = Uf2Reader.ToFlashImage(uf2Bytes);
+
+ var sim = new PicoSimulation();
+ sim.LoadFlash(flashImage);
+ return new MicroPythonRunner(sim);
+ }
+
+ // ── REPL helpers ─────────────────────────────────────────────────
+
+ ///
+ /// Run the simulation until the MicroPython REPL prompt (>>> ) appears on UART
+ /// or USB-CDC, or until elapses.
+ ///
+ public bool WaitForPrompt(double timeoutMs = 15_000)
+ {
+ const double batchMs = 100.0;
+ var elapsed = 0.0;
+ while (elapsed < timeoutMs)
+ {
+ _sim.RunMilliseconds(batchMs);
+ if (Uart.Text.Contains(">>> ", StringComparison.Ordinal))
+ {
+ _replViaUsbCdc = false;
+ return true;
+ }
+ if (UsbCdc.Text.Contains(">>> ", StringComparison.Ordinal))
+ {
+ _replViaUsbCdc = true;
+ return true;
+ }
+ elapsed += batchMs;
+ }
+ return false;
+ }
+
+ ///
+ /// Inject a line of Python code into the REPL (appends \r\n).
+ /// Call first to ensure the REPL is ready.
+ /// Routes the injection to the transport where the REPL was detected (UART or USB-CDC).
+ ///
+ public void Execute(string pythonLine)
+ {
+ if (_replViaUsbCdc)
+ {
+ UsbCdc.Clear();
+ UsbCdc.InjectString(pythonLine + "\r\n");
+ }
+ else
+ {
+ Uart.Clear();
+ _sim.Uart0.InjectString(pythonLine + "\r\n");
+ }
+ }
+
+ ///
+ /// Run the simulation until appears in the output
+ /// captured since the last call.
+ /// Uses the same transport (UART or USB-CDC) where the REPL was detected.
+ ///
+ public bool WaitForOutput(string expectedText, double timeoutMs = 5_000) =>
+ _replViaUsbCdc
+ ? _sim.RunUntilOutput(UsbCdc, expectedText, timeoutMs)
+ : _sim.RunUntilOutput(Uart, expectedText, timeoutMs);
+
+ ///
+ /// Inject a Python line and wait for .
+ /// Returns true if the expected text appeared before the timeout.
+ ///
+ public bool ExecuteAndWait(string pythonLine, string expectedOutput, double timeoutMs = 5_000)
+ {
+ Execute(pythonLine);
+ return WaitForOutput(expectedOutput, timeoutMs);
+ }
+
+ ///
+ /// Run simulation in batches until over the output text returns true.
+ /// Uses the same transport (UART or USB-CDC) where the REPL was detected.
+ ///
+ public bool WaitForOutput(Func predicate, double timeoutMs = 5_000) =>
+ _replViaUsbCdc
+ ? _sim.RunUntilOutput(UsbCdc, predicate, timeoutMs)
+ : _sim.RunUntilOutput(Uart, predicate, timeoutMs);
+
+ ///
+ /// Inject a compound statement (def, for, class, if, etc.) into
+ /// the REPL. Sends the statement, waits for the ... continuation prompt, then sends
+ /// a blank line to execute it, and finally waits for the next >>> prompt.
+ /// Returns true if the REPL prompt was seen before the timeout.
+ ///
+ public bool ExecuteCompound(string pythonLine, double timeoutMs = 15_000)
+ {
+ // Inject the first line (opens the compound statement)
+ if (_replViaUsbCdc)
+ {
+ UsbCdc.Clear();
+ UsbCdc.InjectString(pythonLine + "\r\n");
+ }
+ else
+ {
+ Uart.Clear();
+ _sim.Uart0.InjectString(pythonLine + "\r\n");
+ }
+
+ // Wait for the continuation prompt ("... ") or an immediate ">>> " (single-line executed)
+ const double batchMs = 100.0;
+ var elapsed = 0.0;
+ var gotContinuation = false;
+ while (elapsed < timeoutMs)
+ {
+ _sim.RunMilliseconds(batchMs);
+ var text = _replViaUsbCdc ? UsbCdc.Text : Uart.Text;
+ if (text.Contains(">>> ", StringComparison.Ordinal)) return true; // executed immediately
+ if (text.Contains("... ", StringComparison.Ordinal)) { gotContinuation = true; break; }
+ elapsed += batchMs;
+ }
+
+ if (!gotContinuation) return false;
+
+ // Send a blank line to close the compound block
+ if (_replViaUsbCdc)
+ UsbCdc.InjectString("\r\n");
+ else
+ _sim.Uart0.InjectString("\r\n");
+
+ // Wait for the final ">>> " prompt confirming execution
+ return WaitForPrompt(timeoutMs - elapsed);
+ }
+
+ // ── Filesystem helpers ────────────────────────────────────────────────────
+
+ ///
+ /// Write to on the MicroPython
+ /// virtual filesystem (LittleFS, mounted after boot).
+ ///
+ /// The file is written via REPL injection. Call first
+ /// to ensure the REPL is ready. Content is sent in chunks of 150 escaped characters
+ /// so it always fits within the REPL line buffer.
+ ///
+ /// true if the file was written successfully before the timeout.
+ public bool WriteFile(string path, string content, double timeoutMs = 5_000)
+ {
+ const int chunkSize = 150;
+
+ var escapedPath = EscapePythonString(path);
+ var escapedContent = EscapePythonString(content);
+
+ // Open the file (use a deliberately odd name to avoid clobbering user variables)
+ if (!ExecuteAndWait($"_wf=open('{escapedPath}','w')", ">>> ", timeoutMs))
+ return false;
+
+ // Write content in 150-char chunks (safe for any REPL line-buffer size)
+ var pos = 0;
+ while (pos < escapedContent.Length)
+ {
+ var end = Math.Min(pos + chunkSize, escapedContent.Length);
+
+ // Don't split in the middle of a \-escape sequence:
+ // an ODD run of trailing backslashes means the last one starts an escape.
+ while (end > pos + 1)
+ {
+ var slashes = 0;
+ for (var k = end - 1; k >= pos && escapedContent[k] == '\\'; k--)
+ slashes++;
+ if (slashes % 2 == 0) break;
+ end--;
+ }
+
+ var chunk = escapedContent.Substring(pos, end - pos);
+ if (!ExecuteAndWait($"_wf.write('{chunk}')", ">>> ", timeoutMs))
+ return false;
+
+ pos = end;
+ }
+
+ return ExecuteAndWait("_wf.close()", ">>> ", timeoutMs);
+ }
+
+ ///
+ /// Perform a MicroPython soft reset (CTRL+D). The VM re-runs boot.py then
+ /// main.py if they exist; any output is captured in /
+ /// . Returns true when the >>> prompt
+ /// reappears within .
+ ///
+ public bool SoftReset(double timeoutMs = 20_000)
+ {
+ if (_replViaUsbCdc)
+ {
+ UsbCdc.Clear();
+ UsbCdc.InjectString("\x04");
+ }
+ else
+ {
+ Uart.Clear();
+ Uart.InjectString("\x04");
+ }
+ return WaitForPrompt(timeoutMs);
+ }
+
+ // ── Private helpers ───────────────────────────────────────────────────────
+
+ ///
+ /// Escape so it is safe to embed inside a Python single-quoted
+ /// string literal (e.g. f.write('…')).
+ ///
+ private static string EscapePythonString(string s)
+ {
+ var sb = new System.Text.StringBuilder(s.Length + 8);
+ foreach (var c in s)
+ {
+ switch (c)
+ {
+ case '\\': sb.Append("\\\\"); break;
+ case '\'': sb.Append("\\'"); break;
+ case '\n': sb.Append("\\n"); break;
+ case '\r': sb.Append("\\r"); break;
+ case '\t': sb.Append("\\t"); break;
+ default:
+ if (c < 0x20 || c > 0x7E)
+ sb.Append($"\\x{(int)c:x2}");
+ else
+ sb.Append(c);
+ break;
+ }
+ }
+ return sb.ToString();
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ _sim.Dispose();
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Infrastructure/Uf2Reader.cs b/tests/RP2040Sharp.IntegrationTests/Infrastructure/Uf2Reader.cs
new file mode 100644
index 0000000..5d539c6
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Infrastructure/Uf2Reader.cs
@@ -0,0 +1,77 @@
+namespace RP2040Sharp.IntegrationTests.Infrastructure;
+
+///
+/// Minimal UF2 parser: extracts the Flash image from a UF2 file and returns it as a flat
+/// byte array ready to load with machine.LoadFlash().
+///
+public static class Uf2Reader
+{
+ private const uint UF2_MAGIC_START0 = 0x0A324655; // "UF2\n"
+ private const uint UF2_MAGIC_START1 = 0x9E5D5157;
+ private const uint UF2_MAGIC_END = 0x0AB16F30;
+ private const int UF2_BLOCK_SIZE = 512;
+ private const int UF2_DATA_OFFSET = 32;
+ private const int UF2_DATA_SIZE = 256;
+
+ ///
+ /// Parse a UF2 byte array and return the Flash image.
+ /// All blocks must target a contiguous Flash range; gaps are filled with 0xFF.
+ ///
+ public static byte[] ToFlashImage(byte[] uf2)
+ {
+ var blocks = uf2.Length / UF2_BLOCK_SIZE;
+ uint minAddr = uint.MaxValue;
+ uint maxAddr = 0;
+
+ // First pass: determine address range
+ for (var i = 0; i < blocks; i++)
+ {
+ var off = i * UF2_BLOCK_SIZE;
+ var magic0 = ReadU32(uf2, off);
+ var magic1 = ReadU32(uf2, off + 4);
+ if (magic0 != UF2_MAGIC_START0 || magic1 != UF2_MAGIC_START1)
+ continue;
+
+ var targetAddr = ReadU32(uf2, off + 12);
+ var payloadSize = ReadU32(uf2, off + 16);
+ if (payloadSize == 0 || payloadSize > 256) continue;
+
+ if (targetAddr < minAddr) minAddr = targetAddr;
+ if (targetAddr + payloadSize > maxAddr) maxAddr = targetAddr + payloadSize;
+ }
+
+ if (minAddr == uint.MaxValue)
+ throw new InvalidDataException("No valid UF2 blocks found.");
+
+ // RP2040 Flash starts at 0x10000000 — strip the base address
+ const uint flashBase = 0x10000000;
+ if (minAddr < flashBase)
+ throw new InvalidDataException($"UF2 target address 0x{minAddr:X8} is below Flash base 0x{flashBase:X8}.");
+
+ var imageSize = (int)(maxAddr - flashBase);
+ var image = new byte[imageSize];
+ Array.Fill(image, (byte)0xFF);
+
+ // Second pass: copy payload data
+ for (var i = 0; i < blocks; i++)
+ {
+ var off = i * UF2_BLOCK_SIZE;
+ var magic0 = ReadU32(uf2, off);
+ var magic1 = ReadU32(uf2, off + 4);
+ if (magic0 != UF2_MAGIC_START0 || magic1 != UF2_MAGIC_START1)
+ continue;
+
+ var targetAddr = ReadU32(uf2, off + 12);
+ var payloadSize = ReadU32(uf2, off + 16);
+ if (payloadSize == 0 || payloadSize > 256) continue;
+
+ var destOffset = (int)(targetAddr - flashBase);
+ Buffer.BlockCopy(uf2, off + UF2_DATA_OFFSET, image, destOffset, (int)payloadSize);
+ }
+
+ return image;
+ }
+
+ private static uint ReadU32(byte[] buf, int offset) =>
+ (uint)(buf[offset] | (buf[offset + 1] << 8) | (buf[offset + 2] << 16) | (buf[offset + 3] << 24));
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/RP2040Sharp.IntegrationTests.csproj b/tests/RP2040Sharp.IntegrationTests/RP2040Sharp.IntegrationTests.csproj
new file mode 100644
index 0000000..fafc264
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/RP2040Sharp.IntegrationTests.csproj
@@ -0,0 +1,44 @@
+
+
+
+ net10.0
+ true
+ false
+ enable
+ enable
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/RP2040Sharp.IntegrationTests/Scripts/hello_world.py b/tests/RP2040Sharp.IntegrationTests/Scripts/hello_world.py
new file mode 100644
index 0000000..908ae6d
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Scripts/hello_world.py
@@ -0,0 +1 @@
+print("Hello, MicroPython!")
diff --git a/tests/RP2040Sharp.IntegrationTests/Scripts/pio_fifo_loopback.py b/tests/RP2040Sharp.IntegrationTests/Scripts/pio_fifo_loopback.py
new file mode 100644
index 0000000..c7f970d
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Scripts/pio_fifo_loopback.py
@@ -0,0 +1,42 @@
+"""
+Verify PIO FIFO loopback: OUT from OSR → GPIO pins, then IN from GPIO pins → ISR → RX FIFO.
+
+Uses a two-SM loopback pattern entirely in software (no physical wire needed):
+ SM0: OUT PINS 8 — shifts 8 bits from OSR to 8 GPIO pins
+ SM1: IN PINS 8 — shifts 8 bits from the same GPIO pins into ISR, then PUSH
+
+Both state machines share the same 8-pin window (pin_base=0, count=8).
+We write a value to SM0 TX FIFO and read it back from SM1 RX FIFO.
+"""
+import rp2
+from machine import Pin
+
+@rp2.asm_pio(out_init=[rp2.PIO.OUT_LOW]*8,
+ out_shiftdir=rp2.PIO.SHIFT_RIGHT,
+ autopull=True, pull_thresh=8)
+def out8():
+ out(pins, 8)
+ wrap_target()
+ nop()
+ wrap()
+
+@rp2.asm_pio(in_shiftdir=rp2.PIO.SHIFT_LEFT, autopush=True, push_thresh=8)
+def in8():
+ wrap_target()
+ in_(pins, 8)
+ wrap()
+
+sm0 = rp2.StateMachine(0, out8, freq=10_000_000, out_base=Pin(0))
+sm1 = rp2.StateMachine(1, in8, freq=10_000_000, in_base=Pin(0))
+
+sm0.active(1)
+sm1.active(1)
+
+SENTINEL = 0xA5
+sm0.put(SENTINEL)
+
+import time
+time.sleep_ms(5)
+
+result = sm1.get()
+print("loopback:", hex(result & 0xFF)) # expected: 0xa5
diff --git a/tests/RP2040Sharp.IntegrationTests/Scripts/pio_set_pins.py b/tests/RP2040Sharp.IntegrationTests/Scripts/pio_set_pins.py
new file mode 100644
index 0000000..c39e92a
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Scripts/pio_set_pins.py
@@ -0,0 +1,25 @@
+"""
+Verify that a minimal PIO program using SET PINS can drive a GPIO pin.
+
+The program sets PIN 0 high once, then stalls (blocking PULL with empty FIFO).
+We read back via machine.Pin to confirm the output was driven.
+"""
+import rp2
+from machine import Pin
+
+@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
+def set_pin_high():
+ set(pins, 1) # drive pin high
+ wrap_target()
+ nop() # idle loop
+ wrap()
+
+sm = rp2.StateMachine(0, set_pin_high, set_base=Pin(16))
+sm.active(1)
+
+# Give the SM a few cycles to execute
+import time
+time.sleep_ms(1)
+
+p = Pin(16, Pin.IN)
+print("pin:", p.value()) # expected: 1
diff --git a/tests/RP2040Sharp.IntegrationTests/Scripts/pio_tx_rx_fifo.py b/tests/RP2040Sharp.IntegrationTests/Scripts/pio_tx_rx_fifo.py
new file mode 100644
index 0000000..85bf377
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Scripts/pio_tx_rx_fifo.py
@@ -0,0 +1,31 @@
+"""
+Verify PIO TX→RX FIFO round-trip using a MOV OSR ISR program.
+
+The program:
+ PULL — move TX FIFO word into OSR
+ MOV ISR, OSR — copy OSR to ISR
+ PUSH — move ISR into RX FIFO
+
+This exercises both halves of the FIFO path with a single SM.
+"""
+import rp2
+
+@rp2.asm_pio()
+def copy_tx_to_rx():
+ wrap_target()
+ pull(block)
+ mov(isr, osr)
+ push(block)
+ wrap()
+
+sm = rp2.StateMachine(0, copy_tx_to_rx)
+sm.active(1)
+
+SENTINEL = 0xDEAD_BEEF
+sm.put(SENTINEL)
+
+import time
+time.sleep_ms(2)
+
+result = sm.get()
+print("fifo:", hex(result)) # expected: 0xdeadbeef
diff --git a/tests/RP2040Sharp.IntegrationTests/Scripts/spi_test.py b/tests/RP2040Sharp.IntegrationTests/Scripts/spi_test.py
new file mode 100644
index 0000000..8a4c397
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Scripts/spi_test.py
@@ -0,0 +1,15 @@
+from machine import SPI, Pin
+import time
+
+spi = SPI(0, baudrate=1000000, polarity=0, phase=0, sck=Pin(2), mosi=Pin(3), miso=Pin(4))
+cs = Pin(5, Pin.OUT)
+
+messages = [b'\x01\x02\x03', b'Hello, SPI!', b'\xDE\xAD\xBE\xEF']
+
+for msg in messages:
+ cs.value(0)
+ spi.write(msg)
+ cs.value(1)
+ time.sleep_ms(10)
+
+print("SPI done")
diff --git a/tests/RP2040Sharp.IntegrationTests/Scripts/version_info.py b/tests/RP2040Sharp.IntegrationTests/Scripts/version_info.py
new file mode 100644
index 0000000..f8e5cf2
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Scripts/version_info.py
@@ -0,0 +1,3 @@
+import sys
+print("MicroPython", sys.version)
+print("Platform:", sys.platform)
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/AdcTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/AdcTests.cs
new file mode 100644
index 0000000..25ccc38
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/AdcTests.cs
@@ -0,0 +1,95 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for ADC examples from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class AdcTests
+{
+ // ── hello_adc ─────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HelloAdc_NoHardFault_AfterFirstRead()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloAdc)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur during ADC read");
+ }
+
+ [Fact]
+ public void HelloAdc_Uart0_ProducesVoltageReading()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloAdc)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_adc reads ADC0 (GP26) repeatedly and prints the converted voltage
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("hello_adc must print ADC readings over UART0");
+ }
+
+ [Fact]
+ public void HelloAdc_Uart0_ReceivesMultipleReadings()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloAdc)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(3_000);
+
+ pico.Uart0.Lines.Count.Should().BeGreaterThan(2,
+ "hello_adc should print multiple ADC readings over 3 seconds");
+ }
+
+ // ── onboard_temperature ───────────────────────────────────────────────────
+
+ [Fact]
+ public void OnboardTemperature_NoHardFault_AfterFirstRead()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.OnboardTemperature)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur reading internal temperature");
+ }
+
+ [Fact]
+ public void OnboardTemperature_Uart0_ProducesTemperatureReading()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.OnboardTemperature)!;
+
+ pico.LoadFlash(flash);
+
+ // Reads ADC4 (internal temp sensor) and prints Celsius/Fahrenheit
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("onboard_temperature must print a temperature reading over UART0");
+ }
+
+ [Fact]
+ public void OnboardTemperature_Cpu_IsAliveAfterMultipleReads()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.OnboardTemperature)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(3_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain in SRAM after multiple ADC temperature reads");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/BlinkTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/BlinkTests.cs
new file mode 100644
index 0000000..bee2604
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/BlinkTests.cs
@@ -0,0 +1,88 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for the canonical blink example (pico-examples/blink).
+/// Firmware: GPIO 25 goes HIGH for 250 ms, goes LOW for 250 ms, repeat at 2 Hz.
+/// LED_DELAY_MS = 250; PICO_DEFAULT_LED_PIN = 25 on the Pico board.
+///
+[Trait("Category", "Integration")]
+public sealed class BlinkTests
+{
+ [Fact]
+ public void Blink_NoHardFault_AfterTwoFullCycles()
+ {
+ using var pico = new PicoSimulation();
+ var uf2 = PicoExamplesFirmware.Blink;
+ var flash = RP2040Machine.Uf2ToFlash(uf2);
+ flash.Should().NotBeNull("blink.uf2 must decode to a valid flash image");
+
+ pico.LoadFlash(flash!);
+
+ // Run 600 ms — enough for one full ON/OFF cycle (250 ms + 250 ms + startup margin)
+ pico.RunMilliseconds(600);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "a HardFault (IPSR == 3) must never occur");
+ }
+
+ [Fact]
+ public void Blink_Gpio25_IsHighAfter600ms()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.Blink)!;
+
+ pico.LoadFlash(flash);
+
+ // blink sets GPIO 25 HIGH immediately, then sleeps 250 ms before going LOW
+ pico.RunMilliseconds(150);
+
+ pico.Gpio[25].Should().BeOutput("GPIO 25 must be configured as OUTPUT by blink firmware");
+ pico.Gpio[25].Should().BeHigh("GPIO 25 (onboard LED) should be HIGH for the first 250 ms");
+ }
+
+ [Fact]
+ public void Blink_Gpio25_IsLowAfter350ms()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.Blink)!;
+
+ pico.LoadFlash(flash);
+
+ // After 450 ms the first LOW phase is firmly underway
+ // (HIGH phase: startup~100ms to ~350ms; LOW phase: ~350ms to ~600ms)
+ pico.RunMilliseconds(450);
+
+ pico.Gpio[25].Should().BeLow("GPIO 25 should be LOW during the second half of the blink cycle");
+ }
+
+ [Fact]
+ public void Blink_Gpio25_TogglesAtLeastTwice_InTwoSeconds()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.Blink)!;
+
+ pico.LoadFlash(flash);
+
+ // Sample GPIO 25 every 100 ms and count edges over 2 seconds
+ int toggles = 0;
+ bool? prev = null;
+
+ for (int i = 0; i < 20; i++)
+ {
+ pico.RunMilliseconds(100);
+ bool current = pico.Gpio[25].DigitalValue;
+ if (prev.HasValue && current != prev.Value)
+ toggles++;
+ prev = current;
+ }
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur during blink");
+ // At 2 Hz the LED toggles 8 times in 2 s; allow margin and expect at least 4
+ toggles.Should().BeGreaterThanOrEqualTo(4,
+ "GPIO 25 should toggle at least 4 times (2 full cycles) over 2 seconds of simulated time");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/CircuitPythonBootTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/CircuitPythonBootTests.cs
new file mode 100644
index 0000000..da8e683
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/CircuitPythonBootTests.cs
@@ -0,0 +1,129 @@
+using RP2040Sharp.IntegrationTests.Infrastructure;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests that boot real CircuitPython firmware on the RP2040 emulator
+/// and verify the REPL prompt, USB-CDC enumeration, and basic boot behaviour.
+///
+/// These tests require network access on the first run to download the firmware from
+/// downloads.circuitpython.org. Subsequent runs use the cached UF2 in the system
+/// temp directory.
+///
+/// Set environment variable SKIP_INTEGRATION_TESTS=1 to skip all tests in CI pipelines
+/// that cannot access the internet.
+///
+[Trait("Category", "Integration")]
+public sealed class CircuitPythonBootTests
+{
+ private static bool ShouldSkip =>
+ Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1";
+
+ // ── USB-CDC enumeration ───────────────────────────────────────────────────
+
+ [Theory]
+ [InlineData("9.2.1")]
+ public async Task CircuitPython_UsbCdcEnumerates(string version)
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await CircuitPythonRunner.CreateAsync(version);
+ if (runner is null) return;
+
+ for (var i = 0; i < 20 && !runner.UsbCdc.IsConnected; i++)
+ runner.Simulation.RunMilliseconds(100);
+
+ runner.UsbCdc.IsConnected.Should().BeTrue(
+ $"CircuitPython {version} USB CDC should complete enumeration within 2 s");
+ }
+
+ // ── REPL prompt ───────────────────────────────────────────────────────────
+
+ [Theory]
+ [InlineData("9.2.1")]
+ public async Task CircuitPython_BootsAndShowsReplPrompt(string version)
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await CircuitPythonRunner.CreateAsync(version);
+ if (runner is null) return;
+
+ var booted = runner.WaitForPrompt(timeoutMs: 20_000);
+
+ booted.Should().BeTrue(
+ $"CircuitPython {version} should produce a REPL prompt within 20 s of simulated time");
+ }
+
+ // ── Version header ────────────────────────────────────────────────────────
+
+ [Theory]
+ [InlineData("9.2.1")]
+ public async Task CircuitPython_OutputsVersionHeader(string version)
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await CircuitPythonRunner.CreateAsync(version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt(timeoutMs: 20_000)
+ .Should().BeTrue($"CircuitPython {version} must reach REPL");
+
+ var found = runner.ExecuteAndWait("import sys; print(sys.version)", "CircuitPython");
+ found.Should().BeTrue("sys.version should contain 'CircuitPython'");
+ }
+
+ // ── No hard fault ─────────────────────────────────────────────────────────
+
+ [Theory]
+ [InlineData("9.2.1")]
+ public async Task CircuitPython_NoHardFault_AfterStartup(string version)
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await CircuitPythonRunner.CreateAsync(version);
+ if (runner is null) return;
+
+ runner.Simulation.RunMilliseconds(500);
+
+ runner.Simulation.Cpu.Registers.IPSR.Should().NotBe(3u,
+ $"CircuitPython {version} must not trigger a HardFault during startup");
+ }
+
+ // ── Hello World ───────────────────────────────────────────────────────────
+
+ [Theory]
+ [InlineData("9.2.1")]
+ public async Task CircuitPython_OutputsHelloWorld(string version)
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await CircuitPythonRunner.CreateAsync(version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt(timeoutMs: 20_000)
+ .Should().BeTrue($"CircuitPython {version} must reach REPL");
+
+ var found = runner.ExecuteAndWait("print('Hello, CircuitPython!')", "Hello, CircuitPython!");
+ found.Should().BeTrue("print() output should be captured");
+ }
+
+ // ── sys.platform ─────────────────────────────────────────────────────────
+
+ [Theory]
+ [InlineData("9.2.1")]
+ public async Task CircuitPython_SysPlatform_IsRP2040(string version)
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await CircuitPythonRunner.CreateAsync(version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt(timeoutMs: 20_000)
+ .Should().BeTrue($"CircuitPython {version} must reach REPL");
+
+ // CircuitPython reports "RP2040" via sys.platform on Pico
+ // In CircuitPython 9.x, sys.platform returns "RP2040" (upper-case).
+ var found = runner.ExecuteAndWait("import sys; print(sys.platform)", "RP2040");
+ found.Should().BeTrue("sys.platform should be 'RP2040' for CircuitPython on Pico");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/CircuitPythonReplTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/CircuitPythonReplTests.cs
new file mode 100644
index 0000000..7417173
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/CircuitPythonReplTests.cs
@@ -0,0 +1,235 @@
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Infrastructure;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// REPL-level integration tests for CircuitPython on the RP2040 emulator.
+///
+/// The key test in this suite exercises the official Adafruit "blink LED" example:
+///
+///
+/// # SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries
+/// # SPDX-License-Identifier: MIT
+/// """Example for Pico. Turns on the built-in LED."""
+/// import board
+/// import digitalio
+///
+/// led = digitalio.DigitalInOut(board.LED)
+/// led.direction = digitalio.Direction.OUTPUT
+///
+/// while True:
+/// led.value = True
+///
+///
+/// The while True loop is intentionally not injected; instead the test
+/// verifies that the LED (GPIO 25) is driven high after led.value = True
+/// executes, which is the meaningful observable behaviour of the snippet.
+///
+[Trait("Category", "Integration")]
+public sealed class CircuitPythonReplTests
+{
+ private static bool ShouldSkip =>
+ Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1";
+
+ private const string Version = "9.2.1";
+
+ private static async Task BootToReplAsync()
+ {
+ var runner = await CircuitPythonRunner.CreateAsync(Version);
+ if (runner is null) return null;
+ runner.WaitForPrompt(timeoutMs: 20_000)
+ .Should().BeTrue($"CircuitPython {Version} must reach REPL within 20 s");
+ // Run 200 ms of simulation to drain any pending USB ZLP reads that
+ // accumulated during WaitForPrompt; without this the first Execute()
+ // may only deliver a partial command to the firmware.
+ runner.Simulation.RunMilliseconds(200);
+ runner.UsbCdc.Clear();
+ return runner;
+ }
+
+ // ── Basic arithmetic ──────────────────────────────────────────────────────
+
+ [Fact]
+ public async Task Repl_CanEvaluateArithmeticExpression()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplAsync();
+ if (runner is null) return;
+
+ var found = runner.ExecuteAndWait("print(1 + 2)", "3");
+ found.Should().BeTrue("1 + 2 should evaluate to 3");
+ }
+
+ // ── sys.version ───────────────────────────────────────────────────────────
+
+ [Fact]
+ public async Task Repl_CanReadSysVersion()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplAsync();
+ if (runner is null) return;
+
+ var found = runner.ExecuteAndWait("import sys; print(sys.version)", "CircuitPython");
+ found.Should().BeTrue("sys.version should contain 'CircuitPython'");
+ }
+
+ // ── Function definition ───────────────────────────────────────────────────
+
+ [Fact]
+ public async Task Repl_CanDefineAndCallFunction()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplAsync();
+ if (runner is null) return;
+
+ runner.ExecuteCompound("def greet(name): return 'Hi ' + name");
+
+ var found = runner.ExecuteAndWait("print(greet('world'))", "Hi world");
+ found.Should().BeTrue("user-defined function should be callable from REPL");
+ }
+
+ // ── Variable state ────────────────────────────────────────────────────────
+
+ [Fact]
+ public async Task Repl_MultipleCommands_ProduceCorrectOutput()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplAsync();
+ if (runner is null) return;
+
+ runner.ExecuteAndWait("x = 10", ">>> ");
+ runner.ExecuteAndWait("y = 32", ">>> ");
+ var found = runner.ExecuteAndWait("print(x + y)", "42");
+ found.Should().BeTrue("accumulated variable state should be preserved across REPL lines");
+ }
+
+ // ── Adafruit LED example ──────────────────────────────────────────────────
+ //
+ // SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries
+ // SPDX-License-Identifier: MIT
+ //
+ // Original: "Example for Pico. Turns on the built-in LED."
+ //
+ // The infinite loop (while True: led.value = True) is omitted intentionally
+ // because it would never yield back to the REPL; all meaningful state
+ // is established before it.
+
+ ///
+ /// Imports board and digitalio, creates a DigitalInOut on board.LED
+ /// (GPIO 25), sets its direction to OUTPUT, and asserts that GPIO 25 reads high after
+ /// assigning led.value = True.
+ ///
+ [Fact]
+ public async Task Repl_LedCode_SetsBoardLedHigh()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplAsync();
+ if (runner is null) return;
+
+ // import board
+ runner.Execute("import board");
+ runner.WaitForPrompt(timeoutMs: 3_000)
+ .Should().BeTrue("'import board' should not raise an error");
+
+ // import digitalio
+ runner.Execute("import digitalio");
+ runner.WaitForPrompt(timeoutMs: 3_000)
+ .Should().BeTrue("'import digitalio' should not raise an error");
+
+ // led = digitalio.DigitalInOut(board.LED)
+ runner.Execute("led = digitalio.DigitalInOut(board.LED)");
+ runner.WaitForPrompt(timeoutMs: 3_000)
+ .Should().BeTrue("DigitalInOut constructor should succeed on board.LED (GPIO 25)");
+
+ // led.direction = digitalio.Direction.OUTPUT
+ runner.Execute("led.direction = digitalio.Direction.OUTPUT");
+ runner.WaitForPrompt(timeoutMs: 3_000)
+ .Should().BeTrue("Setting direction to OUTPUT should succeed");
+
+ // led.value = True — this is the observable action of the snippet
+ runner.Execute("led.value = True");
+ runner.WaitForPrompt(timeoutMs: 3_000)
+ .Should().BeTrue("Setting led.value = True should succeed");
+
+ // Allow any pending GPIO writes to propagate
+ runner.Simulation.RunMilliseconds(10);
+
+ // GPIO 25 is the onboard LED on the Pico; it should now be driven high
+ runner.Simulation.Gpio[25].Should().BeHigh(
+ "board.LED (GPIO 25) must be high after led.value = True");
+ }
+
+ ///
+ /// Verifies that the LED can be turned off after being turned on — tests the
+ /// complementary path of the same digitalio pattern.
+ ///
+ [Fact]
+ public async Task Repl_LedCode_ClearsBoardLedLow()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplAsync();
+ if (runner is null) return;
+
+ runner.Execute("import board");
+ runner.WaitForPrompt(timeoutMs: 3_000);
+ runner.Execute("import digitalio");
+ runner.WaitForPrompt(timeoutMs: 3_000);
+ runner.Execute("led = digitalio.DigitalInOut(board.LED)");
+ runner.WaitForPrompt(timeoutMs: 3_000);
+ runner.Execute("led.direction = digitalio.Direction.OUTPUT");
+ runner.WaitForPrompt(timeoutMs: 3_000);
+
+ runner.Execute("led.value = True");
+ runner.WaitForPrompt(timeoutMs: 3_000);
+ runner.Execute("led.value = False");
+ runner.WaitForPrompt(timeoutMs: 3_000);
+
+ runner.Simulation.RunMilliseconds(10);
+
+ runner.Simulation.Gpio[25].Should().BeLow(
+ "GPIO 25 must be low after led.value = False");
+ }
+
+ // ── board module ──────────────────────────────────────────────────────────
+
+ [Fact]
+ public async Task Repl_BoardModule_ExposesLedPin()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplAsync();
+ if (runner is null) return;
+
+ runner.Execute("import board");
+ runner.WaitForPrompt(timeoutMs: 3_000);
+
+ // board.LED should be a valid pin object (its repr contains "GP25" or "LED")
+ // In CircuitPython 9.x on Pico, print(board.LED) outputs "board.LED" (the attribute path).
+ var found = runner.ExecuteAndWait("print(board.LED)", "board.LED");
+ found.Should().BeTrue("board.LED should print 'board.LED'");
+ }
+
+ // ── Multiline for-loop ────────────────────────────────────────────────────
+
+ [Fact]
+ public async Task Repl_ForLoop_PrintsAllLines()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplAsync();
+ if (runner is null) return;
+
+ runner.ExecuteCompound("for i in range(3): print('line', i)");
+ var found = runner.WaitForOutput(text =>
+ text.Contains("line 0") && text.Contains("line 1") && text.Contains("line 2"));
+
+ found.Should().BeTrue("all three for-loop iterations should appear in output");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/CircuitPythonScriptTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/CircuitPythonScriptTests.cs
new file mode 100644
index 0000000..442a079
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/CircuitPythonScriptTests.cs
@@ -0,0 +1,215 @@
+using FluentAssertions;
+using RP2040Sharp.IntegrationTests.Infrastructure;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Tests for CircuitPython's script-execution pipeline and QSPI flash filesystem.
+///
+/// Filesystem write support:
+/// CircuitPython's FAT filesystem (CIRCUITPY drive) flushes data to the RP2040's
+/// QSPI flash via the SSI peripheral. The SSI now emulates the full W25Q flash
+/// command set (WRITE_ENABLE, SECTOR_ERASE, PAGE_PROGRAM, READ_DATA, etc.), so
+/// filesystem writes made via the REPL persist across soft resets.
+///
+/// Test categories:
+///
+/// - Default boot: the code.py shipped with CircuitPython 9.2.1
+/// - Read-side filesystem: listing and reading the firmware's files
+/// - WriteFile + SoftReset: write new scripts via REPL and verify auto-execution
+///
+///
+[Trait("Category", "Integration")]
+public sealed class CircuitPythonScriptTests
+{
+ private static bool ShouldSkip =>
+ Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1";
+
+ private const string Version = "9.2.1";
+
+ // ── Shared boot helper ────────────────────────────────────────────────────
+
+ private static async Task BootToReplAsync()
+ {
+ var runner = await CircuitPythonRunner.CreateAsync(Version);
+ if (runner is null) return null;
+ runner.WaitForPrompt(timeoutMs: 20_000)
+ .Should().BeTrue($"CircuitPython {Version} must reach REPL within 20 s");
+ runner.Simulation.RunMilliseconds(200);
+ runner.UsbCdc.Clear();
+ return runner;
+ }
+
+ // ── Default boot behaviour ────────────────────────────────────────────────
+
+ ///
+ /// CircuitPython 9.2.1 ships with a code.py that prints "Hello World!".
+ /// Verifies the firmware's default script runs automatically on soft reset.
+ ///
+ [Fact]
+ public async Task Script_DefaultCodePy_RunsOnSoftReset()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await CircuitPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt(timeoutMs: 20_000)
+ .Should().BeTrue("CircuitPython must reach REPL");
+
+ runner.SoftReset(timeoutMs: 20_000)
+ .Should().BeTrue("CircuitPython must return to REPL after running code.py");
+
+ var text = runner.UsbCdc.IsConnected ? runner.UsbCdc.Text : runner.Uart.Text;
+ text.Should().Contain("Hello World!",
+ "the default code.py shipped with CircuitPython 9.2.1 must print 'Hello World!'");
+ }
+
+ // ── Read-side filesystem ──────────────────────────────────────────────────
+
+ ///
+ /// Verifies that the CIRCUITPY filesystem is mounted and code.py is visible
+ /// in the root directory listing.
+ ///
+ [Fact]
+ public async Task Script_Filesystem_ListdirShowsCodePy()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplAsync();
+ if (runner is null) return;
+
+ var found = runner.ExecuteAndWait("import os; print(os.listdir('/'))", "code.py");
+ found.Should().BeTrue("os.listdir('/') must include 'code.py' from the firmware image");
+ }
+
+ ///
+ /// Opens the built-in code.py via open() and verifies its content
+ /// contains the expected print statement.
+ ///
+ [Fact]
+ public async Task Script_DefaultCodePy_ContentIsReadable()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplAsync();
+ if (runner is null) return;
+
+ var found = runner.ExecuteAndWait(
+ "print(open('code.py').read())",
+ "Hello World");
+ found.Should().BeTrue("reading code.py must return its source containing 'Hello World'");
+ }
+
+ ///
+ /// Executes the built-in code.py in-session via exec(open('code.py').read())
+ /// and verifies it produces the expected output.
+ ///
+ [Fact]
+ public async Task Script_DefaultCodePy_ExecRunsInSession()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplAsync();
+ if (runner is null) return;
+
+ var found = runner.ExecuteAndWait(
+ "exec(open('code.py').read())",
+ "Hello World!");
+ found.Should().BeTrue("exec(open('code.py').read()) must reproduce 'Hello World!'");
+ }
+
+ // ── Shared boot helper (writable FS) ─────────────────────────────────────
+
+ ///
+ /// Boot helper for tests that need to write files.
+ /// Boots with USB-CDC so CircuitPython initialises normally, then injects a
+ /// boot.py that calls storage.disable_usb_drive() into the FAT
+ /// before performing a soft reset so the file takes effect.
+ /// After the second boot the REPL is on USB-CDC and the filesystem is writable
+ /// from Python code.
+ ///
+ private static async Task BootToReplWritableAsync()
+ {
+ // CreateWithWritableFsAsync boots CircuitPython, injects boot.py, soft-resets so
+ // boot.py runs (disabling USB-MSC), then waits for the REPL again before returning.
+ return await CircuitPythonRunner.CreateWithWritableFsAsync(Version);
+ }
+
+ // ── WriteFile + SoftReset (QSPI flash write) ──────────────────────────────
+
+ ///
+ /// Verifies that the CIRCUITPY filesystem is writable from Python code.
+ /// Runs without USB host so CircuitPython doesn't lock the FAT via USB-MSC.
+ ///
+ [Fact]
+ public async Task Script_Filesystem_IsWritableFromPython()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplWritableAsync();
+ if (runner is null) return;
+
+ // Write a probe file and immediately read it back in the same session.
+ runner.WriteFile("write_probe.txt", "probe_content_xyz");
+
+ runner.Simulation.RunMilliseconds(200);
+ runner.UsbCdc.Clear();
+
+ var found = runner.ExecuteAndWait(
+ "print(open('write_probe.txt').read())",
+ "probe_content_xyz");
+
+ found.Should().BeTrue(
+ "the CIRCUITPY filesystem must be writable from Python when USB host is absent");
+ }
+
+ ///
+ /// Writes a new code.py via REPL, performs a soft reset, and verifies the
+ /// written script runs automatically. Exercises the full QSPI flash write path:
+ /// CircuitPython flushes the FAT filesystem via the bootrom flash_range_program hook,
+ /// which the emulator applies directly to the flash image.
+ ///
+ [Fact]
+ public async Task Script_WriteCodePy_RunsAfterSoftReset()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplWritableAsync();
+ if (runner is null) return;
+
+ runner.WriteFile("code.py", "print('written by WriteFile')\n")
+ .Should().BeTrue("WriteFile must succeed on a ready REPL");
+
+ runner.SoftReset(timeoutMs: 20_000)
+ .Should().BeTrue("CircuitPython must return to REPL after soft reset");
+
+ var text = runner.UsbCdc.IsConnected ? runner.UsbCdc.Text : runner.Uart.Text;
+ text.Should().Contain("written by WriteFile",
+ "the new code.py written via REPL must run automatically after soft reset");
+ }
+
+ ///
+ /// Writes a code.py that computes an arithmetic expression, soft resets,
+ /// and verifies the correct result is printed.
+ ///
+ [Fact]
+ public async Task Script_WriteCodePy_ComputesAndPrintsArithmetic()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await BootToReplWritableAsync();
+ if (runner is null) return;
+
+ runner.WriteFile("code.py", "x = 6 * 7\nprint('result:', x)\n")
+ .Should().BeTrue();
+
+ runner.SoftReset(timeoutMs: 20_000)
+ .Should().BeTrue("must return to REPL after soft reset");
+
+ var text = runner.UsbCdc.IsConnected ? runner.UsbCdc.Text : runner.Uart.Text;
+ text.Should().Contain("result: 42",
+ "code.py must compute 6 * 7 = 42 and print it on soft reset");
+ }
+}
+
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/ClocksTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/ClocksTests.cs
new file mode 100644
index 0000000..9ada9c1
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/ClocksTests.cs
@@ -0,0 +1,57 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for Clock examples from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class ClocksTests
+{
+ // ── hello_48MHz ───────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Hello48MHz_NoHardFault_AfterClockSwitch()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.Hello48MHz)!;
+
+ pico.LoadFlash(flash);
+
+ // The firmware reconfigures the system clock to 48 MHz (from 125 MHz)
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur after clock reconfiguration");
+ }
+
+ [Fact]
+ public void Hello48MHz_Uart0_PrintsFrequencyInfo()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.Hello48MHz)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_48MHz prints the measured clock frequencies after the switch
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("hello_48MHz must print clock frequency info over UART0");
+ }
+
+ [Fact]
+ public void Hello48MHz_Cpu_SurvivesClockReconfiguration()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.Hello48MHz)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(2_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain in SRAM after clock source switch");
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "no HardFault after clock change");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/DividerTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/DividerTests.cs
new file mode 100644
index 0000000..a79ed03
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/DividerTests.cs
@@ -0,0 +1,67 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for the hardware-divider (SIO) example from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class DividerTests
+{
+ // ── hello_divider ─────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HelloDivider_NoHardFault_AfterExecution()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloDivider)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur in hello_divider");
+ }
+
+ [Fact]
+ public void HelloDivider_Uart0_ProducesOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloDivider)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_divider performs signed/unsigned division using SIO hardware and prints results
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("hello_divider must print division results over UART0");
+ }
+
+ [Fact]
+ public void HelloDivider_Uart0_ContainsDivisionResults()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloDivider)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(3_000);
+
+ pico.Uart0.ByteCount.Should().BeGreaterThan(0,
+ "hello_divider must have produced output after hardware division");
+ }
+
+ [Fact]
+ public void HelloDivider_Cpu_FinishesWithoutStackOverflow()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloDivider)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(2_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must stay in SRAM after SIO divider operations");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/DmaTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/DmaTests.cs
new file mode 100644
index 0000000..8abc0e0
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/DmaTests.cs
@@ -0,0 +1,86 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for DMA examples from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class DmaTests
+{
+ // ── hello_dma ─────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HelloDma_NoHardFault_AfterTransfer()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloDma)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur during DMA transfer");
+ }
+
+ [Fact]
+ public void HelloDma_Uart0_ProducesOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloDma)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_dma copies data and prints a status/result line over UART0
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("hello_dma must output a result over UART0 after the DMA transfer");
+ }
+
+ [Fact]
+ public void HelloDma_Cpu_IsAliveAfterCompletion()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloDma)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(1_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain in SRAM after DMA transfer");
+ }
+
+ // ── dma_channel_irq ───────────────────────────────────────────────────────
+
+ [Fact]
+ public void DmaChannelIrq_NoHardFault_AfterTransfer()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.DmaChannelIrq)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(1_000);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur in DMA IRQ example");
+ }
+
+ [Fact]
+ public void DmaChannelIrq_Cpu_IsAliveAfterRepeatedTransfers()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.DmaChannelIrq)!;
+
+ pico.LoadFlash(flash);
+
+ // dma_channel_irq uses DMA → PIO → LED (no UART output).
+ // The IRQ handler restarts the DMA channel in a loop — verify CPU stays alive.
+ pico.RunMilliseconds(2_000);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u,
+ "HardFault must not occur during repeated DMA IRQ-driven PIO transfers");
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain in SRAM after repeated DMA IRQ rounds");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/FatVolumeTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/FatVolumeTests.cs
new file mode 100644
index 0000000..365c220
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/FatVolumeTests.cs
@@ -0,0 +1,180 @@
+using FluentAssertions;
+using RP2040Sharp.IntegrationTests.Infrastructure;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Unit tests for using an in-memory FAT12 disk image.
+/// These tests run entirely offline (no firmware download required).
+///
+public sealed class FatVolumeTests
+{
+ // A minimal 256 KB FAT12 disk (512 sectors × 512 bytes).
+ private const int SECTOR_BYTES = 512;
+ private const int TOTAL_SECTORS = 512;
+ private const int RESERVED = 1; // 1 reserved sector (VBR)
+ private const int NUM_FATS = 2;
+ private const int ROOT_ENTRIES = 32; // 1 sector of root dir
+ private const int SECTORS_PER_CLUSTER = 1;
+ private const int SECTORS_PER_FAT = 1; // FAT12: 512 × 8 / 12 ≈ 341 entries → fits in 1 sector
+
+ private static byte[][] BuildDisk()
+ {
+ var disk = new byte[TOTAL_SECTORS][];
+ for (var i = 0; i < TOTAL_SECTORS; i++) disk[i] = new byte[SECTOR_BYTES];
+
+ // Write VBR / BPB (BIOS Parameter Block).
+ var vbr = disk[0];
+ // Jump boot
+ vbr[0] = 0xEB; vbr[1] = 0x58; vbr[2] = 0x90;
+ // OEM name "MSDOS5.0"
+ System.Text.Encoding.ASCII.GetBytes("MSDOS5.0", 0, 8, vbr, 3);
+ // Bytes per sector = 512
+ vbr[11] = 0x00; vbr[12] = 0x02;
+ // Sectors per cluster = 1
+ vbr[13] = SECTORS_PER_CLUSTER;
+ // Reserved sectors = 1
+ vbr[14] = RESERVED; vbr[15] = 0x00;
+ // Number of FATs = 2
+ vbr[16] = NUM_FATS;
+ // Root entry count = 32
+ vbr[17] = ROOT_ENTRIES; vbr[18] = 0x00;
+ // Total sectors16 = 512
+ vbr[19] = (byte)(TOTAL_SECTORS & 0xFF); vbr[20] = (byte)(TOTAL_SECTORS >> 8);
+ // Media type
+ vbr[21] = 0xF8;
+ // Sectors per FAT = 1
+ vbr[22] = SECTORS_PER_FAT; vbr[23] = 0x00;
+ // Boot sector signature
+ vbr[510] = 0x55; vbr[511] = 0xAA;
+
+ // FAT1: sector 1. Mark cluster 0 (media) and cluster 1 (reserved) as used.
+ var fat1 = disk[1];
+ fat1[0] = 0xF8; fat1[1] = 0xFF; fat1[2] = 0xFF; // clusters 0+1 reserved
+
+ // FAT2: sector 2 (copy).
+ disk[2][0] = 0xF8; disk[2][1] = 0xFF; disk[2][2] = 0xFF;
+
+ // Root directory: sector 3. (Empty for now — tests will write to it.)
+
+ return disk;
+ }
+
+ private static (FatVolume fat, byte[][] disk) OpenDisk()
+ {
+ var disk = BuildDisk();
+ var fat = FatVolume.Open(
+ lba => disk[lba],
+ (lba, data) => { var sec = new byte[SECTOR_BYTES]; data.CopyTo(sec, 0); disk[lba] = sec; });
+ return (fat!, disk);
+ }
+
+ [Fact]
+ public void PadName83_ShortName_PadsCorrectly()
+ {
+ FatVolume.PadName83("code.py").Should().Be("CODE PY ");
+ FatVolume.PadName83("main.py").Should().Be("MAIN PY ");
+ FatVolume.PadName83("readme.txt").Should().Be("README TXT");
+ }
+
+ [Fact]
+ public void PadName83_NoExtension_PadsWithSpaces()
+ {
+ FatVolume.PadName83("boot").Should().Be("BOOT ");
+ }
+
+ [Fact]
+ public void Open_ValidVbr_ReturnsNonNullAndIsValid()
+ {
+ var (fat, _) = OpenDisk();
+ fat.Should().NotBeNull();
+ fat.IsValid.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Open_EmptyDisk_ReturnsNull()
+ {
+ var empty = new byte[SECTOR_BYTES];
+ var fat = FatVolume.Open(_ => empty, (_, __) => { });
+ fat.Should().BeNull("an all-zero VBR is not a valid FAT filesystem");
+ }
+
+ [Fact]
+ public void WriteFile_NewFile_AppearInRootDirectory()
+ {
+ var (fat, disk) = OpenDisk();
+
+ var content = "print('hello')\n"u8.ToArray();
+ fat.WriteFile("code.py", content).Should().BeTrue();
+
+ // Root dir is sector 3 (reserved=1, FAT×2=2 → sector 3).
+ var rootDir = disk[3];
+ var name = System.Text.Encoding.ASCII.GetString(rootDir, 0, 11);
+ name.Should().Be("CODE PY ", "file name must be stored in 8.3 format");
+ rootDir[11].Should().Be(0x20, "archive attribute must be set");
+
+ // First cluster stored at offset 26.
+ var firstCluster = rootDir[26] | (rootDir[27] << 8);
+ firstCluster.Should().BeGreaterThanOrEqualTo(2, "first cluster must be in the data area");
+
+ // File size at offset 28.
+ var fileSize = rootDir[28] | (rootDir[29] << 8) | (rootDir[30] << 16) | (rootDir[31] << 24);
+ fileSize.Should().Be(content.Length);
+ }
+
+ [Fact]
+ public void WriteFile_SmallContent_DataWrittenToCluster()
+ {
+ var (fat, disk) = OpenDisk();
+
+ var content = "hello\n"u8.ToArray();
+ fat.WriteFile("test.txt", content).Should().BeTrue();
+
+ // Root dir sector is 3; first cluster starts at data sector = 3 (root) + 1 = 4.
+ // Cluster 2 → data sector index 4.
+ var rootDir = disk[3];
+ var firstCluster = rootDir[26] | (rootDir[27] << 8);
+ var dataStart = RESERVED + NUM_FATS * SECTORS_PER_FAT +
+ (ROOT_ENTRIES * 32 + SECTOR_BYTES - 1) / SECTOR_BYTES;
+ var dataSector = dataStart + (firstCluster - 2) * SECTORS_PER_CLUSTER;
+
+ var actual = disk[dataSector][..content.Length];
+ actual.Should().Equal(content, "file content must be written to the cluster");
+ }
+
+ [Fact]
+ public void WriteFile_OverwriteExistingFile_UpdatesContent()
+ {
+ var (fat, disk) = OpenDisk();
+
+ fat.WriteFile("code.py", "v1\n"u8.ToArray()).Should().BeTrue();
+ fat.WriteFile("code.py", "v2 longer\n"u8.ToArray()).Should().BeTrue();
+
+ var rootDir = disk[3];
+ var fileSize = rootDir[28] | (rootDir[29] << 8) | (rootDir[30] << 16) | (rootDir[31] << 24);
+ fileSize.Should().Be("v2 longer\n"u8.Length);
+
+ var firstCluster = rootDir[26] | (rootDir[27] << 8);
+ var dataStart = RESERVED + NUM_FATS * SECTORS_PER_FAT +
+ (ROOT_ENTRIES * 32 + SECTOR_BYTES - 1) / SECTOR_BYTES;
+ var dataSector = dataStart + (firstCluster - 2) * SECTORS_PER_CLUSTER;
+ var actual = disk[dataSector][.."v2 longer\n".Length];
+ System.Text.Encoding.UTF8.GetString(actual).Should().Be("v2 longer\n");
+ }
+
+ [Fact]
+ public void WriteFile_MultipleFiles_BothPresent()
+ {
+ var (fat, disk) = OpenDisk();
+
+ fat.WriteFile("code.py", "a"u8.ToArray()).Should().BeTrue();
+ fat.WriteFile("main.py", "b"u8.ToArray()).Should().BeTrue();
+
+ var rootDir = disk[3];
+ var name0 = System.Text.Encoding.ASCII.GetString(rootDir, 0, 11);
+ var name1 = System.Text.Encoding.ASCII.GetString(rootDir, 32, 11);
+
+ name0.Should().Be("CODE PY ");
+ name1.Should().Be("MAIN PY ");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/GpioTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/GpioTests.cs
new file mode 100644
index 0000000..6c77bd8
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/GpioTests.cs
@@ -0,0 +1,101 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for GPIO examples from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class GpioTests
+{
+ // ── blink_simple ──────────────────────────────────────────────────────────
+
+ [Fact]
+ public void BlinkSimple_NoHardFault_AfterOneCycle()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.BlinkSimple)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(600);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault (IPSR == 3) must never occur");
+ }
+
+ [Fact]
+ public void BlinkSimple_Gpio25_IsOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.BlinkSimple)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(150);
+
+ pico.Gpio[25].Should().BeOutput("blink_simple configures GPIO 25 as OUTPUT");
+ }
+
+ [Fact]
+ public void BlinkSimple_Gpio25_TogglesOverTime()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.BlinkSimple)!;
+
+ pico.LoadFlash(flash);
+
+ int toggles = 0;
+ bool? prev = null;
+
+ for (int i = 0; i < 20; i++)
+ {
+ pico.RunMilliseconds(100);
+ bool current = pico.Gpio[25].DigitalValue;
+ if (prev.HasValue && current != prev.Value)
+ toggles++;
+ prev = current;
+ }
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur");
+ toggles.Should().BeGreaterThanOrEqualTo(2, "GPIO 25 should toggle multiple times over 2 seconds");
+ }
+
+ // ── hello_gpio_irq ────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HelloGpioIrq_NoHardFault_AfterInit()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloGpioIrq)!;
+
+ pico.LoadFlash(flash);
+
+ // Allow time for GPIO IRQ setup (the example waits for a button press on GPIO 0)
+ pico.RunMilliseconds(200);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur during GPIO IRQ init");
+ }
+
+ [Fact]
+ public void HelloGpioIrq_InjectedEdge_TriggersOutputChange()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloGpioIrq)!;
+
+ pico.LoadFlash(flash);
+
+ // Let the firmware initialise GPIO IRQ on pin 0
+ pico.RunMilliseconds(200);
+
+ // Capture GPIO 25 state before injecting a falling edge on GPIO 0
+ bool before = pico.Gpio[25].DigitalValue;
+
+ // Inject a falling edge on GP0 (button press) — the ISR toggles GPIO 25
+ pico.Gpio[0].ForceInput(false); // drive GP0 LOW to simulate button press
+ pico.RunMilliseconds(5);
+
+ // After the edge the firmware ISR should have toggled something; at minimum no HardFault
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur after GPIO IRQ fires");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/InterpTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/InterpTests.cs
new file mode 100644
index 0000000..ee59abb
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/InterpTests.cs
@@ -0,0 +1,67 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for the Interpolator example from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class InterpTests
+{
+ // ── hello_interp ──────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HelloInterp_NoHardFault_AfterExecution()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloInterp)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur in hello_interp");
+ }
+
+ [Fact]
+ public void HelloInterp_Uart0_ProducesOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloInterp)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_interp computes interpolated values and prints them to UART0
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("hello_interp must print interpolator results over UART0");
+ }
+
+ [Fact]
+ public void HelloInterp_Uart0_ContainsNumericOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloInterp)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(3_000);
+
+ // Interpolator results are printed as decimal or hex numbers
+ pico.Uart0.ByteCount.Should().BeGreaterThan(0, "hello_interp must have produced UART output");
+ }
+
+ [Fact]
+ public void HelloInterp_Cpu_CompletesWithoutStackOverflow()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloInterp)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(2_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must stay within SRAM after interpolator computations");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonBootTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonBootTests.cs
new file mode 100644
index 0000000..6ea9b79
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonBootTests.cs
@@ -0,0 +1,93 @@
+using RP2040Sharp.IntegrationTests.Infrastructure;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests that boot real MicroPython firmware on the RP2040 emulator
+/// and verify the REPL prompt and basic output via UART.
+///
+/// These tests require network access on the first run to download the firmware.
+/// Subsequent runs use the cached UF2 in the system temp directory.
+///
+/// Set environment variable SKIP_INTEGRATION_TESTS=1 to skip all tests in CI pipelines
+/// that cannot access GitHub Releases.
+///
+[Trait("Category", "Integration")]
+public sealed class MicroPythonBootTests
+{
+ private static bool ShouldSkip =>
+ Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1";
+
+ [Theory]
+ [InlineData("v1.21.0")]
+ public async Task MicroPython_UsbCdcEnumerates(string version)
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(version);
+ if (runner is null) return;
+
+ // Run in 100ms batches so each Tick() delivers one pending USB IN transfer
+ for (var i = 0; i < 20 && !runner.UsbCdc.IsConnected; i++)
+ runner.Simulation.RunMilliseconds(100);
+
+ runner.UsbCdc.IsConnected.Should().BeTrue(
+ $"USB CDC should complete enumeration within 2 seconds of simulated time");
+ }
+
+ [Theory]
+ [InlineData("v1.19.1")]
+ [InlineData("v1.20.0")]
+ [InlineData("v1.21.0")]
+ public async Task MicroPython_BootsAndShowsReplPrompt(string version)
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(version);
+ if (runner is null) return; // firmware unavailable - skip gracefully
+
+ var booted = runner.WaitForPrompt(timeoutMs: 15_000);
+
+ booted.Should().BeTrue(
+ $"MicroPython {version} should produce a REPL prompt within 15 seconds of simulated time");
+ }
+
+ [Theory]
+ [InlineData("v1.19.1")]
+ [InlineData("v1.20.0")]
+ [InlineData("v1.21.0")]
+ public async Task MicroPython_OutputsVersionHeader(string version)
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt();
+
+ // The startup banner may be transmitted before USB-CDC enumeration completes
+ // and therefore may not be captured by the probe. Verify the version string
+ // is accessible via sys.version, which is always available after boot.
+ var found = runner.ExecuteAndWait("import sys; print(sys.version)", "MicroPython");
+ found.Should().BeTrue("the version header should be accessible via sys.version");
+ }
+
+ [Theory]
+ [InlineData("v1.19.1")]
+ [InlineData("v1.20.0")]
+ [InlineData("v1.21.0")]
+ public async Task MicroPython_OutputsHelloWorld(string version)
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(version);
+ if (runner is null) return;
+
+ var booted = runner.WaitForPrompt();
+ booted.Should().BeTrue($"MicroPython {version} must reach REPL before executing code");
+
+ var found = runner.ExecuteAndWait("print('Hello, MicroPython!')", "Hello, MicroPython!");
+
+ found.Should().BeTrue("print() output should appear on UART");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonPioTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonPioTests.cs
new file mode 100644
index 0000000..fa2a773
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonPioTests.cs
@@ -0,0 +1,166 @@
+using RP2040Sharp.IntegrationTests.Infrastructure;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests that verify PIO behaviour via the MicroPython rp2 module.
+///
+/// Each test boots MicroPython, writes a small PIO script to the filesystem, soft-resets
+/// the VM so the script runs as main.py, then checks the output.
+///
+/// This validates both the PIO peripheral emulation AND the MicroPython rp2 binding
+/// layer end-to-end.
+///
+[Trait("Category", "Integration")]
+public sealed class MicroPythonPioTests
+{
+ private static bool ShouldSkip =>
+ Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1";
+
+ private const string Version = "v1.21.0";
+
+ // ── Helper ────────────────────────────────────────────────────────────────
+
+ ///
+ /// Load a Script embedded resource by name (without the .py extension).
+ ///
+ private static string LoadScript(string name)
+ {
+ var asm = typeof(MicroPythonPioTests).Assembly;
+ var resourceName = $"RP2040Sharp.IntegrationTests.Scripts.{name}.py";
+ using var stream = asm.GetManifestResourceStream(resourceName)
+ ?? throw new InvalidOperationException($"Embedded script '{resourceName}' not found.");
+ using var reader = new System.IO.StreamReader(stream);
+ return reader.ReadToEnd();
+ }
+
+ // ── SET PINS ─────────────────────────────────────────────────────────────
+
+ ///
+ /// A PIO program that uses SET PINS to drive a GPIO pin high must produce the expected
+ /// output when the pin value is read back via machine.Pin.
+ /// Exercises: SM creation, set_base, active(1), GPIO OE from PIO.
+ ///
+ [Fact]
+ public async Task MicroPython_Pio_SetPins_DrivesGpioHigh()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue("MicroPython must reach REPL before running PIO test");
+
+ var script = LoadScript("pio_set_pins");
+ runner.WriteFile("main.py", script).Should().BeTrue("script file must be written to VFS");
+
+ runner.SoftReset(timeoutMs: 25_000).Should().BeTrue("MicroPython must return to REPL after running PIO script");
+
+ var text = runner.UsbCdc.IsConnected ? runner.UsbCdc.Text : runner.Uart.Text;
+ text.Should().Contain("pin: 1",
+ "PIO SET PINS must drive GPIO 16 high; machine.Pin.value() must return 1");
+ }
+
+ // ── TX → RX FIFO round-trip ───────────────────────────────────────────────
+
+ ///
+ /// A PIO program that does PULL → MOV ISR,OSR → PUSH should return the exact word
+ /// written to the TX FIFO via the RX FIFO.
+ /// Exercises: PULL (blocking), MOV ISR/OSR, PUSH, sm.put(), sm.get().
+ ///
+ [Fact]
+ public async Task MicroPython_Pio_TxRxFifo_RoundTrip()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ var script = LoadScript("pio_tx_rx_fifo");
+ runner.WriteFile("main.py", script).Should().BeTrue();
+
+ runner.SoftReset(timeoutMs: 25_000).Should().BeTrue();
+
+ var text = runner.UsbCdc.IsConnected ? runner.UsbCdc.Text : runner.Uart.Text;
+ text.Should().Contain("0xdeadbeef",
+ "PIO PULL→MOV ISR,OSR→PUSH round-trip must return the original 0xDEADBEEF sentinel");
+ }
+
+ // ── GPIO loopback (OUT → IN via shared pin window) ───────────────────────
+
+ ///
+ /// Two PIO state machines on the same GPIO pin window: SM0 drives 8 bits via OUT PINS,
+ /// SM1 reads them back via IN PINS. The byte read from SM1 RX FIFO must match the byte
+ /// written to SM0 TX FIFO.
+ /// Exercises: multi-SM setup, autopull, autopush, out_base/in_base.
+ ///
+ [Fact]
+ public async Task MicroPython_Pio_FifoLoopback_ReturnsOriginalByte()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ var script = LoadScript("pio_fifo_loopback");
+ runner.WriteFile("main.py", script).Should().BeTrue();
+
+ runner.SoftReset(timeoutMs: 25_000).Should().BeTrue();
+
+ var text = runner.UsbCdc.IsConnected ? runner.UsbCdc.Text : runner.Uart.Text;
+ text.Should().Contain("0xa5",
+ "PIO OUT PINS → IN PINS loopback must return the 0xA5 sentinel byte");
+ }
+
+ // ── REPL-based smoke test ─────────────────────────────────────────────────
+
+ ///
+ /// Verify that import rp2 succeeds in the MicroPython REPL on the emulated Pico,
+ /// and that rp2.PIO.OUT_LOW evaluates to the expected constant. In CPython/MicroPython
+ /// for RP2040 the pin-init enum is IN_LOW=0, IN_HIGH=1, OUT_LOW=2, OUT_HIGH=3
+ /// (see ports/rp2/modrp2.c in MicroPython).
+ /// This is a minimal sanity check that the rp2 module is present and not broken.
+ ///
+ [Fact]
+ public async Task MicroPython_Pio_ImportRp2_Succeeds()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ var found = runner.ExecuteAndWait("import rp2; print(rp2.PIO.OUT_LOW)", "2");
+ found.Should().BeTrue("import rp2 must succeed and rp2.PIO.OUT_LOW must equal 2");
+ }
+
+ ///
+ /// Verify that creating a simple PIO via the REPL does not
+ /// crash MicroPython (no MemoryError, AttributeError, or HardFault).
+ ///
+ [Fact]
+ public async Task MicroPython_Pio_StateMachineCreate_DoesNotCrash()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ // Define a minimal no-op PIO program and instantiate a SM
+ runner.ExecuteCompound("@rp2.asm_pio()\ndef noop_prog():\n wrap_target()\n nop()\n wrap()");
+
+ var found = runner.ExecuteAndWait(
+ "sm = rp2.StateMachine(0, noop_prog); sm.active(1); print('ok')", "ok");
+ found.Should().BeTrue("creating and activating a StateMachine must succeed without error");
+
+ // Cleanup
+ runner.ExecuteAndWait("sm.active(0)", ">>> ");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonReplTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonReplTests.cs
new file mode 100644
index 0000000..dcc4ae3
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonReplTests.cs
@@ -0,0 +1,94 @@
+using RP2040Sharp.IntegrationTests.Infrastructure;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Tests that exercise the MicroPython REPL: injecting code lines and verifying the output
+/// captured from the emulated UART.
+///
+[Trait("Category", "Integration")]
+public sealed class MicroPythonReplTests
+{
+ private static bool ShouldSkip =>
+ Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1";
+
+ // Use a single firmware version for REPL tests — the behaviour is stable across versions.
+ private const string Version = "v1.21.0";
+
+ [Fact]
+ public async Task Repl_CanEvaluateArithmeticExpression()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ var found = runner.ExecuteAndWait("print(1 + 2)", "3");
+ found.Should().BeTrue("1 + 2 should evaluate to 3");
+ }
+
+ [Fact]
+ public async Task Repl_CanReadSysVersion()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ var found = runner.ExecuteAndWait("import sys; print(sys.version)", "MicroPython");
+ found.Should().BeTrue("sys.version should contain 'MicroPython'");
+ }
+
+ [Fact]
+ public async Task Repl_CanReadSysPlatform()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ var found = runner.ExecuteAndWait("import sys; print(sys.platform)", "rp2");
+ found.Should().BeTrue("sys.platform should be 'rp2' for MicroPython on RP2040");
+ }
+
+ [Fact]
+ public async Task Repl_CanDefineAndCallFunction()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ // def is a compound statement in MicroPython REPL; ExecuteCompound sends
+ // the def line, waits for "... " continuation, then sends a blank line to
+ // complete the definition and waits for the next ">>> " prompt.
+ runner.ExecuteCompound("def greet(name): return 'Hi ' + name");
+
+ var found = runner.ExecuteAndWait("print(greet('world'))", "Hi world");
+ found.Should().BeTrue("user-defined function should be callable from REPL");
+ }
+
+ [Fact]
+ public async Task Repl_MultipleCommands_ProduceCorrectOutput()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ runner.ExecuteAndWait("x = 10", ">>> ");
+ runner.ExecuteAndWait("y = 32", ">>> ");
+ var found = runner.ExecuteAndWait("print(x + y)", "42");
+ found.Should().BeTrue("accumulated variable state should be preserved across REPL lines");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonScriptTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonScriptTests.cs
new file mode 100644
index 0000000..fc0a53c
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonScriptTests.cs
@@ -0,0 +1,105 @@
+using FluentAssertions;
+using RP2040Sharp.IntegrationTests.Infrastructure;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Tests that write Python script files (boot.py / main.py) onto the MicroPython
+/// virtual filesystem via and verify that
+/// they are executed automatically on soft-reset.
+///
+[Trait("Category", "Integration")]
+public sealed class MicroPythonScriptTests
+{
+ private static bool ShouldSkip =>
+ Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1";
+
+ private const string Version = "v1.21.0";
+
+ // ── main.py ───────────────────────────────────────────────────────────────
+
+ ///
+ /// Writes a main.py that prints a sentinel string and verifies the output
+ /// appears automatically on the next soft reset, without any REPL injection.
+ ///
+ [Fact]
+ public async Task Script_MainPy_RunsAutomaticallyOnBoot()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue("MicroPython must reach REPL to write files");
+
+ // Write main.py to the VFS
+ runner.WriteFile("main.py", "print('hello from main.py')\n")
+ .Should().BeTrue("WriteFile should succeed when the REPL is ready");
+
+ // Soft-reset: MicroPython re-runs boot.py then main.py
+ runner.SoftReset(timeoutMs: 20_000)
+ .Should().BeTrue("MicroPython must return to REPL after running main.py");
+
+ // The sentinel output must have appeared during boot
+ var text = runner.UsbCdc.IsConnected ? runner.UsbCdc.Text : runner.Uart.Text;
+ text.Should().Contain("hello from main.py",
+ "main.py output must be captured between soft-reset and the next REPL prompt");
+ }
+
+ ///
+ /// Verifies that arithmetic computed in main.py is output correctly
+ /// (sanity-checks that the script interpreter runs fully).
+ ///
+ [Fact]
+ public async Task Script_MainPy_ComputesAndPrintsArithmetic()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ runner.WriteFile("main.py", "x = 6 * 7\nprint('result:', x)\n")
+ .Should().BeTrue();
+
+ runner.SoftReset(timeoutMs: 20_000)
+ .Should().BeTrue();
+
+ var text = runner.UsbCdc.IsConnected ? runner.UsbCdc.Text : runner.Uart.Text;
+ text.Should().Contain("result: 42",
+ "main.py should execute and print the computed value");
+ }
+
+ // ── boot.py + main.py ─────────────────────────────────────────────────────
+
+ ///
+ /// Writes both boot.py and main.py and verifies the ordering of their
+ /// output: boot.py always runs before main.py on a MicroPython soft reset.
+ ///
+ [Fact]
+ public async Task Script_BootPy_RunsBeforeMainPy()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ runner.WriteFile("boot.py", "print('--- boot.py ---')\n") .Should().BeTrue();
+ runner.WriteFile("main.py", "print('--- main.py ---')\n") .Should().BeTrue();
+
+ runner.SoftReset(timeoutMs: 20_000)
+ .Should().BeTrue();
+
+ var text = runner.UsbCdc.IsConnected ? runner.UsbCdc.Text : runner.Uart.Text;
+
+ text.Should().Contain("--- boot.py ---", "boot.py must run on soft reset");
+ text.Should().Contain("--- main.py ---", "main.py must run on soft reset");
+
+ var bootIdx = text.IndexOf("--- boot.py ---", StringComparison.Ordinal);
+ var mainIdx = text.IndexOf("--- main.py ---", StringComparison.Ordinal);
+ bootIdx.Should().BeLessThan(mainIdx, "boot.py output must precede main.py output");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonUartTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonUartTests.cs
new file mode 100644
index 0000000..287cb5c
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonUartTests.cs
@@ -0,0 +1,86 @@
+using RP2040Sharp.IntegrationTests.Infrastructure;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Tests that verify MicroPython UART output — printing integers, strings, and multi-line output.
+/// These target the emulated UART TX path and confirm the entire pipeline from Python print()
+/// to the UartProbe capture.
+///
+[Trait("Category", "Integration")]
+public sealed class MicroPythonUartTests
+{
+ private static bool ShouldSkip =>
+ Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1";
+
+ private const string Version = "v1.21.0";
+
+ [Fact]
+ public async Task Uart_PrintInteger_AppearsInCapture()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ var found = runner.ExecuteAndWait("print(12345)", "12345");
+ found.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task Uart_PrintMultipleLines_AllCaptured()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ // for-loop is a compound statement in MicroPython REPL; ExecuteCompound
+ // sends the statement, waits for "... " continuation, then sends a blank
+ // line to execute it, and waits for the next ">>> " prompt.
+ runner.ExecuteCompound("for i in range(3): print('line', i)");
+ var found = runner.WaitForOutput(text =>
+ text.Contains("line 0") && text.Contains("line 1") && text.Contains("line 2"));
+
+ found.Should().BeTrue("all three lines from the for-loop should appear on UART");
+ }
+
+ [Fact]
+ public async Task Uart_PrintBytes_HexRepresentationCaptured()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ var found = runner.ExecuteAndWait("print(bytes([0xDE, 0xAD]))", "\\xde\\xad");
+ found.Should().BeTrue("bytes literal should print as expected hex escape");
+ }
+
+ [Fact]
+ public async Task Uart_MachinePinToggle_OutputsMessage()
+ {
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ // Toggle GPIO 25 (onboard LED on Pico) and verify no exception is thrown
+ runner.Execute("from machine import Pin");
+ runner.WaitForPrompt();
+ runner.Execute("led = Pin(25, Pin.OUT)");
+ runner.WaitForPrompt();
+ runner.Execute("led.toggle(); print('toggled')");
+ var found = runner.WaitForOutput("toggled");
+
+ found.Should().BeTrue("GPIO toggle should complete without error");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/MulticoreTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/MulticoreTests.cs
new file mode 100644
index 0000000..aaee6b5
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/MulticoreTests.cs
@@ -0,0 +1,192 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for multicore examples from pico-examples.
+/// Core 1 is launched by the firmware via the SIO FIFO multicore handshake
+/// (RP2040 datasheet §2.8.3). The emulator now implements this handshake natively
+/// in : Core 0's 6-word launch
+/// sequence (0, 0, 1, VTOR, SP, Entry) is echoed back immediately, and Core 1
+/// is configured and started when the sequence completes.
+///
+[Trait("Category", "Integration")]
+public sealed class MulticoreTests
+{
+ // ── Core0 boot (no Core1 needed) ─────────────────────────────────────────
+
+ ///
+ /// Verifies that hello_multicore boots Core0 without a HardFault or CPU lockup
+ /// in the brief window before it attempts to launch Core1.
+ /// This test does NOT wait for the inter-core rendezvous.
+ ///
+ [Fact]
+ public void HelloMulticore_Core0_BootsCleanly()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloMulticore)!;
+
+ pico.LoadFlash(flash);
+
+ // Run only long enough to confirm reset + clock init completes on Core0,
+ // but short enough that the FIFO-wait loop hasn't consumed all budget.
+ pico.RunMilliseconds(50);
+
+ pico.Cpu.IsLockedUp.Should().BeFalse(
+ "hello_multicore Core0 must not lock up during early init");
+ // RP2040 SRAM: 264 KB = 0x20000000–0x20041FFF; stack top = 0x20042000
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must be in SRAM after Core0 reset handler");
+ }
+
+ ///
+ /// Verifies that multicore_fifo_irqs boots Core0 without a HardFault or lockup.
+ ///
+ [Fact]
+ public void MulticoreFifoIrqs_Core0_BootsCleanly()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.MulticoreFifoIrqs)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(50);
+
+ pico.Cpu.IsLockedUp.Should().BeFalse(
+ "multicore_fifo_irqs Core0 must not lock up during early init");
+ // RP2040 SRAM: 264 KB = 0x20000000–0x20041FFF; stack top = 0x20042000
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must be in SRAM after Core0 reset handler");
+ }
+
+ // ── Full multicore tests ──────────────────────────────────────────────────
+
+ [Fact]
+ public void HelloMulticore_NoHardFault_AfterCore1Launch()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloMulticore)!;
+
+ pico.LoadFlash(flash);
+
+ // Allow time for core 0 to launch core 1 via SIO FIFO
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur after core 1 launch");
+ pico.Cpu.IsLockedUp.Should().BeFalse("CPU must not lock up after core 1 launch");
+ }
+
+ [Fact]
+ public void HelloMulticore_Uart0_ContainsCoreMessages()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloMulticore)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_multicore: core 0 sends a value to core 1, core 1 squares it and returns
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("hello_multicore must produce UART0 output after inter-core communication");
+ }
+
+ [Fact]
+ public void HelloMulticore_Cpu_IsAliveAfterRendezvous()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloMulticore)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(2_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain valid after multicore rendezvous");
+ pico.Cpu.IsLockedUp.Should().BeFalse("CPU must not lock up after multicore rendezvous");
+ }
+
+ // ── multicore_fifo_irqs ───────────────────────────────────────────────────
+
+ [Fact]
+ public void MulticoreFifoIrqs_NoHardFault_AfterStart()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.MulticoreFifoIrqs)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(1_000);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur in FIFO IRQ example");
+ pico.Cpu.IsLockedUp.Should().BeFalse("CPU must not lock up in FIFO IRQ example");
+ }
+
+ [Fact]
+ public void MulticoreFifoIrqs_Uart0_ProducesOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.MulticoreFifoIrqs)!;
+
+ pico.LoadFlash(flash);
+
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("multicore_fifo_irqs must produce UART0 output after IRQ-driven FIFO exchange");
+ }
+
+ [Fact]
+ public void MulticoreFifoIrqs_Cpu_IsAliveAfterMultipleIrqs()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.MulticoreFifoIrqs)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(2_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain valid after multiple FIFO IRQ rounds");
+ pico.Cpu.IsLockedUp.Should().BeFalse("CPU must not lock up after FIFO IRQ rounds");
+ }
+
+ // ── Dual-core timing ──────────────────────────────────────────────────────
+
+ ///
+ /// Both cores run in parallel on real hardware, so the wall-clock time advanced by a
+ /// single Run(n) must be max(core0, core1) — never the sum of both.
+ /// Before the fix, summing the two cores' cycles made time-aware peripherals (timer,
+ /// PWM, …) run at up to double speed whenever Core 1 was active.
+ ///
+ [Fact]
+ public void DualCore_ElapsedTime_IsMaxNotSum()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloMulticore)!;
+
+ pico.LoadFlash(flash);
+
+ // Run in fixed batches until Core 0 launches Core 1 via the SIO FIFO handshake.
+ const int batch = 100_000;
+ var launched = false;
+ for (var i = 0; i < 1000 && !launched; i++) // up to ~100M cycles (~0.8 s @125 MHz)
+ {
+ pico.Rp2040.Run(batch);
+ launched = pico.Rp2040.Core1Launched;
+ }
+
+ launched.Should().BeTrue("the test needs both cores active");
+
+ // Both cores run in parallel on real hardware, so the wall-clock advanced by a
+ // single Run must be exactly max(core0, core1). The pre-fix code used Core 0's
+ // cycles alone, which underran the clock whenever Core 1 did more work.
+ var c0Before = pico.Cpu.Cycles;
+ var c1Before = pico.Cpu1.Cycles;
+
+ pico.Rp2040.Run(batch);
+
+ var d0 = pico.Cpu.Cycles - c0Before;
+ var d1 = pico.Cpu1.Cycles - c1Before;
+ pico.Rp2040.LastElapsedCycles.Should().Be(Math.Max(d0, d1),
+ "elapsed time is max(core0, core1) — neither Core 0 alone nor the sum");
+ }
+}
+
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/PioTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/PioTests.cs
new file mode 100644
index 0000000..d65a1c1
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/PioTests.cs
@@ -0,0 +1,157 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for PIO examples from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class PioTests
+{
+ // ── hello_pio ─────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HelloPio_NoHardFault_AfterInit()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloPio)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(300);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur during PIO init");
+ pico.Cpu.IsLockedUp.Should().BeFalse("CPU must not reach lockup (firmware panic) during PIO init");
+ }
+
+ [Fact]
+ public void HelloPio_Cpu_IsAliveAfterStateMachineStart()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloPio)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(1_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain valid after PIO state machine starts");
+ pico.Cpu.IsLockedUp.Should().BeFalse("CPU must not lock up during hello_pio execution");
+ }
+
+ [Fact]
+ public void HelloPio_Gpio25_BecomesOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloPio)!;
+
+ pico.LoadFlash(flash);
+ // hello_pio drives GPIO 25 (onboard LED) via a PIO SET PINS program.
+ // Allow enough time for pio_init and the first SM tick.
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.IsLockedUp.Should().BeFalse("CPU must not lock up before GPIO 25 is driven by PIO");
+ // GPIO 25 is configured as a PIO output via pio_gpio_init() → IO_BANK0 FUNCSEL=6 (PIO0)
+ pico.Gpio[25].Should().BePioOutput("hello_pio configures GPIO 25 as PIO0 output via pio_gpio_init()");
+ }
+
+ [Fact]
+ public void HelloPio_Gpio25_TogglesOverTime()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloPio)!;
+
+ pico.LoadFlash(flash);
+
+ int toggles = 0;
+ bool? prev = null;
+
+ // hello_pio blinks at ~4 Hz — sample over 2 simulated seconds.
+ for (int i = 0; i < 20; i++)
+ {
+ pico.RunMilliseconds(100);
+ if (pico.Cpu.IsLockedUp) break;
+ bool current = pico.Gpio[25].DigitalValue;
+ if (prev.HasValue && current != prev.Value)
+ toggles++;
+ prev = current;
+ }
+
+ pico.Cpu.IsLockedUp.Should().BeFalse("CPU must not lock up while PIO blinks GPIO 25");
+ toggles.Should().BeGreaterThanOrEqualTo(2,
+ "hello_pio PIO SET program must toggle GPIO 25 at least twice over 2 simulated seconds");
+ }
+
+ // ── pio_blink ─────────────────────────────────────────────────────────────
+
+ ///
+ /// pio_blink.uf2 sets up two PIO state machines to blink LEDs autonomously and
+ /// then returns from main(). The pico-sdk startup wrapper calls
+ /// panic_if_returns() after main(), which executes BKPT #0 at flash offset
+ /// 0x3C30. This is identical to real-hardware behaviour — not a simulation
+ /// discrepancy. The test harness captures BKPT events (ARMv6-M §C1.7.2 debug-monitor
+ /// attach), so the BKPT is logged rather than escalating to HardFault.
+ ///
+ [Fact]
+ public void PioBlink_BkptCapturedWhenMainReturns()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.PioBlink)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ // pico-sdk's panic_if_returns() fires BKPT #0 when main() returns.
+ // This is correct behaviour on both real hardware and in simulation.
+ pico.BreakpointHits.Should().NotBeEmpty(
+ "pico-sdk panic_if_returns must fire BKPT #0 after pio_blink main() returns");
+ pico.Cpu.IsLockedUp.Should().BeFalse(
+ "BKPT captured by debug-monitor must not escalate to HardFault lockup");
+ }
+
+ // ── pio_uart_tx ───────────────────────────────────────────────────────────
+
+ [Fact]
+ public void PioUartTx_NoHardFault_AfterTransmit()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.PioUartTx)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur in pio_uart_tx");
+ pico.Cpu.IsLockedUp.Should().BeFalse("CPU must not lock up in pio_uart_tx");
+ }
+
+ [Fact]
+ public void PioUartTx_Cpu_IsAliveAfterPioUartInit()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.PioUartTx)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(1_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain in SRAM after PIO UART transmitter starts");
+ pico.Cpu.IsLockedUp.Should().BeFalse("CPU must not lock up in pio_uart_tx");
+ }
+
+ [Fact]
+ public void PioUartTx_Gpio0_BecomesOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.PioUartTx)!;
+
+ pico.LoadFlash(flash);
+ // pio_uart_tx drives GPIO 0 as the UART TX pin via pio_gpio_init(pio, UART_TX_PIN).
+ // The pico-examples default UART_TX_PIN is GPIO 0.
+ pico.RunMilliseconds(200);
+
+ pico.Cpu.IsLockedUp.Should().BeFalse("CPU must not lock up before PIO UART TX pin is configured");
+ // GPIO 0 is configured via pio_gpio_init() → IO_BANK0 FUNCSEL=6 (PIO0)
+ pico.Gpio[0].Should().BePioOutput("pio_uart_tx must configure GPIO 0 as PIO0 output via pio_gpio_init()");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/PwmTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/PwmTests.cs
new file mode 100644
index 0000000..549b424
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/PwmTests.cs
@@ -0,0 +1,115 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for PWM examples from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class PwmTests
+{
+ // ── hello_pwm ─────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HelloPwm_NoHardFault_AfterStartup()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloPwm)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(200);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur");
+ }
+
+ [Fact]
+ public void HelloPwm_Cpu_IsAliveAfterInit()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloPwm)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_pwm configures PWM on GP0 then sits in an infinite loop
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain in SRAM after PWM init");
+ }
+
+ [Fact]
+ public void HelloPwm_Gpio0_IsConfiguredAsFunctionPwm()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloPwm)!;
+
+ pico.LoadFlash(flash);
+
+ // Allow firmware to run past PWM initialisation
+ pico.RunMilliseconds(100);
+
+ // GP0 is the PWM A output of slice 0; the PWM subsystem must have been enabled
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "no fault during PWM configuration");
+ }
+
+ // ── pwm_led_fade ──────────────────────────────────────────────────────────
+
+ [Fact]
+ public void PwmLedFade_NoHardFault_AfterOneFadeCycle()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.PwmLedFade)!;
+
+ pico.LoadFlash(flash);
+
+ // A full fade-up + fade-down cycle at full clock rate
+ pico.RunMilliseconds(1_000);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur during LED fade");
+ }
+
+ [Fact]
+ public void PwmLedFade_Cpu_IsAliveAfterMultipleCycles()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.PwmLedFade)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(2_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain in SRAM during LED fade loop");
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u);
+ }
+
+ [Fact]
+ public void PwmLedFade_DutyCycle_ChangesOverTime()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.PwmLedFade)!;
+
+ pico.LoadFlash(flash);
+
+ // Allow PWM init and first IRQ wrap to fire
+ pico.RunMilliseconds(100);
+
+ // GPIO 25 (onboard LED) = PWM slice 4, channel B (slice = (pin >> 1) & 7)
+ const int ledSlice = 4;
+ var dutyFirst = pico.Rp2040.Pwm.GetDutyB(ledSlice);
+
+ // Run 400 ms more — the IRQ-driven fade increments the level every wrap
+ pico.RunMilliseconds(400);
+ var dutySecond = pico.Rp2040.Pwm.GetDutyB(ledSlice);
+
+ // At least one sample must be non-zero (IRQ fired and set a level > 0)
+ Math.Max(dutyFirst, dutySecond).Should().BeGreaterThan(0,
+ "PWM IRQ must fire and call pwm_set_gpio_level with a non-zero value");
+
+ // The level must have changed between samples (fade is progressing)
+ dutySecond.Should().NotBe(dutyFirst,
+ "duty cycle must change as the IRQ-driven fade updates pwm_set_gpio_level");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/ResetTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/ResetTests.cs
new file mode 100644
index 0000000..51889f4
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/ResetTests.cs
@@ -0,0 +1,54 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for the Reset example from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class ResetTests
+{
+ // ── hello_reset ───────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HelloReset_NoHardFault_AfterPeripheralReset()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloReset)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur during peripheral reset");
+ }
+
+ [Fact]
+ public void HelloReset_Uart0_ProducesOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloReset)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_reset releases and re-claims the UART/SPI peripheral via the RESETS block
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("hello_reset must produce UART0 output after peripheral re-init");
+ }
+
+ [Fact]
+ public void HelloReset_Cpu_IsAliveAfterPeripheralRelease()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloReset)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(1_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain in SRAM after peripheral reset/re-init cycle");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/RtcTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/RtcTests.cs
new file mode 100644
index 0000000..e63186e
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/RtcTests.cs
@@ -0,0 +1,100 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for RTC examples from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class RtcTests
+{
+ // ── hello_rtc ─────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HelloRtc_NoHardFault_AfterStartup()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloRtc)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur during RTC init");
+ }
+
+ [Fact]
+ public void HelloRtc_Uart0_PrintsDateTime()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloRtc)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_rtc sets the RTC to a fixed date/time and then prints it via UART0
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("hello_rtc must output date/time over UART0");
+ }
+
+ [Fact]
+ public void HelloRtc_Uart0_PrintsMultipleTicks()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloRtc)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_rtc uses printf("\r%s ", datetime) with 100ms sleep — no newlines.
+ // After 2 seconds (20 ticks at 100ms), the raw UART text should be non-trivial.
+ pico.RunMilliseconds(2_000);
+
+ pico.Uart0.Text.Should().NotBeEmpty("hello_rtc should produce datetime output");
+ pico.Uart0.Text.Length.Should().BeGreaterThan(20,
+ "hello_rtc should produce repeated datetime output over 2 seconds");
+ }
+
+ [Fact]
+ public void HelloRtc_Cpu_IsAliveAfterSeveralSeconds()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloRtc)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(3_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain in SRAM while RTC loop is running");
+ }
+
+ // ── rtc_alarm ─────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void RtcAlarm_NoHardFault_AfterFiring()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.RtcAlarm)!;
+
+ pico.LoadFlash(flash);
+
+ // The alarm is set a few seconds into the future; run long enough for it to fire
+ pico.RunMilliseconds(10_000);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur when RTC alarm fires");
+ }
+
+ [Fact]
+ public void RtcAlarm_Uart0_PrintsAlarmFired()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.RtcAlarm)!;
+
+ pico.LoadFlash(flash);
+
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 15_000);
+
+ found.Should().BeTrue("rtc_alarm must produce UART0 output after the alarm fires");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/SystemTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/SystemTests.cs
new file mode 100644
index 0000000..1d993f2
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/SystemTests.cs
@@ -0,0 +1,72 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for System examples from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class SystemTests
+{
+ // ── unique_board_id ───────────────────────────────────────────────────────
+
+ [Fact]
+ public void UniqueBoardId_NoHardFault_AfterIdRead()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.UniqueBoardId)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u,
+ "HardFault must not occur while reading unique board ID via SSI/DMA");
+ }
+
+ [Fact]
+ public void UniqueBoardId_Uart0_PrintsBoardId()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.UniqueBoardId)!;
+
+ pico.LoadFlash(flash);
+
+ // unique_board_id reads the 8-byte flash UID via SSI and prints it as hex over UART0
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("unique_board_id must print the board ID over UART0");
+ }
+
+ [Fact]
+ public void UniqueBoardId_Uart0_OutputLooksLikeHexId()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.UniqueBoardId)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(2_000);
+
+ var text = pico.Uart0.Text;
+ text.Should().NotBeEmpty("unique_board_id must have produced output");
+
+ // Board ID is 8 bytes printed as 16 hex characters
+ var hasHexChars = text.Any(c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'));
+ hasHexChars.Should().BeTrue("the board ID output should contain hexadecimal characters");
+ }
+
+ [Fact]
+ public void UniqueBoardId_Cpu_CompletesWithoutStackCorruption()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.UniqueBoardId)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(1_000);
+
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "SP must remain in SRAM after SSI/DMA flash ID read");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/TimerTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/TimerTests.cs
new file mode 100644
index 0000000..54375fc
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/TimerTests.cs
@@ -0,0 +1,97 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for Timer examples from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class TimerTests
+{
+ // ── hello_timer ───────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HelloTimer_NoHardFault_AfterStartup()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloTimer)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur");
+ }
+
+ [Fact]
+ public void HelloTimer_Uart0_ReceivesTimerFiredOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloTimer)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_timer fires a repeating callback every 1 s and prints to UART0
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("hello_timer must produce UART output after timer fires");
+ }
+
+ [Fact]
+ public void HelloTimer_Uart0_ReceivesMultipleFirings()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloTimer)!;
+
+ pico.LoadFlash(flash);
+
+ // Run long enough for multiple timer firings (callback every 1 s)
+ pico.RunMilliseconds(4_000);
+
+ pico.Uart0.Lines.Count.Should().BeGreaterThan(2,
+ "hello_timer should fire at least 3 times in 4 seconds");
+ }
+
+ [Fact]
+ public void HelloTimer_Cpu_IsAliveAfter3Seconds()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloTimer)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(3_000);
+
+ // SP must remain in valid SRAM range (not corrupted)
+ pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u,
+ "stack pointer must stay within SRAM after timer callbacks");
+ }
+
+ // ── timer_lowlevel ────────────────────────────────────────────────────────
+
+ [Fact]
+ public void TimerLowlevel_NoHardFault_AfterStartup()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.TimerLowlevel)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(1_000);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur in timer_lowlevel");
+ }
+
+ [Fact]
+ public void TimerLowlevel_Uart0_HasOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.TimerLowlevel)!;
+
+ pico.LoadFlash(flash);
+
+ var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000);
+
+ found.Should().BeTrue("timer_lowlevel must produce output after a hardware timer fires");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/UartTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/UartTests.cs
new file mode 100644
index 0000000..16adee1
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/UartTests.cs
@@ -0,0 +1,96 @@
+using RP2040.Peripherals;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for UART examples from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class UartTests
+{
+ // ── hello_serial (hello_world/serial) ─────────────────────────────────────
+
+ [Fact]
+ public void HelloSerial_NoHardFault_AfterStartup()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloSerial)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(200);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur");
+ }
+
+ [Fact]
+ public void HelloSerial_Uart0_ContainsHelloWorld()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloSerial)!;
+
+ pico.LoadFlash(flash);
+
+ var found = pico.RunUntilOutput(pico.Uart0, "Hello, world!", timeoutMs: 5_000);
+
+ found.Should().BeTrue("hello_serial prints 'Hello, world!' over UART0");
+ }
+
+ [Fact]
+ public void HelloSerial_Uart0_HasMultipleLines()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloSerial)!;
+
+ pico.LoadFlash(flash);
+
+ // hello_serial loops forever printing; wait for 3 repetitions
+ var found = pico.RunUntilOutput(
+ pico.Uart0,
+ text => text.Split('\n').Count(l => l.Contains("Hello, world!")) >= 3,
+ timeoutMs: 10_000);
+
+ found.Should().BeTrue("hello_serial should repeat 'Hello, world!' multiple times");
+ }
+
+ // ── hello_uart (uart/hello_uart) ──────────────────────────────────────────
+
+ [Fact]
+ public void HelloUart_NoHardFault_AfterStartup()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloUart)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(200);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur");
+ }
+
+ [Fact]
+ public void HelloUart_Uart0_ContainsHello()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloUart)!;
+
+ pico.LoadFlash(flash);
+
+ var found = pico.RunUntilOutput(pico.Uart0, "Hello", timeoutMs: 5_000);
+
+ found.Should().BeTrue("hello_uart sends a greeting over UART0");
+ }
+
+ [Fact]
+ public void HelloUart_Uart0_HasOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloUart)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(2_000);
+
+ pico.Uart0.ByteCount.Should().BeGreaterThan(0, "hello_uart must transmit bytes over UART0");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/UsbTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/UsbTests.cs
new file mode 100644
index 0000000..0f8d1fd
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/UsbTests.cs
@@ -0,0 +1,83 @@
+using RP2040.Peripherals;
+using RP2040.TestKit;
+using RP2040.TestKit.Boards;
+using RP2040.TestKit.Extensions;
+using RP2040Sharp.IntegrationTests.Firmware;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Integration tests for USB-CDC example from pico-examples.
+///
+[Trait("Category", "Integration")]
+public sealed class UsbTests
+{
+ // ── hello_usb (hello_world/usb) ───────────────────────────────────────────
+
+ [Fact]
+ public void HelloUsb_NoHardFault_AfterStartup()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloUsb)!;
+
+ pico.LoadFlash(flash);
+ pico.RunMilliseconds(500);
+
+ pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur during USB init");
+ }
+
+ [Fact]
+ public void HelloUsb_CdcDevice_EnumeratesSuccessfully()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloUsb)!;
+
+ pico.LoadFlash(flash);
+
+ // Run until the device transmits its first CDC payload — that proves enumeration completed.
+ // _initialized is set before OnSerialData can fire, so receiving data implies IsConnected.
+ var found = pico.RunUntilOutput(pico.UsbCdc, "Hello", timeoutMs: 10_000);
+
+ found.Should().BeTrue("USB CDC device should enumerate and transmit data");
+ pico.UsbCdc.IsConnected.Should().BeTrue("USB CDC device should complete enumeration");
+ }
+
+ [Fact]
+ public void HelloUsb_CdcDevice_TransmitsHelloWorld()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloUsb)!;
+
+ pico.LoadFlash(flash);
+
+ var found = pico.RunUntilOutput(pico.UsbCdc, "Hello, world!", timeoutMs: 10_000);
+
+ found.Should().BeTrue("hello_usb prints 'Hello, world!' over USB CDC");
+ }
+
+ [Fact]
+ public void HelloUsb_CdcDevice_RepeatsOutput()
+ {
+ using var pico = new PicoSimulation();
+ var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloUsb)!;
+
+ pico.LoadFlash(flash);
+
+ // Run in batches until 3 repetitions appear (no lambda overload for UsbCdcProbe)
+ const double batchMs = 100.0;
+ double elapsed = 0;
+ bool found = false;
+ while (elapsed < 15_000)
+ {
+ pico.RunMilliseconds(batchMs);
+ elapsed += batchMs;
+ if (pico.UsbCdc.Text.Split('\n').Count(l => l.Contains("Hello, world!")) >= 3)
+ {
+ found = true;
+ break;
+ }
+ }
+
+ found.Should().BeTrue("hello_usb should repeat the message multiple times");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/VersionMatrixTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/VersionMatrixTests.cs
new file mode 100644
index 0000000..3d065e2
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/VersionMatrixTests.cs
@@ -0,0 +1,57 @@
+using RP2040Sharp.IntegrationTests.Infrastructure;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Tests that run a matrix of MicroPython versions to ensure the emulator is compatible
+/// with each official release. These tests focus on the boot + basic output contract,
+/// not on specific Python features.
+///
+[Trait("Category", "Integration")]
+[Trait("Category", "VersionMatrix")]
+public sealed class VersionMatrixTests
+{
+ private static bool ShouldSkip =>
+ Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1";
+
+ ///
+ /// All MicroPython versions the emulator is expected to be compatible with.
+ /// Mirrors the version matrix used by rp2040js CI (.github/workflows/ci-micropython.yml).
+ ///
+ public static IEnumerable