mirror of
https://github.com/alfishe/amiga-bootcamp.git
synced 2026-06-13 00:26:28 +00:00
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
This commit is contained in:
parent
aeaea88d75
commit
da9e7d3b63
7 changed files with 1599 additions and 301 deletions
|
|
@ -1,10 +1,10 @@
|
||||||
[← Home](../README.md) · [AmigaDOS](README.md)
|
[← 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
|
## 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) |
|
| −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) |
|
| −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) |
|
| −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) */
|
#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
|
## 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 */
|
#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`:
|
The returned handle is a BPTR to a `struct FileHandle`:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
|
/* dos/dosextens.h */
|
||||||
struct FileHandle {
|
struct FileHandle {
|
||||||
struct Message *fh_Link;
|
struct Message *fh_Link; /* +$00: exec message for reply */
|
||||||
struct MsgPort *fh_Interactive; /* non-NULL if console */
|
struct MsgPort *fh_Interactive; /* +$04: non-NULL if console device */
|
||||||
struct MsgPort *fh_Type; /* handler process port */
|
struct MsgPort *fh_Type; /* +$08: handler process MsgPort */
|
||||||
BPTR fh_Buf; /* I/O buffer (BPTR) */
|
BPTR fh_Buf; /* +$0C: I/O buffer (BPTR) */
|
||||||
LONG fh_Pos; /* current position in buffer */
|
LONG fh_Pos; /* +$10: current position in buffer */
|
||||||
LONG fh_End; /* end of valid data in buffer */
|
LONG fh_End; /* +$14: end of valid data in buffer */
|
||||||
LONG fh_Funcs; /* unused */
|
LONG fh_Funcs; /* +$18: unused (was BCPL function) */
|
||||||
LONG fh_Func2; /* unused */
|
LONG fh_Func2; /* +$1C: unused */
|
||||||
LONG fh_Func3; /* unused */
|
LONG fh_Func3; /* +$20: unused */
|
||||||
LONG fh_Args; /* packet args */
|
LONG fh_Args; /* +$24: FH_Arg1 — passed to handler */
|
||||||
BPTR fh_Arg2;
|
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
|
```c
|
||||||
BPTR fh = Open("RAM:test.txt", MODE_NEWFILE);
|
/* Get the current process's stdin/stdout/stderr: */
|
||||||
if (fh) {
|
BPTR in = Input(); /* stdin — from CLI or WB */
|
||||||
Write(fh, "Hello Amiga\n", 12);
|
BPTR out = Output(); /* stdout */
|
||||||
Close(fh);
|
BPTR err = ErrorOutput(); /* stderr (OS 3.0+ only) */
|
||||||
}
|
|
||||||
|
|
||||||
fh = Open("RAM:test.txt", MODE_OLDFILE);
|
/* Write to stdout: */
|
||||||
if (fh) {
|
FPuts(Output(), "Hello from AmigaDOS\n");
|
||||||
UBYTE buf[64];
|
|
||||||
LONG n = Read(fh, buf, sizeof(buf));
|
/* Printf — formatted output to stdout: */
|
||||||
if (n > 0) Write(Output(), buf, n); /* echo 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);
|
Close(fh);
|
||||||
} else {
|
*sizeOut = size;
|
||||||
PrintFault(IoErr(), "Open failed");
|
return buf;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -90,18 +245,41 @@ if (fh) {
|
||||||
## Error Checking
|
## Error Checking
|
||||||
|
|
||||||
```c
|
```c
|
||||||
LONG err = IoErr(); /* LVO −66 — returns last DOS error code */
|
LONG err = IoErr(); /* returns last DOS error code */
|
||||||
/* Common codes: */
|
|
||||||
#define ERROR_OBJECT_NOT_FOUND 205
|
/* Human-readable error message: */
|
||||||
#define ERROR_OBJECT_EXISTS 203
|
PrintFault(err, "Operation failed");
|
||||||
#define ERROR_DISK_FULL 221
|
/* Output: "Operation failed: object not found" */
|
||||||
#define ERROR_SEEK_ERROR 219
|
|
||||||
|
/* 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
|
## 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
|
- [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
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,296 @@
|
||||||
[← Home](../README.md) · [AmigaDOS](README.md)
|
[← Home](../README.md) · [AmigaDOS](README.md)
|
||||||
|
|
||||||
# Filesystem — FFS/OFS Block Structure
|
# Filesystem — FFS/OFS Block Structure and Disk Layout
|
||||||
|
|
||||||
## Overview
|
## 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 Types
|
||||||
|
|
||||||
| Block | Type ID | Description |
|
| Type | ID | Constant | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| Boot block | `"DOS\0"` / `"DOS\1"` | Blocks 0–1; OFS=`DOS\0`, FFS=`DOS\1` |
|
| Boot block | — | (bytes 0–1023) | Boot code + filesystem ID (`DOS\0` to `DOS\7`) |
|
||||||
| Root block | `T_HEADER` (2) | Always at middle of partition; directory root |
|
| Root block | 2 | `T_HEADER` | Volume root directory — always at midpoint |
|
||||||
| File header | `T_HEADER` (2) | Metadata for one file |
|
| File header | 2 | `T_HEADER` | File metadata + first 72 data block pointers |
|
||||||
| Directory header | `T_HEADER` (2) | Metadata for one directory |
|
| Directory | 2 | `T_HEADER` | Subdirectory — contains 72-slot hash table |
|
||||||
| Data block | `T_DATA` (8) | OFS: header + data; FFS: pure data |
|
| Data block | 8 | `T_DATA` | OFS: 24-byte header + 488 data bytes |
|
||||||
| File extension | `T_LIST` (16) | Overflow pointer table for large files |
|
| Extension | 16 | `T_LIST` | Overflow block pointers for files >72 blocks |
|
||||||
| Hash chain | — | Root/dir blocks have a 72-entry hash table |
|
| 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`) |
|
| `DOS\0` | $444F5300 | OFS (original) |
|
||||||
| 4 | `header_key` | Own block number |
|
| `DOS\1` | $444F5301 | FFS (fast file system) |
|
||||||
| 8 | `high_seq` | Number of data blocks in hash table |
|
| `DOS\2` | $444F5302 | OFS + International mode (case-insensitive) |
|
||||||
| 12 | `ht_size` | Hash table size (usually 72) |
|
| `DOS\3` | $444F5303 | FFS + International mode |
|
||||||
| 16 | `first_data` | Unused |
|
| `DOS\4` | $444F5304 | FFS + Directory cache |
|
||||||
| 20 | `checksum` | Block checksum |
|
| `DOS\5` | $444F5305 | OFS + International + Directory cache |
|
||||||
| 24–312 | `ht[72]` | Hash table: block pointers for directory entries |
|
| `DOS\6` | $444F5306 | OFS + Long filenames (OS 3.5+) |
|
||||||
| 420 | `bm_flag` | Bitmap valid flag (`-1` = valid) |
|
| `DOS\7` | $444F5307 | FFS + Long filenames (OS 3.5+) |
|
||||||
| 424–472 | `bm_pages[25]` | Pointers to bitmap blocks |
|
|
||||||
| 484 | `last_altered_days` | Modification date |
|
---
|
||||||
| 504 | `disk_name` | BSTR: volume name |
|
|
||||||
|
## 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
|
## Hash Function
|
||||||
|
|
||||||
File/directory names are hashed into the 72-slot table:
|
File and directory names are hashed into the hash table slots:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
ULONG hash_name(const char *name, int table_size) {
|
/* The canonical AmigaDOS hash function: */
|
||||||
ULONG hash = strlen(name);
|
ULONG HashName(const char *name, ULONG table_size)
|
||||||
for (int i = 0; name[i]; i++) {
|
{
|
||||||
hash = hash * 13 + toupper(name[i]);
|
ULONG hash = (ULONG)strlen(name);
|
||||||
hash &= 0x7FF;
|
for (int i = 0; name[i]; i++)
|
||||||
|
{
|
||||||
|
hash = (hash * 13 + toupper((unsigned char)name[i])) & 0x7FF;
|
||||||
}
|
}
|
||||||
return hash % table_size;
|
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 |
|
| Bytes per data block | 488 | 512 |
|
||||||
| Max filename | 30 chars | 30 chars |
|
| Header overhead | 24 bytes/block | 0 |
|
||||||
| International | No | `DOS\2` (INTL OFS), `DOS\3` (INTL FFS) |
|
| Self-describing blocks | Yes (can recover from corruption) | No |
|
||||||
| Dir cache | No | `DOS\4` (FFS + dir cache) |
|
| Max filename | 30 chars | 30 chars (107 with DOS\6/\7) |
|
||||||
| Throughput | ~488/512 = 95% efficiency | 100% efficiency |
|
| 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
|
## Checksum Algorithm
|
||||||
|
|
||||||
```c
|
```c
|
||||||
LONG compute_checksum(ULONG *block, int longs) {
|
LONG ComputeBlockChecksum(ULONG *block, LONG longs)
|
||||||
|
{
|
||||||
LONG sum = 0;
|
LONG sum = 0;
|
||||||
for (int i = 0; i < longs; i++) sum += block[i];
|
block[5] = 0; /* clear checksum field before computing */
|
||||||
return -sum; /* stored in the checksum field to make total = 0 */
|
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`
|
- NDK39: `dos/filehandler.h`
|
||||||
- Ralph Babel: *The AmigaDOS Manual* (3rd edition) — definitive FFS reference
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -1,126 +1,382 @@
|
||||||
[← Home](../README.md) · [AmigaDOS](README.md)
|
[← Home](../README.md) · [AmigaDOS](README.md)
|
||||||
|
|
||||||
# Locks and Examine — Lock, UnLock, Examine, ExNext, ExAll
|
# Locks, Examine, and Directory Scanning
|
||||||
|
|
||||||
## Overview
|
## 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
|
## Lock Types
|
||||||
|
|
||||||
```c
|
| Mode | Constant | Dec | Meaning |
|
||||||
/* 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 |
|
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| −84 | `Lock(name, mode)` | D1=name, D2=mode | D0=lock BPTR (0=fail) |
|
| Shared | `SHARED_LOCK` / `ACCESS_READ` | −2 | Multiple readers allowed simultaneously |
|
||||||
| −90 | `UnLock(lock)` | D1=lock | — |
|
| Exclusive | `EXCLUSIVE_LOCK` / `ACCESS_WRITE` | −1 | Only one holder; blocks all other locks |
|
||||||
| −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
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
/* dos/dos.h — NDK39 */
|
/* Obtain a shared lock: */
|
||||||
struct FileInfoBlock {
|
BPTR lock = Lock("SYS:Libs", SHARED_LOCK);
|
||||||
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];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
/* Obtain an exclusive lock: */
|
||||||
> `FileInfoBlock` must be longword-aligned. Use `AllocDosObject(DOS_FIB, NULL)` on OS 2.0+ or `AllocMem(sizeof(struct FileInfoBlock), MEMF_PUBLIC)`.
|
BPTR lock = Lock("RAM:temp.dat", EXCLUSIVE_LOCK);
|
||||||
|
|
||||||
---
|
/* Always unlock when done: */
|
||||||
|
|
||||||
## 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);
|
|
||||||
UnLock(lock);
|
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
|
```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);
|
struct ExAllControl *eac = AllocDosObject(DOS_EXALLCONTROL, NULL);
|
||||||
UBYTE buf[4096];
|
eac->eac_LastKey = 0;
|
||||||
|
|
||||||
|
BPTR dirLock = Lock("SYS:Libs", SHARED_LOCK);
|
||||||
BOOL more;
|
BOOL more;
|
||||||
|
|
||||||
eac->eac_LastKey = 0;
|
|
||||||
do {
|
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;
|
struct ExAllData *ead = (struct ExAllData *)buf;
|
||||||
while (ead) {
|
while (ead)
|
||||||
Printf("%s\n", ead->ed_Name);
|
{
|
||||||
|
Printf(" %s (%ld bytes)\n", ead->ed_Name, ead->ed_Size);
|
||||||
ead = ead->ed_Next;
|
ead = ead->ed_Next;
|
||||||
}
|
}
|
||||||
} while (more);
|
} while (more);
|
||||||
|
|
||||||
FreeDosObject(DOS_EXALLCONTROL, eac);
|
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
|
## References
|
||||||
|
|
||||||
- NDK39: `dos/dos.h`, `dos/dosextens.h`, `dos/exall.h`
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -4,69 +4,275 @@
|
||||||
|
|
||||||
## Overview
|
## 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
|
```c
|
||||||
UBYTE allocationMap[] = { 1, 2, 4, 8 }; /* channel masks */
|
/* Period = clock_constant / desired_frequency */
|
||||||
struct IOAudio *aio = (struct IOAudio *)
|
/* PAL: clock = 3546895 Hz */
|
||||||
CreateIORequest(port, sizeof(struct IOAudio));
|
/* NTSC: clock = 3579545 Hz */
|
||||||
aio->ioa_Request.io_Message.mn_Node.ln_Pri = 0;
|
|
||||||
aio->ioa_Data = allocationMap;
|
#define PAL_CLOCK 3546895
|
||||||
aio->ioa_Length = sizeof(allocationMap);
|
#define NTSC_CLOCK 3579545
|
||||||
OpenDevice("audio.device", 0, (struct IORequest *)aio, 0);
|
|
||||||
/* aio->ioa_AllocKey = allocation key for this channel */
|
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 <devices/audio.h>
|
||||||
|
|
||||||
|
/* 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
|
## Playing a Sample
|
||||||
|
|
||||||
```c
|
```c
|
||||||
|
/* After successful OpenDevice: */
|
||||||
aio->ioa_Request.io_Command = CMD_WRITE;
|
aio->ioa_Request.io_Command = CMD_WRITE;
|
||||||
aio->ioa_Request.io_Flags = ADIOF_PERVOL;
|
aio->ioa_Request.io_Flags = ADIOF_PERVOL; /* set period and volume */
|
||||||
aio->ioa_Data = sampleData; /* MUST be in Chip RAM */
|
aio->ioa_Data = sampleData; /* MUST be in Chip RAM! */
|
||||||
aio->ioa_Length = sampleLength; /* in bytes */
|
aio->ioa_Length = sampleLength; /* in bytes (must be even) */
|
||||||
aio->ioa_Period = 428; /* ~8287 Hz (PAL) */
|
aio->ioa_Period = 428; /* ~8287 Hz (PAL) */
|
||||||
aio->ioa_Volume = 64; /* 0–64 */
|
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);
|
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.
|
||||||
|
|
||||||
```
|
### Double-Buffering (Continuous Playback)
|
||||||
Period = clock_constant / desired_frequency
|
|
||||||
PAL: clock = 3546895 Hz → Period = 3546895 / freq
|
|
||||||
NTSC: clock = 3579545 Hz → Period = 3579545 / freq
|
|
||||||
```
|
|
||||||
|
|
||||||
| Frequency | Period (PAL) |
|
```c
|
||||||
|---|---|
|
/* Use two IOAudio requests for gapless audio: */
|
||||||
| 8287 Hz | 428 |
|
struct IOAudio *aio_a = /* ... */;
|
||||||
| 11025 Hz | 322 |
|
struct IOAudio *aio_b = /* ... */;
|
||||||
| 22050 Hz | 161 |
|
|
||||||
| 28867 Hz | 124 (minimum safe) |
|
/* 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) |
|
| AUD0 | 7 | `INTF_AUD0` ($0080) |
|
||||||
| 1 | `$DFF0B0` | AUD1 (right) |
|
| AUD1 | 8 | `INTF_AUD1` ($0100) |
|
||||||
| 2 | `$DFF0C0` | AUD2 (right) |
|
| AUD2 | 9 | `INTF_AUD2` ($0200) |
|
||||||
| 3 | `$DFF0D0` | AUD3 (left) |
|
| 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
|
## References
|
||||||
|
|
||||||
- NDK39: `devices/audio.h`
|
- NDK39: `devices/audio.h`, `hardware/custom.h`
|
||||||
- HRM: audio DMA chapter
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,179 @@
|
||||||
[← Home](../README.md) · [Devices](README.md)
|
[← Home](../README.md) · [Devices](README.md)
|
||||||
|
|
||||||
# keyboard.device — Keyboard Input
|
# keyboard.device — Keyboard Hardware and Raw Key Codes
|
||||||
|
|
||||||
## Overview
|
## 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 |
|
| Code | Key | Code | Key |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `$00` | \` (backtick) | `$40` | Space |
|
| $60 | `Left Shift` | $61 | `Right Shift` |
|
||||||
| `$01`–`$0A` | 1–0 | `$41` | Backspace |
|
| $62 | `Caps Lock` | $63 | `Ctrl` |
|
||||||
| `$10`–`$19` | Q–P | `$42` | Tab |
|
| $64 | `Left Alt` | $65 | `Right Alt` |
|
||||||
| `$20`–`$28` | A–L | `$43` | Enter (keypad) |
|
| $66 | `Left Amiga` | $67 | `Right Amiga` |
|
||||||
| `$31`–`$39` | Z–/ | `$44` | Return |
|
|
||||||
| `$45` | Escape | `$46` | Delete |
|
### Cursor and Function Keys
|
||||||
| `$4C` | Cursor Up | `$4D` | Cursor Down |
|
|
||||||
| `$4E` | Cursor Right | `$4F` | Cursor Left |
|
| Code | Key | Code | Key |
|
||||||
| `$50`–`$59` | F1–F10 | `$5F` | Help |
|
|---|---|---|---|
|
||||||
| `$60` | Left Shift | `$61` | Right Shift |
|
| $4C | `Cursor Up` | $4D | `Cursor Down` |
|
||||||
| `$62` | Caps Lock | `$63` | Control |
|
| $4E | `Cursor Right` | $4F | `Cursor Left` |
|
||||||
| `$64` | Left Alt | `$65` | Right Alt |
|
| $50–$59 | `F1`–`F10` | $5F | `Help` |
|
||||||
| `$66` | Left Amiga | `$67` | Right Amiga |
|
|
||||||
|
### 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 |
|
Most applications receive key events through Intuition IDCMP (see [idcmp.md](../09_intuition/idcmp.md)). Direct keyboard.device use is for system-level software:
|
||||||
|---|---|---|
|
|
||||||
| 2 | `CMD_READ` | Read raw keycodes |
|
```c
|
||||||
| 5 | `CMD_CLEAR` | Clear keyboard buffer |
|
struct MsgPort *kbPort = CreateMsgPort();
|
||||||
| 9 | `KBD_READMATRIX` | Read full key matrix state |
|
struct IOStdReq *kbReq = (struct IOStdReq *)
|
||||||
| 10 | `KBD_ADDRESETHANDLER` | Add Ctrl-Amiga-Amiga handler |
|
CreateIORequest(kbPort, sizeof(struct IOStdReq));
|
||||||
| 11 | `KBD_REMRESETHANDLER` | Remove reset handler |
|
|
||||||
| 12 | `KBD_RESETHANDLERDONE` | Acknowledge reset handler completion |
|
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
|
## 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
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,110 @@
|
||||||
[← Home](../README.md) · [Devices](README.md)
|
[← Home](../README.md) · [Devices](README.md)
|
||||||
|
|
||||||
# timer.device — Timing and Delays
|
# timer.device — Timing, Delays, and High-Resolution Timestamps
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
`timer.device` provides precise timing services: delays, time-of-day, and high-resolution timestamps. It has two units:
|
`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).
|
||||||
|
|
||||||
| 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+) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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<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.
|
||||||
|
|
||||||
|
```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
|
```c
|
||||||
/* devices/timer.h — NDK39 */
|
/* devices/timer.h — NDK39 */
|
||||||
struct timeval {
|
struct timeval {
|
||||||
ULONG tv_secs; /* seconds */
|
ULONG tv_secs; /* seconds */
|
||||||
ULONG tv_micro; /* microseconds */
|
ULONG tv_micro; /* microseconds (0–999999) */
|
||||||
};
|
};
|
||||||
|
|
||||||
struct timerequest {
|
struct timerequest {
|
||||||
struct IORequest tr_node;
|
struct IORequest tr_node;
|
||||||
struct timeval tr_time;
|
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
|
## Simple Delay
|
||||||
|
|
||||||
```c
|
```c
|
||||||
struct timerequest *tr = (struct timerequest *)
|
/* Block the current task for exactly 2.5 seconds: */
|
||||||
CreateIORequest(port, sizeof(struct timerequest));
|
|
||||||
OpenDevice("timer.device", UNIT_VBLANK, (struct IORequest *)tr, 0);
|
|
||||||
|
|
||||||
tr->tr_node.io_Command = TR_ADDREQUEST;
|
tr->tr_node.io_Command = TR_ADDREQUEST;
|
||||||
tr->tr_time.tv_secs = 2;
|
tr->tr_time.tv_secs = 2;
|
||||||
tr->tr_time.tv_micro = 0;
|
tr->tr_time.tv_micro = 500000; /* 0.5 sec */
|
||||||
DoIO((struct IORequest *)tr); /* blocks for 2 seconds */
|
DoIO((struct IORequest *)tr); /* blocks until done */
|
||||||
|
```
|
||||||
|
|
||||||
CloseDevice((struct IORequest *)tr);
|
### Non-Blocking Delay
|
||||||
DeleteIORequest((struct IORequest *)tr);
|
|
||||||
|
```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
|
## Getting Current Time
|
||||||
|
|
||||||
```c
|
```c
|
||||||
|
/* Get system time (wall clock since midnight Jan 1, 1978): */
|
||||||
tr->tr_node.io_Command = TR_GETSYSTIME;
|
tr->tr_node.io_Command = TR_GETSYSTIME;
|
||||||
DoIO((struct IORequest *)tr);
|
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: t1<t2, 0: equal, >0: t1>t2 */
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## High-Resolution Timing
|
## High-Resolution Timing (ReadEClock)
|
||||||
|
|
||||||
```c
|
```c
|
||||||
/* Read E-clock (OS 2.0+): */
|
/* Most precise timing available — E-clock resolution: */
|
||||||
struct EClockVal eclock;
|
struct EClockVal start, end;
|
||||||
ULONG freq = ReadEClock(&eclock); /* returns ticks/second */
|
ULONG efreq = ReadEClock(&start); /* returns ticks/second */
|
||||||
/* eclock.ev_hi, eclock.ev_lo = 64-bit tick count */
|
|
||||||
/* Typical freq: 709379 Hz (PAL) or 715909 Hz (NTSC) */
|
/* ... 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
|
## References
|
||||||
|
|
||||||
- NDK39: `devices/timer.h`
|
- NDK39: `devices/timer.h`
|
||||||
- ADCD 2.1: timer.device autodocs
|
- ADCD 2.1: timer.device autodocs
|
||||||
|
- HRM: CIA timer chapter
|
||||||
|
- See also: [interrupts.md](../06_exec_os/interrupts.md) — VBlank interrupt chain
|
||||||
|
|
|
||||||
|
|
@ -1,74 +1,178 @@
|
||||||
[← Home](../README.md) · [Devices](README.md)
|
[← Home](../README.md) · [Devices](README.md)
|
||||||
|
|
||||||
# trackdisk.device — Floppy Disk I/O
|
# trackdisk.device — Floppy Disk DMA Controller
|
||||||
|
|
||||||
## Overview
|
## 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
|
```mermaid
|
||||||
struct IOExtTD *tdreq = (struct IOExtTD *)
|
graph LR
|
||||||
CreateIORequest(port, sizeof(struct IOExtTD));
|
subgraph "Custom Chips"
|
||||||
OpenDevice("trackdisk.device", 0, (struct IORequest *)tdreq, 0);
|
DSKBYTR["DSKBYTR<br/>$DFF01A<br/>Disk Data Byte"]
|
||||||
|
DSKLEN["DSKLEN<br/>$DFF024<br/>DMA Length"]
|
||||||
|
DSKPT["DSKPT<br/>$DFF020<br/>DMA Pointer"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "CIA-B"
|
||||||
|
CIAPRB["PRA/PRB<br/>$BFD100<br/>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<br/>(~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 |
|
### Opening
|
||||||
|---|---|---|
|
|
||||||
| 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
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
UBYTE buf[512];
|
struct MsgPort *diskPort = CreateMsgPort();
|
||||||
tdreq->iotd_Req.io_Command = CMD_READ;
|
struct IOExtTD *diskReq = (struct IOExtTD *)
|
||||||
tdreq->iotd_Req.io_Data = buf;
|
CreateIORequest(diskPort, sizeof(struct IOExtTD));
|
||||||
tdreq->iotd_Req.io_Length = 512;
|
|
||||||
tdreq->iotd_Req.io_Offset = 0; /* byte offset = sector * 512 */
|
/* Unit numbers: DF0:=0, DF1:=1, DF2:=2, DF3:=3 */
|
||||||
DoIO((struct IORequest *)tdreq);
|
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) |
|
Games often bypass trackdisk.device for speed and copy protection:
|
||||||
|---|---|---|
|
|
||||||
| Heads | 2 | 2 |
|
|
||||||
| Cylinders | 80 | 80 |
|
|
||||||
| Sectors/track | 11 | 22 |
|
|
||||||
| Bytes/sector | 512 | 512 |
|
|
||||||
| Total sectors | 1760 | 3520 |
|
|
||||||
|
|
||||||
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
|
## 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
|
- ADCD 2.1: trackdisk.device autodocs
|
||||||
|
- See also: [filesystem.md](../07_dos/filesystem.md) — FFS/OFS block format on top of trackdisk
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue