diff --git a/src/RP2040Sharp/Peripherals/Pio/PioPeripheral.cs b/src/RP2040Sharp/Peripherals/Pio/PioPeripheral.cs
index e1c95a9..1ea2e38 100644
--- a/src/RP2040Sharp/Peripherals/Pio/PioPeripheral.cs
+++ b/src/RP2040Sharp/Peripherals/Pio/PioPeripheral.cs
@@ -301,7 +301,10 @@ public void InjectRxData(int smIndex, uint value)
{
var sm = _sm[smIndex];
if (sm.RxFifo.Count < sm.RxDepth)
+ {
sm.RxFifo.Enqueue(value);
+ OnRxPush?.Invoke(smIndex, value);
+ }
}
// ── Private: stall wake-up ───────────────────────────────────────
diff --git a/src/RP2040Sharp/Peripherals/Rtc/RtcPeripheral.cs b/src/RP2040Sharp/Peripherals/Rtc/RtcPeripheral.cs
index 7758576..61c075a 100644
--- a/src/RP2040Sharp/Peripherals/Rtc/RtcPeripheral.cs
+++ b/src/RP2040Sharp/Peripherals/Rtc/RtcPeripheral.cs
@@ -15,8 +15,8 @@ public sealed class RtcPeripheral : IMemoryMappedDevice, ITickable
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 RTC_RTC1 = 0x18; // RTC_1 read reg = YEAR/MONTH/DAY (NB: the read regs SWAP layout vs SETUP)
+ private const uint RTC_RTC0 = 0x1C; // RTC_0 read reg = DOTW/HOUR/MIN/SEC
private const uint CTRL_ENABLE = 1u;
private const uint CTRL_ACTIVE = 1u << 1;
@@ -47,8 +47,8 @@ public sealed class RtcPeripheral : IMemoryMappedDevice, ITickable
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 uint _rtcDate; // YEAR[27:16] MONTH[11:8] DAY[4:0]
+ private uint _rtcTime; // DOTW[26:24] HOUR[20:16] MIN[13:8] SEC[5:0]
private long _accumCycles;
@@ -58,15 +58,15 @@ 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
+ _rtcDate = (2024u << 16) | (1u << 8) | 1u;
+ _rtcTime = (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;
+ _rtcDate = ((uint)year << 16) | ((uint)month << 8) | (uint)day;
+ _rtcTime = ((uint)dayOfWeek << 24) | ((uint)hour << 16) | ((uint)min << 8) | (uint)sec;
}
// ── ITickable ─────────────────────────────────────────────────────────
@@ -93,8 +93,8 @@ public void Tick(long deltaCycles)
RTC_CTRL => (_ctrl & CTRL_ENABLE) != 0 ? (_ctrl | CTRL_ACTIVE) : _ctrl,
IRQ_SETUP_0 => _irqSetup0,
IRQ_SETUP_1 => _irqSetup1,
- RTC_RTC1 => _rtc1,
- RTC_RTC0 => _rtc0,
+ RTC_RTC1 => _rtcDate, // RTC_1 (0x18) = YEAR/MONTH/DAY
+ RTC_RTC0 => _rtcTime, // RTC_0 (0x1C) = DOTW/HOUR/MIN/SEC
_ => 0,
};
@@ -117,8 +117,8 @@ public void WriteWord(uint address, uint value)
case RTC_CTRL:
if ((value & CTRL_LOAD) != 0)
{
- _rtc0 = _setup0;
- _rtc1 = _setup1;
+ _rtcDate = _setup0;
+ _rtcTime = _setup1;
_accumCycles = 0;
}
_ctrl = value & CTRL_ENABLE; // LOAD is strobe, ACTIVE is read-only
@@ -150,13 +150,13 @@ public void WriteByte(uint address, byte value)
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);
+ var sec = (int)(_rtcTime & 0x3F);
+ var min = (int)((_rtcTime >> 8) & 0x3F);
+ var hour = (int)((_rtcTime >> 16) & 0x1F);
+ var dotw = (int)((_rtcTime >> 24) & 0x7);
+ var day = (int)(_rtcDate & 0x1F);
+ var month = (int)((_rtcDate >> 8) & 0xF);
+ var year = (int)((_rtcDate >> 16) & 0xFFF);
sec++;
if (sec >= 60) { sec = 0; min++; }
@@ -172,21 +172,21 @@ private void AdvanceSecond()
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;
+ _rtcDate = ((uint)year << 16) | ((uint)month << 8) | (uint)day;
+ _rtcTime = ((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 sec = _rtcTime & 0x3F;
+ var min = (_rtcTime >> 8) & 0x3F;
+ var hour = (_rtcTime >> 16) & 0x1F;
+ var dotw = (_rtcTime >> 24) & 0x7;
+ var day = _rtcDate & 0x1F;
+ var month = (_rtcDate >> 8) & 0xF;
+ var year = (_rtcDate >> 16) & 0xFFF;
var matched = true;
if ((_irqSetup0 & IRQ0_YEAR_ENA) != 0) matched &= ((_irqSetup0 >> 16) & 0xFFF) == year;
diff --git a/src/RP2040Sharp/Peripherals/Ssi/SsiPeripheral.cs b/src/RP2040Sharp/Peripherals/Ssi/SsiPeripheral.cs
index 0a1f643..e2e4d7f 100644
--- a/src/RP2040Sharp/Peripherals/Ssi/SsiPeripheral.cs
+++ b/src/RP2040Sharp/Peripherals/Ssi/SsiPeripheral.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using RP2040.Core.Memory;
@@ -75,6 +76,7 @@ public sealed unsafe class SsiPeripheral : IMemoryMappedDevice
private const byte CMD_READ_DATA = 0x03;
private const byte CMD_FAST_READ = 0x0B;
private const byte CMD_QUAD_IO_READ = 0xEB;
+ private const byte CMD_READ_UNIQUE_ID = 0x4B; // 4 dummy bytes, then 8 id bytes
// ── Registers ─────────────────────────────────────────────────────────────
private uint _ctrlr0;
@@ -105,6 +107,20 @@ public sealed unsafe class SsiPeripheral : IMemoryMappedDevice
private readonly List _rxFrame = new(260);
private bool _lastOpWasRead;
+ // 64-bit flash unique id served for the 0x4B (Read Unique ID) command — what machine.unique_id() reads.
+ // MUST differ per board instance, else two identical boards share an identity (e.g. the same MQTT
+ // client-id, so a broker drops the first when the second connects). Defaults to a random-looking
+ // value generated per instance (like a real chip's factory-programmed id) so this holds even when
+ // the host never touches it; still overridable for tests that need a reproducible id.
+ public byte[] UniqueId = CreateDefaultUniqueId();
+
+ private static byte[] CreateDefaultUniqueId()
+ {
+ var id = new byte[8];
+ Array.Copy(Guid.NewGuid().ToByteArray(), id, 8);
+ return id;
+ }
+
public uint Size => 0x1000;
// ── Wiring API ────────────────────────────────────────────────────────────
@@ -128,6 +144,11 @@ public void OnCsAssert()
if (_csAsserted) return; // guard against double-assert
_csAsserted = true;
_txBuf.Clear();
+ // Each CS assert starts a fresh transaction: reset the RX frame so the command byte lands at index 0
+ // (bytes clocked earlier with CS deasserted, e.g. flash_exit_xip dummies, must not shift the frame).
+ _rxQueue.Clear();
+ _rxFrame.Clear();
+ _lastOpWasRead = false;
}
///
@@ -212,8 +233,11 @@ public void WriteWord(uint address, uint value)
// RXFLR-polling loops, instead of spinning forever on a FIFO that never advances.
if ((_ssienr & 1) != 0)
{
- // A write following a read begins a new command/response frame.
- if (_lastOpWasRead)
+ // A write following a read begins a new command/response frame — but ONLY when the CS is
+ // deasserted. Inside a CS-held transaction, flash_do_cmd interleaves write/read per byte
+ // (0x4B Read Unique ID: cmd + 4 dummy + 8 data), so clearing the frame on every write-after-
+ // read would drop the command byte and every multi-byte read (unique id) would return zero.
+ if (_lastOpWasRead && !_csAsserted)
{
_rxFrame.Clear();
_lastOpWasRead = false;
@@ -279,6 +303,10 @@ private byte ComputeRxByte()
// Layout: [0xEB][A2][A1][A0][M][dummy][D0][D1]… (single-lane model of the quad read)
return ReadFlashByte(RxAddress24() + (uint)(pos - 6));
+ case CMD_READ_UNIQUE_ID when pos >= 5:
+ // Layout: [0x4B][dummy][dummy][dummy][dummy][D0]…[D7] — 64-bit flash unique id.
+ return (pos - 5) < 8 ? UniqueId[pos - 5] : (byte)0x00;
+
default:
return 0x00;
}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/Cyw43HttpRxTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/Cyw43HttpRxTests.cs
new file mode 100644
index 0000000..01903dc
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/Cyw43HttpRxTests.cs
@@ -0,0 +1,79 @@
+using System.Text;
+using FluentAssertions;
+using RP2040.Peripherals.Usb;
+using RP2040.TestKit;
+using RP2040Sharp.Wireless.Cyw43;
+using RP2040Sharp.IntegrationTests.Infrastructure;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Async RX regression (the "Pico 1 never gets the MQTT topic push" bug, at emulator level). A TCP/HTTP
+/// server on real MicroPython over the emulated CYW43439: the guest associates, gets a DHCP lease, listens
+/// on :80 and BLOCKS in accept() — asleep in WFI. The virtual gateway then connects and GETs; the guest
+/// must wake on the WL_HOST_WAKE (GPIO24) edge to receive the request and reply. Ported from the RP2350
+/// Cyw43HttpTests (which passes) to exercise the RP2040 GSpiSlave host-wake path.
+///
+public class Cyw43HttpRxTests(ITestOutputHelper output)
+{
+ private const string PicoW = "/Users/begeistert/Repos/micropython/ports/rp2/build-RPI_PICO_W/firmware.uf2";
+
+ [Fact]
+ public void Guest_http_server_is_reachable_over_the_virtual_network()
+ {
+ if (!File.Exists(PicoW)) { output.WriteLine("skip"); return; }
+ using var sim = RP2040TestSimulation.Create().WithBinary(Uf2Reader.ToFlashImage(File.ReadAllBytes(PicoW)));
+ sim.Rp2040.Pio0.ReadGpioIn = () => sim.Rp2040.IoBank0.GetInputWord();
+ sim.Rp2040.Pio1.ReadGpioIn = () => sim.Rp2040.IoBank0.GetInputWord();
+ sim.Rp2040.Sio.OnGpioChanged += () => sim.Rp2040.IoBank0.NotifyPads(0xFFFFFFFFu);
+
+ var dev = new Cyw43439Device(sim.Rp2040.IoBank0);
+ dev.Sdpcm.VisibleAps.Add(new Sdpcm.VirtualAp("RP2040Sharp-AP", [0x02, 0, 0x5E, 0, 4, 1], 6, -50, false));
+ var net = new VirtualNet(dev.Sdpcm);
+ string? httpResponse = null;
+ net.OnTcpClosed += data => httpResponse = Encoding.Latin1.GetString(data);
+ var diag = new List();
+ net.OnGuestFrame += (itf, et, len) => { if (diag.Count < 80) diag.Add($"FRAME et=0x{et:X4} len={len}"); };
+ net.OnTcpSegment += (fl, seq, ack, plen) => { if (diag.Count < 80) diag.Add($"TCP flags=0x{fl:X2} seq={seq} ack={ack} plen={plen}"); };
+
+ var cdc = new UsbCdcHost(sim.Rp2040.Usb);
+ var rx = new StringBuilder();
+ cdc.OnSerialData += d => rx.Append(Encoding.Latin1.GetString(d));
+
+ void Step(long max, Func done)
+ { for (long i = 0; i < max && !done(); i++) sim.Rp2040.Run(sim.Rp2040.Core0Waiting ? 1600 : 512); }
+
+ Step(120_000_000, () => rx.ToString().Contains(">>>"));
+
+ cdc.SendSerialBytes("\x01"u8);
+ cdc.SendSerialBytes(Encoding.ASCII.GetBytes(
+ "import network,time,socket\n" +
+ "w=network.WLAN(network.STA_IF)\nw.active(True)\nw.connect('RP2040Sharp-AP')\n" +
+ "while w.status()!=3:\n time.sleep_ms(50)\n" +
+ "s=socket.socket()\ns.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)\n" +
+ "s.bind(('0.0.0.0',80))\ns.listen(1)\nprint('LISTENING')\n" +
+ "cl,a=s.accept()\nreq=cl.recv(512)\n" +
+ "cl.send(b'HTTP/1.0 200 OK\\r\\nContent-Type: text/plain\\r\\nConnection: close\\r\\n\\r\\nHello from RP2040Sharp')\n" +
+ "cl.close()\nprint('SERVED')\n"));
+ cdc.SendSerialBytes("\x04"u8);
+
+ Step(2_000_000_000, () => rx.ToString().Contains("LISTENING"));
+ rx.ToString().Should().Contain("LISTENING", "the guest must reach an IP and bind its server socket");
+
+ net.TcpConnect(itf: 0, serverPort: 80,
+ Encoding.ASCII.GetBytes("GET / HTTP/1.0\r\nHost: 192.168.4.2\r\n\r\n"));
+
+ Step(2_000_000_000, () => httpResponse != null);
+
+ output.WriteLine(rx.ToString());
+ output.WriteLine("HTTP RESPONSE: " + httpResponse);
+ File.WriteAllText("/tmp/rp2040_http.txt", $"response={httpResponse}\nREPL:\n{rx}\nDIAG:\n " + string.Join("\n ", diag));
+
+ httpResponse.Should().NotBeNull("the TCP connection must complete and close with a response");
+ httpResponse.Should().Contain("200 OK");
+ httpResponse.Should().Contain("Hello from RP2040Sharp", "the guest HTTP body must arrive over the virtual TCP path");
+ sim.Cpu.IsLockedUp.Should().BeFalse();
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/FreeRtosKernelTest.cs b/tests/RP2040Sharp.IntegrationTests/Tests/FreeRtosKernelTest.cs
new file mode 100644
index 0000000..ef87271
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/FreeRtosKernelTest.cs
@@ -0,0 +1,25 @@
+using System; using System.IO; using RP2040.TestKit.Boards;
+namespace RP2040Sharp.IntegrationTests.Tests;
+/// FreeRTOS ported to PyMCU Python -- ALL subsystems at once: priority preemptive
+/// scheduling, FIFO queue (strict order), counting semaphore, software timer with
+/// callback, task notifications and event groups. Each must progress, the queue
+/// stays in strict FIFO order, no lockup.
+[Trait("Category","Integration")]
+public class FreeRtosKernelTest {
+ private const string Bin="/Users/begeistert/Repos/pymcu-nanort/dist/firmware.bin";
+ [Fact] public void Full_Kernel_AllSubsystems() {
+ using var pico=new PicoSimulation(withUsbCdc:false);
+ pico.LoadFlash(File.ReadAllBytes(Bin));
+ for(int i=0;i<1000;i++) pico.RunInstructions(5000);
+ var b=pico.Cpu.Bus;
+ uint qgot=b.ReadWord(0x20025060), qerr=b.ReadWord(0x20025064), sgot=b.ReadWord(0x20025070);
+ uint tmr=b.ReadWord(0x20025074), nt=b.ReadWord(0x20025078), ev=b.ReadWord(0x2002507C);
+ Assert.False(pico.Cpu.IsLockedUp, "no lockup");
+ Assert.True(qgot>10, $"queue FIFO items (got={qgot})");
+ Assert.Equal(0u, qerr);
+ Assert.True(sgot>5, $"semaphore takes (sgot={sgot})");
+ Assert.True(tmr>5, $"timer callbacks (tmr={tmr})");
+ Assert.True(nt>5, $"notifications (nt={nt})");
+ Assert.True(ev>3, $"event-group waits (ev={ev})");
+ }
+}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonReplTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonReplTests.cs
index dcc4ae3..4e60105 100644
--- a/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonReplTests.cs
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonReplTests.cs
@@ -91,4 +91,48 @@ public async Task Repl_MultipleCommands_ProduceCorrectOutput()
var found = runner.ExecuteAndWait("print(x + y)", "42");
found.Should().BeTrue("accumulated variable state should be preserved across REPL lines");
}
+
+ [Fact]
+ public async Task Repl_MachineUniqueId_IsNotAllZero()
+ {
+ // Regression: SsiPeripheral.UniqueId used to default to an all-zero byte[8], so
+ // machine.unique_id() always read back "0000000000000000" instead of a real per-chip id.
+ if (ShouldSkip) return;
+
+ await using var runner = await MicroPythonRunner.CreateAsync(Version);
+ if (runner is null) return;
+
+ runner.WaitForPrompt().Should().BeTrue();
+
+ runner.Execute("import machine, ubinascii; print(ubinascii.hexlify(machine.unique_id()))");
+ var found = runner.WaitForOutput(text => text.Contains("b'", StringComparison.Ordinal)
+ && !text.Contains("b'0000000000000000'", StringComparison.Ordinal));
+ found.Should().BeTrue("machine.unique_id() must not read back as all zero bytes");
+ }
+
+ [Fact]
+ public async Task Repl_MachineUniqueId_DiffersAcrossTwoBoards()
+ {
+ // Regression: two independently-booted boards both defaulted to the same all-zero
+ // unique id, so client_id = b"pico-" + hexlify(machine.unique_id()) collided between
+ // boards (e.g. an MQTT broker would drop the first connection when the second joined).
+ if (ShouldSkip) return;
+
+ await using var boardA = await MicroPythonRunner.CreateAsync(Version);
+ await using var boardB = await MicroPythonRunner.CreateAsync(Version);
+ if (boardA is null || boardB is null) return;
+
+ boardA.WaitForPrompt().Should().BeTrue();
+ boardB.WaitForPrompt().Should().BeTrue();
+
+ boardA.Execute("import machine, ubinascii; print(ubinascii.hexlify(machine.unique_id()))");
+ boardA.WaitForOutput("b'", 5_000).Should().BeTrue();
+ var idA = boardA.Uart.Text + boardA.UsbCdc.Text;
+
+ boardB.Execute("import machine, ubinascii; print(ubinascii.hexlify(machine.unique_id()))");
+ boardB.WaitForOutput("b'", 5_000).Should().BeTrue();
+ var idB = boardB.Uart.Text + boardB.UsbCdc.Text;
+
+ idA.Should().NotBe(idB, "two boards must not report the same machine.unique_id()");
+ }
}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/PyMcuVsMicroPythonBenchmark.cs b/tests/RP2040Sharp.IntegrationTests/Tests/PyMcuVsMicroPythonBenchmark.cs
new file mode 100644
index 0000000..a41e70c
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/PyMcuVsMicroPythonBenchmark.cs
@@ -0,0 +1,135 @@
+using System;
+using System.IO;
+using RP2040Sharp.IntegrationTests.Infrastructure;
+using RP2040.TestKit.Boards;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Head-to-head: the SAME MicroPython source (a tight GP25 toggle loop, no delay)
+/// run two ways on the identical RP2040 emulator:
+/// (1) compiled to a native binary by PyMCU, and
+/// (2) interpreted by real MicroPython firmware.
+/// Measures emulator instructions per GP25 edge for each and reports the speedup.
+///
+[Trait("Category", "Integration")]
+public class PyMcuVsMicroPythonBenchmark
+{
+ private readonly Xunit.Abstractions.ITestOutputHelper _out;
+ public PyMcuVsMicroPythonBenchmark(Xunit.Abstractions.ITestOutputHelper output) => _out = output;
+
+ private const string PyMcuBin =
+ "/Users/begeistert/Repos/pymcu-arm/examples/mp-blink-tight/dist/firmware.bin";
+
+ private const int MpIters = 4000;
+
+ // Bounded loop with markers so we can measure exactly the toggle phase, plus a
+ // GC report so we can read MicroPython's live RAM footprint.
+ private static readonly string Script =
+ "import gc, machine\n" +
+ "from machine import Pin\n" +
+ "led = Pin(25, Pin.OUT)\n" +
+ "gc.collect()\n" +
+ "print('MEM', gc.mem_alloc(), gc.mem_free())\n" +
+ "print('SSS')\n" +
+ "for i in range(" + MpIters + "):\n" +
+ " led.value(1)\n" +
+ " led.value(0)\n" +
+ "print('EEE')\n";
+
+ private const long Window = 4_000_000; // instruction window to measure over
+
+ // Hook SIO GPIO changes and count transitions of GP25 over a fixed window of
+ // instructions, after the toggle loop has started.
+ private static (long edges, long instrs) MeasureToggles(PicoSimulation pico)
+ {
+ var sio = pico.Rp2040.Sio;
+ bool prev = sio.GetGpioOut(25);
+ long edges = 0;
+ sio.OnGpioChanged = () =>
+ {
+ bool cur = sio.GetGpioOut(25);
+ if (cur != prev) { edges++; prev = cur; }
+ };
+
+ // Warm up: advance until the first GP25 edge (the loop is running).
+ long warm = 0;
+ while (edges == 0 && warm < 60_000_000) { pico.RunInstructions(50_000); warm += 50_000; }
+
+ long startEdges = edges;
+ long startInstr = pico.InstructionCount;
+ long ran = 0;
+ while (ran < Window) { pico.RunInstructions(100_000); ran += 100_000; }
+ long instrs = pico.InstructionCount - startInstr;
+ return (edges - startEdges, instrs);
+ }
+
+ [Fact]
+ public void Native_PyMcu_vs_Interpreted_MicroPython()
+ {
+ if (!File.Exists(PyMcuBin))
+ throw new InvalidOperationException(
+ $"PyMCU binary not built: {PyMcuBin}. Run `pymcu build` in examples/mp-blink-tight.");
+
+ // ── (1) PyMCU native binary ──
+ long pymcuEdges, pymcuInstr;
+ using (var pico = new PicoSimulation(withUsbCdc: false))
+ {
+ pico.LoadFlash(File.ReadAllBytes(PyMcuBin));
+ (pymcuEdges, pymcuInstr) = MeasureToggles(pico);
+ }
+ double pymcuInstrPerEdge = (double)pymcuInstr / Math.Max(1, pymcuEdges);
+
+ // ── (2) Real MicroPython interpreting the same source ──
+ var runner = MicroPythonRunner.CreateAsync("v1.21.0").GetAwaiter().GetResult();
+ if (runner is null)
+ throw new InvalidOperationException("MicroPython firmware unavailable (no network/cache).");
+
+ long mpEdges, mpInstr;
+ try
+ {
+ runner.WaitForPrompt();
+ runner.WriteFile("main.py", Script);
+ var sim = runner.Simulation;
+ // Soft reset (Ctrl-D): reboot and auto-run main.py. The Pico REPL is on
+ // USB-CDC; inject on both channels so the active one takes it.
+ runner.UsbCdc.Clear();
+ runner.UsbCdc.InjectString("\x04");
+ runner.Uart.InjectString("\x04");
+ // Channel-aware: run until the loop's start marker, then time it to the end.
+ if (!runner.WaitForOutput("MEM ", 30_000))
+ throw new InvalidOperationException("MicroPython never reported memory.");
+ // Parse "MEM " from the REPL channel.
+ string memChan = runner.UsbCdc.Text;
+ int mi = memChan.LastIndexOf("MEM ", StringComparison.Ordinal);
+ string memLine = memChan.Substring(mi).Split('\n')[0].Trim();
+ var parts = memLine.Split(' ');
+ long mpAlloc = long.Parse(parts[1]);
+ long mpFree = long.Parse(parts[2]);
+ _out.WriteLine($"MicroPython GC heap: alloc={mpAlloc} free={mpFree} total={mpAlloc + mpFree} bytes (+ static RAM/stack)");
+
+ if (!runner.WaitForOutput("SSS", 30_000))
+ throw new InvalidOperationException("MicroPython never reached the loop start marker.");
+ long sInstr = sim.InstructionCount;
+ if (!runner.WaitForOutput("EEE", 60_000))
+ throw new InvalidOperationException("MicroPython never finished the toggle loop.");
+ mpInstr = sim.InstructionCount - sInstr;
+ mpEdges = 2L * MpIters; // value(1)+value(0) per iteration = 2 edges
+ }
+ finally
+ {
+ runner.DisposeAsync().GetAwaiter().GetResult();
+ }
+ double mpInstrPerEdge = (double)mpInstr / Math.Max(1, mpEdges);
+
+ double speedup = mpInstrPerEdge / Math.Max(1e-9, pymcuInstrPerEdge);
+
+ _out.WriteLine("=== Tight GP25 toggle loop (same MicroPython source) ===");
+ _out.WriteLine($"PyMCU native : {pymcuInstrPerEdge,10:F2} instr/edge ({pymcuEdges} edges in {pymcuInstr} instr)");
+ _out.WriteLine($"MicroPython : {mpInstrPerEdge,10:F2} instr/edge ({mpEdges} edges in {mpInstr} instr)");
+ _out.WriteLine($"SPEEDUP : {speedup,10:F1}x (PyMCU native vs interpreted)");
+
+ Assert.True(pymcuInstrPerEdge < 10, "PyMCU native should toggle in a handful of instructions");
+ Assert.True(speedup > 50, $"PyMCU native must be far faster than the interpreter (was {speedup:F0}x)");
+ }
+}
diff --git a/tests/RP2040Sharp.Tests/Dma/DmaTests.cs b/tests/RP2040Sharp.Tests/Dma/DmaTests.cs
index d77c6bb..041efc5 100644
--- a/tests/RP2040Sharp.Tests/Dma/DmaTests.cs
+++ b/tests/RP2040Sharp.Tests/Dma/DmaTests.cs
@@ -267,5 +267,58 @@ public void TREQ_PERMANENT_ignores_dreq_registration()
f.Bus.ReadWord(0x20015000u).Should().Be(0x12345678u);
}
}
+
+ ///
+ /// Regression for the PIO RX DREQ → DMA pacing fix: injecting a word into a state machine's RX FIFO
+ /// must fire OnRxPush — the only trigger for Dma.ResumeDreq (wired in RP2040Machine).
+ /// Before the fix InjectRxData enqueued silently, so a DREQ-paced DMA channel never woke and stalled
+ /// BUSY forever. (Full end-to-end FIFO→DMA drain is exercised by the nanoFramework harness, which
+ /// runs a real PIO program that configures the SM — a bare machine leaves the RX FIFO unconfigured.)
+ ///
+ public class PioRxDreqPacing
+ {
+ [Fact]
+ public void InjectRxData_fires_OnRxPush_to_wake_a_paced_dma()
+ {
+ using var m = new RP2040Machine();
+ var pushes = new System.Collections.Generic.List<(int sm, uint val)>();
+ m.Pio0.OnRxPush += (sm, v) => pushes.Add((sm, v));
+
+ m.Pio0.InjectRxData(0, 0xCAFEBABEu);
+
+ pushes.Should().ContainSingle();
+ pushes[0].sm.Should().Be(0);
+ pushes[0].val.Should().Be(0xCAFEBABEu);
+ }
+
+ [Fact]
+ public void Injected_rx_words_drive_a_dreq_paced_channel_to_completion()
+ {
+ using var m = new RP2040Machine();
+ m.Pio0.InReset = false; // firmware releases the PIO via RESETS; do the same so RXF reads work
+ const int sm = 0;
+ const uint pio0Rxf0 = 0x50200020u; // PIO0 RXF0 — a read dequeues that SM's RX FIFO
+ const uint dst = 0x20001000u;
+ uint[] src = { 0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u, 0x55555555u };
+
+ // ch0: read PIO0 RXF0 (fixed), write dst (incrementing), paced by PIO0 RX SM0 DREQ (index 4).
+ m.Dma.WriteWord(READ_ADDR(0), pio0Rxf0);
+ m.Dma.WriteWord(WRITE_ADDR(0), dst);
+ m.Dma.WriteWord(TRANS_COUNT(0), (uint)src.Length);
+ m.Dma.WriteWord(CTRL_TRIG(0),
+ CTRL_EN | CTRL_DATA_SIZE_WORD | CTRL_INCR_WRITE | (4u << 15)); // TREQ_SEL = 4
+
+ // Each inject fires OnRxPush → ResumeDreq → one synchronous DMA beat reads the word out.
+ foreach (var w in src)
+ {
+ m.Pio0.InjectRxData(sm, w);
+ }
+
+ for (uint i = 0; i < src.Length; i++)
+ {
+ m.Bus.ReadWord(dst + i * 4).Should().Be(src[i]);
+ }
+ }
+ }
}
diff --git a/tests/RP2040Sharp.Tests/Rtc/RtcTests.cs b/tests/RP2040Sharp.Tests/Rtc/RtcTests.cs
index 3bda99d..a12f3f7 100644
--- a/tests/RP2040Sharp.Tests/Rtc/RtcTests.cs
+++ b/tests/RP2040Sharp.Tests/Rtc/RtcTests.cs
@@ -12,8 +12,8 @@ public class RtcTests
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 RTC_RTC1 = 0x18; // RTC_1 read reg = YEAR/MONTH/DAY (datasheet swaps read-reg layout vs SETUP)
+ private const uint RTC_RTC0 = 0x1C; // RTC_0 read reg = DOTW/HOUR/MIN/SEC
private const uint CTRL_ENABLE = 1u;
private const uint CTRL_ACTIVE = 1u << 1;
@@ -42,13 +42,13 @@ 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);
+ var rtc0 = rtc.ReadWord(RTC_RTC1);
((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);
+ var rtc1 = rtc.ReadWord(RTC_RTC0);
((rtc1 >> 24) & 0x7).Should().Be(1);
((rtc1 >> 16) & 0x1F).Should().Be(0); // hour
((rtc1 >> 8) & 0x3F).Should().Be(0); // min
@@ -66,8 +66,8 @@ public void CtrlLoad_Latches_Setup0_And_Setup1()
rtc.WriteWord(RTC_CTRL, CTRL_LOAD | CTRL_ENABLE);
- var rtc0 = rtc.ReadWord(RTC_RTC0);
- var rtc1 = rtc.ReadWord(RTC_RTC1);
+ var rtc0 = rtc.ReadWord(RTC_RTC1);
+ var rtc1 = rtc.ReadWord(RTC_RTC0);
((rtc0 >> 16) & 0xFFF).Should().Be(2025);
((rtc0 >> 8) & 0xF) .Should().Be(6);
@@ -104,7 +104,7 @@ public void Tick_125M_Cycles_Advances_One_Second()
rtc.Tick(125_000_000L);
- var rtc1 = rtc.ReadWord(RTC_RTC1);
+ var rtc1 = rtc.ReadWord(RTC_RTC0);
(rtc1 & 0x3F).Should().Be(1); // sec = 1
}
@@ -117,7 +117,7 @@ public void Tick_Disabled_Does_Not_Advance()
rtc.Tick(125_000_000L * 60);
- var rtc1 = rtc.ReadWord(RTC_RTC1);
+ var rtc1 = rtc.ReadWord(RTC_RTC0);
(rtc1 & 0x3F).Should().Be(0); // still 0
}
@@ -128,7 +128,7 @@ public void Tick_Rolls_Seconds_Into_Minutes()
rtc.Tick(125_000_000L * 60); // 60 seconds
- var rtc1 = rtc.ReadWord(RTC_RTC1);
+ var rtc1 = rtc.ReadWord(RTC_RTC0);
(rtc1 & 0x3F) .Should().Be(0); // sec = 0
((rtc1 >> 8) & 0x3F) .Should().Be(1); // min = 1
}
@@ -140,8 +140,8 @@ public void Tick_Rolls_Day_And_Increments_DayOfWeek()
rtc.Tick(125_000_000L * 86400); // 24 hours
- var rtc0 = rtc.ReadWord(RTC_RTC0);
- var rtc1 = rtc.ReadWord(RTC_RTC1);
+ var rtc0 = rtc.ReadWord(RTC_RTC1);
+ var rtc1 = rtc.ReadWord(RTC_RTC0);
(rtc0 & 0x1F) .Should().Be(2); // day = 2
((rtc1 >> 24) & 0x7) .Should().Be(2); // Tuesday
@@ -155,8 +155,8 @@ 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);
+ var rtc0 = rtc.ReadWord(RTC_RTC1);
+ var rtc1 = rtc.ReadWord(RTC_RTC0);
((rtc0 >> 16) & 0xFFF).Should().Be(2030);
((rtc0 >> 8) & 0xF) .Should().Be(12);