Introduction
A CPU is a device which takes instructions from RAM and performs actions based on those instructions.
There are a variety of instructions that a CPU can perform:
- Arithmetic instructions - add, subtract, and on bigger CPUs multiply and divide
- Logic instructions - and, or, exclusive or, shifts
- Memory instructions - load a value from RAM into a register, store a register back into RAM
- Control flow instructions - jump to another instruction, either always or only if a condition is met
The CPU keeps its place with a program counter, the address of the next instruction, and does its sums in registers, a few fast storage slots of its own.
Implementation
In theory a CPU can be very simple to implement - it is a for loop over a list of instructions and a switch statement to perform the required instruction each time.
There are many complexities which can be added but let's start simple. This CPU has one register, an accumulator, 256 bytes of RAM, and seven instructions, each two bytes long: the instruction code and an address.
// Instruction codes
const LOAD = 1, STORE = 2, ADD = 3, SUB = 4, JUMP = 5, JUMPZ = 6, HALT = 7
function run( ram ) {
let pc = 0 // Program counter: where the next instruction is
let a = 0 // Accumulator: the one register
for(;;) {
const op = ram[pc++]
const arg = ram[pc++]
switch( op ) {
case LOAD: a = ram[arg]; break
case STORE: ram[arg] = a; break
case ADD: a = (a + ram[arg]) & 255; break
case SUB: a = (a - ram[arg]) & 255; break
case JUMP: pc = arg; break
case JUMPZ: if( a === 0 ) pc = arg; break
case HALT: return
}
}
}
The & 255 keeps the accumulator to 8 bits, so it wraps round like the real thing: 0 minus 1 is 255.
A program
This CPU can't multiply, so let's give it a program which does it by adding. It multiplies 6 by 7 by adding 6 to a total seven times, counting down as it goes:
const ram = new Uint8Array( 256 )
ram.set( [
LOAD, 34, // 0: a = total
ADD, 32, // 2: a += x
STORE, 34, // 4: total = a
LOAD, 33, // 6: a = count
SUB, 35, // 8: a -= 1
STORE, 33, // 10: count = a
JUMPZ, 16, // 12: if count is 0, finish
JUMP, 0, // 14: otherwise go round again
HALT, 0 // 16: stop
] )
ram.set( [ 6, 7, 0, 1 ], 32 ) // x, count, total, the number 1
run( ram )
console.log( ram[34] ) // 42
The program and its data share the same RAM, just as on a real machine. Nothing stops the program writing over its own instructions, which is how a lot of old code saved a few bytes.
From here a real CPU emulator adds more registers, flags for zero, carry and negative, a stack for calling subroutines, and instructions of different lengths. But the heart of it stays the same loop and switch.