Complete rewrite of 14 exec_os articles from stubs to comprehensive deep-dive technical references with architecture diagrams, pitfalls, and best practices. New: multitasking.md (scheduler, IPC, memory safety, real-world scenarios) Enriched: exec_base, tasks_processes, library_system, library_vectors, interrupts, exceptions_traps, memory_management, message_ports, signals, semaphores, io_requests, lists_nodes, resident_modules Updated indexes: 06_exec_os/README.md, root README.md
12 KiB
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
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<br/>($DFF09C)"]
INTENA["INTENA<br/>($DFF09A)"]
end
subgraph "68k CPU"
IPL["IPL0-IPL6<br/>(priority encoder)"]
VEC["Exception Vector<br/>($64–$78)"]
ISR["Exec Interrupt<br/>Dispatcher"]
end
subgraph "Exec"
CHAIN["Interrupt Server<br/>Chain (per level)"]
SOFT["Software Interrupt<br/>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
graph LR
subgraph "Level 1"
L1A["TBE<br/>Serial TX"]
L1B["DSKBLK<br/>Disk DMA"]
L1C["SOFTINT<br/>Software"]
end
subgraph "Level 2"
L2["PORTS<br/>CIA-A"]
end
subgraph "Level 3"
L3A["COPER<br/>Copper"]
L3B["VERTB<br/>VBlank"]
L3C["BLIT<br/>Blitter"]
end
subgraph "Level 4"
L4A["AUD0"]
L4B["AUD1"]
L4C["AUD2"]
L4D["AUD3"]
end
subgraph "Level 5"
L5A["RBF<br/>Serial RX"]
L5B["DSKSYNC<br/>Disk sync"]
end
subgraph "Level 6"
L6["EXTER<br/>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
/* 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
/* 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 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)
; 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
/* 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:
/* 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
/* 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
/* 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
/* 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
- Use server chains (
AddIntServer) for VBL, PORTS, EXTER — these are shared levels - Always
RemIntServerbefore exit or freeing handler memory - Keep handlers fast — signal your main task for heavy work
- Use software interrupts for deferred processing from high-priority hardware handlers
- Acknowledge interrupts by writing to
INTREQwhen usingSetIntVector - Preserve registers D2-D7, A2-A6 in your handler
- Use
Disable()/Enable()only to protect interrupt-shared data — never for general synchronization - 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 — CIA timer and ICR details
- See also: Custom Registers — INTENA/INTREQ register listing
- See also: Multitasking — how interrupts drive the scheduler
- Amiga ROM Kernel Reference Manual: Exec — interrupts chapter