Series: debug80diaries - 2026

Why I Came Back to the Z80

My involvement with the Z80 predates the TEC-1 by several years. In the late 1970s, I was designing systems on paper. During this period, I considered instruction sets. I also considered memory layouts and thought about the requirements for building a usable computer around an affordable processor. Like many people at the time, access to hardware lagged behind understanding. The ideas came first; the opportunity to realise them came later.

The TEC-1 Blue Prototype
The TEC-1 blue prototype: a bridge between 1983 and the modern debugger.

By the early 1980s that opportunity arrived. I built a sequence of Z80-based prototypes while refining the hardware and my understanding of how people might interact with such a machine. Those prototypes eventually became the basis for a collaboration with Ken Stone, and together we produced the TEC-1 computer kit. Talking Electronics magazine published this kit in 1983.

The TEC-1 was a product of its time. It reflected available components and the expectations of hobbyists who prepared to work close to the machine. It also reflected a long period of prior thinking about the minimal infrastructure required to make a computer programmable.

Working close to the machine

Programming the TEC-1 meant engaging directly with the processor. Input was via a hexadecimal keypad and output appeared on a small numeric display. Programmers entered commands as machine code for immediate execution, and because storage remained minimal, the system encouraged careful and incremental work.

I wrote the original monitor software, MON1, to support that style of interaction. It provided controlled access to memory and execution while staying out of the way. Its role was to make the processor usable rather than to abstract it. Over time, users developed a working familiarity with registers, addresses, and instruction flow simply by using the machine.

The Z80 itself suited this mode of work. It was capable enough to do interesting tasks, yet sufficiently compact that developers could program and debug it without elaborate tooling. While an assembler proved helpful, it remained optional. Furthermore, a symbolic debugger provided utility without being a strict requirement. The architecture was knowable and that mattered.

Returning with modern tools

When I began working on Debug80, I was not attempting to invent a new category of software. Debuggers and assemblers have existed for decades. Integrated environments have existed since at least the 1980s. Tools like Turbo Pascal demonstrated how effective a tightly integrated system could be for people who wanted to get productive quickly.

What prompted this project was a set of practical limitations I encountered while using existing Z80 tools. They did not align with how I think about programs or how I read listings. Rather than work around those constraints indefinitely, I decided to build something aligned with my own workflow. At the same time, the available platform had changed dramatically.

VS Code provides a mature and cross-platform environment with a well-developed debugging infrastructure. It runs on macOS as well as on Windows and Linux systems. It supports rich interaction models and extension points. Combined with my experience in JavaScript, it forms a practical foundation for building a debugger without reinventing the surrounding ecosystem.

Emulating the target hardware entirely in software is a natural consequence of that choice. I have explored TEC-1 emulation before, and extending that work into a debugger-capable environment follows directly from it.

Scope and direction

Designing Debug80 first and foremost centres on creating a working environment for my own use. It reflects my personal background and habits along with my expectations of what a debugger should provide. From there, it naturally extends to other Z80-based systems beyond the TEC-1 and eventually to other classic platforms.

The emphasis throughout remains on coherence and immediacy: keeping the number of moving components low while providing enough visibility to reason effectively about a running system. Building ROMs for real hardware remains an important outcome, but it is not the primary driver at this stage. The focus here remains on the process of understanding, iteration, and control.

The TEC-1 as a Platform

In Debug80, the term platform carries a specific structural meaning. A platform acts as a machine description that models the memory and I/O devices alongside specific hardware behaviour. This model defines exactly how software runs on a particular system. Because the debugger remains largely platform-agnostic, adding support for a new machine simply requires a corresponding platform module. The TEC-1 serves as the first full implementation in this system because its small, bounded nature makes it a perfect starting point for development.

The TEC-1 Blue Prototype
The TEC-1 blue prototype: a bridge between 1983 and the modern debugger.

A bounded machine

The original TEC-1 hardware utilizes a fixed and minimal memory layout. The ROM resides at 0000–07FF hex, while the RAM occupies the 0800–0FFF hex range. No operating system or file system exists to abstract this space. Instead, the program code and data share the same limited RAM alongside the stack and display logic. This constraint encourages developers to write compact programs and maintain a disciplined approach to address management.

