Expand documentation suite: 30+ articles enriched with diagrams, code examples, and hardware details

Graphics: text_fonts (bitmap layout, styles), sprites (DMA, multiplexing), gfx_base (chipset detection), rastport (draw modes, clipping), ham_ehb (mermaid fixes), display_modes (HAM palettes)

Devices: scsi (per-model interfaces, Gayle limits, CD-ROM, native vs vendor drivers), console (ANSI sequences, CON:/RAW:), parallel (CIA registers, pinout), timer (resource exhaustion), gameport (quadrature, XOR state)

Libraries: workbench (WBStartup, AppWindow/Icon/MenuItem), rexxsyslib (ARexx port hosting, command parsing), diskfont (font directory, colour fonts), keymap (rawkey codes, dead keys), locale (catalogue system, date formatting), layers (ClipRect, refresh types), utility (TagItem chains), icon (DiskObject, ToolTypes), iffparse (IFF structure, ByteRun1), expansion (Zorro AutoConfig)

Networking: tcp_ip_stacks (major rewrite - Amiga vs Unix architecture, SANA-II pipeline, PPP/SLIP dial-up, Ethernet cards, MiSTer), bsdsocket (pure API ref), sana2 (buffer hooks, driver requirements), protocols (all code examples). Deduplicated overlap between the three files.

Toolchain: debugging (Enforcer patterns, SnoopDOS, GDB remote, kprintf checklist), sasc (pragma encoding, __saveds idioms), stormc (NEW - StormC IDE, C++, PowerPC)

References: error_codes (DOS, Exec, trackdisk, Intuition error tables)
Driver development: rtg_driver (Native driver analysis, P96 tuning)

All 22 README indexes updated. Root README synced with stormc.md entry.
This commit is contained in:
Ilia Sharin 2026-04-23 21:37:26 -04:00
parent 0ded078134
commit f61c26b542
38 changed files with 6402 additions and 1065 deletions

View file

@ -9,12 +9,12 @@ Amiga devices are shared libraries with an exec I/O request interface. They prov
| File | Description |
|---|---|
| [trackdisk.md](trackdisk.md) | Floppy disk DMA: MFM encoding, track format, disk geometry, track caching, direct HW access |
| [scsi.md](scsi.md) | scsi.device / 2nd.scsi.device — hard disk I/O |
| [scsi.md](scsi.md) | Hard disk/CD-ROM I/O: per-model interfaces, Gayle bandwidth limits, native vs vendor drivers, HD_SCSICMD, CD-ROM commands, TD64/NSD 64-bit |
| [serial.md](serial.md) | UART/RS-232: CIA registers, baud rate calculation, serial debugging (KPrintF) |
| [parallel.md](parallel.md) | parallel.device — Centronics parallel port |
| [timer.md](timer.md) | CIA timers, E-clock, VBlank: delays, ReadEClock, periodic patterns, signal-based waiting |
| [parallel.md](parallel.md) | Centronics parallel port: CIA-A Port B mapping, hardware pinout, direct register access |
| [timer.md](timer.md) | CIA timers, E-clock, VBlank: delays, ReadEClock, periodic patterns, signal multiplexing, resource exhaustion |
| [audio.md](audio.md) | 4-channel DMA audio: hardware registers, priority allocation, double-buffering, modulation |
| [keyboard.md](keyboard.md) | CIA-A serial handshake, raw key codes, key matrix, reset sequence, FPGA protocol notes |
| [gameport.md](gameport.md) | gameport.device — joystick/mouse port reading |
| [gameport.md](gameport.md) | Joystick/mouse port: quadrature decoding, XOR state, fire buttons, controller types |
| [input.md](input.md) | Input handler chain: priority dispatch, event classes, key remapping, Commodities Exchange |
| [console.md](console.md) | console.device — text terminal I/O |
| [console.md](console.md) | Text terminal I/O: ANSI escape sequences, cursor/colour control, raw key events, CON:/RAW: handlers |

View file

