top of page

ZX Spectrum Reached Hacker News, but Text Mode Exposes Its ROM Tradeoffs

Aug 4
12 min read

The ZX Spectrum returned to hacker news through a 2026 system tour that exposes a conflict buried inside its 16K ROM. Printing one character can be simple. Building dependable machine-code text output requires understanding undocumented assumptions, mutable system variables, persistent attributes, and hardware-specific input paths.

Michael Martin published the machine-code tour on May 30, 2026. The post follows an earlier BASIC exploration, but it does more than translate familiar commands into Z80 assembly. It shows where Sinclair’s convenient programming environment ends and its loosely structured firmware begins.

That distinction gives the post relevance beyond retrocomputing. Commodore machines offered stable KERNAL jump tables, while MSX defined firmware calls across manufacturers. The Spectrum instead encouraged programmers to combine a few ROM entry points with direct access to system state. That approach saved layers of abstraction, but transferred compatibility and debugging work to developers.

The result is not a newly discovered feature or a modern product announcement. It is a close examination of an old engineering bargain. The Spectrum made useful primitives available in little memory, yet never turned those primitives into a clean machine-code platform.

What the ZX Spectrum System Tour Actually Changed

The new contribution is a connected machine-code path from text output to graphics, input, and a complete running display.

The Spectrum has not suddenly acquired a text mode. Martin’s post changes the available explanation by assembling several scattered mechanisms into one practical sequence. It begins with a compact Hello World routine, then moves through character codes, color controls, custom graphics, screen clearing, keyboard scanning, and joystick input.

The first step uses RST $10, a ROM restart entry that prints the character held in the Z80’s A register. A restart is a compact call to a fixed low-memory address. Before using it, the example writes zero to TVFLAG at IY+2, directing output toward the main screen area.

That sequence makes the basic operation look almost modern. A program loads a pointer to a message, retrieves one byte, calls the printer, and repeats. Martin places the code at address $7000, leaving room below it for BASIC while retaining usable memory on a 16K machine.

The simplicity lasts until text requires state. The Spectrum’s screen normally presents 24 rows of 32 characters. Its firmware divides those rows into a 22-line upper window and a two-line lower window used for editing and status messages. Sinclair’s original display specifications confirm that split and describe a 256 by 192 pixel display.

The machine does not have separate character hardware comparable to a conventional terminal. Its ROM draws an 8 by 8 glyph into bitmap memory, then writes color information for the corresponding cell. That means text output already depends on the graphics layout, current attributes, cursor position, and selected output channel.

Martin then expands the path with 16 predefined semigraphics characters and user-defined graphics. Semigraphics divide a character cell into blocks, allowing simple shapes to travel through the ordinary text printer. User-defined graphics occupy character codes beginning at $90, with their bitmap data located through the UDG system pointer.

The post’s final example combines these facilities into a colored banner with a custom umbrella image. It loads four character definitions, emits inline control bytes, waits for input, and restores the screen. Martin reports that the machine-code package is less than half the size of his earlier BASIC version, even after including its loader and tape header.

That comparison is the event’s real payoff. The post does not merely present isolated addresses. It demonstrates that the Spectrum’s ROM can serve as a compact application framework, provided the programmer accepts responsibility for its hidden state.

Why a Hacker News Audience Still Cares About This ROM

The Spectrum compresses a familiar systems problem into a machine small enough to understand almost completely.

The article reached hacker news because it treats retro hardware as an inspectable software system. Every major operation has a visible path. A character passes through a fixed ROM entry, reads a glyph, touches bitmap memory, applies one attribute byte, and advances a cursor represented in system state.

Modern developers encounter the same categories of problem behind much larger interfaces. Libraries retain configuration. Output streams have state. Compatibility depends on behavior that documentation may not promise. Hardware abstractions expose escape hatches when normal interfaces prove too limited.

On the Spectrum, those issues fit inside a Z80 address space. The original model used a Z80A processor running at 3.5 MHz, a 16K ROM, and either 16K or 48K of RAM. Those constraints make every abstraction visible in the memory map.

