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

12 KiB
Raw Permalink Blame History

← Home · Exec Kernel

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

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

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

  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 — 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