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.
Do this lesson in the simulatorThis 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 examplePC=10 MAR=9 CIR=BLT 4. - Storing to address 254 calls
forward(50, distance=<value>); storing to address 255 callstone(<value>, 0.2). - When
HLTis fetched, stop and printhalted after <cycles> cycles, counting every fetch including theHLT, thenACC=<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
- 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. - 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.
- Add an
LDXinstruction and indexed addressing, and use it to play a tune stored as a list of notes in memory from address 40. - Count how many memory reads your machine does in one run. Which instructions need the most, and which addressing mode would cut them?