Commodore 1541 GCR: Sector Framing, 4-to-5 Encoding, and Track Zones
Decode a 1541 sector from sync marks through GCR symbols, headers, checksums, variable track zones, and the limits of sector-only disk images.
The Commodore 1541 does not store a disk as a neat sequence of fixed-size sectors with a universal address mark. Its drive controller is a small computer, and its disk format is built from magnetic transitions, sync regions, encoded header and data blocks, gaps, and a track-dependent bit rate. The drive’s Group Code Recording (GCR) representation is part of the observable storage protocol, not merely a lower-level implementation detail.
This distinction explains why a file-system image is not always a complete preservation image. A D64-style sector container exposes decoded logical sectors and error metadata, which is ideal for ordinary DOS files. It cannot necessarily preserve exact bit-cell timing, arbitrary sync lengths, nonstandard headers, intentionally malformed sectors, or copy-protection structures. To reason correctly about compatibility, first identify whether a problem belongs to the DOS sector layer, the encoded GCR stream, or the flux and timing layer beneath it.
The disk rotates as a serial stream
When the drive reads a track, the read channel sees a timed stream of magnetic transitions. The 1541’s read/write circuitry detects sync and clock/data patterns, then shifts serial bits through a parallel/serial register connected to the drive controller. The drive’s 6502 and VIA hardware coordinate head movement, motor speed, write gates, stepper phases, and byte timing. The host C64 does not directly issue a hardware seek command to a passive disk; it talks to the drive’s own DOS and command processor over the serial bus.
The format is organized by tracks and sectors, but sectors are discovered in the continuous stream. A sync run marks the beginning of a candidate block. The block starts with a type byte and metadata, followed by encoded data and a checksum; a gap then separates it from the next block. The controller must find valid synchronization, decode GCR symbols, confirm the block marker and expected track/sector identity, and validate the checksum before delivering useful bytes.
Track zones solve a physical recording constraint. The disk’s circumference is larger on outer tracks, so the format can place more sectors there while maintaining a suitable transition density and rotational transfer rate. Standard 1541 DOS uses 21 sectors per track for tracks 1-17, 19 for tracks 18-24, 18 for tracks 25-30, and 17 for tracks 31-35. The DOS layout therefore contains 683 sectors over its conventional 35-track geometry. Some mechanisms physically step farther, but extended tracks are not part of the standard 35-track DOS capacity contract.
The drive selects a bit-rate zone based on track position. These zones do not mean every track has exactly the same number of bytes or that every byte arrives at a fixed interval globally. The read clock and gap placement participate in fitting the encoded sectors around one rotation. A sector-level emulator may abstract those details for routine file access, but a track-level emulator must preserve the appropriate zone and byte timing to reproduce rotational latency, speed-zone changes, and malformed or protected tracks.
Why 4 input bits become 5 recorded bits
The 1541’s standard GCR alphabet maps every four data bits to a five-bit code. The selected code words avoid long runs of zero bits, which helps the drive recover timing and maintain transition density. Two four-bit nibbles become one eight-bit byte before encoding; the encoded output is ten bits, or five GCR bits for each original nibble. Because groups cross byte boundaries, the resulting bitstream is not simply a list of independent encoded bytes.
The canonical nibble table is fixed. For example, hexadecimal 0 maps to 01010, 1 to 01011, 8 to 01001, and F to 10101. The full table is a format primitive: a decoder should reject an unassigned five-bit symbol rather than silently converting it to zero. A bitstream may be misaligned relative to the host byte boundary, so a track decoder first finds synchronization and then consumes exactly five-bit symbols in the block’s expected order.
A data block contains a marker byte followed by 256 logical bytes and a checksum, then padding bytes. The 256 bytes include the file-system sector’s two link bytes and 254 payload bytes; the drive’s disk block is not 256 bytes of application payload. The checksum is the XOR of the sector data bytes, and the block framing and checksum serve different purposes: sync locates a candidate, the marker identifies block type, the header and sector fields select the block, and the checksum detects certain corruptions.
Header blocks similarly contain a marker, checksum, sector number, track number, disk ID bytes, and fixed padding values. The checksum covers the track, sector, and ID values in the standard header. The header’s disk ID is associated with the formatted disk and can be compared with the corresponding ID in the data block. This redundancy helps DOS reject a block that is syntactically valid but belongs to another format pass or disk identity.
A reference GCR codec for unit tests
This compact Python example encodes and decodes the 16 legal 5-bit symbols for ordinary 1541 GCR data. It operates on an aligned byte sequence for clarity; a physical track parser still needs a bit-oriented reader, sync detection, block framing, and checksum checks. The test asserts round-trip correctness and deliberately rejects illegal symbols rather than guessing.
GCR = (
0b01010, 0b01011, 0b10010, 0b10011,
0b01110, 0b01111, 0b10110, 0b10111,
0b01001, 0b11001, 0b11010, 0b11011,
0b01101, 0b11101, 0b11110, 0b10101,
)
DECODE = {code: nibble for nibble, code in enumerate(GCR)}
def encode_gcr(data):
out = 0
bit_count = 0
result = bytearray()
for byte in data:
for nibble in (byte >> 4, byte & 0x0F):
out = (out << 5) | GCR[nibble]
bit_count += 5
while bit_count >= 8:
bit_count -= 8
result.append((out >> bit_count) & 0xFF)
if bit_count:
result.append((out << (8 - bit_count)) & 0xFF)
return bytes(result)
def decode_gcr(encoded, byte_count):
bits = int.from_bytes(encoded, "big")
total_bits = len(encoded) * 8
nibbles = []
for index in range(byte_count * 2):
shift = total_bits - (index + 1) * 5
if shift < 0:
raise ValueError("truncated GCR input")
code = (bits >> shift) & 0x1F
if code not in DECODE:
raise ValueError(f"illegal GCR symbol {code:05b}")
nibbles.append(DECODE[code])
return bytes((nibbles[i] << 4) | nibbles[i + 1]
for i in range(0, len(nibbles), 2))
sample = bytes((index * 37) & 0xFF for index in range(256))
encoded = encode_gcr(sample)
assert decode_gcr(encoded, len(sample)) == sample
The standard alphabet should be verified against the Commodore service material or an established drive implementation before being used as a preservation decoder. Note that every bit pattern not present in the 16-entry table is invalid for standard 1541 GCR. The code’s final partial byte is a test convenience, not a claim about the exact gap or rotational packing of a real sector.
DOS sector layout versus raw track structure
The 1541 DOS file system adds another layer on top of sector framing. A directory entry points to the first track/sector block; each data block begins with a link to the next block. The final block uses a special track value and a byte count in the second link byte to indicate how many bytes of the final sector are meaningful. The Block Availability Map (BAM) tracks free sectors, and the directory and BAM occupy reserved locations on track 18 in the standard format.
That directory graph is not the disk encoding. A valid BAM does not prove that all data sectors are valid GCR, and a valid sector checksum does not prove that the directory chain is complete. When a disk image is damaged, validate in layers: container geometry, sector presence and error codes, block headers and checksums, link chains, BAM counts, and finally file contents. Report which layer failed instead of calling every failure “bad GCR.”
Sector containers are useful because they make ordinary DOS workflows portable. A raw track image such as G64 retains more of the GCR stream and track lengths, while flux-oriented formats preserve transition timing and can retain evidence not representable in a decoded sector container. A conversion from flux to GCR may still discard analog timing; conversion from GCR to sectors necessarily normalizes or removes structures outside the modeled sector grammar. Preserve the original capture and hash each derived artifact.
Copy protection often exploits properties beyond ordinary sector payloads: unusual sync lengths, missing or extra headers, intentionally bad checksums, nonstandard sector ordering, weak bits, variable track density, or timing-sensitive read patterns. A game that fails from a D64 but works from a raw track image may be relying on any of these differences. Do not “repair” the image by regenerating headers or checksums before recording the evidence; that can destroy the feature under investigation.
Emulator design and forensic validation
A sector-oriented drive model can provide a useful compatibility layer, but it should distinguish an ordinary logical read from a raw track read. Track state includes rotation position, current track, zone, and selected side where applicable. A bit-cell model also needs a transition stream, sync detector state, byte shifter, and write-gate behavior. Each layer should expose enough diagnostics to answer whether the data was absent, out of range, badly encoded, framed incorrectly, checksum-invalid, or simply not yet under the head.
Build tests from vectors with known inputs. Verify each GCR nibble, pack groups across byte boundaries, reject illegal symbols, detect a deliberately shifted alignment, validate header and data checksums, and check the sector-zone table. Then test rotational position: two reads of a sector from different angular positions should have different wait times in a track-accurate model even though they return identical payload bytes. Add write tests for the exact bitstream length and gap preservation.
For archival work, retain source, extraction method, drive and alignment notes, error map, raw image, decoded image, and cryptographic hashes. A container’s “good” status should mean that its own structural checks passed, not that the source disk has been proven authentic or complete. Compare independent reads when physical media condition is uncertain; repeated identical sector bytes do not prove that weak-bit or timing information was absent.
Acceptance criteria
A dependable 1541 implementation separates DOS structures from sector blocks, GCR symbols, and rotational track timing. It knows which information each image format preserves, validates checksums and headers independently, and never silently converts an illegal symbol to valid data. When a title depends on a nonstandard track, the emulator can explain which raw property is missing rather than offering an unexplained compatibility patch.
Related:
- CHD Explained: Lossless Compression, Parent Images, Metadata, and Verification
- Preservation-Grade Game Images: Dumps, Hashes, DATs, and Disc Formats Explained
Sources: