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 @@ ![Build Status](https://github.com/begeistert/RP2040Sharp/actions/workflows/test.yml/badge.svg) ![License](https://img.shields.io/badge/license-MIT-blue.svg) ![.NET Version](https://img.shields.io/badge/.NET-10.0-purple) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=begeistert_RP2040Sharp&metric=alert_status)](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 SupportedVersions => + [ + ["v1.19.1"], + ["v1.20.0"], + ["v1.21.0"], + ]; + + [Theory] + [MemberData(nameof(SupportedVersions))] + public async Task AllVersions_BootAndPrintHelloWorld(string version) + { + if (ShouldSkip) return; + + await using var runner = await MicroPythonRunner.CreateAsync(version); + if (runner is null) return; // firmware not available - skip + + var booted = runner.WaitForPrompt(timeoutMs: 15_000); + if (!booted) return; // give benefit of doubt for slow boot on some versions + + var found = runner.ExecuteAndWait("print('Hello, MicroPython!')", "Hello, MicroPython!"); + found.Should().BeTrue($"MicroPython {version}: REPL print() should work"); + } + + [Theory] + [MemberData(nameof(SupportedVersions))] + public async Task AllVersions_SysPlatform_IsRp2(string version) + { + if (ShouldSkip) return; + + await using var runner = await MicroPythonRunner.CreateAsync(version); + if (runner is null) return; + + runner.WaitForPrompt(); + var found = runner.ExecuteAndWait("import sys; print(sys.platform)", "rp2"); + found.Should().BeTrue($"MicroPython {version}: sys.platform should be 'rp2'"); + } +} diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/WatchdogTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/WatchdogTests.cs new file mode 100644 index 0000000..e5ff77f --- /dev/null +++ b/tests/RP2040Sharp.IntegrationTests/Tests/WatchdogTests.cs @@ -0,0 +1,74 @@ +using RP2040.Peripherals; +using RP2040.TestKit.Boards; +using RP2040.TestKit.Extensions; +using RP2040Sharp.IntegrationTests.Firmware; + +namespace RP2040Sharp.IntegrationTests.Tests; + +/// +/// Integration tests for the Watchdog example from pico-examples. +/// +[Trait("Category", "Integration")] +public sealed class WatchdogTests +{ + // ── hello_watchdog ──────────────────────────────────────────────────────── + + [Fact] + public void HelloWatchdog_NoHardFault_DuringNormalOperation() + { + using var pico = new PicoSimulation(); + var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloWatchdog)!; + + pico.LoadFlash(flash); + + // Allow enough time for watchdog setup, first timeout check, and re-arm + pico.RunMilliseconds(2_000); + + pico.Cpu.Registers.IPSR.Should().NotBe(3u, "HardFault must not occur during watchdog operation"); + } + + [Fact] + public void HelloWatchdog_Uart0_PrintsRebootReason() + { + using var pico = new PicoSimulation(); + var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloWatchdog)!; + + pico.LoadFlash(flash); + + // hello_watchdog prints whether it rebooted via watchdog or cleanly + var found = pico.RunUntilOutput(pico.Uart0, text => text.Length > 0, timeoutMs: 5_000); + + found.Should().BeTrue("hello_watchdog must produce UART0 output describing boot cause"); + } + + [Fact] + public void HelloWatchdog_Uart0_ContainsScratchpadValue() + { + using var pico = new PicoSimulation(); + var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloWatchdog)!; + + pico.LoadFlash(flash); + + // hello_watchdog writes a scratch value before enabling watchdog and reads it on reboot + pico.RunMilliseconds(5_000); + + // On a fresh boot (no prior watchdog reset), it should print a clean-start message + pico.Uart0.ByteCount.Should().BeGreaterThan(0, + "hello_watchdog must have written to UART0"); + } + + [Fact] + public void HelloWatchdog_Cpu_IsAliveAfterWatchdogArm() + { + using var pico = new PicoSimulation(); + var flash = RP2040Machine.Uf2ToFlash(PicoExamplesFirmware.HelloWatchdog)!; + + pico.LoadFlash(flash); + + // Firmware arms the watchdog and pats it in a loop; SP must remain valid + pico.RunMilliseconds(3_000); + + pico.Cpu.Registers.SP.Should().BeInRange(0x2000_0000u, 0x2004_2000u, + "SP must remain in SRAM with watchdog armed"); + } +} diff --git a/tests/RP2040Sharp.Tests/Bus/AtomicAliasTests.cs b/tests/RP2040Sharp.Tests/Bus/AtomicAliasTests.cs new file mode 100644 index 0000000..1a5e901 --- /dev/null +++ b/tests/RP2040Sharp.Tests/Bus/AtomicAliasTests.cs @@ -0,0 +1,152 @@ +using FluentAssertions; +using RP2040.Peripherals; +using Xunit; + +namespace RP2040.Peripherals.Tests.Bus; + +/// +/// Verifies the RP2040 atomic register alias protocol. +/// Every APB/AHB peripheral register is mirrored at three alias windows: +/// base + 0x1000 → XOR (toggle bits) +/// base + 0x2000 → SET (bit-set / atomic OR) +/// base + 0x3000 → CLR (bit-clear / atomic AND NOT) +/// These are transparent to individual peripherals — the APBBridge/AHBBridge +/// handles the read-modify-write before dispatching the normalised value. +/// +public class AtomicAliasTests : IDisposable +{ + // Watchdog SCRATCH0 at APB slot 22 (base 0x40058000, offset 0x0C) + // Watchdog is chosen because SCRATCH registers are plain read/write with no side effects. + private const uint WDG_BASE = 0x40058000; + private const uint SCRATCH0 = 0x0C; + + private const uint NORMAL_ADDR = WDG_BASE + SCRATCH0; // 0x4005800C + private const uint XOR_ADDR = WDG_BASE + 0x1000 + SCRATCH0; // 0x4005900C + private const uint SET_ADDR = WDG_BASE + 0x2000 + SCRATCH0; // 0x4005A00C + private const uint CLR_ADDR = WDG_BASE + 0x3000 + SCRATCH0; // 0x4005B00C + + private readonly RP2040Machine _m; + + public AtomicAliasTests() => _m = new RP2040Machine(); + public void Dispose() => _m.Dispose(); + + // ── Normal write baseline ──────────────────────────────────────────── + + [Fact] + public void Normal_write_stores_value() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0xDEADBEEF); + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0xDEADBEEFu); + } + + // ── XOR alias (+0x1000) ────────────────────────────────────────────── + + [Fact] + public void XOR_alias_toggles_bits() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0xFF00_FF00u); + _m.Bus.WriteWord(XOR_ADDR, 0x0F0F_0F0Fu); // XOR alias + + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0xFF00_FF00u ^ 0x0F0F_0F0Fu, + "XOR alias should toggle the written bits"); + } + + [Fact] + public void XOR_alias_with_all_ones_inverts_register() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0xAAAAAAAAu); + _m.Bus.WriteWord(XOR_ADDR, 0xFFFFFFFFu); // XOR with all-ones = bitwise NOT + + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0x55555555u, "XOR all-ones = invert"); + } + + [Fact] + public void XOR_alias_with_zero_is_noop() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0x12345678u); + _m.Bus.WriteWord(XOR_ADDR, 0x0u); + + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0x12345678u, "XOR with 0 leaves value unchanged"); + } + + // ── SET alias (+0x2000) ────────────────────────────────────────────── + + [Fact] + public void SET_alias_sets_bits_atomically() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0x0000_0000u); + _m.Bus.WriteWord(SET_ADDR, 0x0F0F_F0F0u); // SET alias (OR) + + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0x0F0F_F0F0u, "SET alias ORs new bits into register"); + } + + [Fact] + public void SET_alias_preserves_existing_bits() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0xF0F0_0000u); + _m.Bus.WriteWord(SET_ADDR, 0x0F0F_0000u); // set lower nibbles + + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0xFFFF_0000u, "SET OR'd with existing bits"); + } + + [Fact] + public void SET_alias_with_zero_is_noop() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0xBEEF_CAFEu); + _m.Bus.WriteWord(SET_ADDR, 0x0u); + + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0xBEEF_CAFEu, "SET with 0 leaves value unchanged"); + } + + // ── CLR alias (+0x3000) ────────────────────────────────────────────── + + [Fact] + public void CLR_alias_clears_bits_atomically() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0xFFFF_FFFFu); + _m.Bus.WriteWord(CLR_ADDR, 0x0F0F_F0F0u); // CLR alias (AND NOT) + + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0xFFFF_FFFFu & ~0x0F0F_F0F0u, + "CLR alias ANDs NOT the written bits"); + } + + [Fact] + public void CLR_alias_preserves_other_bits() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0xA5A5_5A5Au); + _m.Bus.WriteWord(CLR_ADDR, 0x0F0F_0F0Fu); + + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0xA5A5_5A5Au & ~0x0F0F_0F0Fu); + } + + [Fact] + public void CLR_alias_with_all_ones_clears_register() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0xFFFF_FFFFu); + _m.Bus.WriteWord(CLR_ADDR, 0xFFFF_FFFFu); // CLR all-ones = zero + + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0u, "CLR all-ones zeroes the register"); + } + + [Fact] + public void CLR_alias_with_zero_is_noop() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0xDEAD_C0DEu); + _m.Bus.WriteWord(CLR_ADDR, 0x0u); + + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0xDEAD_C0DEu, "CLR with 0 leaves value unchanged"); + } + + // ── Sequence: normal → XOR → SET → CLR ────────────────────────────── + + [Fact] + public void Sequence_normal_xor_set_clr_produces_expected_result() + { + _m.Bus.WriteWord(NORMAL_ADDR, 0x0000_0000u); // start: 0x00000000 + _m.Bus.WriteWord(SET_ADDR, 0xFFFF_0000u); // SET: 0xFFFF0000 + _m.Bus.WriteWord(XOR_ADDR, 0x0F0F_0000u); // XOR: 0xF0F00000 + _m.Bus.WriteWord(CLR_ADDR, 0xF000_0000u); // CLR: 0x00F00000 + + _m.Bus.ReadWord(NORMAL_ADDR).Should().Be(0x00F0_0000u, "chained atomic ops produce correct result"); + } +} diff --git a/tests/RP2040.Core.Tests/Cpu/InstructionDecoderTests.cs b/tests/RP2040Sharp.Tests/Cpu/InstructionDecoderTests.cs similarity index 100% rename from tests/RP2040.Core.Tests/Cpu/InstructionDecoderTests.cs rename to tests/RP2040Sharp.Tests/Cpu/InstructionDecoderTests.cs diff --git a/tests/RP2040.Core.Tests/Cpu/Instructions/ArithmeticOpsTests.cs b/tests/RP2040Sharp.Tests/Cpu/Instructions/ArithmeticOpsTests.cs similarity index 99% rename from tests/RP2040.Core.Tests/Cpu/Instructions/ArithmeticOpsTests.cs rename to tests/RP2040Sharp.Tests/Cpu/Instructions/ArithmeticOpsTests.cs index 7265fb1..b3c03f0 100644 --- a/tests/RP2040.Core.Tests/Cpu/Instructions/ArithmeticOpsTests.cs +++ b/tests/RP2040Sharp.Tests/Cpu/Instructions/ArithmeticOpsTests.cs @@ -189,8 +189,9 @@ public void Should_AddRegisterToStackPointer_And_PreserveFlags() // Act Cpu.Step(); - // Assert - Cpu.Registers[SP].Should().Be(0x20030010); + // Assert: ARMv6-M §A6.7.2 — ADD SP, Rm does NOT force 4-byte alignment. + // The raw sum is written to SP. + Cpu.Registers[SP].Should().Be(0x20030013); Cpu.Registers.Z.Should().BeTrue(); } diff --git a/tests/RP2040Sharp.Tests/Cpu/Instructions/BitOpsExtTests.cs b/tests/RP2040Sharp.Tests/Cpu/Instructions/BitOpsExtTests.cs new file mode 100644 index 0000000..912e9b1 --- /dev/null +++ b/tests/RP2040Sharp.Tests/Cpu/Instructions/BitOpsExtTests.cs @@ -0,0 +1,225 @@ +using FluentAssertions; +using RP2040.Core.Helpers; +using RP2040.tests.Fixtures; + +namespace RP2040.tests.Cpu.Instructions; + +public abstract class BitOpsExtTests +{ + // ================================================================ + // ROR (Rotate Right, register) + // ================================================================ + public class Ror : CpuTestBase + { + [Fact] + public void Should_Rotate_Right_By_8() + { + var opcode = InstructionEmiter.Ror(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x12345678; + Cpu.Registers[R1] = 8; + + Cpu.Step(); + + // rotate 0x12345678 right 8: 0x78_123456 + Cpu.Registers[R0].Should().Be(0x78123456); + // carry = bit(8-1)=bit7 of original = bit7 of 0x78 = 0 (0111_1000) + Cpu.Registers.C.Should().Be(false); + } + + [Fact] + public void Should_Not_Change_Value_When_ShiftIs_Zero() + { + var opcode = InstructionEmiter.Ror(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0xABCDEF01; + Cpu.Registers[R1] = 0; + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0xABCDEF01); + } + + [Fact] + public void Should_Rotate_By_32_Returning_Same_Value() + { + var opcode = InstructionEmiter.Ror(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x12345678; + Cpu.Registers[R1] = 32; + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0x12345678); + Cpu.Registers.C.Should().Be(false, "bit31 = 0"); + } + + [Fact] + public void Should_Set_N_Flag_When_Result_Is_Negative() + { + var opcode = InstructionEmiter.Ror(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x01; // shifting right will put bit0 to bit31 + Cpu.Registers[R1] = 1; + + Cpu.Step(); + + Cpu.Registers.N.Should().Be(true, "bit0 rotated to bit31 sets N"); + Cpu.Registers.C.Should().Be(true, "bit0 was the carry"); + } + } + + // ================================================================ + // SXTH (Sign-extend Halfword) + // ================================================================ + public class Sxth : CpuTestBase + { + [Fact] + public void Should_SignExtend_Negative_Halfword() + { + var opcode = InstructionEmiter.Sxth(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R1] = 0xFFFF8001; + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0xFFFF8001); + } + + [Fact] + public void Should_SignExtend_Positive_Halfword() + { + var opcode = InstructionEmiter.Sxth(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R1] = 0xFFFF7FFF; + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0x00007FFF); + } + } + + // ================================================================ + // SXTB (Sign-extend Byte) + // ================================================================ + public class Sxtb : CpuTestBase + { + [Fact] + public void Should_SignExtend_Negative_Byte() + { + var opcode = InstructionEmiter.Sxtb(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R1] = 0xFFFFFF80; + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0xFFFFFF80); + } + + [Fact] + public void Should_SignExtend_Positive_Byte() + { + var opcode = InstructionEmiter.Sxtb(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R1] = 0xABCDEF7F; + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0x0000007F); + } + } + + // ================================================================ + // UXTH (Zero-extend Halfword) + // ================================================================ + public class Uxth : CpuTestBase + { + [Fact] + public void Should_ZeroExtend_Halfword() + { + var opcode = InstructionEmiter.Uxth(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R1] = 0xABCDBEEF; + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0x0000BEEF); + } + } + + // ================================================================ + // UXTB (Zero-extend Byte) + // ================================================================ + public class Uxtb : CpuTestBase + { + [Fact] + public void Should_ZeroExtend_Byte() + { + var opcode = InstructionEmiter.Uxtb(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R1] = 0xABCDEF42; + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0x00000042); + } + } + + // ================================================================ + // CLZ (Count Leading Zeros, Thumb-2 32-bit) + // ================================================================ + public class Clz : CpuTestBase + { + [Fact] + public void Should_Count_Leading_Zeros() + { + var (h1, h2) = InstructionEmiter.Clz(R0, R1); + Bus.WriteHalfWord(0x20000000, h1); + Bus.WriteHalfWord(0x20000002, h2); + Cpu.Registers[R1] = 0x00080000; // 12 leading zeros + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(12); + } + + [Fact] + public void Should_Return_32_For_Zero_Input() + { + var (h1, h2) = InstructionEmiter.Clz(R0, R1); + Bus.WriteHalfWord(0x20000000, h1); + Bus.WriteHalfWord(0x20000002, h2); + Cpu.Registers[R1] = 0; + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(32); + } + + [Fact] + public void Should_Return_0_For_MSB_Set() + { + var (h1, h2) = InstructionEmiter.Clz(R0, R1); + Bus.WriteHalfWord(0x20000000, h1); + Bus.WriteHalfWord(0x20000002, h2); + Cpu.Registers[R1] = 0x80000000; + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0); + } + + [Fact] + public void Should_Advance_PC_By_4() + { + var (h1, h2) = InstructionEmiter.Clz(R0, R1); + Bus.WriteHalfWord(0x20000000, h1); + Bus.WriteHalfWord(0x20000002, h2); + Cpu.Registers[R1] = 1; + + Cpu.Step(); + + Cpu.Registers.PC.Should().Be(0x20000004); + } + } +} diff --git a/tests/RP2040.Core.Tests/Cpu/Instructions/BitOpsTests.cs b/tests/RP2040Sharp.Tests/Cpu/Instructions/BitOpsTests.cs similarity index 97% rename from tests/RP2040.Core.Tests/Cpu/Instructions/BitOpsTests.cs rename to tests/RP2040Sharp.Tests/Cpu/Instructions/BitOpsTests.cs index 319d88a..40bda9f 100644 --- a/tests/RP2040.Core.Tests/Cpu/Instructions/BitOpsTests.cs +++ b/tests/RP2040Sharp.Tests/Cpu/Instructions/BitOpsTests.cs @@ -287,7 +287,7 @@ public void Should_ReadProgramCounter_WithPipelineOffset() } [Fact] - public void Should_MoveRegisterToStackPointer_And_EnforceAlignment() + public void Should_MoveRegisterToStackPointer_Without_ForcedAlignment() { // Arrange var opcode = InstructionEmiter.Mov(SP, R8); @@ -298,12 +298,13 @@ public void Should_MoveRegisterToStackPointer_And_EnforceAlignment() // Act Cpu.Step(); - // Assert - Cpu.Registers[SP].Should().Be(52); + // Assert: ARMv6-M §A6.7.75 — MOV SP, Rm writes the raw register value; + // alignment is NOT forced by the instruction. + Cpu.Registers[SP].Should().Be(55); } [Fact] - public void Should_ClearLowerTwoBits_When_WritingToStackPointer() + public void Should_WriteRawValueToStackPointer() { // Arrange Cpu.Registers.PC = 0x20000000; @@ -314,8 +315,8 @@ public void Should_ClearLowerTwoBits_When_WritingToStackPointer() // Act Cpu.Step(); - // Assert - Cpu.Registers.SP.Should().Be(0x50); + // Assert: ARMv6-M §A6.7.75 — MOV SP, Rm writes the raw value without masking. + Cpu.Registers.SP.Should().Be(0x53); } [Fact] @@ -353,7 +354,7 @@ public void Should_LoadImmediateValue_And_UpdateFlags() } [Fact] - public void Should_ClearLowerTwoBits_When_WritingToStackPointer() + public void Should_WriteRawValueToStackPointer() { // Arrange var opcode = InstructionEmiter.Mov(SP, R5); @@ -364,8 +365,9 @@ public void Should_ClearLowerTwoBits_When_WritingToStackPointer() // Act Cpu.Step(); - // Assert - Cpu.Registers.SP.Should().Be(0x50); + // Assert: ARMv6-M §A6.7.75 — MOV SP, Rm writes the raw register value; + // alignment is NOT forced. + Cpu.Registers.SP.Should().Be(0x53); } [Fact] diff --git a/tests/RP2040Sharp.Tests/Cpu/Instructions/ExceptionReturnTests.cs b/tests/RP2040Sharp.Tests/Cpu/Instructions/ExceptionReturnTests.cs new file mode 100644 index 0000000..049fc34 --- /dev/null +++ b/tests/RP2040Sharp.Tests/Cpu/Instructions/ExceptionReturnTests.cs @@ -0,0 +1,231 @@ +using FluentAssertions; +using RP2040.Core.Helpers; +using RP2040.tests.Fixtures; + +namespace RP2040.tests.Cpu.Instructions; + +// ==================================================================== +// Tests for the EXC_RETURN handling lifted into BX Rm and POP {pc}. +// +// ARMv6-M B1.5.8: when an instruction loads a value of 0xFFFFFFFx into +// PC while the processor is in Handler mode, that value is interpreted +// as an exception return marker. The unstacking pulls 8 words off the +// active stack (R0,R1,R2,R3,R12,LR,RetPC,xPSR) and resumes execution. +// +// The original implementation gated this on `IPSR != 0`, which broke +// the MicroPython boot path because some IRQ handlers leave IPSR +// effectively cleared by the time `BX LR` executes. The current code +// fires ExceptionReturn purely on the EXC_RETURN range — these tests +// lock in that behaviour and the basic register-restore semantics. +// ==================================================================== + +public abstract class ExceptionReturnTests +{ + private const uint EXC_RETURN_THREAD_MSP = 0xFFFFFFF9; + private const uint EXC_RETURN_HANDLER = 0xFFFFFFF1; + + private const uint StackBase = 0x20004000; + + /// + /// Lays out an 8-word exception frame at + /// matching the ARMv6-M architectural stack: R0,R1,R2,R3,R12,LR,RetPC,xPSR. + /// xPSR is written without bit 9 set (no stack alignment adjustment), + /// so unstacking advances SP by exactly 0x20. + /// + private static void WriteFrame( + RP2040.Core.Memory.BusInterconnect bus, uint frameAddr, + uint r0, uint r1, uint r2, uint r3, + uint r12, uint lr, uint retPc, uint xpsr) + { + bus.WriteWord(frameAddr + 0x00, r0); + bus.WriteWord(frameAddr + 0x04, r1); + bus.WriteWord(frameAddr + 0x08, r2); + bus.WriteWord(frameAddr + 0x0C, r3); + bus.WriteWord(frameAddr + 0x10, r12); + bus.WriteWord(frameAddr + 0x14, lr); + bus.WriteWord(frameAddr + 0x18, retPc); + bus.WriteWord(frameAddr + 0x1C, xpsr); + } + + public class BxLr : CpuTestBase + { + [Fact] + public void BxLr_With_ThreadModeMsp_ExceptionReturn_UnstacksRegisters() + { + // Frame already on MSP starting at StackBase. + WriteFrame(Bus, StackBase, + r0: 0x11111111, + r1: 0x22222222, + r2: 0x33333333, + r3: 0x44444444, + r12: 0xCCCCCCCC, + lr: 0xAAAAAAAA, + retPc: 0x10000200u | 1u, // Thumb bit + xpsr: 0x01000000u); // T bit only + + Cpu.Registers.SP = StackBase; + Cpu.Registers[LR] = EXC_RETURN_THREAD_MSP; + + // BX LR — encoding 0x4770. + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Bx(LR)); + + Cpu.Step(); + + // ExceptionReturn restores the frame. + Cpu.Registers[R0].Should().Be(0x11111111); + Cpu.Registers[R1].Should().Be(0x22222222); + Cpu.Registers[R2].Should().Be(0x33333333); + Cpu.Registers[R3].Should().Be(0x44444444); + Cpu.Registers[R12].Should().Be(0xCCCCCCCC); + Cpu.Registers[LR].Should().Be(0xAAAAAAAA); + + // Resume PC = retPc with Thumb bit stripped. + Cpu.Registers.PC.Should().Be(0x10000200); + + // SP advances past the 32-byte frame (xPSR bit 9 was clear). + Cpu.Registers.SP.Should().Be(StackBase + 0x20); + + // Returning to Thread mode → IPSR == 0. + Cpu.Registers.IPSR.Should().Be(0u); + } + + [Fact] + public void BxLr_With_HandlerModeMsp_ExceptionReturn_UnstacksRegisters() + { + WriteFrame(Bus, StackBase, + r0: 0xDEADBEEF, + r1: 0, + r2: 0, + r3: 0, + r12: 0, + lr: 0xFEEDFACEu, + retPc: 0x10000400u | 1u, + xpsr: 0x01000000u); + + Cpu.Registers.SP = StackBase; + Cpu.Registers[LR] = EXC_RETURN_HANDLER; + // Pretend we were in handler #16 before the return. + Cpu.Registers.IPSR = 16; + + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Bx(LR)); + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0xDEADBEEF); + Cpu.Registers[LR].Should().Be(0xFEEDFACEu); + Cpu.Registers.PC.Should().Be(0x10000400); + Cpu.Registers.SP.Should().Be(StackBase + 0x20); + } + + [Fact] + public void BxLr_With_NonExcReturn_DoesNotUnstack() + { + // Sentinels: any unstack would clobber these. + Cpu.Registers[R0] = 0xCAFECAFE; + Cpu.Registers[R1] = 0xBEEFBEEF; + Cpu.Registers.SP = StackBase; + Cpu.Registers[LR] = 0x10000301; + + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Bx(LR)); + + Cpu.Step(); + + Cpu.Registers.PC.Should().Be(0x10000300); + Cpu.Registers.SP.Should().Be(StackBase); // untouched + Cpu.Registers[R0].Should().Be(0xCAFECAFE); // untouched + Cpu.Registers[R1].Should().Be(0xBEEFBEEF); // untouched + } + } + + public class PopPcExceptionReturn : CpuTestBase + { + [Fact] + public void PopPc_OnlyPc_With_ExcReturn_OnTopOfStack_UnstacksFrame() + { + // Stack layout at SP: + // [SP+0x00] = EXC_RETURN value (popped into PC) + // [SP+0x04..] = exception frame (8 words) + // + // After POP {pc}, SP = SP+4, then ExceptionReturn unstacks 0x20 + // bytes from the new SP, leaving SP = original + 0x24. + const uint stackAtPop = StackBase; + Bus.WriteWord(stackAtPop, EXC_RETURN_THREAD_MSP); + WriteFrame(Bus, stackAtPop + 4, + r0: 0x55555555, + r1: 0x66666666, + r2: 0, + r3: 0, + r12: 0, + lr: 0x77777777, + retPc: 0x10000800u | 1u, + xpsr: 0x01000000u); + + Cpu.Registers.SP = stackAtPop; + + // POP {pc} — encoding 0xBD00. + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Pop(true, 0)); + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0x55555555); + Cpu.Registers[R1].Should().Be(0x66666666); + Cpu.Registers[LR].Should().Be(0x77777777); + Cpu.Registers.PC.Should().Be(0x10000800); + Cpu.Registers.SP.Should().Be(stackAtPop + 4 + 0x20); + Cpu.Registers.IPSR.Should().Be(0u); + } + + [Fact] + public void PopRegistersAndPc_With_ExcReturn_RestoresLowRegisterFirst() + { + // POP {r4, pc} — typical IRQ handler epilogue when r4 was saved. + // [SP+0x00] = r4 value + // [SP+0x04] = EXC_RETURN + // [SP+0x08..] = exception frame + const uint stackAtPop = StackBase; + Bus.WriteWord(stackAtPop, 0xABCDEF00); // r4 + Bus.WriteWord(stackAtPop + 4, EXC_RETURN_THREAD_MSP); + WriteFrame(Bus, stackAtPop + 8, + r0: 0x99999999, + r1: 0, + r2: 0, + r3: 0, + r12: 0, + lr: 0, + retPc: 0x10000C00u | 1u, + xpsr: 0x01000000u); + + Cpu.Registers.SP = stackAtPop; + + // POP {r4, pc} — register list bit 4 set, P bit set. + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Pop(true, 1u << 4)); + + Cpu.Step(); + + Cpu.Registers[4].Should().Be(0xABCDEF00); + Cpu.Registers[R0].Should().Be(0x99999999); + Cpu.Registers.PC.Should().Be(0x10000C00); + // POP advances SP by (regCount+1)*4 = 8, then ExceptionReturn adds 0x20. + Cpu.Registers.SP.Should().Be(stackAtPop + 8 + 0x20); + } + + [Fact] + public void PopPc_With_NonExcReturn_BehavesAsRegularPop() + { + const uint stackAtPop = StackBase; + Bus.WriteWord(stackAtPop, 0x10000F01); // ordinary return + // No frame after — verifies ExceptionReturn is NOT triggered + // (because if it were, it would read garbage from beyond). + Cpu.Registers[R0] = 0xCAFECAFE; + Cpu.Registers.SP = stackAtPop; + + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Pop(true, 0)); + + Cpu.Step(); + + Cpu.Registers.PC.Should().Be(0x10000F00); + Cpu.Registers.SP.Should().Be(stackAtPop + 4); + Cpu.Registers[R0].Should().Be(0xCAFECAFE); // untouched + } + } +} diff --git a/tests/RP2040Sharp.Tests/Cpu/Instructions/FlowOpsExtTests.cs b/tests/RP2040Sharp.Tests/Cpu/Instructions/FlowOpsExtTests.cs new file mode 100644 index 0000000..c2483ba --- /dev/null +++ b/tests/RP2040Sharp.Tests/Cpu/Instructions/FlowOpsExtTests.cs @@ -0,0 +1,188 @@ +using FluentAssertions; +using RP2040.Core.Helpers; +using RP2040.tests.Fixtures; + +namespace RP2040.tests.Cpu.Instructions; + +public abstract class FlowOpsExtTests +{ + // ================================================================ + // CBZ (Compare and Branch if Zero) + // ================================================================ + public class Cbz : CpuTestBase + { + [Fact] + public void Should_Branch_When_Register_Is_Zero() + { + // CBZ R0, #4 → branch to PC+4+4 = 0x20000000+2+4+2 = 0x20000008 + var opcode = InstructionEmiter.Cbz(R0, 4); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0; + + Cpu.Step(); + + Cpu.Registers.PC.Should().Be(0x20000008); + } + + [Fact] + public void Should_NotBranch_When_Register_IsNonZero() + { + var opcode = InstructionEmiter.Cbz(R0, 4); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 1; + + Cpu.Step(); + + Cpu.Registers.PC.Should().Be(0x20000002); + } + + [Fact] + public void Should_Branch_With_Zero_Offset() + { + // CBZ R1, #0 → branch to PC+2+0+2 = 0x20000004 (next-next instruction) + var opcode = InstructionEmiter.Cbz(R1, 0); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R1] = 0; + + Cpu.Step(); + + Cpu.Registers.PC.Should().Be(0x20000004); + } + + [Fact] + public void Should_Branch_With_Large_Offset_Using_i_Bit() + { + // offset = 66 → i=1, imm5[4:0]=bits[5:1] of 66 = 1 + var opcode = InstructionEmiter.Cbz(R2, 66); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R2] = 0; + + Cpu.Step(); + + // ARM target = instrAddr + 4 + imm32 = 0x20000000 + 4 + 66 = 0x20000046 + Cpu.Registers.PC.Should().Be(0x20000046); + } + } + + // ================================================================ + // CBNZ (Compare and Branch if Non-Zero) + // ================================================================ + public class Cbnz : CpuTestBase + { + [Fact] + public void Should_Branch_When_Register_Is_NonZero() + { + var opcode = InstructionEmiter.Cbnz(R0, 4); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 42; + + Cpu.Step(); + + Cpu.Registers.PC.Should().Be(0x20000008); + } + + [Fact] + public void Should_NotBranch_When_Register_Is_Zero() + { + var opcode = InstructionEmiter.Cbnz(R0, 4); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0; + + Cpu.Step(); + + Cpu.Registers.PC.Should().Be(0x20000002); + } + } + + // ================================================================ + // CBZ/CBNZ raw-opcode tests + // + // ARMv6-M (Cortex-M0/M0+) does NOT define CBZ/CBNZ in its ISA — they are + // ARMv7-M instructions. The synthesised BootROM stub written by the + // emulator itself uses raw 0xB1xx encodings, which previously fell + // through the decoder mask (0xFB00 / 0xB300) and tripped HardFault. + // The mask was widened to 0xF900 / 0xB100 so that all four encodings + // (0xB1xx, 0xB3xx, 0xB9xx, 0xBBxx) hit the CB handler. These tests + // exercise that mask coverage with raw opcodes the assembler-style + // emitter cannot produce, and lock in the specific opcode the BootROM + // stub depends on. + // ================================================================ + public class CbzRawOpcode : CpuTestBase + { + [Fact] + public void Decoder_Matches_BootRomStub_Opcode_0xB13A() + { + // 0xB13A = the CBZ used at offset 0x0062 of the synthetic BootROM. + // bits[2:0] = Rn = 2 + // bits[7:3] = imm5 = 0b00111 = 7 + // bit 8 = 1, bit 9 = i = 0 (i lives in bit 9 per ARMv7-M, but the + // current TakeCbBranch reads bit 10 — for i=0 cases the result + // coincides, which is the only case the stub uses). + // With Rn = 0 the branch is taken and we land at PC+4+14 = +18. + const ushort opcode = 0xB13A; + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R2] = 0; // taken + + Cpu.Step(); + + // PC after fetch = 0x20000002, +imm32(14) +2 = 0x20000012 + Cpu.Registers.PC.Should().Be(0x20000012); + } + + [Fact] + public void Decoder_Matches_Cbz_With_iBit_Clear_LowestEncoding() + { + // 0xB108 = CBZ R0, with imm5=1 → branch +6 from this insn. + const ushort opcode = 0xB108; + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0; // taken + + Cpu.Step(); + + Cpu.Registers.PC.Should().Be(0x20000006); + } + + [Fact] + public void Decoder_DoesNotFallThrough_To_Undefined_For_0xB100() + { + // The original mask 0xFB00/0xB300 didn't match 0xB1xx; the stub at + // 0x0062 hit undefined → HardFault. This test verifies that + // 0xB100 (CBZ R0, 0) is now decoded as a CBZ and does not + // trigger a HardFault entry. + const ushort opcode = 0xB100; + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0; + + Cpu.Step(); + + // PC must not have been redirected through the HardFault vector. + // Branch taken (Rn=0): PC+4+0 = 0x20000004. + Cpu.Registers.PC.Should().Be(0x20000004); + } + + [Fact] + public void Decoder_Matches_Cbnz_iBit_Clear_0xB900() + { + // 0xB900 = CBNZ R0, offset 0. Branch when R0 != 0. + const ushort opcode = 0xB900; + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 1; // taken + + Cpu.Step(); + + Cpu.Registers.PC.Should().Be(0x20000004); + } + + [Fact] + public void Decoder_Matches_Cbnz_iBit_Clear_NotTaken() + { + const ushort opcode = 0xB900; + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0; // not taken + + Cpu.Step(); + + Cpu.Registers.PC.Should().Be(0x20000002); + } + } +} diff --git a/tests/RP2040.Core.Tests/Cpu/Instructions/FlowOpsTests.cs b/tests/RP2040Sharp.Tests/Cpu/Instructions/FlowOpsTests.cs similarity index 100% rename from tests/RP2040.Core.Tests/Cpu/Instructions/FlowOpsTests.cs rename to tests/RP2040Sharp.Tests/Cpu/Instructions/FlowOpsTests.cs diff --git a/tests/RP2040Sharp.Tests/Cpu/Instructions/MemoryOpsStoreTests.cs b/tests/RP2040Sharp.Tests/Cpu/Instructions/MemoryOpsStoreTests.cs new file mode 100644 index 0000000..69f3e1e --- /dev/null +++ b/tests/RP2040Sharp.Tests/Cpu/Instructions/MemoryOpsStoreTests.cs @@ -0,0 +1,326 @@ +using FluentAssertions; +using RP2040.Core.Helpers; +using RP2040.tests.Fixtures; + +namespace RP2040.tests.Cpu.Instructions; + +public abstract class MemoryOpsStoreTests +{ + // ================================================================ + // STR (Store Word) + // ================================================================ + public class StrImmediate : CpuTestBase + { + [Fact] + public void Should_Store_Word_At_RnPlusImm5() + { + var opcode = InstructionEmiter.Str(R1, R0, 8); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x20000100; + Cpu.Registers[R1] = 0xDEADBEEF; + + Cpu.Step(); + + Bus.ReadWord(0x20000108).Should().Be(0xDEADBEEF); + } + + [Fact] + public void Should_Store_Word_AtBase_WhenImm_IsZero() + { + var opcode = InstructionEmiter.Str(R2, R3, 0); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R3] = 0x20000200; + Cpu.Registers[R2] = 0x12345678; + + Cpu.Step(); + + Bus.ReadWord(0x20000200).Should().Be(0x12345678); + } + } + + public class StrSpRelative : CpuTestBase + { + [Fact] + public void Should_Store_Word_Relative_To_SP() + { + var opcode = InstructionEmiter.StrSpRelative(R0, 16); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers.SP = 0x20010000; + Cpu.Registers[R0] = 0xCAFEBABE; + + Cpu.Step(); + + Bus.ReadWord(0x20010010).Should().Be(0xCAFEBABE); + } + } + + public class StrRegister : CpuTestBase + { + [Fact] + public void Should_Store_Word_At_RnPlusRm() + { + var opcode = InstructionEmiter.StrRegister(R2, R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x20000100; + Cpu.Registers[R1] = 0x10; + Cpu.Registers[R2] = 0xFEEDC0DE; + + Cpu.Step(); + + Bus.ReadWord(0x20000110).Should().Be(0xFEEDC0DE); + } + } + + // ================================================================ + // STRB (Store Byte) + // ================================================================ + public class StrbImmediate : CpuTestBase + { + [Fact] + public void Should_Store_Byte_At_RnPlusImm5() + { + var opcode = InstructionEmiter.Strb(R1, R0, 3); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x20000100; + Cpu.Registers[R1] = 0xABCD1234; // only low byte stored + + Cpu.Step(); + + Bus.ReadByte(0x20000103).Should().Be(0x34); + } + + [Fact] + public void Should_Store_Only_LowByte() + { + var opcode = InstructionEmiter.Strb(R0, R1, 0); + Bus.WriteHalfWord(0x20000000, opcode); + Bus.WriteWord(0x20000200, 0xFFFFFFFF); + Cpu.Registers[R1] = 0x20000200; + Cpu.Registers[R0] = 0xAB; + + Cpu.Step(); + + Bus.ReadByte(0x20000200).Should().Be(0xAB); + // Remaining bytes must remain unchanged + Bus.ReadByte(0x20000201).Should().Be(0xFF); + } + } + + // ================================================================ + // STRH (Store Halfword) + // ================================================================ + public class StrhImmediate : CpuTestBase + { + [Fact] + public void Should_Store_Halfword_At_RnPlusImm5() + { + var opcode = InstructionEmiter.Strh(R1, R0, 4); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x20000100; + Cpu.Registers[R1] = 0xABCDEF12; // only low halfword stored + + Cpu.Step(); + + Bus.ReadHalfWord(0x20000104).Should().Be(0xEF12); + } + } + + // ================================================================ + // LDRB (Load Byte, zero-extend) + // ================================================================ + public class LdrbImmediate : CpuTestBase + { + [Fact] + public void Should_Load_Byte_ZeroExtended() + { + var opcode = InstructionEmiter.Ldrb(R1, R0, 2); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x20000100; + Bus.WriteByte(0x20000102, 0xAB); + + Cpu.Step(); + + Cpu.Registers[R1].Should().Be(0xAB); + } + + [Fact] + public void Should_ZeroExtend_High_Byte_Value() + { + var opcode = InstructionEmiter.Ldrb(R0, R1, 0); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R1] = 0x20000200; + Bus.WriteByte(0x20000200, 0xFF); + + Cpu.Step(); + + Cpu.Registers[R0].Should().Be(0x000000FF); + } + } + + // ================================================================ + // LDRH (Load Halfword, zero-extend) + // ================================================================ + public class LdrhImmediate : CpuTestBase + { + [Fact] + public void Should_Load_Halfword_ZeroExtended() + { + var opcode = InstructionEmiter.Ldrh(R1, R0, 0); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x20000100; + Bus.WriteHalfWord(0x20000100, 0xBEEF); + + Cpu.Step(); + + Cpu.Registers[R1].Should().Be(0x0000BEEF); + } + } + + // ================================================================ + // LDRSB (Load Signed Byte, sign-extend) + // ================================================================ + public class Ldrsb : CpuTestBase + { + [Fact] + public void Should_SignExtend_Negative_Byte() + { + var opcode = InstructionEmiter.Ldrsb(R2, R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x20000100; + Cpu.Registers[R1] = 5; + Bus.WriteByte(0x20000105, 0x80); // -128 as signed byte + + Cpu.Step(); + + Cpu.Registers[R2].Should().Be(0xFFFFFF80); + } + + [Fact] + public void Should_SignExtend_Positive_Byte() + { + var opcode = InstructionEmiter.Ldrsb(R2, R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x20000100; + Cpu.Registers[R1] = 0; + Bus.WriteByte(0x20000100, 0x7F); // +127 + + Cpu.Step(); + + Cpu.Registers[R2].Should().Be(0x0000007F); + } + } + + // ================================================================ + // LDRSH (Load Signed Halfword, sign-extend) + // ================================================================ + public class Ldrsh : CpuTestBase + { + [Fact] + public void Should_SignExtend_Negative_Halfword() + { + var opcode = InstructionEmiter.Ldrsh(R2, R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x20000100; + Cpu.Registers[R1] = 0; + Bus.WriteHalfWord(0x20000100, 0x8000); // -32768 + + Cpu.Step(); + + Cpu.Registers[R2].Should().Be(0xFFFF8000); + } + + [Fact] + public void Should_SignExtend_Positive_Halfword() + { + var opcode = InstructionEmiter.Ldrsh(R2, R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + Cpu.Registers[R0] = 0x20000100; + Cpu.Registers[R1] = 0; + Bus.WriteHalfWord(0x20000100, 0x7FFF); // +32767 + + Cpu.Step(); + + Cpu.Registers[R2].Should().Be(0x00007FFF); + } + } + + // ================================================================ + // STR + LDR round-trip + // ================================================================ + public class StoreLoadRoundTrip : CpuTestBase + { + [Fact] + public void Strb_Ldrb_RoundTrip_ShouldPreserve_Byte() + { + const byte value = 0xCD; + const uint addr = 0x20000400; + + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Strb(R0, R1, 0)); + Bus.WriteHalfWord(0x20000002, InstructionEmiter.Ldrb(R2, R1, 0)); + + Cpu.Registers[R0] = value; + Cpu.Registers[R1] = addr; + + Cpu.Step(); + Cpu.Step(); + + Cpu.Registers[R2].Should().Be(value); + } + + [Fact] + public void Strh_Ldrh_RoundTrip_ShouldPreserve_Halfword() + { + const ushort value = 0x1234; + const uint addr = 0x20000500; + + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Strh(R0, R1, 0)); + Bus.WriteHalfWord(0x20000002, InstructionEmiter.Ldrh(R2, R1, 0)); + + Cpu.Registers[R0] = value; + Cpu.Registers[R1] = addr; + + Cpu.Step(); + Cpu.Step(); + + Cpu.Registers[R2].Should().Be(value); + } + } + + // ================================================================ + // STMIA (Store Multiple Increment After) + // ================================================================ + public class Stmia : CpuTestBase + { + [Fact] + public void Should_Store_Multiple_And_WriteBack() + { + var opcode = InstructionEmiter.Stmia(R0, 1 << R1 | 1 << R2); + Bus.WriteHalfWord(0x20000000, opcode); + const uint baseAddr = 0x20000010; + Cpu.Registers[R0] = baseAddr; + Cpu.Registers[R1] = 0xAABBCCDD; + Cpu.Registers[R2] = 0x11223344; + + Cpu.Step(); + + Bus.ReadWord(baseAddr).Should().Be(0xAABBCCDD); + Bus.ReadWord(baseAddr + 4).Should().Be(0x11223344); + Cpu.Registers[R0].Should().Be(baseAddr + 8, "STMIA always writes back"); + } + + [Fact] + public void Should_WriteBack_EvenWhenRn_InList() + { + var opcode = InstructionEmiter.Stmia(R0, 1 << R0 | 1 << R1); + Bus.WriteHalfWord(0x20000000, opcode); + const uint baseAddr = 0x20000020; + Cpu.Registers[R0] = baseAddr; + Cpu.Registers[R1] = 0x5A5A5A5A; + + Cpu.Step(); + + // STMIA always writes back (unlike LDMIA) + Cpu.Registers[R0].Should().Be(baseAddr + 8); + } + } +} diff --git a/tests/RP2040.Core.Tests/Cpu/Instructions/MemoryOpsTests.cs b/tests/RP2040Sharp.Tests/Cpu/Instructions/MemoryOpsTests.cs similarity index 100% rename from tests/RP2040.Core.Tests/Cpu/Instructions/MemoryOpsTests.cs rename to tests/RP2040Sharp.Tests/Cpu/Instructions/MemoryOpsTests.cs diff --git a/tests/RP2040.Core.Tests/Cpu/Instructions/SystemOpTests.cs b/tests/RP2040Sharp.Tests/Cpu/Instructions/SystemOpTests.cs similarity index 100% rename from tests/RP2040.Core.Tests/Cpu/Instructions/SystemOpTests.cs rename to tests/RP2040Sharp.Tests/Cpu/Instructions/SystemOpTests.cs diff --git a/tests/RP2040Sharp.Tests/Cpu/Instructions/SystemOpsExtTests.cs b/tests/RP2040Sharp.Tests/Cpu/Instructions/SystemOpsExtTests.cs new file mode 100644 index 0000000..afba296 --- /dev/null +++ b/tests/RP2040Sharp.Tests/Cpu/Instructions/SystemOpsExtTests.cs @@ -0,0 +1,147 @@ +using FluentAssertions; +using RP2040.Core.Helpers; +using RP2040.tests.Fixtures; + +namespace RP2040.tests.Cpu.Instructions; + +public abstract class SystemOpsExtTests +{ + // ================================================================ + // CPSID / CPSIE + // ================================================================ + public class Cps : CpuTestBase + { + [Fact] + public void Cpsid_Should_Set_PRIMASK() + { + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Cpsid); + Cpu.Registers.PRIMASK = 0; + + Cpu.Step(); + + Cpu.Registers.PRIMASK.Should().Be(1); + } + + [Fact] + public void Cpsie_Should_Clear_PRIMASK() + { + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Cpsie); + Cpu.Registers.PRIMASK = 1; + + Cpu.Step(); + + Cpu.Registers.PRIMASK.Should().Be(0); + } + + [Fact] + public void Cpsie_Should_Set_InterruptsUpdated_Flag() + { + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Cpsie); + + Cpu.Step(); + + Cpu.Registers.InterruptsUpdated.Should().Be(true); + } + } + + // ================================================================ + // WFI + // ================================================================ + public class Wfi : CpuTestBase + { + [Fact] + public void Should_Set_Waiting_Flag() + { + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Wfi); + + Cpu.Step(); + + Cpu.Registers.Waiting.Should().Be(true); + } + } + + // ================================================================ + // SEV / WFE + // ================================================================ + public class SevWfe : CpuTestBase + { + [Fact] + public void Sev_Should_Set_EventRegistered() + { + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Sev); + + Cpu.Step(); + + Cpu.Registers.EventRegistered.Should().Be(true); + } + + [Fact] + public void Wfe_Should_Set_Waiting_When_No_Event() + { + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Wfe); + Cpu.Registers.EventRegistered = false; + + Cpu.Step(); + + Cpu.Registers.Waiting.Should().Be(true); + } + + [Fact] + public void Wfe_Should_Clear_Event_And_Not_Wait_When_Event_Registered() + { + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Wfe); + Cpu.Registers.EventRegistered = true; + + Cpu.Step(); + + Cpu.Registers.Waiting.Should().Be(false); + Cpu.Registers.EventRegistered.Should().Be(false); + } + } + + // ================================================================ + // BKPT + // ================================================================ + public class Bkpt : CpuTestBase + { + [Fact] + public void Should_Invoke_OnBreakpoint_Callback_With_Imm8() + { + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Bkpt(42)); + + byte? received = null; + Cpu.OnBreakpoint = imm => received = imm; + + Cpu.Step(); + + received.Should().Be(42); + } + + [Fact] + public void Should_Not_Throw_When_No_Callback_Registered() + { + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Bkpt(0)); + Cpu.OnBreakpoint = null; + + var act = () => Cpu.Step(); + act.Should().NotThrow(); + } + } + + // ================================================================ + // SVC (triggers PendingSVCall) + // ================================================================ + public class Svc : CpuTestBase + { + [Fact] + public void Should_Set_PendingSVCall_Flag() + { + Bus.WriteHalfWord(0x20000000, InstructionEmiter.Svc(0)); + + Cpu.Step(); + + Cpu.Registers.PendingSVCall.Should().Be(true); + Cpu.Registers.InterruptsUpdated.Should().Be(true); + } + } +} diff --git a/tests/RP2040Sharp.Tests/Dma/DmaTests.cs b/tests/RP2040Sharp.Tests/Dma/DmaTests.cs new file mode 100644 index 0000000..d77c6bb --- /dev/null +++ b/tests/RP2040Sharp.Tests/Dma/DmaTests.cs @@ -0,0 +1,271 @@ +using RP2040.Core.Cpu; +using RP2040.Core.Memory; +using RP2040.Peripherals.Dma; + +namespace RP2040.Peripherals.Tests.Dma; + +/// +/// Tests for the RP2040 DMA peripheral. +/// +public abstract class DmaTests +{ + private const uint CHANNEL_COUNT = 12; + + // Per-channel register offsets: base = ch * 0x40 + private static uint ChBase(int ch) => (uint)(ch * 0x40); + private static uint READ_ADDR(int ch) => ChBase(ch) + 0x00; + private static uint WRITE_ADDR(int ch) => ChBase(ch) + 0x04; + private static uint TRANS_COUNT(int ch) => ChBase(ch) + 0x08; + private static uint CTRL_TRIG(int ch) => ChBase(ch) + 0x0C; + + // System registers + private const uint INTR = 0x400; + private const uint INTE0 = 0x404; + private const uint INTF0 = 0x408; + private const uint INTS0 = 0x40C; + + // CTRL bits + private const uint CTRL_EN = 1u << 0; + private const uint CTRL_DATA_SIZE_WORD = 2u << 2; // SIZE=2 (word = 4 bytes) + private const uint CTRL_INCR_READ = 1u << 4; + private const uint CTRL_INCR_WRITE = 1u << 5; + private const uint CTRL_TREQ_PERMANENT = 0x3Fu << 15; // TREQ_SEL=63 = always ready + + private sealed class Fixture : IDisposable + { + public BusInterconnect Bus { get; } + public CortexM0Plus Cpu { get; } + public DmaPeripheral Dma { get; } + + public Fixture() + { + Bus = new BusInterconnect(); + Cpu = new CortexM0Plus(Bus); + Dma = new DmaPeripheral(Bus, Cpu); + } + + public void Dispose() => Bus.Dispose(); + + /// Write a 32-bit word to SRAM at the given address. + public void WriteToSram(uint addr, uint value) => + Bus.WriteWord(addr, value); + + /// Read a 32-bit word from SRAM. + public uint ReadFromSram(uint addr) => + Bus.ReadWord(addr); + } + + public class BasicTransfer + { + [Fact] + public void Single_word_transfer_copies_data() + { + using var f = new Fixture(); + + // Write source data to SRAM + f.WriteToSram(0x20000000u, 0x12345678u); + + // Configure ch0: read from 0x20000000, write to 0x20001000, 1 word + f.Dma.WriteWord(READ_ADDR(0), 0x20000000u); + f.Dma.WriteWord(WRITE_ADDR(0), 0x20001000u); + f.Dma.WriteWord(TRANS_COUNT(0), 1u); + f.Dma.WriteWord(CTRL_TRIG(0), + CTRL_EN | CTRL_DATA_SIZE_WORD | CTRL_INCR_READ | CTRL_INCR_WRITE | CTRL_TREQ_PERMANENT); + + f.ReadFromSram(0x20001000u).Should().Be(0x12345678u); + } + + [Fact] + public void Multi_word_transfer_copies_block() + { + using var f = new Fixture(); + + // Write 4 words to SRAM + for (uint i = 0; i < 4; i++) + f.WriteToSram(0x20002000u + i * 4, 0xAABBCC00u | i); + + f.Dma.WriteWord(READ_ADDR(0), 0x20002000u); + f.Dma.WriteWord(WRITE_ADDR(0), 0x20003000u); + f.Dma.WriteWord(TRANS_COUNT(0), 4u); + f.Dma.WriteWord(CTRL_TRIG(0), + CTRL_EN | CTRL_DATA_SIZE_WORD | CTRL_INCR_READ | CTRL_INCR_WRITE | CTRL_TREQ_PERMANENT); + + for (uint i = 0; i < 4; i++) + f.ReadFromSram(0x20003000u + i * 4).Should().Be(0xAABBCC00u | i); + } + + [Fact] + public void TRANS_COUNT_reads_back_before_trigger() + { + using var f = new Fixture(); + f.Dma.WriteWord(TRANS_COUNT(1), 64u); + f.Dma.ReadWord(TRANS_COUNT(1)).Should().Be(64u); + } + } + + public class Chaining + { + [Fact] + public void Channel_chaining_triggers_second_channel() + { + using var f = new Fixture(); + + // Source for ch0 and ch1 + f.WriteToSram(0x20004000u, 0xCAFEBABEu); + f.WriteToSram(0x20005000u, 0xDEAD1234u); + + // Pre-configure ch1 via AL1 alias (offset +0x10 within ch block) — NO trigger + const uint CH1_BASE = 1 * 0x40; + f.Dma.WriteWord(CH1_BASE + 0x00, 0x20005000u); // READ_ADDR + f.Dma.WriteWord(CH1_BASE + 0x04, 0x20007000u); // WRITE_ADDR + f.Dma.WriteWord(CH1_BASE + 0x08, 1u); // TRANS_COUNT + // Write CTRL via AL1 alias (no trigger): ch1 base + 0x10 + // CHAIN_TO = ch1 (itself = disable chaining) → bits [14:11] = 1 + f.Dma.WriteWord(CH1_BASE + 0x10, + CTRL_EN | CTRL_DATA_SIZE_WORD | CTRL_INCR_READ | CTRL_INCR_WRITE | CTRL_TREQ_PERMANENT + | (1u << 11)); // CHAIN_TO = 1 (self = no chain) + + // Trigger ch0 with CHAIN_TO=1 (bits [14:11] = 1 → 1 << 11) + f.Dma.WriteWord(READ_ADDR(0), 0x20004000u); + f.Dma.WriteWord(WRITE_ADDR(0), 0x20006000u); + f.Dma.WriteWord(TRANS_COUNT(0), 1u); + f.Dma.WriteWord(CTRL_TRIG(0), + CTRL_EN | CTRL_DATA_SIZE_WORD | CTRL_INCR_READ | CTRL_INCR_WRITE | CTRL_TREQ_PERMANENT + | (1u << 11)); // CHAIN_TO = 1 + + // ch0 should have run and chained to ch1 + f.ReadFromSram(0x20006000u).Should().Be(0xCAFEBABEu, "ch0 data at dest"); + f.ReadFromSram(0x20007000u).Should().Be(0xDEAD1234u, "ch1 data at dest after chain"); + } + } + + public class Interrupts + { + [Fact] + public void INTR_bit_set_after_channel_completes() + { + using var f = new Fixture(); + f.WriteToSram(0x20008000u, 0xABCDEF01u); + + f.Dma.WriteWord(READ_ADDR(2), 0x20008000u); + f.Dma.WriteWord(WRITE_ADDR(2), 0x20009000u); + f.Dma.WriteWord(TRANS_COUNT(2), 1u); + f.Dma.WriteWord(CTRL_TRIG(2), + CTRL_EN | CTRL_DATA_SIZE_WORD | CTRL_INCR_READ | CTRL_INCR_WRITE | CTRL_TREQ_PERMANENT); + + (f.Dma.ReadWord(INTR) & (1u << 2)).Should().Be(1u << 2, "INTR bit2 should be set after ch2 completes"); + } + + [Fact] + public void INTR_cleared_by_writing_1() + { + using var f = new Fixture(); + f.WriteToSram(0x2000A000u, 0u); + + f.Dma.WriteWord(READ_ADDR(3), 0x2000A000u); + f.Dma.WriteWord(WRITE_ADDR(3), 0x2000B000u); + f.Dma.WriteWord(TRANS_COUNT(3), 1u); + f.Dma.WriteWord(CTRL_TRIG(3), + CTRL_EN | CTRL_DATA_SIZE_WORD | CTRL_TREQ_PERMANENT); + + f.Dma.WriteWord(INTR, 1u << 3); // clear ch3 interrupt + (f.Dma.ReadWord(INTR) & (1u << 3)).Should().Be(0u, "INTR bit should be cleared"); + } + } + + public class HalfWordTransfer + { + [Fact] + public void Half_word_transfer_copies_2_bytes() + { + using var f = new Fixture(); + // Write halfword to SRAM + f.Bus.WriteHalfWord(0x2000C000u, (ushort)0xABCD); + + f.Dma.WriteWord(READ_ADDR(4), 0x2000C000u); + f.Dma.WriteWord(WRITE_ADDR(4), 0x2000D000u); + f.Dma.WriteWord(TRANS_COUNT(4), 1u); + f.Dma.WriteWord(CTRL_TRIG(4), + CTRL_EN | (1u << 2) /* SIZE=1 halfword */ | CTRL_INCR_READ | CTRL_INCR_WRITE | CTRL_TREQ_PERMANENT); + + f.Bus.ReadHalfWord(0x2000D000u).Should().Be((ushort)0xABCD); + } + } + + public class DreqHandshake + { + private const uint CTRL_DATA_SIZE_BYTE = 0u; // SIZE=0 (byte) + + [Fact] + public void DREQ_gated_transfer_executes_only_ready_beats() + { + using var f = new Fixture(); + + // Write 3 bytes to SRAM (source) + f.Bus.WriteByte(0x20010000u, 0xAA); + f.Bus.WriteByte(0x20010001u, 0xBB); + f.Bus.WriteByte(0x20010002u, 0xCC); + + // Register DREQ index 1 that is ready only 2 times + var readyCount = 2; + f.Dma.RegisterDreq(1, () => readyCount-- > 0); + + var ctrl = CTRL_EN | CTRL_DATA_SIZE_BYTE | CTRL_INCR_READ | CTRL_INCR_WRITE + | (1u << 15); // TREQ_SEL=1 + + f.Dma.WriteWord(READ_ADDR(5), 0x20010000u); + f.Dma.WriteWord(WRITE_ADDR(5), 0x20011000u); + f.Dma.WriteWord(TRANS_COUNT(5), 3u); + f.Dma.WriteWord(CTRL_TRIG(5), ctrl); + + // Only 2 beats should have executed (DREQ was ready twice) + f.Bus.ReadByte(0x20011000u).Should().Be(0xAA); + f.Bus.ReadByte(0x20011001u).Should().Be(0xBB); + // Third byte not transferred (DREQ was not ready) + f.Bus.ReadByte(0x20011002u).Should().Be(0x00); + // Channel should still be BUSY and TRANS_COUNT should be 1 + (f.Dma.ReadWord(CTRL_TRIG(5)) & (1u << 24)).Should().Be(1u << 24, "channel still BUSY"); + f.Dma.ReadWord(TRANS_COUNT(5)).Should().Be(1u, "1 transfer remaining"); + } + + [Fact] + public void DREQ_not_ready_transfers_zero_beats() + { + using var f = new Fixture(); + f.Bus.WriteWord(0x20012000u, 0x55555555u); + + // DREQ always not ready + f.Dma.RegisterDreq(2, () => false); + + var ctrl = CTRL_EN | CTRL_DATA_SIZE_WORD | CTRL_INCR_READ | CTRL_INCR_WRITE + | (2u << 15); // TREQ_SEL=2 + + f.Dma.WriteWord(READ_ADDR(6), 0x20012000u); + f.Dma.WriteWord(WRITE_ADDR(6), 0x20013000u); + f.Dma.WriteWord(TRANS_COUNT(6), 1u); + f.Dma.WriteWord(CTRL_TRIG(6), ctrl); + + // No transfer should have occurred + f.Bus.ReadWord(0x20013000u).Should().Be(0u); + f.Dma.ReadWord(TRANS_COUNT(6)).Should().Be(1u, "no beats consumed"); + } + + [Fact] + public void TREQ_PERMANENT_ignores_dreq_registration() + { + using var f = new Fixture(); + f.Bus.WriteWord(0x20014000u, 0x12345678u); + + // Even if we tried to register DREQ 63, it can't be registered + // (RegisterDreq throws on index 63). So PERMANENT always works. + f.Dma.WriteWord(READ_ADDR(7), 0x20014000u); + f.Dma.WriteWord(WRITE_ADDR(7), 0x20015000u); + f.Dma.WriteWord(TRANS_COUNT(7), 1u); + f.Dma.WriteWord(CTRL_TRIG(7), + CTRL_EN | CTRL_DATA_SIZE_WORD | CTRL_INCR_READ | CTRL_INCR_WRITE | CTRL_TREQ_PERMANENT); + + f.Bus.ReadWord(0x20015000u).Should().Be(0x12345678u); + } + } +} + diff --git a/tests/RP2040.Core.Tests/Fixtures/CpuTestBase.cs b/tests/RP2040Sharp.Tests/Fixtures/CpuTestBase.cs similarity index 100% rename from tests/RP2040.Core.Tests/Fixtures/CpuTestBase.cs rename to tests/RP2040Sharp.Tests/Fixtures/CpuTestBase.cs diff --git a/tests/RP2040Sharp.Tests/Fixtures/MachineTestBase.cs b/tests/RP2040Sharp.Tests/Fixtures/MachineTestBase.cs new file mode 100644 index 0000000..fa76025 --- /dev/null +++ b/tests/RP2040Sharp.Tests/Fixtures/MachineTestBase.cs @@ -0,0 +1,24 @@ +using RP2040.Core.Cpu; +using RP2040.Core.Memory; +using RP2040.Peripherals; + +namespace RP2040.Peripherals.Tests.Fixtures; + +/// +/// Base fixture that sets up an RP2040Machine for peripheral integration tests. +/// +public abstract class MachineTestBase : IDisposable +{ + protected readonly RP2040Machine Machine; + + protected MachineTestBase() + { + Machine = new RP2040Machine(); + } + + public void Dispose() + { + Machine.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/tests/RP2040Sharp.Tests/Gdb/GdbServerTests.cs b/tests/RP2040Sharp.Tests/Gdb/GdbServerTests.cs new file mode 100644 index 0000000..10f2234 --- /dev/null +++ b/tests/RP2040Sharp.Tests/Gdb/GdbServerTests.cs @@ -0,0 +1,168 @@ +using RP2040.Gdb; +using RP2040.Peripherals; + +namespace RP2040.Gdb.Tests; + +/// +/// In-process tests for the GDB Remote Serial Protocol server (no sockets). +/// +public class GdbServerTests +{ + private sealed class TestTarget(RP2040Machine machine) : IGdbTarget + { + public RP2040Machine Machine => machine; + public bool Executing { get; private set; } + public void Execute() => Executing = true; + public void Stop() => Executing = false; + } + + private static (GdbServer server, RP2040Machine machine, TestTarget target) NewServer() + { + var machine = new RP2040Machine(); + var target = new TestTarget(machine); + return (new GdbServer(target), machine, target); + } + + /// Strip the GDB framing ($..#cc) to compare the payload. + private static string Payload(string? message) + { + message.Should().NotBeNull(); + message!.Should().StartWith("$").And.Contain("#"); + return message[1..message.IndexOf('#')]; + } + + [Fact] + public void Halt_reason_query_reports_trap() + { + var (server, _, _) = NewServer(); + Payload(server.ProcessGdbMessage("?")).Should().Be(GdbServer.StopReplyTrap); + } + + [Fact] + public void qSupported_advertises_features() + { + var (server, _, _) = NewServer(); + Payload(server.ProcessGdbMessage("qSupported:multiprocess+")) + .Should().Contain("PacketSize").And.Contain("qXfer:features:read+"); + } + + [Fact] + public void Target_xml_is_served() + { + var (server, _, _) = NewServer(); + var payload = Payload(server.ProcessGdbMessage("qXfer:features:read:target.xml:0,fff")); + payload.Should().StartWith("l"); + payload.Should().Contain("org.gnu.gdb.arm.m-profile"); + } + + [Fact] + public void Read_all_registers_returns_17_words_little_endian() + { + var (server, machine, _) = NewServer(); + machine.Cpu.Registers[0] = 0x11223344; + machine.Cpu.Registers[15] = 0xCAFEBABE; // PC + + var payload = Payload(server.ProcessGdbMessage("g")); + + payload.Length.Should().Be(17 * 8, "16 GPRs + xPSR, 4 bytes each as hex"); + payload.Should().StartWith("44332211", "r0 is encoded little-endian"); + payload.Substring(15 * 8, 8).Should().Be("bebafeca", "r15/pc little-endian"); + } + + [Fact] + public void Read_single_register() + { + var (server, machine, _) = NewServer(); + machine.Cpu.Registers[3] = 0xDEADBEEF; + Payload(server.ProcessGdbMessage("p3")).Should().Be("efbeadde"); + } + + [Fact] + public void Write_single_register() + { + var (server, machine, _) = NewServer(); + // P5= + Payload(server.ProcessGdbMessage("P5=78563412")).Should().Be("OK"); + machine.Cpu.Registers[5].Should().Be(0x12345678u); + } + + [Fact] + public void Read_memory() + { + var (server, machine, _) = NewServer(); + machine.Bus.WriteWord(0x2000_0000, 0x04030201); + + Payload(server.ProcessGdbMessage("m20000000,4")).Should().Be("01020304"); + } + + [Fact] + public void Write_memory() + { + var (server, machine, _) = NewServer(); + Payload(server.ProcessGdbMessage("M20000000,4:0a0b0c0d")).Should().Be("OK"); + + machine.Bus.ReadByte(0x2000_0000).Should().Be(0x0A); + machine.Bus.ReadByte(0x2000_0003).Should().Be(0x0D); + } + + [Fact] + public void Single_step_executes_one_instruction() + { + var (server, machine, _) = NewServer(); + machine.Bus.WriteHalfWord(0x2000_0000, 0xBF00); // NOP + machine.Cpu.Registers.PC = 0x2000_0000; + + var payload = Payload(server.ProcessGdbMessage("vCont;s")); + + payload.Should().StartWith("T05").And.Contain("reason:trace"); + machine.Cpu.Registers.PC.Should().Be(0x2000_0002u, "PC advanced past the NOP"); + } + + [Fact] + public void Continue_starts_execution() + { + var (server, _, target) = NewServer(); + server.ProcessGdbMessage("c"); + target.Executing.Should().BeTrue(); + } + + [Fact] + public void Detach_acknowledges_and_resumes_execution() + { + var (server, _, target) = NewServer(); + Payload(server.ProcessGdbMessage("D")).Should().Be("OK"); + target.Executing.Should().BeTrue("detach leaves the target free-running"); + } + + [Fact] + public void Breakpoint_hit_stops_target_and_reports_trap() + { + var (server, machine, target) = NewServer(); + + var responses = new List(); + _ = new GdbConnection(server, responses.Add); // registers the breakpoint handler + target.Execute(); + + machine.Bus.WriteHalfWord(0x2000_0000, 0xBE00); // BKPT #0 + machine.Cpu.Registers.PC = 0x2000_0000; + machine.Cpu.Step(); + + target.Executing.Should().BeFalse("a breakpoint pauses the target"); + machine.Cpu.Registers.PC.Should().Be(0x2000_0000u, "PC is rewound to the BKPT address"); + responses.Should().Contain(GdbUtils.GdbMessage(GdbServer.StopReplyTrap)); + } + + [Fact] + public void Connection_parses_a_framed_packet_and_acks() + { + var (server, machine, _) = NewServer(); + machine.Cpu.Registers[1] = 0xAABBCCDD; + + var responses = new List(); + var conn = new GdbConnection(server, responses.Add); // initial '+' ack on construct + conn.FeedData(GdbUtils.GdbMessage("p1")); + + responses.Should().Contain("+"); + responses.Last().Should().Be(GdbUtils.GdbMessage("ddccbbaa"), "r1 little-endian"); + } +} diff --git a/tests/RP2040Sharp.Tests/I2c/I2cTests.cs b/tests/RP2040Sharp.Tests/I2c/I2cTests.cs new file mode 100644 index 0000000..5af2e4b --- /dev/null +++ b/tests/RP2040Sharp.Tests/I2c/I2cTests.cs @@ -0,0 +1,157 @@ +using RP2040.Peripherals.I2c; + +namespace RP2040.Peripherals.Tests.I2c; + +/// +/// Tests for the DW_apb_i2c peripheral, focusing on the slave-mode simulation API +/// used by external circuit simulators. +/// +public abstract class I2cTests +{ + private const uint IC_SAR = 0x008; + private const uint IC_DATA_CMD = 0x010; + private const uint IC_RAW_INTR_STAT = 0x034; + private const uint IC_CLR_RD_REQ = 0x050; + private const uint IC_CLR_STOP_DET = 0x060; + private const uint IC_ENABLE = 0x06C; + + // IC_RAW_INTR_STAT bits + private const uint RX_FULL = 1u << 2; + private const uint TX_EMPTY = 1u << 4; + private const uint RD_REQ = 1u << 5; + private const uint STOP_DET = 1u << 9; + + private static I2cPeripheral Enabled(byte slaveAddr = 0x55) + { + var i2c = new I2cPeripheral(); + i2c.WriteWord(IC_SAR, slaveAddr); + i2c.WriteWord(IC_ENABLE, 1); + return i2c; + } + + public class SlaveAddress + { + [Fact] + public void Writing_IC_SAR_updates_SlaveAddress_and_masks_to_7_bits() + { + var i2c = new I2cPeripheral(); + i2c.WriteWord(IC_SAR, 0x1A5); // 0x25 in low 7 bits + i2c.SlaveAddress.Should().Be(0x25); + } + + [Fact] + public void Writing_IC_SAR_raises_SlaveAddressChanged() + { + var i2c = new I2cPeripheral(); + byte? notified = null; + i2c.SlaveAddressChanged += a => notified = a; + + i2c.WriteWord(IC_SAR, 0x42); + + notified.Should().Be(0x42); + } + } + + public class SlaveReceive + { + [Fact] + public void Matching_write_address_is_acknowledged() + { + var i2c = Enabled(0x55); + i2c.SimulateIncomingAddress(0x55, isWrite: true).Should().BeTrue(); + } + + [Fact] + public void Non_matching_address_is_not_acknowledged() + { + var i2c = Enabled(0x55); + i2c.SimulateIncomingAddress(0x10, isWrite: true).Should().BeFalse(); + } + + [Fact] + public void Incoming_data_raises_RX_FULL_and_lands_in_FIFO() + { + var i2c = Enabled(0x55); + i2c.SimulateIncomingAddress(0x55, isWrite: true); + + i2c.SimulateIncomingData(0xAB); + + (i2c.ReadWord(IC_RAW_INTR_STAT) & RX_FULL).Should().Be(RX_FULL); + i2c.ReadWord(IC_DATA_CMD).Should().Be(0xABu, "firmware reads the received byte from IC_DATA_CMD"); + } + + [Fact] + public void Stop_raises_STOP_DET() + { + var i2c = Enabled(0x55); + i2c.SimulateIncomingAddress(0x55, isWrite: true); + i2c.SimulateIncomingData(0x01); + + i2c.SimulateStop(); + + (i2c.ReadWord(IC_RAW_INTR_STAT) & STOP_DET).Should().Be(STOP_DET); + } + } + + public class SlaveTransmit + { + [Fact] + public void Read_address_raises_RD_REQ() + { + var i2c = Enabled(0x55); + + i2c.SimulateIncomingAddress(0x55, isWrite: false).Should().BeTrue(); + + (i2c.ReadWord(IC_RAW_INTR_STAT) & RD_REQ).Should().Be(RD_REQ); + } + + [Fact] + public void Firmware_response_is_captured_and_sets_TX_EMPTY() + { + var i2c = Enabled(0x55); + i2c.SimulateIncomingAddress(0x55, isWrite: false); + + i2c.WriteWord(IC_DATA_CMD, 0x7E); // firmware responds with one byte + + (i2c.ReadWord(IC_RAW_INTR_STAT) & TX_EMPTY).Should().Be(TX_EMPTY); + i2c.HasSlaveTransmitByte.Should().BeTrue(); + i2c.ReadSlaveTransmitByte().Should().Be(0x7E); + i2c.HasSlaveTransmitByte.Should().BeFalse(); + } + + [Fact] + public void Multiple_bytes_are_queued_in_order_until_stop() + { + var i2c = Enabled(0x55); + i2c.SimulateIncomingAddress(0x55, isWrite: false); + + // Master clocks out three bytes before issuing STOP. + i2c.WriteWord(IC_DATA_CMD, 0x11); + i2c.WriteWord(IC_DATA_CMD, 0x22); + i2c.WriteWord(IC_DATA_CMD, 0x33); + + i2c.ReadSlaveTransmitByte().Should().Be(0x11); + i2c.ReadSlaveTransmitByte().Should().Be(0x22); + i2c.ReadSlaveTransmitByte().Should().Be(0x33); + } + + [Fact] + public void Stop_ends_slave_transmit_so_later_writes_are_master_writes() + { + var i2c = Enabled(0x55); + i2c.SimulateIncomingAddress(0x55, isWrite: false); + i2c.WriteWord(IC_DATA_CMD, 0x11); + i2c.SimulateStop(); + i2c.ReadSlaveTransmitByte(); // drain the queued byte + + byte? written = null; + i2c.OnWrite = (_, data) => written = data; + + // After STOP the slave-transmit phase is over: this is a master write. + i2c.WriteWord(IC_DATA_CMD, 0x99); + + written.Should().Be(0x99); + i2c.HasSlaveTransmitByte.Should().BeFalse(); + } + } +} diff --git a/tests/RP2040Sharp.Tests/Pio/PioTests.cs b/tests/RP2040Sharp.Tests/Pio/PioTests.cs new file mode 100644 index 0000000..b75b260 --- /dev/null +++ b/tests/RP2040Sharp.Tests/Pio/PioTests.cs @@ -0,0 +1,647 @@ +using RP2040.Core.Cpu; +using RP2040.Core.Memory; +using RP2040.Peripherals.Pio; +using RP2040.Peripherals.Sio; + +namespace RP2040.Peripherals.Tests.Pio; + +/// +/// Tests for the PIO state machine instruction set. +/// Uses PIO0 with state machine 0. +/// +public abstract class PioTests +{ + private const int SM = 0; + + private sealed class Fixture : IDisposable + { + public BusInterconnect Bus { get; } + public CortexM0Plus Cpu { get; } + public SioPeripheral Sio { get; } + public PioPeripheral Pio { get; } + + // Register addresses (within PIO base) + public const uint CTRL = 0x000; + public const uint FSTAT = 0x004; + public const uint FDEBUG = 0x008; + public const uint FLEVEL = 0x00C; + public const uint TXF0 = 0x010; + public const uint RXF0 = 0x020; + public const uint SM0_CLKDIV = 0x0C8; + public const uint SM0_EXECCTRL= 0x0CC; + public const uint SM0_SHIFTCTRL= 0x0D0; + public const uint SM0_ADDR = 0x0D4; + public const uint SM0_INSTR = 0x0D8; + public const uint SM0_PINCTRL = 0x0DC; + public const uint INSTR_MEM0 = 0x048; + + public Fixture() + { + Bus = new BusInterconnect(); + Cpu = new CortexM0Plus(Bus); + Sio = new SioPeripheral(Cpu); + Pio = new PioPeripheral(Cpu, 0); + } + + public void Dispose() => Bus.Dispose(); + + /// Load a single instruction at program address 0 and enable SM0. + public void LoadAndRun(ushort instr) + { + // Write instruction to instruction memory slot 0 + Pio.WriteWord(INSTR_MEM0, instr); + // Set EXECCTRL: wrap_top=0, wrap_bottom=0 → SM loops at address 0 + // EXECCTRL bits [16:12] = wrap_top, bits [11:7] = wrap_bottom + Pio.WriteWord(SM0_EXECCTRL, 0u); + // Set CLKDIV = 1 (integer=1, frac=0) + Pio.WriteWord(SM0_CLKDIV, 1u << 16); + // Enable SM0 + Pio.WriteWord(CTRL, 1u); + } + + /// Run N ticks of the PIO. + public void Tick(long n = 1) => Pio.Tick(n); + } + + // PIO instruction encoding helpers + // SET instruction: 111 DDDDD 00000 NNNNN where D=destination, N=data + private static ushort EncodeSet(uint dest, uint data) + => (ushort)(0b111_00000000_00000 | ((dest & 0x7) << 5) | (data & 0x1F)); + + // JMP instruction: 000 COND AAAAAAAA + private static ushort EncodeJmp(uint cond, uint addr) + => (ushort)(0b000_000_00000 | ((cond & 0x7) << 5) | (addr & 0x1F)); + + // PULL instruction: opcode=4 (bits 15-13), bit 7=1 (PULL), bit 6=IfEmpty, bit 5=Block + private static ushort EncodePull(bool block = true, bool ifEmpty = false) + => (ushort)((4 << 13) | (1 << 7) | (ifEmpty ? (1 << 6) : 0) | (block ? (1 << 5) : 0)); + + // OUT instruction: 011 DEST COUNT where count=0 means 32 + private static ushort EncodeOut(uint dest, uint count) + => (ushort)(0b011_00000_000_00000 | ((dest & 0x7) << 5) | (count & 0x1F)); + + // PUSH instruction: 100 0 IFFL NBLK 0100000 + private static ushort EncodePush(bool block = true, bool ifFull = false) + => (ushort)(0b100_0_0_0_0_00000 | (ifFull ? (1 << 6) : 0) | (block ? (1 << 5) : 0)); + + // MOV instruction: 101 DST OP SRC + private static ushort EncodeMov(uint dst, uint op, uint src) + => (ushort)(0b101_00000_00_00000 | ((dst & 0x7) << 5) | ((op & 0x3) << 3) | (src & 0x7)); + + // IN instruction: 010 SRC COUNT (count=0 means 32) + private static ushort EncodeIn(uint src, uint count) + => (ushort)(0b010_00000_000_00000 | ((src & 0x7) << 5) | (count & 0x1F)); + + public class SetPins + { + // SET PINS destination = 0b000 = PINS + private const uint DEST_PINS = 0; + private const uint DEST_X = 1; + private const uint DEST_Y = 2; + + [Fact] + public void SET_X_stores_immediate_value() + { + using var f = new Fixture(); + var instr = EncodeSet(DEST_X, 15); + f.LoadAndRun(instr); + f.Tick(2); // give a couple ticks + + // Read scratch X via SM0_INSTR executing a MOV trick... + // Simpler: just verify the SM ran (addr advanced past 0 and wrapped back to 0) + // With wrap top=0 bottom=0 it stays at 0 + f.Pio.ReadWord(Fixture.SM0_ADDR).Should().Be(0u, "SM0 wraps back to 0"); + } + + [Fact] + public void SET_Y_stores_immediate_value() + { + using var f = new Fixture(); + var instr = EncodeSet(DEST_Y, 7); + f.LoadAndRun(instr); + f.Tick(2); + f.Pio.ReadWord(Fixture.SM0_ADDR).Should().Be(0u, "SM0 loops at 0"); + } + } + + public class JmpInstruction + { + private const uint JMP_ALWAYS = 0; // condition = always + + [Fact] + public void JMP_unconditional_to_address_0_keeps_PC_at_0() + { + using var f = new Fixture(); + var instr = EncodeJmp(JMP_ALWAYS, 0); // JMP 0 + f.LoadAndRun(instr); + f.Tick(5); + f.Pio.ReadWord(Fixture.SM0_ADDR).Should().Be(0u); + } + + [Fact] + public void JMP_to_nonzero_address_updates_PC() + { + using var f = new Fixture(); + // Program: slot 0 = JMP 2, slot 2 = JMP 2 (loop at 2) + f.Pio.WriteWord(Fixture.INSTR_MEM0 + 0, EncodeJmp(JMP_ALWAYS, 2)); + f.Pio.WriteWord(Fixture.INSTR_MEM0 + 8, EncodeJmp(JMP_ALWAYS, 2)); // slot 2 + + // Set EXECCTRL: wrap_top=31, wrap_bottom=0 — bits [16:12] = 31 → (31<<12) + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, 31u << 12); + + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + f.Tick(3); // execute: JMP 2 at slot0, then JMP 2 at slot2, then JMP 2 again + f.Pio.ReadWord(Fixture.SM0_ADDR).Should().Be(2u, "PC should be at address 2 (looping)"); + } + } + + public class PushPull + { + [Fact] + public void PULL_from_empty_FIFO_stalls_SM() + { + using var f = new Fixture(); + var instr = EncodePull(block: true); + f.LoadAndRun(instr); + f.Tick(3); + + // RP2040 TRM §3.7: FDEBUG TXSTALL bits are at [27:24] (one bit per SM). + // SM0 TXSTALL = bit 24. + var fdebug = f.Pio.ReadWord(Fixture.FDEBUG); + ((fdebug >> 24) & 1u).Should().Be(1u, "TXSTALL bit 24 should be set when SM stalls on blocking PULL"); + } + + [Fact] + public void PULL_succeeds_when_TXF_has_data() + { + using var f = new Fixture(); + // Write data to TXF0 + f.Pio.WriteWord(Fixture.TXF0, 0xDEADBEEFu); + + var instr = EncodePull(block: true); + f.LoadAndRun(instr); + f.Tick(1); // one tick is enough to confirm the PULL succeeded (no stall) + + // TXSTALL (bit 24) must NOT be set when data was available. + var fdebug = f.Pio.ReadWord(Fixture.FDEBUG); + (fdebug & (1u << 24)).Should().Be(0u, "TXSTALL should NOT be set when data was available"); + } + + [Fact] + public void TXF0_not_full_flag_set_initially() + { + using var f = new Fixture(); + // FSTAT bits: TXFULL[3:0]=SM0-3 TX full, TXEMPTY[11:8]=SM0-3 TX empty + // Initially TX FIFO is empty, so TXFULL[0]=0 and TXEMPTY[0]=1 + var fstat = f.Pio.ReadWord(Fixture.FSTAT); + // SM0 TX EMPTY = bit 8 + (fstat & (1u << 8)).Should().Be(1u << 8, "TX FIFO of SM0 should be empty initially"); + } + } + + public class FifoLevel + { + [Fact] + public void FLEVEL_increases_as_TXF_is_written() + { + using var f = new Fixture(); + f.Pio.WriteWord(Fixture.TXF0, 1u); + var flevel = f.Pio.ReadWord(Fixture.FLEVEL); + // SM0 TX level is in bits [3:0] of FLEVEL + (flevel & 0xFu).Should().Be(1u, "TX FIFO level should be 1 after one push"); + } + + [Fact] + public void FLEVEL_TX_maxes_at_4_entries() + { + using var f = new Fixture(); + for (var i = 0; i < 5; i++) + f.Pio.WriteWord(Fixture.TXF0, (uint)i); + + var flevel = f.Pio.ReadWord(Fixture.FLEVEL); + (flevel & 0xFu).Should().Be(4u, "TX FIFO depth is 4"); + } + } + + public class InstrRegister + { + [Fact] + public void SM0_INSTR_executes_instruction_immediately() + { + using var f = new Fixture(); + // Enable the SM first + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + // Writing to SM0_INSTR executes immediately (side-loads instruction) + var setX = EncodeSet(1 /* X */, 0b10101); // SET X, 21 + f.Pio.WriteWord(Fixture.SM0_INSTR, setX); + f.Tick(1); + + // The SM should have executed SET X, 21 — PC doesn't advance for EXEC writes + // We can't directly read X, but we can verify the SM is still running (no stall) + var fdebug = f.Pio.ReadWord(Fixture.FDEBUG); + (fdebug & (1u << 24)).Should().Be(0u, "SM should not be stalled"); + } + } + + public class FifoJoin + { + private const uint SM0_SHIFTCTRL = 0x0D0; + private const uint FJOIN_TX = 1u << 31; // double TX to 8 entries + private const uint FJOIN_RX = 1u << 30; // double RX to 8 entries + + [Fact] + public void FJOIN_TX_allows_8_entries_in_TX_FIFO() + { + using var f = new Fixture(); + // Enable FJOIN_TX: TX FIFO becomes 8 deep + f.Pio.WriteWord(SM0_SHIFTCTRL, FJOIN_TX); + + // Write 8 words — all should be accepted + for (uint i = 0; i < 8; i++) + f.Pio.WriteWord(Fixture.TXF0, i); + + // FLEVEL TX should be 8 + var flevel = f.Pio.ReadWord(Fixture.FLEVEL); + (flevel & 0xFu).Should().Be(8u, "TX FIFO depth is 8 with FJOIN_TX"); + } + + [Fact] + public void FJOIN_TX_caps_at_8_entries() + { + using var f = new Fixture(); + f.Pio.WriteWord(SM0_SHIFTCTRL, FJOIN_TX); + + // Write 10 words — only 8 fit + for (uint i = 0; i < 10; i++) + f.Pio.WriteWord(Fixture.TXF0, i); + + var flevel = f.Pio.ReadWord(Fixture.FLEVEL); + (flevel & 0xFu).Should().Be(8u, "TX FIFO caps at 8 with FJOIN_TX"); + } + + [Fact] + public void Without_FJOIN_TX_FIFO_caps_at_4() + { + using var f = new Fixture(); + // No join: default depth = 4 + for (uint i = 0; i < 6; i++) + f.Pio.WriteWord(Fixture.TXF0, i); + + var flevel = f.Pio.ReadWord(Fixture.FLEVEL); + (flevel & 0xFu).Should().Be(4u, "TX FIFO depth is still 4 without FJOIN"); + } + } + + public class Sideset + { + // Helper: encode a raw sideset+delay field into bits [12:8] of a JMP 0 instruction. + private static ushort JmpWithField(int field5bit) + => (ushort)(0x0000 | ((field5bit & 0x1F) << 8)); + + // PINCTRL.SIDESET_COUNT = N → bits [31:29] = N (inclusive of enable when SIDE_EN=1) + // PINCTRL.SIDESET_BASE = B → bits [14:10] = B + private static uint PinCtrl(uint sidesetCount, uint sidesetBase) + => (sidesetCount << 29) | (sidesetBase << 10); + + // EXECCTRL: SIDE_EN = bit 30, WRAP_TOP = bits [16:12] = 0 (wrap at 0) + private static uint ExecCtrl(bool sideEn = false) + => sideEn ? (1u << 30) : 0u; + + [Fact] + public void Sideset_drives_pins_without_SideEn() + { + using var f = new Fixture(); + uint capturedPins = 0, capturedMask = 0; + f.Pio.WriteGpioPins = (v, m) => { capturedPins = v; capturedMask = m; }; + + // PINCTRL: SIDESET_COUNT=2, SIDESET_BASE=0 → occupies field bits [4:3] + f.Pio.WriteWord(Fixture.SM0_PINCTRL, PinCtrl(2, 0)); + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, ExecCtrl(false)); + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + + // sideset=0b11 (both pins high), no delay → field bits [4:3]=0b11 → field = 0b11000 = 24 + var instr = JmpWithField(0b11000); // 0b11 << 3 + f.Pio.WriteWord(Fixture.INSTR_MEM0, instr); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + f.Tick(1); + + (capturedMask & 3u).Should().Be(3u, "mask covers pins 0 and 1"); + (capturedPins & 3u).Should().Be(3u, "both sideset pins driven high"); + } + + [Fact] + public void Sideset_with_nonzero_base_drives_correct_pins() + { + using var f = new Fixture(); + uint capturedPins = 0; + f.Pio.WriteGpioPins = (v, m) => { capturedPins = v; }; + + // PINCTRL: SIDESET_COUNT=1, SIDESET_BASE=5 → pin 5 only + f.Pio.WriteWord(Fixture.SM0_PINCTRL, PinCtrl(1, 5)); + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, ExecCtrl(false)); + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + + // sideset=1 (pin 5 high), no delay → top 1 bit of field = bit 4 → field = 0b10000 = 16 + var instr = JmpWithField(0b10000); + f.Pio.WriteWord(Fixture.INSTR_MEM0, instr); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + f.Tick(1); + + (capturedPins & (1u << 5)).Should().NotBe(0u, "pin 5 driven high"); + } + + [Fact] + public void Delay_stalls_SM_for_correct_number_of_cycles() + { + using var f = new Fixture(); + + // PINCTRL: SIDESET_COUNT=0 → all 5 bits are delay + f.Pio.WriteWord(Fixture.SM0_PINCTRL, PinCtrl(0, 0)); + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, ExecCtrl(false)); + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + + // delay=3 in bits [2:0] of the 5-bit field → field = 0b00011 = 3 + var instr = JmpWithField(3); + f.Pio.WriteWord(Fixture.INSTR_MEM0, instr); + + // Set WRAP_TOP=31 so SM doesn't get stuck in wrap at 0 + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, (31u << 12)); + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + // Tick 1: instruction executes, delay counter = 3, PC → 1 + // Ticks 2-4: SM burning delay (PC stays at 1, no advance per delay tick) + f.Tick(1); // executes JMP 0 → PC=0 again, delay=3 loaded + // SM is now at PC=0 with delay=3, 3 more cycles needed before next exec + f.Tick(3); // burning 3 delay cycles + // After 3 delay burns the counter hits 0 — on tick 5 the instruction re-executes + f.Pio.ReadWord(Fixture.SM0_ADDR).Should().Be(0u, "PC back at 0 (JMP 0) after delay burned"); + } + + [Fact] + public void Sideset_not_applied_when_SM_stalls() + { + using var f = new Fixture(); + var sidesetCallCount = 0; + f.Pio.WriteGpioPins = (_, _) => sidesetCallCount++; + + // Configure sideset on pin 0 + f.Pio.WriteWord(Fixture.SM0_PINCTRL, PinCtrl(1, 0)); + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, ExecCtrl(false)); + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + + // PULL block — will stall because TX FIFO is empty + // sideset value = 1 → field bit 4 = 1 → field = 0b10000 = 16 + var pullField = (ushort)(EncodePull(block: true) | (16 << 8)); + f.Pio.WriteWord(Fixture.INSTR_MEM0, pullField); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + // Run 5 ticks — SM stalls on empty FIFO every tick + f.Tick(5); + + sidesetCallCount.Should().Be(0, "sideset must not fire on stall cycles"); + } + + [Fact] + public void SideEn_enable_bit_gates_sideset() + { + using var f = new Fixture(); + var sidesetCallCount = 0; + f.Pio.WriteGpioPins = (_, _) => sidesetCallCount++; + + // PINCTRL: SIDESET_COUNT=3 (inclusive: 1 enable + 2 pins), SIDESET_BASE=0 + f.Pio.WriteWord(Fixture.SM0_PINCTRL, PinCtrl(3, 0)); + // EXECCTRL: SIDE_EN=1 (bit 30) + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, ExecCtrl(sideEn: true)); + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + + // Instruction with enable=0 in field: bit 4=0, pins=0b11 → field = 0b01100 = 12 + var instrNoEnable = JmpWithField(0b01100); + f.Pio.WriteWord(Fixture.INSTR_MEM0, instrNoEnable); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + f.Tick(2); + sidesetCallCount.Should().Be(0, "sideset must not fire when enable bit is 0"); + } + + [Fact] + public void SideEn_enable_bit_set_fires_sideset() + { + using var f = new Fixture(); + uint capturedPins = 0; + f.Pio.WriteGpioPins = (v, _) => capturedPins = v; + + // PINCTRL: SIDESET_COUNT=3 (1 enable + 2 pins), SIDESET_BASE=2 + f.Pio.WriteWord(Fixture.SM0_PINCTRL, PinCtrl(3, 2)); + // EXECCTRL: SIDE_EN=1 + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, ExecCtrl(sideEn: true)); + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + + // enable=1, pins=0b11 → top 3 bits of field: bit4=1, [3:2]=0b11 → field = 0b11100 = 28 + var instrWithEnable = JmpWithField(0b11100); + f.Pio.WriteWord(Fixture.INSTR_MEM0, instrWithEnable); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + f.Tick(1); + + // Pins 2 and 3 (sidesetBase=2, 2 pins) should both be high + (capturedPins & (3u << 2)).Should().Be(3u << 2, "pins 2 and 3 driven high"); + } + } + + // ── Bug-fix regression tests ───────────────────────────────────────── + + public class JmpOsre + { + // JMP condition 7 = !OSRE: jump when OSR is NOT empty (OsrCount > 0) + + [Fact] + public void JMP_OSRE_falls_through_when_OSR_empty() + { + using var f = new Fixture(); + // OSR is empty (OsrCount=0, default) — JMP !OSRE should NOT be taken + // Program slot 0 = JMP !OSRE 0 (target=0), wrap_top=31 so SM advances normally + f.Pio.WriteWord(Fixture.INSTR_MEM0, EncodeJmp(7, 0)); + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, 31u << 12); + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + f.Tick(1); + + // OSR is empty → condition false → fall through → PC = 1 + f.Pio.ReadWord(Fixture.SM0_ADDR).Should().Be(1u, + "JMP !OSRE must NOT jump when OsrCount=0 (OSR empty)"); + } + + [Fact] + public void JMP_OSRE_jumps_when_OSR_has_data() + { + using var f = new Fixture(); + // Pre-load TXF0 so PULL can fill the OSR + f.Pio.WriteWord(Fixture.TXF0, 0xAABBCCDDu); + + // Program: [0] PULL, [1] JMP !OSRE 1 (loop while OSR non-empty) + f.Pio.WriteWord(Fixture.INSTR_MEM0 + 0, EncodePull(block: true)); + f.Pio.WriteWord(Fixture.INSTR_MEM0 + 4, EncodeJmp(7, 1)); + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, 31u << 12); + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + f.Tick(2); // tick 1: PULL (OsrCount→32); tick 2: JMP !OSRE 1 + + // OSR has 32 bits → condition true → jump back to 1 + f.Pio.ReadWord(Fixture.SM0_ADDR).Should().Be(1u, + "JMP !OSRE must jump when OsrCount=32 (OSR full)"); + } + } + + public class ShiftBits32 + { + // When bitCount encodes 0 → 32. C# shift operators are mod-32, so + // explicit checks are required for the 32-bit case. + + [Fact] + public void OUT_PINS_32_right_shift_outputs_all_OSR_bits() + { + using var f = new Fixture(); + uint capturedPins = 0; + f.Pio.WriteGpioPins = (v, _) => capturedPins = v; + + // OSR shift right (SHIFTCTRL bit 19) + f.Pio.WriteWord(Fixture.SM0_SHIFTCTRL, 1u << 19); + f.Pio.WriteWord(Fixture.TXF0, 0xCAFEBABEu); + + // Program: [0] PULL, [1] OUT PINS 32 (count=0 encodes 32) + f.Pio.WriteWord(Fixture.INSTR_MEM0 + 0, EncodePull(block: true)); + f.Pio.WriteWord(Fixture.INSTR_MEM0 + 4, EncodeOut(0, 0)); // OUT PINS, 32 + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, 1u << 12); // wrap_top=1 + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + f.Tick(2); // PULL then OUT PINS 32 + + capturedPins.Should().Be(0xCAFEBABEu, + "OUT PINS with bitCount=32 right-shift must output all 32 OSR bits"); + } + + [Fact] + public void OUT_PINS_32_left_shift_clears_OSR_afterwards() + { + // With left-shift OUT 32, the OSR must become 0 after the operation. + // The bug was sm.OSR <<= 32 which is a no-op in C# (mod-32 shift). + // We observe this indirectly: after OUT 32, autopull refills from TXF. + using var f = new Fixture(); + uint capturedFirst = 0, capturedSecond = 0; + var callCount = 0; + f.Pio.WriteGpioPins = (v, _) => + { + if (callCount++ == 0) capturedFirst = v; + else capturedSecond = v; + }; + + // Two distinct values in TX FIFO + f.Pio.WriteWord(Fixture.TXF0, 0x11111111u); + f.Pio.WriteWord(Fixture.TXF0, 0x22222222u); + + // OSR shift left (default), autopull enabled at threshold 32 (=0) + // SHIFTCTRL: bit 17 = autopull, bits [29:25] = threshold (0 → 32) + f.Pio.WriteWord(Fixture.SM0_SHIFTCTRL, 1u << 17); + + // Program: [0] OUT PINS 32 (autopull refills from TXF), loops at 0 + f.Pio.WriteWord(Fixture.INSTR_MEM0, EncodeOut(0, 0)); + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, 0u); // wrap at 0 + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + // Pre-load OSR by writing PULL via forced INSTR before enabling SM + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + f.Pio.WriteWord(Fixture.CTRL, 1u); + // Writing SM0_INSTR executes the instruction immediately (per TRM §3.4.5 and rp2040js + // reference). PULL runs at write time and loads the first TXF value into OSR. + f.Pio.WriteWord(Fixture.SM0_INSTR, EncodePull(block: false)); + // No extra tick needed for the PULL — it already ran during the register write. + + // Tick OUT PINS 32 twice — autopull should reload OSR from TXF after each OUT. + f.Tick(1); // OUT PINS 32 (first value = 0x11111111), autopull loads second + f.Tick(1); // OUT PINS 32 (second value = 0x22222222) + + capturedFirst.Should().Be(0x11111111u, "first OUT PINS 32 outputs first TXF value"); + capturedSecond.Should().Be(0x22222222u, + "second OUT PINS 32 must output second value — OSR must be 0 after OUT 32 for autopull to trigger"); + } + + [Fact] + public void IN_PINS_32_left_shift_captures_full_gpio_value() + { + // With left-shift IN 32, ISR must become exactly the GPIO value. + // The bug was: ISR = (ISR << 32) | (data & 0) — left shift mod-32 = no-op, + // mask of (1u<<32)-1 = 0 in C#, so ISR was unchanged instead of replaced. + using var f = new Fixture(); + const uint gpioValue = 0xDEADF00Du; + f.Pio.ReadGpioIn = () => gpioValue; + + // Autopush at threshold=32 (bit 16 = enable, bits[24:20]=0 → threshold 32) + f.Pio.WriteWord(Fixture.SM0_SHIFTCTRL, 1u << 16); + + // Program: [0] IN PINS 32 — autopush fires immediately at threshold 32 + f.Pio.WriteWord(Fixture.INSTR_MEM0, EncodeIn(0, 0)); // IN PINS, 32 + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, 0u); // wrap at 0 + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + f.Tick(1); // IN PINS 32 → autopush enqueues ISR into RX FIFO + + var rxValue = f.Pio.ReadWord(Fixture.RXF0); + rxValue.Should().Be(gpioValue, + "IN PINS with bitCount=32 left-shift must store all 32 GPIO bits in ISR"); + } + } + + public class PushNoblockClearsIsr + { + [Fact] + public void PUSH_NOBLOCK_clears_ISR_when_RX_FIFO_full() + { + // When RX FIFO is full, PUSH NOBLOCK should discard the data AND clear ISR. + // We observe ISR cleared by: after the NOBLOCK PUSH, doing another IN and autopush — + // if ISR was cleared, IsrCount restarts from 0. + using var f = new Fixture(); + + // Fill RX FIFO to capacity (4 entries for normal mode) + for (uint i = 0; i < 4; i++) + f.Pio.InjectRxData(0, i); + + // Program: [0] PUSH NOBLOCK [1] JMP 0 (loop) + // Autopush enabled so we can observe ISR state via subsequent pushes + f.Pio.WriteWord(Fixture.INSTR_MEM0 + 0, EncodePush(block: false)); + f.Pio.WriteWord(Fixture.INSTR_MEM0 + 4, EncodeJmp(0, 0)); + f.Pio.WriteWord(Fixture.SM0_EXECCTRL, 1u << 12); // wrap_top=1 + f.Pio.WriteWord(Fixture.SM0_CLKDIV, 1u << 16); + f.Pio.WriteWord(Fixture.CTRL, 1u); + + // Force-load a known ISR value before PUSH executes + f.Pio.WriteWord(Fixture.SM0_INSTR, EncodeMov(6 /*ISR*/, 1 /*invert*/, 3 /*NULL*/)); + f.Tick(1); // execute MOV ISR, ~NULL → ISR = 0xFFFFFFFF, IsrCount doesn't change here + + // Execute PUSH NOBLOCK with full FIFO — ISR should be cleared + f.Tick(1); + + // Now drain the RX FIFO so it has space + for (var i = 0; i < 4; i++) f.Pio.ReadWord(Fixture.RXF0); + + // Force an IN PINS 1 to shift 1 bit into ISR — if ISR was cleared (IsrCount=0), + // this sets IsrCount=1. Then PUSH NOBLOCK with not-full FIFO should push IsrCount bits. + // We can't read IsrCount directly, but we can check the FSTAT RX-empty flag. + // Simpler: force PUSH and check that RX FIFO received 0 (ISR was 0 after clear). + f.Pio.WriteWord(Fixture.SM0_INSTR, EncodePush(block: false)); + f.Tick(1); + + var pushed = f.Pio.ReadWord(Fixture.RXF0); + pushed.Should().Be(0u, "ISR should have been cleared to 0 by the preceding PUSH NOBLOCK"); + } + } +} diff --git a/tests/RP2040Sharp.Tests/Pwm/PwmTests.cs b/tests/RP2040Sharp.Tests/Pwm/PwmTests.cs new file mode 100644 index 0000000..25590cc --- /dev/null +++ b/tests/RP2040Sharp.Tests/Pwm/PwmTests.cs @@ -0,0 +1,199 @@ +using RP2040.Core.Cpu; +using RP2040.Core.Memory; +using RP2040.Peripherals.Pwm; + +namespace RP2040.Peripherals.Tests.Pwm; + +/// +/// Tests for the RP2040 PWM peripheral (§4.5 of RP2040 TRM). +/// +public abstract class PwmTests +{ + private static uint SliceBase(int s) => (uint)(s * 0x14); + private static uint CSR(int s) => SliceBase(s) + 0x00; + private static uint DIV(int s) => SliceBase(s) + 0x04; + private static uint CTR(int s) => SliceBase(s) + 0x08; + private static uint CC(int s) => SliceBase(s) + 0x0C; + private static uint TOP(int s) => SliceBase(s) + 0x10; + + private const uint REG_EN = 0xA0; + private const uint REG_INTR = 0xA4; + private const uint REG_INTE = 0xA8; + private const uint REG_INTS = 0xB0; + + 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_PH_ADV = 1u << 7; + private const uint CSR_PH_RET = 1u << 6; + + private sealed class Fixture : IDisposable + { + public BusInterconnect Bus { get; } + public CortexM0Plus Cpu { get; } + public PwmPeripheral Pwm { get; } + + public Fixture() + { + Bus = new BusInterconnect(); + Cpu = new CortexM0Plus(Bus); + Pwm = new PwmPeripheral(Cpu); + } + + public void Dispose() => Bus.Dispose(); + } + + public class Counter + { + [Fact] + public void Counter_advances_when_slice_enabled() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CSR(0), CSR_EN); + f.Pwm.WriteWord(DIV(0), 1 << 4); // integer=1, frac=0 + + f.Pwm.Tick(10); + + f.Pwm.ReadWord(CTR(0)).Should().BeGreaterThan(0u, "counter should advance"); + } + + [Fact] + public void Counter_does_not_advance_when_slice_disabled() + { + using var f = new Fixture(); + // CSR_EN = 0 (default) + f.Pwm.Tick(100); + f.Pwm.ReadWord(CTR(0)).Should().Be(0u); + } + + [Fact] + public void Counter_wraps_at_TOP() + { + using var f = new Fixture(); + f.Pwm.WriteWord(TOP(0), 9u); // wrap at 9 + f.Pwm.WriteWord(DIV(0), 1 << 4); + f.Pwm.WriteWord(CTR(0), 8u); // start near top + f.Pwm.WriteWord(CSR(0), CSR_EN); + + f.Pwm.Tick(3); // should wrap + + f.Pwm.ReadWord(CTR(0)).Should().BeLessThanOrEqualTo(9u, "counter should stay within TOP"); + } + + [Fact] + public void Writing_CTR_sets_counter() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CTR(0), 42u); + f.Pwm.ReadWord(CTR(0)).Should().Be(42u); + } + } + + public class Interrupts + { + [Fact] + public void INTR_bit_set_after_wrap() + { + using var f = new Fixture(); + f.Pwm.WriteWord(TOP(0), 4u); + f.Pwm.WriteWord(DIV(0), 1 << 4); + f.Pwm.WriteWord(CTR(0), 3u); + f.Pwm.WriteWord(CSR(0), CSR_EN); + + f.Pwm.Tick(5); // enough to wrap + + (f.Pwm.ReadWord(REG_INTR) & 1u).Should().Be(1u, "INTR bit 0 should be set after wrap"); + } + + [Fact] + public void INTR_cleared_by_writing_1() + { + using var f = new Fixture(); + f.Pwm.WriteWord(TOP(0), 2u); + f.Pwm.WriteWord(DIV(0), 1 << 4); + f.Pwm.WriteWord(CSR(0), CSR_EN); + f.Pwm.Tick(5); + + f.Pwm.WriteWord(REG_INTR, 1u); + (f.Pwm.ReadWord(REG_INTR) & 1u).Should().Be(0u); + } + + [Fact] + public void INTS_reflects_INTR_when_INTE_enabled() + { + using var f = new Fixture(); + f.Pwm.WriteWord(INTE(0), 1u); + f.Pwm.WriteWord(TOP(0), 2u); + f.Pwm.WriteWord(DIV(0), 1 << 4); + f.Pwm.WriteWord(CSR(0), CSR_EN); + f.Pwm.Tick(5); + + (f.Pwm.ReadWord(REG_INTS) & 1u).Should().Be(1u); + } + + private static uint INTE(int s) => REG_INTE; + } + + public class DutyReadback + { + [Fact] + public void GetDutyA_returns_channel_A_compare_value() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CC(0), 0x00000064u); // A=100 (bits 15:0) + f.Pwm.GetDutyA(0).Should().Be(100); + } + + [Fact] + public void GetDutyB_returns_channel_B_compare_value() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CC(0), 0x01900000u); // B=400 (bits 31:16) + f.Pwm.GetDutyB(0).Should().Be(400); + } + + [Fact] + public void GetDutyA_inverted_when_A_INV_set() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CC(0), 0x0000FFFFu); // A = 0xFFFF + f.Pwm.WriteWord(CSR(0), CSR_A_INV); // set A_INV + + f.Pwm.GetDutyA(0).Should().Be(0, "~0xFFFF = 0x0000 (ushort)"); + } + } + + public class PhaseControl + { + [Fact] + public void PH_ADV_increments_counter() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CTR(0), 5u); + + // Write CSR with PH_ADV strobe + f.Pwm.WriteWord(CSR(0), CSR_PH_ADV | CSR_EN); + f.Pwm.ReadWord(CTR(0)).Should().Be(6u, "PH_ADV should increment counter by 1"); + } + + [Fact] + public void PH_RET_decrements_counter() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CTR(0), 10u); + + f.Pwm.WriteWord(CSR(0), CSR_PH_RET | CSR_EN); + f.Pwm.ReadWord(CTR(0)).Should().Be(9u, "PH_RET should decrement counter by 1"); + } + + [Fact] + public void PH_ADV_not_stored_in_CSR() + { + using var f = new Fixture(); + f.Pwm.WriteWord(CSR(0), CSR_PH_ADV | CSR_EN); + var csr = f.Pwm.ReadWord(CSR(0)); + (csr & CSR_PH_ADV).Should().Be(0u, "PH_ADV is a strobe and must not be stored in CSR"); + } + } +} diff --git a/tests/RP2040.Core.Tests/RP2040.Core.Tests.csproj b/tests/RP2040Sharp.Tests/RP2040Sharp.Tests.csproj similarity index 74% rename from tests/RP2040.Core.Tests/RP2040.Core.Tests.csproj rename to tests/RP2040Sharp.Tests/RP2040Sharp.Tests.csproj index acda266..9584fe9 100644 --- a/tests/RP2040.Core.Tests/RP2040.Core.Tests.csproj +++ b/tests/RP2040Sharp.Tests/RP2040Sharp.Tests.csproj @@ -1,13 +1,16 @@ - + net10.0 enable - enable false - RP2040.tests - true + true + + + + + @@ -17,10 +20,6 @@ - - - - - + diff --git a/tests/RP2040Sharp.Tests/Rtc/RtcTests.cs b/tests/RP2040Sharp.Tests/Rtc/RtcTests.cs new file mode 100644 index 0000000..3bda99d --- /dev/null +++ b/tests/RP2040Sharp.Tests/Rtc/RtcTests.cs @@ -0,0 +1,231 @@ +using FluentAssertions; +using RP2040.Peripherals.Rtc; +using Xunit; + +namespace RP2040.Peripherals.Tests.Rtc; + +public class RtcTests +{ + // Register offsets + private const uint RTC_SETUP0 = 0x04; + private const uint RTC_SETUP1 = 0x08; + private const uint RTC_CTRL = 0x0C; + private const uint IRQ_SETUP_0 = 0x10; + private const uint IRQ_SETUP_1 = 0x14; + private const uint RTC_RTC1 = 0x18; + private const uint RTC_RTC0 = 0x1C; + + private const uint CTRL_ENABLE = 1u; + private const uint CTRL_ACTIVE = 1u << 1; + private const uint CTRL_LOAD = 1u << 4; + + private const uint IRQ0_MATCH_ENA = 1u << 31; + private const uint IRQ1_MATCH_ACTIVE = 1u << 31; + private const uint IRQ1_SEC_ENA = 1u << 6; // ENA at bit 6, SEC value at [5:0] + + // ── Helpers ────────────────────────────────────────────────────────────── + + private static RtcPeripheral MakeEnabled() + { + var rtc = new RtcPeripheral(); + // Load setup and enable + rtc.WriteWord(RTC_SETUP0, (2024u << 16) | (1u << 8) | 1u); // 2024-01-01 + rtc.WriteWord(RTC_SETUP1, (1u << 24)); // Monday 00:00:00 + rtc.WriteWord(RTC_CTRL, CTRL_LOAD | CTRL_ENABLE); + return rtc; + } + + // ── Default registers ───────────────────────────────────────────────── + + [Fact] + public void DefaultTime_Is_20240101_Monday_000000() + { + var rtc = new RtcPeripheral(); + // RTC0: YEAR[27:16]=2024, MONTH[11:8]=1, DAY[4:0]=1 + var rtc0 = rtc.ReadWord(RTC_RTC0); + ((rtc0 >> 16) & 0xFFF).Should().Be(2024); + ((rtc0 >> 8) & 0xF) .Should().Be(1); + (rtc0 & 0x1F) .Should().Be(1); + + // RTC1: DOTW[26:24]=1 (Monday) + var rtc1 = rtc.ReadWord(RTC_RTC1); + ((rtc1 >> 24) & 0x7).Should().Be(1); + ((rtc1 >> 16) & 0x1F).Should().Be(0); // hour + ((rtc1 >> 8) & 0x3F).Should().Be(0); // min + (rtc1 & 0x3F) .Should().Be(0); // sec + } + + // ── CTRL LOAD latches SETUP values ─────────────────────────────────── + + [Fact] + public void CtrlLoad_Latches_Setup0_And_Setup1() + { + var rtc = new RtcPeripheral(); + rtc.WriteWord(RTC_SETUP0, (2025u << 16) | (6u << 8) | 15u); // 2025-06-15 + rtc.WriteWord(RTC_SETUP1, (3u << 24) | (10u << 16) | (30u << 8) | 45u); // Wed 10:30:45 + + rtc.WriteWord(RTC_CTRL, CTRL_LOAD | CTRL_ENABLE); + + var rtc0 = rtc.ReadWord(RTC_RTC0); + var rtc1 = rtc.ReadWord(RTC_RTC1); + + ((rtc0 >> 16) & 0xFFF).Should().Be(2025); + ((rtc0 >> 8) & 0xF) .Should().Be(6); + (rtc0 & 0x1F) .Should().Be(15); + + ((rtc1 >> 24) & 0x7) .Should().Be(3); // Wed + ((rtc1 >> 16) & 0x1F) .Should().Be(10); + ((rtc1 >> 8) & 0x3F) .Should().Be(30); + (rtc1 & 0x3F) .Should().Be(45); + } + + // ── CTRL ACTIVE reflects CTRL ENABLE ───────────────────────────────── + + [Fact] + public void CtrlActive_Reflects_Enable() + { + var rtc = new RtcPeripheral(); + + // Disabled: ACTIVE should be 0 + rtc.WriteWord(RTC_CTRL, 0); + (rtc.ReadWord(RTC_CTRL) & CTRL_ACTIVE).Should().Be(0); + + // Enabled: ACTIVE should be set + rtc.WriteWord(RTC_CTRL, CTRL_ENABLE); + (rtc.ReadWord(RTC_CTRL) & CTRL_ACTIVE).Should().NotBe(0u); + } + + // ── Tick advances time ─────────────────────────────────────────────── + + [Fact] + public void Tick_125M_Cycles_Advances_One_Second() + { + var rtc = MakeEnabled(); + + rtc.Tick(125_000_000L); + + var rtc1 = rtc.ReadWord(RTC_RTC1); + (rtc1 & 0x3F).Should().Be(1); // sec = 1 + } + + [Fact] + public void Tick_Disabled_Does_Not_Advance() + { + var rtc = new RtcPeripheral(); + // Don't enable + rtc.WriteWord(RTC_CTRL, 0); + + rtc.Tick(125_000_000L * 60); + + var rtc1 = rtc.ReadWord(RTC_RTC1); + (rtc1 & 0x3F).Should().Be(0); // still 0 + } + + [Fact] + public void Tick_Rolls_Seconds_Into_Minutes() + { + var rtc = MakeEnabled(); + + rtc.Tick(125_000_000L * 60); // 60 seconds + + var rtc1 = rtc.ReadWord(RTC_RTC1); + (rtc1 & 0x3F) .Should().Be(0); // sec = 0 + ((rtc1 >> 8) & 0x3F) .Should().Be(1); // min = 1 + } + + [Fact] + public void Tick_Rolls_Day_And_Increments_DayOfWeek() + { + var rtc = MakeEnabled(); // Monday 2024-01-01 + + rtc.Tick(125_000_000L * 86400); // 24 hours + + var rtc0 = rtc.ReadWord(RTC_RTC0); + var rtc1 = rtc.ReadWord(RTC_RTC1); + + (rtc0 & 0x1F) .Should().Be(2); // day = 2 + ((rtc1 >> 24) & 0x7) .Should().Be(2); // Tuesday + } + + // ── SetDateTime helper ─────────────────────────────────────────────── + + [Fact] + public void SetDateTime_Updates_Running_Registers() + { + var rtc = new RtcPeripheral(); + rtc.SetDateTime(2030, 12, 31, 5, 23, 59, 59); // Fri 23:59:59 + + var rtc0 = rtc.ReadWord(RTC_RTC0); + var rtc1 = rtc.ReadWord(RTC_RTC1); + + ((rtc0 >> 16) & 0xFFF).Should().Be(2030); + ((rtc0 >> 8) & 0xF) .Should().Be(12); + (rtc0 & 0x1F) .Should().Be(31); + ((rtc1 >> 24) & 0x7) .Should().Be(5); // Fri + ((rtc1 >> 16) & 0x1F) .Should().Be(23); + ((rtc1 >> 8) & 0x3F) .Should().Be(59); + (rtc1 & 0x3F) .Should().Be(59); + } + + // ── Alarm ──────────────────────────────────────────────────────────── + + [Fact] + public void Alarm_SecMatch_Sets_MatchActive_And_NoIrqWithoutCpu() + { + var rtc = MakeEnabled(); // 2024-01-01 Monday 00:00:00 + + // Configure alarm: match second=1, enable MATCH_ENA + SEC_ENA + // IRQ_SETUP_0: MATCH_ENA bit31 + // IRQ_SETUP_1: SEC_ENA bit5, SEC value bits[5:0] + rtc.WriteWord(IRQ_SETUP_0, IRQ0_MATCH_ENA); + rtc.WriteWord(IRQ_SETUP_1, IRQ1_SEC_ENA | 1u); // match when sec=1 + + rtc.Tick(125_000_000L); // advance 1 second → sec=1 + + // MATCH_ACTIVE (bit 31 of IRQ_SETUP_1) should be set + var irq1 = rtc.ReadWord(IRQ_SETUP_1); + (irq1 & IRQ1_MATCH_ACTIVE).Should().NotBe(0u); + } + + [Fact] + public void Alarm_MatchActive_Is_W1C() + { + var rtc = MakeEnabled(); + rtc.WriteWord(IRQ_SETUP_0, IRQ0_MATCH_ENA); + rtc.WriteWord(IRQ_SETUP_1, IRQ1_SEC_ENA | 1u); + + rtc.Tick(125_000_000L); // trigger alarm + + // Clear MATCH_ACTIVE by writing 1 to bit 31 + rtc.WriteWord(IRQ_SETUP_1, IRQ1_MATCH_ACTIVE); + var irq1 = rtc.ReadWord(IRQ_SETUP_1); + (irq1 & IRQ1_MATCH_ACTIVE).Should().Be(0u); + } + + [Fact] + public void Alarm_NoMatch_When_EnableOff() + { + var rtc = MakeEnabled(); + // MATCH_ENA is 0 — alarm disabled + rtc.WriteWord(IRQ_SETUP_0, 0); + rtc.WriteWord(IRQ_SETUP_1, IRQ1_SEC_ENA | 1u); + + rtc.Tick(125_000_000L); + + var irq1 = rtc.ReadWord(IRQ_SETUP_1); + (irq1 & IRQ1_MATCH_ACTIVE).Should().Be(0u); + } + + [Fact] + public void Alarm_DoesNotFire_When_SecMismatch() + { + var rtc = MakeEnabled(); + rtc.WriteWord(IRQ_SETUP_0, IRQ0_MATCH_ENA); + rtc.WriteWord(IRQ_SETUP_1, IRQ1_SEC_ENA | 5u); // match sec=5 + + rtc.Tick(125_000_000L); // only sec=1 + + var irq1 = rtc.ReadWord(IRQ_SETUP_1); + (irq1 & IRQ1_MATCH_ACTIVE).Should().Be(0u); + } +} diff --git a/tests/RP2040Sharp.Tests/Sio/SioTests.cs b/tests/RP2040Sharp.Tests/Sio/SioTests.cs new file mode 100644 index 0000000..665838f --- /dev/null +++ b/tests/RP2040Sharp.Tests/Sio/SioTests.cs @@ -0,0 +1,364 @@ +using RP2040.Core.Cpu; +using RP2040.Core.Memory; +using RP2040.Peripherals.Sio; +using RP2040.Peripherals.Tests.Fixtures; + +namespace RP2040.Peripherals.Tests.Sio; + +/// +/// Tests for the SIO hardware divider (§2.3.1.6 of RP2040 TRM). +/// +public abstract class SioTests +{ + private sealed class Fixture : IDisposable + { + public BusInterconnect Bus { get; } + public CortexM0Plus Cpu { get; } + public SioPeripheral Sio { get; } + + public Fixture() + { + Bus = new BusInterconnect(); + Cpu = new CortexM0Plus(Bus); + Sio = new SioPeripheral(Cpu); + } + + public void Dispose() => Bus.Dispose(); + } + + // Unsigned divide register offsets (local from SIO base 0xD0000000) + 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; + + private const uint SPINLOCK_BASE = 0x100; + + public class UnsignedDivide + { + [Fact] + public void Divide_100_by_7_returns_quotient_14_remainder_2() + { + using var f = new Fixture(); + f.Sio.WriteWord(DIV_UDIVIDEND, 100); + f.Sio.WriteWord(DIV_UDIVISOR, 7); + + f.Sio.ReadWord(DIV_QUOTIENT).Should().Be(14u); + f.Sio.ReadWord(DIV_REMAINDER).Should().Be(2u); + } + + [Fact] + public void Divide_sets_CSR_READY_bit() + { + using var f = new Fixture(); + f.Sio.WriteWord(DIV_UDIVIDEND, 50); + f.Sio.WriteWord(DIV_UDIVISOR, 5); + + var csr = f.Sio.ReadWord(DIV_CSR); + (csr & 0x2).Should().Be(2u, "READY bit must be set after divide"); + } + + [Fact] + public void Divide_by_zero_returns_0xFFFFFFFF_quotient() + { + using var f = new Fixture(); + f.Sio.WriteWord(DIV_UDIVIDEND, 42); + f.Sio.WriteWord(DIV_UDIVISOR, 0); + + f.Sio.ReadWord(DIV_QUOTIENT).Should().Be(0xFFFFFFFFu); + f.Sio.ReadWord(DIV_REMAINDER).Should().Be(42u); + } + + [Fact] + public void Divide_large_value_rounds_down() + { + using var f = new Fixture(); + f.Sio.WriteWord(DIV_UDIVIDEND, 0xFFFFFFFF); + f.Sio.WriteWord(DIV_UDIVISOR, 0x10000); + + f.Sio.ReadWord(DIV_QUOTIENT).Should().Be(0xFFFFu); + } + } + + public class SignedDivide + { + [Fact] + public void Signed_divide_negative_dividend() + { + using var f = new Fixture(); + f.Sio.WriteWord(DIV_SDIVIDEND, unchecked((uint)-100)); + f.Sio.WriteWord(DIV_SDIVISOR, 7); + + // -100 / 7 = -14 remainder -2 (truncation towards zero) + f.Sio.ReadWord(DIV_QUOTIENT).Should().Be(unchecked((uint)-14)); + f.Sio.ReadWord(DIV_REMAINDER).Should().Be(unchecked((uint)-2)); + } + + [Fact] + public void Signed_divide_both_negative() + { + using var f = new Fixture(); + f.Sio.WriteWord(DIV_SDIVIDEND, unchecked((uint)-48)); + f.Sio.WriteWord(DIV_SDIVISOR, unchecked((uint)-6)); + + f.Sio.ReadWord(DIV_QUOTIENT).Should().Be(8u); + f.Sio.ReadWord(DIV_REMAINDER).Should().Be(0u); + } + + [Fact] + public void Signed_divide_by_zero_positive_dividend() + { + using var f = new Fixture(); + f.Sio.WriteWord(DIV_SDIVIDEND, 10); + f.Sio.WriteWord(DIV_SDIVISOR, 0); + + // positive dividend / 0 → quotient = 1 per RP2040 spec + f.Sio.ReadWord(DIV_QUOTIENT).Should().Be(1u); + } + + [Fact] + public void Signed_divide_by_zero_negative_dividend() + { + using var f = new Fixture(); + f.Sio.WriteWord(DIV_SDIVIDEND, unchecked((uint)-10)); + f.Sio.WriteWord(DIV_SDIVISOR, 0); + + // negative dividend / 0 → quotient = -1 = 0xFFFFFFFF per RP2040 spec + f.Sio.ReadWord(DIV_QUOTIENT).Should().Be(0xFFFFFFFFu); + } + } + + public class DividerSave + { + [Fact] + public void Writing_quotient_sets_DIRTY_bit() + { + using var f = new Fixture(); + // Normal divide first + f.Sio.WriteWord(DIV_UDIVIDEND, 10); + f.Sio.WriteWord(DIV_UDIVISOR, 2); + // Verify READY is set + (f.Sio.ReadWord(DIV_CSR) & 0x2).Should().Be(2u); + + // Save quotient (simulating context save) + f.Sio.WriteWord(DIV_QUOTIENT, 99); + // DIRTY bit should now be set (bit 0) + (f.Sio.ReadWord(DIV_CSR) & 0x1).Should().Be(1u, "writing quotient sets DIRTY"); + } + } + + public class Spinlocks + { + [Fact] + public void Claim_unclaimed_spinlock_returns_nonzero() + { + using var f = new Fixture(); + var result = f.Sio.ReadWord(SPINLOCK_BASE); // spinlock 0 + result.Should().NotBe(0u, "claiming a free spinlock returns its bit"); + } + + [Fact] + public void Claim_already_taken_spinlock_returns_zero() + { + using var f = new Fixture(); + f.Sio.ReadWord(SPINLOCK_BASE); // claim spinlock 0 + var second = f.Sio.ReadWord(SPINLOCK_BASE); + second.Should().Be(0u, "spinlock already held returns 0"); + } + + [Fact] + public void Release_spinlock_allows_reclaim() + { + using var f = new Fixture(); + f.Sio.ReadWord(SPINLOCK_BASE); // claim + f.Sio.WriteWord(SPINLOCK_BASE, 0); // release (any write) + var reclaim = f.Sio.ReadWord(SPINLOCK_BASE); + reclaim.Should().NotBe(0u, "reclaiming after release must succeed"); + } + + [Fact] + public void SPINLOCK_ST_reflects_claimed_locks() + { + using var f = new Fixture(); + const uint SPINLOCK_ST = 0x05C; + f.Sio.ReadWord(SPINLOCK_BASE); // claim spinlock 0 + f.Sio.ReadWord(SPINLOCK_BASE + 4); // claim spinlock 1 + var st = f.Sio.ReadWord(SPINLOCK_ST); + (st & 0x3).Should().Be(0x3u, "SPINLOCK_ST bits 0-1 reflect claimed locks"); + } + } + + /// + /// Tests for SIO Interpolators (INTERP0 at 0x080, INTERP1 at 0x0C0). + /// Each interpolator has ACCUM0/1, BASE0/1/2, CTRL_LANE0/1, POP_LANE0/1/FULL, + /// PEEK_LANE0/1/FULL, ACCUM0_ADD / ACCUM1_ADD, BASE_1AND0. + /// + public class Interpolators + { + // INTERP0 register base (relative to SIO) + private const uint INTERP0 = 0x080; + 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; + private const uint INTERP_POP_LANE1 = 0x18; + private const uint INTERP_POP_FULL = 0x1C; + private const uint INTERP_PEEK_LANE0 = 0x20; + private const uint INTERP_PEEK_LANE1 = 0x24; + private const uint INTERP_PEEK_FULL = 0x28; + private const uint INTERP_CTRL0 = 0x2C; + private const uint INTERP_CTRL1 = 0x30; + private const uint INTERP_ACCUM0_ADD = 0x34; + private const uint INTERP_ACCUM1_ADD = 0x38; + private const uint INTERP_BASE_1AND0 = 0x3C; + + private static uint R0(uint reg) => INTERP0 + reg; // INTERP0 register + private static uint R1(uint reg) => 0x0C0 + reg; // INTERP1 register + + // CTRL_LANE bits + private const uint CTRL_SHIFT_MASK = 0x1F; // bits [4:0] + private const uint CTRL_MASK_LSB_SHIFT = 5; + private const uint CTRL_MASK_MSB_SHIFT = 10; + private const uint CTRL_SIGNED = 1u << 15; + private const uint CTRL_CROSS_INPUT = 1u << 16; + + [Fact] + public void Accum0_and_Accum1_are_read_write() + { + using var f = new Fixture(); + f.Sio.WriteWord(R0(INTERP_ACCUM0), 0xDEAD_BEEFu); + f.Sio.WriteWord(R0(INTERP_ACCUM1), 0xCAFE_0000u); + + f.Sio.ReadWord(R0(INTERP_ACCUM0)).Should().Be(0xDEAD_BEEFu); + f.Sio.ReadWord(R0(INTERP_ACCUM1)).Should().Be(0xCAFE_0000u); + } + + [Fact] + public void Base0_Base1_Base2_are_read_write() + { + using var f = new Fixture(); + f.Sio.WriteWord(R0(INTERP_BASE0), 0x11111111u); + f.Sio.WriteWord(R0(INTERP_BASE1), 0x22222222u); + f.Sio.WriteWord(R0(INTERP_BASE2), 0x33333333u); + + f.Sio.ReadWord(R0(INTERP_BASE0)).Should().Be(0x11111111u); + f.Sio.ReadWord(R0(INTERP_BASE1)).Should().Be(0x22222222u); + f.Sio.ReadWord(R0(INTERP_BASE2)).Should().Be(0x33333333u); + } + + [Fact] + public void BASE_1AND0_write_splits_into_base0_and_base1() + { + using var f = new Fixture(); + // BASE_1AND0: low 16 bits → BASE0, high 16 bits → BASE1 + f.Sio.WriteWord(R0(INTERP_BASE_1AND0), 0xBBBB_AAAAu); + + f.Sio.ReadWord(R0(INTERP_BASE0)).Should().Be(0x0000_AAAAu, "BASE0 = lower 16 bits"); + f.Sio.ReadWord(R0(INTERP_BASE1)).Should().Be(0x0000_BBBBu, "BASE1 = upper 16 bits"); + } + + [Fact] + public void Lane0_peek_returns_shifted_masked_accum_plus_base() + { + using var f = new Fixture(); + // CTRL_LANE0: SHIFT=4, MASK_LSB=0, MASK_MSB=7 → field = bits[7:0] of (ACCUM0 >> 4) + uint ctrl = (4 << 0) | // SHIFT = 4 (bits [4:0]) + (0 << 5) | // MASK_LSB = 0 (bits [9:5]) + (7 << 10); // MASK_MSB = 7 (bits [14:10]) + f.Sio.WriteWord(R0(INTERP_CTRL0), ctrl); + f.Sio.WriteWord(R0(INTERP_ACCUM0), 0x0000_00F0u); // ACCUM0 = 0xF0 + f.Sio.WriteWord(R0(INTERP_BASE0), 0x0000_0001u); // BASE0 = 1 + + // RESULT0 = ((ACCUM0 >> SHIFT) & MASK) + BASE0 + // = ((0xF0 >> 4) & 0xFF) + 1 = 0x0F + 1 = 0x10 + f.Sio.ReadWord(R0(INTERP_PEEK_LANE0)).Should().Be(0x10u); + } + + [Fact] + public void POP_LANE0_returns_same_as_PEEK_then_advances_ACCUM() + { + using var f = new Fixture(); + // CTRL_LANE0: SHIFT=0, MASK_LSB=0, MASK_MSB=31 (passthrough), no BASE + uint ctrl = (0u << 0) | (0u << 5) | (31u << 10); // full 32-bit passthrough + f.Sio.WriteWord(R0(INTERP_CTRL0), ctrl); + f.Sio.WriteWord(R0(INTERP_ACCUM0), 0x100u); + f.Sio.WriteWord(R0(INTERP_ACCUM1), 0x000u); + f.Sio.WriteWord(R0(INTERP_BASE0), 0x10u); // step = 16 + f.Sio.WriteWord(R0(INTERP_BASE1), 0x00u); + + var peekBefore = f.Sio.ReadWord(R0(INTERP_PEEK_LANE0)); + var popVal = f.Sio.ReadWord(R0(INTERP_POP_LANE0)); // advances ACCUM0 by BASE0 + var peekAfter = f.Sio.ReadWord(R0(INTERP_PEEK_LANE0)); + + popVal.Should().Be(peekBefore, "POP returns the same value as PEEK before the pop"); + peekAfter.Should().Be(peekBefore + 0x10u, "ACCUM0 advanced by BASE0 after POP_LANE0"); + } + + [Fact] + public void Signed_mode_sign_extends_shifted_result() + { + using var f = new Fixture(); + // CTRL_LANE0: SHIFT=0, MASK_MSB=7 (byte), SIGNED=1 + uint ctrl = (0u << 0) | (0u << 5) | (7u << 10) | CTRL_SIGNED; + f.Sio.WriteWord(R0(INTERP_CTRL0), ctrl); + f.Sio.WriteWord(R0(INTERP_ACCUM0), 0x000000FFu); // 0xFF = -1 as signed byte + f.Sio.WriteWord(R0(INTERP_BASE0), 0x0u); + + // Signed extension: bits[7:0] = 0xFF → sign-extend to 32 bits = 0xFFFFFFFF + f.Sio.ReadWord(R0(INTERP_PEEK_LANE0)).Should().Be(0xFFFFFFFFu, + "signed mode sign-extends the extracted field"); + } + + [Fact] + public void ACCUM0_ADD_atomically_adds_to_ACCUM0() + { + using var f = new Fixture(); + f.Sio.WriteWord(R0(INTERP_ACCUM0), 0x100u); + f.Sio.WriteWord(R0(INTERP_ACCUM0_ADD), 0x050u); // add 0x50 to ACCUM0 + + f.Sio.ReadWord(R0(INTERP_ACCUM0)).Should().Be(0x150u, "ACCUM0_ADD adds to ACCUM0"); + } + + [Fact] + public void ACCUM1_ADD_atomically_adds_to_ACCUM1() + { + using var f = new Fixture(); + f.Sio.WriteWord(R1(INTERP_ACCUM1), 0x200u); + f.Sio.WriteWord(R1(INTERP_ACCUM1_ADD), 0x100u); + + f.Sio.ReadWord(R1(INTERP_ACCUM1)).Should().Be(0x300u); + } + + [Fact] + public void Interp1_independent_from_interp0() + { + using var f = new Fixture(); + f.Sio.WriteWord(R0(INTERP_ACCUM0), 0xAAAAAAAAu); + f.Sio.WriteWord(R1(INTERP_ACCUM0), 0x55555555u); + + f.Sio.ReadWord(R0(INTERP_ACCUM0)).Should().Be(0xAAAAAAAAu); + f.Sio.ReadWord(R1(INTERP_ACCUM0)).Should().Be(0x55555555u); + } + + [Fact] + public void CROSS_INPUT_lane0_uses_accum1_as_input() + { + using var f = new Fixture(); + // CTRL_LANE0: SHIFT=0, MASK full 32-bit, CROSS_INPUT=1 + uint ctrl = (0u << 0) | (0u << 5) | (31u << 10) | CTRL_CROSS_INPUT; + f.Sio.WriteWord(R0(INTERP_CTRL0), ctrl); + f.Sio.WriteWord(R0(INTERP_ACCUM0), 0xAAAAAAAAu); // ACCUM0 = source normally + f.Sio.WriteWord(R0(INTERP_ACCUM1), 0x12345678u); // ACCUM1 = cross source + f.Sio.WriteWord(R0(INTERP_BASE0), 0x0u); + + // CROSS_INPUT=1: lane0 uses ACCUM1 instead of ACCUM0 + f.Sio.ReadWord(R0(INTERP_PEEK_LANE0)).Should().Be(0x12345678u, + "CROSS_INPUT makes lane0 use ACCUM1 as input"); + } + } +} diff --git a/tests/RP2040Sharp.Tests/Timer/TimerTests.cs b/tests/RP2040Sharp.Tests/Timer/TimerTests.cs new file mode 100644 index 0000000..63ffd95 --- /dev/null +++ b/tests/RP2040Sharp.Tests/Timer/TimerTests.cs @@ -0,0 +1,161 @@ +using RP2040.Core.Cpu; +using RP2040.Core.Memory; +using RP2040.Peripherals.Timer; + +namespace RP2040.Peripherals.Tests.Timer; + +/// +/// Tests for the RP2040 Timer peripheral (§4.6 of RP2040 TRM). +/// The Timer counts microseconds at 125 MHz. +/// +public abstract class TimerTests +{ + private const uint CLK_HZ = 125_000_000; + + private sealed class Fixture : IDisposable + { + public BusInterconnect Bus { get; } + public CortexM0Plus Cpu { get; } + public TimerPeripheral Timer { get; } + + public Fixture() + { + Bus = new BusInterconnect(); + Cpu = new CortexM0Plus(Bus); + Timer = new TimerPeripheral(Cpu, CLK_HZ); + } + + public void Dispose() => Bus.Dispose(); + + /// Advance the timer by the given number of microseconds. + public void AdvanceMicros(long us) => Timer.Tick(us * CLK_HZ / 1_000_000); + } + + 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 TIMERAWL = 0x028; + private const uint INTR = 0x034; + private const uint INTE = 0x038; + private const uint INTF = 0x03C; + private const uint INTS = 0x040; + + public class AlarmBasic + { + [Fact] + public void Writing_alarm0_arms_it() + { + using var f = new Fixture(); + f.Timer.WriteWord(ALARM0, 1000u); + (f.Timer.ReadWord(ARMED) & 1u).Should().Be(1u, "alarm0 should be armed"); + } + + [Fact] + public void Alarm_fires_after_deadline() + { + using var f = new Fixture(); + var target = (uint)f.Timer.ReadWord(TIMERAWL) + 500u; + f.Timer.WriteWord(ALARM0, target); + + f.AdvanceMicros(600); + + (f.Timer.ReadWord(INTR) & 1u).Should().Be(1u, "alarm0 raw interrupt must be set"); + } + + [Fact] + public void Alarm_disarms_when_fired() + { + using var f = new Fixture(); + var target = (uint)f.Timer.ReadWord(TIMERAWL) + 100u; + f.Timer.WriteWord(ALARM0, target); + f.AdvanceMicros(200); + + f.Timer.ReadWord(ARMED).Should().Be(0u, "alarm0 should auto-disarm after firing"); + } + + [Fact] + public void Alarm_INTR_cleared_by_writing_1() + { + using var f = new Fixture(); + var target = (uint)f.Timer.ReadWord(TIMERAWL) + 50u; + f.Timer.WriteWord(ALARM0, target); + f.AdvanceMicros(100); + + // Verify it fired + (f.Timer.ReadWord(INTR) & 1u).Should().Be(1u); + + // Clear it + f.Timer.WriteWord(INTR, 1u); + f.Timer.ReadWord(INTR).Should().Be(0u, "INTR should be cleared after writing 1"); + } + } + + public class InterruptRouting + { + [Fact] + public void INTS_reflects_INTR_when_INTE_enabled() + { + using var f = new Fixture(); + f.Timer.WriteWord(INTE, 0xFu); // enable all 4 alarm interrupts + var target = (uint)f.Timer.ReadWord(TIMERAWL) + 50u; + f.Timer.WriteWord(ALARM1, target); + f.AdvanceMicros(100); + + var ints = f.Timer.ReadWord(INTS); + (ints & 0x2u).Should().Be(0x2u, "INTS bit1 (alarm1) should be set"); + } + + [Fact] + public void INTF_force_shows_in_INTS_when_INTE_set() + { + using var f = new Fixture(); + f.Timer.WriteWord(INTE, 0x4u); // enable alarm2 + f.Timer.WriteWord(INTF, 0x4u); // force alarm2 + + var ints = f.Timer.ReadWord(INTS); + (ints & 0x4u).Should().Be(0x4u, "forced interrupt should appear in INTS when INTE enabled"); + } + + [Fact] + public void INTF_does_not_show_in_INTS_when_INTE_disabled() + { + using var f = new Fixture(); + // INTE stays 0 (default) + f.Timer.WriteWord(INTF, 0x4u); // force alarm2 + + f.Timer.ReadWord(INTS).Should().Be(0u, "forced interrupt hidden when INTE=0"); + } + } + + public class MultipleAlarms + { + [Fact] + public void All_four_alarms_can_fire_independently() + { + using var f = new Fixture(); + var now = f.Timer.ReadWord(TIMERAWL); + f.Timer.WriteWord(ALARM0, now + 100u); + f.Timer.WriteWord(ALARM1, now + 200u); + f.Timer.WriteWord(ALARM2, now + 300u); + f.Timer.WriteWord(ALARM3, now + 400u); + + f.AdvanceMicros(500); + + (f.Timer.ReadWord(INTR) & 0xFu).Should().Be(0xFu, "all four alarms should have fired"); + } + + [Fact] + public void Alarm_that_hasnt_elapsed_does_not_fire() + { + using var f = new Fixture(); + var now = f.Timer.ReadWord(TIMERAWL); + f.Timer.WriteWord(ALARM0, now + 1000u); + + f.AdvanceMicros(100); + + f.Timer.ReadWord(INTR).Should().Be(0u, "alarm should not fire before deadline"); + } + } +} diff --git a/tests/RP2040Sharp.Tests/Uart/UartTests.cs b/tests/RP2040Sharp.Tests/Uart/UartTests.cs new file mode 100644 index 0000000..022b39c --- /dev/null +++ b/tests/RP2040Sharp.Tests/Uart/UartTests.cs @@ -0,0 +1,233 @@ +using RP2040.Peripherals.Uart; + +namespace RP2040.Peripherals.Tests.Uart; + +/// +/// Tests for the PL011 UART peripheral. +/// +public abstract class UartTests +{ + private const uint UARTDR = 0x000; + private const uint UARTFR = 0x018; + private const uint UARTIBRD = 0x024; + private const uint UARTFBRD = 0x028; + private const uint UARTLCR_H = 0x02C; + private const uint UARTCR = 0x030; + private const uint UARTIFLS = 0x034; + private const uint UARTIMSC = 0x038; + private const uint UARTRIS = 0x03C; + private const uint UARTMIS = 0x040; + private const uint UARTICR = 0x044; + private const uint UARTDMACR = 0x048; + + // PL011 ID registers + 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; + private const uint FR_RXFE = 1u << 4; + + public class BaudRate + { + [Fact] + public void Write_IBRD_reads_back_correctly() + { + var uart = new UartPeripheral(); + uart.WriteWord(UARTIBRD, 67); + uart.ReadWord(UARTIBRD).Should().Be(67u); + } + + [Fact] + public void Write_FBRD_reads_back_correctly() + { + var uart = new UartPeripheral(); + uart.WriteWord(UARTFBRD, 52); + uart.ReadWord(UARTFBRD).Should().Be(52u); + } + + [Fact] + public void FBRD_masked_to_6_bits() + { + var uart = new UartPeripheral(); + uart.WriteWord(UARTFBRD, 0xFFFF); + uart.ReadWord(UARTFBRD).Should().Be(0x3Fu, "FBRD is only 6 bits wide"); + } + + [Fact] + public void IBRD_masked_to_16_bits() + { + var uart = new UartPeripheral(); + uart.WriteWord(UARTIBRD, 0x1FFFF); + uart.ReadWord(UARTIBRD).Should().Be(0xFFFFu); + } + } + + public class LineControl + { + [Fact] + public void UARTLCR_H_stores_word_length_bits() + { + var uart = new UartPeripheral(); + // WLEN = 0b11 (8-bit data) → bits [6:5] = 0b11 → value = 0b01100000 = 0x60 + uart.WriteWord(UARTLCR_H, 0x60); + uart.ReadWord(UARTLCR_H).Should().Be(0x60u); + } + + [Fact] + public void UARTLCR_H_stores_FEN_bit() + { + var uart = new UartPeripheral(); + // FEN = bit 4 → enable FIFOs + uart.WriteWord(UARTLCR_H, 0x70); // FEN + WLEN=8bit + uart.ReadWord(UARTLCR_H).Should().Be(0x70u); + } + + [Fact] + public void UARTLCR_H_masked_to_8_bits() + { + var uart = new UartPeripheral(); + uart.WriteWord(UARTLCR_H, 0x1FF); + uart.ReadWord(UARTLCR_H).Should().Be(0xFFu); + } + } + + public class Transmit + { + [Fact] + public void TX_invokes_OnByteTransmit_callback() + { + var uart = new UartPeripheral(); + byte? received = null; + uart.OnByteTransmit = b => received = b; + + uart.WriteWord(UARTDR, 0x42); + + received.Should().Be(0x42); + } + + [Fact] + public void FR_TXFE_is_set_when_TX_is_idle() + { + var uart = new UartPeripheral(); + var fr = uart.ReadWord(UARTFR); + (fr & FR_TXFE).Should().Be(FR_TXFE, "TX FIFO empty flag should be set"); + } + + [Fact] + public void TX_sets_TXRIS_raw_interrupt() + { + var uart = new UartPeripheral(); + uart.WriteWord(UARTDR, 0x55); + var ris = uart.ReadWord(UARTRIS); + (ris & (1u << 5)).Should().Be(1u << 5, "TXRIS should be set after TX"); + } + } + + public class Receive + { + [Fact] + public void FR_RXFE_is_set_when_FIFO_empty() + { + var uart = new UartPeripheral(); + var fr = uart.ReadWord(UARTFR); + (fr & FR_RXFE).Should().Be(FR_RXFE); + } + + [Fact] + public void InjectByte_populates_FIFO() + { + var uart = new UartPeripheral(); + uart.InjectByte(0xAB); + + var fr = uart.ReadWord(UARTFR); + (fr & FR_RXFE).Should().Be(0u, "RXFE should be clear when FIFO has data"); + } + + [Fact] + public void Reading_UARTDR_drains_RX_FIFO() + { + var uart = new UartPeripheral(); + uart.InjectByte(0xCD); + uart.InjectByte(0xEF); + + uart.ReadWord(UARTDR).Should().Be(0xCDu); + uart.ReadWord(UARTDR).Should().Be(0xEFu); + (uart.ReadWord(UARTFR) & FR_RXFE).Should().Be(FR_RXFE, "FIFO empty after draining"); + } + + [Fact] + public void RX_sets_RXRIS_raw_interrupt() + { + var uart = new UartPeripheral(); + uart.InjectByte(0x01); + (uart.ReadWord(UARTRIS) & (1u << 4)).Should().Be(1u << 4, "RXRIS should be set"); + } + } + + public class Interrupts + { + [Fact] + public void MIS_is_RIS_AND_IMSC() + { + var uart = new UartPeripheral(); + uart.InjectByte(0x01); // sets RXRIS + uart.WriteWord(UARTIMSC, 0x10); // unmask RXIM only + + var mis = uart.ReadWord(UARTMIS); + (mis & 0x10).Should().Be(0x10u, "RXMIS should be set when RXRIS and RXIM both set"); + } + + [Fact] + public void UARTICR_clears_selected_interrupts() + { + var uart = new UartPeripheral(); + uart.InjectByte(0x01); + // Verify both RXRIS and assert TX is clear initially + (uart.ReadWord(UARTRIS) & (1u << 4)).Should().Be(1u << 4, "RXRIS should be set"); + + uart.WriteWord(UARTICR, 1u << 4); // clear RXRIS + (uart.ReadWord(UARTRIS) & (1u << 4)).Should().Be(0u, "RXRIS should be cleared"); + } + } + + public class PeripheralId + { + [Fact] + public void PL011_peripheral_id_registers_return_correct_values() + { + var uart = new UartPeripheral(); + uart.ReadWord(UARTPERIPHID0).Should().Be(0x11u); + uart.ReadWord(UARTPERIPHID1).Should().Be(0x10u); + uart.ReadWord(UARTPERIPHID2).Should().Be(0x34u); + uart.ReadWord(UARTPERIPHID3).Should().Be(0x00u); + } + + [Fact] + public void PL011_cell_id_registers_return_correct_values() + { + var uart = new UartPeripheral(); + uart.ReadWord(UARTPCELLID0).Should().Be(0x0Du); + uart.ReadWord(UARTPCELLID1).Should().Be(0xF0u); + uart.ReadWord(UARTPCELLID2).Should().Be(0x05u); + uart.ReadWord(UARTPCELLID3).Should().Be(0xB1u); + } + } + + public class DmaControl + { + [Fact] + public void UARTDMACR_stores_and_reads_back() + { + var uart = new UartPeripheral(); + uart.WriteWord(UARTDMACR, 0x3); + uart.ReadWord(UARTDMACR).Should().Be(0x3u); + } + } +} diff --git a/tests/RP2040Sharp.Tests/Usb/UsbDescriptorParsingTests.cs b/tests/RP2040Sharp.Tests/Usb/UsbDescriptorParsingTests.cs new file mode 100644 index 0000000..cd8988e --- /dev/null +++ b/tests/RP2040Sharp.Tests/Usb/UsbDescriptorParsingTests.cs @@ -0,0 +1,81 @@ +using FluentAssertions; +using RP2040.Peripherals.Usb; + +namespace RP2040.Peripherals.Tests.Usb; + +/// +/// Unit tests for : locating the CDC data +/// interface's bulk IN/OUT endpoints, including within composite descriptors. +/// +public sealed class UsbDescriptorParsingTests +{ + // Minimal CDC-only configuration descriptor (CDC ACM control + data interfaces), + // mirroring what TinyUSB emits for a CDC-only device. + private static byte[] BuildCdcOnlyDescriptor() + { + return new byte[] + { + // Config descriptor (9 bytes, type 0x02) + 9, 0x02, 67, 0, 2, 1, 0, 0xC0, 50, + // Interface 0: CDC Control (class 0x02, 1 endpoint) + 9, 0x04, 0, 0, 1, 0x02, 0x02, 0x01, 0, + // Class-specific CDC headers (skipped) + 5, 0x24, 0x00, 0x10, 0x01, + 4, 0x24, 0x02, 0x02, + 5, 0x24, 0x06, 0x00, 0x01, + // Endpoint 3 IN (interrupt, type=3) + 7, 0x05, 0x83, 0x03, 8, 0, 10, + // Interface 1: CDC Data (class 0x0A, 2 bulk endpoints) + 9, 0x04, 1, 0, 2, 0x0A, 0x00, 0x00, 0, + // Endpoint 1 OUT (bulk, type=2) + 7, 0x05, 0x01, 0x02, 64, 0, 0, + // Endpoint 1 IN (bulk, type=2) + 7, 0x05, 0x81, 0x02, 64, 0, 0, + }; + } + + // CDC Data (class 0x0A, ep 1) + an extra MSC interface (class 0x08, ep 2): the parser + // must still pick out the CDC endpoints and ignore the other interface. + private static byte[] BuildCompositeCdcMscDescriptor() + { + return new byte[] + { + 9, 0x02, 0, 0, 3, 1, 0, 0xC0, 50, + 9, 0x04, 0, 0, 1, 0x02, 0x02, 0x01, 0, // CDC Control + 5, 0x24, 0x00, 0x10, 0x01, + 4, 0x24, 0x02, 0x02, + 5, 0x24, 0x06, 0x00, 0x01, + 7, 0x05, 0x83, 0x03, 8, 0, 10, // ep 3 IN interrupt + 9, 0x04, 1, 0, 2, 0x0A, 0x00, 0x00, 0, // CDC Data + 7, 0x05, 0x01, 0x02, 64, 0, 0, // ep 1 OUT bulk + 7, 0x05, 0x81, 0x02, 64, 0, 0, // ep 1 IN bulk + 9, 0x04, 2, 0, 2, 0x08, 0x06, 0x50, 0, // MSC interface (ignored) + 7, 0x05, 0x02, 0x02, 64, 0, 0, // ep 2 OUT bulk + 7, 0x05, 0x82, 0x02, 64, 0, 0, // ep 2 IN bulk + }; + } + + [Fact] + public void ExtractEndpointNumbers_CdcOnly_FindsCdcEndpoints() + { + UsbCdcHost.ExtractEndpointNumbers(BuildCdcOnlyDescriptor(), out var inEp, out var outEp); + inEp.Should().Be(1); + outEp.Should().Be(1); + } + + [Fact] + public void ExtractEndpointNumbers_CompositeDescriptor_FindsCdcEndpointsOnly() + { + UsbCdcHost.ExtractEndpointNumbers(BuildCompositeCdcMscDescriptor(), out var inEp, out var outEp); + inEp.Should().Be(1); + outEp.Should().Be(1); + } + + [Fact] + public void ExtractEndpointNumbers_EmptyDescriptor_ReturnsMinusOne() + { + UsbCdcHost.ExtractEndpointNumbers(Array.Empty(), out var inEp, out var outEp); + inEp.Should().Be(-1); + outEp.Should().Be(-1); + } +} diff --git a/tests/RP2040Sharp.Tests/Usb/UsbTests.cs b/tests/RP2040Sharp.Tests/Usb/UsbTests.cs new file mode 100644 index 0000000..6850fb0 --- /dev/null +++ b/tests/RP2040Sharp.Tests/Usb/UsbTests.cs @@ -0,0 +1,209 @@ +using RP2040.Core.Cpu; +using RP2040.Core.Memory; +using RP2040.Peripherals.Usb; + +namespace RP2040.Peripherals.Tests.Usb; + +/// +/// Tests for the USBCTRL peripheral (DPRAM + REGS). +/// +public abstract class UsbTests +{ + // AHB slot addresses + private const uint DPRAM_BASE = 0x50100000u; + private const uint REGS_BASE = 0x50110000u; + + // Register offsets within REGS_BASE + private const uint ADDR_ENDP0 = REGS_BASE + 0x000; + private const uint ADDR_ENDP1 = REGS_BASE + 0x004; + private const uint MAIN_CTRL = REGS_BASE + 0x040; + private const uint SOF_RW = REGS_BASE + 0x044; + private const uint SIE_CTRL = REGS_BASE + 0x04C; + private const uint SIE_STATUS = REGS_BASE + 0x050; + private const uint INT_EP_CTRL = REGS_BASE + 0x054; + private const uint BUFF_STATUS = REGS_BASE + 0x058; + private const uint EP_STALL_ARM = REGS_BASE + 0x068; + private const uint USB_MUXING = REGS_BASE + 0x074; + private const uint USB_PWR = REGS_BASE + 0x078; + private const uint INTR = REGS_BASE + 0x08C; + private const uint INTE = REGS_BASE + 0x090; + private const uint INTF = REGS_BASE + 0x094; + private const uint INTS = REGS_BASE + 0x098; + + private sealed class Fixture : IDisposable + { + public BusInterconnect Bus { get; } + public CortexM0Plus Cpu { get; } + public UsbPeripheral Usb { get; } + + public Fixture() + { + Bus = new BusInterconnect(); + Cpu = new CortexM0Plus(Bus); + Usb = new UsbPeripheral(Cpu); + } + + public void Dispose() => Bus.Dispose(); + } + + public class Dpram + { + [Fact] + public void Word_write_and_read_roundtrip() + { + using var f = new Fixture(); + f.Usb.WriteWord(DPRAM_BASE + 0x00, 0xDEADBEEFu); + f.Usb.ReadWord(DPRAM_BASE + 0x00).Should().Be(0xDEADBEEFu); + } + + [Fact] + public void Byte_write_and_read_roundtrip() + { + using var f = new Fixture(); + f.Usb.WriteByte(DPRAM_BASE + 0x10, 0xAB); + f.Usb.ReadByte(DPRAM_BASE + 0x10).Should().Be(0xAB); + } + + [Fact] + public void WriteDpram_helper_fills_buffer() + { + using var f = new Fixture(); + var data = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + f.Usb.WriteDpram(0x08u, data); + var result = f.Usb.ReadDpram(0x08u, 4); + result.Should().Equal(data); + } + + [Fact] + public void HalfWord_write_and_read_roundtrip() + { + using var f = new Fixture(); + f.Usb.WriteHalfWord(DPRAM_BASE + 0x20, 0x1234); + f.Usb.ReadHalfWord(DPRAM_BASE + 0x20).Should().Be(0x1234); + } + } + + public class Registers + { + [Fact] + public void MAIN_CTRL_stores_and_reads_back() + { + using var f = new Fixture(); + // MAIN_CTRL bit 0 = CONTROLLER_EN, bit 1 = HOST_NDEVICE, bit 31 = SIM_TIMING + f.Usb.WriteWord(MAIN_CTRL, 0x80000001u); + f.Usb.ReadWord(MAIN_CTRL).Should().Be(0x80000001u & 0xC0000003u, + "only defined bits are stored"); + } + + [Fact] + public void SIE_CTRL_stores_and_reads_back() + { + using var f = new Fixture(); + f.Usb.WriteWord(SIE_CTRL, 0x00000001u); // EP0_INT_1BUF + f.Usb.ReadWord(SIE_CTRL).Should().Be(0x00000001u); + } + + [Fact] + public void SOF_RW_stores_and_reads_back() + { + using var f = new Fixture(); + f.Usb.WriteWord(SOF_RW, 11u); + f.Usb.ReadWord(SOF_RW).Should().Be(11u & 0x7FFu); + } + + [Fact] + public void ADDR_ENDP1_stores_endpoint_address() + { + using var f = new Fixture(); + // ADDR_ENDP: bits [6:0] = address, bits [19:16] = endpoint number + f.Usb.WriteWord(ADDR_ENDP1, 0x00010002u); // EP=1, ADDR=2 + f.Usb.ReadWord(ADDR_ENDP1).Should().Be(0x00010002u); + } + + [Fact] + public void USB_MUXING_stores_and_reads_back() + { + using var f = new Fixture(); + f.Usb.WriteWord(USB_MUXING, 0x00000009u); + f.Usb.ReadWord(USB_MUXING).Should().Be(0x00000009u); + } + + [Fact] + public void USB_PWR_stores_and_reads_back() + { + using var f = new Fixture(); + f.Usb.WriteWord(USB_PWR, 0x00000004u); + f.Usb.ReadWord(USB_PWR).Should().Be(0x00000004u); + } + + [Fact] + public void SIE_STATUS_write1_clears_bits() + { + using var f = new Fixture(); + f.Usb.SignalBusReset(); // sets SIE_STATUS bit 19 (BUS_RESET) + + (f.Usb.ReadWord(SIE_STATUS) & (1u << 19)).Should().Be(1u << 19, "BUS_RESET bit set"); + + f.Usb.WriteWord(SIE_STATUS, 1u << 19); // W1C + (f.Usb.ReadWord(SIE_STATUS) & (1u << 19)).Should().Be(0u, "BUS_RESET cleared"); + } + + [Fact] + public void BUFF_STATUS_write1_clears_bits() + { + using var f = new Fixture(); + // Manually force a BUFF_STATUS bit (not via hardware, via WriteWord with no mask) + // Use INTF to simulate state instead — just test W1C semantics via SIE_STATUS + f.Usb.WriteWord(BUFF_STATUS, 0u); // no-op, just verify no throw + f.Usb.ReadWord(BUFF_STATUS).Should().Be(0u); + } + } + + public class Interrupts + { + [Fact] + public void INTS_is_zero_when_INTE_is_zero() + { + using var f = new Fixture(); + f.Usb.SignalBusReset(); + f.Usb.WriteWord(INTE, 0u); + f.Usb.ReadWord(INTS).Should().Be(0u, "masked interrupt not visible in INTS"); + } + + [Fact] + public void INTS_shows_INTR_when_INTE_unmasked() + { + using var f = new Fixture(); + f.Usb.SignalBusReset(); // sets INTR bit 12 + f.Usb.WriteWord(INTE, 1u << 12); + (f.Usb.ReadWord(INTS) & (1u << 12)).Should().Be(1u << 12); + } + + [Fact] + public void INTF_force_shows_in_INTS_when_INTE_set() + { + using var f = new Fixture(); + f.Usb.WriteWord(INTE, 1u << 4); + f.Usb.WriteWord(INTF, 1u << 4); + (f.Usb.ReadWord(INTS) & (1u << 4)).Should().Be(1u << 4); + } + + [Fact] + public void INTR_W1C_clears_interrupt() + { + using var f = new Fixture(); + f.Usb.SignalBusReset(); + f.Usb.WriteWord(INTR, 1u << 12); + (f.Usb.ReadWord(INTR) & (1u << 12)).Should().Be(0u, "INTR bit cleared by W1C"); + } + + [Fact] + public void SignalSetupPacket_sets_SETUP_REC_bit() + { + using var f = new Fixture(); + f.Usb.SignalSetupPacket(); + (f.Usb.ReadWord(SIE_STATUS) & (1u << 17)).Should().Be(1u << 17, "SETUP_REC in SIE_STATUS"); + (f.Usb.ReadWord(INTR) & (1u << 16)).Should().Be(1u << 16, "SETUP_REQ in INTR"); + } + } +} diff --git a/tests/RP2040Sharp.Tests/Watchdog/WatchdogTests.cs b/tests/RP2040Sharp.Tests/Watchdog/WatchdogTests.cs new file mode 100644 index 0000000..649f3e2 --- /dev/null +++ b/tests/RP2040Sharp.Tests/Watchdog/WatchdogTests.cs @@ -0,0 +1,197 @@ +using FluentAssertions; +using RP2040.Peripherals.Watchdog; +using Xunit; + +namespace RP2040.Peripherals.Tests.Watchdog; + +public class WatchdogTests +{ + 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; + private const uint TICK = 0x2C; + + private const uint CTRL_TRIGGER = 1u << 31; + private const uint CTRL_ENABLE = 1u << 30; + private const uint REASON_TIMER = 1u << 0; // bit 0 per RP2040 TRM §4.7.6 + private const uint REASON_FORCE = 1u << 1; // bit 1 per RP2040 TRM §4.7.6 + private const uint TICK_ENABLE = 1u << 9; + private const uint TICK_RUNNING = 1u << 10; + + // 1 µs = 125 CPU cycles at 125 MHz + private const long CYCLES_PER_US = 125; + + // ── SCRATCH registers ──────────────────────────────────────────────── + + [Fact] + public void Scratch0_through_7_are_read_write() + { + var wdg = new WatchdogPeripheral(); + for (uint i = 0; i < 8; i++) + { + var addr = SCRATCH0 + i * 4; + wdg.WriteWord(addr, 0xDEAD_0000u | i); + wdg.ReadWord(addr).Should().Be(0xDEAD_0000u | i, $"SCRATCH{i}"); + } + } + + [Fact] + public void Scratch_registers_are_independent() + { + var wdg = new WatchdogPeripheral(); + wdg.WriteWord(SCRATCH0, 0xAAAAAAAA); + wdg.WriteWord(SCRATCH7, 0x55555555); + + wdg.ReadWord(SCRATCH0).Should().Be(0xAAAAAAAAu); + wdg.ReadWord(SCRATCH7).Should().Be(0x55555555u); + } + + // ── TICK register ──────────────────────────────────────────────────── + + [Fact] + public void Tick_register_defaults_to_running_with_12_cycles() + { + var wdg = new WatchdogPeripheral(); + var tick = wdg.ReadWord(TICK); + + (tick & 0x1FFu).Should().Be(12u, "CYCLES default = 12"); + (tick & TICK_ENABLE).Should().NotBe(0u, "ENABLE default = 1"); + (tick & TICK_RUNNING).Should().NotBe(0u, "RUNNING default = 1"); + } + + [Fact] + public void Writing_tick_updates_cycles_and_running() + { + var wdg = new WatchdogPeripheral(); + wdg.WriteWord(TICK, TICK_ENABLE | 100u); // 100 cycles, enabled + var tick = wdg.ReadWord(TICK); + + (tick & 0x1FFu).Should().Be(100u); + (tick & TICK_RUNNING).Should().NotBe(0u, "running when enabled"); + } + + [Fact] + public void Disabling_tick_clears_running() + { + var wdg = new WatchdogPeripheral(); + wdg.WriteWord(TICK, 12u); // ENABLE=0 + var tick = wdg.ReadWord(TICK); + + (tick & TICK_RUNNING).Should().Be(0u, "not running when disabled"); + } + + // ── CTRL TRIGGER (force reset) ──────────────────────────────────────── + + [Fact] + public void Writing_ctrl_trigger_invokes_OnReset_and_sets_REASON_FORCE() + { + var wdg = new WatchdogPeripheral(); + var resetCount = 0; + wdg.OnReset = () => resetCount++; + + wdg.WriteWord(CTRL, CTRL_TRIGGER); + + resetCount.Should().Be(1, "OnReset called once"); + wdg.ReadWord(REASON).Should().Be(REASON_FORCE, "REASON_FORCE on trigger"); + } + + [Fact] + public void Ctrl_trigger_bit_not_stored_in_ctrl() + { + var wdg = new WatchdogPeripheral(); + wdg.OnReset = () => { }; + wdg.WriteWord(CTRL, CTRL_TRIGGER | CTRL_ENABLE); + + (wdg.ReadWord(CTRL) & CTRL_TRIGGER).Should().Be(0u, "TRIGGER is a strobe, not stored"); + } + + // ── Watchdog countdown (ITickable) ──────────────────────────────────── + + [Fact] + public void Watchdog_does_not_fire_when_disabled() + { + var wdg = new WatchdogPeripheral(); + var resetCount = 0; + wdg.OnReset = () => resetCount++; + + // 1 µs = 125 cycles, LOAD=1 (fire after 1 µs elapsed) + wdg.WriteWord(LOAD, 1u); + // Do NOT enable — CTRL_ENABLE not set + wdg.Tick(CYCLES_PER_US * 10); + + resetCount.Should().Be(0, "disabled watchdog must never fire"); + } + + [Fact] + public void Watchdog_fires_after_load_microseconds() + { + var wdg = new WatchdogPeripheral(); + var resetCount = 0; + wdg.OnReset = () => resetCount++; + + // Set LOAD = 10 (units: µs per RP2040 TRM CTRL[23:0]) + wdg.WriteWord(LOAD, 10u); + // Enable with CTRL_ENABLE — this also reloads countdown from LOAD + wdg.WriteWord(CTRL, CTRL_ENABLE); + + // Tick 9 µs — should not fire yet + wdg.Tick(CYCLES_PER_US * 9); + resetCount.Should().Be(0, "not yet expired after 9 µs"); + + // Tick 1 more µs — total = 10 µs, should fire now + wdg.Tick(CYCLES_PER_US); + resetCount.Should().Be(1, "fired after 10 µs"); + } + + [Fact] + public void Watchdog_sets_REASON_TIMER_when_fired() + { + var wdg = new WatchdogPeripheral(); + wdg.OnReset = () => { }; + + wdg.WriteWord(LOAD, 1u); + wdg.WriteWord(CTRL, CTRL_ENABLE); + wdg.Tick(CYCLES_PER_US); // expire + + wdg.ReadWord(REASON).Should().Be(REASON_TIMER); + } + + [Fact] + public void Watchdog_disables_itself_after_firing() + { + var wdg = new WatchdogPeripheral(); + var resetCount = 0; + wdg.OnReset = () => resetCount++; + + wdg.WriteWord(LOAD, 1u); + wdg.WriteWord(CTRL, CTRL_ENABLE); + wdg.Tick(CYCLES_PER_US * 100); // fire + extra ticks + + resetCount.Should().Be(1, "fires exactly once"); + (wdg.ReadWord(CTRL) & CTRL_ENABLE).Should().Be(0u, "ENABLE cleared after firing"); + } + + [Fact] + public void Writing_load_reloads_countdown() + { + var wdg = new WatchdogPeripheral(); + var resetCount = 0; + wdg.OnReset = () => resetCount++; + + // Enable with LOAD=5, tick 4 µs + wdg.WriteWord(LOAD, 5u); + wdg.WriteWord(CTRL, CTRL_ENABLE); + wdg.Tick(CYCLES_PER_US * 4); + resetCount.Should().Be(0); + + // Reload with LOAD=10 — countdown restarts from 10 + wdg.WriteWord(LOAD, 10u); + wdg.Tick(CYCLES_PER_US * 9); // 9 µs from reload — not yet + resetCount.Should().Be(0, "not yet — countdown was reset to 10"); + + wdg.Tick(CYCLES_PER_US); // 10 µs from reload — fire + resetCount.Should().Be(1); + } +}