From 038cc867ba185f7a69b55c64398b35d5567711e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Sun, 12 Jul 2026 01:31:51 -0600 Subject: [PATCH] =?UTF-8?q?fix(cpu):=20ARMv6-M=20exception=20priority=20mo?= =?UTF-8?q?del=20=E2=80=94=20stop=20GPIO=20IRQ=20self-preemption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CheckForInterrupts took any pending+enabled exception with no notion of execution priority. A level-held IRQ like IO_IRQ_BANK0 (re-asserted by IoBank0 until the handler acks INTR) therefore re-preempted its own handler after every instruction; the stack unwound into RAM and the core died in a HardFault lockup loop the moment a MicroPython Pin.irq handler saw its first edge. Implement the ARMv6-M (B1.5.4) model: - NVIC IPR levels live in four per-level bitmask buckets, rebuilt on IPR writes (2-bit priorities in bits 7:6 of each byte); reset puts every IRQ at level 0. - Execution priority derives from IPSR (NMI=-2, HardFault=-1, SVCall/ PendSV/SysTick from SHPR2/3, IRQs from their bucket); PRIMASK boosts it to 0. SHPR2/3 writes are masked to their implemented bits. - A pending exception preempts only when strictly more urgent; ties resolve to the lowest exception number, matching NVIC arbitration. - MSR PRIMASK re-arms the interrupt check so unmasking takes effect immediately. Regression-tested with MicroPython v1.21: Pin.irq callbacks now count externally forced edges and a 50-edge burst leaves the REPL responsive. Claude-Session: https://claude.ai/code/session_01LzMZxpFZefZTzGPCfg2AHq --- src/RP2040Sharp/Core/Cpu/CortexM0Plus.cs | 118 ++++++++++++------ .../Core/Cpu/Instructions/SystemOps.cs | 1 + .../Peripherals/Ppb/PpbPeripheral.cs | 42 ++++--- .../Tests/MicroPythonGpioIrqTests.cs | 87 +++++++++++++ 4 files changed, 194 insertions(+), 54 deletions(-) create mode 100644 tests/RP2040Sharp.IntegrationTests/Tests/MicroPythonGpioIrqTests.cs 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"); + } +}