For the Debug80 environment, this simplicity ensures that memory behaves as a concrete rather than abstract resource. The debugger presents the entire address space directly to the user. Every change to a memory location carries immediate meaning because no background activity or hidden consumers compete for the system state.

I/O as the system boundary

All interaction with the outside world passes through a small number of Z80 I/O ports. Port 0 handles input by reading the 20-key hexadecimal keypad. The system also uses the higher bits of this port for bit-banged serial input. While key presses can trigger a non-maskable interrupt for immediate response, many programs rely on polling to keep control flow explicit and predictable.

Output functions concentrate on Ports 1 and 2. Port 1 selects the active display digit and controls additional components such as the speaker and bit-banged serial output. Port 2 drives the seven-segment display data and the decimal point. Because the hardware requires software-based multiplexing, the scanning loop remains a visible component of program execution with observable timing consequences. From a platform perspective, display output represents a core behaviour that Debug80 must model with absolute accuracy.

The keypad as a control surface

The TEC-1 keypad defines the physical interface and how a developer uses the machine. It includes hexadecimal keys from 0 to F for data entry, alongside specialized function keys. The AD key toggles between address and data modes, while GO initiates execution from the current address. Users increment or decrement addresses using the + and - keys. A dedicated hardware RESET key returns the system to its initial state.

Programming on this platform remains an iterative and physical process. A developer selects addresses and enters bytes before starting execution and observing the results on the segmented display. While the monitor software supports this workflow, the programmer maintains the essential mental model. Debug80 informs its own execution control via this interaction style, even when software views replace the physical keypad and display.

Platform modules in Debug80

Debug80 models each machine as a distinct platform module that defines the memory layout and I/O port behaviour while specifying attached devices and timing assumptions. The TEC-1 module includes full software emulation of the hardware. This covers keypad scanning and display multiplexing alongside speaker output and bit-banged serial communication. Every instruction executes entirely within this precise emulated environment.

Alongside the TEC-1, the project maintains a minimal simple platform. This intentionally stripped-down Z80 system provides only serial input and output. It serves as a baseline and a development aid that allows for the exercise of debugger features without the overhead of device complexity. By testing against this simple model, I can ensure the core debugger logic remains sound before moving to more complex hardware.

Incremental expansion

The current platform model enables support for other classic Z80 machines as the project evolves. Some platforms attract interest because of their excellent hardware documentation. In other cases, developers can adapt existing software emulation for debugger use. Each addition brings both opportunity and technical cost that requires careful examination.

Hardware behaviour requires deep understanding, and every device requires a thorough model to ensure accuracy. Furthermore, I must evaluate the assumptions present in existing emulation code before integration. For these reasons, Debug80 expands its platform support deliberately and incrementally. The TEC-1 establishes the baseline for this growth: a small, complete machine where the developer can account for every single interaction.

Debug80 as a System

The Debug80 architecture functions as a small and tightly integrated system built around three primary components. It utilizes a JavaScript-based Z80 assembler alongside a software execution model for the processor hardware. The system also incorporates the standard VS Code debugging interface. Each component plays a specific functional role, and the ultimate value of the system depends on the precise alignment of these responsibilities. The development of Debug80 concentrates on integration rather than the invention of new core technologies. Because the underlying pieces already exist and remain well understood in the industry, the primary challenge involves connecting them in a way that respects the realities of Z80 programming. This integration ensures that execution remains observable and meaningful within a modern development environment.

VS Code as the execution host

VS Code provides a mature debugging framework that defines how environment launches programs and how the user controls execution. Structural pieces such as registers and breakpoints form an essential feature set, while call stacks and variable views complete the interaction model. Debug80 adapts this framework directly to a Z80 execution context.

When a debug session starts, VS Code drives execution through its standard mechanisms while the actual processor and memory run inside the internal software model. From the user’s perspective, Z80 machine code participates naturally in the same debugging workflow used for contemporary high-level languages. This approach allows the project to concentrate on machine behaviour and hardware state rather than building a custom debugger interface from scratch.

asm80 and the role of listings

