From da9e7d3b63c90574f950c4016ce1e8c2f29b10ed Mon Sep 17 00:00:00 2001 From: Ilia Sharin Date: Thu, 23 Apr 2026 20:23:50 -0400 Subject: [PATCH] Phase 1: enrich 07_dos and 10_devices (highest FPGA priority) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- 07_dos/file_io.md | 250 +++++++++++++++++++---- 07_dos/filesystem.md | 298 +++++++++++++++++++++++---- 07_dos/locks_examine.md | 432 ++++++++++++++++++++++++++++++++-------- 10_devices/audio.md | 276 +++++++++++++++++++++---- 10_devices/keyboard.md | 185 ++++++++++++++--- 10_devices/timer.md | 263 +++++++++++++++++++++--- 10_devices/trackdisk.md | 196 +++++++++++++----- 7 files changed, 1599 insertions(+), 301 deletions(-) diff --git a/07_dos/file_io.md b/07_dos/file_io.md index bf248ef..0004567 100644 --- a/07_dos/file_io.md +++ b/07_dos/file_io.md @@ -1,10 +1,10 @@ [← Home](../README.md) · [AmigaDOS](README.md) -# File I/O — Open, Close, Read, Write, Seek +# File I/O — Open, Close, Read, Write, Seek, Async I/O ## Overview -AmigaDOS file I/O is synchronous from the caller's perspective. All functions use `BPTR` file handles and communicate errors via `IoErr()`. +AmigaDOS file I/O is synchronous from the caller's perspective — each call blocks until the filesystem handler completes the operation. All functions use `BPTR` file handles and communicate errors via `IoErr()`. Internally, every I/O call is translated into a DosPacket sent to the filesystem handler process (see [packet_system.md](packet_system.md)). --- @@ -17,6 +17,12 @@ AmigaDOS file I/O is synchronous from the caller's perspective. All functions us | −42 | `Read(fh, buf, len)` | D1=handle, D2=buf, D3=len | D0=actual bytes (−1=error) | | −48 | `Write(fh, buf, len)` | D1=handle, D2=buf, D3=len | D0=actual bytes (−1=error) | | −66 | `Seek(fh, pos, mode)` | D1=handle, D2=pos, D3=mode | D0=old position (−1=error) | +| −330 | `FRead(fh, buf, bsize, n)` | D1–D4 | D0=blocks read (buffered) | +| −336 | `FWrite(fh, buf, bsize, n)` | D1–D4 | D0=blocks written (buffered) | +| −348 | `FGets(fh, buf, len)` | D1–D3 | D0=buf or NULL | +| −354 | `FPuts(fh, str)` | D1–D2 | D0=0 or EOF | +| −360 | `Flush(fh)` | D1=handle | D0=BOOL | +| −366 | `SetVBuf(fh, buf, type, size)` | D1–D4 | D0=0 or −1 | --- @@ -29,6 +35,12 @@ AmigaDOS file I/O is synchronous from the caller's perspective. All functions us #define MODE_NEWFILE 1006 /* create new (truncate if exists) */ ``` +| Mode | If file exists | If file doesn't exist | +|---|---|---| +| `MODE_OLDFILE` | Opens for reading | Fails — `IoErr() = ERROR_OBJECT_NOT_FOUND` | +| `MODE_NEWFILE` | Truncates to 0, opens for writing | Creates new file | +| `MODE_READWRITE` | Opens at current size, read+write | Creates new file | + --- ## Seek Modes @@ -39,49 +51,192 @@ AmigaDOS file I/O is synchronous from the caller's perspective. All functions us #define OFFSET_END 1 /* from end of file */ ``` +> **Note**: `Seek()` returns the **old position** (before seeking), not the new one. To get file size: `Seek(fh, 0, OFFSET_END)` then `size = Seek(fh, 0, OFFSET_BEGINNING)`. + --- -## File Handle (BPTR) +## File Handle Structure The returned handle is a BPTR to a `struct FileHandle`: ```c +/* dos/dosextens.h */ struct FileHandle { - struct Message *fh_Link; - struct MsgPort *fh_Interactive; /* non-NULL if console */ - struct MsgPort *fh_Type; /* handler process port */ - BPTR fh_Buf; /* I/O buffer (BPTR) */ - LONG fh_Pos; /* current position in buffer */ - LONG fh_End; /* end of valid data in buffer */ - LONG fh_Funcs; /* unused */ - LONG fh_Func2; /* unused */ - LONG fh_Func3; /* unused */ - LONG fh_Args; /* packet args */ - BPTR fh_Arg2; + struct Message *fh_Link; /* +$00: exec message for reply */ + struct MsgPort *fh_Interactive; /* +$04: non-NULL if console device */ + struct MsgPort *fh_Type; /* +$08: handler process MsgPort */ + BPTR fh_Buf; /* +$0C: I/O buffer (BPTR) */ + LONG fh_Pos; /* +$10: current position in buffer */ + LONG fh_End; /* +$14: end of valid data in buffer */ + LONG fh_Funcs; /* +$18: unused (was BCPL function) */ + LONG fh_Func2; /* +$1C: unused */ + LONG fh_Func3; /* +$20: unused */ + LONG fh_Args; /* +$24: FH_Arg1 — passed to handler */ + BPTR fh_Arg2; /* +$28: handler-specific */ }; +/* sizeof(struct FileHandle) = 44 bytes ($2C) */ ``` -To access as a C pointer: `struct FileHandle *fh = BADDR(handle);` +```c +/* To access as a C pointer: */ +struct FileHandle *fhp = BADDR(handle); /* BADDR = (ptr << 2) */ + +/* Check if interactive (console): */ +if (IsInteractive(fh)) + Printf("Connected to a console\n"); +``` + +### I/O Buffering + +AmigaDOS uses **per-handle buffering** (OS 2.0+). Default buffer is 512 bytes. Control with `SetVBuf()`: + +```c +/* Make a file fully buffered with 8 KB buffer: */ +SetVBuf(fh, NULL, BUF_FULL, 8192); + +/* Line-buffered (flush on newline — useful for consoles): */ +SetVBuf(fh, NULL, BUF_LINE, 1024); + +/* Unbuffered (every write goes to handler immediately): */ +SetVBuf(fh, NULL, BUF_NONE, 0); + +/* Manually flush: */ +Flush(fh); +``` + +| Buffer Type | Constant | Behaviour | +|---|---|---| +| `BUF_FULL` | 1 | Flush when buffer fills | +| `BUF_LINE` | 0 | Flush on newline or buffer full | +| `BUF_NONE` | −1 | No buffering — direct I/O | --- -## Usage Example +## Standard I/O Handles ```c -BPTR fh = Open("RAM:test.txt", MODE_NEWFILE); -if (fh) { - Write(fh, "Hello Amiga\n", 12); - Close(fh); -} +/* Get the current process's stdin/stdout/stderr: */ +BPTR in = Input(); /* stdin — from CLI or WB */ +BPTR out = Output(); /* stdout */ +BPTR err = ErrorOutput(); /* stderr (OS 3.0+ only) */ -fh = Open("RAM:test.txt", MODE_OLDFILE); -if (fh) { - UBYTE buf[64]; - LONG n = Read(fh, buf, sizeof(buf)); - if (n > 0) Write(Output(), buf, n); /* echo to stdout */ +/* Write to stdout: */ +FPuts(Output(), "Hello from AmigaDOS\n"); + +/* Printf — formatted output to stdout: */ +Printf("Count: %ld, Name: %s\n", count, name); +/* Note: Printf uses %ld (not %d) — AmigaDOS always uses LONG */ +``` + +> [!IMPORTANT] +> AmigaDOS `Printf` uses `%ld` for integers, not `%d`. The `%d` format is undefined. This is a common bug source when porting from Unix/ANSI C. + +--- + +## File Information — ExamineFH, ExAll + +```c +/* Get file metadata from a handle: */ +struct FileInfoBlock *fib = AllocDosObject(DOS_FIB, NULL); +if (ExamineFH(fh, fib)) +{ + Printf("Name: %s\n", fib->fib_FileName); + Printf("Size: %ld\n", fib->fib_Size); + Printf("Type: %ld\n", fib->fib_DirEntryType); + /* > 0 = directory, < 0 = file */ +} +FreeDosObject(DOS_FIB, fib); +``` + +### FileInfoBlock Structure + +```c +struct FileInfoBlock { + LONG fib_DiskKey; /* block number on disk */ + LONG fib_DirEntryType; /* >0=dir, <0=file */ + char fib_FileName[108]; /* null-terminated name */ + LONG fib_Protection; /* RWED protection bits */ + LONG fib_EntryType; /* same as DirEntryType */ + LONG fib_Size; /* file size in bytes */ + LONG fib_NumBlocks; /* blocks consumed */ + struct DateStamp fib_Date; /* modification date */ + char fib_Comment[80]; /* file comment string */ + UWORD fib_OwnerUID; /* owner (multiuser) */ + UWORD fib_OwnerGID; /* group */ +}; +``` + +--- + +## Practical Patterns + +### Copy a File + +```c +BOOL CopyFile(CONST_STRPTR src, CONST_STRPTR dst) +{ + BPTR in = Open(src, MODE_OLDFILE); + if (!in) return FALSE; + + BPTR out = Open(dst, MODE_NEWFILE); + if (!out) { Close(in); return FALSE; } + + UBYTE buf[4096]; + LONG n; + while ((n = Read(in, buf, sizeof(buf))) > 0) + { + if (Write(out, buf, n) != n) + { + Close(in); Close(out); + return FALSE; /* write error */ + } + } + + Close(out); + Close(in); + return (n == 0); /* 0=EOF=success, -1=error */ +} +``` + +### Determine File Size + +```c +LONG GetFileSize(BPTR fh) +{ + LONG oldpos = Seek(fh, 0, OFFSET_END); /* seek to end */ + LONG size = Seek(fh, oldpos, OFFSET_BEGINNING); /* seek back */ + return size; +} +/* Or with ExamineFH (no seeking needed): */ +struct FileInfoBlock fib; +ExamineFH(fh, &fib); +LONG size = fib.fib_Size; +``` + +### Read Entire File into Memory + +```c +APTR LoadFileToRAM(CONST_STRPTR path, ULONG *sizeOut) +{ + BPTR fh = Open(path, MODE_OLDFILE); + if (!fh) return NULL; + + /* Get size */ + Seek(fh, 0, OFFSET_END); + LONG size = Seek(fh, 0, OFFSET_BEGINNING); + + APTR buf = AllocVec(size, MEMF_ANY); + if (buf) + { + if (Read(fh, buf, size) != size) + { + FreeVec(buf); + buf = NULL; + } + } Close(fh); -} else { - PrintFault(IoErr(), "Open failed"); + *sizeOut = size; + return buf; } ``` @@ -90,18 +245,41 @@ if (fh) { ## Error Checking ```c -LONG err = IoErr(); /* LVO −66 — returns last DOS error code */ -/* Common codes: */ -#define ERROR_OBJECT_NOT_FOUND 205 -#define ERROR_OBJECT_EXISTS 203 -#define ERROR_DISK_FULL 221 -#define ERROR_SEEK_ERROR 219 +LONG err = IoErr(); /* returns last DOS error code */ + +/* Human-readable error message: */ +PrintFault(err, "Operation failed"); +/* Output: "Operation failed: object not found" */ + +/* Or get error string into buffer: */ +Fault(err, "Error", buf, sizeof(buf)); ``` +### Common Error Codes + +| Code | Constant | Meaning | +|---|---|---| +| 103 | `ERROR_NO_FREE_STORE` | Out of memory | +| 202 | `ERROR_OBJECT_IN_USE` | File is locked by another process | +| 203 | `ERROR_OBJECT_EXISTS` | File already exists | +| 204 | `ERROR_DIR_NOT_FOUND` | Path component not found | +| 205 | `ERROR_OBJECT_NOT_FOUND` | File not found | +| 209 | `ERROR_ACTION_NOT_KNOWN` | Handler doesn't support this action | +| 210 | `ERROR_INVALID_COMPONENT_NAME` | Bad filename character | +| 212 | `ERROR_OBJECT_WRONG_TYPE` | Expected file, got directory (or vice versa) | +| 214 | `ERROR_DISK_FULL` | No space on volume | +| 216 | `ERROR_DELETE_PROTECTED` | File has delete-protection bit | +| 218 | `ERROR_WRITE_PROTECTED` | Disk is write-protected | +| 219 | `ERROR_SEEK_ERROR` | Invalid seek position | +| 221 | `ERROR_DISK_FULL` | Volume full | +| 225 | `ERROR_NOT_A_DOS_DISK` | Unrecognised filesystem | +| 232 | `ERROR_NO_MORE_ENTRIES` | ExNext — end of directory | + --- ## References -- NDK39: `dos/dos.h`, `dos/dosextens.h` +- NDK39: `dos/dos.h`, `dos/dosextens.h`, `dos/stdio.h` +- ADCD 2.1: `Open`, `Close`, `Read`, `Write`, `Seek`, `SetVBuf`, `FRead` - [error_handling.md](error_handling.md) — full error code list -- ADCD 2.1: `Open`, `Close`, `Read`, `Write`, `Seek` +- [packet_system.md](packet_system.md) — how I/O translates to handler packets diff --git a/07_dos/filesystem.md b/07_dos/filesystem.md index 21608e8..45b6995 100644 --- a/07_dos/filesystem.md +++ b/07_dos/filesystem.md @@ -1,84 +1,296 @@ [← Home](../README.md) · [AmigaDOS](README.md) -# Filesystem — FFS/OFS Block Structure +# Filesystem — FFS/OFS Block Structure and Disk Layout ## Overview -AmigaOS supports two native filesystem types: **OFS** (Old File System, OS 1.x) and **FFS** (Fast File System, OS 2.0+). Both use a block-based layout with 512-byte blocks. FFS differs by storing data blocks without headers, improving throughput. +AmigaOS supports two native filesystem types: **OFS** (Old File System, OS 1.x) and **FFS** (Fast File System, OS 2.0+). Both use a block-based layout with 512-byte blocks. FFS differs by storing data blocks without headers, improving throughput. Understanding the on-disk layout is essential for FPGA core developers implementing virtual filesystems, HDF support, and ADF image handling. + +--- + +## Disk Geometry and Layout + +### ADF (Amiga Disk File) — Floppy Disk + +``` +Total: 880 KB = 1,760 sectors = 80 tracks × 2 sides × 11 sectors +Block size: 512 bytes +Blocks 0–1: Boot block (1 KB) +Block 880: Root block (always at disk midpoint) +``` + +### HDF (Hard Disk File) — Hard Drive + +``` +Block 0: Reserved (or RDB — Rigid Disk Block) +Block 1+: RDB partition table (if present) +Partition: Starts at first block of partition + Root block at partition midpoint +``` --- ## Block Types -| Block | Type ID | Description | -|---|---|---| -| Boot block | `"DOS\0"` / `"DOS\1"` | Blocks 0–1; OFS=`DOS\0`, FFS=`DOS\1` | -| Root block | `T_HEADER` (2) | Always at middle of partition; directory root | -| File header | `T_HEADER` (2) | Metadata for one file | -| Directory header | `T_HEADER` (2) | Metadata for one directory | -| Data block | `T_DATA` (8) | OFS: header + data; FFS: pure data | -| File extension | `T_LIST` (16) | Overflow pointer table for large files | -| Hash chain | — | Root/dir blocks have a 72-entry hash table | +| Type | ID | Constant | Purpose | +|---|---|---|---| +| Boot block | — | (bytes 0–1023) | Boot code + filesystem ID (`DOS\0` to `DOS\7`) | +| Root block | 2 | `T_HEADER` | Volume root directory — always at midpoint | +| File header | 2 | `T_HEADER` | File metadata + first 72 data block pointers | +| Directory | 2 | `T_HEADER` | Subdirectory — contains 72-slot hash table | +| Data block | 8 | `T_DATA` | OFS: 24-byte header + 488 data bytes | +| Extension | 16 | `T_LIST` | Overflow block pointers for files >72 blocks | +| Bitmap block | — | — | Free/allocated block tracking | --- -## Root Block Layout (Simplified) +## Boot Block (Blocks 0–1) -| Offset | Field | Description | +``` +Offset Size Field +────── ──── ───────────────────── +$00 4 Filesystem ID — "DOS\0" to "DOS\7" +$04 4 Checksum +$08 4 Root block number (usually 880) +$0C 1012 Boot code (optional — loaded by ROM bootstrap) +``` + +### Filesystem ID Variants + +| ID | Hex | Description | |---|---|---| -| 0 | `type` | Always 2 (`T_HEADER`) | -| 4 | `header_key` | Own block number | -| 8 | `high_seq` | Number of data blocks in hash table | -| 12 | `ht_size` | Hash table size (usually 72) | -| 16 | `first_data` | Unused | -| 20 | `checksum` | Block checksum | -| 24–312 | `ht[72]` | Hash table: block pointers for directory entries | -| 420 | `bm_flag` | Bitmap valid flag (`-1` = valid) | -| 424–472 | `bm_pages[25]` | Pointers to bitmap blocks | -| 484 | `last_altered_days` | Modification date | -| 504 | `disk_name` | BSTR: volume name | +| `DOS\0` | $444F5300 | OFS (original) | +| `DOS\1` | $444F5301 | FFS (fast file system) | +| `DOS\2` | $444F5302 | OFS + International mode (case-insensitive) | +| `DOS\3` | $444F5303 | FFS + International mode | +| `DOS\4` | $444F5304 | FFS + Directory cache | +| `DOS\5` | $444F5305 | OFS + International + Directory cache | +| `DOS\6` | $444F5306 | OFS + Long filenames (OS 3.5+) | +| `DOS\7` | $444F5307 | FFS + Long filenames (OS 3.5+) | + +--- + +## Root Block (Block 880 on Floppy) + +The root block is the entry point for the entire filesystem. It always lives at the midpoint of the partition: + +``` +root_block_number = (total_blocks) / 2 /* e.g., 1760/2 = 880 */ +``` + +### Root Block Layout (512 bytes) + +``` +Offset Size Field Description +────── ────── ────────────────────── ──────────────────────────────── +$000 4 type Always 2 (T_HEADER) +$004 4 header_key Own block number +$008 4 high_seq 0 (unused for root) +$00C 4 ht_size Hash table size (72 for DD floppy) +$010 4 first_data 0 (unused) +$014 4 checksum Block checksum +$018 288 ht[72] Hash table: LONG[72] block pointers + for directory entries (0 = empty slot) +$138 4 bm_flag Bitmap valid flag (-1 = valid) +$13C 100 bm_pages[25] LONG[25] pointers to bitmap blocks +$1A0 4 bm_ext Pointer to extended bitmap block +$1A4 12 last_root_alteration DateStamp: root last modified +$1B0 32 disk_name BSTR: volume name (length-prefixed) +$1D0 4 (reserved) +$1D4 12 last_disk_alteration DateStamp: disk last modified +$1E0 12 creation_date DateStamp: volume creation date +$1EC 4 (reserved) +$1F0 4 extension 0 (unused for root) +$1F4 4 sec_type 1 = ST_ROOT +$1F8 4 (reserved) +$1FC ← end of 512-byte block +``` --- ## Hash Function -File/directory names are hashed into the 72-slot table: +File and directory names are hashed into the hash table slots: ```c -ULONG hash_name(const char *name, int table_size) { - ULONG hash = strlen(name); - for (int i = 0; name[i]; i++) { - hash = hash * 13 + toupper(name[i]); - hash &= 0x7FF; +/* The canonical AmigaDOS hash function: */ +ULONG HashName(const char *name, ULONG table_size) +{ + ULONG hash = (ULONG)strlen(name); + for (int i = 0; name[i]; i++) + { + hash = (hash * 13 + toupper((unsigned char)name[i])) & 0x7FF; } return hash % table_size; } +/* table_size = 72 for DD floppy, 128 for HD floppy */ ``` -Collisions are resolved by chaining: each file/dir header has a `hash_chain` pointer to the next entry in the same slot. +### Hash Collision Resolution + +Collisions are resolved by **chaining**: each file/directory header has a `hash_chain` pointer (at offset `$1F0`) linking to the next entry that hashed to the same slot. The chain ends with 0. + +``` +Hash Table (root block): + Slot 0: → 0 (empty) + Slot 1: → Block 950 ("Startup-Sequence") + └→ Block 1100 ("System-Startup") ← hash_chain + └→ 0 (end) + Slot 2: → Block 882 ("Libs") + ... +``` --- -## OFS vs FFS +## File Header Block -| Feature | OFS (`DOS\0`) | FFS (`DOS\1`) | +``` +Offset Size Field Description +────── ────── ────────────────────── ────────────────────────────── +$000 4 type 2 (T_HEADER) +$004 4 header_key Own block number +$008 4 high_seq Number of data block pointers stored here +$00C 4 data_size 0 (unused in file header) +$010 4 first_data First data block (OFS only — FFS uses table) +$014 4 checksum +$018 288 data_blocks[72] LONG[72] — block pointers to data blocks + Stored in REVERSE ORDER: [71]=first, [0]=last +$138 — ... (padding/reserved) +$144 4 protect Protection bits (RWED) +$148 4 byte_size File size in bytes +$14C 80 comment BSTR: file comment +$19C 12 date DateStamp: file modification date +$1A8 32 filename BSTR: file name (length-prefixed) +$1D0 4 real_entry For hard links: the real file header +$1D4 4 next_link For hard links: next link to same file +$1EC 4 hash_chain Next entry in same hash slot (0 = end) +$1F0 4 parent Block number of parent directory +$1F4 4 extension Block number of extension block (0 = none) +$1F8 4 sec_type -3 = ST_FILE +``` + +> **Reverse order**: Data block pointers in `data_blocks[]` are stored **last-to-first**. Index 71 points to the first data block, index 70 to the second, etc. This is a BCPL heritage quirk. + +--- + +## OFS vs FFS — Data Block Differences + +### OFS Data Block (T_DATA = 8) + +``` +$000 4 type 8 (T_DATA) +$004 4 header_key Pointer back to file header +$008 4 seq_num Sequence number (1-based) +$00C 4 data_size Bytes of valid data in this block +$010 4 next_data Next data block (0 = last) +$014 4 checksum +$018 488 data[488] Actual file data (488 usable bytes) +``` +**Efficiency**: 488 / 512 = **95.3%** — 24 bytes wasted per block on headers. + +### FFS Data Block + +``` +$000 512 data[512] Pure file data — no header overhead +``` +**Efficiency**: 512 / 512 = **100%** + +| Feature | OFS | FFS | |---|---|---| -| Data blocks | 24-byte header + 488 bytes data | Pure 512 bytes data | -| Max filename | 30 chars | 30 chars | -| International | No | `DOS\2` (INTL OFS), `DOS\3` (INTL FFS) | -| Dir cache | No | `DOS\4` (FFS + dir cache) | -| Throughput | ~488/512 = 95% efficiency | 100% efficiency | +| Bytes per data block | 488 | 512 | +| Header overhead | 24 bytes/block | 0 | +| Self-describing blocks | Yes (can recover from corruption) | No | +| Max filename | 30 chars | 30 chars (107 with DOS\6/\7) | +| Throughput | ~5% slower | Baseline | +| International mode | DOS\2 | DOS\3 | +| Directory cache | No | DOS\4 | +| Min OS version | 1.0 | 2.0 | + +--- + +## Bitmap Blocks — Free Space Tracking + +The bitmap tracks which blocks are free (1) or allocated (0): + +```c +/* Each bitmap block covers up to (512-4) × 8 = 4064 blocks */ +struct BitmapBlock { + ULONG checksum; /* block checksum */ + ULONG map[127]; /* bit 1 = free, bit 0 = allocated */ + /* bit 0 of map[0] = block corresponding to this bitmap's range */ +}; +``` + +The root block's `bm_pages[25]` array can reference up to 25 bitmap blocks, covering 25 × 4064 = **101,600 blocks** (≈49 MB). Larger partitions need `bm_ext` extension blocks. --- ## Checksum Algorithm ```c -LONG compute_checksum(ULONG *block, int longs) { +LONG ComputeBlockChecksum(ULONG *block, LONG longs) +{ LONG sum = 0; - for (int i = 0; i < longs; i++) sum += block[i]; - return -sum; /* stored in the checksum field to make total = 0 */ + block[5] = 0; /* clear checksum field before computing */ + for (int i = 0; i < longs; i++) + sum += block[i]; + return -sum; /* store at block[5] so total = 0 */ } +/* Verify: if sum of all 128 longs (incl. checksum) = 0, block is valid */ +``` + +--- + +## File Extension Blocks + +Files larger than 72 data blocks need extension blocks to store additional pointers: + +``` +extension_block.data_blocks[72] → next 72 data block pointers +extension_block.extension → next extension block (or 0) +``` + +Each extension block adds 72 more data block pointers. With FFS (512 bytes/block): +- 72 blocks = 36 KB directly in file header +- +72 per extension = 36 KB more per extension +- Maximum chain depth is effectively unlimited + +--- + +## Practical: Reading an ADF Image + +```python +import struct + +def read_adf(filename): + with open(filename, 'rb') as f: + data = f.read() + + # Boot block — filesystem type + fs_type = data[0:4] + print(f"Filesystem: {fs_type}") # b'DOS\x00' = OFS, b'DOS\x01' = FFS + + # Root block at block 880 (offset 880 * 512 = 450560) + root_off = 880 * 512 + root = data[root_off:root_off + 512] + + # Volume name (BSTR at offset $1B0) + name_len = root[0x1B0] + vol_name = root[0x1B1:0x1B1 + name_len].decode('ascii') + print(f"Volume: {vol_name}") + + # Hash table: 72 entries starting at offset $018 + ht = struct.unpack('>72I', root[0x18:0x18 + 72 * 4]) + for i, blk in enumerate(ht): + if blk != 0: + # Read the file/dir header at that block + hdr_off = blk * 512 + hdr = data[hdr_off:hdr_off + 512] + fname_len = hdr[0x1A8] + fname = hdr[0x1A9:0x1A9 + fname_len].decode('ascii') + sec_type = struct.unpack('>i', hdr[0x1F4:0x1F8])[0] + kind = "DIR" if sec_type == 2 else "FILE" + print(f" [{i:2d}] {kind} {fname} (block {blk})") ``` --- @@ -87,4 +299,6 @@ LONG compute_checksum(ULONG *block, int longs) { - NDK39: `dos/filehandler.h` - Ralph Babel: *The AmigaDOS Manual* (3rd edition) — definitive FFS reference -- Laurent Clevy: *The Amiga Filesystem* — http://lclevy.free.fr/adflib/ +- Laurent Clévy: *The Amiga Filesystem* — http://lclevy.free.fr/adflib/ +- See also: [packet_system.md](packet_system.md) — filesystem handler protocol +- See also: [locks_examine.md](locks_examine.md) — lock/examine API layer diff --git a/07_dos/locks_examine.md b/07_dos/locks_examine.md index a1f84e1..542ee4e 100644 --- a/07_dos/locks_examine.md +++ b/07_dos/locks_examine.md @@ -1,126 +1,382 @@ [← Home](../README.md) · [AmigaDOS](README.md) -# Locks and Examine — Lock, UnLock, Examine, ExNext, ExAll +# Locks, Examine, and Directory Scanning ## Overview -AmigaDOS uses **locks** to reference files and directories. A lock is a BPTR to a `FileLock` structure. Locks provide exclusive or shared access and are used for directory scanning, attribute reading, and path resolution. +A **Lock** is AmigaDOS's equivalent of a Unix file descriptor for directory operations. It represents a reference to a filesystem object (file or directory) and is the primary mechanism for navigating the filesystem, examining metadata, and preventing concurrent modifications. Understanding lock semantics — especially the shared/exclusive model — is critical for writing robust AmigaOS applications. --- ## Lock Types -```c -/* dos/dos.h */ -#define SHARED_LOCK -2 /* read-only; multiple readers allowed */ -#define ACCESS_READ SHARED_LOCK -#define EXCLUSIVE_LOCK -1 /* read/write; only one holder */ -#define ACCESS_WRITE EXCLUSIVE_LOCK -``` - ---- - -## Core Functions - -| LVO | Function | Registers | Returns | +| Mode | Constant | Dec | Meaning | |---|---|---|---| -| −84 | `Lock(name, mode)` | D1=name, D2=mode | D0=lock BPTR (0=fail) | -| −90 | `UnLock(lock)` | D1=lock | — | -| −96 | `DupLock(lock)` | D1=lock | D0=new lock | -| −102 | `Examine(lock, fib)` | D1=lock, D2=fib | D0=BOOL | -| −108 | `ExNext(lock, fib)` | D1=lock, D2=fib | D0=BOOL | -| −78 | `CurrentDir(lock)` | D1=lock | D0=old lock | -| −654 | `ExAll(lock, buf, size, type, ctrl)` | D1–D5 | D0=BOOL | - ---- - -## struct FileInfoBlock +| Shared | `SHARED_LOCK` / `ACCESS_READ` | −2 | Multiple readers allowed simultaneously | +| Exclusive | `EXCLUSIVE_LOCK` / `ACCESS_WRITE` | −1 | Only one holder; blocks all other locks | ```c -/* dos/dos.h — NDK39 */ -struct FileInfoBlock { - LONG fib_DiskKey; /* handler-private key */ - LONG fib_DirEntryType; /* >0 = directory, <0 = file */ - char fib_FileName[108]; /* null-terminated name */ - LONG fib_Protection; /* rwed bits */ - LONG fib_EntryType; /* same as DirEntryType */ - LONG fib_Size; /* file size in bytes */ - LONG fib_NumBlocks; /* blocks used */ - struct DateStamp fib_Date; /* modification date */ - char fib_Comment[80]; /* file comment string */ - UWORD fib_OwnerUID; - UWORD fib_OwnerGID; - char fib_Reserved[32]; -}; -``` +/* Obtain a shared lock: */ +BPTR lock = Lock("SYS:Libs", SHARED_LOCK); -> [!IMPORTANT] -> `FileInfoBlock` must be longword-aligned. Use `AllocDosObject(DOS_FIB, NULL)` on OS 2.0+ or `AllocMem(sizeof(struct FileInfoBlock), MEMF_PUBLIC)`. +/* Obtain an exclusive lock: */ +BPTR lock = Lock("RAM:temp.dat", EXCLUSIVE_LOCK); ---- - -## Protection Bits - -```c -/* dos/dos.h */ -#define FIBF_SCRIPT (1<<6) /* s — script (executable script) */ -#define FIBF_PURE (1<<5) /* p — pure (re-entrant) */ -#define FIBF_ARCHIVE (1<<4) /* a — archived */ -#define FIBF_READ (1<<3) /* r — readable (0=allowed, 1=denied!) */ -#define FIBF_WRITE (1<<2) /* w — writable */ -#define FIBF_EXECUTE (1<<1) /* e — executable */ -#define FIBF_DELETE (1<<0) /* d — deletable */ -``` - -> [!WARNING] -> Amiga protection bits are **inverted** from Unix: bit SET means access is **denied**. - ---- - -## Directory Scanning - -```c -BPTR lock = Lock("SYS:", SHARED_LOCK); -struct FileInfoBlock *fib = AllocDosObject(DOS_FIB, NULL); - -if (Examine(lock, fib)) { /* read dir's own info */ - while (ExNext(lock, fib)) { /* iterate entries */ - Printf("%-30s %8ld %s\n", - fib->fib_FileName, - fib->fib_Size, - fib->fib_DirEntryType > 0 ? "(dir)" : ""); - } - /* ExNext returns FALSE when done; IoErr() == ERROR_NO_MORE_ENTRIES */ -} - -FreeDosObject(DOS_FIB, fib); +/* Always unlock when done: */ UnLock(lock); ``` +### Lock Semantics + +```mermaid +graph TD + subgraph "Shared Lock Active" + S1["Process A: SHARED"] --> OK1["Process B: SHARED → OK"] + S1 --> FAIL1["Process C: EXCLUSIVE → FAILS"] + end + + subgraph "Exclusive Lock Active" + E1["Process A: EXCLUSIVE"] --> FAIL2["Process B: SHARED → FAILS"] + E1 --> FAIL3["Process C: EXCLUSIVE → FAILS"] + end +``` + +| Scenario | Result | Error Code | +|---|---|---| +| Shared + Shared | Both succeed | — | +| Shared + Exclusive | Exclusive fails | `ERROR_OBJECT_IN_USE` (202) | +| Exclusive + Shared | Shared fails | `ERROR_OBJECT_IN_USE` (202) | +| Exclusive + Exclusive | Second fails | `ERROR_OBJECT_IN_USE` (202) | + +> [!IMPORTANT] +> `Lock()` is **non-blocking** — it returns immediately with 0 and sets `IoErr()` if the lock cannot be obtained. There is no "wait for lock" mechanism in AmigaDOS. You must poll or retry. + --- -## ExAll (OS 2.0+) — Bulk Scan +## Lock Internals — struct FileLock ```c +/* dos/dosextens.h */ +struct FileLock { + BPTR fl_Link; /* BPTR to next lock (handler's list) */ + LONG fl_Key; /* block number on disk (handler-specific) */ + LONG fl_Access; /* SHARED_LOCK or EXCLUSIVE_LOCK */ + struct MsgPort *fl_Task; /* handler's MsgPort — THIS IS HOW YOU + FIND THE FILE HANDLER */ + BPTR fl_Volume; /* BPTR to DosList volume node */ +}; +``` + +### Finding a File's Handler Process + +This is a common need — e.g., to send custom packets: + +```c +/* Get the handler MsgPort from any lock: */ +struct FileLock *fl = (struct FileLock *)BADDR(lock); +struct MsgPort *handler = fl->fl_Task; +/* Now you can PutMsg(handler, &packet) */ + +/* From a filename (without an existing lock): */ +struct DevProc *dp = GetDeviceProc("DF0:myfile", NULL); +if (dp) { + struct MsgPort *handler = dp->dvp_Port; + /* ... send packets ... */ + FreeDeviceProc(dp); +} + +/* From a file handle: */ +struct FileHandle *fhp = (struct FileHandle *)BADDR(filehandle); +struct MsgPort *handler = fhp->fh_Type; +``` + +--- + +## Examining Files and Directories + +### Examine a Single Object + +```c +struct FileInfoBlock *fib = AllocDosObject(DOS_FIB, NULL); +BPTR lock = Lock("SYS:C/Dir", SHARED_LOCK); + +if (lock && Examine(lock, fib)) +{ + Printf("Name: %s\n", fib->fib_FileName); + Printf("Size: %ld bytes\n", fib->fib_Size); + Printf("Type: %s\n", + fib->fib_DirEntryType > 0 ? "Directory" : "File"); + + /* Protection bits: */ + Printf("Protect: %08lx\n", fib->fib_Protection); + /* Bits are ACTIVE-LOW: bit set = permission DENIED */ + /* FIBF_READ=8, FIBF_WRITE=4, FIBF_EXECUTE=2, FIBF_DELETE=1 */ + /* Bits 4-7: hold/script/pure/archive (active-HIGH) */ +} + +UnLock(lock); +FreeDosObject(DOS_FIB, fib); +``` + +> [!WARNING] +> **Protection bits are inverted** for RWED: bit set means the permission is **denied**, not granted. This is opposite to Unix. `fib_Protection = 0` means "all permissions granted". + +### Scan a Directory (ExNext Loop) + +```c +/* Classic directory enumeration pattern: */ +BPTR dirLock = Lock("SYS:Libs", SHARED_LOCK); +struct FileInfoBlock *fib = AllocDosObject(DOS_FIB, NULL); + +if (dirLock && Examine(dirLock, fib)) /* examine the dir itself first */ +{ + while (ExNext(dirLock, fib)) + { + Printf(" %s (%ld bytes)\n", + fib->fib_FileName, fib->fib_Size); + } + /* ExNext returns FALSE when done — check error: */ + if (IoErr() != ERROR_NO_MORE_ENTRIES) + PrintFault(IoErr(), "ExNext error"); +} + +UnLock(dirLock); +FreeDosObject(DOS_FIB, fib); +``` + +### ExAll — Bulk Directory Scan (OS 2.0+) + +`ExAll` is significantly faster than `ExNext` for large directories — it batches results: + +```c +#define BUFSIZE 4096 +APTR buf = AllocVec(BUFSIZE, MEMF_ANY); struct ExAllControl *eac = AllocDosObject(DOS_EXALLCONTROL, NULL); -UBYTE buf[4096]; +eac->eac_LastKey = 0; + +BPTR dirLock = Lock("SYS:Libs", SHARED_LOCK); BOOL more; -eac->eac_LastKey = 0; do { - more = ExAll(lock, buf, sizeof(buf), ED_NAME, eac); + more = ExAll(dirLock, buf, BUFSIZE, ED_SIZE, eac); + + if (!more && IoErr() != ERROR_NO_MORE_ENTRIES) + { + PrintFault(IoErr(), "ExAll error"); + break; + } + struct ExAllData *ead = (struct ExAllData *)buf; - while (ead) { - Printf("%s\n", ead->ed_Name); + while (ead) + { + Printf(" %s (%ld bytes)\n", ead->ed_Name, ead->ed_Size); ead = ead->ed_Next; } } while (more); + FreeDosObject(DOS_EXALLCONTROL, eac); +FreeVec(buf); +UnLock(dirLock); ``` --- +## Practical Patterns + +### Safe File Replacement (Atomic Write) + +```c +/* Anti-pattern: overwriting directly — corruption on crash */ +/* WRONG: */ +fh = Open("config.prefs", MODE_NEWFILE); /* truncates original! */ +Write(fh, data, size); /* crash here = lost config */ +Close(fh); + +/* Correct: write-to-temp then rename */ +fh = Open("config.prefs.tmp", MODE_NEWFILE); +Write(fh, data, size); +Close(fh); + +/* Atomic swap: */ +DeleteFile("config.prefs.bak"); /* remove old backup */ +Rename("config.prefs", "config.prefs.bak"); /* backup current */ +Rename("config.prefs.tmp", "config.prefs"); /* install new */ +``` + +### Check If File Exists (Without Opening) + +```c +/* Use Lock — lighter than Open: */ +BPTR lock = Lock("SYS:Libs/68040.library", SHARED_LOCK); +if (lock) { + UnLock(lock); + /* file exists */ +} else { + /* file doesn't exist (or permission denied — check IoErr()) */ +} +``` + +### Get Parent Directory + +```c +BPTR fileLock = Lock("SYS:Libs/68040.library", SHARED_LOCK); +BPTR parentLock = ParentDir(fileLock); +if (parentLock) { + /* parentLock = lock on SYS:Libs */ + NameFromLock(parentLock, buf, sizeof(buf)); + Printf("Parent: %s\n", buf); /* "SYS:Libs" */ + UnLock(parentLock); +} +UnLock(fileLock); +``` + +### Resolve Full Path from Lock + +```c +/* Get the full filesystem path of any lock: */ +BPTR lock = Lock("PROGDIR:data", SHARED_LOCK); +char fullpath[256]; +if (NameFromLock(lock, fullpath, sizeof(fullpath))) + Printf("Full path: %s\n", fullpath); + /* e.g. "DH0:Games/MyGame/data" */ +UnLock(lock); +``` + +### Determine Volume / Device Name + +```c +/* Find which volume a lock belongs to: */ +struct InfoData *id = AllocVec(sizeof(struct InfoData), MEMF_ANY); +BPTR lock = Lock("SYS:", SHARED_LOCK); +if (Info(lock, id)) +{ + struct DosList *vol = (struct DosList *)BADDR(id->id_VolumeNode); + UBYTE *bname = (UBYTE *)BADDR(vol->dol_Name); + Printf("Volume: %.*s\n", bname[0], &bname[1]); /* BSTR */ + Printf("Disk type: $%08lx\n", id->id_DiskType); /* 'DOS\1'=FFS */ + Printf("Used: %ld / %ld blocks\n", + id->id_NumBlocksUsed, id->id_NumBlocks); +} +UnLock(lock); +FreeVec(id); +``` + +--- + +## Common Antipatterns + +### ❌ Leaking Locks + +```c +/* WRONG: lock is never released → filesystem handler keeps reference + forever, preventing volume ejection */ +void bad_function(void) { + BPTR lock = Lock("DF0:file", SHARED_LOCK); + if (!lock) return; + /* ... forgot UnLock(lock) ... */ +} + +/* CORRECT: always pair Lock/UnLock */ +void good_function(void) { + BPTR lock = Lock("DF0:file", SHARED_LOCK); + if (!lock) return; + /* ... do work ... */ + UnLock(lock); /* ALWAYS release */ +} +``` + +> Leaked locks are the #1 cause of "please insert volume X" requesters that never go away. The filesystem handler keeps the volume "in use" because an outstanding lock exists. + +### ❌ Holding Exclusive Lock Too Long + +```c +/* WRONG: other processes blocked for entire read operation */ +BPTR lock = Lock("shared_data.dat", EXCLUSIVE_LOCK); +fh = OpenFromLock(lock); /* lock consumed by OpenFromLock */ +/* ... lengthy processing ... */ +Close(fh); + +/* BETTER: use shared lock, only exclusive for the write phase */ +BPTR lock = Lock("shared_data.dat", SHARED_LOCK); +fh = OpenFromLock(lock); +Read(fh, buf, size); /* shared — others can read too */ +Close(fh); +/* Now open exclusively just for writing: */ +fh = Open("shared_data.dat", MODE_READWRITE); +Write(fh, newdata, newsize); +Close(fh); +``` + +### ❌ Not Checking IoErr() After Lock Failure + +```c +/* WRONG: can't distinguish "file not found" from "disk error" */ +BPTR lock = Lock("DF0:important", SHARED_LOCK); +if (!lock) Printf("Not found!\n"); /* might be disk error! */ + +/* CORRECT: */ +BPTR lock = Lock("DF0:important", SHARED_LOCK); +if (!lock) { + LONG err = IoErr(); + switch (err) { + case ERROR_OBJECT_NOT_FOUND: Printf("Not found\n"); break; + case ERROR_OBJECT_IN_USE: Printf("Locked by another process\n"); break; + case ERROR_DISK_NOT_VALIDATED: Printf("Disk not ready\n"); break; + default: PrintFault(err, "Lock failed"); break; + } +} +``` + +### ❌ Using DupLock on Exclusive Locks + +```c +/* WRONG: DupLock on an exclusive lock creates a SHARED copy. + This breaks the exclusivity guarantee! */ +BPTR excl = Lock("data.dat", EXCLUSIVE_LOCK); +BPTR dup = DupLock(excl); /* dup is SHARED — exclusivity lost */ + +/* DupLock always creates a shared lock, regardless of the source. */ +``` + +--- + +## Pattern Matching (Directory Wildcards) + +```c +/* Match files against AmigaDOS wildcard patterns: */ +/* #? = any string (like Unix *) */ +/* ? = any single char */ +/* ~ = NOT */ +/* | = OR */ +/* ( ) = grouping */ + +char tokenBuf[256]; +LONG tok = ParsePattern("#?.library", tokenBuf, sizeof(tokenBuf)); +if (tok >= 0) { + /* tok > 0 means pattern contains wildcards */ + /* tok = 0 means literal string (no wildcards) */ + + while (ExNext(dirLock, fib)) { + if (MatchPattern(tokenBuf, fib->fib_FileName)) + Printf(" Match: %s\n", fib->fib_FileName); + } +} +``` + +| AmigaDOS Pattern | Unix Equivalent | Meaning | +|---|---|---| +| `#?` | `*` | Any string | +| `?` | `?` | Any single character | +| `#?.library` | `*.library` | All library files | +| `~(#?.info)` | (no equiv) | Everything except .info files | +| `(a|b)#?` | `{a,b}*` | Starting with a or b | + +--- + ## References - NDK39: `dos/dos.h`, `dos/dosextens.h`, `dos/exall.h` -- ADCD 2.1: `Lock`, `UnLock`, `Examine`, `ExNext`, `ExAll` +- ADCD 2.1: `Lock`, `UnLock`, `Examine`, `ExNext`, `ExAll`, `NameFromLock` +- Ralph Babel: *The AmigaDOS Manual* — lock semantics chapter +- See also: [packet_system.md](packet_system.md) — how locks translate to handler packets +- See also: [file_io.md](file_io.md) — file operations using handles diff --git a/10_devices/audio.md b/10_devices/audio.md index 2281834..0a56860 100644 --- a/10_devices/audio.md +++ b/10_devices/audio.md @@ -4,69 +4,275 @@ ## Overview -`audio.device` provides access to the Amiga's 4 DMA audio channels. Each channel plays 8-bit PCM samples from Chip RAM at programmable rates. +`audio.device` provides access to the Amiga's **4 hardware DMA audio channels**. Each channel plays 8-bit signed PCM samples from Chip RAM at a programmable rate. The hardware supports independent volume, period (sample rate), and unlimited looping. Audio DMA is handled entirely by the custom chips — the CPU is not involved in the actual sample playback. --- -## Channel Allocation +## Audio Hardware Architecture + +```mermaid +graph TD + subgraph "Custom Chips (Paula)" + AUD0["AUD0 (Left)"] + AUD1["AUD1 (Right)"] + AUD2["AUD2 (Right)"] + AUD3["AUD3 (Left)"] + end + + subgraph "Chip RAM" + S0["Sample 0"] + S1["Sample 1"] + S2["Sample 2"] + S3["Sample 3"] + end + + S0 -->|DMA| AUD0 + S1 -->|DMA| AUD1 + S2 -->|DMA| AUD2 + S3 -->|DMA| AUD3 + + AUD0 --> LEFT["Left Output"] + AUD3 --> LEFT + AUD1 --> RIGHT["Right Output"] + AUD2 --> RIGHT +``` + +### Channel Stereo Assignment + +| Channel | Default Panning | Hardware Register Base | +|---|---|---| +| 0 | **Left** | `$DFF0A0` | +| 1 | **Right** | `$DFF0B0` | +| 2 | **Right** | `$DFF0C0` | +| 3 | **Left** | `$DFF0D0` | + +This is fixed in hardware — you cannot reassign channels to different outputs. Software panning requires mixing samples before playback. + +--- + +## Channel Registers + +Each channel has 5 registers (10 bytes): + +| Offset | Register | Size | Description | +|---|---|---|---| +| +$00 | `AUDxLCH` | WORD | Pointer high — bits 18:16 of sample address | +| +$02 | `AUDxLCL` | WORD | Pointer low — bits 15:1 of sample address | +| +$04 | `AUDxLEN` | WORD | Length in **words** (not bytes). Min=1, Max=65535 | +| +$06 | `AUDxPER` | WORD | Period (DMA clock divider). Lower = faster = higher frequency | +| +$08 | `AUDxVOL` | WORD | Volume: 0 (silent) to 64 (max). Values >64 produce distortion on some models | + +### Period ↔ Frequency Conversion ```c -UBYTE allocationMap[] = { 1, 2, 4, 8 }; /* channel masks */ -struct IOAudio *aio = (struct IOAudio *) - CreateIORequest(port, sizeof(struct IOAudio)); -aio->ioa_Request.io_Message.mn_Node.ln_Pri = 0; -aio->ioa_Data = allocationMap; -aio->ioa_Length = sizeof(allocationMap); -OpenDevice("audio.device", 0, (struct IORequest *)aio, 0); -/* aio->ioa_AllocKey = allocation key for this channel */ +/* Period = clock_constant / desired_frequency */ +/* PAL: clock = 3546895 Hz */ +/* NTSC: clock = 3579545 Hz */ + +#define PAL_CLOCK 3546895 +#define NTSC_CLOCK 3579545 + +UWORD period_from_hz(ULONG freq, BOOL isPAL) { + return (isPAL ? PAL_CLOCK : NTSC_CLOCK) / freq; +} + +ULONG hz_from_period(UWORD period, BOOL isPAL) { + return (isPAL ? PAL_CLOCK : NTSC_CLOCK) / period; +} ``` +| Frequency | Period (PAL) | Period (NTSC) | Quality | +|---|---|---|---| +| 8287 Hz | 428 | 432 | Amiga standard (MOD default) | +| 11025 Hz | 322 | 325 | Low-quality speech | +| 16726 Hz | 212 | 214 | CD/4 quality | +| 22050 Hz | 161 | 162 | Near-CD quality | +| 28867 Hz | 123 | 124 | Maximum safe rate | + +> [!WARNING] +> Periods below ~124 cause DMA contention with other chip resources (sprites, bitplanes). The hardware minimum is period=1, but anything below ~124 steals so many DMA cycles that display and other I/O suffers. + +--- + +## Channel Allocation via audio.device + +The OS manages channel allocation to prevent conflicts between applications: + +```c +/* Request channels: */ +#include + +/* Allocation map: which channels we want, in preference order */ +UBYTE allocMap[] = { + 0x01, /* channel 0 only */ + 0x02, /* channel 1 only */ + 0x04, /* channel 2 only */ + 0x08, /* channel 3 only */ + 0x03, /* channels 0+1 */ + 0x05, /* channels 0+2 (both left) */ + 0x0A, /* channels 1+3 (both right) */ + 0x0F /* all four channels */ +}; + +struct MsgPort *audioPort = CreateMsgPort(); +struct IOAudio *aio = (struct IOAudio *) + CreateIORequest(audioPort, sizeof(struct IOAudio)); + +aio->ioa_Request.io_Message.mn_Node.ln_Pri = 0; /* priority */ +aio->ioa_Data = allocMap; +aio->ioa_Length = sizeof(allocMap); + +BYTE err = OpenDevice("audio.device", 0, (struct IORequest *)aio, 0); +if (err == 0) +{ + /* aio->ioa_AllocKey = unique key for this allocation */ + UWORD allocKey = aio->ioa_AllocKey; + /* ioa_Request.io_Unit bits indicate which channel was allocated */ +} +``` + +### Priority System + +| Priority | User | +|---|---| +| +127 | System critical (alerts) | +| +100 | Music player (dedicated) | +| 0 | Normal application | +| −128 | Background / optional | + +Higher-priority requests can **steal** channels from lower-priority holders. The displaced holder receives an `ADCMD_ALLOCATE` signal. + --- ## Playing a Sample ```c +/* After successful OpenDevice: */ aio->ioa_Request.io_Command = CMD_WRITE; -aio->ioa_Request.io_Flags = ADIOF_PERVOL; -aio->ioa_Data = sampleData; /* MUST be in Chip RAM */ -aio->ioa_Length = sampleLength; /* in bytes */ +aio->ioa_Request.io_Flags = ADIOF_PERVOL; /* set period and volume */ +aio->ioa_Data = sampleData; /* MUST be in Chip RAM! */ +aio->ioa_Length = sampleLength; /* in bytes (must be even) */ aio->ioa_Period = 428; /* ~8287 Hz (PAL) */ aio->ioa_Volume = 64; /* 0–64 */ -aio->ioa_Cycles = 1; /* 0 = loop forever */ +aio->ioa_Cycles = 1; /* 0 = loop forever, N = play N times */ BeginIO((struct IORequest *)aio); +/* Returns immediately — DMA plays in background */ +/* Wait for completion: */ +WaitIO((struct IORequest *)aio); ``` -### Period Calculation +> [!IMPORTANT] +> Sample data **must** be in Chip RAM (`MEMF_CHIP`). The DMA hardware can only read from the first 2 MB of address space. Passing a Fast RAM pointer results in silence or random noise. -``` -Period = clock_constant / desired_frequency -PAL: clock = 3546895 Hz → Period = 3546895 / freq -NTSC: clock = 3579545 Hz → Period = 3579545 / freq -``` +### Double-Buffering (Continuous Playback) -| Frequency | Period (PAL) | -|---|---| -| 8287 Hz | 428 | -| 11025 Hz | 322 | -| 22050 Hz | 161 | -| 28867 Hz | 124 (minimum safe) | +```c +/* Use two IOAudio requests for gapless audio: */ +struct IOAudio *aio_a = /* ... */; +struct IOAudio *aio_b = /* ... */; + +/* Start first buffer: */ +aio_a->ioa_Data = buffer_a; +aio_a->ioa_Length = BUFFER_SIZE; +aio_a->ioa_Cycles = 1; +BeginIO((struct IORequest *)aio_a); + +/* Queue second buffer immediately: */ +aio_b->ioa_Data = buffer_b; +aio_b->ioa_Length = BUFFER_SIZE; +aio_b->ioa_Cycles = 1; +BeginIO((struct IORequest *)aio_b); + +/* When aio_a completes, fill buffer_a with new data and re-queue: */ +WaitIO((struct IORequest *)aio_a); +/* fill buffer_a... */ +BeginIO((struct IORequest *)aio_a); +/* When aio_b completes, fill buffer_b... */ +``` --- -## Channel Registers +## Audio Interrupts -| Channel | Address | Description | +Each channel generates an interrupt when its DMA period completes (all sample words played): + +| Channel | Interrupt Bit | INTENA/INTREQ | |---|---|---| -| 0 | `$DFF0A0` | AUD0 (left) | -| 1 | `$DFF0B0` | AUD1 (right) | -| 2 | `$DFF0C0` | AUD2 (right) | -| 3 | `$DFF0D0` | AUD3 (left) | +| AUD0 | 7 | `INTF_AUD0` ($0080) | +| AUD1 | 8 | `INTF_AUD1` ($0100) | +| AUD2 | 9 | `INTF_AUD2` ($0200) | +| AUD3 | 10 | `INTF_AUD3` ($0400) | -Each channel: pointer (PTH/PTL), length (LEN), period (PER), volume (VOL). +```c +/* Set up audio interrupt: */ +struct Interrupt audioInt; +audioInt.is_Node.ln_Type = NT_INTERRUPT; +audioInt.is_Node.ln_Pri = 0; +audioInt.is_Node.ln_Name = "MyAudioInt"; +audioInt.is_Data = myData; +audioInt.is_Code = (APTR)AudioIntHandler; +AddIntServer(INTB_AUD0, &audioInt); + +/* Handler — called when channel 0 finishes playing: */ +__saveds void AudioIntHandler(void) +{ + /* Reload next sample segment into AUD0LCH/AUD0LCL/AUD0LEN */ + custom->aud[0].ac_ptr = nextSample; + custom->aud[0].ac_len = nextLength / 2; /* length in words */ +} +``` + +--- + +## Modulation Modes + +The Amiga audio hardware supports two modulation modes for channels 0+1 and 2+3: + +### Amplitude Modulation + +Channel `N` modulates the volume of channel `N+1`: + +```c +/* Channel 0 data controls channel 1's volume */ +custom->adkcon = ADKF_USE0V1; /* enable AM: ch0 → ch1 volume */ +``` + +### Period Modulation (Frequency Modulation) + +Channel `N` modulates the period of channel `N+1`: + +```c +/* Channel 0 data controls channel 1's period */ +custom->adkcon = ADKF_USE0P1; /* enable PM: ch0 → ch1 period */ +``` + +These modes are rarely used in practice — they reduce the effective channel count from 4 to 2. + +--- + +## Direct Hardware Access (Bypassing audio.device) + +Games and demos often bypass `audio.device` entirely: + +```asm +; Direct hardware — play a sample on channel 0: + LEA $DFF000, A5 ; custom chip base + MOVE.L #sample, $A0(A5) ; AUD0LC — sample pointer + MOVE.W #length/2, $A4(A5) ; AUD0LEN — length in words + MOVE.W #428, $A6(A5) ; AUD0PER — period (8287 Hz) + MOVE.W #64, $A8(A5) ; AUD0VOL — max volume + + ; Enable DMA for channel 0: + MOVE.W #$8201, $96(A5) ; DMACON — set DMAEN + AUD0EN +``` + +> **Warning**: Direct access conflicts with any OS audio. Always `OwnBlitter()` / `Forbid()` and disable audio device first if mixing approaches. --- ## References -- NDK39: `devices/audio.h` -- HRM: audio DMA chapter +- NDK39: `devices/audio.h`, `hardware/custom.h` +- HRM: *Amiga Hardware Reference Manual* — Audio DMA chapter +- ADCD 2.1: `audio.device` autodocs +- See also: [interrupts.md](../06_exec_os/interrupts.md) — interrupt server chain diff --git a/10_devices/keyboard.md b/10_devices/keyboard.md index ab95733..ad9b15f 100644 --- a/10_devices/keyboard.md +++ b/10_devices/keyboard.md @@ -1,48 +1,179 @@ [← Home](../README.md) · [Devices](README.md) -# keyboard.device — Keyboard Input +# keyboard.device — Keyboard Hardware and Raw Key Codes ## Overview -`keyboard.device` provides raw keycode events from the keyboard controller (8520 CIA-A). Normally used indirectly via `input.device`, but can be accessed directly for keymap-independent scanning. +`keyboard.device` provides access to the Amiga keyboard via the CIA-A serial port handshake protocol. The keyboard controller (a dedicated 6500/1 or 68HC05 microcontroller inside the keyboard) transmits **raw key codes** as serial data through CIA-A. Understanding this protocol is essential for FPGA core implementation. --- -## Keycodes +## Hardware Protocol -Amiga keycodes are 7-bit values (0–127). Bit 7 indicates key-up: +```mermaid +sequenceDiagram + participant KB as Keyboard MCU + participant CIA as CIA-A (SP/CNT pins) + participant CPU as 68000 (interrupt) + + KB->>CIA: Serial data (8 bits, MSB first) + Note over CIA: CIA-A SP register latches byte + CIA->>CPU: CIA-A ICR bit 3 (SP interrupt) + CPU->>CIA: Read $BFEC01 (SP data register) + CPU->>CIA: Pulse KDAT line low for ~85µs + Note over CIA: Handshake acknowledges receipt + KB->>KB: Ready for next key +``` + +### CIA-A Registers Used + +| Register | Address | Bit | Function | +|---|---|---|---| +| `CIASDR` | `$BFEC01` | 7:0 | Serial data register — receives raw key code | +| `CIACRA` | `$BFEE01` | 6 | SP direction: 0=input (keyboard), 1=output | +| `CIAICR` | `$BFED01` | 3 | SP interrupt flag — set when byte received | + +### Raw Key Code Format + +The keyboard sends an 8-bit value where: +- **Bit 7** = key state: 0 = **pressed**, 1 = **released** +- **Bits 6:0** = key code (0–127) + +```c +/* Decode a raw key event: */ +UBYTE raw = ~(cia->ciaSDR); /* invert (active low) */ +raw = (raw >> 1) | (raw << 7); /* rotate right 1 bit */ +UBYTE keycode = raw & 0x7F; +BOOL keyup = raw & 0x80; +``` + +> **Bit rotation**: The keyboard transmits bits in a rotated format. The software must rotate the received byte right by 1 bit to get the actual key code. This is a hardware design quirk. + +### Handshake Timing + +After reading the key code, the CPU must acknowledge by pulsing the KDAT line: + +```asm +; Acknowledge key reception: + OR.B #$40, $BFEE01 ; CIACRA — set SP to output + ; Wait at least 85 µs + MOVE.B #0, $BFEC01 ; drive KDAT low + ; (timer or loop delay ~85 µs) + AND.B #~$40, $BFEE01 ; CIACRA — set SP back to input + ; Keyboard is now ready to send next key +``` + +> **FPGA note**: If the handshake acknowledgement is too fast (<75µs) or too slow (>200ms), the keyboard MCU will resend the key code or initiate a reset sequence. + +--- + +## Raw Key Code Map + +### Main Keys + +| Code | Key | Code | Key | Code | Key | +|---|---|---|---|---|---| +| $00 | `` ` `` (backtick) | $01 | `1` | $02 | `2` | +| $03 | `3` | $04 | `4` | $05 | `5` | +| $06 | `6` | $07 | `7` | $08 | `8` | +| $09 | `9` | $0A | `0` | $0B | `-` | +| $0C | `=` | $0D | `\` | $10 | `Q` | +| $11 | `W` | $12 | `E` | $13 | `R` | +| $14 | `T` | $15 | `Y` | $16 | `U` | +| $17 | `I` | $18 | `O` | $19 | `P` | +| $1A | `[` | $1B | `]` | $20 | `A` | +| $21 | `S` | $22 | `D` | $23 | `F` | +| $24 | `G` | $25 | `H` | $26 | `J` | +| $27 | `K` | $28 | `L` | $29 | `;` | +| $2A | `'` | $31 | `Z` | $32 | `X` | +| $33 | `C` | $34 | `V` | $35 | `B` | +| $36 | `N` | $37 | `M` | $38 | `,` | +| $39 | `.` | $3A | `/` | $40 | `Space` | +| $41 | `Backspace` | $42 | `Tab` | $43 | `Numpad Enter` | +| $44 | `Return` | $45 | `Escape` | $46 | `Delete` | + +### Modifier and Special Keys | Code | Key | Code | Key | |---|---|---|---| -| `$00` | \` (backtick) | `$40` | Space | -| `$01`–`$0A` | 1–0 | `$41` | Backspace | -| `$10`–`$19` | Q–P | `$42` | Tab | -| `$20`–`$28` | A–L | `$43` | Enter (keypad) | -| `$31`–`$39` | Z–/ | `$44` | Return | -| `$45` | Escape | `$46` | Delete | -| `$4C` | Cursor Up | `$4D` | Cursor Down | -| `$4E` | Cursor Right | `$4F` | Cursor Left | -| `$50`–`$59` | F1–F10 | `$5F` | Help | -| `$60` | Left Shift | `$61` | Right Shift | -| `$62` | Caps Lock | `$63` | Control | -| `$64` | Left Alt | `$65` | Right Alt | -| `$66` | Left Amiga | `$67` | Right Amiga | +| $60 | `Left Shift` | $61 | `Right Shift` | +| $62 | `Caps Lock` | $63 | `Ctrl` | +| $64 | `Left Alt` | $65 | `Right Alt` | +| $66 | `Left Amiga` | $67 | `Right Amiga` | + +### Cursor and Function Keys + +| Code | Key | Code | Key | +|---|---|---|---| +| $4C | `Cursor Up` | $4D | `Cursor Down` | +| $4E | `Cursor Right` | $4F | `Cursor Left` | +| $50–$59 | `F1`–`F10` | $5F | `Help` | + +### Special Codes + +| Code | Meaning | +|---|---| +| $78 | **Reset warning** — Ctrl+Amiga+Amiga pressed (keyboard sends this before resetting) | +| $F9 | Last key code was bad (parity error) — resend | +| $FA | Keyboard buffer overflow | +| $FC | Keyboard self-test failed | +| $FD | Initiate power-up key stream | +| $FE | Terminate power-up key stream | --- -## Commands +## Using keyboard.device (OS Level) -| Code | Constant | Description | -|---|---|---| -| 2 | `CMD_READ` | Read raw keycodes | -| 5 | `CMD_CLEAR` | Clear keyboard buffer | -| 9 | `KBD_READMATRIX` | Read full key matrix state | -| 10 | `KBD_ADDRESETHANDLER` | Add Ctrl-Amiga-Amiga handler | -| 11 | `KBD_REMRESETHANDLER` | Remove reset handler | -| 12 | `KBD_RESETHANDLERDONE` | Acknowledge reset handler completion | +Most applications receive key events through Intuition IDCMP (see [idcmp.md](../09_intuition/idcmp.md)). Direct keyboard.device use is for system-level software: + +```c +struct MsgPort *kbPort = CreateMsgPort(); +struct IOStdReq *kbReq = (struct IOStdReq *) + CreateIORequest(kbPort, sizeof(struct IOStdReq)); + +OpenDevice("keyboard.device", 0, (struct IORequest *)kbReq, 0); + +/* Read raw key events: */ +struct InputEvent ie; +kbReq->io_Command = KBD_READMATRIX; +kbReq->io_Data = &keyMatrix; /* 16-byte key matrix bitmap */ +kbReq->io_Length = 16; +DoIO((struct IORequest *)kbReq); +/* keyMatrix bit N = 1 if key code N is currently held down */ + +/* Reset keyboard (force self-test): */ +kbReq->io_Command = KBD_RESETHANDLER; +DoIO((struct IORequest *)kbReq); +``` + +### Key Matrix + +The `KBD_READMATRIX` command returns a 16-byte (128-bit) bitmap where each bit corresponds to a raw key code: + +```c +UBYTE keyMatrix[16]; +/* Bit test: is key $45 (Escape) pressed? */ +BOOL escPressed = keyMatrix[0x45 / 8] & (1 << (0x45 % 8)); +``` + +--- + +## Keyboard Reset Sequence + +The Ctrl+Amiga+Amiga three-key combination triggers a hardware reset: + +1. User presses Ctrl+Left Amiga+Right Amiga +2. Keyboard MCU detects the combo +3. Sends raw code `$78` (reset warning) — gives software ~10 seconds to clean up +4. Pulls KBRST line low → triggers 68000 reset via RESET pin + +> **FPGA**: The core must implement this reset path. The `$78` warning code allows software (e.g., debuggers) to save state before reset. --- ## References -- NDK39: `devices/keyboard.h` +- NDK39: `devices/keyboard.h`, `devices/inputevent.h` +- HRM: *Amiga Hardware Reference Manual* — Keyboard chapter +- See also: [input.md](input.md) — input.device handler chain +- See also: [idcmp.md](../09_intuition/idcmp.md) — high-level key events via Intuition diff --git a/10_devices/timer.md b/10_devices/timer.md index 0b67e15..1b8e5bc 100644 --- a/10_devices/timer.md +++ b/10_devices/timer.md @@ -1,34 +1,110 @@ [← Home](../README.md) · [Devices](README.md) -# timer.device — Timing and Delays +# timer.device — Timing, Delays, and High-Resolution Timestamps ## Overview -`timer.device` provides precise timing services: delays, time-of-day, and high-resolution timestamps. It has two units: - -| Unit | Constant | Resolution | Use | -|---|---|---|---| -| 0 | `UNIT_MICROHZ` | ~2µs (E-clock) | Short, precise delays | -| 1 | `UNIT_VBLANK` | ~20ms (VBlank) | Long delays, lower overhead | -| 2 | `UNIT_ECLOCK` | CIA E-clock ticks | Highest resolution timing (OS 2.0+) | -| 3 | `UNIT_WAITUNTIL` | absolute time | Wait until specific time (OS 2.0+) | -| 4 | `UNIT_WAITECLOCK` | E-clock absolute | (OS 2.0+) | +`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). --- -## struct timeval / timerequest +## 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? + +```mermaid +flowchart TD + Q["How long is the delay?"] --> SHORT["< 100 ms"] + Q --> LONG["> 100 ms"] + SHORT --> MICRO["UNIT_MICROHZ
or UNIT_ECLOCK"] + LONG --> VBLANK["UNIT_VBLANK
(lower CPU overhead)"] + Q --> MEASURE["Need to measure
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. + +```c +/* 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 ```c /* devices/timer.h — NDK39 */ struct timeval { ULONG tv_secs; /* seconds */ - ULONG tv_micro; /* microseconds */ + ULONG tv_micro; /* microseconds (0–999999) */ }; struct timerequest { struct IORequest tr_node; struct timeval tr_time; }; +/* 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 */ +}; +``` + +--- + +## Opening timer.device + +```c +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() */ ``` --- @@ -36,17 +112,51 @@ struct timerequest { ## Simple Delay ```c -struct timerequest *tr = (struct timerequest *) - CreateIORequest(port, sizeof(struct timerequest)); -OpenDevice("timer.device", UNIT_VBLANK, (struct IORequest *)tr, 0); - +/* 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 = 0; -DoIO((struct IORequest *)tr); /* blocks for 2 seconds */ +tr->tr_time.tv_micro = 500000; /* 0.5 sec */ +DoIO((struct IORequest *)tr); /* blocks until done */ +``` -CloseDevice((struct IORequest *)tr); -DeleteIORequest((struct IORequest *)tr); +### Non-Blocking Delay + +```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; +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 */ +} ``` --- @@ -54,26 +164,125 @@ DeleteIORequest((struct IORequest *)tr); ## Getting Current Time ```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\n", tr->tr_time.tv_secs, tr->tr_time.tv_micro); +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: t10: t1>t2 */ ``` --- -## High-Resolution Timing +## High-Resolution Timing (ReadEClock) ```c -/* Read E-clock (OS 2.0+): */ -struct EClockVal eclock; -ULONG freq = ReadEClock(&eclock); /* returns ticks/second */ -/* eclock.ev_hi, eclock.ev_lo = 64-bit tick count */ -/* Typical freq: 709379 Hz (PAL) or 715909 Hz (NTSC) */ +/* 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) */ + +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](../06_exec_os/interrupts.md) — VBlank interrupt chain diff --git a/10_devices/trackdisk.md b/10_devices/trackdisk.md index 5c7e7e2..2cf03df 100644 --- a/10_devices/trackdisk.md +++ b/10_devices/trackdisk.md @@ -1,74 +1,178 @@ [← Home](../README.md) · [Devices](README.md) -# trackdisk.device — Floppy Disk I/O +# trackdisk.device — Floppy Disk DMA Controller ## Overview -`trackdisk.device` provides raw sector I/O for Amiga floppy drives. Each drive is a unit (0–3). The device operates on 512-byte sectors, 11 sectors per track (880 KB DD disks) or 22 per track (1760 KB HD). +`trackdisk.device` interfaces with the Amiga's floppy disk controller — a custom DMA engine that reads and writes raw MFM-encoded data from double-density 3.5" disks. It provides block-level access (512 bytes/sector, 11 sectors/track, 80 tracks × 2 sides = 1,760 sectors = 880 KB per disk). --- -## Opening +## Hardware Architecture -```c -struct IOExtTD *tdreq = (struct IOExtTD *) - CreateIORequest(port, sizeof(struct IOExtTD)); -OpenDevice("trackdisk.device", 0, (struct IORequest *)tdreq, 0); +```mermaid +graph LR + subgraph "Custom Chips" + DSKBYTR["DSKBYTR
$DFF01A
Disk Data Byte"] + DSKLEN["DSKLEN
$DFF024
DMA Length"] + DSKPT["DSKPT
$DFF020
DMA Pointer"] + end + + subgraph "CIA-B" + CIAPRB["PRA/PRB
$BFD100
Motor, Side, Step"] + end + + DISK["3.5 DD Disk"] -->|"MFM bitstream"| DSKBYTR + CIAPRB -->|"Motor/Step/Side"| DISK + DSKPT -->|"DMA to/from"| CHIPRAM["Chip RAM Buffer
(~13 KB/track)"] +``` + +### Disk Geometry + +| Parameter | Value | +|---|---| +| Tracks | 80 (0–79) | +| Sides | 2 (0=upper, 1=lower) | +| Sectors per track | 11 (DD), 22 (HD) | +| Bytes per sector | 512 | +| Total capacity | 880 KB (DD), 1,760 KB (HD) | +| Rotation speed | 300 RPM (1 revolution = 200 ms) | +| Transfer rate | ~250 kbit/s (DD raw MFM) | +| Track-to-track seek | ~3 ms | + +### MFM Encoding + +The disk stores data in **Modified Frequency Modulation** format. Each byte becomes 16 bits on disk (clock + data interleaved): + +``` +Data bit: 1 0 1 1 0 0 0 1 +MFM: 01 10 01 01 10 10 10 01 + ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ + c/d pairs (clock bit inserted before each data bit) +``` + +A raw track is ~12,668 bytes of MFM data (including gaps, sync words, and sector headers). + +### Track Format (AmigaDOS) + +``` +Track = 11 sectors, each containing: + + Sync: $4489 $4489 (2 words — MFM-encoded $A1 $A1) + Header: format, track, sector, sectors_to_gap (MFM-encoded) + Header checksum: XOR of header longs + Data: 512 bytes of payload (MFM-encoded = 1024 bytes on disk) + Data checksum: XOR of data longs + +Gaps between sectors: variable-length padding ``` --- -## Commands +## Using trackdisk.device -| Code | Constant | Description | -|---|---|---| -| 2 | `CMD_READ` | Read sectors | -| 3 | `CMD_WRITE` | Write sectors | -| 4 | `CMD_UPDATE` | Flush write buffer to disk | -| 9 | `TD_MOTOR` | Turn motor on/off | -| 10 | `TD_FORMAT` | Low-level format track | -| 11 | `TD_SEEK` | Move head to track | -| 12 | `TD_REMOVE` | Notify on disk change | -| 13 | `TD_CHANGENUM` | Get disk change count | -| 14 | `TD_CHANGESTATE` | Check if disk present | -| 15 | `TD_PROTSTATUS` | Check write-protect | -| 16 | `TD_RAWREAD` | Read raw MFM data | -| 17 | `TD_RAWWRITE` | Write raw MFM data | -| 18 | `TD_GETDRIVETYPE` | Get drive type | -| 19 | `TD_GETNUMTRACKS` | Get total tracks | -| 20 | `TD_ADDCHANGEINT` | Add disk change interrupt | -| 21 | `TD_REMCHANGEINT` | Remove disk change interrupt | - ---- - -## Reading a Sector +### Opening ```c -UBYTE buf[512]; -tdreq->iotd_Req.io_Command = CMD_READ; -tdreq->iotd_Req.io_Data = buf; -tdreq->iotd_Req.io_Length = 512; -tdreq->iotd_Req.io_Offset = 0; /* byte offset = sector * 512 */ -DoIO((struct IORequest *)tdreq); +struct MsgPort *diskPort = CreateMsgPort(); +struct IOExtTD *diskReq = (struct IOExtTD *) + CreateIORequest(diskPort, sizeof(struct IOExtTD)); + +/* Unit numbers: DF0:=0, DF1:=1, DF2:=2, DF3:=3 */ +BYTE err = OpenDevice("trackdisk.device", 0, + (struct IORequest *)diskReq, 0); ``` +### Reading Sectors + +```c +UBYTE *buf = AllocMem(512, MEMF_CHIP); /* MUST be Chip RAM */ + +diskReq->iotd_Req.io_Command = CMD_READ; +diskReq->iotd_Req.io_Data = buf; +diskReq->iotd_Req.io_Length = 512; /* bytes to read */ +diskReq->iotd_Req.io_Offset = 0; /* byte offset on disk */ +/* offset = (track * 2 + side) * 11 * 512 + sector * 512 */ +DoIO((struct IORequest *)diskReq); +``` + +### Writing + Updating (Motor Control) + +```c +/* Write a sector: */ +diskReq->iotd_Req.io_Command = CMD_WRITE; +diskReq->iotd_Req.io_Data = buf; +diskReq->iotd_Req.io_Length = 512; +diskReq->iotd_Req.io_Offset = 512 * 10; /* sector 10 */ +DoIO((struct IORequest *)diskReq); + +/* Flush write buffer to disk: */ +diskReq->iotd_Req.io_Command = CMD_UPDATE; +DoIO((struct IORequest *)diskReq); + +/* Turn off motor when done: */ +diskReq->iotd_Req.io_Command = TD_MOTOR; +diskReq->iotd_Req.io_Length = 0; /* 0=off, 1=on */ +DoIO((struct IORequest *)diskReq); +``` + +### Disk Change Notification + +```c +/* Wait for disk insertion/removal: */ +diskReq->iotd_Req.io_Command = TD_CHANGENUM; +DoIO((struct IORequest *)diskReq); +ULONG changeCount = diskReq->iotd_Req.io_Actual; + +/* Async notification: */ +diskReq->iotd_Req.io_Command = TD_ADDCHANGEINT; +diskReq->iotd_Req.io_Data = (APTR)&myInterrupt; +SendIO((struct IORequest *)diskReq); +/* myInterrupt is signalled on disk change */ +``` + +### Track Caching + +trackdisk.device reads an **entire track** (11 sectors) into an internal buffer on each access. Subsequent reads of other sectors on the same track are served from cache: + +``` +Read sector 0 → DMA reads track 0 (11 sectors) → cache hit for sectors 1–10 +Read sector 11 → new track → DMA reads track 1 +Read sector 5 → cache hit (still in track 0 buffer) +``` + +> **FPGA implication**: the MiSTer core must emulate this whole-track DMA behaviour for correct timing. Games that measure seek+read latency will behave incorrectly if only single sectors are transferred. + --- -## Disk Geometry +## Direct Hardware Access (Games/Demos) -| Parameter | DD (880 KB) | HD (1760 KB) | -|---|---|---| -| Heads | 2 | 2 | -| Cylinders | 80 | 80 | -| Sectors/track | 11 | 22 | -| Bytes/sector | 512 | 512 | -| Total sectors | 1760 | 3520 | +Games often bypass trackdisk.device for speed and copy protection: -Byte offset = `(cylinder * 2 + head) * sectors_per_track * 512 + sector * 512` +```asm +; Direct floppy read — raw track DMA: + LEA $DFF000, A5 ; custom base + MOVE.L #TrackBuffer, $20(A5) ; DSKPT — DMA pointer (Chip RAM) + MOVE.W #$8210, $96(A5) ; DMACON — enable disk DMA + + ; Select drive, side, seek to track: + MOVE.B #$F7, $BFD100 ; CIA-B PRB — select DF0, motor on + ; ... step head to desired track ... + + ; Start reading one track: + MOVE.W #$8000|6300, $24(A5) ; DSKLEN — enable, ~6300 words + MOVE.W #$8000|6300, $24(A5) ; write twice to start DMA + + ; Wait for DMA complete (DSKBLK interrupt): + BTST #1, $DFF01F ; INTREQR — DSKBLK bit + BEQ.S .-4 +``` --- ## References -- NDK39: `devices/trackdisk.h` +- NDK39: `devices/trackdisk.h`, `resources/disk.h` +- HRM: *Amiga Hardware Reference Manual* — Disk Controller chapter - ADCD 2.1: trackdisk.device autodocs +- See also: [filesystem.md](../07_dos/filesystem.md) — FFS/OFS block format on top of trackdisk