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
3 changes: 3 additions & 0 deletions src/RP2040Sharp/Peripherals/Pio/PioPeripheral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ───────────────────────────────────────
Expand Down
56 changes: 28 additions & 28 deletions src/RP2040Sharp/Peripherals/Rtc/RtcPeripheral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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
}

/// <summary>Inject a specific date/time into the RTC.</summary>
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 ─────────────────────────────────────────────────────────
Expand All @@ -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,
};

Expand All @@ -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
Expand Down Expand Up @@ -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++; }
Expand All @@ -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;
Expand Down
32 changes: 30 additions & 2 deletions src/RP2040Sharp/Peripherals/Ssi/SsiPeripheral.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using RP2040.Core.Memory;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -105,6 +107,20 @@ public sealed unsafe class SsiPeripheral : IMemoryMappedDevice
private readonly List<byte> _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 ────────────────────────────────────────────────────────────
Expand All @@ -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;
}

/// <summary>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
79 changes: 79 additions & 0 deletions tests/RP2040Sharp.IntegrationTests/Tests/Cyw43HttpRxTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
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<string>();
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<bool> 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();
}
}
25 changes: 25 additions & 0 deletions tests/RP2040Sharp.IntegrationTests/Tests/FreeRtosKernelTest.cs
Original file line number Diff line number Diff line change
@@ -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})");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()");
}
}
Loading
Loading