The system utilizes asm80, a Z80 assembler implemented in JavaScript for Node. This tool produces machine code alongside listing files. These listing files record exactly how source lines correspond to assembled addresses and bytes. They form the central input for Debug80 because they capture the assembler’s decisions directly without any reinterpretation. Debug80 uses these listings to establish a precise relationship between source text and generated machine code. This mirrors the traditional workflow of Z80 development, where the listing represents the bridge between the programmer's intention and the machine's reality. By treating the listing as a primary source of truth, the system ensures that the debugger always accurately reflects the assembled output.

Mapping source to execution

While the VS Code debugger operates in terms of source locations, the Z80 processor operates entirely in terms of memory addresses. Debug80 connects these two disparate views by generating explicit mappings from the assembler listings. These mappings relate source files and line numbers to assembled addresses. They also relate them to runtime instruction execution.

Breakpoints and stepping, alongside state inspection, all pass through this critical mapping layer. When execution stops, the system shows exactly where the processor resides and how that location corresponds to both the original source and the assembled binary output. Mapping remains a structural foundation of the system rather than a secondary convenience, and much of the debugger's core behaviour depends on its accuracy.

Initialisation and workflow

The design of Debug80 prioritises minimal friction during setup and daily use. Since the assembler, emulator, and debugger extension all use JavaScript, the project simplifies distribution. This architecture allows the same environment to run seamlessly on macOS as well as on Windows and Linux systems. The extension coordinates assembler invocation, program loading, and debugger initialisation into a single, unified workflow.

This integration means that editing source code and observing execution happen within one environment using familiar controls. While the underlying complexity of the machine remains visible to the developer, the system organises the information into a coherent structure. By consolidating these disparate tools, Debug80 allows the programmer to focus on the logic of their program rather than the mechanics of the toolchain.

Execution under debugger control

During a debug session, the Z80 processor and all attached devices execute within the software model. Machine execution advances under the direct control of the VS Code interface. Actions like stepping and continuing, or breaking at defined points, allow the user to inspect the machine state consistently across different platforms.

The debugger actively drives the execution process rather than simply observing it from the outside. Registers and memory remain available for thorough examination at every step, as does the I/O state. This deep integration ensures that the tool behaves like a first-class debugger for an 8-bit architecture, providing the same level of control one expects from modern software development tools.

System coherence

Taken together, Debug80 forms a coherent environment rather than a loose collection of disparate tools. The assembler establishes the ground truth of the machine code while the platform model defines the hardware behaviour. Furthermore, the debugger interface provides the necessary control and visibility. Every component reinforces the others to maintain a consistent user experience. The emphasis throughout the project remains on architectural alignment and keeping the number of moving components small. This strategy allows low-level programming to remain approachable for a wider audience. By providing a clear window into the machine's internal state, Debug80 recovers the clarity of early computing within a modern context.

Listings, Mapping, and Making Execution Legible

Z80 programs execute at specific addresses while developers author code in source files. understanding is either preserved or lost in the space between these two views. In the Debug80 environment, the assembler listing occupies that critical gap.

The assembler used by Debug80, asm80, produces listing files that record how source text expands into bytes at specific addresses. These files remain simple and direct because they show the assembled output exactly as it will appear in memory. Debug80 treats these listings as primary input rather than secondary artefacts.

Why listings matter

A listing captures a moment of agreement between developer intention and machine execution. It records which source line produced which bytes and where the assembler placed that code. This information remains precise even when the source language utilizes macros and conditionals, or involves repeated constructs.

Because of this precision, listings provide a reliable foundation for mapping. They allow Debug80 to reason about execution without guessing how the assembler interpreted the original source. The resulting mapping reflects what actually happened during assembly. This approach aligns with traditional Z80 development practices, where programmers worked directly from listings by reading addresses and opcodes alongside their source. Debug80 builds on that legacy while extending the concept into a live debugging context.

Building the mapping

From the listing output, Debug80 constructs a set of explicit relationships. These connections link source files and line numbers while identifying assembled address ranges and instruction boundaries. These relationships form a mapping table that bridges the text editor and the execution engine.

The mapping process remains mechanical and deterministic because it follows the assembler’s output without relying on inference. When execution reaches a particular address, the debugger identifies the corresponding source location. When a user sets a breakpoint on a line of source, Debug80 resolves that location to one or more concrete machine addresses.

