Scoped IRQ Blocker
RAII (Resource Acquisition Is Initialization) interrupt blocking for Daisy hardware.
This module provides safe, automatic interrupt management using C++'s RAII pattern. When a ScopedIrqBlocker object is created, interrupts are disabled. When the object goes out of scope (destroyed), interrupts are automatically restored to their previous state.
Why Disable Interrupts?
Interrupts can cause race conditions and data corruption when:
- Sharing data between main code and interrupt handlers (ISRs)
- Accessing hardware peripherals that are also used by ISRs
- Critical timing sections that must execute without interruption
Example problem without protection:
# Main code var counter = 0 counter = 100 # Step 1: Write 100 # [INTERRUPT OCCURS HERE - ISR increments counter to 101] # Step 2: Code expects counter = 100, but it's now 101!
Solution: Disable interrupts during critical section:
var blocker = ScopedIrqBlocker() # Interrupts disabled counter = 100 # blocker destroyed here - interrupts re-enabled automatically
RAII Pattern Advantages
Traditional approach (error-prone):
disableInterrupts() criticalOperation() enableInterrupts() # PROBLEM: If criticalOperation() fails, # this line might not execute!
RAII approach (automatic cleanup):
block: var blocker = ScopedIrqBlocker() # Interrupts disabled criticalOperation() # blocker destroyed automatically at end of block # Interrupts re-enabled even if criticalOperation() fails!
The RAII pattern guarantees interrupts are restored, even if:
- Early return from function
- Exception raised
- Code panics
Basic Usage
Example 1: Protect shared variable
var sharedCounter: uint32 = 0 proc incrementCounter() = # ISR might also modify sharedCounter block: var blocker = ScopedIrqBlocker() # Disable IRQs sharedCounter += 1 # Atomic operation # blocker destroyed - IRQs restored
Example 2: Protect hardware access
proc updateDisplay() = block: var blocker = ScopedIrqBlocker() # Disable IRQs # SPI bus also used by audio ISR - must protect spi.transmit(displayData.addr, 128) # blocker destroyed - IRQs restored
Example 3: Multiple operations
proc updateState() = block: var blocker = ScopedIrqBlocker() # All these operations are atomic state.position = newPosition state.velocity = newVelocity state.timestamp = getNow() # One atomic update - no partial state visible to ISRs
Nim Template Wrapper
This module provides a convenient withoutInterrupts template:
withoutInterrupts: # Code here runs with interrupts disabled sharedCounter += 1 hardwareRegister.write(value) # Interrupts automatically restored here
This is more idiomatic Nim than creating the blocker object manually.
Performance Considerations
Keep critical sections SHORT:
- ❌ BAD: Disable interrupts for milliseconds
- ✅ GOOD: Disable interrupts for microseconds
While interrupts are disabled:
- Audio callback cannot run (causes audio glitches)
- Timer interrupts are missed (timing drift)
- USB/UART cannot receive data (data loss)
Example: TOO LONG
withoutInterrupts: delay(10) # ❌ 10ms with no interrupts = audio glitches!
Example: APPROPRIATE
withoutInterrupts: register.bits = value # ✅ Single instruction, a few cycles
When to Use
✅ Use ScopedIrqBlocker when:
- Modifying variables shared with ISRs (audio callback, timers)
- Accessing hardware peripherals used by interrupts
- Reading/writing multi-word data (must be atomic)
- Critical timing sections (e.g., bit-banging protocols)
❌ Do NOT use when:
- Data is NOT shared with ISRs (unnecessary overhead)
- Already in an ISR (interrupts are already disabled)
- Operation takes > 100 microseconds (too long)
- Better synchronization exists (atomic operations, lock-free queues)
Complete Examples
Example 1: Ring buffer for audio samples
type RingBuffer = object data: array[1024, float32] writeIndex: uint32 readIndex: uint32 var buffer: RingBuffer proc audioCallback(input: ptr float32, output: ptr float32, size: int) = # ISR writes to buffer for i in 0..<size: withoutInterrupts: buffer.data[buffer.writeIndex] = input[i] buffer.writeIndex = (buffer.writeIndex + 1) mod 1024 proc processAudio() = # Main loop reads from buffer while true: var sample: float32 withoutInterrupts: sample = buffer.data[buffer.readIndex] buffer.readIndex = (buffer.readIndex + 1) mod 1024 processSample(sample)
Example 2: State machine with atomic transitions
type State = enum IDLE, ATTACK, DECAY, SUSTAIN, RELEASE var currentState = IDLE var stateStartTime: uint32 proc changeState(newState: State) = withoutInterrupts: # Atomic state transition currentState = newState stateStartTime = getNow() # Both values updated together - no race condition
Example 3: Hardware register sequence
proc configureSpi() = withoutInterrupts: # Multi-step hardware configuration must be atomic spiRegisters.CR1 = 0 # Disable SPI spiRegisters.CR2 = configValue spiRegisters.CR1 = SPI_ENABLE # Re-enable # Configuration complete before ISR can use SPI
Implementation Details
The C++ implementation uses ARM Cortex-M PRIMASK register:
- Constructor:
- Save current PRIMASK (interrupt enable/disable state)
- Call __disable_irq() (sets PRIMASK bit)
- Destructor:
- If interrupts were previously enabled (PRIMASK was 0)
- Call __enable_irq() (clears PRIMASK bit)
This ensures nesting works correctly:
# Interrupts initially enabled withoutInterrupts: # Disable (PRIMASK = 1) withoutInterrupts: # Already disabled, save state criticalOp() # Inner blocker: Don't re-enable (PRIMASK was 1) # Outer blocker: Re-enable (PRIMASK was 0)
See Also
- sys/system - Timing functions
- examples/system_control.nim - Interrupt protection examples
Types
ScopedIrqBlocker {.importcpp: "daisy::ScopedIrqBlocker".} = object
-
RAII interrupt blocker for critical sections.
When created, disables all interrupts (sets ARM PRIMASK). When destroyed, restores interrupts to previous state.
Usage:
block: var blocker = ScopedIrqBlocker() # IRQs disabled criticalOperation() # blocker destroyed - IRQs restored
Internal state:
- Saves previous PRIMASK value (interrupt enable state)
- Restores PRIMASK on destruction (if interrupts were enabled)
Note: Prefer the withoutInterrupts template for cleaner syntax.
Source Edit
Procs
proc initScopedIrqBlocker(): ScopedIrqBlocker {. importcpp: "daisy::ScopedIrqBlocker(@)", constructor, ...raises: [], tags: [], forbids: [].}
-
Create a ScopedIrqBlocker object (disables interrupts).
Returns: ScopedIrqBlocker instance
Side effect: Interrupts are disabled immediately
Example:
block: var blocker = initScopedIrqBlocker() sharedVariable = newValue # Protected from ISRs # blocker destroyed - interrupts restored
Note: Usually not called directly - use withoutInterrupts template.
Source Edit
Templates
template atomicBlock(body: untyped)
-
Alias for withoutInterrupts (alternative name).
Emphasizes that the code block executes atomically (no interruption).
Example:
atomicBlock: state.x = newX state.y = newY # Both updated together - no partial state visible to ISRs
Source Edit template criticalSection(body: untyped)
-
Alias for withoutInterrupts (alternative name).
Identical functionality to withoutInterrupts, just a different name. Use whichever reads better in your code.
Example:
criticalSection: hardwareRegister.write(value)
Source Edit template withoutInterrupts(body: untyped)
-
Execute code block with interrupts disabled (RAII protection).
This is the recommended way to use ScopedIrqBlocker in Nim.
Guarantees:
- Interrupts disabled before body executes
- Interrupts restored after body completes
- Restoration happens even if body returns early or raises
Parameters:
- body - Code to execute with interrupts disabled
Example:
var counter: uint32 = 0 withoutInterrupts: counter += 1 # Atomic increment # Interrupts restored here
Nested calls work correctly:
withoutInterrupts: operation1() withoutInterrupts: operation2() # Still protected operation3() # Interrupts restored only once at outermost level
Early return is safe:
proc example(): bool = withoutInterrupts: if errorCondition: return false # Interrupts still restored! criticalOp() return true
Source Edit