@ -4,44 +4,241 @@
## Overview
`console.device` provides ANSI-compatible text rendering into Intuition windows. It translates raw keycodes to ASCII and supports a rich set of escape sequences for cursor positioning, colour, and text formatting.
`console.device` provides an ANSI-compatible text terminal within Intuition windows. It handles:
- **Input**: translating raw keycodes from `input.device` into ASCII/ANSI characters
- **Output**: rendering text, parsing escape sequences for cursor control, colour, and formatting
- **Clipboard**: cut/copy/paste integration
Every CLI/Shell window is backed by a console.device unit. Applications can open their own console units in any Intuition window for text I/O without implementing their own keyboard translation or cursor rendering.
```mermaid
flowchart LR
KB["Keyboard<br/>(raw keycodes)"] --> INPUT["input.device"]
INPUT --> CON["console.device"]
CON -->|"Read: ASCII chars"| APP["Application"]
APP -->|"Write: text + ESC sequences"| CON
CON -->|"Renders text via<br/>RastPort drawing"| WIN["Intuition Window"]
style CON fill:#e8f4fd,stroke:#2196f3,color:#333
style WIN fill:#c8e6c9,stroke:#2e7d32,color:#333
```
---
## Opening
```c
struct IOStdReq *con = CreateStdIO(port);
con->io_Data = (APTR)win; /* the Intuition Window */
struct MsgPort *conPort = CreateMsgPort();
struct IOStdReq *con = (struct IOStdReq *)
CreateIORequest(conPort, sizeof(struct IOStdReq));
/* Attach to an Intuition window: */
con->io_Data = (APTR)window;
con->io_Length = sizeof(struct Window);
OpenDevice("console.device", 0, (struct IORequest *)con, 0);
if (OpenDevice("console.device", CONU_STANDARD, (struct IORequest *)con, 0))
{
/* error — can't open console */
}
```
### Unit Types
| Unit | Constant | Description |
|---|---|---|
| 0 | `CONU_STANDARD` | Full-feature console with cursor and scrolling |
| 1 | `CONU_CHARMAP` | Character-mapped console (OS 3.0+) — faster for full-screen updates |
| 3 | `CONU_SNIPMAP` | Snip-mapped: supports clipboard cut/paste (OS 3.0+) |
| -1 | `CONU_LIBRARY` | Library mode — no window, just keymap translation |
---
## Writing Text and Escape Sequences
```c
/* Write text to the console window: */
void ConPuts(struct IOStdReq *con, char *str)
{
con->io_Command = CMD_WRITE;
con->io_Data = (APTR)str;
con->io_Length = -1; /* -1 = null-terminated */
DoIO((struct IORequest *)con);
}
/* Usage: */
ConPuts(con, "Hello, Amiga!\n");
ConPuts(con, "\033[1mBold text\033[0m\n"); /* bold on/off */
ConPuts(con, "\033[33mYellow text\033[0m\n"); /* colour */
ConPuts(con, "\033[10;20HText at row 10 col 20"); /* absolute position */
```
---
## Common Escape Sequences
## Reading Input
```c
/* Read characters (blocking): */
char buffer[256];
con->io_Command = CMD_READ;
con->io_Data = (APTR)buffer;
con->io_Length = sizeof(buffer);
DoIO((struct IORequest *)con);
/* con->io_Actual = number of bytes read */
/* Non-blocking read via SendIO + WaitPort: */
con->io_Command = CMD_READ;
con->io_Data = (APTR)buffer;
con->io_Length = 1; /* read 1 char at a time */
SendIO((struct IORequest *)con);
/* Wait for input alongside other events: */
ULONG consoleSig = 1 << conPort->mp_SigBit;
ULONG windowSig = 1 << window->UserPort->mp_SigBit;
ULONG sigs = Wait(consoleSig | windowSig);
if (sigs & consoleSig)
{
WaitIO((struct IORequest *)con);
char ch = buffer[0];
/* process character... */
}
```
---
## ANSI Escape Sequences
Console.device supports a rich subset of ANSI/VT100 escape sequences (CSI = `\033[` = ESC + `[`):
### Cursor Movement
| Sequence | Description | Example |
|---|---|---|
| `\033[nA` | Cursor up n lines | `\033[5A` = up 5 |
| `\033[nB` | Cursor down n lines | |
| `\033[nC` | Cursor right n columns | |
| `\033[nD` | Cursor left n columns | |
| `\033[y;xH` | Move to row y, column x (1-based) | `\033[1;1H` = home |
| `\033[H` | Home cursor (top-left) | |
| `\033[6n` | Report cursor position → replies `\033[y;xR` | |
| `\033[s` | Save cursor position | |
| `\033[u` | Restore cursor position | |
### Erasing
| Sequence | Description |
|---|---|
| `\033[H` | Home cursor |
| `\033[nA` | Cursor up n lines |
| `\033[nB` | Cursor down n lines |
| `\033[nC` | Cursor right n columns |
| `\033[nD` | Cursor left n columns |
| `\033[y;xH` | Move to row y, column x |
| `\033[J` | Clear to end of screen |
| `\033[K` | Clear to end of line |
| `\033[nm` | Set graphics rendition (colour/style) |
| `\033[0m` | Reset attributes |
| `\033[1m` | Bold |
| `\033[3m` | Italic |
| `\033[4m` | Underline |
| `\033[3037m` | Foreground colour |
| `\033[4047m` | Background colour |
| `\033[J` | Clear from cursor to end of screen |
| `\033[1J` | Clear from start of screen to cursor |
| `\033[2J` | Clear entire screen |
| `\033[K` | Clear from cursor to end of line |
| `\033[1K` | Clear from start of line to cursor |
| `\033[2K` | Clear entire line |
### Text Attributes (SGR)
| Sequence | Effect |
|---|---|
| `\033[0m` | Reset all attributes |
| `\033[1m` | **Bold** (high intensity) |
| `\033[3m` | *Italic* |
| `\033[4m` | <u>Underline</u> |
| `\033[7m` | Inverse video (swap fg/bg) |
| `\033[22m` | Normal intensity (cancel bold) |
| `\033[23m` | Cancel italic |
| `\033[24m` | Cancel underline |
### Colours
| Sequence | Foreground | Background |
|---|---|---|
| `\033[30m` / `\033[40m` | Black | Black |
| `\033[31m` / `\033[41m` | Red | Red |
| `\033[32m` / `\033[42m` | Green | Green |
| `\033[33m` / `\033[43m` | Yellow/Brown | Yellow/Brown |
| `\033[34m` / `\033[44m` | Blue | Blue |
| `\033[35m` / `\033[45m` | Magenta | Magenta |
| `\033[36m` / `\033[46m` | Cyan | Cyan |
| `\033[37m` / `\033[47m` | White | White |
| `\033[39m` / `\033[49m` | Default | Default |
> [!NOTE]
> Colour indices map to the **Intuition pen palette** of the window's screen, not absolute colours. Pen 0 = background, pen 1 = foreground by default.
### Amiga-Specific Extensions
| Sequence | Description |
|---|---|
| `\033[>1h` | Enable auto-scroll |
| `\033[>1l` | Disable auto-scroll |
| `\033[ p` | Enable cursor |
| `\033[0 p` | Disable cursor |
| `\033[t` / `\033[b` | Set top/bottom scroll margins |
| `\033[20h` | Linefeed mode (LF = CR+LF) |
---
## Raw Key Events
In addition to ASCII, console.device reports **special keys** as multi-byte escape sequences:
| Key | Sequence Received |
|---|---|
| Cursor Up | `\033[A` |
| Cursor Down | `\033[B` |
| Cursor Right | `\033[C` |
| Cursor Left | `\033[D` |
| Shift+Up | `\033[T` |
| Shift+Down | `\033[S` |
| F1F10 | `\033[0~` `\033[9~` |
| Shift+F1F10 | `\033[10~` `\033[19~` |
| Help | `\033[?~` |
---
## Proper Shutdown
```c
/* Must abort any pending read before closing: */
if (!CheckIO((struct IORequest *)con))
{
AbortIO((struct IORequest *)con);
WaitIO((struct IORequest *)con);
}
CloseDevice((struct IORequest *)con);
DeleteIORequest((struct IORequest *)con);
DeleteMsgPort(conPort);
```
---
## CON: and RAW: Handlers
The AmigaDOS file handlers `CON:` and `RAW:` are wrappers around console.device:
| Handler | Description |
|---|---|
| `CON:` | Line-buffered console — input is buffered until Enter is pressed. Supports line editing. |
| `RAW:` | Raw console — each keypress is delivered immediately. No line editing. |
```c
/* Open a CON: window from DOS: */
BPTR fh = Open("CON:0/0/640/200/My Window/CLOSE", MODE_OLDFILE);
FPuts(fh, "Type something: ");
char buf[80];
FGets(fh, buf, sizeof(buf));
Close(fh);
/* RAW: for unbuffered key-by-key input: */
BPTR raw = Open("RAW:0/0/640/200/Raw Input", MODE_OLDFILE);
/* Each Read returns immediately with 1 char */
```
---
## References
- NDK39: `devices/conunit.h`
- NDK39: `devices/conunit.h`, `devices/console.h`
- ADCD 2.1: console.device autodocs
- See also: [keyboard.md](keyboard.md) — raw keycode to console.device pipeline
- See also: [input.md](input.md) — input handler chain

View file

@ -1,64 +1,130 @@
[← Home](../README.md) · [Devices](README.md)
# gameport.device — Joystick and Mouse
# gameport.device — Joystick and Mouse Ports
## Overview
`gameport.device` reads joystick and mouse ports (active on port 1 for joystick, port 0 for mouse). Uses CIA and custom chip registers.
`gameport.device` provides access to the Amiga's two **controller ports** (mouse/joystick). The hardware reads controller state through custom chip registers `JOY0DAT`/`JOY1DAT` and the CIA-A port for fire buttons. Understanding the hardware protocol is critical for FPGA cores.
---
## Hardware Registers
| Register | Address | Controller | Function |
|---|---|---|---|
| `JOY0DAT` | `$DFF00A` | Port 1 (mouse) | Quadrature-encoded position data |
| `JOY1DAT` | `$DFF00C` | Port 2 (joystick) | Direction bits or quadrature data |
| `JOYTEST` | `$DFF036` | Both | Write to set counter test value |
| `POTGO` | `$DFF034` | Both | Proportional/paddle port control |
| `POTGOR` | `$DFF016` | Both | Read paddle/proportional values |
| CIA-A PRA | `$BFE001` | Both | Fire buttons (active low) |
### JOYxDAT Bit Layout
```
Bits 158: Y counter (or vertical quadrature)
Bits 70: X counter (or horizontal quadrature)
```
### Mouse Quadrature Decoding
For mouse input, the X/Y values are **quadrature counters** — they increment/decrement as the mouse moves:
```c
/* Read mouse delta: */
UWORD joy = custom->joy0dat; /* current counter */
WORD dx = (WORD)(joy & 0xFF) - (WORD)(prev_joy & 0xFF);
WORD dy = (WORD)(joy >> 8) - (WORD)(prev_joy >> 8);
/* dx/dy = movement since last read (signed, wraps at 255→0) */
prev_joy = joy;
```
### Joystick Digital Decoding
For digital joysticks, the direction bits are encoded:
```c
UWORD joy = custom->joy1dat;
BOOL right = joy & 0x0002;
BOOL left = (joy >> 1) ^ (joy & 0x0001); /* XOR of bit 1 and bit 0 */
BOOL down = joy & 0x0200;
BOOL up = (joy >> 9) ^ ((joy >> 8) & 0x0001);
/* Fire button — from CIA-A: */
BOOL fire = !(ciaa->ciapra & 0x80); /* port 2 button, active low */
/* Port 1 fire = bit 6 of CIAA PRA */
```
> [!IMPORTANT]
> The joystick directional decoding uses **XOR** between adjacent bits — not direct reads. This is because the hardware shares the same quadrature interface used for mice. This is a common source of bugs in FPGA implementations.
### Fire Buttons
| Button | Register | Bit | Port |
|---|---|---|---|
| Port 1 fire (left mouse) | CIA-A PRA `$BFE001` | 6 | Mouse port |
| Port 2 fire (joystick) | CIA-A PRA `$BFE001` | 7 | Joystick port |
| Port 1 middle/right | POTGOR `$DFF016` | 8, 10 | Mouse port |
| Port 2 second button | POTGOR `$DFF016` | 12, 14 | Joystick port |
---
## Using gameport.device (OS Level)
```c
struct MsgPort *gpPort = CreateMsgPort();
struct IOStdReq *gpReq = (struct IOStdReq *)
CreateIORequest(gpPort, sizeof(struct IOStdReq));
/* Open port 1 (joystick port): */
OpenDevice("gameport.device", 1, (struct IORequest *)gpReq, 0);
/* Set controller type: */
UBYTE type = GPCT_ABSJOYSTICK;
gpReq->io_Command = GPD_SETCTYPE;
gpReq->io_Data = (APTR)&type;
gpReq->io_Length = 1;
DoIO((struct IORequest *)gpReq);
/* Set trigger conditions: */
struct GamePortTrigger trigger;
trigger.gpt_Keys = GPTF_UPKEYS | GPTF_DOWNKEYS;
trigger.gpt_Timeout = 0; /* no timeout */
trigger.gpt_XDelta = 1; /* report on any movement */
trigger.gpt_YDelta = 1;
gpReq->io_Command = GPD_SETTRIGGER;
gpReq->io_Data = (APTR)&trigger;
gpReq->io_Length = sizeof(trigger);
DoIO((struct IORequest *)gpReq);
/* Read events: */
struct InputEvent ie;
gpReq->io_Command = GPD_READEVENT;
gpReq->io_Data = (APTR)&ie;
gpReq->io_Length = sizeof(ie);
SendIO((struct IORequest *)gpReq);
/* Wait for joystick event: */
Wait(1L << gpPort->mp_SigBit);
WaitIO((struct IORequest *)gpReq);
/* ie now contains joystick movement/button data */
```
---
## Controller Types
```c
/* devices/gameport.h */
#define GPCT_ALLOCATED -1 /* port is allocated */
#define GPCT_NOCONTROLLER 0 /* nothing connected */
#define GPCT_MOUSE 1 /* mouse */
#define GPCT_RELJOYSTICK 2 /* relative joystick (proportional) */
#define GPCT_ABSJOYSTICK 3 /* absolute joystick */
```
---
## Reading a Joystick
```c
struct IOStdReq *gp = CreateStdIO(port);
OpenDevice("gameport.device", 1, (struct IORequest *)gp, 0);
/* Set controller type: */
UBYTE type = GPCT_ABSJOYSTICK;
gp->io_Command = GPD_SETCTYPE;
gp->io_Data = &type;
gp->io_Length = 1;
DoIO((struct IORequest *)gp);
/* Set trigger conditions: */
struct GamePortTrigger trigger = {
GPTF_UPKEYS | GPTF_DOWNKEYS, /* report buttons */
10000, /* X delta timeout */
10000, /* Y delta timeout */
1, 1 /* X/Y delta threshold */
};
gp->io_Command = GPD_SETTRIGGER;
gp->io_Data = &trigger;
gp->io_Length = sizeof(trigger);
DoIO((struct IORequest *)gp);
/* Read events: */
struct InputEvent ie;
gp->io_Command = GPD_READEVENT;
gp->io_Data = &ie;
gp->io_Length = sizeof(ie);
DoIO((struct IORequest *)gp);
/* ie.ie_Code, ie.ie_position.ie_xy give button/direction */
CloseDevice((struct IORequest *)gp);
```
| Constant | Value | Device |
|---|---|---|
| `GPCT_MOUSE` | 1 | Standard Amiga mouse |
| `GPCT_RELJOYSTICK` | 2 | Relative joystick (proportional) |
| `GPCT_ABSJOYSTICK` | 3 | Absolute/digital joystick |
---
## References
- NDK39: `devices/gameport.h`
- NDK39: `devices/gameport.h`, `hardware/custom.h`, `hardware/cia.h`
- HRM: *Amiga Hardware Reference Manual* — Controller Ports chapter
- See also: [input.md](input.md) — input handler chain that receives gameport events
- See also: [keyboard.md](keyboard.md) — keyboard shares CIA-A interrupt infrastructure

