nimphea/nimphea_scoped_irq

Source   Edit  

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:

  1. Sharing data between main code and interrupt handlers (ISRs)
  2. Accessing hardware peripherals that are also used by ISRs
  3. 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:

  1. Constructor:
    • Save current PRIMASK (interrupt enable/disable state)
    • Call __disable_irq() (sets PRIMASK bit)
  2. 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
Compile-time examples (not executable without hardware)

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