The display is particularly instructive. A standard Spectrum screen occupies 6,912 bytes, consisting of a 6,144-byte monochrome bitmap and 768 attribute bytes. Each attribute byte supplies foreground and background colors, brightness, and flashing for one 8 by 8 cell.

That design conserved memory, but it coupled nearby pixels to one color choice. The familiar result is attribute clash, where differently colored objects cannot pass through the same cell without affecting each other. Text printing inherits that architecture because each glyph lands in one of those cells.

Martin’s walkthrough adds a second lesson. A small documented interface does not necessarily create a stable programming platform. The Spectrum exposes an effective character printer, but sophisticated programs must also know addresses for variables such as ATTR-T, MASK-T, P-FLAG, SCR-CT, and UDG.

The official system variables document describes the shared memory used by BASIC and ROM routines. Calling firmware while changing those values directly is efficient, but it creates tight coupling. A program depends on both the callable routine and the internal state that routine expects.

This is where the Spectrum differs from machines designed around more formal firmware boundaries. Commodore’s KERNAL used fixed jump vectors for common services. MSX standardized calls so software could target machines from different manufacturers. The IBM PC’s BIOS also established callable services above the hardware, even when developers later bypassed them for speed.

Sinclair’s approach was less formal. It served BASIC well because Sinclair controlled both the interpreter and the ROM. Assembly programmers received useful implementation details instead of a broad compatibility contract.

That bargain explains the continuing interest. The Spectrum offers an unusually clear case study in how an internal implementation becomes a public interface. Once programmers build software against addresses and quirks, those details become difficult to change, whether their designer intended that outcome or not.

The Real Opponent Is Convenience Versus Stability

The Spectrum’s ROM makes simple programs easy, but every shortcut increases dependence on machine-specific behavior.

The central conflict is not the ZX Spectrum against the Commodore 64. It is convenience against stability inside the Spectrum itself. Direct system access reduces code size and exposes useful capabilities. It also makes software responsible for assumptions that a stronger firmware contract would have contained.

Consider text attributes. Character codes $10 through $17 control INK, PAPER, FLASH, BRIGHT, INVERSE, OVER, cursor placement, and tabbing. A program can embed those bytes inside a string, then send the entire sequence through RST $10.

That is a compact mechanism. It resembles terminal escape sequences, where nonprinting bytes alter the interpretation of later text. It allows messages to carry formatting without separate drawing calls.

The surprising part is persistence. Those machine-code controls do not reset after the equivalent of a BASIC PRINT statement. A carriage return also does not restore the previous state. A helper that assumes formatting ends with one string can therefore change every later print operation.

The ROM tracks temporary and permanent attributes through several system variables. ATTR-T contains current color, brightness, and flash settings. MASK-T determines which bits should remain unchanged. Permanent counterparts influence screen clearing and establish defaults.

This division works because BASIC manages it as part of a larger language operation. Assembly code enters below that layer. It must reproduce the setup and cleanup behavior that BASIC normally supplies.

Screen clearing reveals the same tension. Calling the ROM’s CLS routine clears the display, but Martin notes that it also redirects subsequent output to the lower window. It does not fully coordinate the screen border with the upper and lower areas.

His clrto helper repairs that behavior. It sets permanent attributes, derives the border color, clears masks and mode flags, invokes CLS, then opens channel two for the upper screen. A supposedly basic operation becomes a small state-restoration protocol.

The CHAN-OPEN routine at $1601 demonstrates why the ROM remains useful. Opening a channel is clearer than merely patching a flag. Yet the program still needs direct variable writes and an output instruction to port $FE. Firmware and hardware access remain intertwined.

This combination can be productive on a fixed target. It avoids duplicating the ROM’s character rasterizer and cursor handling. A developer gains readable text, color controls, windowing behavior, and custom glyphs without writing every pixel routine.

The cost appears when the target changes. Martin highlights the Timex Sinclair 2068, whose incompatible ROM damaged Spectrum software compatibility in the United States. Programs that depended on fixed routines or system layouts could not assume equivalent behavior.

A conventional application programming interface separates supported behavior from internal organization. The Spectrum’s assembly environment offers only a partial version of that boundary. Its ROM calls are attractive because they are already present, but the surrounding contract is partly reconstructed by developers.

