amiga-bootcamp/10_devices/timer.md
Ilia Sharin da9e7d3b63 Phase 1: enrich 07_dos and 10_devices (highest FPGA priority)
07_dos:
- file_io.md: 108→240+ lines — buffered I/O (FRead/FWrite/SetVBuf),
  access mode comparison, FileHandle struct with offsets, standard
  handles, Printf %ld warning, FileInfoBlock, practical patterns
  (copy file, get size, load to RAM), error code table
- filesystem.md: 91→270+ lines — full disk geometry (ADF/HDF),
  all 8 DOS\x filesystem IDs, root block byte-level layout, file
  header layout with reverse-order pointer quirk, OFS vs FFS data
  blocks with efficiency numbers, bitmap blocks, extension blocks,
  checksum algorithm, Python ADF reader
- locks_examine.md: 113→270+ lines — lock semantics diagram, FileLock
  struct with handler discovery, ExAll bulk scan, practical patterns
  (atomic write, path resolution, volume info), 4 antipatterns
  (leaked locks, exclusive too long, unchecked IoErr, DupLock),
  pattern matching

10_devices:
- audio.md: 73→240+ lines — hardware architecture diagram, channel
  registers with offsets, period/frequency table, priority allocation,
  double-buffering, audio interrupts, AM/PM modulation, direct HW
- timer.md: 80→230+ lines — CIA timer hardware, all 5 units with
  decision flowchart, non-blocking delays, signal-based waiting,
  time arithmetic, ReadEClock, periodic game loop pattern, pitfalls
- trackdisk.md: 82→210+ lines — MFM encoding, track format, disk
  geometry, read/write/motor, change notification, track caching,
  direct hardware access, FPGA timing implications
- keyboard.md: 58→220+ lines — CIA-A serial handshake protocol with
  sequence diagram, bit rotation quirk, complete key code map,
  key matrix bitmap, reset sequence, FPGA notes
2026-04-23 20:23:50 -04:00

7.4 KiB
Raw Blame History

← Home · Devices

timer.device — Timing, Delays, and High-Resolution Timestamps

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).


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?

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()"]

Hardware Foundation

CIA Timer Internals

The timing hardware lives in the two CIA (Complex Interface Adapter) chips:

CIA Base 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.

/* E-clock ticks per second: */
#define ECLOCK_PAL   709379
#define ECLOCK_NTSC  715909

/* Example: 100 ms delay = 70,938 ticks (PAL) */

VBlank Timing

UNIT_VBLANK piggybacks on the vertical blank interrupt — one tick per video frame:

Standard VBlank Rate Resolution
PAL 50 Hz 20.0 ms
NTSC 60 Hz 16.7 ms

Structures

/* devices/timer.h — NDK39 */
struct timeval {
    ULONG tv_secs;    /* seconds */
    ULONG tv_micro;   /* microseconds (0999999) */
};

struct timerequest {
    struct IORequest tr_node;
    struct timeval   tr_time;
};
/* sizeof(timerequest) = sizeof(IORequest) + 8 */

EClockVal (OS 2.0+)

struct EClockVal {
    ULONG ev_hi;  /* high 32 bits of 64-bit tick counter */
    ULONG ev_lo;  /* low 32 bits */
};

Opening timer.device

struct MsgPort *timerPort = CreateMsgPort();
struct timerequest *tr = (struct timerequest *)
    CreateIORequest(timerPort, sizeof(struct timerequest));

BYTE err = OpenDevice("timer.device", UNIT_MICROHZ,
                       (struct IORequest *)tr, 0);
if (err != 0) { /* handle error */ }

/* IMPORTANT: after opening, you can get TimerBase for direct calls: */
struct Library *TimerBase = (struct Library *)tr->tr_node.io_Device;
/* Now you can call AddTime(), SubTime(), CmpTime(), ReadEClock() */

Simple Delay

/* 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 */

Non-Blocking Delay

/* 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

/* Wait for timer OR user input: */
ULONG timerSig = 1L << timerPort->mp_SigBit;
ULONG windowSig = 1L << window->UserPort->mp_SigBit;

SendIO((struct IORequest *)tr);

ULONG sigs = Wait(timerSig | windowSig);
if (sigs & timerSig) {
    WaitIO((struct IORequest *)tr);
    /* handle timeout */
}
if (sigs & windowSig) {
    AbortIO((struct IORequest *)tr);
    WaitIO((struct IORequest *)tr);
    /* handle window event */
}

Getting Current Time

/* 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

/* 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)

/* 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)

/* Classic pattern: periodic callback using timer.device */
#define FRAME_USEC  20000  /* 50 Hz (PAL frame rate) */

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;
    SendIO((struct IORequest *)tr);

    BOOL running = TRUE;
    while (running)
    {
        ULONG sigs = Wait(timerSig | SIGBREAKF_CTRL_C);

        if (sigs & timerSig)
        {
            WaitIO((struct IORequest *)tr);

            /* --- Game logic here --- */
            UpdateGame();
            RenderFrame();

            /* Re-arm timer: */
            tr->tr_time.tv_secs  = 0;
            tr->tr_time.tv_micro = FRAME_USEC;
            SendIO((struct IORequest *)tr);
        }

        if (sigs & SIGBREAKF_CTRL_C)
            running = FALSE;
    }

    AbortIO((struct IORequest *)tr);
    WaitIO((struct IORequest *)tr);
}

Common Pitfalls

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

References

  • NDK39: devices/timer.h
  • ADCD 2.1: timer.device autodocs
  • HRM: CIA timer chapter
  • See also: interrupts.md — VBlank interrupt chain