Seeing many people attempting to build a processor “from the inside out” (i.e., mindlessly connecting individual logic gates without a structured plan), here is a concrete roadmap. We aren’t building a full MOS or Z80 replica right away. Instead, we will create a minimalist architecture capable of memory reads/writes, jumps, and basic arithmetic operations. Here is the correct sequence of abstraction layers required to complete the project successfully:
Step 1: Define the ISA (Instruction Set Architecture)
Before placing a single gate, you must strictly define your instruction set. A set of 4 to 8 elementary instructions is perfect for a start. Determine the format of the opcodes and their arguments, for example:
- NOP – Does nothing.
- BRK – Stops the processor (deasserts the clock enable signal until a hardware RESET).
- LOAD A, #value – Read a literal byte directly from the memory location following the opcode into Register A (Immediate mode).
- LOAD A, [addr] – Read a byte from the specified RAM address into Register A (Absolute mode).
- LOAD B, #value – Read a literal byte directly from the memory location following the opcode into Register B (Immediate mode).
- LOAD B, [addr] – Read a byte from the specified RAM address into Register B (Absolute mode).
- STORE A, [addr] – Write the current contents of Register A into the specified RAM address.
- STORE B, [addr] – Write the current contents of Register B into the specified RAM address.
- TAB – Transfer the contents of Register A directly into Register B.
- TBA – Transfer the contents of Register B directly into Register A.
- ADD A, #value – Add a literal byte directly to the contents of Register A (Immediate mode).
- ADD A, [addr] – Fetch a byte from the specified RAM address and add it to Register A (Absolute mode).
- ADD A, B – Add the contents of Register B to Register A.
- SUB A, #value – Subtract a literal byte directly from the contents of Register A using ALU logic (Immediate mode).
- SUB A, [addr] – Fetch a byte from the specified RAM address and subtract it from Register A using ALU logic (Absolute mode).
- SUB A, B – Subtract the contents of Register B from Register A using ALU logic.
- JUMP [addr] – Unconditional jump (overwrite the Program Counter with the target address).
I am stopping at these few, because more and more ideas for instructions and address modes that MUST be implemented keep popping into my head ;)
Step 2: Design the Data Path
Prepare a structure of registers connected by data and address buses. Every register output connected to a bus must be isolated using a tri-state buffer or controlled via a multiplexer to prevent bus contention. The minimum required blocks are:
- PC (Program Counter) - A counter indicating the address of the instruction currently being fetched.
- AR (Address Register) - A register that latches the address for the RAM.
- A, B - General-purpose registers (Accumulator and helper register).
- ALU (Arithmetic Logic Unit) - The arithmetic and logic core.
💡 ALU Tip: Instead of designing an ALU from scratch, it is best to implement the architecture presented by Sebastien Lague (the author of DLS) in his YouTube series on building a computer. He demonstrates step-by-step how to build a unit that handles both addition and subtraction using simple adders controlled by inversion and carry-in lines.
Step 3: The Flags Register – The Link Between ALU and CPU
Borrowing from the MOS 6502 architecture, the processor must be able to react to the results of mathematical operations. This is handled by the Status Register (P - Processor Status). Each bit (flag) is hardware-set by the ALU after executing an ADD or SUB instruction:
- Z (Zero) Flag - Set to 1 if the ALU result is exactly zero.
- N (Negative) Flag - Copies the highest bit of the result (the sign bit in two’s complement). If bit 7 is 1, the result is negative.
- C (Carry) Flag - For ADD, it signals an unsigned overflow (result > 255). For SUB, it acts as a borrow indicator—Sebastien’s ALU naturally generates this signal by analyzing the output carry bit when the second operand is inverted.
- V (Overflow) Flag - Indicates a signed overflow (Two’s Complement) when an operation on two numbers of the same sign yields a result with the opposite sign. These flags are critical because, in the next stage of your processor’s development, they will allow you to implement conditional jumps (e.g., BEQ – branch if Z=1).
Step 4: The Sequencer – Microstep Counter
Executing a single instruction requires several clock cycles (control phases, also kwnown as Instruction Cycle or Fetch-Decode-Execute Cycle). For instance, a LOAD instruction needs time to:
- Assert the PC onto the address bus and latch the operation code (Opcode Fetch).
- Decode the instruction within the Control Unit.
- Assert the argument’s address onto the address bus.
- Read data from RAM and latch it into the destination register.
To manage this process, a step counter is required (e.g., a 3-bit counter counting from 0 to 7) which resets back to its initial state (0) upon completing the full instruction sequence.
Step 5: The Control Unit Based on Microcode
Designing combinational control logic purely out of logic gates becomes unmanageable as the number of instructions grows. The most efficient approach is using a ROM component as a microcode decoder.The ROM input address is a combination of: [Current Opcode] + [Current Microstep Counter Value]. The ROM output is a wide Control Word (e.g., 16, 24, or 32 bits), where each individual bit is physically wired to a control line (Enable, Write, Read, Clear) of a specific block within the CPU.
💡 Engineering Pro-Tip: To generate the contents of your control ROM, it is best to write a simple compiler in Python. The script should parse a text file containing symbolic state descriptions (e.g., PC_OUT, RAM_READ, AR_IN) and automatically map them to their respective bit positions in the control word, outputting a clean binary file ready to be loaded into DLS. Manually toggling bits in a hex editor is a guarantee for logic bugs.
Step 6: Initialization (RESET) and the First Test
The final stage is implementing the RST (Reset) line. This signal must force an asynchronous load of the start address (e.g., 0x0000) into the PC register and clear the microstep counter. To verify that your logic structures work properly, place a simple sequence of instructions into the RAM:
0x0000: LOAD A, [0x0010] ; Fetch value from address 0x10
0x0003: SUB A, [0x0011] ; Subtract value at address 0x11
0x0006: JUMP 0x0003 ; Loop back to repeat the operation
If the microstep counter cycles correctly after a reset, the PC register performs the proper address jumps, and the accumulator stabilizes with the correct values according to the ALU, your processor core is functional. With this solid foundation, you can move on to implementing advanced mechanisms like hardware interrupt handling (IRQ/NMI), additional registers, stack, conditional jumps, indexed address modes or more ALU operations (incl. BCD mode).
Since I am nearing the completion of a MOS 8502 replica — the heart of the 8-bit computers from the Commodore stable — and ultimately plan a full Commodore PLUS/4 replica, I have already built many of these circuits and ironed out most of the edge cases. Feel free to ask about the details. To cover absolutely everything, I would need hours of lectures or reams of paper ;) Which I might actually do someday, who knows.
