The TOFU Circuit Simulator

Harry Porter's Relay Computer

Introduction

Harry Porter's relay computer is the ultimate retrocomputing project. He designed and constructed a simple CPU using a meager 415 four-pole double-throw relays.

http://web.cecs.pdx.edu/~harry/Relay/

I decided build a relay-level simulation of his machine using Tofu. On his web page, you’ll find a PowerPoint presentation describing the relay circuits he used and his computer’s architecture. You’ll also find detailed commentary on the machine’s instruction set including sample programs. Unfortunately, the documents leave many gaps, the most significant of which are the circuits he used to construct his state machine. But, I built the simulation as a learning experience and filling in the gaps was part of the fun. Due to this lack of an exact specification, my machine is not a perfect clone of his, but it’s able to run his programs.

You can download the latest resources here: harryporter2007.02.03.zip.

Instruction Set

I started out by creating an assembly language compiler. I defined my own assembly language mnemonics and syntax, but the compiler generates the same machine language described in Harry Porter’s documents. Here’s the instruction set accepted by my compiler:

ADD [destination register]

The ADD instruction puts the sum of B and C into either A or D. If the result is negative (i.e. the left-most bit of the result is 1), the sign flag is set. If the addition caused a carry-out, the carry flag is set. If the sum is 0, the zero flag is set. Here are some examples:

ADD A
ADD D

AND [destination register]

The AND instruction puts the bitwise-and of B and C into either A or D. If the result is negative (i.e. the left-most bit of the result is 1), the sign flag is set. If the result is 0, the zero flag is set. Here are some examples:

AND A
AND D

BE [label]

The BE (branch equal) instruction causes program execution to continue at the specified location if the zero flag is set. It is equivalent to BZ. Labels are discussed below. The following example compares B and C and jumps to a label if they are equal:

XOR A
BE B_AND_C_MATCH

BNE [label]

The BNE (branch not equal) instruction causes program execution to continue at the specified location if the zero flag is not set. It is equivalent to BNZ. Labels are discussed below. The following example compares B and C and jumps to a label if they are not equal:

XOR A
BNE B_AND_C_DO_NOT_MATCH

BNG [label]

The BNG (branch negative) instruction causes program execution to continue at the specified location if the sign flag is set. Labels are discussed below. The following example puts B+1 into A and jumps to a label if the sum is negative:

INC A
BNG A_IS_NEGATIVE

BNZ [label]

The BNZ (branch not zero) instruction causes program execution to continue at the specified location if the zero flag is not set. It is equivalent to BNE. Labels are discussed below. The following example puts B+1 into A and jumps to a label if the sum is not 0:

INC A
BNZ A_IS_NOT_ZERO

BZ [label]

The BZ (branch zero) instruction causes program execution to continue at the specified location if the zero flag is set. It is equivalent to BE. Labels are discussed below. The following example puts B+1 into A and jumps to a label if the sum is 0:

INC A
BZ A_IS_ZERO

CAL [label]

The CAL (call) instruction causes program execution to continue at the specified location. In addition, the address of the next instruction after the CAL instruction is saved in the XY register. Labels are discussed below. Here’s an example:

CAL MULTIPLY_ROUTINE

CLR [register]

The CLR (clear) instruction sets the specified register to 0. The register can be one of the following: A, B, C, D, M1, M2, X, Y. Here are some examples:

CLR A
CLR C
CLR M1
CLR X

HLT

The HLT (halt) instruction suspends program execution. Here’s the only way to use it:

HLT

INC [register]

The INC (increment) instruction adds 1 to either A, D or XY; however, incrementing XY does not affect the status register flags. For A and D, the following rules apply: If the result is negative (i.e. the left-most bit of the result is 1), the sign flag is set. If the addition of 1 caused a carry-out, the carry flag is set. If the result is 0, the zero flag is set. Here are some examples:

INC A
INC D
INC XY

JMP [label]