This is why the walkthrough matters more than another Hello World example. It makes the hidden contract explicit. The code documents which state must be set, which routines alter it, and which values need restoration afterward.

Text Mode Is Really a Bitmap and State Machine

Calling this a text mode is useful shorthand, but the implementation is a bitmap renderer governed by shared mutable state.

The phrase “text mode” usually suggests dedicated character cells stored as character codes. Hardware retrieves a glyph for each code and draws it automatically. Changing one cell means writing a character value and perhaps a color value.

The original Spectrum works differently. Software invokes the ROM printer, which renders glyph pixels into the same bitmap used by graphics. A separate attribute area supplies color at character-cell resolution. The visual grid exists as a programming convention, not as a complete hardware text buffer.

This distinction explains several mechanisms in Martin’s tour. The ROM can print normal characters, block graphics, and user-defined glyphs through one path because all of them become 8 by 8 pixel patterns. The printer does not need to know whether a glyph represents a letter or part of an umbrella.

The high portion of the Spectrum character set supports this approach. Codes $80 through $8F represent 16 block combinations. Codes beginning at $90 address user-defined graphics. Later codes encode BASIC keywords, allowing the interpreter to store commands compactly.

Custom graphics depend on indirection. The UDG system variable points toward the current user-defined bitmaps. Each character occupies eight bytes, one byte for each row. Martin’s sample copies 32 bytes into that area to define four adjacent pieces of the umbrella.

That indirection is a modest but important abstraction. The drawing routine does not require one hard-coded graphics address. A program can discover the active area through the pointer, then replace the shapes. The behavior resembles a configurable font atlas on a much smaller scale.

Color remains cell-based. An attribute byte assigns one ink color, one paper color, a brightness bit, and a flashing bit. Individual pixels determine whether the cell shows ink or paper, but cannot choose unrelated colors.

This memory-saving design turns formatting controls into operations on both state and screen memory. When the ROM prints a character, it consults ATTR-T and MASK-T, writes pixels, and updates the attribute cell. Transparent ink or paper options work by masking selected fields instead of replacing the whole byte.

The ROM disassembly remains valuable because it exposes the paths behind these effects. That material can verify what an entry point actually changes, especially when a program depends on behavior beyond a manual’s surface description.

The shared state also creates subtle failures. Martin’s first print loop uses zero as its string terminator. That works until zero becomes meaningful data. The completed banner needs to print control arguments whose value is zero, including settings for paper and brightness.

The revised loop therefore uses $FF as its sentinel. That byte represents the BASIC keyword COPY, which the banner will not print. The change is small, but it illustrates a general protocol problem: an in-band terminator fails once the data format expands to include that value.

The same issue appears in network protocols, file formats, command streams, and serialization libraries. A byte is safe as a delimiter only while the payload excludes it. Once control data and display data share one stream, framing deserves explicit design.

That is the strongest modern lesson in the post. The machine’s limits are old, but its failure modes are current. Shared state, undocumented side effects, overloaded byte values, and narrow compatibility assumptions still shape software systems.

Input Completes the Firmware Tradeoff

Keyboard and joystick handling show the same pattern as text output: use firmware when its policy helps, then bypass it when direct control matters.

Martin’s tour moves from display output to keyboard input because a usable text system needs interaction. The Spectrum again offers two paths. Programs can consume keyboard state prepared by the ROM, or read hardware ports directly.

The firmware path relies on the machine’s frame interrupt. An interrupt is a hardware-triggered transfer into a service routine. On each video frame, the Spectrum’s handler updates its FRAMES timer and scans the keyboard matrix.

When it finds a key, the handler decodes that input and stores a character in LAST-K. It also sets bit five of the FLAGS system variable. Martin’s getkey routine waits with the Z80 HALT instruction, tests the flag, retrieves the character, clears the flag, and returns.

Using HALT matters. The loop has nothing useful to do until the interrupt handler performs another keyboard scan. Waiting for that interrupt avoids repeatedly reading an unchanged flag at full processor speed.

This path offers interpretation rather than raw electrical state. The ROM understands keyboard combinations and maps them into characters. A program can accept text without reproducing the keyboard decoder.

