My 8-bit "System on a Table" computer built from TTL logic chips.
This work is derived from the "JCPU" by James Bates, who did an excellent YouTube series on his work. You can find his design files and other materials here on GitHub.
Bates' work, in turn, was inspired by the Ben Eater breadboard computer.
Like Eater, Bates built his system on breadboards. The SoT8 will instead be built on a single PCB.
This is all still a work-in-progress, so keep checking back in for updates.
The SoT8 has several improvements over the Bates design, described here:
The Bates CPU places the opcode in bits 0-7 of the decode ROM address, and the microcode step in bits 8-10. The SoT8 rearranges this to place the microcode step in bits 0-2 and the opcode in bits 3-10. This places all of the micrcode steps for a given instruction sequentially in the decode ROM, which simplifies microcode generation and seems like a more natural way to organize the decode ROM.
The Bates CPU has a single PGM
signal to select program memory, and if
that signal is not asserted, then memory accesses hit data memory. I wanted
the SoT8 to be able to support additional address spaces (specifically,
an I/O space), so I replaced the single PGM
signal with a pair of signals,
AS0
and AS1
. These signals allow us to select one of 4 address spaces:
- D-space (data memory)
- I-space (program memory)
- S-space (a stack region)
- I/O-space (for external peripherals)
These signals are decoded by the Memory Module.
In order to support an IMMEDIATE addressing mode for more instructions (e.g. call), I have added an Immediate Value (IV) register to the Special Registers Module. When loading an immediate value from the instruction stream, the value is always stored in the IV register, in addition to being stored in another destination register as needed.
This adds _IVE
and _IVW
signals.
The Bates CPU implicitly uses Rc to hold the target of the call
instruction. The reason for this is because his call instruction is
encoded as "PUSH PC
", which is actually "ST [SPa], PC
", which consumes
both register slots in the opcode. This is inefficient because the
typical use of call is to call a function at a label. A label, of course,
is simply an immediate value, which means that the typical usage requires
2 instructions:
mov %r2, $some_label
call %r2
What I've done is removed the magic from "PUSH PC
". This allows the
atypical case to still be used in an open-coded manner:
push %pc
mov %pc, %r2
...but allows typical usage to be encoded in 2 bytes instead of 3, which is important when you only have 256 bytes of program space!
Our syntax is:
call $some_label
which uses the encoding "MOV SPa, PC
" plus an immediate value. This
does what you might expect: pushes the PC onto the stack and then moves
the immediate value into the program counter. This instruction uses the
IV register to hold the call destination while the push portion of
the instruction is being executed.
The call instruction is the slowest on the SoT8, using all 6 available microcode steps, but it's faster than the pattern enforced by the original Bates design.
The Bates CPU hard-codes the B-operand of the ALU to either 0 or Rb. This is largely because the opcode doesn't have room for both the ALU function and 2 registers. However, Bates baked this into the hardware; Rb has a back-door input into the ALU. I don't like this approach for a couple of reasons:
- It makes the Rb register different from all of the other General Purpose Registers, and I'd really rather that not be the case.
- It limits our ability to use the ALU for some other things (see below).
So, instead of the selector bank that selects 0 or Rb, I have added an
ALU_B register that is cleared at the end of every instruction (by
snooping for the assertion of _ucSR
). This provides the hard-coded 0
for instructions that require it. Then, instead of the ALB
signal
selecting Rb directly, we burn a microcode cycle copying Rb into the
ALU_B register (using the ALB
signal to enable writing to ALU_B).
This is slower than the Bates design, but has the advantage of making
Rb magical only in the microcode, and unlocks some other uses of the ALU
that we'll see below.
Side note: because this means that the ALU now has 2 internal registers (B-operand and the result register), I have renamed the result register to ALU_R.
The condition codes work somewhat differently on the SoT8 compared to
the Bates design. The CC register is not located in the ALU in this
design. Instead, the CC register resides in the Control Unit and can be
updated from two different places: the ALU (by asserting the ACC
signal)
or the processor bus (by asserting the BCC
signal). This allows us to
do a couple of useful things:
- Selectively update the flags from the ALU, in case we're using the ALU to perform some implicit operation (like computing an effective address).
- Update the Z and N flags during a "
LD
" or "MOV
" into one of the general purpose regsiters.
The latter allows us to reduce the following pattern:
ld %r0, #1[%sp]
tst %r0
jz $somewhere
to this:
ld %r0, #1[%sp]
jz $somewhere
Because this design has so much stack space and a more useful call
instruction, it seemed like a good idea to be able to pass function
arguments on the stack. But accessing them in the callee is really
tough without the ability to load from an offset relative to the stack
pointer. Happily, the ALU_B register makes it really easy for us
to this; all we need to do is find a pair of opcods that we can hijack
for it. Limiting these to the GPRs seems pretty reasonable, so these
are encoded as "LD SPa, Rx
" and "ST Rx, SPa
", followed by an immediate
value to use as the offset. Note that in this encoding, the source and
destination registers are swapped; that's the price we pay for only having
8 bits of opcode!
Since we can use the stack to pass arguments around, it is useful to be able to push an immediate value onto the stack directly:
push #0xff
..rather than having to do:
mov %r2, #0xff
push %r2
Now that we've pushed all those arguments onto the stack, we would like
to be able to pop them off without having to clobber a GPR or burn precious
program space. We encode this as "MOV SPa, IMM
" followed by an immediate
value.
spa #3 ; pop the 3 arguments
Loads from I-space are useful for e.g. loading tables in from ROM.
The Bates CPU did this by treating Rc as a magic register for loads
and stores. I wasn't willing to do that without a different opcode.
We hijack the "LD Rx, PC
" and "ST PC, Rx
" encodings to do this, and
use Ra as the enforced address register.
To support I/O space, INB and OUTB instructions are added to the LD and ST classes. These instructions all use an 8-bit immediate address. These instructions only allow I/O using the general purpose registers, thus saving us a few opcodes for future use.