diff --git a/src/RP2040Sharp/Peripherals/Psm/PsmPeripheral.cs b/src/RP2040Sharp/Peripherals/Psm/PsmPeripheral.cs index 9d48d26..cac7c2f 100644 --- a/src/RP2040Sharp/Peripherals/Psm/PsmPeripheral.cs +++ b/src/RP2040Sharp/Peripherals/Psm/PsmPeripheral.cs @@ -16,10 +16,23 @@ public sealed class PsmPeripheral : IMemoryMappedDevice // All 17 subsystem bits (proc0..spi1) private const uint ALL_BITS = 0x0001FFFF; + // FRCE_OFF bit 16 = PROC1 (force Core 1 into its power-on reset state). + private const uint PROC1_BIT = 1u << 16; + private uint _frceOn; private uint _frceOff; private uint _wdsel; + /// + /// Raised when firmware releases Core 1 from forced-off state, i.e. clears the PROC1 + /// bit of FRCE_OFF after having set it. On real silicon this brings Core 1 out of + /// reset so it re-runs its bootrom. pico-sdk's multicore_reset_core1() — called + /// by MicroPython before every _thread.start_new_thread — does exactly this and + /// then blocks on the FIFO for Core 1's "ready" word, so + /// hooks this to reset Core 1 and inject that word. + /// + public Action? OnProc1Released; + public uint Size => 0x1000; public uint ReadWord(uint address) => address switch @@ -42,7 +55,15 @@ public void WriteWord(uint address, uint value) switch (address) { case FRCE_ON: _frceOn = value & ALL_BITS; break; - case FRCE_OFF: _frceOff = value & ALL_BITS; break; + case FRCE_OFF: + { + var previous = _frceOff; + _frceOff = value & ALL_BITS; + // PROC1 going set → clear releases Core 1 from reset (multicore_reset_core1). + if ((previous & PROC1_BIT) != 0 && (_frceOff & PROC1_BIT) == 0) + OnProc1Released?.Invoke(); + break; + } case WDSEL: _wdsel = value & ALL_BITS; break; } } diff --git a/src/RP2040Sharp/Peripherals/RP2040Machine.cs b/src/RP2040Sharp/Peripherals/RP2040Machine.cs index 1e93752..56a1978 100644 --- a/src/RP2040Sharp/Peripherals/RP2040Machine.cs +++ b/src/RP2040Sharp/Peripherals/RP2040Machine.cs @@ -152,6 +152,9 @@ public RP2040Machine(uint flashSize = 2 * 1024 * 1024, // PSM @ 0x40010000 (slot 4) Psm = new PsmPeripheral(); + // Firmware releasing PROC1 from FRCE_OFF is multicore_reset_core1(): reset Core 1 + // and signal its bootrom is ready so Core 0's blocking FIFO pop returns. + Psm.OnProc1Released = ResetCore1FromPsm; apb.Register(0x40010000, Psm); // IO_BANK0 @ 0x40014000 (slot 5) @@ -967,6 +970,24 @@ private void LaunchCore1(uint vtor, uint sp, uint entry) _core1Launched = true; } + /// + /// Invoked when firmware releases PROC1 from .FRCE_OFF, i.e. + /// calls pico-sdk's multicore_reset_core1() (MicroPython does this before every + /// _thread.start_new_thread). On real silicon Core 1 leaves reset, re-runs its + /// bootrom and pushes a "ready" word to Core 0, which is blocked in a FIFO pop. We + /// reproduce that here: stop and reset Core 1, clear the SIO launch handshake so the + /// following §2.8.3 sequence re-launches it, and inject the ready word to unblock Core 0. + /// Without this the register write is inert, Core 1 never signals, and Core 0 sleeps in + /// WFE forever. + /// + private void ResetCore1FromPsm() + { + _core1Launched = false; + Sio.ResetMulticoreLaunch(); + Cpu1.Reset(); + Sio.SignalCore1BootromReady(); + } + // ── Per-core PPB router ─────────────────────────────────────────── /// diff --git a/src/RP2040Sharp/Peripherals/Sio/SioPeripheral.cs b/src/RP2040Sharp/Peripherals/Sio/SioPeripheral.cs index 8c8012b..b22d409 100644 --- a/src/RP2040Sharp/Peripherals/Sio/SioPeripheral.cs +++ b/src/RP2040Sharp/Peripherals/Sio/SioPeripheral.cs @@ -629,6 +629,26 @@ public void ResetMulticoreLaunch() _launchSeqPos = 0; } + /// + /// Simulate Core 1's bootrom reporting "ready" after it is released from a PSM reset + /// (multicore_reset_core1()). On real silicon Core 1 comes out of reset, drains + /// its own inbound mailbox, then pushes a single 0 to Core 0. We reproduce that: clear + /// Core 1's RX FIFO and status flags, enqueue the 0 into Core 0's RX FIFO, and raise an + /// event so Core 0's blocking multicore_fifo_pop_blocking()/WFE returns. + /// + public void SignalCore1BootromReady() + { + // Core 1 drains its own inbound mailbox (Core0→Core1) as it reboots. + _fifo01.Clear(); + _wof1 = _roe1 = false; + + // Push the "ready" sentinel (0) into Core 0's RX FIFO (Core1→Core0) and wake it. + if (_fifo10.Count < FIFO_DEPTH) + _fifo10.Enqueue(0); + _cpu.SetInterrupt(15, true); // SIO_IRQ_PROC0: data available for Core 0 + _cpu.Registers.EventRegistered = true; // SEV: wake Core 0 out of WFE + } + // ── FIFO helpers ────────────────────────────────────────────────── private uint BuildFifoStatus(int coreId) diff --git a/tests/RP2040Sharp.IntegrationTests/Firmware/python/micropython-v1.28.0.uf2 b/tests/RP2040Sharp.IntegrationTests/Firmware/python/micropython-v1.28.0.uf2 new file mode 100644 index 0000000..86ec9fc Binary files /dev/null and b/tests/RP2040Sharp.IntegrationTests/Firmware/python/micropython-v1.28.0.uf2 differ diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonThreadTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonThreadTests.cs new file mode 100644 index 0000000..368f2f6 --- /dev/null +++ b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonThreadTests.cs @@ -0,0 +1,66 @@ +using FluentAssertions; +using RP2040Sharp.IntegrationTests.Infrastructure; + +namespace RP2040Sharp.IntegrationTests.Tests; + +/// +/// End-to-end tests for MicroPython's _thread module on the second core. +/// +/// MicroPython runs multicore_reset_core1() (force PROC1 off then on via +/// PSM.FRCE_OFF) before every _thread.start_new_thread, then blocks in +/// multicore_fifo_pop_blocking() waiting for Core 1's bootrom "ready" word. Before +/// the PSM-driven Core 1 reset was emulated, Core 0 slept in WFE forever and any _thread +/// program hung — this is what crashes the Wokwi pedestrian-crossing sketch (project +/// 469350452950129665) and hangs it under this emulator. +/// +[Trait("Category", "Integration")] +public sealed class MicroPythonThreadTests +{ + private static bool ShouldSkip => + Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1"; + + // v1.28.0: this is where MicroPython's _thread path runs multicore_reset_core1() before + // launching Core 1 (older builds such as v1.21.0 do not, so they cannot exercise the bug). + private const string Version = "v1.28.0"; + + /// + /// A _thread.start_new_thread must return (not hang in multicore_reset_core1's + /// blocking FIFO pop) and the spawned thread must actually run on Core 1. + /// + [Fact] + public async Task Thread_StartNewThread_RunsOnCore1_WithoutHanging() + { + if (ShouldSkip) return; + + await using var runner = await MicroPythonRunner.CreateAsync(Version); + if (runner is null) return; + + runner.WaitForPrompt().Should().BeTrue("MicroPython must reach the REPL to write files"); + + // Mirror the Wokwi crosswalk: a worker thread runs alongside the main loop. + const string mainPy = + "import _thread, time\n" + + "def worker():\n" + + " for i in range(3):\n" + + " print('CORE1', i)\n" + + " time.sleep(0.05)\n" + + "_thread.start_new_thread(worker, ())\n" + + "time.sleep(1)\n" + + "print('MAIN-DONE')\n"; + + runner.WriteFile("main.py", mainPy) + .Should().BeTrue("WriteFile should succeed when the REPL is ready"); + + runner.SoftReset(timeoutMs: 30_000) + .Should().BeTrue("MicroPython must return to the REPL after main.py — not hang at start_new_thread"); + + var text = runner.UsbCdc.IsConnected ? runner.UsbCdc.Text : runner.Uart.Text; + + // start_new_thread returned and the main thread ran to completion. + text.Should().Contain("MAIN-DONE", + "the main thread must continue past start_new_thread instead of blocking in WFE"); + // Core 1 actually executed the spawned thread body. + text.Should().Contain("CORE1", + "the worker thread must run on Core 1"); + } +} diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/VersionMatrixTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/VersionMatrixTests.cs index 3d065e2..5b5482d 100644 --- a/tests/RP2040Sharp.IntegrationTests/Tests/VersionMatrixTests.cs +++ b/tests/RP2040Sharp.IntegrationTests/Tests/VersionMatrixTests.cs @@ -23,6 +23,7 @@ public sealed class VersionMatrixTests ["v1.19.1"], ["v1.20.0"], ["v1.21.0"], + ["v1.28.0"], ]; [Theory] diff --git a/tests/RP2040Sharp.Tests/Multicore/MulticoreResetTests.cs b/tests/RP2040Sharp.Tests/Multicore/MulticoreResetTests.cs new file mode 100644 index 0000000..8d08eba --- /dev/null +++ b/tests/RP2040Sharp.Tests/Multicore/MulticoreResetTests.cs @@ -0,0 +1,67 @@ +using RP2040.Peripherals.Tests.Fixtures; + +namespace RP2040.Peripherals.Tests.Multicore; + +/// +/// Verifies the emulator reproduces pico-sdk's multicore_reset_core1(), which forces +/// PROC1 off then on through PSM.FRCE_OFF. On silicon this brings Core 1 out of reset; +/// Core 1 re-runs its bootrom and pushes a "ready" word to Core 0, which is blocked in +/// multicore_fifo_pop_blocking(). MicroPython's _thread module runs this before +/// every start_new_thread, so without it a _thread firmware (e.g. the Wokwi +/// pedestrian-crossing sketch) hangs with Core 0 asleep in WFE. +/// +public sealed class MulticoreResetTests : MachineTestBase +{ + // PSM @ 0x40010000. hw_set_bits/hw_clear_bits target the atomic SET/CLR aliases. + private const uint PSM_FRCE_OFF = 0x40010004; + private const uint PSM_FRCE_OFF_SET = 0x40012004; // base + SET(0x2000) + FRCE_OFF(0x04) + private const uint PSM_FRCE_OFF_CLR = 0x40013004; // base + CLR(0x3000) + FRCE_OFF(0x04) + private const uint PROC1_BIT = 1u << 16; + + // SIO @ 0xD0000000. FIFO_ST bit0 = VLD (Core 0's RX FIFO has data). + private const uint SIO_FIFO_ST = 0xD0000050; + private const uint SIO_FIFO_RD = 0xD0000058; + private const uint FIFO_ST_VLD = 1u; + + [Fact] + public void MulticoreResetCore1_InjectsReadyWord_ForBlockedPop() + { + // Core 0's RX FIFO starts empty (nothing available to a blocking pop). + (Machine.Bus.ReadWord(SIO_FIFO_ST) & FIFO_ST_VLD).Should().Be(0u, + "Core 0's RX FIFO must be empty before the reset"); + + // multicore_reset_core1(): hw_set_bits(PROC1), spin on read-back, hw_clear_bits(PROC1). + Machine.Bus.WriteWord(PSM_FRCE_OFF_SET, PROC1_BIT); + (Machine.Bus.ReadWord(PSM_FRCE_OFF) & PROC1_BIT).Should().Be(PROC1_BIT, + "the PROC1 force-off bit must read back as set (the SDK spins on this)"); + Machine.Bus.WriteWord(PSM_FRCE_OFF_CLR, PROC1_BIT); + + // Releasing PROC1 must make Core 1's bootrom push its "ready" sentinel to Core 0. + (Machine.Bus.ReadWord(SIO_FIFO_ST) & FIFO_ST_VLD).Should().Be(FIFO_ST_VLD, + "Core 1 leaving reset must signal Core 0 via the FIFO"); + Machine.Bus.ReadWord(SIO_FIFO_RD).Should().Be(0u, + "the ready sentinel Core 1 pushes is 0"); + } + + [Fact] + public void MulticoreResetCore1_WakesCore0FromWfe() + { + Machine.Cpu.Registers.EventRegistered = false; + + Machine.Bus.WriteWord(PSM_FRCE_OFF_SET, PROC1_BIT); + Machine.Bus.WriteWord(PSM_FRCE_OFF_CLR, PROC1_BIT); + + Machine.Cpu.Registers.EventRegistered.Should().BeTrue( + "the reset must SEV Core 0 so a WFE inside multicore_fifo_pop_blocking wakes"); + } + + [Fact] + public void SettingProc1_WithoutRelease_DoesNotSignalCore0() + { + // Only the set → clear transition releases Core 1; setting alone must be inert. + Machine.Bus.WriteWord(PSM_FRCE_OFF_SET, PROC1_BIT); + + (Machine.Bus.ReadWord(SIO_FIFO_ST) & FIFO_ST_VLD).Should().Be(0u, + "forcing PROC1 off must not push anything to Core 0"); + } +}