Direct input trades that convenience for immediacy. The Spectrum keyboard is arranged as a matrix accessed through I/O ports. Selecting a row and checking returned bits reveals which keys are currently held.

The Z80 introduces a historical wrinkle. Some input and output instructions appear to expose an eight-bit port address, but IN A,(C) and OUT (C),A place the full BC register on the address bus. Sinclair used that behavior when designing its hardware interface.

Martin’s example checks the A key through port value $FDFE. That code does not wait for the ROM to translate a press. It asks the hardware about one position in the matrix, making it useful for games that need continuous directional state.

The Kempston joystick is simpler. It uses port $1F, where bits represent directions and the fire button. Martin’s reader translates that bitfield into horizontal and vertical deltas plus a fire value.

These options show why programmers bypass abstractions even when an abstraction exists. Firmware keyboard input is well suited to entering text or waiting for a command. Direct port reads are better for simultaneous movement, low latency, and repeated state checks.

The cost is portability. A routine tied to the Spectrum keyboard matrix assumes the machine’s electrical arrangement. A Kempston reader assumes that particular interface. Emulators must reproduce those behaviors, while alternate hardware or joystick standards need different code.

Martin also notes inconsistencies involving synthetic shift keys in emulators. This is a useful skeptical angle. A technically accurate routine can still behave differently when the surrounding implementation interprets host input in another way.

The hacker news appearance should not be mistaken for broad verification of every emulator or Spectrum variant. The linked submission received a modest response and no recorded discussion in the supplied snapshot. The technical value comes from the reproducible code path, not from crowd consensus.

Readers should therefore separate three layers of claim. The original Sinclair documentation establishes the machine’s intended facilities. ROM analysis reveals implementation behavior. Martin’s examples demonstrate one working development path, but they do not guarantee identical results across every clone, ROM revision, interface, or emulator.

What Developers Should Watch After the Hacker News Spike

The next test is whether this tour becomes durable technical infrastructure instead of a short-lived link.

The first signal is continuation of the system tour. Martin ends with a clear gap: the examples can reproduce the main display, input, and animation needed for a machine-code version of the earlier game, but sound and its title screen remain unresolved. A follow-up that handles graphics or audio would show whether the same method scales beyond character-oriented output.

The second signal is reproducibility across targets. Developers should test the examples on original 48K hardware, later Spectrum models, common emulators, and ROM variants. Matching output would strengthen the case for treating these routines as a practical compatibility layer. Divergence would identify where direct state access overtakes the stability of ROM calls.

The third signal is whether the code becomes easier to inspect and reuse. A downloadable example, documented build process, fixed test image, or emulator automation would turn the article into an executable reference. Martin already names the assembler, tape-packaging tool, and FUSE emulator used for the initial Hello World flow. Preserving those dependencies matters as much as preserving the assembly listing.

There is also a wider documentation question. Retro platforms often have abundant information but fragmented authority. Manuals describe intended behavior, disassemblies expose internals, community references correct errors, and modern tutorials connect the pieces. A useful platform guide can reduce that fragmentation if it clearly distinguishes documented contracts from observed quirks.

Developers following the story should resist turning one elegant example into a universal rule. Direct ROM calls can save memory and development effort. Direct hardware access can improve responsiveness. Neither guarantees compatibility outside the exact environment where it was tested.

That uncertainty is part of the value. The ZX Spectrum makes it possible to trace failures through a complete stack, from a string byte to a ROM routine, system variable, memory address, and display cell. Few modern systems permit that level of inspection.

The most useful next action is simple: reproduce the banner, change one assumption, and observe what breaks. Move the code, alter the terminator, leave an attribute active, select the wrong channel, or test another ROM. The hacker news moment will pass, but those experiments preserve the real lesson: an interface is defined as much by its state and side effects as by the entry point a programmer calls.

Give every agent the context to do better work

Connect your agents to the knowledge, decisions, and history already organized in remio.

remio currently supports Windows 10+ (x64) and Macs with Apple silicon.

Your AI Partner at Work
Get more done with remio

Plan. Create. Deliver.
All in one place.

bottom of page