View file

@ -4,33 +4,146 @@
## Overview
`parallel.device` provides I/O for the Amiga's Centronics-compatible parallel port, used primarily for printers.
`parallel.device` provides I/O for the Amiga's Centronics-compatible 25-pin parallel port. Primarily used for printers, it also serves as a general-purpose 8-bit data port for hardware projects, dongles, MIDI interfaces, and inter-machine transfers (Laplink-style).
The parallel port is directly connected to **CIA-A** Port B data register (`$BFE101`) with handshake lines on CIA-A and CIA-B.
```mermaid
flowchart LR
APP["Application"] -->|"CMD_WRITE<br/>CMD_READ"| PAR["parallel.device"]
PAR --> CIA["CIA-A Port B<br/>($BFE101)<br/>8 data bits"]
CIA --> PORT["DB-25 Connector<br/>(pins 2-9 = data)"]
PAR --> CIAB["CIA-B<br/>Handshake lines"]
style PAR fill:#e8f4fd,stroke:#2196f3,color:#333
style CIA fill:#c8e6c9,stroke:#2e7d32,color:#333
```
---
## Hardware Pinout
| Pin | Signal | Direction | Description |
|---|---|---|---|
| 1 | /STROBE | Output | Data strobe (active low) |
| 29 | D0D7 | I/O | 8-bit bidirectional data |
| 10 | /ACK | Input | Acknowledge from printer |
| 11 | BUSY | Input | Printer busy |
| 12 | POUT | Input | Paper out |
| 13 | SELECT | Input | Printer selected |
| 1417 | — | — | Reserved / unused |
| 1825 | GND | — | Ground |
---
## Opening
```c
struct MsgPort *parPort = CreateMsgPort();
struct IOExtPar *par = (struct IOExtPar *)
CreateIORequest(port, sizeof(struct IOExtPar));
OpenDevice("parallel.device", 0, (struct IORequest *)par, 0);
CreateIORequest(parPort, sizeof(struct IOExtPar));
if (OpenDevice("parallel.device", 0, (struct IORequest *)par, 0))
{
Printf("Cannot open parallel.device\n");
/* Port may be in use by another application (printer, etc.) */
}
```
> [!NOTE]
> Only **one** application can have the parallel port open at a time. If the port is busy (e.g., printer spooler has it), `OpenDevice` will fail. This is unlike serial.device which supports shared modes.
---
## Commands
| Code | Constant | Description |
|---|---|---|
| 2 | `CMD_READ` | Read bytes (if bidirectional) |
| 3 | `CMD_WRITE` | Write bytes |
| 5 | `CMD_CLEAR` | Clear buffers |
| 8 | `CMD_FLUSH` | Abort pending |
| 9 | `PDCMD_QUERY` | Get status |
| 10 | `PDCMD_SETPARAMS` | Set parameters |
| 2 | `CMD_READ` | Read bytes (bidirectional mode) |
| 3 | `CMD_WRITE` | Write bytes to the port |
| 5 | `CMD_CLEAR` | Clear internal buffers |
| 7 | `CMD_RESET` | Reset the device |
| 8 | `CMD_FLUSH` | Abort all pending I/O |
| 9 | `PDCMD_QUERY` | Get port status (busy, paper out, etc.) |
| 10 | `PDCMD_SETPARAMS` | Configure port parameters |
### Writing Data
```c
/* Send data to the parallel port: */
char printData[] = "Hello, Printer!\r\n";
par->IOPar.io_Command = CMD_WRITE;
par->IOPar.io_Data = printData;
par->IOPar.io_Length = sizeof(printData) - 1;
DoIO((struct IORequest *)par);
if (par->IOPar.io_Error)
Printf("Write error: %ld\n", par->IOPar.io_Error);
```
### Querying Status
```c
par->IOPar.io_Command = PDCMD_QUERY;
DoIO((struct IORequest *)par);
UBYTE status = par->io_Status;
if (status & IOPTF_PARBUSY) Printf("Printer busy\n");
if (status & IOPTF_PAPEROUT) Printf("Paper out\n");
if (status & IOPTF_PARSEL) Printf("Printer selected\n");
```
### Setting Parameters
```c
/* Configure for custom use (e.g., no handshaking): */
par->IOPar.io_Command = PDCMD_SETPARAMS;
par->io_ParFlags = PARF_SHARED; /* shared access (OS 2.0+) */
DoIO((struct IORequest *)par);
```
---
## Direct Hardware Access
For hardware projects that need precise control (bypassing the device driver):
```c
/* Direct CIA-A Port B access: */
volatile UBYTE *ciaaPrb = (UBYTE *)0xBFE101; /* data register */
volatile UBYTE *ciaaDdrb = (UBYTE *)0xBFE301; /* data direction */
/* Set all 8 bits as output: */
*ciaaDdrb = 0xFF;
/* Write a byte: */
*ciaaPrb = 0x42;
/* Set as input: */
*ciaaDdrb = 0x00;
UBYTE value = *ciaaPrb;
```
> [!CAUTION]
> Direct hardware access bypasses the OS completely. Do not mix direct register access with parallel.device operations — close the device first or don't open it at all.
---
## Common Uses
| Use Case | Method |
|---|---|
| Printer output | `CMD_WRITE` through printer.device (which opens parallel.device) |
| Hardware dongle | Direct CIA-A Port B register read |
| MIDI interface | Parallel-to-MIDI adapter with custom timing |
| Amiga-to-PC transfer | Laplink cable with ParNet or ParBench software |
| Sampling hardware | ADC/DAC connected to data lines |
---
## References
- NDK39: `devices/parallel.h`
- ADCD 2.1: parallel.device autodocs
- See also: [serial.md](serial.md) — serial port I/O
- See also: [cia_chips.md](../01_hardware/common/cia_chips.md) — CIA register details

