diff --git a/src/RP2040Sharp/Core/Cpu/CortexM0Plus.cs b/src/RP2040Sharp/Core/Cpu/CortexM0Plus.cs
index ecad186..1f455fc 100644
--- a/src/RP2040Sharp/Core/Cpu/CortexM0Plus.cs
+++ b/src/RP2040Sharp/Core/Cpu/CortexM0Plus.cs
@@ -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;
}
@@ -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,
+ };
+
/// Returns true if an interrupt was taken (PC changed).
[MethodImpl(MethodImplOptions.NoInlining)]
private bool CheckForInterrupts()
@@ -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)]
diff --git a/src/RP2040Sharp/Core/Cpu/Instructions/SystemOps.cs b/src/RP2040Sharp/Core/Cpu/Instructions/SystemOps.cs
index 8a4f47c..3ae9520 100644
--- a/src/RP2040Sharp/Core/Cpu/Instructions/SystemOps.cs
+++ b/src/RP2040Sharp/Core/Cpu/Instructions/SystemOps.cs
@@ -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
diff --git a/src/RP2040Sharp/Peripherals/Ppb/PpbPeripheral.cs b/src/RP2040Sharp/Peripherals/Ppb/PpbPeripheral.cs
index f624bf7..6fb50cb 100644
--- a/src/RP2040Sharp/Peripherals/Ppb/PpbPeripheral.cs
+++ b/src/RP2040Sharp/Peripherals/Ppb/PpbPeripheral.cs
@@ -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;
}
}
@@ -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;
}
}
diff --git a/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonGpioIrqTests.cs b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonGpioIrqTests.cs
new file mode 100644
index 0000000..c7fef7a
--- /dev/null
+++ b/tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonGpioIrqTests.cs
@@ -0,0 +1,87 @@
+using FluentAssertions;
+using RP2040Sharp.IntegrationTests.Infrastructure;
+
+namespace RP2040Sharp.IntegrationTests.Tests;
+
+///
+/// Regression tests for GPIO interrupts raised from machine.Pin.irq.
+/// 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.
+///
+[Trait("Category", "Integration")]
+public sealed class MicroPythonGpioIrqTests
+{
+ private static bool ShouldSkip =>
+ Environment.GetEnvironmentVariable("SKIP_INTEGRATION_TESTS") == "1";
+
+ private const string Version = "v1.21.0";
+
+ ///
+ /// Registers a rising-edge IRQ callback on GP15, injects external edges via
+ /// , and verifies the
+ /// callback counted every edge and the REPL is still alive afterwards.
+ ///
+ [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");
+ }
+
+ ///
+ /// 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.
+ ///
+ [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");
+ }
+}