Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.0] - 2026-06-06

First stable release. Promotes `1.0.0-rc.2` after the TestKit gained CI guardrails for
use as a compiler/firmware validator (e.g. PyMCU). The public API is now considered
stable under [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Added

- **CI guardrails for the TestKit**, aimed at using the emulator to validate compiler
output (e.g. PyMCU) without flaky or hanging builds:
- `RP2040TestSimulation.RunUntilHalt(predicate, maxInstructions)` — a bounded run that
can never hang: it returns a diagnostic `RunResult` (`PredicateMet` / `LockedUp` /
`BudgetReached`) instead of stalling on wedged firmware.
- CPU-health assertions: `Should().NotBeLockedUp()`, `NotHaveFaulted()`,
`BeInThreadMode()`, and `HaveExecutedAtMost(n)`.
- `RP2040TestSimulation.InstructionCount` — a deterministic, reproducible
instruction-count metric for compiler-size regression checks.
- **`rp2040sharp` runner CLI** (`src/RP2040Sharp.Runner`) — loads a UF2/bin, runs it under
a hard instruction budget, watches UART or USB-CDC for an expected string, and exits with
a CI-friendly code (0 found · 1 not found · 2 firmware crashed). The `rp2040js`-style
`--expect-text` workflow, headless.

### Changed

- Repository moved to the [PyMCU organization](https://github.com/PyMCU/RP2040Sharp);
package metadata URLs updated accordingly. Published by `begeistert` as before.

## [1.0.0-rc.2] - 2026-06-06

### Changed
Expand Down Expand Up @@ -54,6 +81,7 @@ First public release candidate. A high-performance RP2040 emulator in modern C#
- Flash programming uses C# hooks rather than the SSI XIP hardware path.
- USB host support is CDC-only (sufficient for the MicroPython REPL).

[Unreleased]: https://github.com/begeistert/RP2040Sharp/compare/v1.0.0-rc.2...HEAD
[1.0.0-rc.2]: https://github.com/begeistert/RP2040Sharp/compare/v1.0.0-rc.1...v1.0.0-rc.2
[1.0.0-rc.1]: https://github.com/begeistert/RP2040Sharp/releases/tag/v1.0.0-rc.1
[Unreleased]: https://github.com/PyMCU/RP2040Sharp/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/PyMCU/RP2040Sharp/compare/v1.0.0-rc.2...v1.0.0
[1.0.0-rc.2]: https://github.com/PyMCU/RP2040Sharp/compare/v1.0.0-rc.1...v1.0.0-rc.2
[1.0.0-rc.1]: https://github.com/PyMCU/RP2040Sharp/releases/tag/v1.0.0-rc.1
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
<PropertyGroup>
<Authors>Iván Montiel Cardona</Authors>
<Company>begeistert</Company>
<RepositoryUrl>https://github.com/begeistert/RP2040Sharp</RepositoryUrl>
<RepositoryUrl>https://github.com/PyMCU/RP2040Sharp</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageProjectUrl>https://github.com/begeistert/RP2040Sharp</PackageProjectUrl>
<PackageProjectUrl>https://github.com/PyMCU/RP2040Sharp</PackageProjectUrl>
<PackageTags>rp2040;raspberry-pi;emulator;cortex-m0plus;microcontroller;simulation</PackageTags>
<Copyright>Copyright © 2024-2026 Iván Montiel Cardona</Copyright>
<Deterministic>true</Deterministic>
Expand Down
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# RP2040Sharp

![Build Status](https://github.com/begeistert/RP2040Sharp/actions/workflows/test.yml/badge.svg)
![Build Status](https://github.com/PyMCU/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)

Expand Down Expand Up @@ -35,7 +35,7 @@ The emulator boots MicroPython v1.21.0 and reaches the interactive REPL in appro
## Getting Started

```bash
git clone https://github.com/begeistert/RP2040Sharp.git
git clone https://github.com/PyMCU/RP2040Sharp.git
cd RP2040Sharp
dotnet restore
dotnet build
Expand Down Expand Up @@ -81,6 +81,34 @@ sim.RunMilliseconds(100);
Assert.Contains("Hello", uart.Text);
```

### Validating firmware in CI

Built for using the emulator as a compiler/firmware testkit (e.g. for
[PyMCU](https://github.com/PyMCU/PyMCU)) without flaky or hanging builds. A run is
always **bounded** — wedged firmware fails the test with a reason instead of stalling the
job — and the instruction count is **deterministic** and reproducible across machines.

```csharp
var sim = RP2040TestSimulation.Create()
.WithBinary(File.ReadAllBytes("firmware.bin"))
.AddUart(0, out var uart);

// Never hangs: returns PredicateMet / LockedUp / BudgetReached.
var result = sim.RunUntilHalt(() => uart.Text.Contains("PASS"), maxInstructions: 5_000_000);

result.Succeeded.Should().BeTrue();
sim.Cpu.Should().NotHaveFaulted();
sim.Cpu.Should().HaveExecutedAtMost(2_000_000); // compiler-size regression guard
```

Or headless from a pipeline, with the `rp2040sharp` runner CLI (exit 0 found · 1 not
found · 2 crashed):

```bash
dotnet run --project src/RP2040Sharp.Runner -c Release -- \
firmware.uf2 --expect-text "PASS" --channel uart
```

### GPIO integration (circuit simulators)

```csharp
Expand Down Expand Up @@ -117,6 +145,7 @@ server.Start();
|---|---|
| `src/RP2040Sharp` | Core library — CPU, bus, peripherals, machine |
| `src/RP2040.TestKit` | Fluent test harness for firmware integration tests |
| `src/RP2040Sharp.Runner` | Headless `rp2040sharp` CLI: run firmware, `--expect-text`, CI exit codes |
| `src/RP2040Sharp.Demo` | Demo: boots MicroPython and drives the REPL |

## Architecture Notes
Expand Down
15 changes: 15 additions & 0 deletions RP2040.sln
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RP2040Sharp.Demo", "src\RP2
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RP2040Sharp.Runner", "src\RP2040Sharp.Runner\RP2040Sharp.Runner.csproj", "{2C4890EB-07AD-4197-BE40-6268A2C1DD94}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -98,6 +100,18 @@ Global
{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
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Debug|x64.ActiveCfg = Debug|Any CPU
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Debug|x64.Build.0 = Debug|Any CPU
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Debug|x86.ActiveCfg = Debug|Any CPU
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Debug|x86.Build.0 = Debug|Any CPU
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Release|Any CPU.Build.0 = Release|Any CPU
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Release|x64.ActiveCfg = Release|Any CPU
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Release|x64.Build.0 = Release|Any CPU
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Release|x86.ActiveCfg = Release|Any CPU
{2C4890EB-07AD-4197-BE40-6268A2C1DD94}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -109,5 +123,6 @@ Global
{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}
{2C4890EB-07AD-4197-BE40-6268A2C1DD94} = {F168743D-BA33-466E-AFAF-BFC9DD2AF698}
EndGlobalSection
EndGlobal
52 changes: 52 additions & 0 deletions src/RP2040.TestKit/Assertions/CortexM0Assertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,56 @@ private AndConstraint<CortexM0Assertions> HaveFlag(string name, bool actual, boo
.FailWith("Expected flag {0} to be {1}{reason}, but found {2}.", name, expected, actual);
return new AndConstraint<CortexM0Assertions>(this);
}

// ── Health checks (handy for CI / firmware smoke tests) ──────────────────────

/// <summary>Assert the CPU has not locked up (no HardFault escalation / firmware crash).</summary>
public AndConstraint<CortexM0Assertions> NotBeLockedUp(
string because = "", params object[] becauseArgs)
{
Execute.Assertion.BecauseOf(because, becauseArgs)
.ForCondition(!Subject.IsLockedUp)
.FailWith("Expected the CPU not to be locked up{reason}, but it was " +
"(a HardFault escalated — the firmware crashed). PC=0x{0:X8}.", Subject.Registers.PC);
return new AndConstraint<CortexM0Assertions>(this);
}

/// <summary>
/// Assert the CPU is healthy: not locked up and not handling a HardFault (IPSR != 3).
/// The go-to smoke check after running firmware.
/// </summary>
public AndConstraint<CortexM0Assertions> NotHaveFaulted(
string because = "", params object[] becauseArgs)
{
Execute.Assertion.BecauseOf(because, becauseArgs)
.ForCondition(!Subject.IsLockedUp && Subject.Registers.IPSR != 3)
.FailWith("Expected the CPU not to have faulted{reason}, but IPSR={0} (3 = HardFault) " +
"and IsLockedUp={1}.", Subject.Registers.IPSR, Subject.IsLockedUp);
return new AndConstraint<CortexM0Assertions>(this);
}

/// <summary>Assert the CPU is in Thread mode (IPSR == 0), i.e. not inside an exception handler.</summary>
public AndConstraint<CortexM0Assertions> BeInThreadMode(
string because = "", params object[] becauseArgs)
{
Execute.Assertion.BecauseOf(because, becauseArgs)
.ForCondition(Subject.Registers.IPSR == 0)
.FailWith("Expected the CPU to be in Thread mode{reason}, but it was handling exception {0}.",
Subject.Registers.IPSR);
return new AndConstraint<CortexM0Assertions>(this);
}

/// <summary>
/// Assert the CPU has executed no more than <paramref name="maxInstructions"/> since reset.
/// Useful as a deterministic compiler-regression guard (instruction count is reproducible).
/// </summary>
public AndConstraint<CortexM0Assertions> HaveExecutedAtMost(long maxInstructions,
string because = "", params object[] becauseArgs)
{
Execute.Assertion.BecauseOf(because, becauseArgs)
.ForCondition(Subject.Cycles <= maxInstructions)
.FailWith("Expected the CPU to execute at most {0} instructions{reason}, but it executed {1}.",
maxInstructions, Subject.Cycles);
return new AndConstraint<CortexM0Assertions>(this);
}
}
41 changes: 41 additions & 0 deletions src/RP2040.TestKit/RP2040TestSimulation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,47 @@ public RP2040TestSimulation RunToBreak(int maxInstructions = 1_000_000)
return this;
}

/// <summary>
/// Total instructions executed by Core 0 since reset. Deterministic and reproducible
/// across machines (the emulator's clock is driven by executed cycles, never by
/// wall-clock time), which makes it usable as a compiler-regression metric — e.g.
/// asserting that a given program still compiles to no more than N instructions.
/// </summary>
public long InstructionCount => Machine.InstructionCount;

/// <summary>
/// Run in bounded batches until <paramref name="until"/> returns true, the CPU locks up,
/// or <paramref name="maxInstructions"/> is reached — whichever comes first. Unlike a
/// fixed <c>RunMilliseconds</c>, this never hangs: wedged or crashed firmware terminates
/// with a diagnostic <see cref="RunResult"/> instead of stalling the CI job.
/// </summary>
/// <param name="until">Predicate evaluated between batches (e.g. UART output check).</param>
/// <param name="maxInstructions">Hard upper bound on executed instructions.</param>
public RunResult RunUntilHalt(Func<bool> until, long maxInstructions = 50_000_000)
{
var batch = (int)Math.Min(100_000, Math.Max(1, maxInstructions));
var start = Machine.InstructionCount;

while (true)
{
if (until())
return new RunResult(RunOutcome.PredicateMet, Machine.InstructionCount - start, Cpu.Registers.Waiting);
if (Cpu.IsLockedUp)
return new RunResult(RunOutcome.LockedUp, Machine.InstructionCount - start, Cpu.Registers.Waiting);
if (Machine.InstructionCount - start >= maxInstructions)
return new RunResult(RunOutcome.BudgetReached, Machine.InstructionCount - start, Cpu.Registers.Waiting);

Machine.Run(batch);
}
}

/// <summary>
/// Convenience overload: run until <paramref name="probe"/> captures
/// <paramref name="expectedText"/>, the CPU crashes, or the budget is reached.
/// </summary>
public RunResult RunUntilHalt(UartProbe probe, string expectedText, long maxInstructions = 50_000_000)
=> RunUntilHalt(() => probe.Text.Contains(expectedText, StringComparison.Ordinal), maxInstructions);

/// <summary>Reset the CPU to its initial state.</summary>
public RP2040TestSimulation Reset()
{
Expand Down
37 changes: 37 additions & 0 deletions src/RP2040.TestKit/RunResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace RP2040.TestKit;

/// <summary>Why a <see cref="RP2040TestSimulation.RunUntilHalt"/> call stopped.</summary>
public enum RunOutcome
{
/// <summary>The supplied predicate became true — the run did what the test wanted.</summary>
PredicateMet,

/// <summary>The CPU entered lockup (a HardFault escalated): the firmware crashed.</summary>
LockedUp,

/// <summary>
/// The instruction budget was exhausted before the predicate was met. Either the
/// firmware is doing more work than expected, or it is wedged (e.g. spinning or asleep
/// waiting for an event that never arrives — see <see cref="RunResult.CpuWasWaiting"/>).
/// </summary>
BudgetReached,
}

/// <summary>
/// Outcome of a bounded <see cref="RP2040TestSimulation.RunUntilHalt"/> run. Designed for
/// CI: a run always terminates, and the outcome distinguishes success from a firmware
/// crash or a timeout so failing firmware fails the test with a clear reason instead of
/// hanging the build.
/// </summary>
public readonly record struct RunResult(
RunOutcome Outcome,
long InstructionsExecuted,
bool CpuWasWaiting)
{
/// <summary>True when the predicate was met (the run succeeded).</summary>
public bool Succeeded => Outcome == RunOutcome.PredicateMet;

public override string ToString() =>
$"{Outcome} after {InstructionsExecuted} instructions" +
(CpuWasWaiting ? " (CPU was in WFI/WFE)" : "");
}
Loading
Loading