Timer content improved

This commit is contained in:
Ilia Sharin 2026-04-26 23:06:26 -04:00
parent cb14c243b6
commit 9b0dfbcf32
2 changed files with 352 additions and 27 deletions

View file

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

View file

@ -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 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 |
### 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: 020ms. 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["&lt; 20ms accuracy?"]
ACC -->|"Yes"| MICRO["timer.device<br/>UNIT_MICROHZ"]
ACC -->|"No"| BLOCK["Can block the task?"]
BLOCK -->|"Yes — simple CLI tool"| DOS["dos.library/Delay()<br/>simpler, good enough"]
BLOCK -->|"No — GUI/game/app"| TD["timer.device<br/>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<br/>or Copper waits"]
GAME -->|"Yes, &le;50Hz"| VB["UNIT_VBLANK<br/>timer.device"]
GAME -->|"No — UI update"| VB2["UNIT_VBLANK<br/>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 14 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