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:
Ilia Sharin 2026-04-23 20:23:50 -04:00
parent aeaea88d75
commit da9e7d3b63
7 changed files with 1599 additions and 301 deletions

View file

@ -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 01: 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 01; 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 01023) | 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 01)
| 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 |
| 24312 | `ht[72]` | Hash table: block pointers for directory entries |
| 420 | `bm_flag` | Bitmap valid flag (`-1` = valid) |
| 424472 | `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