Project: a processor of your own

Build a von Neumann machine with register transfers, addressing modes, flags and memory-mapped I/O that plays a scale on the robot.

A9.10Computer architectureA level35 min

Do this lesson in the simulator

This project pulls the module together by building a working processor in Python. It has a von Neumann memory holding its program and data together, the registers from lesson A9.2, a fetch written as register transfers, an instruction set with immediate and direct addressing, status flags and conditional branches, and memory-mapped I/O controllers that drive the real robot. When it works, a program stored in its memory plays a scale on BugBot's piezo, and nothing in your Python mentions notes at all.

The machine

Registers. pc, mar, mdr, cir and acc, as in lesson A9.2, and two status flags, z and n, each 0 or 1.

Memory. A list of 256 locations, addresses 0 to 255. The program is loaded at address 0. Data can go anywhere else, which is the stored program concept: nothing but the program counter separates instructions from data.

The instruction set. Each instruction is a string: a three-letter opcode, a space, then an operand. An operand starting with # is immediate: the number itself. Otherwise it is direct: the address of the value.

Instruction Effect
LDA x ACC ← the value of x
ADD x ACC ← ACC + the value of x
SUB x ACC ← ACC - the value of x
STA a store ACC at address a (always direct)
CMP x work out ACC - the value of x, throw the answer away, and set z to 1 if it was zero (else 0) and n to 1 if it was negative (else 0)
BRA a PC ← a
BEQ a PC ← a, if z is 1
BLT a PC ← a, if n is 1
HLT stop

Memory-mapped I/O. Two addresses are not memory. They are wired to I/O controllers, so storing to them makes the robot act:

Address Writing a value there
254 the motor controller: drive forward that many centimetres, at speed 50
255 the piezo controller: play that many hertz for 0.2 seconds

The program in memory

0   LDA #10       // ACC = 10
1   STA 254       // drive forward 10 cm
2   LDA #300
3   STA 20        // note, at address 20, starts at 300
4   LDA 20        // loop: ACC = note
5   STA 255       // play it
6   ADD #100
7   STA 20        // note = note + 100
8   CMP #700      // is note - 700 negative?
9   BLT 4         // if so, go round again
10  HLT

Trace the loop in your head before you write any code. The notes played should be 300, 400, 500 and 600. After 600 is played, ACC becomes 700, CMP #700 gives zero, so z is 1, n is 0, BLT does not branch, and the machine halts. Count the fetches: 4 before the loop, 6 in each of the 4 passes, and the HLT: 29 in all.

Step 1: fetch

Write the fetch as the four register transfers from lesson A9.3. Straight after each fetch, add 1 to a cycle counter and print PC=1 MAR=0 CIR=LDA #10. Run the machine with only HLT handled, to check the fetch before you go on. With no branches working, the PC simply runs through the program in order, so it should print 11 fetch lines, the last PC=11 MAR=10 CIR=HLT, and stop.

Step 2: decode and the addressing modes

Split the CIR into opcode and operand. Then write one helper that turns an operand into a value, so every instruction shares it:

  • immediate (#): the number after the #;
  • direct: an address, so copy it into the MAR, copy the contents of that address into the MDR, and use the MDR.

Now LDA, ADD and SUB are one line each.

Step 3: store and memory-mapped I/O

STA a puts the address in the MAR and the accumulator in the MDR, then writes. Before writing to memory, check the address: 254 and 255 go to the robot instead. This is exactly the job of the address decoding in a real computer: the address on the bus decides whether RAM or an I/O controller responds.

Step 4: flags and branches

CMP is a subtraction that only sets flags. The branches never look at the accumulator: they look only at z and n, exactly as a real processor's branch instructions read only the status register. A branch is just an execute step that loads the PC.

Step 5: finish

When HLT is fetched, leave the loop and print halted after 29 cycles and then ACC=700 Z=1 N=0, using the values in your registers and counter.

Task: build the processor

Build the machine described above and run the program. The starter loads the program into memory and sets up the registers; you write everything else, using exactly the variable names pc, mar, mdr, cir and acc for the registers.

  • After every fetch, print PC=<pc> MAR=<mar> CIR=<cir> with the register values straight after the four fetch transfers, for example PC=10 MAR=9 CIR=BLT 4.
  • Storing to address 254 calls forward(50, distance=<value>); storing to address 255 calls tone(<value>, 0.2).
  • When HLT is fetched, stop and print halted after <cycles> cycles, counting every fetch including the HLT, then ACC=<acc> Z=<z> N=<n>.

A working machine drives 10 cm, plays 300, 400, 500 and 600 Hz, prints 29 fetch lines, then halted after 29 cycles and ACC=700 Z=1 N=0.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

memory = [0] * 256
program = [
    "LDA #10", "STA 254", "LDA #300", "STA 20", "LDA 20", "STA 255",
    "ADD #100", "STA 20", "CMP #700", "BLT 4", "HLT",
]
memory[:len(program)] = program

pc, mar, mdr, cir, acc = 0, 0, 0, "", 0
z, n = 0, 0
cycles = 0

Challenges

  1. Write a new program for your machine, in its memory, that drives forward 5 cm three times using a counter stored in memory and BEQ.
  2. Add an interrupt: a list of cycle numbers at which a timer fires. At the end of those cycles, push the registers onto a stack, run a short ISR stored elsewhere in memory that plays a high note, and pop them back. Show that the scale still plays correctly.
  3. Add an LDX instruction and indexed addressing, and use it to play a tune stored as a list of notes in memory from address 40.
  4. Count how many memory reads your machine does in one run. Which instructions need the most, and which addressing mode would cut them?