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
118 changes: 80 additions & 38 deletions src/RP2040Sharp/Core/Cpu/CortexM0Plus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ public void Reset()
Registers.C = false;
Registers.V = false;

// All IRQs reset to priority level 0 (each InterruptPrioritiesN is the bitmask of
// IRQs currently at that level; NVIC IPR writes move bits between buckets).
Registers.InterruptPriorities0 = 0x03FFFFFF;
Registers.InterruptPriorities1 = 0;
Registers.InterruptPriorities2 = 0;
Registers.InterruptPriorities3 = 0;
Registers.SHPR2 = 0;
Registers.SHPR3 = 0;

Cycles = 0;
}

Expand Down Expand Up @@ -479,6 +488,29 @@ public void SetInterrupt(int irq, bool pending)
public void TriggerPendSv() { Registers.PendingPendSV = true; Registers.InterruptsUpdated = true; }
public void TriggerHardFault() => ExceptionEntry(EXC_HARDFAULT);

private int IrqPriority(int irq)
{
var bit = 1u << irq;
if ((Registers.InterruptPriorities0 & bit) != 0) return 0;
if ((Registers.InterruptPriorities1 & bit) != 0) return 1;
if ((Registers.InterruptPriorities2 & bit) != 0) return 2;
return 3;
}

// ARMv6-M §B1.5.4: group priority of an exception. 256 = Thread mode base level
// (lower than any configurable priority), so anything pending can preempt it.
private int ExceptionPriority(uint exceptionNumber) => exceptionNumber switch
{
0 => 256,
EXC_NMI => -2,
EXC_HARDFAULT => -1,
EXC_SVCALL => (int)(Registers.SHPR2 >> 30),
EXC_PENDSV => (int)((Registers.SHPR3 >> 22) & 3),
EXC_SYSTICK => (int)(Registers.SHPR3 >> 30),
>= 16 => IrqPriority((int)exceptionNumber - 16),
_ => -3,
};

/// <summary>Returns true if an interrupt was taken (PC changed).</summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private bool CheckForInterrupts()
Expand All @@ -497,57 +529,67 @@ private bool CheckForInterrupts()
Registers.Waiting = false;
}

if (Registers.PRIMASK != 0 && !Registers.PendingNMI)
return false;

// NMI (priority -2, always takes over everything)
if (Registers.PendingNMI)
// ARMv6-M §B1.5.4: an exception only preempts when its group priority is strictly
// higher (numerically lower) than the current execution priority. Without this
// gate a level/held IRQ (e.g. IO_IRQ_BANK0 before the handler acks INTR) preempts
// its own handler after every instruction, the stack unwinds into RAM and the core
// ends up in a HardFault lockup loop. PRIMASK boosts execution priority to 0.
var execPriority = ExceptionPriority(Registers.IPSR);
if (Registers.PRIMASK != 0 && execPriority > 0)
execPriority = 0;

// Pick the highest-priority pending exception; ties resolve to the lowest
// exception number (candidates are considered in exception-number order).
uint bestExc = 0;
var bestPriority = execPriority;

if (Registers.PendingNMI && -2 < bestPriority)
{
Registers.PendingNMI = false;
Registers.Waiting = false;
ExceptionEntry(EXC_NMI);
return true;
bestExc = EXC_NMI;
bestPriority = -2;
}

// SVCall — only when triggered via SVC instruction
if (Registers.PendingSVCall && Registers.PRIMASK == 0)
if (Registers.PendingSVCall)
{
Registers.PendingSVCall = false;
Registers.Waiting = false;
ExceptionEntry(EXC_SVCALL);
return true;
var p = ExceptionPriority(EXC_SVCALL);
if (p < bestPriority) { bestExc = EXC_SVCALL; bestPriority = p; }
}

// SysTick
if (Registers.PendingSystick && Registers.PRIMASK == 0)
if (Registers.PendingPendSV)
{
Registers.PendingSystick = false;
Registers.Waiting = false;
ExceptionEntry(EXC_SYSTICK);
return true;
var p = ExceptionPriority(EXC_PENDSV);
if (p < bestPriority) { bestExc = EXC_PENDSV; bestPriority = p; }
}

