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

@ -4,69 +4,275 @@
## Overview
`audio.device` provides access to the Amiga's 4 DMA audio channels. Each channel plays 8-bit PCM samples from Chip RAM at programmable rates.
`audio.device` provides access to the Amiga's **4 hardware DMA audio channels**. Each channel plays 8-bit signed PCM samples from Chip RAM at a programmable rate. The hardware supports independent volume, period (sample rate), and unlimited looping. Audio DMA is handled entirely by the custom chips — the CPU is not involved in the actual sample playback.
---
## Channel Allocation
## Audio Hardware Architecture
```mermaid
graph TD
subgraph "Custom Chips (Paula)"
AUD0["AUD0 (Left)"]
AUD1["AUD1 (Right)"]
AUD2["AUD2 (Right)"]
AUD3["AUD3 (Left)"]
end
subgraph "Chip RAM"
S0["Sample 0"]
S1["Sample 1"]
S2["Sample 2"]
S3["Sample 3"]
end
S0 -->|DMA| AUD0
S1 -->|DMA| AUD1
S2 -->|DMA| AUD2
S3 -->|DMA| AUD3
AUD0 --> LEFT["Left Output"]
AUD3 --> LEFT
AUD1 --> RIGHT["Right Output"]
AUD2 --> RIGHT
```
### Channel Stereo Assignment
| Channel | Default Panning | Hardware Register Base |
|---|---|---|
| 0 | **Left** | `$DFF0A0` |
| 1 | **Right** | `$DFF0B0` |
| 2 | **Right** | `$DFF0C0` |
| 3 | **Left** | `$DFF0D0` |
This is fixed in hardware — you cannot reassign channels to different outputs. Software panning requires mixing samples before playback.
---
## Channel Registers
Each channel has 5 registers (10 bytes):
| Offset | Register | Size | Description |
|---|---|---|---|
| +$00 | `AUDxLCH` | WORD | Pointer high — bits 18:16 of sample address |
| +$02 | `AUDxLCL` | WORD | Pointer low — bits 15:1 of sample address |
| +$04 | `AUDxLEN` | WORD | Length in **words** (not bytes). Min=1, Max=65535 |
| +$06 | `AUDxPER` | WORD | Period (DMA clock divider). Lower = faster = higher frequency |
| +$08 | `AUDxVOL` | WORD | Volume: 0 (silent) to 64 (max). Values >64 produce distortion on some models |
### Period ↔ Frequency Conversion
```c
UBYTE allocationMap[] = { 1, 2, 4, 8 }; /* channel masks */
struct IOAudio *aio = (struct IOAudio *)
CreateIORequest(port, sizeof(struct IOAudio));
aio->ioa_Request.io_Message.mn_Node.ln_Pri = 0;
aio->ioa_Data = allocationMap;
aio->ioa_Length = sizeof(allocationMap);
OpenDevice("audio.device", 0, (struct IORequest *)aio, 0);
/* aio->ioa_AllocKey = allocation key for this channel */
/* Period = clock_constant / desired_frequency */
/* PAL: clock = 3546895 Hz */
/* NTSC: clock = 3579545 Hz */
#define PAL_CLOCK 3546895
#define NTSC_CLOCK 3579545
UWORD period_from_hz(ULONG freq, BOOL isPAL) {
return (isPAL ? PAL_CLOCK : NTSC_CLOCK) / freq;
}
ULONG hz_from_period(UWORD period, BOOL isPAL) {
return (isPAL ? PAL_CLOCK : NTSC_CLOCK) / period;
}
```
| Frequency | Period (PAL) | Period (NTSC) | Quality |
|---|---|---|---|
| 8287 Hz | 428 | 432 | Amiga standard (MOD default) |
| 11025 Hz | 322 | 325 | Low-quality speech |
| 16726 Hz | 212 | 214 | CD/4 quality |
| 22050 Hz | 161 | 162 | Near-CD quality |
| 28867 Hz | 123 | 124 | Maximum safe rate |
> [!WARNING]
> Periods below ~124 cause DMA contention with other chip resources (sprites, bitplanes). The hardware minimum is period=1, but anything below ~124 steals so many DMA cycles that display and other I/O suffers.
---
## Channel Allocation via audio.device
The OS manages channel allocation to prevent conflicts between applications:
```c
/* Request channels: */
#include <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
```c
/* After successful OpenDevice: */
aio->ioa_Request.io_Command = CMD_WRITE;
aio->ioa_Request.io_Flags = ADIOF_PERVOL;
aio->ioa_Data = sampleData; /* MUST be in Chip RAM */
aio->ioa_Length = sampleLength; /* in bytes */
aio->ioa_Request.io_Flags = ADIOF_PERVOL; /* set period and volume */
aio->ioa_Data = sampleData; /* MUST be in Chip RAM! */
aio->ioa_Length = sampleLength; /* in bytes (must be even) */
aio->ioa_Period = 428; /* ~8287 Hz (PAL) */
aio->ioa_Volume = 64; /* 064 */
aio->ioa_Cycles = 1; /* 0 = loop forever */
aio->ioa_Cycles = 1; /* 0 = loop forever, N = play N times */
BeginIO((struct IORequest *)aio);
/* Returns immediately — DMA plays in background */
/* Wait for completion: */
WaitIO((struct IORequest *)aio);
```
### Period Calculation
> [!IMPORTANT]
> Sample data **must** be in Chip RAM (`MEMF_CHIP`). The DMA hardware can only read from the first 2 MB of address space. Passing a Fast RAM pointer results in silence or random noise.
```
Period = clock_constant / desired_frequency
PAL: clock = 3546895 Hz → Period = 3546895 / freq
NTSC: clock = 3579545 Hz → Period = 3579545 / freq
```
### Double-Buffering (Continuous Playback)
| Frequency | Period (PAL) |
|---|---|
| 8287 Hz | 428 |
| 11025 Hz | 322 |
| 22050 Hz | 161 |
| 28867 Hz | 124 (minimum safe) |
```c
/* Use two IOAudio requests for gapless audio: */
struct IOAudio *aio_a = /* ... */;
struct IOAudio *aio_b = /* ... */;
/* Start first buffer: */
aio_a->ioa_Data = buffer_a;
aio_a->ioa_Length = BUFFER_SIZE;
aio_a->ioa_Cycles = 1;
BeginIO((struct IORequest *)aio_a);
/* Queue second buffer immediately: */
aio_b->ioa_Data = buffer_b;
aio_b->ioa_Length = BUFFER_SIZE;
aio_b->ioa_Cycles = 1;
BeginIO((struct IORequest *)aio_b);
/* When aio_a completes, fill buffer_a with new data and re-queue: */
WaitIO((struct IORequest *)aio_a);
/* fill buffer_a... */
BeginIO((struct IORequest *)aio_a);
/* When aio_b completes, fill buffer_b... */
```
---
## Channel Registers
## Audio Interrupts
| Channel | Address | Description |
Each channel generates an interrupt when its DMA period completes (all sample words played):
| Channel | Interrupt Bit | INTENA/INTREQ |
|---|---|---|
| 0 | `$DFF0A0` | AUD0 (left) |
| 1 | `$DFF0B0` | AUD1 (right) |
| 2 | `$DFF0C0` | AUD2 (right) |
| 3 | `$DFF0D0` | AUD3 (left) |
| AUD0 | 7 | `INTF_AUD0` ($0080) |
| AUD1 | 8 | `INTF_AUD1` ($0100) |
| AUD2 | 9 | `INTF_AUD2` ($0200) |
| AUD3 | 10 | `INTF_AUD3` ($0400) |
Each channel: pointer (PTH/PTL), length (LEN), period (PER), volume (VOL).
```c
/* Set up audio interrupt: */
struct Interrupt audioInt;
audioInt.is_Node.ln_Type = NT_INTERRUPT;
audioInt.is_Node.ln_Pri = 0;
audioInt.is_Node.ln_Name = "MyAudioInt";
audioInt.is_Data = myData;
audioInt.is_Code = (APTR)AudioIntHandler;
AddIntServer(INTB_AUD0, &audioInt);
/* Handler — called when channel 0 finishes playing: */
__saveds void AudioIntHandler(void)
{
/* Reload next sample segment into AUD0LCH/AUD0LCL/AUD0LEN */
custom->aud[0].ac_ptr = nextSample;
custom->aud[0].ac_len = nextLength / 2; /* length in words */
}
```
---
## Modulation Modes
The Amiga audio hardware supports two modulation modes for channels 0+1 and 2+3:
### Amplitude Modulation
Channel `N` modulates the volume of channel `N+1`:
```c
/* Channel 0 data controls channel 1's volume */
custom->adkcon = ADKF_USE0V1; /* enable AM: ch0 → ch1 volume */
```
### Period Modulation (Frequency Modulation)
Channel `N` modulates the period of channel `N+1`:
```c
/* Channel 0 data controls channel 1's period */
custom->adkcon = ADKF_USE0P1; /* enable PM: ch0 → ch1 period */
```
These modes are rarely used in practice — they reduce the effective channel count from 4 to 2.
---
## Direct Hardware Access (Bypassing audio.device)
Games and demos often bypass `audio.device` entirely:
```asm
; Direct hardware — play a sample on channel 0:
LEA $DFF000, A5 ; custom chip base
MOVE.L #sample, $A0(A5) ; AUD0LC — sample pointer
MOVE.W #length/2, $A4(A5) ; AUD0LEN — length in words
MOVE.W #428, $A6(A5) ; AUD0PER — period (8287 Hz)
MOVE.W #64, $A8(A5) ; AUD0VOL — max volume
; Enable DMA for channel 0:
MOVE.W #$8201, $96(A5) ; DMACON — set DMAEN + AUD0EN
```
> **Warning**: Direct access conflicts with any OS audio. Always `OwnBlitter()` / `Forbid()` and disable audio device first if mixing approaches.
---
## References
- NDK39: `devices/audio.h`
- HRM: audio DMA chapter
- NDK39: `devices/audio.h`, `hardware/custom.h`
- HRM: *Amiga Hardware Reference Manual* — Audio DMA chapter
- ADCD 2.1: `audio.device` autodocs
- See also: [interrupts.md](../06_exec_os/interrupts.md) — interrupt server chain

View file

@ -1,48 +1,179 @@
[← Home](../README.md) · [Devices](README.md)
# keyboard.device — Keyboard Input
# keyboard.device — Keyboard Hardware and Raw Key Codes
## Overview
`keyboard.device` provides raw keycode events from the keyboard controller (8520 CIA-A). Normally used indirectly via `input.device`, but can be accessed directly for keymap-independent scanning.
`keyboard.device` provides access to the Amiga keyboard via the CIA-A serial port handshake protocol. The keyboard controller (a dedicated 6500/1 or 68HC05 microcontroller inside the keyboard) transmits **raw key codes** as serial data through CIA-A. Understanding this protocol is essential for FPGA core implementation.
---
## Keycodes
## Hardware Protocol
Amiga keycodes are 7-bit values (0127). 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 (0127)
```c
/* Decode a raw key event: */
UBYTE raw = ~(cia->ciaSDR); /* invert (active low) */
raw = (raw >> 1) | (raw << 7); /* rotate right 1 bit */
UBYTE keycode = raw & 0x7F;
BOOL keyup = raw & 0x80;
```
> **Bit rotation**: The keyboard transmits bits in a rotated format. The software must rotate the received byte right by 1 bit to get the actual key code. This is a hardware design quirk.
### Handshake Timing
After reading the key code, the CPU must acknowledge by pulsing the KDAT line:
```asm
; Acknowledge key reception:
OR.B #$40, $BFEE01 ; CIACRA — set SP to output
; Wait at least 85 µs
MOVE.B #0, $BFEC01 ; drive KDAT low
; (timer or loop delay ~85 µs)
AND.B #~$40, $BFEE01 ; CIACRA — set SP back to input
; Keyboard is now ready to send next key
```
> **FPGA note**: If the handshake acknowledgement is too fast (<75µs) or too slow (>200ms), the keyboard MCU will resend the key code or initiate a reset sequence.
---
## Raw Key Code Map
### Main Keys
| Code | Key | Code | Key | Code | Key |
|---|---|---|---|---|---|
| $00 | `` ` `` (backtick) | $01 | `1` | $02 | `2` |
| $03 | `3` | $04 | `4` | $05 | `5` |
| $06 | `6` | $07 | `7` | $08 | `8` |
| $09 | `9` | $0A | `0` | $0B | `-` |
| $0C | `=` | $0D | `\` | $10 | `Q` |
| $11 | `W` | $12 | `E` | $13 | `R` |
| $14 | `T` | $15 | `Y` | $16 | `U` |
| $17 | `I` | $18 | `O` | $19 | `P` |
| $1A | `[` | $1B | `]` | $20 | `A` |
| $21 | `S` | $22 | `D` | $23 | `F` |
| $24 | `G` | $25 | `H` | $26 | `J` |
| $27 | `K` | $28 | `L` | $29 | `;` |
| $2A | `'` | $31 | `Z` | $32 | `X` |
| $33 | `C` | $34 | `V` | $35 | `B` |
| $36 | `N` | $37 | `M` | $38 | `,` |
| $39 | `.` | $3A | `/` | $40 | `Space` |
| $41 | `Backspace` | $42 | `Tab` | $43 | `Numpad Enter` |
| $44 | `Return` | $45 | `Escape` | $46 | `Delete` |
### Modifier and Special Keys
| Code | Key | Code | Key |
|---|---|---|---|
| `$00` | \` (backtick) | `$40` | Space |
| `$01``$0A` | 10 | `$41` | Backspace |
| `$10``$19` | QP | `$42` | Tab |
| `$20``$28` | AL | `$43` | Enter (keypad) |
| `$31``$39` | Z/ | `$44` | Return |
| `$45` | Escape | `$46` | Delete |
| `$4C` | Cursor Up | `$4D` | Cursor Down |
| `$4E` | Cursor Right | `$4F` | Cursor Left |
| `$50``$59` | F1F10 | `$5F` | Help |
| `$60` | Left Shift | `$61` | Right Shift |
| `$62` | Caps Lock | `$63` | Control |
| `$64` | Left Alt | `$65` | Right Alt |
| `$66` | Left Amiga | `$67` | Right Amiga |
| $60 | `Left Shift` | $61 | `Right Shift` |
| $62 | `Caps Lock` | $63 | `Ctrl` |
| $64 | `Left Alt` | $65 | `Right Alt` |
| $66 | `Left Amiga` | $67 | `Right Amiga` |
### Cursor and Function Keys
| Code | Key | Code | Key |
|---|---|---|---|
| $4C | `Cursor Up` | $4D | `Cursor Down` |
| $4E | `Cursor Right` | $4F | `Cursor Left` |
| $50$59 | `F1``F10` | $5F | `Help` |
### Special Codes
| Code | Meaning |
|---|---|
| $78 | **Reset warning** — Ctrl+Amiga+Amiga pressed (keyboard sends this before resetting) |
| $F9 | Last key code was bad (parity error) — resend |
| $FA | Keyboard buffer overflow |
| $FC | Keyboard self-test failed |
| $FD | Initiate power-up key stream |
| $FE | Terminate power-up key stream |
---
## Commands
## Using keyboard.device (OS Level)
| Code | Constant | Description |
|---|---|---|
| 2 | `CMD_READ` | Read raw keycodes |
| 5 | `CMD_CLEAR` | Clear keyboard buffer |
| 9 | `KBD_READMATRIX` | Read full key matrix state |
| 10 | `KBD_ADDRESETHANDLER` | Add Ctrl-Amiga-Amiga handler |
| 11 | `KBD_REMRESETHANDLER` | Remove reset handler |
| 12 | `KBD_RESETHANDLERDONE` | Acknowledge reset handler completion |
Most applications receive key events through Intuition IDCMP (see [idcmp.md](../09_intuition/idcmp.md)). Direct keyboard.device use is for system-level software:
```c
struct MsgPort *kbPort = CreateMsgPort();
struct IOStdReq *kbReq = (struct IOStdReq *)
CreateIORequest(kbPort, sizeof(struct IOStdReq));
OpenDevice("keyboard.device", 0, (struct IORequest *)kbReq, 0);
/* Read raw key events: */
struct InputEvent ie;
kbReq->io_Command = KBD_READMATRIX;
kbReq->io_Data = &keyMatrix; /* 16-byte key matrix bitmap */
kbReq->io_Length = 16;
DoIO((struct IORequest *)kbReq);
/* keyMatrix bit N = 1 if key code N is currently held down */
/* Reset keyboard (force self-test): */
kbReq->io_Command = KBD_RESETHANDLER;
DoIO((struct IORequest *)kbReq);
```
### Key Matrix
The `KBD_READMATRIX` command returns a 16-byte (128-bit) bitmap where each bit corresponds to a raw key code:
```c
UBYTE keyMatrix[16];
/* Bit test: is key $45 (Escape) pressed? */
BOOL escPressed = keyMatrix[0x45 / 8] & (1 << (0x45 % 8));
```
---
## Keyboard Reset Sequence
The Ctrl+Amiga+Amiga three-key combination triggers a hardware reset:
1. User presses Ctrl+Left Amiga+Right Amiga
2. Keyboard MCU detects the combo
3. Sends raw code `$78` (reset warning) — gives software ~10 seconds to clean up
4. Pulls KBRST line low → triggers 68000 reset via RESET pin
> **FPGA**: The core must implement this reset path. The `$78` warning code allows software (e.g., debuggers) to save state before reset.
---
## References
- NDK39: `devices/keyboard.h`
- NDK39: `devices/keyboard.h`, `devices/inputevent.h`
- HRM: *Amiga Hardware Reference Manual* — Keyboard chapter
- See also: [input.md](input.md) — input.device handler chain
- See also: [idcmp.md](../09_intuition/idcmp.md) — high-level key events via Intuition

View file

@ -1,34 +1,110 @@
[← Home](../README.md) · [Devices](README.md)
# timer.device — Timing and Delays
# timer.device — Timing, Delays, and High-Resolution Timestamps
## Overview
`timer.device` provides precise timing services: delays, time-of-day, and high-resolution timestamps. It has two units:
| Unit | Constant | Resolution | Use |
|---|---|---|---|
| 0 | `UNIT_MICROHZ` | ~2µs (E-clock) | Short, precise delays |
| 1 | `UNIT_VBLANK` | ~20ms (VBlank) | Long delays, lower overhead |
| 2 | `UNIT_ECLOCK` | CIA E-clock ticks | Highest resolution timing (OS 2.0+) |
| 3 | `UNIT_WAITUNTIL` | absolute time | Wait until specific time (OS 2.0+) |
| 4 | `UNIT_WAITECLOCK` | E-clock absolute | (OS 2.0+) |
`timer.device` provides all timing services on AmigaOS: delays, system clock queries, and high-resolution timestamps. It interfaces with two independent hardware sources — the **CIA timers** (microsecond resolution) and the **vertical blank interrupt** (frame-rate resolution).
---
## struct timeval / timerequest
## Units
| Unit | Constant | Resolution | Clock Source | Use Case |
|---|---|---|---|---|
| 0 | `UNIT_MICROHZ` | ~1.4 µs (E-clock tick) | CIA-A Timer A | Short, precise delays |
| 1 | `UNIT_VBLANK` | ~20 ms (PAL) / ~16.7 ms (NTSC) | VBlank interrupt | Long delays, low CPU overhead |
| 2 | `UNIT_ECLOCK` | ~1.4 µs | CIA-B Timer A | Highest resolution timing (OS 2.0+) |
| 3 | `UNIT_WAITUNTIL` | absolute time | System clock | Wait until specific wall-clock time |
| 4 | `UNIT_WAITECLOCK` | E-clock absolute | CIA | Wait until specific E-clock value |
### Which Unit to Use?
```mermaid
flowchart TD
Q["How long is the delay?"] --> SHORT["< 100 ms"]
Q --> LONG["> 100 ms"]
SHORT --> MICRO["UNIT_MICROHZ<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
/* devices/timer.h — NDK39 */
struct timeval {
ULONG tv_secs; /* seconds */
ULONG tv_micro; /* microseconds */
ULONG tv_micro; /* microseconds (0999999) */
};
struct timerequest {
struct IORequest tr_node;
struct timeval tr_time;
};
/* sizeof(timerequest) = sizeof(IORequest) + 8 */
```
### EClockVal (OS 2.0+)
```c
struct EClockVal {
ULONG ev_hi; /* high 32 bits of 64-bit tick counter */
ULONG ev_lo; /* low 32 bits */
};
```
---
## Opening timer.device
```c
struct MsgPort *timerPort = CreateMsgPort();
struct timerequest *tr = (struct timerequest *)
CreateIORequest(timerPort, sizeof(struct timerequest));
BYTE err = OpenDevice("timer.device", UNIT_MICROHZ,
(struct IORequest *)tr, 0);
if (err != 0) { /* handle error */ }
/* IMPORTANT: after opening, you can get TimerBase for direct calls: */
struct Library *TimerBase = (struct Library *)tr->tr_node.io_Device;
/* Now you can call AddTime(), SubTime(), CmpTime(), ReadEClock() */
```
---
@ -36,17 +112,51 @@ struct timerequest {
## Simple Delay
```c
struct timerequest *tr = (struct timerequest *)
CreateIORequest(port, sizeof(struct timerequest));
OpenDevice("timer.device", UNIT_VBLANK, (struct IORequest *)tr, 0);
/* Block the current task for exactly 2.5 seconds: */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 2;
tr->tr_time.tv_micro = 0;
DoIO((struct IORequest *)tr); /* blocks for 2 seconds */
tr->tr_time.tv_micro = 500000; /* 0.5 sec */
DoIO((struct IORequest *)tr); /* blocks until done */
```
CloseDevice((struct IORequest *)tr);
DeleteIORequest((struct IORequest *)tr);
### Non-Blocking Delay
```c
/* Start delay, continue doing work, then wait: */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 0;
tr->tr_time.tv_micro = 100000; /* 100 ms */
SendIO((struct IORequest *)tr); /* non-blocking */
/* ... do other work ... */
/* Check if timer expired: */
if (CheckIO((struct IORequest *)tr))
{
WaitIO((struct IORequest *)tr); /* collect result */
/* timer expired */
}
```
### Signal-Based Waiting
```c
/* Wait for timer OR user input: */
ULONG timerSig = 1L << timerPort->mp_SigBit;
ULONG windowSig = 1L << window->UserPort->mp_SigBit;
SendIO((struct IORequest *)tr);
ULONG sigs = Wait(timerSig | windowSig);
if (sigs & timerSig) {
WaitIO((struct IORequest *)tr);
/* handle timeout */
}
if (sigs & windowSig) {
AbortIO((struct IORequest *)tr);
WaitIO((struct IORequest *)tr);
/* handle window event */
}
```
---
@ -54,26 +164,125 @@ DeleteIORequest((struct IORequest *)tr);
## Getting Current Time
```c
/* Get system time (wall clock since midnight Jan 1, 1978): */
tr->tr_node.io_Command = TR_GETSYSTIME;
DoIO((struct IORequest *)tr);
Printf("Time: %lu.%06lu\n", tr->tr_time.tv_secs, tr->tr_time.tv_micro);
Printf("Time: %lu.%06lu seconds since epoch\n",
tr->tr_time.tv_secs, tr->tr_time.tv_micro);
```
### Time Arithmetic
```c
/* After opening timer.device and getting TimerBase: */
struct timeval t1, t2, diff;
/* Measure elapsed time: */
tr->tr_node.io_Command = TR_GETSYSTIME;
DoIO((struct IORequest *)tr);
t1 = tr->tr_time;
/* ... do work ... */
DoIO((struct IORequest *)tr);
t2 = tr->tr_time;
/* Compute difference: */
diff = t2;
SubTime(&diff, &t1);
Printf("Elapsed: %lu.%06lu s\n", diff.tv_secs, diff.tv_micro);
/* Compare times: */
LONG cmp = CmpTime(&t1, &t2); /* <0: t1<t2, 0: equal, >0: t1>t2 */
```
---
## High-Resolution Timing
## High-Resolution Timing (ReadEClock)
```c
/* Read E-clock (OS 2.0+): */
struct EClockVal eclock;
ULONG freq = ReadEClock(&eclock); /* returns ticks/second */
/* eclock.ev_hi, eclock.ev_lo = 64-bit tick count */
/* Typical freq: 709379 Hz (PAL) or 715909 Hz (NTSC) */
/* Most precise timing available — E-clock resolution: */
struct EClockVal start, end;
ULONG efreq = ReadEClock(&start); /* returns ticks/second */
/* ... code to benchmark ... */
ReadEClock(&end);
/* Compute elapsed microseconds: */
ULONG ticks = end.ev_lo - start.ev_lo; /* assumes <4 billion ticks */
ULONG usecs = ticks * 1000000 / efreq;
Printf("Elapsed: %lu µs (E-clock freq: %lu Hz)\n", usecs, efreq);
```
| Standard | E-clock Freq | Tick Resolution |
|---|---|---|
| PAL | 709,379 Hz | ~1.410 µs |
| NTSC | 715,909 Hz | ~1.397 µs |
---
## Periodic Timer (Game Loop / Audio Refill)
```c
/* Classic pattern: periodic callback using timer.device */
#define FRAME_USEC 20000 /* 50 Hz (PAL frame rate) */
void GameLoop(void)
{
ULONG timerSig = 1L << timerPort->mp_SigBit;
/* Kick off first timer request: */
tr->tr_node.io_Command = TR_ADDREQUEST;
tr->tr_time.tv_secs = 0;
tr->tr_time.tv_micro = FRAME_USEC;
SendIO((struct IORequest *)tr);
BOOL running = TRUE;
while (running)
{
ULONG sigs = Wait(timerSig | SIGBREAKF_CTRL_C);
if (sigs & timerSig)
{
WaitIO((struct IORequest *)tr);
/* --- Game logic here --- */
UpdateGame();
RenderFrame();
/* Re-arm timer: */
tr->tr_time.tv_secs = 0;
tr->tr_time.tv_micro = FRAME_USEC;
SendIO((struct IORequest *)tr);
}
if (sigs & SIGBREAKF_CTRL_C)
running = FALSE;
}
AbortIO((struct IORequest *)tr);
WaitIO((struct IORequest *)tr);
}
```
---
## Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Reusing active IORequest | Sending a `TR_ADDREQUEST` while previous is pending | Use two timerequest structs, or `WaitIO` first |
| Forgetting `WaitIO` after `AbortIO` | Leaves IORequest in limbo — crash on next use | Always `WaitIO` after `AbortIO`, even if aborted |
| Using `UNIT_VBLANK` for short delays | 20 ms granularity — actual delay is 0 to 20 ms | Use `UNIT_MICROHZ` for sub-20ms precision |
| Not opening timer for `ReadEClock` | `TimerBase` is NULL — crash | Must `OpenDevice` first to get `TimerBase` |
| Ignoring PAL/NTSC differences | Hardcoded periods wrong on other standard | Use `ReadEClock()` frequency for calculations |
---
## References
- NDK39: `devices/timer.h`
- ADCD 2.1: timer.device autodocs
- HRM: CIA timer chapter
- See also: [interrupts.md](../06_exec_os/interrupts.md) — VBlank interrupt chain

View file

@ -1,74 +1,178 @@
[← Home](../README.md) · [Devices](README.md)
# trackdisk.device — Floppy Disk I/O
# trackdisk.device — Floppy Disk DMA Controller
## Overview
`trackdisk.device` provides raw sector I/O for Amiga floppy drives. Each drive is a unit (03). The device operates on 512-byte sectors, 11 sectors per track (880 KB DD disks) or 22 per track (1760 KB HD).
`trackdisk.device` interfaces with the Amiga's floppy disk controller — a custom DMA engine that reads and writes raw MFM-encoded data from double-density 3.5" disks. It provides block-level access (512 bytes/sector, 11 sectors/track, 80 tracks × 2 sides = 1,760 sectors = 880 KB per disk).
---
## Opening
## Hardware Architecture
```c
struct IOExtTD *tdreq = (struct IOExtTD *)
CreateIORequest(port, sizeof(struct IOExtTD));
OpenDevice("trackdisk.device", 0, (struct IORequest *)tdreq, 0);
```mermaid
graph LR
subgraph "Custom Chips"
DSKBYTR["DSKBYTR<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 (079) |
| Sides | 2 (0=upper, 1=lower) |
| Sectors per track | 11 (DD), 22 (HD) |
| Bytes per sector | 512 |
| Total capacity | 880 KB (DD), 1,760 KB (HD) |
| Rotation speed | 300 RPM (1 revolution = 200 ms) |
| Transfer rate | ~250 kbit/s (DD raw MFM) |
| Track-to-track seek | ~3 ms |
### MFM Encoding
The disk stores data in **Modified Frequency Modulation** format. Each byte becomes 16 bits on disk (clock + data interleaved):
```
Data bit: 1 0 1 1 0 0 0 1
MFM: 01 10 01 01 10 10 10 01
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
c/d pairs (clock bit inserted before each data bit)
```
A raw track is ~12,668 bytes of MFM data (including gaps, sync words, and sector headers).
### Track Format (AmigaDOS)
```
Track = 11 sectors, each containing:
Sync: $4489 $4489 (2 words — MFM-encoded $A1 $A1)
Header: format, track, sector, sectors_to_gap (MFM-encoded)
Header checksum: XOR of header longs
Data: 512 bytes of payload (MFM-encoded = 1024 bytes on disk)
Data checksum: XOR of data longs
Gaps between sectors: variable-length padding
```
---
## Commands
## Using trackdisk.device
| Code | Constant | Description |
|---|---|---|
| 2 | `CMD_READ` | Read sectors |
| 3 | `CMD_WRITE` | Write sectors |
| 4 | `CMD_UPDATE` | Flush write buffer to disk |
| 9 | `TD_MOTOR` | Turn motor on/off |
| 10 | `TD_FORMAT` | Low-level format track |
| 11 | `TD_SEEK` | Move head to track |
| 12 | `TD_REMOVE` | Notify on disk change |
| 13 | `TD_CHANGENUM` | Get disk change count |
| 14 | `TD_CHANGESTATE` | Check if disk present |
| 15 | `TD_PROTSTATUS` | Check write-protect |
| 16 | `TD_RAWREAD` | Read raw MFM data |
| 17 | `TD_RAWWRITE` | Write raw MFM data |
| 18 | `TD_GETDRIVETYPE` | Get drive type |
| 19 | `TD_GETNUMTRACKS` | Get total tracks |
| 20 | `TD_ADDCHANGEINT` | Add disk change interrupt |
| 21 | `TD_REMCHANGEINT` | Remove disk change interrupt |
---
## Reading a Sector
### Opening
```c
UBYTE buf[512];
tdreq->iotd_Req.io_Command = CMD_READ;
tdreq->iotd_Req.io_Data = buf;
tdreq->iotd_Req.io_Length = 512;
tdreq->iotd_Req.io_Offset = 0; /* byte offset = sector * 512 */
DoIO((struct IORequest *)tdreq);
struct MsgPort *diskPort = CreateMsgPort();
struct IOExtTD *diskReq = (struct IOExtTD *)
CreateIORequest(diskPort, sizeof(struct IOExtTD));
/* Unit numbers: DF0:=0, DF1:=1, DF2:=2, DF3:=3 */
BYTE err = OpenDevice("trackdisk.device", 0,
(struct IORequest *)diskReq, 0);
```
### Reading Sectors
```c
UBYTE *buf = AllocMem(512, MEMF_CHIP); /* MUST be Chip RAM */
diskReq->iotd_Req.io_Command = CMD_READ;
diskReq->iotd_Req.io_Data = buf;
diskReq->iotd_Req.io_Length = 512; /* bytes to read */
diskReq->iotd_Req.io_Offset = 0; /* byte offset on disk */
/* offset = (track * 2 + side) * 11 * 512 + sector * 512 */
DoIO((struct IORequest *)diskReq);
```
### Writing + Updating (Motor Control)
```c
/* Write a sector: */
diskReq->iotd_Req.io_Command = CMD_WRITE;
diskReq->iotd_Req.io_Data = buf;
diskReq->iotd_Req.io_Length = 512;
diskReq->iotd_Req.io_Offset = 512 * 10; /* sector 10 */
DoIO((struct IORequest *)diskReq);
/* Flush write buffer to disk: */
diskReq->iotd_Req.io_Command = CMD_UPDATE;
DoIO((struct IORequest *)diskReq);
/* Turn off motor when done: */
diskReq->iotd_Req.io_Command = TD_MOTOR;
diskReq->iotd_Req.io_Length = 0; /* 0=off, 1=on */
DoIO((struct IORequest *)diskReq);
```
### Disk Change Notification
```c
/* Wait for disk insertion/removal: */
diskReq->iotd_Req.io_Command = TD_CHANGENUM;
DoIO((struct IORequest *)diskReq);
ULONG changeCount = diskReq->iotd_Req.io_Actual;
/* Async notification: */
diskReq->iotd_Req.io_Command = TD_ADDCHANGEINT;
diskReq->iotd_Req.io_Data = (APTR)&myInterrupt;
SendIO((struct IORequest *)diskReq);
/* myInterrupt is signalled on disk change */
```
### Track Caching
trackdisk.device reads an **entire track** (11 sectors) into an internal buffer on each access. Subsequent reads of other sectors on the same track are served from cache:
```
Read sector 0 → DMA reads track 0 (11 sectors) → cache hit for sectors 110
Read sector 11 → new track → DMA reads track 1
Read sector 5 → cache hit (still in track 0 buffer)
```
> **FPGA implication**: the MiSTer core must emulate this whole-track DMA behaviour for correct timing. Games that measure seek+read latency will behave incorrectly if only single sectors are transferred.
---
## Disk Geometry
## Direct Hardware Access (Games/Demos)
| Parameter | DD (880 KB) | HD (1760 KB) |
|---|---|---|
| Heads | 2 | 2 |
| Cylinders | 80 | 80 |
| Sectors/track | 11 | 22 |
| Bytes/sector | 512 | 512 |
| Total sectors | 1760 | 3520 |
Games often bypass trackdisk.device for speed and copy protection:
Byte offset = `(cylinder * 2 + head) * sectors_per_track * 512 + sector * 512`
```asm
; Direct floppy read — raw track DMA:
LEA $DFF000, A5 ; custom base
MOVE.L #TrackBuffer, $20(A5) ; DSKPT — DMA pointer (Chip RAM)
MOVE.W #$8210, $96(A5) ; DMACON — enable disk DMA
; Select drive, side, seek to track:
MOVE.B #$F7, $BFD100 ; CIA-B PRB — select DF0, motor on
; ... step head to desired track ...
; Start reading one track:
MOVE.W #$8000|6300, $24(A5) ; DSKLEN — enable, ~6300 words
MOVE.W #$8000|6300, $24(A5) ; write twice to start DMA
; Wait for DMA complete (DSKBLK interrupt):
BTST #1, $DFF01F ; INTREQR — DSKBLK bit
BEQ.S .-4
```
---
## References
- NDK39: `devices/trackdisk.h`
- NDK39: `devices/trackdisk.h`, `resources/disk.h`
- HRM: *Amiga Hardware Reference Manual* — Disk Controller chapter
- ADCD 2.1: trackdisk.device autodocs
- See also: [filesystem.md](../07_dos/filesystem.md) — FFS/OFS block format on top of trackdisk