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
23 changes: 22 additions & 1 deletion src/RP2040Sharp/Peripherals/Psm/PsmPeripheral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// 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 <c>multicore_reset_core1()</c> — called
/// by MicroPython before every <c>_thread.start_new_thread</c> — does exactly this and
/// then blocks on the FIFO for Core 1's "ready" word, so <see cref="RP2040Machine"/>
/// hooks this to reset Core 1 and inject that word.
/// </summary>
public Action? OnProc1Released;

public uint Size => 0x1000;

public uint ReadWord(uint address) => address switch
Expand All @@ -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;
}
}
Expand Down
21 changes: 21 additions & 0 deletions src/RP2040Sharp/Peripherals/RP2040Machine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -967,6 +970,24 @@ private void LaunchCore1(uint vtor, uint sp, uint entry)
_core1Launched = true;
}

/// <summary>
/// Invoked when firmware releases PROC1 from <see cref="PsmPeripheral"/>.FRCE_OFF, i.e.
/// calls pico-sdk's <c>multicore_reset_core1()</c> (MicroPython does this before every
/// <c>_thread.start_new_thread</c>). 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.
/// </summary>
private void ResetCore1FromPsm()
{
_core1Launched = false;
Sio.ResetMulticoreLaunch();
Cpu1.Reset();
Sio.SignalCore1BootromReady();
}

// ── Per-core PPB router ───────────────────────────────────────────

/// <summary>
Expand Down
20 changes: 20 additions & 0 deletions src/RP2040Sharp/Peripherals/Sio/SioPeripheral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,26 @@ public void ResetMulticoreLaunch()
_launchSeqPos = 0;
}

/// <summary>
/// Simulate Core 1's bootrom reporting "ready" after it is released from a PSM reset
/// (<c>multicore_reset_core1()</c>). 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 <c>multicore_fifo_pop_blocking()</c>/WFE returns.
/// </summary>
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)
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using FluentAssertions;
using RP2040Sharp.IntegrationTests.Infrastructure;

namespace RP2040Sharp.IntegrationTests.Tests;

/// <summary>
/// End-to-end tests for MicroPython's <c>_thread</c> module on the second core.
///
/// MicroPython runs <c>multicore_reset_core1()</c> (force PROC1 off then on via
/// <c>PSM.FRCE_OFF</c>) before every <c>_thread.start_new_thread</c>, then blocks in
/// <c>multicore_fifo_pop_blocking()</c> 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 <c>_thread</c>
/// program hung — this is what crashes the Wokwi pedestrian-crossing sketch (project
/// 469350452950129665) and hangs it under this emulator.
/// </summary>
[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";

/// <summary>
/// A <c>_thread.start_new_thread</c> must return (not hang in multicore_reset_core1's
/// blocking FIFO pop) and the spawned thread must actually run on Core 1.
/// </summary>
[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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public sealed class VersionMatrixTests
["v1.19.1"],
["v1.20.0"],
["v1.21.0"],
["v1.28.0"],
];

[Theory]
Expand Down
67 changes: 67 additions & 0 deletions tests/RP2040Sharp.Tests/Multicore/MulticoreResetTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using RP2040.Peripherals.Tests.Fixtures;

namespace RP2040.Peripherals.Tests.Multicore;

/// <summary>
/// Verifies the emulator reproduces pico-sdk's <c>multicore_reset_core1()</c>, which forces
/// PROC1 off then on through <c>PSM.FRCE_OFF</c>. 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
/// <c>multicore_fifo_pop_blocking()</c>. MicroPython's <c>_thread</c> module runs this before
/// every <c>start_new_thread</c>, so without it a <c>_thread</c> firmware (e.g. the Wokwi
/// pedestrian-crossing sketch) hangs with Core 0 asleep in WFE.
/// </summary>
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");
}
}
Loading