diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..f326a2d --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +_build/ +.venv/ diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 0000000..4f98b58 --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,2 @@ +/* RP2040Sharp docs — custom tweaks on top of the PyData theme. + Kept minimal; matches the shared PyMCU docs styling. */ diff --git a/docs/compat/index.md b/docs/compat/index.md new file mode 100644 index 0000000..7f11c32 --- /dev/null +++ b/docs/compat/index.md @@ -0,0 +1,27 @@ +# Compatibility + +RP2040Sharp runs real, unmodified firmware images. + +## MicroPython + +Stock MicroPython UF2 images for the Raspberry Pi Pico boot to an interactive REPL over +emulated USB-CDC. The integration suite runs against **v1.19.1**, **v1.20.0**, and +**v1.21.0**. Flash program/erase is implemented via native C# hooks, so MicroPython's +LittleFS filesystem works correctly. + +```csharp +using var pico = new PicoSimulation(); +pico.LoadFlash(RP2040Machine.Uf2ToFlash(microPythonUf2)!); +pico.RunUntilOutput(pico.UsbCdc, ">>> ", timeoutMs: 60_000); +``` + +## CircuitPython + +CircuitPython boots to its REPL as well. USB host support is currently **CDC-only** +(sufficient for the REPL); MSC/HID host drivers are not included. + +## Bare-metal (pico-sdk, PyMCU) + +Any firmware built with the pico-sdk toolchain runs directly. This is the primary use +case for [PyMCU](https://docs.pymcu.org), which compiles Python to bare-metal ARM and uses +RP2040Sharp as its RP2040 integration testkit. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..50893ce --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,92 @@ +project = "RP2040Sharp" +copyright = "2026, Iván Montiel Cardona" +author = "Iván Montiel Cardona" +release = "1.0.0" +version = "1.0" + +extensions = [ + "myst_parser", + "sphinx_copybutton", + "sphinx_design", + "sphinx.ext.intersphinx", +] + +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + +templates_path = ["_templates"] +exclude_patterns = ["_build", ".venv", "Thumbs.db", ".DS_Store"] + +# Served as a sub-path of the shared PyMCU docs site. +html_baseurl = "https://docs.pymcu.org/rp2040sharp/" + +# --------------------------------------------------------------------------- +# MyST extensions +# --------------------------------------------------------------------------- +myst_enable_extensions = [ + "colon_fence", + "deflist", + "tasklist", + "attrs_inline", +] +myst_heading_anchors = 4 + +# --------------------------------------------------------------------------- +# HTML / PyData theme (matches the PyMCU docs site) +# --------------------------------------------------------------------------- +html_theme = "pydata_sphinx_theme" +html_static_path = ["_static"] +html_css_files = ["css/custom.css"] +html_title = "RP2040Sharp" + +html_theme_options = { + "navbar_align": "left", + "navbar_end": ["navbar-icon-links", "theme-switcher"], + "secondary_sidebar_items": ["page-toc", "edit-this-page"], + "show_prev_next": True, + "navigation_with_keys": True, + "footer_start": ["copyright"], + "footer_end": ["sphinx-version"], + "pygments_light_style": "friendly", + "pygments_dark_style": "monokai", + "header_links_before_dropdown": 6, + "navigation_depth": 3, + "show_nav_level": 1, + "icon_links": [ + { + "name": "PyMCU docs", + "url": "https://docs.pymcu.org", + "icon": "fa-solid fa-house", + }, + { + "name": "GitHub", + "url": "https://github.com/PyMCU/RP2040Sharp", + "icon": "fa-brands fa-github", + }, + { + "name": "NuGet", + "url": "https://www.nuget.org/packages/RP2040Sharp", + "icon": "fa-solid fa-cube", + }, + ], +} + +html_sidebars = { + "**": ["sidebar-nav-bs"], +} + +# "Edit this page" links point at the repo. +html_context = { + "github_user": "PyMCU", + "github_repo": "RP2040Sharp", + "github_version": "master", + "doc_path": "docs", +} + +# --------------------------------------------------------------------------- +# copybutton: strip prompt characters +# --------------------------------------------------------------------------- +copybutton_prompt_text = r">>> |\.\.\. |\$ " +copybutton_prompt_is_regexp = True diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 0000000..fe1b17c --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,10 @@ +# Getting Started + +Install the packages, run your first firmware, and learn the basic API. + +```{toctree} +:maxdepth: 1 + +installation +quickstart +``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 0000000..99d86e7 --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,29 @@ +# Installation + +RP2040Sharp targets **.NET 10**. You need the .NET 10 SDK installed. + +## From NuGet + +```bash +dotnet add package RP2040Sharp # the emulator core +dotnet add package RP2040Sharp.TestKit # fluent harness for firmware tests (optional) +``` + +| Package | Purpose | +|---|---| +| [`RP2040Sharp`](https://www.nuget.org/packages/RP2040Sharp) | CPU, bus, peripherals, `RP2040Machine`. AOT-compatible, trimmable. | +| [`RP2040Sharp.TestKit`](https://www.nuget.org/packages/RP2040Sharp.TestKit) | `RP2040TestSimulation`, probes, and FluentAssertions-based health checks. | + +## From source + +```bash +git clone https://github.com/PyMCU/RP2040Sharp.git +cd RP2040Sharp +dotnet build +dotnet test +``` + +## Headless runner CLI + +The repository also ships a headless runner (`src/RP2040Sharp.Runner`) for CI pipelines — +see [Validating firmware in CI](../guides/ci-validation.md). diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md new file mode 100644 index 0000000..d500315 --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,52 @@ +# Quickstart + +## Run a firmware image and capture its serial output + +```csharp +using RP2040.Peripherals; + +var machine = new RP2040Machine(); + +// Load a raw flash image or a UF2 (use Uf2ToFlash for UF2 files). +machine.LoadFlash(File.ReadAllBytes("firmware.bin")); + +// Forward UART0 TX to the console. +machine.Uart0.OnByteTransmit += b => Console.Write((char)b); + +// Run 1 ms of simulated time (125 000 cycles at 125 MHz). +machine.Run(125_000); +``` + +Loading a UF2 image: + +```csharp +var flash = RP2040Machine.Uf2ToFlash(File.ReadAllBytes("firmware.uf2")); +machine.LoadFlash(flash!); +``` + +## Drive a test with the TestKit + +The TestKit wraps `RP2040Machine` in a fluent harness with probes and assertions: + +```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); +``` + +For bounded, never-hanging runs and CPU-health checks, see +[Firmware testing with the TestKit](../guides/testkit.md). + +## Inject and read GPIO + +```csharp +machine.Sio.SetGpioExternalIn(5, high: true); // drive GP5 from outside +bool isHigh = machine.Sio.GetGpioOut(3); // read firmware's GP3 output +bool isOutput = machine.Sio.GetGpioOutputEnable(3); +``` diff --git a/docs/guides/ci-validation.md b/docs/guides/ci-validation.md new file mode 100644 index 0000000..07bc502 --- /dev/null +++ b/docs/guides/ci-validation.md @@ -0,0 +1,57 @@ +# Validating firmware in CI + +RP2040Sharp is designed to validate compiler/firmware output (for example, the +[PyMCU](https://docs.pymcu.org) compiler) in CI **without flaky or hanging builds**: + +- Runs are **bounded** — a wedged program fails with a reason instead of stalling the job. +- The clock is driven by executed cycles, so results are **deterministic** and reproducible. + +There are two ways to use it in a pipeline. + +## In a .NET test project (recommended) + +Use the TestKit directly from xUnit/NUnit. This is what PyMCU's integration suite does: + +```csharp +[Fact] +public void Blink_firmware_reports_pass() +{ + using var pico = new PicoSimulation(); + pico.LoadFlash(RP2040Machine.Uf2ToFlash(File.ReadAllBytes("blink.uf2"))!); + + var result = pico.RunUntilHalt(pico.Uart0, "PASS"); + + result.Succeeded.Should().BeTrue($"firmware halted with {result.Outcome}"); + pico.Cpu.Should().NotHaveFaulted(); +} +``` + +## Headless runner CLI + +For pipelines that just need an exit code (no C# harness), use the `rp2040sharp` runner: + +```bash +dotnet run --project src/RP2040Sharp.Runner -c Release -- \ + firmware.uf2 --expect-text "PASS" --channel uart --max-instructions 5000000 +``` + +Exit codes: + +| Code | Meaning | +|---|---| +| `0` | expected text found | +| `1` | text not found within the instruction budget | +| `2` | the firmware crashed (CPU lockup) | +| `64` | usage error | +| `66` | image file not found | + +Options: + +| Option | Default | Description | +|---|---|---| +| `--expect-text ` | — | Pass only if `` appears in serial output | +| `--channel uart\|usb` | `uart` | Serial channel to watch | +| `--max-instructions ` | `500000000` | Hard execution budget | +| `--quiet` | off | Do not echo serial output to stdout | + +Serial output goes to **stdout**; the run summary goes to **stderr**. diff --git a/docs/guides/gdb-debugging.md b/docs/guides/gdb-debugging.md new file mode 100644 index 0000000..bd2715c --- /dev/null +++ b/docs/guides/gdb-debugging.md @@ -0,0 +1,43 @@ +# Debugging with GDB + +RP2040Sharp ships a GDB Remote Serial Protocol server, so you can debug Core 0 with +`arm-none-eabi-gdb` exactly as you would on real hardware. + +## From the demo + +```bash +dotnet run --project src/RP2040Sharp.Demo -c Release -- --gdb +``` + +Then, in another terminal: + +```bash +arm-none-eabi-gdb -ex "target remote :3333" +``` + +``` +(gdb) info registers +(gdb) x/4xw 0x20000000 +(gdb) stepi +(gdb) break *0x10000100 +(gdb) continue +``` + +## Embedding the server in your own host + +```csharp +using RP2040.Gdb; + +var server = new GdbTcpServer(myGdbTarget, port: 3333); // myGdbTarget : IGdbTarget +server.OnLog = Console.Error.WriteLine; +server.Start(); +``` + +`IGdbTarget` exposes the machine and an `Execute()`/`Stop()` pair so your host controls +when the CPU is free-running versus halted. + +## Supported commands + +Registers (`g`, `p`/`P`, including xPSR and MSP/PSP/PRIMASK/CONTROL), memory (`m`/`M`), +`c` (continue), `s`/`vCont;s` (step), `D` (detach), and BKPT-driven stop replies with PC +rewind. `BASEPRI`/`FAULTMASK` read as 0 — the Cortex-M0+ does not implement them. diff --git a/docs/guides/index.md b/docs/guides/index.md new file mode 100644 index 0000000..7d5aa3e --- /dev/null +++ b/docs/guides/index.md @@ -0,0 +1,11 @@ +# Guides + +Task-oriented guides for the main use cases. + +```{toctree} +:maxdepth: 1 + +testkit +ci-validation +gdb-debugging +``` diff --git a/docs/guides/testkit.md b/docs/guides/testkit.md new file mode 100644 index 0000000..d29c397 --- /dev/null +++ b/docs/guides/testkit.md @@ -0,0 +1,78 @@ +# Firmware testing with the TestKit + +`RP2040Sharp.TestKit` turns the emulator into a fluent test harness: build a simulation, +attach probes, run it under a bound, and assert on the result. + +## Building a simulation + +```csharp +using RP2040.TestKit; + +var sim = RP2040TestSimulation.Create() + .WithFrequency(125_000_000) + .WithBinary(File.ReadAllBytes("firmware.bin")) + .AddUart(0, out var uart); +``` + +`PicoSimulation` is a convenience board preset (UART0/1 + USB-CDC wired up): + +```csharp +using RP2040.TestKit.Boards; + +using var pico = new PicoSimulation(); +pico.LoadFlash(RP2040Machine.Uf2ToFlash(uf2)!); +``` + +## Bounded runs that never hang + +A fixed `RunMilliseconds` is fine for healthy firmware, but a wedged or crashed program +would run forever. `RunUntilHalt` is **bounded** and returns *why* it stopped: + +```csharp +var result = sim.RunUntilHalt( + () => uart.Text.Contains("PASS"), + maxInstructions: 5_000_000); + +switch (result.Outcome) +{ + case RunOutcome.PredicateMet: /* success */ break; + case RunOutcome.LockedUp: /* firmware crashed */ break; + case RunOutcome.BudgetReached: /* timed out / wedged */ break; +} +``` + +There is a convenience overload for the common "wait for serial text" case: + +```csharp +var result = sim.RunUntilHalt(uart, "PASS"); +result.Succeeded.Should().BeTrue(); +``` + +## CPU-health assertions + +```csharp +using RP2040.TestKit.Extensions; // brings in .Should() for the CPU + +sim.Cpu.Should().NotBeLockedUp(); +sim.Cpu.Should().NotHaveFaulted(); // not in HardFault (IPSR != 3) +sim.Cpu.Should().BeInThreadMode(); // IPSR == 0 +``` + +## Deterministic instruction count + +`InstructionCount` is reproducible across machines (the clock is driven by executed +cycles, not wall-clock), so it works as a compiler-size regression guard: + +```csharp +sim.RunUntilHalt(uart, "PASS"); +sim.Cpu.Should().HaveExecutedAtMost(2_000_000); +``` + +## Output, GPIO and peripherals + +```csharp +uart.Text.Should().Contain("ready"); // captured UART text +pico.Gpio[25].Should().BeHigh(); // GpioPin assertions +``` + +See [Peripherals](../peripherals/index.md) for the per-peripheral API. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..f50fb8f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,118 @@ +# RP2040Sharp + +**RP2040Sharp** is a high-performance emulator for the Raspberry Pi **RP2040** +microcontroller, written entirely in modern **C# (.NET 10)**. It runs real, unmodified +RP2040 firmware — including **MicroPython** — and reaches an interactive REPL in ~3–4 s of +simulated time (~460 MIPS on Apple Silicon). + +It is a C# port and re-imagination of [rp2040js](https://github.com/wokwi/rp2040js) by +Uri Shaked, built for the .NET ecosystem with a focus on speed, type safety, and use as a +**deterministic firmware testkit** — for example, validating the output of the +[PyMCU](https://docs.pymcu.org) compiler in CI. + +```bash +dotnet add package RP2040Sharp +dotnet add package RP2040Sharp.TestKit # fluent harness for firmware tests +``` + +```csharp +using RP2040.Peripherals; + +var machine = new RP2040Machine(); +machine.LoadFlash(File.ReadAllBytes("firmware.bin")); +machine.Uart0.OnByteTransmit += b => Console.Write((char)b); +machine.Run(125_000); // 1 ms at 125 MHz +``` + +--- + +## Why RP2040Sharp + +::::{grid} 1 2 2 3 +:gutter: 3 + +:::{grid-item-card} Real firmware, unmodified +Boots the real RP2040 B1 BootROM and runs stock MicroPython/CircuitPython UF2 images — +no patches, no shims. +::: + +:::{grid-item-card} Deterministic by design +Time is driven by executed cycles, never by wall-clock. Runs are reproducible across +machines — ideal for CI and compiler regression checks. +::: + +:::{grid-item-card} Never hangs in CI +Bounded execution: wedged or crashed firmware fails a test with a diagnostic reason +instead of stalling the build. +::: + +:::{grid-item-card} GDB debugging +Attach `arm-none-eabi-gdb` over `target remote :3333` — registers, memory, single-step, +breakpoints. +::: + +:::{grid-item-card} Dual-core +Core 1 launches through the SIO FIFO multicore handshake (RP2040 §2.8.3); both cores +advance in lock-step. +::: + +:::{grid-item-card} AOT-friendly +The core library is trimmable and NativeAOT-compatible — embed it anywhere. +::: +:::: + +--- + +## How it compares to rp2040js + +| | rp2040js | **RP2040Sharp** | +|---|---|---| +| Language | TypeScript | **C# (.NET 10)** | +| GDB stub | Yes | **Yes** | +| Dual-core | Partial | **Yes (working handshake)** | +| Test harness | — | **Fluent TestKit + assertions** | +| CI guardrails | — | **Bounded runs, health assertions, headless runner** | +| Deterministic clock | Yes | **Yes** | +| AOT / trimming | n/a | **Yes** | + +--- + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Getting Started + +getting-started/index +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Guides + +guides/index +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Peripherals + +peripherals/index +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Compatibility + +compat/index +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Reference + +reference/index +``` diff --git a/docs/peripherals/index.md b/docs/peripherals/index.md new file mode 100644 index 0000000..e329bf6 --- /dev/null +++ b/docs/peripherals/index.md @@ -0,0 +1,28 @@ +# Peripherals + +RP2040Sharp emulates the RP2040 peripheral set with enough fidelity to boot real firmware. +Most peripherals are accessed through `RP2040Machine` (e.g. `machine.Uart0`, +`machine.Sio`, `machine.Dma`) and observed in tests via TestKit probes. + +## Coverage + +| Peripheral | Status | Notes | +|---|---|---| +| GPIO / SIO | ✅ | Per-pin API, spinlocks, interpolators, hardware divider | +| UART0 / UART1 | ✅ | FIFO, baud config, DREQ for DMA | +| SPI0 / SPI1 | ✅ | Master mode, FIFO, DREQ | +| I2C0 / I2C1 | ✅ | Master + slave-mode simulation | +| ADC | ✅ | 5 channels incl. temperature sensor, FIFO | +| PWM | ✅ | All 8 slices | +| PIO0 / PIO1 | ✅ | 4 state machines each, GPIO integration | +| DMA | ✅ | All channels, DREQ sources | +| Timer / Watchdog / RTC | ✅ | Alarms, microsecond counter | +| USB | ✅ | CDC-ACM host (MicroPython REPL) | +| Clocks / Resets | ✅ | Clock mux, per-peripheral reset | +| XOSC / ROSC / PLL / PSM / VREG | 🟡 | Register stubs (report stable/locked; no frequency model) | +| SSI flash (XIP hardware path) | ⛔ | Flash program/erase via C# hooks instead | + +```{note} +Per-peripheral reference pages are being filled in. For now, the API surface is documented +via XML-doc comments in the source; see [Reference](../reference/index.md). +``` diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 0000000..5421942 --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,23 @@ +# Reference + +## Key public types + +| Type | Namespace | Role | +|---|---|---| +| `RP2040Machine` | `RP2040.Peripherals` | The emulated machine: CPU(s), bus, peripherals, `LoadFlash`/`Run`. | +| `CortexM0Plus` | `RP2040.Core.Cpu` | The CPU core: registers, `Step()`, `IsLockedUp`. | +| `RP2040TestSimulation` | `RP2040.TestKit` | Fluent test harness: `RunUntilHalt`, probes, `InstructionCount`. | +| `PicoSimulation` | `RP2040.TestKit.Boards` | Raspberry Pi Pico board preset. | +| `RunResult` / `RunOutcome` | `RP2040.TestKit` | Diagnostic outcome of a bounded run. | +| `GdbTcpServer` / `GdbServer` | `RP2040.Gdb` | GDB Remote Serial Protocol server. | + +## API documentation + +The full public API is documented with XML-doc comments in the source. A generated API +reference (DocFX) is planned; until then, browse the +[source on GitHub](https://github.com/PyMCU/RP2040Sharp/tree/master/src) or rely on +IntelliSense from the NuGet packages. + +```{note} +This page is a stub. Per-namespace reference pages will be added as the docs site grows. +``` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..35675f3 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,5 @@ +sphinx>=7.3 +pydata-sphinx-theme>=0.15 +myst-parser>=3.0 +sphinx-copybutton>=0.5 +sphinx-design>=0.6