amiga-bootcamp/06_exec_os/interrupts.md
Ilia Sharin 59929047d4 exec_os: enrich all stubs to bootcamp-quality reference articles
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
2026-04-23 17:55:31 -04:00

404 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[← Home](../README.md) · [Exec Kernel](README.md)
# Interrupts — Levels, INTENA, AddIntServer, CIA Interrupts
## Overview
AmigaOS supports 7 hardware interrupt levels (68k IPL0IPL6) 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<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 | AUD0AUD3 | 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 15 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<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
```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