View file

@ -1,66 +1,283 @@
[← Home](../README.md) · [Devices](README.md)
# scsi.device — Hard Disk I/O
# scsi.device — Hard Disk, CD-ROM, and Mass Storage I/O
## Overview
SCSI and IDE hard disks are accessed via `scsi.device` (A3000/A4000 built-in) or `2nd.scsi.device` / `ide.device` (A1200/A600). The API is the same as trackdisk for basic read/write, with additional SCSI direct commands.
SCSI and IDE hard disks on the Amiga are accessed through `scsi.device` or compatible device drivers. The API is consistent across implementations — the same IORequest structure and commands work regardless of whether the underlying hardware is Commodore's native Gayle IDE, a Zorro SCSI card, or an accelerator-integrated controller.
---
## Disk Interfaces by Amiga Model
| Model | Interface | Controller | Device Name | Bandwidth Limit |
|---|---|---|---|---|
| A500 | None (stock) | — | — | Requires external SCSI card |
| A500+ | None (stock) | — | — | Requires external SCSI card |
| A600 | IDE (44-pin) | **Gayle** | `scsi.device` | ~1.5 MB/s (PIO, 16-bit CIA) |
| A1000 | None (stock) | — | — | Requires SCSI sidecar |
| A1200 | IDE (44-pin) | **Gayle** | `scsi.device` | ~1.5 MB/s (PIO) |
| A2000 | None (stock) | — | — | Zorro II SCSI cards |
| A3000 | SCSI (50-pin) | **WD33C93** | `scsi.device` | ~3 MB/s (DMA) |
| A4000 | IDE (40-pin) | **A4000 IDE** | `scsi.device` | ~2 MB/s (PIO) |
| A4000T | SCSI (50-pin) + IDE | **NCR 53C710** + IDE | `2nd.scsi.device` / `scsi.device` | ~10 MB/s (SCSI DMA) |
| CD32 | IDE (internal CD) | **Akiko** | `scsi.device` | ~1.5 MB/s |
| CDTV | SCSI (internal CD) | Custom | `scsi.device` | Slow |
### Why Native Bandwidth Is Limited
The A600/A1200 **Gayle** IDE interface is the most common and the most constrained:
```mermaid
flowchart LR
CPU["68020/68030<br/>(fast bus)"] -->|"PIO transfer<br/>WORD at a time"| GAYLE["Gayle IDE<br/>(no DMA!)"]
GAYLE -->|"16-bit ATA"| DRIVE["IDE Drive"]
style GAYLE fill:#ffcdd2,stroke:#c62828,color:#333
```
| Bottleneck | Explanation |
|---|---|
| **No DMA** | Gayle has no DMA engine — every word must be moved by the CPU (`MOVE.W` loop) |
| **16-bit bus** | Gayle connects via 16-bit data path even on 32-bit CPUs |
| **PIO mode 0** | Stock Gayle supports only PIO mode 0 (~3.3 MB/s theoretical, ~1.5 MB/s actual) |
| **CIA timing** | CIA chip access introduces wait states |
| **CPU overhead** | 100% CPU utilisation during transfers — no multitasking during disk I/O |
### Community Solutions — Fast IDE
| Solution | Method | Improvement |
|---|---|---|
| **FastATA** (E-Matrix/Elbox) | Zorro SCSI/IDE card with DMA | Up to ~10 MB/s, frees CPU |
| **Buddha/Catweasel** | Clock port / Zorro IDE with optimised driver | ~23 MB/s, multiple IDE channels |
| **Blizzard SCSI** | Accelerator-integrated SCSI | Up to ~5 MB/s with DMA |
| **GVP Series II** | Zorro II SCSI with custom DMA ASIC | ~3 MB/s, DMA frees CPU |
| **A4091** | Zorro III SCSI (NCR 53C710) | **~10 MB/s** — fastest stock Amiga SCSI |
| **CF adapters** | CompactFlash in IDE socket | Same speed, but no mechanical seek latency |
| **SD/microSD** | Adapter on clock port | Varies, typically slow but no noise |
---
## Native vs. Vendor Drivers — Software Differences
All drivers expose the same `CMD_READ`/`CMD_WRITE`/`HD_SCSICMD` API, but internal differences matter:
| Aspect | Commodore `scsi.device` | Vendor Drivers (GVP, A4091, etc.) |
|---|---|---|
| **Source** | Commodore ROM or Devs: | Third-party (ships with hardware) |
| **DMA support** | None (Gayle PIO) | Often DMA-capable |
| **Interrupt handling** | Level 2 interrupt (CIA) | Card-specific interrupt level |
| **TD64 / NSD** | Added via patches | Often built-in from factory |
| **Multi-LUN** | Usually ignored | Proper LUN scanning on SCSI |
| **Disconnect/reselect** | Not supported (Gayle) | SCSI disconnect on good controllers |
| **Tagged queuing** | No | Some advanced SCSI controllers |
| **CD-ROM support** | Basic (atapi.device) | Full ATAPI/SCSI CD support |
| **Removable media** | Basic change detection | Proper unit attention handling |
> [!IMPORTANT]
> When writing software that accesses disks, **always use the device name from the mountlist** — don't hardcode `scsi.device`. Different systems use different device names for the same physical drive.
---
## Opening
```c
struct IOStdReq *scsi = CreateStdIO(port);
OpenDevice("scsi.device", 0, (struct IORequest *)scsi, 0);
/* unit 0 = first SCSI/IDE device */
struct MsgPort *diskPort = CreateMsgPort();
struct IOStdReq *diskReq = (struct IOStdReq *)
CreateIORequest(diskPort, sizeof(struct IOStdReq));
/* Unit 0 = first device on the bus (master/ID 0): */
if (OpenDevice("scsi.device", 0, (struct IORequest *)diskReq, 0))
{
Printf("Cannot open scsi.device unit 0\n");
}
```
### Units
| Unit | SCSI Meaning | IDE Meaning |
|---|---|---|
| 0 | SCSI ID 0 | Master |
| 1 | SCSI ID 1 | Slave |
| 26 | SCSI ID 26 | N/A |
| 7 | Host adapter (reserved) | N/A |
---
## Standard Commands
Same as trackdisk: `CMD_READ`, `CMD_WRITE`, `CMD_UPDATE`, `TD_CHANGENUM`, etc.
```c
/* Read 512 bytes from byte offset on disk: */
diskReq->io_Command = CMD_READ;
diskReq->io_Data = buffer;
diskReq->io_Length = 512;
diskReq->io_Offset = 0; /* byte offset */
DoIO((struct IORequest *)diskReq);
/* Write: */
diskReq->io_Command = CMD_WRITE;
diskReq->io_Data = buffer;
diskReq->io_Length = 512;
diskReq->io_Offset = 512;
DoIO((struct IORequest *)diskReq);
/* Flush write cache: */
diskReq->io_Command = CMD_UPDATE;
DoIO((struct IORequest *)diskReq);
```
> [!IMPORTANT]
> The `io_Offset` field is a `ULONG` (32-bit), limiting addressable space to **4 GB**. For larger drives, use TD64/NSD 64-bit commands.
---
## Direct SCSI Commands (HD_SCSICMD)
For operations beyond read/write — device identification, mode pages, CD-ROM commands:
```c
#include <devices/scsidisk.h>
struct SCSICmd scsicmd;
UBYTE cdb[10]; /* SCSI CDB */
UBYTE sense[20]; /* sense data buffer */
UBYTE data[512]; /* data buffer */
UBYTE cdb[10], sense[20], data[512];
/* Read 1 sector at LBA 0: */
cdb[0] = 0x28; /* READ(10) */
cdb[1] = 0;
cdb[2] = 0; cdb[3] = 0; cdb[4] = 0; cdb[5] = 0; /* LBA = 0 */
cdb[6] = 0;
cdb[7] = 0; cdb[8] = 1; /* transfer length = 1 sector */
cdb[9] = 0;
/* READ(10) — read 1 sector at LBA 0: */
memset(cdb, 0, sizeof(cdb));
cdb[0] = 0x28; /* READ(10) opcode */
cdb[7] = 0; cdb[8] = 1; /* transfer length = 1 sector */
scsicmd.scsi_Data = (UWORD *)data;
scsicmd.scsi_Length = 512;
scsicmd.scsi_Command = cdb;
scsicmd.scsi_CmdLength = 10;
scsicmd.scsi_Flags = SCSIF_READ;
scsicmd.scsi_SenseData = sense;
scsicmd.scsi_Data = (UWORD *)data;
scsicmd.scsi_Length = 512;
scsicmd.scsi_Command = cdb;
scsicmd.scsi_CmdLength = 10;
scsicmd.scsi_Flags = SCSIF_READ | SCSIF_AUTOSENSE;
scsicmd.scsi_SenseData = sense;
scsicmd.scsi_SenseLength = sizeof(sense);
scsi->io_Command = HD_SCSICMD;
scsi->io_Data = &scsicmd;
scsi->io_Length = sizeof(scsicmd);
DoIO((struct IORequest *)scsi);
diskReq->io_Command = HD_SCSICMD;
diskReq->io_Data = &scsicmd;
diskReq->io_Length = sizeof(scsicmd);
DoIO((struct IORequest *)diskReq);
if (scsicmd.scsi_Status != 0) {
/* SCSI error — check sense data */
}
if (scsicmd.scsi_Status != 0)
Printf("SCSI error: status=%ld, sense key=%ld\n",
scsicmd.scsi_Status, sense[2] & 0x0F);
```
### Common SCSI Commands
| Opcode | Name | CDB | Description |
|---|---|---|---|
| `0x00` | TEST UNIT READY | 6 | Check device readiness |
| `0x03` | REQUEST SENSE | 6 | Get error details |
| `0x12` | INQUIRY | 6 | Device identification |
| `0x1A` | MODE SENSE(6) | 6 | Read device parameters |
| `0x25` | READ CAPACITY | 10 | Get total sectors and sector size |
| `0x28` | READ(10) | 10 | Read sectors (32-bit LBA) |
| `0x2A` | WRITE(10) | 10 | Write sectors |
| `0x35` | SYNCHRONIZE CACHE | 10 | Flush write cache |
| `0x43` | READ TOC | 10 | Read CD table of contents |
| `0xBE` | READ CD | 12 | Read CD sector (any format) |
---
## CD-ROM Specifics
CD-ROM drives require special handling — they use ATAPI (IDE) or SCSI commands but with CD-specific extensions:
### Opening a CD-ROM
```c
/* CD-ROM is typically on a different device or unit: */
/* A1200 + ATAPI CD: */
OpenDevice("scsi.device", 2, req, 0); /* unit 2 = slave on second channel */
/* External SCSI CD: */
OpenDevice("2nd.scsi.device", 3, req, 0); /* SCSI ID 3 */
/* AmiCDFS or CacheCDFS handles mounting automatically via:
DEVS:DOSDrivers/CD0 mountlist entry */
```
### Reading the Table of Contents
```c
UBYTE tocData[804]; /* max TOC size */
memset(cdb, 0, 10);
cdb[0] = 0x43; /* READ TOC */
cdb[6] = 1; /* starting track */
cdb[7] = (sizeof(tocData) >> 8) & 0xFF;
cdb[8] = sizeof(tocData) & 0xFF;
scsicmd.scsi_Data = (UWORD *)tocData;
scsicmd.scsi_Length = sizeof(tocData);
scsicmd.scsi_Command = cdb;
scsicmd.scsi_CmdLength = 10;
scsicmd.scsi_Flags = SCSIF_READ | SCSIF_AUTOSENSE;
DoIO((struct IORequest *)diskReq);
```
### Audio CD Playback
```c
/* PLAY AUDIO MSF — play from start to end: */
memset(cdb, 0, 10);
cdb[0] = 0x47; /* PLAY AUDIO MSF */
cdb[3] = 0; /* start minute */
cdb[4] = 2; /* start second */
cdb[5] = 0; /* start frame */
cdb[6] = 60; /* end minute */
cdb[7] = 0; /* end second */
cdb[8] = 0; /* end frame */
scsicmd.scsi_Flags = SCSIF_READ;
DoIO((struct IORequest *)diskReq);
```
### CD Filesystem Drivers
| Driver | Type | Features |
|---|---|---|
| **AmiCDFS** | Commodore (stock) | Basic ISO 9660, slow |
| **CacheCDFS** | Third-party (popular) | ISO 9660 + Joliet + RockRidge, caching, fast |
| **AsimCDFS** | Third-party | Similar to CacheCDFS, commercial |
---
## 64-Bit Addressing (TD64 / NSD)
For drives larger than 4 GB:
```c
/* TD64 — uses io_Actual for high 32 bits: */
diskReq->io_Command = NSCMD_TD_READ64;
diskReq->io_Data = buffer;
diskReq->io_Length = 512;
diskReq->io_Offset = lowOffset; /* low 32 bits */
diskReq->io_Actual = highOffset; /* high 32 bits */
DoIO((struct IORequest *)diskReq);
```
> [!TIP]
> **New Style Device (NSD)** provides a standard way to query 64-bit support: send `NSCMD_DEVICEQUERY` and check the returned command list.
---
## MiSTer / FPGA Notes
| Aspect | Implementation |
|---|---|
| IDE emulation | MiSTer emulates Gayle IDE — presents virtual ATA backed by SD card `.hdf` |
| SCSI emulation | A2091/A3000 SCSI cores map targets to `.hdf` files |
| Sector size | Must be 512 bytes — all Amiga drivers assume this |
| RDB | Rigid Disk Block at sectors 015 — must be present for HDToolBox |
| Performance | Virtual IDE is faster than real Gayle (no PIO bottleneck) |
---
## References
- NDK39: `devices/scsidisk.h`
- NDK39: `devices/scsidisk.h`, `devices/trackdisk.h`
- ADCD 2.1: scsi.device autodocs
- SCSI-2 standard: ANSI X3.131-1994
- See also: [trackdisk.md](trackdisk.md) — floppy I/O (shares the same API model)

