[← Home](../README.md) · [Exec Kernel](README.md) # Interrupts — Levels, INTENA, AddIntServer, CIA Interrupts ## Overview AmigaOS supports 7 hardware interrupt levels (68k IPL0–IPL6) plus a software interrupt mechanism. Custom chip interrupts are filtered through the `INTENA` / `INTREQ` registers; CIA-generated interrupts arrive on level 2 (CIA-A) and level 6 (CIA-B). The interrupt system is the foundation of all real-time behavior — audio DMA, vertical blank timing, keyboard input, and the scheduler itself all depend on it. --- ## Architecture ```mermaid graph TB subgraph "Hardware Sources" SER["Serial Port"] DSK["Disk Controller"] KBD["Keyboard (CIA-A)"] TMR["CIA Timers"] COP["Copper"] VBL["Vertical Blank"] BLT["Blitter"] AUD["Audio DMA"] EXT["External (CIA-B)"] end subgraph "Custom Chips" INTREQ["INTREQ
($DFF09C)"] INTENA["INTENA
($DFF09A)"] end subgraph "68k CPU" IPL["IPL0-IPL6
(priority encoder)"] VEC["Exception Vector
($64–$78)"] ISR["Exec Interrupt
Dispatcher"] end subgraph "Exec" CHAIN["Interrupt Server
Chain (per level)"] SOFT["Software Interrupt
Queue"] end SER --> INTREQ DSK --> INTREQ COP --> INTREQ VBL --> INTREQ BLT --> INTREQ AUD --> INTREQ KBD --> INTREQ EXT --> INTREQ INTREQ --> INTENA INTENA --> IPL IPL --> VEC VEC --> ISR ISR --> CHAIN CHAIN --> SOFT style INTREQ fill:#fff3e0,stroke:#ff9800,color:#333 style INTENA fill:#fff3e0,stroke:#ff9800,color:#333 style ISR fill:#e8f4fd,stroke:#2196f3,color:#333 ``` --- ## Interrupt Priority Levels | IPL | Source Bits | AmigaOS Use | Typical Latency | |---|---|---|---| | 1 | TBE, DSKBLK, SOFTINT | Software interrupts, serial TX, disk DMA complete | ~10 µs | | 2 | PORTS (CIA-A) | Keyboard, CIA-A timers, parallel port, floppy index | ~15 µs | | 3 | COPER, VERTB, BLIT | Copper, vertical blank, blitter done | ~10 µs | | 4 | AUD0–AUD3 | Audio channel DMA completion | ~10 µs | | 5 | RBF, DSKSYNC | Serial receive, disk sync word | ~8 µs | | 6 | EXTER (CIA-B) | CIA-B timers, external interrupts, TOD alarm | ~8 µs | | 7 | NMI | Non-maskable (unused on stock Amiga hardware) | — | Higher IPL = higher priority. A level 6 interrupt can preempt level 1–5 handlers. The CPU's SR (Status Register) mask bits determine which levels are currently enabled. ### Interrupt Sources per Level ```mermaid graph LR subgraph "Level 1" L1A["TBE
Serial TX"] L1B["DSKBLK
Disk DMA"] L1C["SOFTINT
Software"] end subgraph "Level 2" L2["PORTS
CIA-A"] end subgraph "Level 3" L3A["COPER
Copper"] L3B["VERTB
VBlank"] L3C["BLIT
Blitter"] end subgraph "Level 4" L4A["AUD0"] L4B["AUD1"] L4C["AUD2"] L4D["AUD3"] end subgraph "Level 5" L5A["RBF
Serial RX"] L5B["DSKSYNC
Disk sync"] end subgraph "Level 6" L6["EXTER
CIA-B"] end ``` --- ## Custom Chip Interrupt Registers | Register | Address | R/W | Description | |---|---|---|---| | `INTENAR` | `$DFF01C` | R | Interrupt enable status (read) | | `INTENA` | `$DFF09A` | W | Interrupt enable set/clear (write) | | `INTREQR` | `$DFF01E` | R | Interrupt request status (read) | | `INTREQ` | `$DFF09C` | W | Interrupt request clear/set (write) | ### INTENA / INTREQ Bit Map | Bit | Constant | Level | Source | |---|---|---|---| | 0 | `INTF_TBE` | 1 | Serial transmit buffer empty | | 1 | `INTF_DSKBLK` | 1 | Disk DMA block complete | | 2 | `INTF_SOFTINT` | 1 | Software interrupt | | 3 | `INTF_PORTS` | 2 | CIA-A interrupt (keyboard, timers) | | 4 | `INTF_COPER` | 3 | Copper interrupt | | 5 | `INTF_VERTB` | 3 | Vertical blank | | 6 | `INTF_BLIT` | 3 | Blitter finished | | 7 | `INTF_AUD0` | 4 | Audio channel 0 DMA done | | 8 | `INTF_AUD1` | 4 | Audio channel 1 DMA done | | 9 | `INTF_AUD2` | 4 | Audio channel 2 DMA done | | 10 | `INTF_AUD3` | 4 | Audio channel 3 DMA done | | 11 | `INTF_RBF` | 5 | Serial receive buffer full | | 12 | `INTF_DSKSYNC` | 5 | Disk sync word match | | 13 | `INTF_EXTER` | 6 | CIA-B / external interrupt | | 14 | `INTF_INTEN` | — | **Master interrupt enable** | | 15 | `INTF_SETCLR` | — | Set/Clear control bit (write only) | ### Enabling and Clearing Interrupts ```c /* Enable vertical blank interrupt: Bit 15 (SET) | Bit 14 (INTEN) | Bit 5 (VERTB) = $C020 */ custom.intena = INTF_SETCLR | INTF_INTEN | INTF_VERTB; /* Disable vertical blank (clear bit 5): */ custom.intena = INTF_VERTB; /* Bit 15 clear = CLEAR mode */ /* Acknowledge (clear) a vertical blank request: */ custom.intreq = INTF_VERTB; ``` > **Important**: You must write `INTF_SETCLR` (bit 15) to SET bits. Without it, you CLEAR them. This is a common source of bugs. --- ## Exec Interrupt Dispatch ### Server Chain vs Direct Handler Exec provides two models for handling interrupts: | Model | Function | Use Case | |---|---|---| | **Server chain** | `AddIntServer()` / `RemIntServer()` | Shared interrupts (multiple handlers per level) | | **Direct handler** | `SetIntVector()` | Exclusive interrupt ownership | For levels with multiple sources (VBL, PORTS), use the **server chain**. Each handler in the chain checks if the interrupt is for it and returns D0=0 (not mine) or D0≠0 (handled). ### Adding an Interrupt Server ```c /* Vertical blank interrupt server */ struct Interrupt myVBL; myVBL.is_Node.ln_Type = NT_INTERRUPT; myVBL.is_Node.ln_Pri = 0; /* Priority within this level */ myVBL.is_Node.ln_Name = "MyApp VBL"; myVBL.is_Data = myDataPtr; /* Passed in A1 */ myVBL.is_Code = myVBLFunc; /* Handler address */ AddIntServer(INTB_VERTB, &myVBL); /* INTB_VERTB = 5 */ /* ... application runs ... */ RemIntServer(INTB_VERTB, &myVBL); /* MUST remove before exit! */ ``` ### Interrupt Handler Implementation ```c /* C handler — called at interrupt level */ ULONG __saveds __interrupt MyVBLHandler( struct Interrupt *irq __asm("a1")) /* is_Data in A1 */ { struct MyData *data = (struct MyData *)irq; /* Do fast work only */ data->frameCount++; if (data->needUpdate) { data->needUpdate = FALSE; Signal(data->mainTask, data->updateSig); /* Wake main task */ } return 1; /* Handled — stop chain (for exclusive sources) Return 0 to let next server in chain try */ } ``` ### Assembly Handler (Maximum Performance) ```asm ; A1 = is_Data pointer ; Must preserve D2-D7, A2-A6 ; May trash D0, D1, A0, A1 MyVBLHandler: move.l (a1),a0 ; Load data pointer addq.l #1,FRAMECOUNT(a0) moveq #1,d0 ; Handled rts ``` --- ## Handler Rules | Rule | Reason | Consequence | |---|---|---| | **No `Wait()` or `WaitPort()`** | Can't sleep at interrupt level | System freeze | | **No `AllocMem()` / `FreeMem()`** | May internally `Wait()` | System freeze | | **No DOS calls** | DOS is not reentrant | Corruption | | **No Intuition calls** | May `Wait()` internally | Deadlock | | **Preserve D2-D7, A2-A6** | Calling convention | Register corruption | | **Minimize execution time** | Blocks all lower-priority interrupts | Audio glitches, serial data loss | | **Use `Signal()` for deferred work** | Only safe IPC from interrupt context | — | | **Always acknowledge the interrupt** | Write to INTREQ to clear the request | Infinite interrupt loop | --- ## CIA Interrupts CIA-A (`$BFE001`) generates level 2 (PORTS) interrupts. CIA-B (`$BFD000`) generates level 6 (EXTER) interrupts. Each CIA has an ICR (Interrupt Control Register) with 5 sources: | Bit | Source | CIA-A Use | CIA-B Use | |---|---|---|---| | 0 | Timer A underflow | Keyboard scan timer | System timer | | 1 | Timer B underflow | Available | Available | | 2 | TOD alarm | Real-time clock alarm | VSync counter | | 3 | Serial register (SP) | Keyboard data received | Available | | 4 | FLAG pin | Accent key / disk index | Available | ### CIA Interrupt Handler ```c /* CIA-A keyboard interrupt (level 2) */ struct Interrupt kbdHandler; kbdHandler.is_Code = KbdISR; kbdHandler.is_Data = kbdData; kbdHandler.is_Node.ln_Pri = 120; /* High priority within level 2 */ AddIntServer(INTB_PORTS, &kbdHandler); /* Level 2 = CIA-A */ ULONG __interrupt KbdISR(struct Interrupt *irq __asm("a1")) { UBYTE icr = ciaa.ciaicr; /* Read + acknowledge CIA-A interrupts */ if (icr & 0x08) /* Bit 3 = serial port (keyboard data) */ { UBYTE rawKey = ciaa.ciasdr; /* Process key... */ Signal(mainTask, keySig); return 1; /* Handled */ } return 0; /* Not ours — pass to next handler */ } ``` --- ## Software Interrupts Software interrupts run at level 1 priority but are scheduled by exec, not hardware. They're used for deferred interrupt processing — a hardware interrupt handler can queue a software interrupt to do longer processing at a lower priority: ```c /* Cause a software interrupt */ struct Interrupt softInt; softInt.is_Code = MySoftHandler; softInt.is_Data = myData; softInt.is_Node.ln_Pri = 0; /* -32, -16, 0, +16, +32 are typical */ Cause(&softInt); /* LVO -78 */ /* MySoftHandler runs at next opportunity (level 1) */ ``` ### Software Interrupt Priorities | Priority | Constant | Use | |---|---|---| | +32 | `SIH_PRIMOUSE` | Mouse/gameport processing | | +16 | — | High-priority deferred work | | 0 | — | Normal deferred work | | -16 | — | Low-priority deferred work | | -32 | `SIH_PRISERIAL` | Serial port processing | --- ## Disable / Enable vs Forbid / Permit | Function | Effect | Scope | Max Safe Duration | |---|---|---|---| | `Forbid()` | Disables task switching | Tasks only (interrupts still run) | ~100 ms | | `Permit()` | Re-enables task switching | Reverses `Forbid()` | — | | `Disable()` | Masks all hardware interrupts | Hardware + task switching | **~250 µs** | | `Enable()` | Unmasks hardware interrupts | Reverses `Disable()` | — | > **Caution**: `Disable()` / `Enable()` stop ALL hardware — serial data loss, audio DMA glitches, floppy read errors. Use only for accessing data structures shared between task and interrupt context. --- ## Pitfalls ### 1. Forgetting to Acknowledge ```c /* BUG — interrupt fires infinitely */ ULONG MyHandler(void) { DoWork(); return 1; /* Forgot: custom.intreq = INTF_VERTB; */ /* INTREQ bit still set → interrupt fires again immediately → system hangs */ } ``` > **Note**: For server-chain interrupts (AddIntServer), exec handles INTREQ acknowledgment. For `SetIntVector`, you must do it yourself. ### 2. RemIntServer After Handler Memory Freed ```c /* BUG — handler struct on stack */ void SetupVBL(void) { struct Interrupt vbl; /* ON STACK */ AddIntServer(INTB_VERTB, &vbl); /* Function returns — stack frame destroyed */ /* VBL interrupt fires → jumps to garbage → Guru */ } ``` ### 3. Spending Too Long in Handler ```c /* BUG — complex processing at interrupt level */ ULONG MyAudioHandler(void) { DecodeMP3Frame(buffer); /* Takes >250 µs on 68000 */ /* During this time, keyboard, serial, and disk are unserviced */ return 1; } /* CORRECT — signal task for heavy processing */ ULONG MyAudioHandler(void) { Signal(decoderTask, decodeSig); /* ~5 µs */ return 1; } ``` --- ## Best Practices 1. **Use server chains** (`AddIntServer`) for VBL, PORTS, EXTER — these are shared levels 2. **Always `RemIntServer`** before exit or freeing handler memory 3. **Keep handlers fast** — signal your main task for heavy work 4. **Use software interrupts** for deferred processing from high-priority hardware handlers 5. **Acknowledge interrupts** by writing to `INTREQ` when using `SetIntVector` 6. **Preserve registers** D2-D7, A2-A6 in your handler 7. **Use `Disable()`/`Enable()`** only to protect interrupt-shared data — never for general synchronization 8. **Set appropriate priority** in `is_Node.ln_Pri` — higher priority handlers check first --- ## References - NDK39: `hardware/intbits.h`, `hardware/cia.h`, `exec/interrupts.h` - ADCD 2.1: `AddIntServer`, `RemIntServer`, `SetIntVector`, `Cause`, `Disable`, `Enable` - See also: [CIA Chips](../01_hardware/common/cia_chips.md) — CIA timer and ICR details - See also: [Custom Registers](../01_hardware/ocs_a500/custom_registers.md) — INTENA/INTREQ register listing - See also: [Multitasking](multitasking.md) — how interrupts drive the scheduler - *Amiga ROM Kernel Reference Manual: Exec* — interrupts chapter