The ULA
One Ferranti gate array — display, keyboard, sound, I/O, and the Spectrum's whole personality
The Uncommitted Logic Array (ULA) is the custom Ferranti chip that combined video generation, memory contention, keyboard scanning, tape and beeper I/O, and the border into a single integrated circuit. It is the reason the ZX Spectrum cost £125 instead of £250, and the reason the platform has the visual and acoustic identity it does.
Overview
The Uncommitted Logic Array — the ULA — is the custom Ferranti gate array that sits next to the Z80 in every Sinclair Spectrum. It does almost everything that isn't CPU and RAM: video signal generation, memory-bus arbitration, keyboard matrix scanning, tape and beeper I/O, border colour, and (on later models) memory bank switching. The Spectrum's hardware bill of materials is, essentially, "Z80 + 16 KB or 48 KB of RAM + ROM + ULA + RF modulator." That extreme integration is what made the £125 launch price possible in April 1982.
The ULA is also the source of nearly everything that gives the Spectrum its distinctive feel. Attribute clash, contended memory timing, the one-bit beeper, the way the border can be striped during loading and demos — all of it is the ULA. Programming the Spectrum at the hardware level is programming the ULA.
Fast facts
- Manufacturer: Ferranti, designed in collaboration with Sinclair Research; Amstrad-redesigned variant on the +2A/+3.
- Process: Ferranti 5C series uncommitted logic array (mask-programmable gate array).
- Function: display, memory arbitration, keyboard, tape, beeper, border.
- Video: PAL composite via RF modulator; 256 × 192 active display in a 32 × 24 attribute grid.
- Colour space: 8 hues × 2 brightnesses = 15 unique colours (BRIGHT 0 black ≡ BRIGHT 1 black).
- Timing: 3.5 MHz CPU clock; 69,888 T-states per frame; 312 scanlines; ~50.08 Hz refresh.
Chip variants
The Spectrum line shipped at least three different ULA designs:
| Machine | Part number | Crystal | Memory contention | Floating bus |
|---|---|---|---|---|
| 16K / 48K | Ferranti 6C001E (also 5C112E, 5C102E early variants) | 14 MHz | MREQ + IORQ | Yes |
| 128K / +2 (Sinclair-era) | Sinclair-branded 7K010E | 17.7345 MHz (PAL master) | MREQ + IORQ | Yes |
| +2A / +3 (Amstrad-redesign) | Amstrad 40077 | 17.7345 MHz | MREQ only | No |
The +2A/+3 was a clean redesign by Amstrad after they acquired Sinclair's computer business in 1986. Software relying on the floating-bus read (port $FF) for video-data sniffing breaks on those models — a small but real source of game incompatibility across the Spectrum range.
Display generation
The ULA fetches pixel and attribute data directly from main memory and converts it to a PAL composite video signal. Pixel and attribute regions are fixed:
| Memory region | Size | Purpose |
|---|---|---|
| $4000-$57FF | 6,144 bytes | Pixel bitmap (256 × 192 at 1 bit/pixel) |
| $5800-$5AFF | 768 bytes | Attribute data (32 × 24 cells, one byte each) |
The screen address quirk
The pixel data is not linearly arranged. It's interleaved by character row within each third of the display:
addr = $4000
| ((y & 0xC0) << 5) ; which third (top / middle / bottom)
| ((y & 0x07) << 8) ; pixel row within character
| ((y & 0x38) << 2) ; character row within third
| (x >> 3) ; column
This non-linearity exists because it matches the ULA's scanline fetch order — the chip reads the bitmap in display order, and the address bits decompose into a shape that minimises addressing logic. It's awkward for game code (no straight address + y * 32 arithmetic) but is the foundation of the platform's distinctive bitmap_rows lookup tables. Shadowkeep Unit 3 uses one.
Attribute format
Each 8 × 8 pixel cell has one attribute byte at $5800 + row*32 + col:
| Bit | Purpose |
|---|---|
| 7 | FLASH — swaps ink and paper every 16 frames |
| 6 | BRIGHT — affects both ink and paper |
| 5-3 | PAPER colour (0-7) |
| 2-0 | INK colour (0-7) |
This single-byte-per-cell scheme is the origin of the Spectrum's colour clash: a 1 in the bitmap shows in INK, a 0 shows in PAPER, and there are only two colours per 8 × 8 cell. Sprites crossing cell boundaries change colour at those boundaries. The Project's whole Spectrum aesthetic strategy is downstream of this byte.
Memory contention
The ULA and the Z80 share the same RAM bus. When the ULA is fetching pixel or attribute data — during active display lines — the Z80 has to wait. This is contention:
- During active display (top border end through bottom border start), the ULA owns the bus for specific T-states per scanline.
- The Z80 is delayed when accessing contended memory ($4000-$7FFF on 48K) during those T-states.
- The contention pattern repeats predictably every 8 T-states per scanline:
T-state within group: 0 1 2 3 4 5 6 7
CPU delay (T-states): 6 5 4 3 2 1 0 0
This means CPU access to screen memory during display is slower than access to upper RAM ($8000-$FFFF on 48K). Performance-critical code typically lives above $8000.
128K / +2 use the same shape with a small starting offset. The Amstrad +2A/+3 contends MREQ only — no IORQ contention — which changes the exact T-state cost of IN/OUT instructions on those machines.
Demo-scene coders exploit contention deliberately, building effects that depend on the exact T-state cost of each scanline. For game code it's usually a thing to avoid by structuring data layout, not exploit.
I/O ports
The ULA decodes I/O addresses incompletely — the low byte determines the port. Two addresses matter:
| Port | Read | Write |
|---|---|---|
| $FE (any odd port with A0=0) | Keyboard rows (bits 0-4) + EAR input (bit 6) | Border (bits 0-2) + MIC (bit 3) + EAR/beeper (bit 4) |
| $FF (any port with A0=1, where contention is active) | Floating bus — last byte the ULA fetched (48K / 128K / +2 only) | — |
Port $FE — write
| Bit | Function |
|---|---|
| 0-2 | Border colour (0-7) |
| 3 | MIC out (tape signal) |
| 4 | EAR out (beeper / speaker) |
| 5-7 | Unused (reads back as 1 on the floating bus) |
The single-bit speaker — bit 4 — is the entire Spectrum sound system on the 48K. Beeper music is the art of generating perceived polyphony from this one bit.
Port $FE — read (keyboard)
The keyboard is an 8 × 5 matrix. Eight address-line bits select a half-row; the data byte reads bits 0-4 of the five keys in that half-row (active low — 0 means pressed).
| Address read | Keys returned in bits 0-4 |
|---|---|
| $FEFE | SHIFT, Z, X, C, V |
| $FDFE | A, S, D, F, G |
| $FBFE | Q, W, E, R, T |
| $F7FE | 1, 2, 3, 4, 5 |
| $EFFE | 0, 9, 8, 7, 6 |
| $DFFE | P, O, I, U, Y |
| $BFFE | ENTER, L, K, J, H |
| $7FFE | SPACE, SYM, M, N, B |
Shadowkeep Unit 4 builds a QAOP input handler by reading $FBFE for Q, $FDFE for A, $DFFE for O and P.
Border colour
ld a, 2 ; red
out ($fe), a ; set border
Bits 0-2 of the byte written to port $FE set the border. The classic "loading stripes" effect is the border being set rapidly to different colours during tape pulse decoding — driven by the same bit 4 that the beeper uses.
Cultural impact
The ULA's design constraints — colour clash, contention, the one-bit beeper, the slow-clear screen, the awkward addressing — are not the platform's bugs; they're the platform's voice. Spectrum games look like Spectrum games because of the ULA. Programmers who learnt to dodge contention, design around clash, and squeeze beeper polyphony from one bit weren't being held back; they were learning a craft. The platform's aesthetic and the techniques that produce it both trace to this single Ferranti chip.
Why the ULA matters for Code Like It's 198x
Three out of four of Shadowkeep's load-bearing techniques are ULA techniques: bitmap addressing via the non-linear screen layout (Unit 3), attribute-as-game-rule via the one-attribute-per-cell scheme (Unit 5), and port $FE for keyboard and beeper (Units 4 and 7). The Project teaches the Spectrum as a platform whose constraints are integral to the design, not obstacles to it — which is, structurally, an argument about learning to think with the ULA rather than against it.