From df0830f45f53adda9931f603b0dc0d94fc6a72fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Mon, 29 Jun 2026 01:14:10 -0600 Subject: [PATCH 1/2] Fire OnRxPush from InjectRxData so injected RX data drives a DREQ-paced DMA A DMA channel paced by a PIO RX DREQ only advances when OnRxPush fires (the RP2040Machine wires OnRxPush -> Dma.ResumeDreq). InjectRxData enqueued without firing it, so injected words never woke a stalled channel. Add a DMA regression that drives a paced channel to completion from injected RX words. --- .../Peripherals/Pio/PioPeripheral.cs | 3 ++ tests/RP2040Sharp.Tests/Dma/DmaTests.cs | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+) 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/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]); + } + } + } } From 216efcbcb538c122b420f948417ae9bbc15201b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 8 Jul 2026 22:11:12 -0600 Subject: [PATCH 2/2] feat(peripherals): RTC/SSI fixes + DMA/CYW43/FreeRTOS test coverage Working-set of emulator refinements shipped as 1.0.1-beta.7: RTC and SSI peripheral corrections, plus new integration coverage (CYW43 HTTP RX, FreeRTOS kernel, MicroPython REPL, PyMCU-vs-MicroPython benchmark). Licensing is unchanged: the RP2040Sharp core is MIT and the TestKit / Wireless / NanoFramework.TestKit packages remain BUSL-1.1. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Peripherals/Rtc/RtcPeripheral.cs | 56 ++++---- .../Peripherals/Ssi/SsiPeripheral.cs | 32 ++++- .../Tests/Cyw43HttpRxTests.cs | 79 ++++++++++ .../Tests/FreeRtosKernelTest.cs | 25 ++++ .../Tests/MicroPythonReplTests.cs | 44 ++++++ .../Tests/PyMcuVsMicroPythonBenchmark.cs | 135 ++++++++++++++++++ tests/RP2040Sharp.Tests/Rtc/RtcTests.cs | 26 ++-- 7 files changed, 354 insertions(+), 43 deletions(-) create mode 100644 tests/RP2040Sharp.IntegrationTests/Tests/Cyw43HttpRxTests.cs create mode 100644 tests/RP2040Sharp.IntegrationTests/Tests/FreeRtosKernelTest.cs create mode 100644 tests/RP2040Sharp.IntegrationTests/Tests/PyMcuVsMicroPythonBenchmark.cs 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/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);