CompactFlash interface (CF card v1.0 circuit) — theory of operation¶
The YACC1's mass storage: a CompactFlash card, run in 8-bit True IDE mode, reached through two I/O ports. It is what
lets Y1/OS leave the emulators: the ROM monitor's O command boots the OS from the card, and the OS's file layer reads
and writes its sectors.
Written 2026-09-23 from the YACC1-D tree, while the card was being designed (not yet built); section 0 updated 2026-09-24, when the interface was planned onto the memory card.
Sources: hardware/cards/cf/kicad/v1.0/cf_netlist.py (the circuit: the single source the schematic and the board are
generated from), docs/system/OS-PLAN.md (the decision for two ports in I/O space), docs/system/BUS.md (the bus
pins), firmware/microcode/ucode-generator2/io.c (the I/O strobe timing), firmware/monitor/monitor.asm (the CF
driver: cfwait, cfinit, cfread, cfwrite), software/cfmodel.h (the emulators' model of the card),
~/Developer/p8x/generators/gen_eagle.py (the P8X CF card, the source of the proven IDE pinout),
docs/datasheets/ (74LS parts). The ATA and CompactFlash timing figures quoted below are the standard PIO mode 0 values;
To verify: against the datasheet of the card actually used (none is in the tree).
0. Where the circuit goes (2026-09-24)¶
- The CF card v1.0 (
hardware/cards/cf/kicad/v1.0/) is the interface as a card of its own: five chips (U1 74LS138, U2 74LS32, U3 74LS175, U4 74LS08, U5 74LS245) and a 40-pin IDE header, decoding P8/P9. Routed and checked, never ordered. It is the reference circuit this document describes, with its reference designators. - Plan (2026-09-24): the same circuit goes onto the memory card and stays I/O-mapped on P8/P9, not
memory-mapped:
hardware/cards/memory/kicad/v2.0/(schematic proven). On the card as actually built (hardware/cards/memory/eagle/v1.3/, the Fusion 360 export; not the earlier save the plan was first drawn on, noweagle/deprecated/v1.3-do-not-use/) the TMP registers' data tracks cross the free area, so with that copper kept the CF chips did not fit, so the whole memory card is laid out again with the CF section designed in. A first finished board (J2 at the top edge, a TAODAN adapter standing on it) was superseded the same day and is kept as a record. The v2.0 board (chosen 2026-09-25, standoff option E, finished for fabrication, NOT ORDERED):memory-v2.0.kicad_pcbwith its gerbers, NPTH/PTH drill, renders, placement PDF, BOM, JLCPCB order note and a 1:1 print. The CF chips (the CF card's U1-U5 are IC30-IC34 there) sit under the adapter; the adapter, an HX-2118P CF-to-IDE (60 x 44 mm, male 40-pin header, no pin 20), mounts flat on two 15 mm M3 standoffs (holes H1 / H2, 3.2 mm NPTH) above them, its CF slot at the card's free top edge; J2 (the card's own 2x20 IDE header, parallel to the bus connector) feeds it through a short straight 40-wire ribbon, pin 1 to pin 1 (pin 20 open on the J2 plug, or J2's pin 20 pulled). The adapter takes +5 V through its four power pads by a short cable from J3 (+5V, G, G, nc); JP2 (IDE pin 20 = +5 V) stays open for the HX-2118P. Open before ordering (that folder's README): the ribbon plug's pin 20 (the adapter's power pads: a floppy pinout in J3's order, straight-through cable, resolved 2026-09-26). The memory card today uses no I/O signals: IO-ADDR0..3, -IO-RD and -IO-WR are on its connector but unwired (docs/cards/memory.md). The ROM in the machine (ROM 2026-09-23) and both emulators already use P8/P9, so nothing on the software side changes. - The CF-to-IDE adapter: the HX-2118P (2026-09-24/25, measured and checked with a 1:1 print), on standoffs as above. Considered before it: a SinLoon CF-IDE adapter on a short ribbon, and the TAODAN CF-IDE40 V2.0 (70 x 63 mm, a female 40-pin socket that plugs straight onto a male IDE header, standing perpendicular to the card and needing ~75 mm free on the component side) - the latter was the adapter of the superseded top-edge board.
1. Purpose and place in the machine¶
A CompactFlash card can pretend to be an IDE hard disk ("True IDE mode"). In that mode it has eight registers, the
ATA task file, and talks 8 bits at a time once told to (SET FEATURES $01, which the ROM's cfinit sends). So the card
the YACC1 needs is small: something to choose which of the eight registers the next access means, a buffer to put
the data bus onto the CF's data lines in the right direction, and read and write strobes that happen only when the
CPU is talking to this card.
The card sits in I/O space, not memory space, on two ports (docs/system/OS-PLAN.md, decision 1):
| Port | Direction | What |
|---|---|---|
| P8 | write | a 4-bit latch: bits 0..2 = the ATA register number (0 data, 1 error/features, 2 sector count, 3..5 LBA 0..2, 6 drive/head, 7 status/command), bit 3 = CF reset (1 = held in reset), bits 4..7 ignored |
| P9 | read/write | the register P8 selected |
A sector read is therefore: select the LBA registers one by one on P8 and write their bytes on P9, write the READ
SECTORS command, poll status, then select register 0 once and read P9 512 times. The ROM driver does exactly that;
selecting the data register once and streaming 512 bytes costs one OUTI for the whole sector.
bus CF card v1.0 40-pin IDE header
IOADDR0..3 ─────►┌──────────┐ -P8SEL ┌────────┐ LATCHCLK ┌──────────┐ DA0..2 ─────────────► J1 35/33/36
│ U1 138 ├───────►│ U2 32 ├─────────►│ U3 175 │ -SRST ─┐
│ Y0=P8 │ -P9SEL │ (OR: │ │ latch │ │ ┌────────┐
│ Y1=P9 ├───────►│ strobe │ -IOR ────┼──────────┼────────┼─►│ U4 08 │ -CFRESET ──► J1 1
└──────────┘ │ gating)│ -IOW ─┐ └──────────┘ └─►│ (AND) │ -CFOE ─┐
-IO-WR ────────────────────────────► │ │ │ ▲ DATA0..3 │ │ ACTK ──┼─► LED2 ACT
-IO-RD ────────────────────────────► └────────┘ │ │ -RESET ─────────► └────────┘ │
│ │ │
DATA0..7 ◄──────────────────────────────────────────► │ ┌─┴──────────┐ CFD0..7 (pull-ups RN1) ─────┼─► J1 17..3 (odd)
└►│ U5 245 │◄───────────────────────────┘ -OE
DIR=-IOR ─►│ A=bus B=CF │
└────────────┘ J1 23 -IOW, 25 -IOR, 37 -CS0=GND, 38 -CS1=VCC
2. Bus signals¶
| Signal | Pin | Direction | Use on this card |
|---|---|---|---|
DATA0..7 |
A19–A26 | both | through U5 to and from the CF; DATA0..3 also into the P8 latch |
IO-ADDR0..3 |
C7–C10 | in | the port number, decoded by U1 |
-IO-RD |
B25 | in | gated with the port-9 select into the CF's -IOR |
-IO-WR |
B26 | in | gated with the port-8 select into the latch clock, and with port 9 into -IOW |
-RESET |
C30 | in | clears the latch (register 0, reset bit off) and resets the CF |
| VCC, GND | A/B/C 2, 31 and 1, 32 | power |
The pins were cross-checked on 2026-09-23 against the connector of the I/O card v1.1 schematic, a card that works in
the machine. The card reads nothing else from the bus: no memory address lines, no -VMA, no -BUS-EN. It drives
the data bus only while the CPU reads port 9.
3. Circuit, part by part¶
U1, 74LS138: the port decoder. Its select inputs take IO-ADDR0..2 and its active-high enable G1 takes IO-ADDR3, the
two active-low enables are tied low. So Y0 goes low for port 8 and Y1 for port 9, and nothing responds in ports 0..7,
the I/O card's half. Y2..Y7 (ports 10..15) are unused; docs/system/OS-PLAN.md reserves them for the next video card
and the PS/2 interface, which will decode their own ports. The select outputs carry no timing: they follow the port
number, which the microcode sets two or three steps before the strobe (section 4).
U2, 74LS32: strobe gating. Three OR gates, each an active-low AND: the output is low only when both inputs are low.
- -P8SEL OR -IO-WR = LATCHCLK: low while the CPU writes port 8, rising when the write strobe ends.
- -P9SEL OR -IO-RD = -IOR, the CF's read strobe.
- -P9SEL OR -IO-WR = -IOW, the CF's write strobe.
The fourth gate is unused, its inputs tied to ground.
U3, 74LS175: the register-select latch. Four D flip-flops clocked on the rising edge of LATCHCLK, that is at the
end of an OUT to port 8, taking DATA0..3. Q1..Q3 drive the CF's register address DA0..2 directly, so the register
number is stable long before and long after every access to port 9. Q4 is the reset bit; its inverted output -SRST
goes to U4. The bus -RESET clears all four, so after a front-panel reset the card points at register 0 and is out of
reset.
U4, 74LS08: enables and reset. Three AND gates, each an active-low OR:
- -IOR AND -IOW = -CFOE: low during any access to port 9. It enables the buffer U5.
- -RESET AND -SRST = -CFRESET: the CF is reset while the bus reset is active or while software holds bit 3 of
P8 at 1.
- -CFOE AND -CFOE = ACTK: a buffered copy that sinks the ACT LED, so the LED current does not load the enable.
U5, 74LS245: the data buffer. Its A side is the bus DATA0..7, its B side the CF's D0..7. -OE is -CFOE, so it
connects the two only during a port-9 access. DIR is -IOR: high (A to B, bus to CF) except during a read, when it
is low (B to A, CF to bus). So the card drives the data bus only while the CPU reads port 9, and never fights another
card.
J1: the 40-pin IDE header, for a commercial CF-to-IDE adapter, as on the P8X. The pin assignment is the P8X card's,
which works: data D0..7 on pins 17, 15, 13, 11, 9, 7, 5 and 3; DA0 35, DA1 33, DA2 36; -IOW 23, -IOR 25; reset
pin 1; grounds on 2, 19, 22, 24, 26, 30 and 40. D8..15 on the even pins 4..18 are left open: the card runs 8 bits wide.
The other pins:
- -CS0 (37) is tied low and -CS1 (38) high: see section 4 for why chip select is not gated.
- CSEL (28) grounded: the CF is the master drive.
- IORDY (27), -PDIAG (34) and -DASP (39) have 10k pull-ups; -DASP also lights the DASP LED, the CF's own
activity signal.
- -DMACK (29) is held high through 10k: no DMA.
- DMARQ (21), INTRQ (31) and -IOCS16 (32) are unused: the driver polls, and the card never asks for 16-bit
transfers once in 8-bit mode.
- Pin 20 is the IDE key position. Some CF-to-IDE adapters take +5 V there; jumper JP1 connects it to VCC for those.
Leave JP1 open otherwise: on a real IDE cable pin 20 is blocked or unconnected.
RN1, a 10k SIP network, pulls CF D0..7 up. Without a card in the adapter those lines would float; with the pull-ups
a status read returns $FF, which has the BSY bit set, so the ROM's bounded cfwait times out and reports CF ERROR
instead of acting on noise.
J2: adapter power. A 4-pin header in the floppy-power order: pin 1 +5 V, pins 2 and 3 ground, pin 4 (+12 V on a floppy lead) unconnected. Most CF-to-IDE adapters take their power through a floppy connector. C6, 10 µF, sits beside it: a CF card can draw about 100 mA in bursts. To verify: the power connector and pinout of the adapter chosen (section 0; the TAODAN CF-IDE40 can take +5 V on IDE pin 20 through JP1 instead).
LEDs. PWR (green) on VCC; ACT (yellow) during every port-9 access, so a sector transfer shows as a flicker; DASP (red), driven by the CF itself while it is busy. Each has a 1k resistor. Every IC has its own 100 nF capacitor.
4. Timing¶
The microcode sets the port number on IO-ADDR0..3 two or three steps before the strobe and holds it to the end of the
instruction (firmware/microcode/ucode-generator2/io.c: setIo() is written into every following step;
docs/isa/INP.json, OUTA.json, OUTI.json for the step numbers):
| Instruction | IO-ADDR valid from | strobe low | strobe released | IO-ADDR released |
|---|---|---|---|---|
OUTI Pn,byte |
step 5 | -IO-WR step 8 |
step 9 | end of instruction |
OUTA Pn |
step 6 | -IO-WR step 9 |
step 10 | end of instruction |
INP Pn |
step 6 | -IO-RD step 8 |
end of instruction | end of instruction |
A step is two clocks, so at 1 MHz every margin here is at least 2 µs. Against the CF's PIO mode 0 figures (address set-up before a strobe about 70 ns, strobe width about 165 ns, data set-up for a write about 60 ns, holds 20–30 ns) there is nothing tight:
- The register number comes from the latch, written by an earlier instruction, so it is valid for thousands of nanoseconds before and after every port-9 strobe.
- Port-9 strobes start at least two steps after the port select settles, so
-IOR/-IOWbegin with a single clean edge. A write ends one step before the port number changes, so the CF samples its data on a steady bus. For anOUTAthe accumulator stays on the bus to the end of the instruction; for anOUTIthe memory keeps driving the operand byte until after the strobe. - The latch clock rises at the end of the port-8 write strobe while the port select is still steady, and U3 samples DATA0..3 at that edge, while the bus still holds the byte.
-
The one simultaneous edge is at the end of an
INP: the port number and-IO-RDare both released by the same clock.-IORis an OR of the two, so it simply goes high once, at whichever arrives first, with no glitch. The CPU has already taken the byte into the accumulator a step earlier (-AC-LD, step 9). This is why chip select is not gated by the decoder: if-CS0were the port-9 select, it would rise straight from the decoder while-IORrises one OR-gate delay later, so chip select would end before the read strobe. That breaks the CF's chip-select hold time after-IOR, and a data-register read is exactly the access where the CF advances its sector-buffer pointer on the end of-IOR. With-CS0tied low permanently, the strobes alone define each cycle, which True IDE allows. -
The buffer's direction at the end of a read.
DIRis-IORitself, while the enable-CFOEcomes one AND gate later. So when-IORrises, the 245 turns back towards the CF (A to B) about one gate delay, roughly 10 ns, before it switches off, and for that moment it can drive the CF's data lines while the CF is still releasing them. The CPU took its byte a step earlier, so no data is at risk; the effect is a brief current spike through two drivers. The alternatives are worse: taking direction from the write strobe instead moves the same race to the start of every write, where the bus byte is being driven, and the bus carries no direction signal earlier than the strobes (reads and writes use the same port). The P8X CF card has the same arrangement. If it ever shows on a scope as ringing on the CF data lines, a revision could delay the rising edge ofDIRwith an RC or spare gates.
5. Programming model¶
What the ROM driver does (firmware/monitor/monitor.asm; the same sequence is in software/cfmodel.h):
cfwait: P8 <- 7, then INP P9 until status BSY (bit 7) is clear (bounded: an absent card reads $FF and times out)
cfinit: cfwait
P8 <- 6, P9 <- $E0 drive/head: LBA mode, master, LBA bits 24..27 = 0
P8 <- 1, P9 <- $01 features: 8-bit transfers
P8 <- 7, P9 <- $EF SET FEATURES
cfwait; ACC = the ERR bit (0 = ok)
cfsetl: P8 <- 3, 4, 5 with P9 <- CFLBA0, CFLBA1, CFLBA2 (the ROM variables at $0F10..$0F12)
P8 <- 6, P9 <- $E0; P8 <- 2, P9 <- 1 (one sector)
cfread: cfwait, cfsetl, P8 <- 7, P9 <- $20 READ SECTORS, wait for DRQ (bit 3)
P8 <- 0, then 512 x INP P9 the sector, one byte per read, into the buffer at R7
cfwrite: the same with $30 WRITE SECTORS and 512 x OUTA P9
Status bits: 7 BSY, 6 DRDY, 3 DRQ, 0 ERR. The emulators model the card with these commands ($EF, $EC IDENTIFY, $20, $30)
behind a disk image (-c disk.img); an absent card reads $FF, as the real card's pull-ups make it.
Bit 3 of P8 holds the CF in reset while 1. The ROM never sets it (it writes register numbers 0..7), and the
emulators ignore it. It is there so a later driver can recover a card that stops answering without a front-panel reset:
write 8 to P8, wait at least 25 µs, write 0, then run cfinit again. To verify: the reset pulse and recovery time
of the card used; 25 µs is the ATA minimum, CF cards may need up to about 2 ms before BSY clears.
docs/system/OS-PLAN.md first pencilled bit 3 as the selector for the ATA control block (the alternate status and
device control registers on -CS1). This design ties -CS1 off instead: the driver polls the ordinary status
register, which gives the same bits, and the device-control soft reset is covered more simply by the hardware reset
bit.
6. Jumpers, connectors, LEDs¶
| Item | Setting | Purpose |
|---|---|---|
| JP1 | open (default) | fit only for a CF-to-IDE adapter powered on IDE pin 20 |
| J1 | 40-pin IDE header, pin 1 marked | the CF-to-IDE adapter, directly or on a short ribbon |
| J2 | +5 V, GND, GND, n/c | the adapter's power lead |
| LED1 PWR, LED2 ACT, LED3 DASP | power; any port-9 access; the CF busy |
The interface needs the I/O card strapped to ports 0..7 (its IO-ADDR-HL header), which is how it has always been
used; strapped to P8-P15 the I/O card would answer on P8/P9 too. To verify on the bench before plugging the CF
card in (docs/cards/io.md). On the memory card the designators will be that design's own.
7. Bring-up and test¶
The ROM in the machine (ROM 2026-09-23, burned 2026-09-23) already drives P8/P9; no new burn is needed.
- Without the CF adapter: power up, reset, and in the monitor type
O. With nothing in J1 the pull-ups make status read $FF, socfinitshould time out and printCF ERRORafter about a second. That proves the decode, the strobes and the buffer direction for reads, without any risk to a card. - With the bus tester (port 8 on IO-ADDR0..3, a byte on the data bus, a
-IO-WRpulse) or a two-instruction program entered with the monitor'sEcommand (OUTI P8,n/RET, run withG), write to P8 and watch DA0..2 on J1 pins 35, 33, 36 with a meter or LEDs: each value 0..7 must appear, and bit 3 must pull J1 pin 1 low. - Fit the adapter with a card prepared on the Mac:
make -C os, thenpython3 tools/cfcard.py write os/disk.img --disk diskN(docs/procedures/CF-CARD.md). ThenOshould boot Y1/OS. tests/os/*.sessionthen run on the machine by hand:dir,cat README.TXT,hello.
If it misbehaves: ACT not flickering during O means no port-9 strobes (U1/U2, the I/O card's strap); status always
$FF with a card fitted means the buffer is not enabled or points the wrong way (U4 gate 1, U5 DIR); reads right but
writes lost means -IOW or the data set-up (U2 gate 3); a card that works once and hangs after a reset suggests the
reset path (U3 Q4, U4 gate 2).
8. History and next revision¶
- 2026-09-22: the two-port design chosen over an eight-port memory-mapped one like the P8X's
(
docs/system/OS-PLAN.md); the emulator model and the ROM driver written against it, on P8/P9. - 2026-09-23: the circuit written as
hardware/cards/cf/kicad/v1.0/cf_netlist.py; the KiCad schematic and board are generated from it (hardware/cards/cf/README.md). Never fabricated. - 2026-09-23 evening to 2026-09-24: the interface was moved onto an I/O card v2.0 on ports P4/P5 (commit 24378cb, ROM
2026-09-23B), then that plan was dropped (6eeb259, 65851b0): back on P8/P9, now planned on the memory card (section 0). - 2026-09-24: the memory card v2.0 re-laid out with the CF section; a top-edge board (TAODAN adapter on J2) finished, then superseded by the standoff decision (HX-2118P on two M3 standoffs on the card, a short ribbon from J2); five standoff placements trial-routed.
- 2026-09-25: standoff option E chosen;
hardware/cards/memory/kicad/v2.0/memory-v2.0.kicad_pcbfinished for fabrication (routed, silkscreen, fab files), not ordered. The card cage slot in front of the memory card stays empty (decided 2026-09-25), so the ~35-44 mm stack of adapter, plug and ribbon needs no other clearance.
For a next revision: a second drive (the P8X card has two headers), INTRQ to the bus -INT if the OS ever wants
interrupt-driven transfers, and a CF socket on the card itself instead of the adapter (surface-mount on most
footprints, which the YACC1 has avoided so far).