Stepping with intent

Stepping through a Z80 program involves more than simply advancing the program counter. Instructions vary in length, and macros or pseudo-operations often expand into multiple machine instructions. Furthermore, some source lines correspond to no emitted machine code at all.

The mapping layer allows Debug80 to step through code in ways that respect the source structure. A single step can advance execution to the next relevant source line, even when the operation involves multiple instructions.

At the same time, the underlying address-level execution remains visible and accessible to the user. This dual view supports two styles of reasoning: reading the program as written and inspecting the machine as it runs.

Breakpoints and visibility

Breakpoints operate through this same mapping layer. When a developer sets a breakpoint in the editor, Debug80 resolves it to the appropriate machine addresses based on the listing data. When execution reaches one of those addresses, the debugger stops so that the developer can inspect the current machine state.

Because the mapping remains explicit, the debugger shows how the stopped location relates to both the source and the assembled output. The user can then inspect registers and memory along with the I/O state to gain a clear understanding of exactly how the program arrived at that point.

Mapping as structure

Debug80 treats mapping as a structural foundation rather than a convenience feature. This layer influences how the system controls execution and presents state while also affecting how the user reasons about a running program.

The assembler listing provides the raw material while the mapping layer organises it into a form that connects human intent to machine behaviour. This connection makes debugging effective in low-level systems where abstraction layers remain thin and consequences are immediate. This focus on explicit mapping remains central to Debug80’s design, allowing classic development practices to persist in a modern environment without losing their original clarity.

The VS Code Skeleton

A debugger in VS Code lives in two distinct worlds. One world encompasses the user interface where developers interact with breakpoints and hover over variables. The other world houses the execution engine where bytes move through memory and registers undergo state changes. The Debug Adapter Protocol, or DAP, serves as the critical bridge between these two environments. For Debug80, building this connection started with a "skeleton"—a minimal implementation of the protocol that allowed VS Code to communicate with my Z80 runtime. This foundational layer established the basic dialogue needed to control execution from within the editor.

The common language of DAP

The Debug Adapter Protocol provides a standardized way for editors to interact with debuggers. Instead of developers writing custom interfaces for every language or architecture, VS Code expects a debug adapter to handle specific requests like initialize as well as launch and setBreakpoints. This abstraction allows me to focus on Z80 specific logic while VS Code handles the heavy lifting of the graphical interface. When a user clicks the margin to set a breakpoint, VS Code sends a setBreakpoints request. My adapter then translates that request into a machine address and instructs the engine to stop when it reaches that location.

Why I chose an Inline implementation

Debug adapters typically run as separate processes to provide stability, as a crash in the debugger will not take down the editor. However, Debug80 utilizes an "inline" implementation where the adapter runs directly inside the extension process. I chose this approach to simplify the communication between the debugger and my custom UI panels, such as the TEC-1 emulator view. Running inline avoids the need for complex inter-process communication when synchronizing the machine state with the visual hardware representation. This tight integration ensures that the LED displays and speaker output remain perfectly in sync with the underlying code execution.

Activation and the Resolution cycle

The lifecycle of a Debug80 session begins with activation. In the package.json manifest, I instruct VS Code to activate my extension whenever a user requests a z80 debug type.

"activationEvents": [
  "onDebugResolve:z80"
]

When a user initiates a session, VS Code triggers an initialization handshake. The adapter responds by describing its specific capabilities, such as support for stepping or variable inspection. Once this handshake completes, the environment remains ready to load the Z80 program and begin the execution cycle, bridging the final gap between the static JSON configuration and the live machine.

The Z80DebugSession

The heart of the skeleton resides in the Z80DebugSession class. This component inherits from the standard DebugSession base and acts as a central coordinator for the entire system. It manages the runtime state while utilizing the mapping data I established in previous articles and routing commands directly to the CPU engine.

By implementing this backbone first, I created a stable platform for the more complex work that follows. With the skeleton in place, I could finally begin the technical task of simulating the Z80 heartbeat—the execution loop that truly brings the machine to life.

This structural readiness allowed me to transition from protocol management to physical system simulation without revisiting my architectural assumptions.

Years