View file

@ -4,63 +4,132 @@
## Overview
`timer.device` provides all timing services on AmigaOS: delays, system clock queries, and high-resolution timestamps. It interfaces with two independent hardware sources — the **CIA timers** (microsecond resolution) and the **vertical blank interrupt** (frame-rate resolution).
`timer.device` is the system's central timing service — every delay, timeout, periodic callback, and timestamp on AmigaOS flows through it. Unlike most devices that map to one piece of hardware, timer.device **virtualises** two physical clock sources ([CIA timers](../01_hardware/common/cia_chips.md) and the vertical blank interrupt) into an unlimited number of independent timer requests.
> For low-level CIA register programming and hardware timer theory, see [CIA Chips — Hardware Reference](../01_hardware/common/cia_chips.md). This article covers the OS-level `timer.device` API that sits on top of that hardware.
**Key insight**: timer.device is **fully multiplexed**. Any number of tasks can have active timer requests simultaneously — the device maintains a sorted queue of pending requests and satisfies them from the same hardware clocks. There is no "one subscriber per timer" limit.
---
## Units
| Unit | Constant | Resolution | Clock Source | Use Case |
|---|---|---|---|---|
| 0 | `UNIT_MICROHZ` | ~1.4 µs (E-clock tick) | CIA-A Timer A | Short, precise delays |
| 1 | `UNIT_VBLANK` | ~20 ms (PAL) / ~16.7 ms (NTSC) | VBlank interrupt | Long delays, low CPU overhead |
| 2 | `UNIT_ECLOCK` | ~1.4 µs | CIA-B Timer A | Highest resolution timing (OS 2.0+) |
| 3 | `UNIT_WAITUNTIL` | absolute time | System clock | Wait until specific wall-clock time |
| 4 | `UNIT_WAITECLOCK` | E-clock absolute | CIA | Wait until specific E-clock value |
### Which Unit to Use?
## System Architecture
```mermaid
flowchart TD
Q["How long is the delay?"] --> SHORT["< 100 ms"]
Q --> LONG["> 100 ms"]
SHORT --> MICRO["UNIT_MICROHZ<br/>or UNIT_ECLOCK"]
LONG --> VBLANK["UNIT_VBLANK<br/>(lower CPU overhead)"]
Q --> MEASURE["Need to measure<br/>elapsed time?"]
MEASURE --> ECLOCK["ReadEClock()"]
subgraph "Hardware"
CIAA["CIA-A Timer A/B<br/>709,379 Hz (PAL)<br/>715,909 Hz (NTSC)"]
CIAB["CIA-B Timer A/B"]
VBLANK["VBlank Interrupt<br/>50 Hz (PAL) / 60 Hz (NTSC)"]
end
subgraph "timer.device (kernel)"
UM["UNIT_MICROHZ<br/>queue"] --> CIAA
UE["UNIT_ECLOCK<br/>queue"] --> CIAB
UV["UNIT_VBLANK<br/>queue"] --> VBLANK
UW["UNIT_WAITUNTIL"] --> CIAA
UWE["UNIT_WAITECLOCK"] --> CIAB
CLOCK["System Clock<br/>(seconds since 1978-01-01)"]
CIAA --> CLOCK
VBLANK --> CLOCK
end
subgraph "User Tasks (unlimited)"
T1["App 1: 100ms delay"]
T2["App 2: 50ms delay"]
T3["App 3: 2s delay"]
T4["Game: 20ms periodic"]
T5["Audio: 5ms refill"]
end
T1 --> UM
T2 --> UM
T3 --> UV
T4 --> UV
T5 --> UE
style UM fill:#e8f4fd,stroke:#2196f3,color:#333
style UV fill:#fff9c4,stroke:#f9a825,color:#333
style UE fill:#fff3e0,stroke:#ff9800,color:#333
```
### How Multiplexing Works
timer.device keeps a **sorted linked list** of pending requests per unit. When a `TR_ADDREQUEST` arrives:
1. The device calculates the absolute expiry time (current time + requested delay)
2. Inserts the request into the sorted queue
3. Programs the hardware timer to fire at the **soonest** expiry time
4. When the timer interrupt fires, the device scans the queue, replies all expired requests, and reprograms for the next one
This means 1000 applications can each have an active timer — the device just has one hardware timer firing at the nearest deadline. The overhead is the sorted insertion (O(n) in the worst case, but n is rarely large).
### Can You Run Out of Timer Resources?
**Short answer**: timer.device itself never runs out — you can queue an unlimited number of requests. But the **surrounding resources** can be exhausted:
| Resource | Limit | What Happens When Exhausted |
|---|---|---|
| **Signal bits** | **32 per task** (and ~16 are reserved by the system) | `CreateMsgPort()` returns NULL because `AllocSignal()` can't find a free bit. You can't create a new MsgPort — but you can share one port across multiple timers (see "Multiple Timers Per Task" below). |
| **Memory for IORequests** | Available RAM | `CreateIORequest()` returns NULL. Each `timerequest` is 40 bytes — you'd need thousands to notice. |
| **Queue depth (device internal)** | Unbounded linked list | Theoretically infinite. In practice, if you queue 10,000+ pending requests, the O(n) sorted insertion becomes noticeable — each new request must walk the list to find its insertion point. At ~50,000+ the system may feel sluggish. |
| **Hardware timers (CIA)** | 4 total (2 per CIA) | **Not your problem.** timer.device owns the hardware timers and virtualises them. You never directly compete for CIA timer resources — the device multiplexes everything internally. |
| **VBlank slots** | 1 per frame (50/60 Hz) | Not a resource limit — VBlank fires once per frame regardless of how many UNIT_VBLANK requests are queued. All expired requests are serviced in the same interrupt. |
**The real bottleneck is signal bits.** A task only has 32, and each `CreateMsgPort` consumes one. If your application already uses signals for windows, ARexx, commodities, and other devices, you may only have 510 left. The solution is **sharing a single MsgPort** across multiple timer requests and using `GetMsg()` to distinguish which timer fired (compare the reply message pointer to each `timerequest`).
> [!TIP]
> `OpenDevice("timer.device", ...)` itself never fails — timer.device is always available and has no "maximum open count". You can open it thousands of times. It's `CreateMsgPort` (signal bits) and `CreateIORequest` (memory) that can fail.
---
## Units — When to Use What
| Unit | Constant | Resolution | Clock Source | Best For |
|---|---|---|---|---|
| 0 | `UNIT_MICROHZ` | ~1.4 µs | CIA-A Timer A | Sub-millisecond delays, benchmarking |
| 1 | `UNIT_VBLANK` | 20 ms (PAL) / 16.7 ms (NTSC) | VBlank IRQ | Game loops, UI timeouts, long waits |
| 2 | `UNIT_ECLOCK` | ~1.4 µs | CIA-B Timer A | Highest resolution measurement (OS 2.0+) |
| 3 | `UNIT_WAITUNTIL` | absolute | System clock | Wake at specific wall-clock time |
| 4 | `UNIT_WAITECLOCK` | E-clock ticks | CIA | Wait until specific E-clock count |
```mermaid
flowchart TD
Q["What do you need?"] --> |"< 20ms delay"| MICRO["UNIT_MICROHZ"]
Q --> |"> 100ms delay"| VBLANK["UNIT_VBLANK<br/>(lower CPU cost)"]
Q --> |"Measure elapsed time"| ECLOCK["ReadEClock()<br/>via UNIT_ECLOCK"]
Q --> |"Wake at specific time"| WAITUNTIL["UNIT_WAITUNTIL"]
Q --> |"Frame-rate sync"| VBLANK2["UNIT_VBLANK<br/>(natural frame sync)"]
```
> [!IMPORTANT]
> **UNIT_VBLANK has 20ms granularity** — requesting a 5ms delay will actually wait 020ms. For anything shorter than one frame, use UNIT_MICROHZ or UNIT_ECLOCK.
---
## Hardware Foundation
### CIA Timer Internals
The timing hardware lives in the two CIA (Complex Interface Adapter) chips:
| CIA | Base | Timer | E-Clock Frequency |
| CIA | Base Address | Timer | E-Clock Frequency |
|---|---|---|---|
| CIA-A | `$BFE001` | Timer A, Timer B | 709,379 Hz (PAL) / 715,909 Hz (NTSC) |
| CIA-B | `$BFD000` | Timer A, Timer B | Same |
The **E-clock** is derived from the system clock ÷ 10 (PAL: 7,093,790 / 10 = 709,379 Hz). Each tick is ~1.4 µs.
```c
/* E-clock ticks per second: */
#define ECLOCK_PAL 709379
#define ECLOCK_NTSC 715909
/* Example: 100 ms delay = 70,938 ticks (PAL) */
```
The **E-clock** is derived from the system clock ÷ 10:
- PAL: 7,093,790 Hz / 10 = **709,379 Hz** → tick = **1.410 µs**
- NTSC: 7,159,090 Hz / 10 = **715,909 Hz** → tick = **1.397 µs**
### VBlank Timing
UNIT_VBLANK piggybacks on the vertical blank interrupt — one tick per video frame:
UNIT_VBLANK is synchronised to the display's vertical blank interrupt:
| Standard | VBlank Rate | Resolution |
|---|---|---|
| PAL | 50 Hz | 20.0 ms |
| NTSC | 60 Hz | 16.7 ms |
| Standard | VBlank Rate | Period | Use |
|---|---|---|---|
| PAL | 50 Hz | 20.0 ms | European systems |
| NTSC | 60 Hz | 16.7 ms | American/Japanese systems |
VBlank is the natural heartbeat of the system — graphics updates, animation, and game logic are traditionally synced to it.
---
@ -69,170 +138,186 @@ UNIT_VBLANK piggybacks on the vertical blank interrupt — one tick per video fr
```c
/* devices/timer.h — NDK39 */
struct timeval {
ULONG tv_secs; /* seconds */
ULONG tv_secs; /* seconds (since midnight 1 Jan 1978) */
ULONG tv_micro; /* microseconds (0999999) */
};
struct timerequest {
struct IORequest tr_node;
struct timeval tr_time;
struct IORequest tr_node; /* standard I/O request header */
struct timeval tr_time; /* delay/time value */
};
/* sizeof(timerequest) = sizeof(IORequest) + 8 */
```
### EClockVal (OS 2.0+)
```c
struct EClockVal {
ULONG ev_hi; /* high 32 bits of 64-bit tick counter */
ULONG ev_lo; /* low 32 bits */
struct EClockVal { /* OS 2.0+ */
ULONG ev_hi; /* high 32 bits of 64-bit tick counter */
ULONG ev_lo; /* low 32 bits */
};
```
---
## Opening timer.device
## Proper Initialisation and Shutdown
timer.device uses the standard Exec I/O model: a **MsgPort** for signal delivery, an **IORequest** to describe the operation, and an explicit **open/close** lifecycle. Every step matters — skipping any one causes subtle or catastrophic failures.
### Opening (The Right Way)
The correct sequence is: create a MsgPort (for reply signals) → create an IORequest (the "ticket" for timer operations) → open the device (binds the IORequest to the hardware). Each step depends on the previous one, so errors must unwind in reverse order:
```c
struct MsgPort *timerPort = CreateMsgPort();
if (!timerPort) { /* handle error */ }
struct timerequest *tr = (struct timerequest *)
CreateIORequest(timerPort, sizeof(struct timerequest));
if (!tr) { DeleteMsgPort(timerPort); /* handle error */ }
BYTE err = OpenDevice("timer.device", UNIT_MICROHZ,
(struct IORequest *)tr, 0);
if (err != 0) { /* handle error */ }
if (err != 0)
{
DeleteIORequest((struct IORequest *)tr);
DeleteMsgPort(timerPort);
/* handle error — timer.device should always be available */
}
/* IMPORTANT: after opening, you can get TimerBase for direct calls: */
/* After opening, get TimerBase for utility functions: */
struct Library *TimerBase = (struct Library *)tr->tr_node.io_Device;
/* Now you can call AddTime(), SubTime(), CmpTime(), ReadEClock() */
/* Now AddTime(), SubTime(), CmpTime(), ReadEClock() are available */
```
**Why each step is mandatory:**
| Shortcut | Consequence |
|---|---|
| Skip `CreateMsgPort` | No signal bit allocated → `Wait()` will never wake up. Or worse: signal bit 0 (CTRL-C) gets reused, causing random task termination. |
| Skip error check on `CreateIORequest` | NULL IORequest passed to `OpenDevice` → immediate crash (NULL pointer dereference in Exec). |
| Skip `OpenDevice` error check | If device can't open (e.g. wrong unit), the IORequest is uninitialised — any subsequent `DoIO`/`SendIO` writes to random memory → Guru Meditation. |
| Use `AllocMem` instead of `CreateIORequest` | IORequest fields (`io_Message.mn_ReplyPort`, `io_Message.mn_Length`) are not initialised → device replies to garbage address → memory corruption. |
| Not saving `TimerBase` | Can't call `AddTime`/`SubTime`/`CmpTime`/`ReadEClock` — they require the device's library base in A6. |
### Shutdown (The Right Way)
Shutdown must drain all pending I/O before freeing anything. The device's internal request queue holds a pointer to your IORequest — if you free it while it's still queued, the next timer interrupt dereferences freed memory.
```c
/* CRITICAL: abort any pending request before closing! */
if (!CheckIO((struct IORequest *)tr))
{
AbortIO((struct IORequest *)tr);
WaitIO((struct IORequest *)tr); /* MUST wait even after abort */
}
CloseDevice((struct IORequest *)tr);
DeleteIORequest((struct IORequest *)tr);
DeleteMsgPort(timerPort);
```
**Why `AbortIO` + `WaitIO` — not just one or the other:**
- **`AbortIO` alone is not enough**: `AbortIO` marks the request for cancellation, but the device may be in the middle of processing it (e.g., inside the timer interrupt handler). The request isn't truly "done" until the device replies it back to your MsgPort. `WaitIO` collects that reply.
- **`WaitIO` alone is not enough**: If you `WaitIO` a request that has 5 minutes left, your task blocks for 5 minutes. `AbortIO` tells the device "cancel this immediately" so `WaitIO` returns right away.
- **Skipping both**: The IORequest stays in the device's sorted queue. When the timer fires later, the device writes to your (now freed) IORequest → memory corruption → delayed Guru Meditation, often in an unrelated task.
> [!WARNING]
> **Never call `CloseDevice` with a pending timer request.** This corrupts the device's internal queue and will eventually Guru Meditation. Always `AbortIO` + `WaitIO` first. This is the single most common timer.device bug in Amiga software.
---
## Simple Delay
## Use Case 1: Simple Blocking Delay
```c
/* Block the current task for exactly 2.5 seconds: */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 2;
tr->tr_time.tv_micro = 500000; /* 0.5 sec */
DoIO((struct IORequest *)tr); /* blocks until done */
tr->tr_time.tv_micro = 500000; /* 500ms */
DoIO((struct IORequest *)tr);
/* Task is now blocked — other tasks run during this time */
```
### Non-Blocking Delay
---
## Use Case 2: Non-Blocking Timeout (UI Pattern)
The standard Intuition event loop with a timeout — essential for UI applications that need to update periodically:
```c
/* Start delay, continue doing work, then wait: */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 0;
tr->tr_time.tv_micro = 100000; /* 100 ms */
SendIO((struct IORequest *)tr); /* non-blocking */
/* ... do other work ... */
/* Check if timer expired: */
if (CheckIO((struct IORequest *)tr))
{
WaitIO((struct IORequest *)tr); /* collect result */
/* timer expired */
}
```
### Signal-Based Waiting
```c
/* Wait for timer OR user input: */
ULONG timerSig = 1L << timerPort->mp_SigBit;
/* Classic Intuition event loop with timer: */
ULONG timerSig = 1L << timerPort->mp_SigBit;
ULONG windowSig = 1L << window->UserPort->mp_SigBit;
BOOL timerPending = FALSE;
/* Start a 1-second timeout: */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 1;
tr->tr_time.tv_micro = 0;
SendIO((struct IORequest *)tr);
timerPending = TRUE;
ULONG sigs = Wait(timerSig | windowSig);
if (sigs & timerSig) {
WaitIO((struct IORequest *)tr);
/* handle timeout */
BOOL running = TRUE;
while (running)
{
ULONG sigs = Wait(timerSig | windowSig | SIGBREAKF_CTRL_C);
/* Handle window events: */
if (sigs & windowSig)
{
struct IntuiMessage *msg;
while ((msg = (struct IntuiMessage *)GetMsg(window->UserPort)))
{
switch (msg->Class)
{
case IDCMP_CLOSEWINDOW:
running = FALSE;
break;
case IDCMP_GADGETUP:
HandleGadget(msg);
break;
}
ReplyMsg((struct Message *)msg);
}
}
/* Handle timer expiry: */
if (sigs & timerSig)
{
WaitIO((struct IORequest *)tr);
timerPending = FALSE;
/* --- Update UI clock, animation, status bar, etc. --- */
UpdateStatusBar();
/* Re-arm timer for next second: */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 1;
tr->tr_time.tv_micro = 0;
SendIO((struct IORequest *)tr);
timerPending = TRUE;
}
if (sigs & SIGBREAKF_CTRL_C)
running = FALSE;
}
if (sigs & windowSig) {
/* Clean shutdown: */
if (timerPending)
{
AbortIO((struct IORequest *)tr);
WaitIO((struct IORequest *)tr);
/* handle window event */
}
```
---
## Getting Current Time
## Use Case 3: Game/Demo Frame Sync (Periodic Timer)
```c
/* Get system time (wall clock since midnight Jan 1, 1978): */
tr->tr_node.io_Command = TR_GETSYSTIME;
DoIO((struct IORequest *)tr);
Printf("Time: %lu.%06lu seconds since epoch\n",
tr->tr_time.tv_secs, tr->tr_time.tv_micro);
```
### Time Arithmetic
```c
/* After opening timer.device and getting TimerBase: */
struct timeval t1, t2, diff;
/* Measure elapsed time: */
tr->tr_node.io_Command = TR_GETSYSTIME;
DoIO((struct IORequest *)tr);
t1 = tr->tr_time;
/* ... do work ... */
DoIO((struct IORequest *)tr);
t2 = tr->tr_time;
/* Compute difference: */
diff = t2;
SubTime(&diff, &t1);
Printf("Elapsed: %lu.%06lu s\n", diff.tv_secs, diff.tv_micro);
/* Compare times: */
LONG cmp = CmpTime(&t1, &t2); /* <0: t1<t2, 0: equal, >0: t1>t2 */
```
---
## High-Resolution Timing (ReadEClock)
```c
/* Most precise timing available — E-clock resolution: */
struct EClockVal start, end;
ULONG efreq = ReadEClock(&start); /* returns ticks/second */
/* ... code to benchmark ... */
ReadEClock(&end);
/* Compute elapsed microseconds: */
ULONG ticks = end.ev_lo - start.ev_lo; /* assumes <4 billion ticks */
ULONG usecs = ticks * 1000000 / efreq;
Printf("Elapsed: %lu µs (E-clock freq: %lu Hz)\n", usecs, efreq);
```
| Standard | E-clock Freq | Tick Resolution |
|---|---|---|
| PAL | 709,379 Hz | ~1.410 µs |
| NTSC | 715,909 Hz | ~1.397 µs |
---
## Periodic Timer (Game Loop / Audio Refill)
```c
/* Classic pattern: periodic callback using timer.device */
#define FRAME_USEC 20000 /* 50 Hz (PAL frame rate) */
/* 50 Hz game loop synchronised to PAL frame rate: */
#define FRAME_USEC 20000 /* 1/50th second = 20ms */
void GameLoop(void)
{
ULONG timerSig = 1L << timerPort->mp_SigBit;
/* Kick off first timer request: */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 0;
tr->tr_time.tv_micro = FRAME_USEC;
@ -247,11 +332,13 @@ void GameLoop(void)
{
WaitIO((struct IORequest *)tr);
/* --- Game logic here --- */
UpdateGame();
/* === Frame logic === */
ReadInput();
UpdatePhysics();
RenderFrame();
SwapBuffers();
/* Re-arm timer: */
/* Re-arm for next frame: */
tr->tr_time.tv_secs = 0;
tr->tr_time.tv_micro = FRAME_USEC;
SendIO((struct IORequest *)tr);
@ -266,17 +353,169 @@ void GameLoop(void)
}
```
> **Demo effects**: For smooth copper-style effects at higher rates (100+ Hz), demos typically bypass timer.device entirely and use direct CIA timer interrupts or copper waits. timer.device is better suited for system-friendly applications.
---
## Common Pitfalls
## Use Case 4: Audio Buffer Refill
```c
/* Double-buffered audio playback with timer-driven refill: */
#define AUDIO_BUFFER_MS 10 /* refill every 10ms */
void AudioRefillLoop(void)
{
ULONG timerSig = 1L << timerPort->mp_SigBit;
/* Use UNIT_MICROHZ for sub-frame precision: */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 0;
tr->tr_time.tv_micro = AUDIO_BUFFER_MS * 1000;
SendIO((struct IORequest *)tr);
while (!quit)
{
Wait(timerSig);
WaitIO((struct IORequest *)tr);
/* Fill the next audio DMA buffer: */
FillAudioBuffer(currentBuffer);
SwapAudioBuffers();
/* Re-arm: */
tr->tr_time.tv_secs = 0;
tr->tr_time.tv_micro = AUDIO_BUFFER_MS * 1000;
SendIO((struct IORequest *)tr);
}
AbortIO((struct IORequest *)tr);
WaitIO((struct IORequest *)tr);
}
```
---
## Use Case 5: Benchmarking with ReadEClock
```c
/* Precise code benchmarking using E-clock: */
struct EClockVal start, end;
ULONG efreq = ReadEClock(&start);
/* --- Code to benchmark --- */
SortLargeArray(data, count);
/* --- End benchmark --- */
ReadEClock(&end);
/* Calculate elapsed microseconds: */
ULONG ticks = end.ev_lo - start.ev_lo;
ULONG usecs = (ULONG)((UQUAD)ticks * 1000000ULL / efreq);
Printf("Elapsed: %lu µs (%lu E-clock ticks at %lu Hz)\n",
usecs, ticks, efreq);
```
---
## Use Case 6: Getting System Time
```c
/* Read wall-clock time: */
tr->tr_node.io_Command = TR_GETSYSTIME;
DoIO((struct IORequest *)tr);
Printf("Seconds since 1978-01-01: %lu.%06lu\n",
tr->tr_time.tv_secs, tr->tr_time.tv_micro);
/* Time arithmetic: */
struct timeval t1, t2, elapsed;
/* ... get t1, do work, get t2 ... */
elapsed = t2;
SubTime(&elapsed, &t1);
Printf("Operation took: %lu.%06lu s\n",
elapsed.tv_secs, elapsed.tv_micro);
/* Compare times: */
LONG cmp = CmpTime(&t1, &t2);
/* Returns: -1 if t1 > t2, 0 if equal, +1 if t1 < t2 */
/* WARNING: return values are opposite to strcmp convention! */
```
---
## Multiple Timers Per Task
A single task can have **multiple simultaneous timer requests** — just use separate `timerequest` structures sharing the same MsgPort:
```c
/* Two independent timers on one port: */
struct timerequest *tr_fast = CreateIORequest(port, sizeof(*tr_fast));
struct timerequest *tr_slow = CreateIORequest(port, sizeof(*tr_slow));
OpenDevice("timer.device", UNIT_MICROHZ, (struct IORequest *)tr_fast, 0);
/* Clone the device for the second request: */
*tr_slow = *tr_fast; /* copy device/unit/port */
/* Start both timers: */
tr_fast->tr_time.tv_micro = 50000; /* 50ms — UI animation */
SendIO((struct IORequest *)tr_fast);
tr_slow->tr_time.tv_secs = 5; /* 5s — autosave */
SendIO((struct IORequest *)tr_slow);
/* Wait for either: */
while (running)
{
ULONG sigs = Wait(1L << port->mp_SigBit);
struct Message *msg;
while ((msg = GetMsg(port)))
{
if (msg == (struct Message *)tr_fast)
{
/* Fast timer expired — animate */
WaitIO((struct IORequest *)tr_fast);
AnimateUI();
tr_fast->tr_time.tv_micro = 50000;
SendIO((struct IORequest *)tr_fast);
}
else if (msg == (struct Message *)tr_slow)
{
/* Slow timer expired — autosave */
WaitIO((struct IORequest *)tr_slow);
AutoSave();
tr_slow->tr_time.tv_secs = 5;
SendIO((struct IORequest *)tr_slow);
}
}
}
```
---
## Common Pitfalls and Anti-Patterns
| Pitfall | Problem | Solution |
|---|---|---|
| Reusing active IORequest | Sending a `TR_ADDREQUEST` while previous is pending | Use two timerequest structs, or `WaitIO` first |
| Forgetting `WaitIO` after `AbortIO` | Leaves IORequest in limbo — crash on next use | Always `WaitIO` after `AbortIO`, even if aborted |
| Using `UNIT_VBLANK` for short delays | 20 ms granularity — actual delay is 0 to 20 ms | Use `UNIT_MICROHZ` for sub-20ms precision |
| Not opening timer for `ReadEClock` | `TimerBase` is NULL — crash | Must `OpenDevice` first to get `TimerBase` |
| Ignoring PAL/NTSC differences | Hardcoded periods wrong on other standard | Use `ReadEClock()` frequency for calculations |
| **Reusing active IORequest** | `SendIO` while previous is pending → queue corruption | Use two `timerequest` structs, or `WaitIO` first |
| **Missing `WaitIO` after `AbortIO`** | IORequest in limbo — crash on next use | **Always** `WaitIO` after `AbortIO`, even if aborted |
| **Using `UNIT_VBLANK` for short delays** | 20ms granularity — actual delay is 020ms | Use `UNIT_MICROHZ` for sub-20ms precision |
| **Not opening timer for `ReadEClock`** | `TimerBase` is NULL — immediate crash | Must `OpenDevice` first to get `TimerBase` |
| **Hardcoded PAL/NTSC values** | Wrong timing on the other standard | Use `ReadEClock()` frequency for calculations |
| **Polling in a loop instead of `Wait()`** | Burns 100% CPU for no benefit | Use signal-based `Wait()` — CPU sleeps until timer fires |
| **Forgetting to abort on shutdown** | Device queue contains pointer to freed memory | Always `AbortIO`+`WaitIO` before `CloseDevice` |
| **Using Delay() for everything** | `Delay()` has 20ms granularity and blocks the process | Use `SendIO` + signals for responsive apps |
### The `Delay()` Trap
`dos.library/Delay()` uses timer.device internally, but:
- Fixed to UNIT_VBLANK (20ms granularity)
- Blocks the entire process (no signal checking possible)
- Takes ticks, not milliseconds: `Delay(50)` = 1 second (at 50 Hz)
```c
/* DON'T use Delay() in event loops — use timer.device directly */
Delay(25); /* blocks for 0.5s — can't handle window events during this! */
```
---
@ -286,3 +525,5 @@ void GameLoop(void)
- ADCD 2.1: timer.device autodocs
- HRM: CIA timer chapter
- See also: [interrupts.md](../06_exec_os/interrupts.md) — VBlank interrupt chain
- See also: [multitasking.md](../06_exec_os/multitasking.md) — task scheduling and signals
- See also: [audio.md](audio.md) — audio buffer timing