Work in Progress

CPU8

Building a Computer from Scratch

Building a Computer from Scratch

“What the fuck am I fucking doing here?!”

Charli XCX

It's a question that goes surprisingly well with learning how computers work.

What actually happens to the code we write? How does software control physical hardware? And how can something made from silicon become a programmable machine?

CPU8 is my attempt to find out. A computer reduced to its core components, built from basic logic.

PC → program ROM addressProgram ROM → instruction registerProgram ROM operand → MARIR → opcode decoderPhase → control enablesMAR → data memory addressData memory → AData memory → BData memory → PC for JMPA → ALU operandB → ALU operandControl → ALU operation selectALU → RESULTRESULT → data memory writePCProgram counterProgram ROM256 × 8 bitsIRInstruction registerMARMemory addressDecoder / controlInstruction → enablesPhase controllerFETCH → COMBINE → COMMITData memoryRAM + LED MMIOAOperand registerBOperand registerALU11 combinational operationsRESULTStored ALU result

ALU

An 8-bit combinational Arithmetic Logic Unit. A four-bit control selects its operation. Its output feeds RESULT, which only captures it at COMMIT for an ALU instruction.

Doing Math on a Rock

The half adder and full adder form the foundation of CPU8. They're the first step where basic logical operations become something more: a circuit that can actually perform arithmetic.

OperationExpressionOutput
ANDA & B1
ORA | B1
XORA ^ B0
NOT~A0

One Unit, Many Operations

The interesting part isn't just adding more operations. It's using control signals to choose what the same hardware does. This is where a collection of circuits starts feeling like a computer.

Control0000
0x05
0x07
Result12
0x0C
00001100
error0

ADD/SUB signal unsigned overflow or borrow, or signed overflow. TWC signals signed overflow only. This wire is not stored as a CPU flag.

Making a Processor

Rather than studying existing CPU architectures, I started with a simple question: What actually needs to happen for a computer to execute an instruction?

First, it needs to know what to do. Then, it needs to perform the required operation. Finally, it needs to preserve the result.

I figured it might be time to fire up the good ol' brain instead of asking AI or the internet to do the thinking for me. I spent several days trying to figure out how information should move through the processor, where it comes from and where it needs to go, visualizing everything with pen and paper to the point where I was even dreaming about the problem in my sleep.

Eventually, I arrived at a three-phase execution cycle: FETCH, COMBINE and COMMIT.

It was definitely not the fastest way to design a CPU, nor did it lead to the most efficient outcome. But that wasn't really the point. I wanted to understand the problem well enough to solve it myself, rather than just understand someone else's solution.

And that's what made this step so special to me. For the first time, it felt like I wasn't just connecting circuits anymore. I was actually building a computer, and I had figured out its fundamental architecture on my own.

PC → program ROM addressProgram ROM → instruction registerProgram ROM operand → MARIR → opcode decoderPhase → control enablesMAR → data memory addressData memory → AData memory → BData memory → PC for JMPA → ALU operandB → ALU operandControl → ALU operation selectALU → RESULTRESULT → data memory writePCProgram counterProgram ROM256 × 8 bitsIRInstruction registerMARMemory addressDecoder / controlInstruction → enablesPhase controllerFETCH → COMBINE → COMMITData memoryRAM + LED MMIOAOperand registerBOperand registerALU11 combinational operationsRESULTStored ALU result

ALU

An 8-bit combinational Arithmetic Logic Unit. A four-bit control selects its operation. Its output feeds RESULT, which only captures it at COMMIT for an ALU instruction.

FETCH

Find out what the program says to do.

COMBINE

Decode the instruction and let the required paths respond.

COMMIT

Capture the stable result on the clock edge.

A Computer, One Step at a Time

LDA brings data into register A. ADD writes A + B into RESULT. Step through the phases to see what changes, and when.

PC → program ROM addressProgram ROM → instruction registerProgram ROM operand → MARIR → opcode decoderPhase → control enablesMAR → data memory addressData memory → AData memory → BData memory → PC for JMPA → ALU operandB → ALU operandControl → ALU operation selectALU → RESULTRESULT → data memory writePC0x00Program ROM256 × 8 bitsIR0x00MAR0x00Decoder / controlInstruction → enablesPhase controllerFETCH → COMBINE → COMMITData memoryRAM + LED MMIOA0x00B0x00ALU11 combinational operationsRESULT0x00

Ready for GET 0x20.

  1. FETCH
  2. COMBINE
  3. COMMIT

Things GET complicated.

I wanted to keep the instruction set simple and easy to understand. An instruction like LDA already has a clear job: load a value into register A. Adding memory addressing to the same instruction felt like doing too many things at once.

So I introduced GET, a separate instruction whose only job is to bring a memory address into the CPU.

However, GET turned out to be one of the more complicated parts of the project. Unlike most instructions, it needs an additional byte from program memory, meaning the program counter has to advance within the instruction rather than just following the normal execution cycle.

In a way, GET combines two bytes into one operation. It also connects two parts of the system that had previously been developed separately: the CPU and its memory.

Since memory integration wasn't what I wanted to focus on, I handed that part over to AI. My main interest was understanding and designing the CPU architecture itself, not spending time getting the surrounding hardware integration to work.

Giving the Machine Instructions

Writing the assembler was another interesting moment. I finally understood what my code actually gets translated into.

An assembler is basically just a translator between human-readable instructions and a memory image made up of nothing but ones and zeros.

And that's what the CPU eventually reads. No magic, just a bunch of bytes telling the hardware what to do.

asm

RAM

0x20
5
0x21
7
0x22
0
Reference

One byte per instruction, except GET: opcode + address byte. JMP is indirect: PC ← DATA[MAR]. STR stores RESULT. Unary ALU instructions use A; binary ones use A and B.

Demo RAM starts with the selected example’s data; other bytes start at zero. Hardware RAM is unspecified without an initialization image.

Unused demo ROM bytes are NOP (00000000); the original assembler emits only supplied bytes. CPU8 has no HALT opcode. Stepping stops outside this 16-byte view. Assembling resets execution.

NOP00000000
LDA00000001
LDB00000010
STR00000011
ADD00000100
AND00000101
NOT00000110
OR00000111
SUB00001000
XOR00001001
SHL00001010
ROL00001011
SHR00001100
ROR00001101
TWC00001110
GET00001111
JMP00010000

Program ROM

PC
0x000
IR
0x000
MAR
0x000
A
0x000
B
0x000
RESULT
0x000

What's Next?

The next big goal is to develop a compiler for CPU8.

So far, programming the processor means writing assembly instructions directly. A compiler would make it possible to write programs in a higher-level language and translate them into instructions that my CPU can actually execute.

I'm especially interested in understanding how that translation works, from parsing source code to generating machine instructions.

Like the CPU architecture itself, this is something I want to figure out mostly on my own.

The project is still a work in progress, so I'll leave the final conclusions for when it's actually finished.

  1. Higher-level
    source
  2. CompilerPlanned
  3. Assembly /
    machine instructions
  4. Program ROM
  5. CPU8