Skip to content
FreeDOSDeep Dive Published Updated 7 min readViews unavailable

ATA PIO on DOS: Poll Status, Transfer Data, and Bound Every Wait

Understand legacy ATA PIO command phases, status-bit ordering, sector counts, error handling, and why DOS drivers should prefer documented BIOS paths.

Programming an ATA disk through legacy I/O ports is fundamentally different from asking DOS or BIOS to read a sector. The controller exposes task-file registers, command status, data transfer, and device-selection behavior; software must order those operations correctly and handle timeouts and errors. A DOS application should normally use file handles, a block driver, or BIOS INT 13h. Direct ATA PIO belongs in a device driver, diagnostic tool, or tightly controlled bare-metal environment that owns the interface.

The core safety idea is to model a request as a state machine rather than as a sequence of unbounded register reads. Select a device, wait until it is not busy, program the task-file inputs, issue a command, poll or receive an interrupt for each required phase, transfer exactly the advertised amount, and inspect terminal status. At every wait, impose a deadline. A status bit that never changes must become a clear timeout, not a frozen machine.

Define ownership before touching ports

Traditional parallel ATA controllers use command-block and control-block registers, commonly exposed at legacy base addresses such as 1F0h and 3F6h for the primary channel. These values are conventional, not proof that a channel is present or available. PCI IDE controllers, compatibility modes, virtualization, and chipset routing can change ownership. A DOS program that writes directly to a guessed port can collide with a disk driver that is already active.

Before a driver claims a channel, it must discover the controller using the platform interface it supports, determine the task-file mapping and interrupt route, and coordinate ownership with existing drivers. Do not run a direct-I/O experiment against a live filesystem or while a DOS disk cache, BIOS extension, or block driver may access the same device. For diagnostics, prefer a disposable image attached to an emulator with an explicit controller model.

The ATA status register’s BSY bit takes precedence: while it is set, software must not interpret other status bits as a completed command state. Once BSY clears, DRQ signals a data phase when the command requires one, and ERR indicates an error condition whose details are read from the error register. DRDY, DF, and other status bits need interpretation in the selected command and device context. A single “ready” bit is not a substitute for the command-specific completion rules.

Treat task-file programming as a transaction

An LBA or CHS request is encoded into task-file registers according to the command set and address mode supported by the drive. The driver must validate the requested range before writing the registers, select the correct device, wait for the interface’s required idle state, and program the sector count and address fields before issuing the command byte. For extended commands, the register write order and high-order fields differ from legacy 28-bit commands. Do not mix the two formats or assume that a drive supports a command merely because it responds to a reset.

Read Identify Device data and validate the advertised capabilities before selecting a command. The identify block is a packed little-endian structure defined by the ATA command-set specification; strings have word-byte ordering quirks, and fields can be version-dependent. Keep parsed capacity in a wide integer, validate sector size where supported, and avoid hard-coded CHS geometry as a modern capacity source.

deadline = monotonic_now() + command_timeout
select_device_and_wait_for_selection_settle()
wait_until(BSY == 0, deadline)                 else timeout
write_task_file_inputs(command, lba, count)
issue_command(command)
for each expected data phase:
    wait_until(BSY == 0 and (DRQ or ERR), deadline) else timeout
    if ERR: capture status + error register; stop
    transfer exactly one command-defined block
wait_until(BSY == 0, deadline)                 else timeout
check final status and record completion

This is conceptual pseudocode, not a controller driver. A production implementation must follow the exact ATA command and transport rules, include channel locking, account for interrupt versus polling operation, and implement reset/recovery policy. Never issue the same write again automatically after an ambiguous timeout: the device may have completed it even if the host did not observe the completion.

Transfer lengths and partial completion

On the classic parallel ATA interface, the task-file data register is 16 bits wide and PIO transfers move words. The command still defines the logical payload length; do not infer buffer size from bus cycles, and honor any transport-specific padding rules when the path is not plain ATA disk PIO. A sector count is not an arbitrary memory byte length. Compute the transfer size with overflow checks, validate that the caller’s buffer is at least that large, and keep a record of how many blocks were accepted before failure. Multi-sector commands can fail after some data has moved; a final error does not mean the target media was unchanged.

For writes, a timeout after the data phase is especially ambiguous. The drive may have committed the sector while the interrupt, status poll, or host-side bookkeeping failed. Retrying a write to the same LBA may be safe only for an idempotent, fully specified block update and only after recovery confirms the device state. A filesystem metadata update may not be idempotent. Do not use raw PIO to “repair” a disk without an image or verified backup.

Use the command’s exact completion contract. Some operations have no data phase; others transfer one or more sectors and report status after the final block. Do not infer “done” just because DRQ cleared once. A driver should retain the original command, LBA range, sector count, deadline, and final status/error values so a support report can distinguish device error, bad address, timeout, and programming error.

Timeouts, interrupts, and recovery

Unbounded polling is a classic hang. Use a clock source whose progress remains reliable for the environment, and cap every wait for selection, BSY, DRQ, and reset completion. Do not derive a CPU loop count from a processor frequency and call it a timeout. Emulation pacing and real hardware differ.

An interrupt-driven driver must acknowledge and serialize channel events according to the controller and platform contract; reading a status register can have side effects on interrupt state for some interfaces. A polling driver must not race an interrupt handler that owns the same channel. Choose one operating model for the request, protect shared task-file state, and record the transition sequence.

Recovery should be staged and bounded: capture status/error first, stop issuing new commands to the channel, perform the specified device or channel reset only when required, wait for the documented reset-completion condition, re-identify or revalidate the device, and then decide whether retry is safe. Do not power-cycle or reset both master and slave automatically when a single selected device errors; the channel may contain another active device.

Prefer the right layer

BIOS INT 13h is a compatibility abstraction for boot and legacy disk access; DOS block devices and drivers provide the filesystem’s active block interface. A process that bypasses them can desynchronize caches and corrupt mounted filesystems. The direct ATA protocol is appropriate when the code is the storage driver or when a test environment explicitly gives it exclusive control.

If the goal is simply to copy data, use a DOS file API or a verified image tool. If the goal is to inspect a disk, capture a read-only image and analyze the image. If the goal is a driver, define the controller discovery and ownership contract, supported ATA revisions, addressing mode, buffer constraints, timeout source, interrupt route, and recovery semantics before implementing writes.

Acceptance checks

Test absent device, not-ready device, successful identify, successful read, unsupported command, out-of-range LBA, injected ERR, and a timeout at each wait phase. Use an emulator or sacrificial image and compare read results against a known checksum. Confirm no command loops forever, no error path transfers more bytes than the buffer holds, and a failed write is reported as potentially ambiguous rather than silently retried.

ATA PIO is manageable when each register phase has an explicit precondition, deadline, transfer count, and failure result. It is dangerous when treated as a handful of magic port writes. Keep controller ownership exclusive, use the published command-set contract, and place the protocol below the DOS filesystem rather than bypassing it from an ordinary application.

Related:

Sources:

Comments