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#

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):

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:

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:

var result = sim.RunUntilHalt(uart, "PASS");
result.Succeeded.Should().BeTrue();

CPU-health assertions#

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:

sim.RunUntilHalt(uart, "PASS");
sim.Cpu.Should().HaveExecutedAtMost(2_000_000);

Output, GPIO and peripherals#

uart.Text.Should().Contain("ready");          // captured UART text
pico.Gpio[25].Should().BeHigh();              // GpioPin assertions

See Peripherals for the per-peripheral API.