The JMP (jump) instruction causes program execution to continue at the specified location. Labels are discussed below. Here’s an example:

JMP LOOP

LD [destination register]

The LD (load) instruction uses the 16-bit value in the M register as an address and reads a byte from main memory into either the A, B, C, or D registers. Main memory is organized as 32K bytes, addressed from hex 0000 through 7FFF. Here are some examples:

LD A
LD B
LD C
LD D

MOV [destination register] [source register]

The MOV (move) instruction copies the value in the source register into the destination register. There’s an 8-bit version and a 16-bit version of this instruction. In the 8-bit version, the source and destination registers can be any of the following: A, B, C, D, M1, M2, X, Y. Here’s a few examples:

MOV A B
MOV C D
MOV M1 X
MOV M2 Y

If the source register and the destination register are the same, the register will be set to 0.

The 16-bit version copies the value of a 16-bit source register into a 16-bit destination register; however, it’s less flexible than the 8-bit version. It can only move values between the following register pairs:

MOV PC XY
MOV PC M
MOV PC J
MOV XY M
MOV XY J

NOT [destination register]

The NOT instruction puts the bitwise-inversion (i.e. it flips each bit) of B into either A or D. If the result is negative (i.e. the left-most bit of the result is 1), the sign flag is set. If the result is 0, the zero flag is set. Here are some examples:

NOT A
NOT D

OR [destination register]

The OR instruction puts the bitwise-or of B and C into either A or D. If the result is negative (i.e. the left-most bit of the result is 1), the sign flag is set. If the result is 0, the zero flag is set. Here are some examples:

OR A
OR D

RET

The RET (return) instruction causes program execution to continue at the address stored in the XY register. It is meant to be used in conjunction with CAL. Here’s the only way to use it:

RET

SET [destination register] [immediate or label]

There are 3 version of the SET instruction. The first version assigns a 5-bit immediate (a value inclusively between -16 and 15) to the specified destination register, which can be either A or B. Negative immediate values are sign extended to 8 bits. The second version assigns a 16-bit immediate to M or J. For both versions, if the immediate ends with H, it is interpreted as a hexadecimal value. Otherwise, the immediate is assumed to be a decimal value. The third version is equivalent to the second version, but instead of a 16-bit immediate, it accepts a label, which is effectively a 16-bit address value. Here are some examples:

SET A 7
SET B -2
SET M 1234h
SET J 4660
SET M SOME_LOCATION

SHL [destination register]

The SHL (shift left) instruction performs a circular left-shift (a.k.a. a left rotation) on B and puts the result into either A or D. If the result is negative (i.e. the left-most bit of the result is 1), the sign flag is set. If the result is 0, the zero flag is set. Here are some examples:

SHL A
SHL D

STO [source register]

The STO (store) instruction uses the 16-bit value in the M register as an address and stores the value from either A, B, C, or D into a byte in main memory. Main memory is organized as 32K bytes, addressed from hex 0000 through 7FFF. Here are some examples:

STO A
STO B
STO C
STO D

XOR [destination register]

The XOR instruction puts the bitwise-xor of B and C into either A or D. If the result is negative (i.e. the left-most bit of the result is 1), the sign flag is set. If the result is 0, the zero flag is set. Here are some examples:

XOR A
XOR D

Language Features

My assembly language is case-insensitive. It supports the following features and directives:

BYTE [8-bit value]

The BYTE data definition directive lets the programmer reserve storage for a byte of data. If the value ends with an H, it is assumed to be a hexadecimal value; otherwise, it is assumed to be decimal. Here are some examples:

BYTE 123
BYTE 7Bh

Comments

Comments begin with #. The # and everything to the right is ignored by the compiler.

# This is what a comment looks like.
SET A 3 # This is another comment.

DATA [hex pair] [hex pair] ... [hex pair]

The DATA directive lets the programmer reserve storage for one or more bytes of data. The values are hex pairs. Here’s an example:

DATA 48 65 6C 6C 6F 20 57 6F 72 6C 64 21

Labels

Labels end with a colon. A label cannot share a line with a command or a directive. Here’s an example:
LOOP:

Labels provide a convenient way of moving immediate values into registers as shown here:

# Move multiplier into Y
SET M MULTIPLIER 
LD A
MOV Y A

# ...

MULTIPLIER:
BYTE 179

STRING “...”

The STRING directive is functionally equivalent to the DATA directive, but more convenient for character data. Strings start and end with quotes. A string cannot be wrapped onto multiple lines; however, you can always declare multiple STRING directives on successive lines. There is no notation for escape characters. But, the end of a string is marked the last quote on the line. This means that you can put quote characters inside of strings without escaping them. Also, #’s are allowed within strings; they will not cause part of the string to be interpreted as a comment. Strings are not automatically null-terminated. To null terminate a string, follow the STRING directive with a BYTE 0 directive.

HELLO_WORLD:
STRING "Hello World!"
DATA 0D 0A 00

WORD [16-bit value]

The WORD data definition directive lets the programmer reserve storage for a 16-bit word of data. If the value ends with an H, it is assumed to be a hexadecimal value; otherwise, it is assumed to be decimal. Here are some examples:

WORD 1234h
WORD 4660

Assembler

Here’s an example of running the assembler with Java 5:

java -jar assembler.jar
Args: [input file] [output file]

java -jar assembler.jar multiply.asm multiply.tape

Notice that I named the output file with an extension of “tape”. The output file is a binary file containing the machine language instructions generated from the assembly language statements in the input file. However, the output file represents data stored on punched paper tape, a storage technology available in the era when Harry Porter’s computer would have been considered cutting-edge technology.

Printer

The Main Memory Unit is organized as 32K bytes, addressed from hex 0000 through 7FFF; however, 64K is potentially addressable. With 32K’s worth of addresses unused, I decided to reserve 2 addresses to support printing characters. Printing a character is a 2 step process. First, to prepare the printer for new data, write any byte to address FFFE. Then, to print a character, write the character’s value to address FFFF. For example, to print a character stored in register B, do this:

SET M FFFEh 
STO B
SET M FFFFh
STO B

Here’s a program containing a subroutine that will print null-terminated strings:

# HELLO WORLD PROGRAM

SET M HELLO_WORLD
CAL PRINT_STRING
HLT


# PRINT STRING FUNCTION
# SET M TO ADDRESS OF NULL TERMINATED STRING.
# MODIFIES A, B, C, D, XY, M, J
PRINT_STRING:

# STORE XY IN C,D FOR RET
MOV C X
MOV D Y
PRINT_STRING_LOOP:

# B = STRING CHARACTER
LD B                          

# IF B == 0 GOTO PRINT_STRING_DONE
SHL A
BZ PRINT_STRING_DONE

# XY = M
MOV XY M

# PRINT STRING CHARACTER IN B
SET M FFFEh 
STO B
SET M FFFFh
STO B

# M = XY + 1
INC XY
MOV M1 X
MOV M2 Y 

JMP PRINT_STRING_LOOP

# RESTORE XY FROM C,D FOR RET
PRINT_STRING_DONE:
MOV X C
MOV Y D
RET

HELLO_WORLD:
STRING "Hello World!"
DATA 0D 0A 00

Emulator

After writing the assembler, I created an emulator to test out the compiled programs. The emulator runs until the HLT instruction is encountered. Before terminating, it displays the values in the registers. Prior to HLT, nothing is display unless you use the aforementioned printer. Here’s what happens when you run the hello world program:

java -jar assembler.jar helloworld.asm helloworld.tape

java -jar emulator.jar
Args: [input file]

java -jar emulator.jar helloworld.tape
Hello World!
 A: 00000000 00 0 0
 B: 00000000 00 0 0
 C: 00000000 00 0 0
 D: 00000110 06 6 6
 J: 0000000000011101 001D 29 29
 M: 0000000000101110 002E 46 46