// PendSV (lowest priority system exception)
if (Registers.PendingPendSV && Registers.PRIMASK == 0)
if (Registers.PendingSystick)
{
Registers.PendingPendSV = false;
Registers.Waiting = false;
ExceptionEntry(EXC_PENDSV);
return true;
var p = ExceptionPriority(EXC_SYSTICK);
if (p < bestPriority) { bestExc = EXC_SYSTICK; bestPriority = p; }
}

// Hardware IRQs
var pending = Registers.PendingInterrupts & Registers.EnabledInterrupts;
if (pending != 0 && Registers.PRIMASK == 0)
while (pending != 0)
{
var irq = System.Numerics.BitOperations.TrailingZeroCount(pending);
Registers.PendingInterrupts &= ~(1u << irq);
Registers.Waiting = false;
ExceptionEntry((uint)(irq + 16)); // IRQ0 = Exception 16
return true;
pending &= pending - 1;
var p = IrqPriority(irq);
if (p < bestPriority)
{
bestExc = (uint)(irq + 16); // IRQ0 = Exception 16
bestPriority = p;
}
}

return false;
if (bestExc == 0)
return false;

switch (bestExc)
{
case EXC_NMI: Registers.PendingNMI = false; break;
case EXC_SVCALL: Registers.PendingSVCall = false; break;
case EXC_PENDSV: Registers.PendingPendSV = false; break;
case EXC_SYSTICK: Registers.PendingSystick = false; break;
default: Registers.PendingInterrupts &= ~(1u << ((int)bestExc - 16)); break;
}
Registers.Waiting = false;
ExceptionEntry(bestExc);
return true;
}

[MethodImpl(MethodImplOptions.NoInlining)]
Expand Down
1 change: 1 addition & 0 deletions src/RP2040Sharp/Core/Cpu/Instructions/SystemOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ public static void Msr(ushort opcodeH1, CortexM0Plus cpu)

case 16: // PRIMASK
cpu.Registers.PRIMASK = value & 1;
cpu.Registers.InterruptsUpdated = true;
break;

case 20: // CONTROL
Expand Down
42 changes: 26 additions & 16 deletions src/RP2040Sharp/Peripherals/Ppb/PpbPeripheral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,13 @@ public void WriteWord(uint address, uint value)
break;

case SCB_SHPR2:
_cpu.Registers.SHPR2 = value;
_cpu.Registers.SHPR2 = value & 0xC0000000;
_cpu.Registers.InterruptsUpdated = true;
break;

case SCB_SHPR3:
_cpu.Registers.SHPR3 = value;
_cpu.Registers.SHPR3 = value & 0xC0C00000;
_cpu.Registers.InterruptsUpdated = true;
break;
}
}
Expand Down Expand Up @@ -234,19 +236,27 @@ private void SetPendingBits(uint mask)

