From 9b0dfbcf3268351789a9515941cba4be11473a3c Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Sun, 26 Apr 2026 23:06:26 -0400 Subject: [PATCH] Timer content improved --- 10_devices/README.md | 2 +- 10_devices/timer.md | 377 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 352 insertions(+), 27 deletions(-) diff --git a/10_devices/README.md b/10_devices/README.md index aec0c13..c278510 100644 --- a/10_devices/README.md +++ b/10_devices/README.md @@ -12,7 +12,7 @@ Amiga devices are shared libraries with an exec I/O request interface. They prov | [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) | 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 | +| [timer.md](timer.md) | Virtualised timing service: named antipatterns, use-case cookbook (blocking delay, UI timeout, game frame sync, audio refill, benchmarking, system time), decision flowchart, FPGA/MiSTer impact, 1985 competitive landscape, modern analogies, FAQ | | [audio.md](audio.md) | 4-channel DMA audio: Paula architecture, DMA slot budget, named antipatterns, use-case cookbook, decision flowchart (audio.device vs direct HW), FPGA/MiSTer impact, cross-platform comparison | | [keyboard.md](keyboard.md) | CIA-A serial handshake, raw key codes, key matrix, reset sequence, FPGA protocol notes | | [gameport.md](gameport.md) | Joystick/mouse port: quadrature decoding, XOR state, fire buttons, controller types | diff --git a/10_devices/timer.md b/10_devices/timer.md index 557953c..d09626f 100644 --- a/10_devices/timer.md +++ b/10_devices/timer.md @@ -223,7 +223,9 @@ DeleteMsgPort(timerPort); --- -## Use Case 1: Simple Blocking Delay +## Use-Case Cookbook + +### 1. Simple Blocking Delay ```c /* Block the current task for exactly 2.5 seconds: */ @@ -236,7 +238,7 @@ DoIO((struct IORequest *)tr); --- -## Use Case 2: Non-Blocking Timeout (UI Pattern) +### 2. 2: Non-Blocking Timeout (UI Pattern) The standard Intuition event loop with a timeout — essential for UI applications that need to update periodically: @@ -308,7 +310,7 @@ if (timerPending) --- -## Use Case 3: Game/Demo Frame Sync (Periodic Timer) +### 3. 3: Game/Demo Frame Sync (Periodic Timer) ```c /* 50 Hz game loop synchronized to PAL frame rate: */ @@ -357,7 +359,7 @@ void GameLoop(void) --- -## Use Case 4: Audio Buffer Refill +### 4. 4: Audio Buffer Refill ```c /* Double-buffered audio playback with timer-driven refill: */ @@ -395,7 +397,7 @@ void AudioRefillLoop(void) --- -## Use Case 5: Benchmarking with ReadEClock +### 5. 5: Benchmarking with ReadEClock ```c /* Precise code benchmarking using E-clock: */ @@ -417,7 +419,7 @@ Printf("Elapsed: %lu µs (%lu E-clock ticks at %lu Hz)\n", --- -## Use Case 6: Getting System Time +### 6. 6: Getting System Time ```c /* Read wall-clock time: */ @@ -492,31 +494,351 @@ while (running) --- -## Common Pitfalls and Anti-Patterns +## Named Antipatterns -| Pitfall | Problem | Solution | -|---|---|---| -| **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 0–20ms | 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 | +### 1. "The Re-Arm Without Wait" -### 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) +**What fails** — calling `SendIO` on a `timerequest` that is already pending: ```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! */ +/* BROKEN — re-arming without draining the previous request */ +tr->tr_time.tv_micro = 50000; +SendIO((struct IORequest *)tr); /* starts first timer */ + +/* ... some code later ... */ + +tr->tr_time.tv_micro = 50000; +SendIO((struct IORequest *)tr); /* 💥 IORequest is still queued! */ ``` +**Why it fails:** Each `timerequest` can hold exactly one pending I/O operation. A second `SendIO` while the first is still queued corrupts the device's internal linked list — the old node pointer is overwritten before the device has finished with it. The timer interrupt handler may dereference freed or duplicated list nodes, producing silent data corruption or a Guru Meditation. + +**Correct:** + +```c +/* Wait for previous to finish before re-arming: */ +tr->tr_time.tv_micro = 50000; +SendIO((struct IORequest *)tr); + +WaitIO((struct IORequest *)tr); /* drain it */ + +/* Now safe to re-arm: */ +tr->tr_time.tv_micro = 50000; +SendIO((struct IORequest *)tr); +``` + +For cases where you need overlapping timers, use **two `timerequest` structs** (see [Multiple Timers Per Task](#multiple-timers-per-task) above). + +--- + +### 2. "The Abandoned Abort" + +**What fails** — calling `AbortIO` but not following it with `WaitIO`: + +```c +/* BROKEN — aborts but never collects the reply */ +AbortIO((struct IORequest *)tr); +/* ... tr is now in limbo — still queued internally ... */ +CloseDevice((struct IORequest *)tr); /* 💥 queue corruption */ +``` + +**Why it fails:** `AbortIO` *marks* the request for cancellation; it does not guarantee the request has been removed from the device's internal queue. The device may still be inside the timer interrupt handler processing your request. Only `WaitIO` confirms the device has replied the IORequest back to your MsgPort — *that* is when the request is truly done. + +**Correct:** + +```c +AbortIO((struct IORequest *)tr); /* mark for cancellation */ +WaitIO((struct IORequest *)tr); /* collect the reply — always */ +/* Now tr is truly free */ +CloseDevice((struct IORequest *)tr); +``` + +--- + +### 3. "The Microsecond VBlank" + +**What fails** — requesting sub-20 ms delays on `UNIT_VBLANK`: + +```c +/* BROKEN — 5ms request on UNIT_VBLANK */ +tr->tr_time.tv_micro = 5000; +DoIO((struct IORequest *)tr); +/* Actual delay: 0–20ms. At 50 Hz, it might return instantly! */ +``` + +**Why it fails:** `UNIT_VBLANK` fires at 50 Hz (PAL) / 60 Hz (NTSC) — one pulse every 20 ms or 16.7 ms. A request for 5 ms is rounded down to the next VBlank tick, which may already be pending. The call can return in **zero time** if a VBlank was imminent, giving the caller the false impression that a 5 ms delay was satisfied. + +**Correct:** + +```c +/* Use UNIT_MICROHZ for sub-20ms delays */ +/* Original OpenDevice must specify UNIT_MICROHZ */ +tr->tr_time.tv_micro = 5000; /* 5ms */ +DoIO((struct IORequest *)tr); /* actually waits ~5ms */ +``` + +--- + +### 4. "The Naked ReadEClock" + +**What fails** — calling `ReadEClock()` without opening timer.device: + +```c +/* BROKEN — TimerBase is NULL */ +struct EClockVal start; +ReadEClock(&start); /* 💥 writes to A6-relative memory at NULL */ +``` + +**Why it fails:** `ReadEClock()` is a utility function that requires `TimerBase` in A6 (the timer.device library base). The library base is set in `io_Device` when `OpenDevice` returns — until then, `TimerBase` is NULL. The call dereferences through address 0 + function offset, crashing immediately. + +**Correct:** + +```c +/* Open the device first to obtain TimerBase */ +BYTE err = OpenDevice("timer.device", UNIT_MICROHZ, + (struct IORequest *)tr, 0); +struct Library *TimerBase = (struct Library *)tr->tr_node.io_Device; +/* Now ReadEClock() is available */ +struct EClockVal start; +ReadEClock(&start); +``` + +--- + +### 5. "The Hard-Coded Hertz" + +**What fails** — hardcoding PAL E-clock frequency instead of querying it: + +```c +/* BROKEN — assumes PAL */ +#define ECLOCK_FREQ 709379 +ULONG usecs = ticks * 1000000 / ECLOCK_FREQ; +/* Wrong on NTSC systems — 0.9% error */ +``` + +**Why it fails:** The E-clock frequency differs between PAL (709,379 Hz) and NTSC (715,909 Hz). A hardcoded constant produces a ~0.9% timing error on the opposite video standard. Worse, future hardware (FPGA reimplementations, overclocked systems) may use a different master clock entirely. + +**Correct:** + +```c +ULONG efreq = ReadEClock(&start); /* returns actual frequency */ +/* ... do work ... */ +ReadEClock(&end); +ULONG ticks = end.ev_lo - start.ev_lo; +ULONG usecs = (ULONG)((UQUAD)ticks * 1000000ULL / efreq); +``` + +--- + +### 6. "The Spin Loop" + +**What fails** — polling in a loop instead of using signals: + +```c +/* BROKEN — burns 100% CPU */ +while (!timeout_expired) +{ + /* Do nothing, check timer, repeat — CPU never sleeps */ + if (CheckIO((struct IORequest *)tr)) + timeout_expired = TRUE; +} +``` + +**Why it fails:** The CPU runs continuously, stealing cycles from every other task in the system. On a single-core Amiga with preemptive multitasking, this starves every lower-priority task — no window redraws, no mouse movement, no disk I/O. The timer will still fire (it's hardware-driven), but the CPU spent every microsecond between now and then doing nothing useful. + +**Correct:** + +```c +/* CPU sleeps until timer or other signals arrive */ +ULONG sigs = Wait(timerSig | windowSig | SIGBREAKF_CTRL_C); +if (sigs & timerSig) +{ + WaitIO((struct IORequest *)tr); + timeout_expired = TRUE; +} +``` + +--- + +### 7. "The Leaky Shutdown" + +**What fails** — calling `CloseDevice` with a still-pending timer: + +```c +/* BROKEN — 5-second timer still queued internally */ +tr->tr_time.tv_secs = 5; +SendIO((struct IORequest *)tr); +CloseDevice((struct IORequest *)tr); /* 💥 queue corruption */ +DeleteIORequest((struct IORequest *)tr); +/* When the 5-second timer fires, it writes to freed memory */ +``` + +**Why it fails:** The device's internal sorted linked list still holds a pointer to your `timerequest`. When the timer interrupt eventually fires (5 seconds from now, or immediately if a shorter timer triggers a queue scan), the device replies to freed memory. This produces a **delayed Guru Meditation** — often in a completely unrelated task, making debugging nearly impossible. + +**Correct:** + +```c +if (!CheckIO((struct IORequest *)tr)) +{ + AbortIO((struct IORequest *)tr); /* cancel */ + WaitIO((struct IORequest *)tr); /* drain */ +} +CloseDevice((struct IORequest *)tr); /* now safe */ +DeleteIORequest((struct IORequest *)tr); +``` + +--- + +### 8. "The `Delay()` Dependency" + +`dos.library/Delay()` is a convenience wrapper that uses timer.device internally, but with **fixed restrictions**: + +```c +/* BROKEN in event loops — blocks entire process */ +Delay(25); /* 0.5s — can't check windows, ARexx, signals! */ +``` + +**Why it fails:** +- **Locked to UNIT_VBLANK** — 20 ms granularity, no sub-frame precision +- **Blocks the process** — no signal checking possible during the wait +- **Takes ticks, not milliseconds** — `Delay(50)` = 1 second at 50 Hz. NTSC users get different durations from the same numeric argument +- **DOS/Packet-level** — uses the DOS packet system, adding latency vs direct `timer.device` access + +Use `Delay()` only in trivial CLI tools where you don't need to respond to anything during the wait. For all GUI applications, game loops, and multi-signal event handlers, use timer.device directly with `SendIO` + `Wait`. + +--- + +## Best Practices + +1. **Always `AbortIO` + `WaitIO` before `CloseDevice`** — never skip the drain. This is the #1 timer crash vector on Amiga. +2. **Check `TimerBase` validity once after `OpenDevice`** — if the open fails, don't call `ReadEClock`/`AddTime`/`SubTime`. +3. **Use `ReadEClock` frequency for calculations** — never hardcode 709,379 or 715,909. Let the hardware tell you. +4. **Share one MsgPort across multiple `timerequest`s** — signal bits are scarce (32 per task, ~16 reserved). One MsgPort serves unlimited timers. +5. **Use UNIT_VBLANK for anything ≥ 100 ms** — the lower CPU cost of a 50/60 Hz interrupt matters at scale. +6. **Use UNIT_MICROHZ for anything < 20 ms** — VBlank granularity makes short delays unpredictable. +7. **Never call `SendIO` until `WaitIO` has confirmed the previous request is done** — or use separate `timerequest` structs. +8. **`WaitIO` after `AbortIO` even if aborted** — nothing is truly done until the device replies. +9. **Use `SendIO` + `Wait` for responsive apps** — never `DoIO` in event loops; it blocks the whole task. +10. **Don't assume PAL or NTSC** — always query the actual hardware frequency. + +--- + +## When to Use / When NOT to Use + +```mermaid +flowchart TD + START["You need timing"] --> ACC["< 20ms accuracy?"] + ACC -->|"Yes"| MICRO["timer.device
UNIT_MICROHZ"] + ACC -->|"No"| BLOCK["Can block the task?"] + BLOCK -->|"Yes — simple CLI tool"| DOS["dos.library/Delay()
simpler, good enough"] + BLOCK -->|"No — GUI/game/app"| TD["timer.device
SendIO + Wait"] + + START2["You need timestamps"] --> WALL["Wall-clock time?"] + WALL -->|Yes| GETSYSTIME["TR_GETSYSTIME"] + WALL -->|No — benchmarking| ECLOCK["ReadEClock()"] + + START3["Frame-rate sync"] --> GAME["Game or demo?"] + GAME -->|"Yes, >100Hz"| CIA["Direct CIA interrupts
or Copper waits"] + GAME -->|"Yes, ≤50Hz"| VB["UNIT_VBLANK
timer.device"] + GAME -->|"No — UI update"| VB2["UNIT_VBLANK
timer.device"] +``` + +| Scenario | Recommended | Why | +|---|---|---| +| One-shot 2.5s blocking wait | `DoIO` + unit of choice | Simplest; blocks task but allows multitasking | +| UI clock tick every 1s | `SendIO` + `Wait(signals)` | CPU sleeps between ticks, still handles window events | +| Game loop at 50 Hz | UNIT_MICROHZ or UNIT_VBLANK | 20ms frame budget; VBlank sync is natural | +| Audio buffer refill every 10ms | UNIT_MICROHZ | Sub-frame precision matters for audio | +| Code benchmarking | `ReadEClock()` | 1.4 µs resolution, query frequency don't hardcode | +| Wake at specific wall-clock time | `UNIT_WAITUNTIL` | Compare `timeval` against current time | +| CLI tool: sleep 2 seconds | `Delay(100)` or `DoIO` | Simpler; doesn't need signal loop | +| 100+ Hz demo effect | Direct CIA or Copper | timer.device overhead is too high for sub-millisecond rates | + +--- + +## FPGA & MiSTer Impact + +timer.device's behavior is tightly coupled to the CIA chip's E-clock, which is derived from the system clock. On FPGA reimplementations (MiSTer Minimig core, Vampire V4), timer accuracy depends on how faithfully the E-clock is reproduced. + +| Aspect | Real Hardware | FPGA (MiSTer) | FPGA (Vampire) | +|---|---|---|---| +| **E-Clock source** | 28.375 MHz crystal ÷ 40 | Same division in Minimig core | Derived from SAGA PLL | +| **UNIT_MICROHZ precision** | ±1 E-clock tick (1.4 µs) | Matches when cycle-accurate | Can drift if SAGA clock ≠ 28.375 | +| **VBlank rate** | 50.0 Hz (PAL) / 59.94 Hz (NTSC) | 50.0 Hz / 60.0 Hz (sometimes integer) | Configurable (50/60/100+ Hz) | +| **CIA timer count-in** | E-clock synchronous to 68000 bus | Minimig core replicates CIA faithfully | SAGA CIA reimplementation may differ | +| **`Delay()` behavior** | Tied to INTB_VERTB | Same on Minimig | May break if VBlank rate is non-standard | +| **`ReadEClock()` returns** | 709,379 or 715,909 | Should match real hardware | May return Vampire-specific value | + +### What to check on FPGA + +- **E-Clock frequency**: `ReadEClock()` must return the correct value for timer software to calculate microseconds correctly. If the FPGA core uses a slightly different master clock, all timer-based delays shift proportionally. +- **CIA-A timer A ownership**: timer.device monopolizes CIA-A Timer A for the system clock. On Minimig, confirm that no other core component writes to `$BFE401`/`$BFE501`. +- **VBlank interrupt → CIAB TOD**: The VBlank interrupt handler increments the CIA-B Time-of-Day clock, which feeds `TR_GETSYSTIME`. On FPGA cores with non-standard refresh rates (e.g. 100 Hz PAL), the system clock runs fast. +- **Cycle-exact CIA behavior**: The Minimig CIA implementation must handle timer reload, one-shot vs continuous mode, and count-in synchronization identically to real 8520 CIAs. + +> [!NOTE] +> Most MiSTer Minimig core builds replicate CIA timers cycle-accurately from the TG68K soft CPU's perspective. The bigger issue is **non-standard refresh rates** (50 Hz forced on NTSC, or custom PAL modes) that shift the relationship between VBlank and wall-clock time. + +--- + +## Historical Context & Modern Analogies + +### 1985 Competitive Landscape + +| Platform | Timing | Precision | Notes | +|---|---|---|---| +| **Amiga (timer.device)** | Multiplexed virtual timers | 1.4 µs (CIA) + 20 ms (VBlank) | Any number of tasks share 2 hardware timers | +| **C64 (CIA timers)** | Two CIA 6526 timers | 1 µs | 2 hardware timers only — no virtualisation | +| **MS-DOS (PC XT/AT)** | Intel 8254 PIT | 0.838 µs | Single programmable timer at 1.193182 MHz; shared by system clock, DRAM refresh, PC speaker — one subscriber at a time | +| **Atari ST (MFP 68901)** | 4 timers + 200 Hz system timer | 0.8 µs (MFP) / 5 ms (200 Hz) | Four hardware timers; no OS-level virtualisation in TOS | +| **Mac 128K/512K (6522 VIA)** | Two 6522 timers | Cycles at 7.8336 MHz | Used for serial I/O timing and cursor blinking; no developer-facing timer API | +| **Amiga timer.device innovation** | **Virtualisation** — 2 CIA timers + VBlank serve unlimited tasks | — | Nothing else on a consumer machine did this | + +### Why Virtualised Timers Mattered + +On every contemporary platform, a timer was a **scarce resource** — you had 1–4 hardware timers, period. If two applications needed a timer, they fought. The Amiga's timer.device solved this with multiplexed queues: every `TR_ADDREQUEST` gets inserted into a sorted list, the hardware timer fires at the nearest deadline, and *all* expired requests get replied in one interrupt handler pass. + +This meant a clock widget could tick every second, a game could run at 50 Hz, an audio mixer could refill buffers every 10 ms, a network stack could track TCP retransmit timeouts, and an ARexx script could `WAIT 5 SEC` — **all simultaneously**, on a 7 MHz 68000 with 512 KB of RAM. This was unprecedented on a consumer microcomputer in 1985. + +### Modern Analogies + +| Amiga Concept | Modern Equivalent | Similarity | Key Difference | +|---|---|---|---| +| `UNIT_MICROHZ` + `SendIO` | `setTimeout()` / `setInterval()` (JS) | Async callback at specified delay | JavaScript timers use the libuv event loop, not hardware interrupts | +| `UNIT_VBLANK` | `requestAnimationFrame()` | Schedules work to align with display refresh | `requestAnimationFrame` is for rendering only; `UNIT_VBLANK` is general-purpose | +| `UNIT_WAITUNTIL` | `clock_nanosleep(TIMER_ABSTIME)` (POSIX) | Wake at absolute wall-clock time | POSIX uses CLOCK_REALTIME; timer.device uses CIA + VBlank | +| `ReadEClock()` | `rdtsc` (x86) / `mach_absolute_time()` (macOS) | CPU timestamp counter for benchmarking | Modern APIs return ticks; timer.device also returns ticks/second for µs conversion | +| `AddTime` / `SubTime` / `CmpTime` | `clock_gettime` + `timespec` arithmetic | Time interval computation | POSIX uses 64-bit ns; timer.device uses 32-bit seconds + microseconds | +| timer.device multiplexing | Linux kernel timer wheels | One hardware timer, unlimited virtual | Same principle — sorted expiry queues driving a single hardware clock | +| `MsgPort` signal delivery | epoll / kqueue / IOCP | Wait for one of several event sources to fire | Same pattern: sleep on multiple sources, wake when any is ready | + +> **Notable absence**: Modern OSes do not expose a raw hardware tick counter directly to user space — high-resolution timing requires `clock_gettime()` or platform-specific monotonic clock APIs. The Amiga gave every application direct access to the 1.4 µs E-Clock counter through `ReadEClock()`, which was both powerful and dangerous (no privilege separation). + +--- + +## FAQ + +### Q: Do I need to open timer.device separately for each unit? +**No.** One `OpenDevice` call binds one IORequest to one unit. For a different unit, open again with a separate IORequest. Multiple `SendIO` calls on the same IORequest must be for the same unit. + +### Q: Does UNIT_VBLANK guarantee exactly-frame pacing? +**No.** A UNIT_VBLANK request fires at the *next* VBlank interrupt. If the interrupt handler for one VBlank takes 18 ms (because 50 expired requests are being replied), the next UNIT_VBLANK request you send immediately after `WaitIO` may already be 2 ms from expiring. VBlank is frame-synchronous, not frame-precise. + +### Q: Is `ReadEClock()` monotonic? +**No.** `ReadEClock()` returns the raw 64-bit E-Clock tick counter, which is derived from the CIA's free-running hardware timer. It wraps when the low 32 bits overflow (every ~6,040 seconds ≈ 100 minutes at 709 kHz). Use 64-bit subtraction with wrap-around awareness for interval measurement. + +### Q: Can I use timer.device inside an interrupt handler? +**No.** timer.device uses MsgPorts and signals, which require a valid task context. In an interrupt handler there is no task context — you cannot call `Wait` or receive messages. Use direct CIA timer programming or the Copper for interrupt-level timing. + +### Q: Why does my 50 Hz game loop drift over time? +Each frame fires `SendIO` → `Wait` → `WaitIO` → game logic → `SendIO` again. The 20 ms timer starts *after* `SendIO`, not after the previous frame's deadline. Frame work (input, physics, rendering) takes variable N ms → frame period = 20 + N ms → you lose synchronization every frame. For locked frame rates, use `ReadEClock` to measure frame execution time and adjust the *next* delay to absorb the variable overhead. + +### Q: timer.device or `SetSignal` for self-wakeup? +`Signal` + `SetSignal` + `Wait` can also implement timed behavior, but is more cumbersome: you must manually set a signal bit on your own task and embed the timing logic in a software loop. timer.device provides a cleaner abstraction with the full `timeval` API and hardware-backed precision. + +### Q: Does `OpenDevice("timer.device", ...)` ever fail? +**Effectively never.** timer.device is built into the Kickstart ROM and has no artificial open limit — there is no "maximum open count." `OpenDevice()` can theoretically return `IOERR_OPENFAIL` if the unit number is invalid, but otherwise always succeeds. + --- ## References @@ -524,6 +846,9 @@ Delay(25); /* blocks for 0.5s — can't handle window events during this! */ - NDK39: `devices/timer.h` - ADCD 2.1: timer.device autodocs - HRM: CIA timer chapter +- See also: [CIA Chips — Hardware Reference](../01_hardware/common/cia_chips.md) — low-level CIA timer programming - 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 +- See also: [semaphores.md](../06_exec_os/semaphores.md) — `Procure`/`Vacate` with timeout via timer signals +- See also: [signals.md](../06_exec_os/signals.md) — `Wait()` with multiple signal sources +- See also: [audio.md](audio.md) — audio buffer refill timing \ No newline at end of file