PC: 0000000000000000 0000 0 0
XY: 0000000000000110 0006 6 6

The register values are displayed in 4 ways: binary, hexadecimal, unsigned decimal and signed decimal. In this case, none of the values were negative.

Simulator

To construct the Tofu simulation of Harry Porter’s computer, I needed to build a few components in Java. First, to display the registers, I created bulb components that accept 8-bit and 16-bit values. Here’s the first 2 lines of harryporter.config:

tofu.harryporter.Bulb8 bulb8 <init> i7 i6 i5 i4 i3 i2 i1 i0
tofu.harryporter.Bulb16 bulb16 <init> i15 i14 i13 i12 i11 i10 i9 i8 i7 i6 i5 i4 i3 i2 i1 i0

The update() method of each bulb generates a string to be displayed reflecting the value across the input terminals. Those strings are only printed when the HLT instruction is encountered. The HLT instruction actually signals another Java component:

tofu.harryporter.Halter halter input

When its single input terminal is set to 1, the Halter’s update() method prints the bulb strings and then it calls System.exit() to terminate the simulator.

The 32K memory component acts like an addressable latch. It’s also responsible for handling the 2 addresses involved in printing.

tofu.harryporter.Memory memory select load d7 d6 d5 d4 d3 d2 d1 d0 a15 a14 a13 a12 a11 a10 a9 a8 a7 a6 a5 a4 a3 a2 a1 a0

Finally, to read in the punched paper tape programs, I needed a tape reader:

tofu.harryporter.TapeReader tapereader <init> select advance d7 d6 d5 d4 d3 d2 d1 d0 end

The initialization parameter is the file name representing the paper tape to read. Unfortunately, this means to point the simulated computer to a different program, you need to edit the Tofu source file and alter this parameter. The 8 D terminals connect the tape reader to the data bus. When the select terminal is asserted, a channel opens between the data bus and the tape reader. The process of reading a paper tape begins by asserting the advance signal. When the advance signal is raised from 0 to 1, the tape reader internally advances to the next byte on the paper tape. When the advance signal drops back to 0, the paper tape byte is maintained across the D terminals. Or, if it reached the end of the tape, the end signal will be asserted instead.

With those components, I constructed the Tofu file, which you can view online here: harryporter.tofu.

If you point the tape reader to the hello world program described above, you can slowly watch it print out “Hello World!” character-by-character:

java -cp tofu.jar;harryporter.jar tofu.Main harryporter.tofu harryporter.config
Build complete: 1092 components, 1592 nodes.
Hello World!
 A: 00000000 00 0 0
 B: 00000000 00 0 0
 C: 00000000 00 0 0
 D: 00000110 06 6 6
 J: 0000000000011101 001D 29 29
 M: 0000000000101110 002E 46 46
PC: 0000000000000000 0000 0 0
XY: 0000000000000110 0006 6 6

At this time, you might be asking yourself, if Harry Porter’s computer required 415 relays, then why does this simulation of it use 1,092 components?! That’s a good question. First, Harry Porter used four-pole relays, which means he used the equivalent of somewhere between 415 and 1,660 single-pole relays, the type used by Tofu. Second, I needed extra states in my state machine to load programs from tape. The actual Harry Porter computer requires the operator to tediously key in each byte of the program into main memory. Finally, Harry Porter certainly optimized his design to use the lowest number of relays possible while I all I cared about was getting the thing working since I never built anything like this before.

So, what can this puppy do? In the zip file above, you’ll find several sample programs including the examples from Harry Porter’s webpage translated into my assembly language. The most interesting example is probably dragoncurve.asm which prints out the Dragon Curve Fractal.

The TOFU Circuit Simulator
dragon.pdf

Updated: February 3, 2007. Copyright 2007 Michael Birken. All rights reserved.