private void UpdatePriorityBucket(int iprIdx, uint iprValue)
{
// Each InterruptPrioritiesN field holds 8 priority bytes (2 IPR registers).
// iprIdx 0-1 → InterruptPriorities0, 2-3 → InterruptPriorities1, etc.
var inBucket = (iprIdx & 1) << 4; // 0 or 16 bit shift within the 32-bit bucket
var mask = 0xFFFFu << inBucket;
var twoBytes = (iprValue & 0xC0C0u) << inBucket;

if (iprIdx < 2)
_cpu.Registers.InterruptPriorities0 = (_cpu.Registers.InterruptPriorities0 & ~(uint)mask) | (uint)twoBytes;
else if (iprIdx < 4)
_cpu.Registers.InterruptPriorities1 = (_cpu.Registers.InterruptPriorities1 & ~(uint)mask) | (uint)twoBytes;
else if (iprIdx < 6)
_cpu.Registers.InterruptPriorities2 = (_cpu.Registers.InterruptPriorities2 & ~(uint)mask) | (uint)twoBytes;
else
_cpu.Registers.InterruptPriorities3 = (_cpu.Registers.InterruptPriorities3 & ~(uint)mask) | (uint)twoBytes;
// Each InterruptPrioritiesN field is the bitmask of IRQs currently at priority
// level N; an IPR write moves its 4 IRQs to the level in bits 7:6 of each byte.
for (var b = 0; b < 4; b++)
{
var irq = iprIdx * 4 + b;
if (irq > 25) break;
var level = (int)((iprValue >> (b * 8 + 6)) & 3);
var bit = 1u << irq;

_cpu.Registers.InterruptPriorities0 &= ~bit;
_cpu.Registers.InterruptPriorities1 &= ~bit;
_cpu.Registers.InterruptPriorities2 &= ~bit;
_cpu.Registers.InterruptPriorities3 &= ~bit;
switch (level)
{
case 0: _cpu.Registers.InterruptPriorities0 |= bit; break;
case 1: _cpu.Registers.InterruptPriorities1 |= bit; break;
case 2: _cpu.Registers.InterruptPriorities2 |= bit; break;
default: _cpu.Registers.InterruptPriorities3 |= bit; break;
}
}
_cpu.Registers.InterruptsUpdated = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using FluentAssertions;
using RP2040Sharp.IntegrationTests.Infrastructure;

namespace RP2040Sharp.IntegrationTests.Tests;

/// <summary>
/// Regression tests for GPIO interrupts raised from <c>machine.Pin.irq</c>.
/// IO_IRQ_BANK0 is level-held by IoBank0 (re-asserted until the handler acks INTR),
/// so without ARMv6-M execution-priority gating the IRQ preempts its own handler
/// after every instruction and the core hard-faults from stack overflow.
/// </summary>
[Trait("Category", "Integration")]
public sealed class MicroPythonGpioIrqTests
{
private static bool ShouldSkip =>
Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1";

private const string Version = "v1.21.0";

/// <summary>
/// Registers a rising-edge IRQ callback on GP15, injects external edges via
/// <see cref="RP2040.Peripherals.Gpio.GpioPin.ForceInput"/>, and verifies the
/// callback counted every edge and the REPL is still alive afterwards.
/// </summary>
[Fact]
public async Task GpioIrq_ExternalRisingEdges_FireCallbackWithoutHardFault()
{
if (ShouldSkip) return;

await using var runner = await MicroPythonRunner.CreateAsync(Version);
if (runner is null) return;

runner.WaitForPrompt().Should().BeTrue("MicroPython must reach REPL");

runner.ExecuteAndWait("from machine import Pin", ">>> ").Should().BeTrue();
runner.ExecuteAndWait("n = 0", ">>> ").Should().BeTrue();
runner.ExecuteAndWait("exec('def cb(p):\\n global n\\n n += 1')", ">>> ").Should().BeTrue();
runner.ExecuteAndWait("Pin(15, Pin.IN).irq(cb, Pin.IRQ_RISING)", ">>> ")
.Should().BeTrue("registering the IRQ must return to the REPL");

var pin = runner.Simulation.Gpio[15];
for (var i = 0; i < 5; i++)
{
pin.ForceInput(true);
runner.Simulation.RunMilliseconds(5);
pin.ForceInput(false);
runner.Simulation.RunMilliseconds(5);
}

runner.ExecuteAndWait("print('edges =', n)", "edges =", 10_000)
.Should().BeTrue("the REPL must answer after the edges, so the core did not lock up");
var text = runner.UsbCdc.IsConnected ? runner.UsbCdc.Text : runner.Uart.Text;
text.Should().Contain("edges = 5", "the callback must run once per rising edge");
}

/// <summary>
/// A burst of edges much faster than the Python scheduler drains them must not
/// wedge the core; the REPL must survive and the counter must have advanced.
/// </summary>
[Fact]
public async Task GpioIrq_EdgeBurst_CoreSurvivesAndReplResponds()
{
if (ShouldSkip) return;

await using var runner = await MicroPythonRunner.CreateAsync(Version);
if (runner is null) return;

runner.WaitForPrompt().Should().BeTrue("MicroPython must reach REPL");

runner.ExecuteAndWait("from machine import Pin", ">>> ").Should().BeTrue();
runner.ExecuteAndWait("n = 0", ">>> ").Should().BeTrue();
runner.ExecuteAndWait("exec('def cb(p):\\n global n\\n n += 1')", ">>> ").Should().BeTrue();
runner.ExecuteAndWait("Pin(15, Pin.IN).irq(cb, Pin.IRQ_RISING | Pin.IRQ_FALLING)", ">>> ")
.Should().BeTrue();

var pin = runner.Simulation.Gpio[15];
for (var i = 0; i < 50; i++)
{
pin.ForceInput((i & 1) == 0);
runner.Simulation.RunMilliseconds(0.2);
}
runner.Simulation.RunMilliseconds(20);

runner.ExecuteAndWait("print('alive', n > 0)", "alive True", 10_000)
.Should().BeTrue("the REPL must stay responsive after an IRQ burst");
}
}
Loading