In Part 1 of this mega-mini-series, I introduced Joe Farr and we revisited the concept of the Heath Robinson Rube Goldberg (HRRG) 4-bit computer, which has been on the drawing board for longer than I care to remember. In Part 2, I introduced Nils van den Heuvel and we discussed how the registers in the HRRG’s CPU have evolved over the years (you may wish to bounce over to Part 2 and refresh your mind regarding these registers before proceeding further).
The next thing for us to consider is the HRRG’s instruction set, which has also seen more than a few changes since it was first conceived deep in the mists of time.
The HRRG’s Latest and Greatest Instruction Set
The first thing to remember is that we wanted to be able to use a single 4-bit nybble to specify which instruction we were talking about. Obviously, this limited us to only 16 instructions. There’s a lot to wrap one’s brain around here, and I don’t intend to go into too much detail, but there are a couple of points that are well worth noting.
First, we decided to implement an ADDC (“add with carry”) instruction rather than an ADD (“add without carry”) because these are necessary to implement multi-nybble operations, and it’s a lot easier to make an ADDC work like an ADD by setting the carry flag to 0 before performing the operation than it is to make an ADD work like an ADDC. Similarly, we opted for a SUBB (“subtract with borrow”) rather than a SUB (“subtract without borrow”) because it’s easier to make a SUBB work like a SUB by setting the carry flag to 1 before performing the operation than it is to make an SUB work like a SUBB.
The same thing applies to the ROLC (“rotate left through carry”) and RORC (“rotate right through carry”). It’s easier to use these as starting points to implement alternative rotate and shift operations than it is to go the other way round.
As we see in the illustration below, the instruction opcode is followed by additional nybble operands that are used to specify the
Originally, we had opted for a
One thing that’s worth emphasizing is that both the source and target can be a CPU register or a memory location. For example, we can add a nybble in a CPU register to a nybble in memory and store the result in the register or in the memory depending on which we define as the source and which we define as the target.
- #1 If the source is a 4-bit register or a 12-bit register, then there won’t be a
<src-aop> (additional operand).
- #2 If the source is a memory location (as indicated using the MD, MI, MX, or MR virtual registers), then the
<src-aop> will be a 3-nybble address.
- #3 If the target is a 4-bit or 12-bit register, then there won’t be a
<tar-aop> (additional operand).
- #4 If the target is a memory location (as indicated using the MD, MI, MX, or MR virtual registers), then the
<tar-aop> will be a 3-nybble address.
- #5 If the source is a 4-bit quantity (a CV4 or a 4-bit register), the target will typically be a 4-bit register or memory location. If the source is a 4-bit register and the target is one of the 12-bit registers (PC, SP, IX, GP), then the contents of the 4-bit register will be moved (copied) into the least-significant nybble (LSN) of the target register. If the source is a 12-bit register, the target will typically be a 12-bit register or a memory location (see note #6); if the source is a 12-bit register and the target is a 4-bit register, then the contents of the LSN of the 12-bit register will be copied into the 4-bit register.
- #6 If the contents of a 12-bit register are copied into memory, the target operand will be the least-significant address of a 3-nybble field. If the contents of memory are copied into a 12-bit register, the source operand will be the least-significant address of a 3-nybble field.
- #7 If the source is a 4-bit quantity (a CV4, a 4-bit register, or a memory location), then a 1-nybble value will be pushed onto the stack and SP = SP + 1. If the source is a 12-bit quantity (a CV12 or a 12-bit register), then a 3-nybble value will be pushed onto the stack and SP = SP + 3. (The stack pointer will be incremented after the PUSH.)
- #8 If the target is a 4-bit register or a memory location, then SP = SP – 1 and a 1-nybble value will be popped off the stack (the stack pointer will be decremented before the POP). If the target is a 12-bit register, then a 3-nybble value will be popped off the stack (ending up with SP = SP – 3).
- #9 There are no RTS (“return from subroutine”) or RTI (“return from interrupt”) instructions – the same effect is achieved by using a POP instruction to retrieve the return address off the stack and load it into the PC.
- #10 The JMP and JSR opcodes (instructions) are followed by a control nybble, a
<src> nybble, and (possibly) a <src-aop> in the form of a 3-nybble address. The control nybble is used to perform unconditional jumps or conditional jumps. The least-significant three bits of the control nibble point to the bit to be tested in the 8-bit status register formed from S1 and S0; this will be in the range 000 to 111 (0 to 7). Assuming a 0 in the most-significant bit of the control nibble, then a value of 1 in the selected status bit will cause a jump; for example, JMP %0001 is equivalent to a “Jump if Zero”. By comparison, a 1 in the most-significant bit of the control nibble will invert the operation of the jump; for example, JMP %1001 is equivalent to a “Jump if Not Zero”. Note that status bit 7 [bit 3 in S1] is a hard-wired 1, which is equivalent to an unconditional jump.
- #11 As part of its execution, the JSR instruction will cause the return address (the 3-nybble address of the next opcode) to be automatically pushed onto the top of the stack and SP = SP + 3. Similarly, when an interrupt occurs (assuming the interrupt status flag is enabled), the CPU will cause the return address (the 3-nybble address of the next opcode) to be automatically pushed onto the top of the stack and SP = SP + 3 prior to handing control over to the interrupt service routine (ISR), where the interrupt vector (IV) pointing to the first instruction in the ISR is stored in the three most-significant nybbles of the main memory (addresses $FFD, $FFE, and $FFF).
What’s Your Status?
Before we proceed, we really need to consider the HRRG’s status register, which is where we find the status bits (or flags) that reflect the results from an operation. In the case of the HRRG, we have two 4-bit status registers in the CPU that we call S0 and S1.
Since the HRRG has only 16 instructions, we don’t have any special instructions dedicated to clearing (to 0) or setting (to 1) the status bits. However, we can achieve the same effect using AND and OR operations. For example, if we wanted to clear the C flag, we could do so using the following instruction:
AND %1011, S0
The corresponding machine code would be as follows:
$7 $A $B $4
Where $7 specifies the AND operation, $A says the source (the
As a somewhat related point, we don’t have special instructions to enable or disable interrupts. However, we can achieve the same effect by setting or clearing bit 0 in status register S1.
Most processors have an unconditional JMP (“jump”) instruction along with a suite of jumps based on the state of the status flags, like JZ (“jump if zero”), JNZ (“jump if not zero”), JN (“jump if negative”), JNN (“jump if not negative”), and so forth. We do things a little bit differently because we only have a JMP instruction. However, as discussed in point #10, this instruction is followed by a control nybble, which is used to perform both unconditional and conditional jumps. Also, even though we have only a JMP instruction at the CPU level, we can have JMP, JZ, HNZ, JN, JNN... etc. mnemonics in our assembly language because our assembler can translate these into the appropriate control codes.
Also of interest is the fact that our JSR (“jump to subroutine”) instruction has the same capabilities with regard to jumping depending on the values of status bits as does our JMP instruction.
And, as one more related point, we don’t have special “return from subroutine” or “return from interrupt” instructions. In this case, the same effect is achieved by popping the return address off the top of the stack into the PC.
When Instructions and Status Bits Collide
The following table illustrates which of the O (Overflow), C (Carry), Z (Zero) and N (Negative) status bits are affected by the various instructions.
- #1 As part of rotating the target value one bit to the left, the most-significant bit (MSB) of the target value is copied into the C flag; at the same time, the original contents of the C flag are copied into the least-significant bit (LSB) of the target value.
- #2 As part of rotating the target value one bit to the right, the LSB of the target value is copied into the C (carry) flag; at the same time, the original contents of the C flag are copied into the MSB of the target value.
- #3 The Z flag is set to 1 if the values being compared are equal, otherwise it’s cleared to 0.
- #4 The values being compared are considered to be 4-bit unsigned integers. The C flag is set to 1 if the value in the
<src1> is greater than the value in <src2>; otherwise, it’s cleared to 0.
- #5 If the target is status register S1, then its flags will be loaded from the stack and the contents of S0 will not be affected; if the target is S0, then its flags will be loaded from the stack; if the target is any other 4-bit location (register or memory), then the N and Z flags will behave as usual.
- #6 These flags work as usual for a 4-bit target with one exception — if the target is one of the status registers S0 or S1, then the flags in S0 will not be automatically updated; if the source is a 12-bit register and the target is a 4-bit register, then the Z and N flags will be based on the contents of the least-significant nybble of the 12-bit register (i.e., the only bits to be copied); if the source is a 12-bit register and the target is memory, then Z flag will be set based on the contents of the entire 12-bit register and the N flag will be set based on the contents of the MSB of the most-significant nybble (MSN) of the 12-bit register.
- #7 The Z flag will be set based on the contents of the entire 12-bit register and the N flag will be set based on the contents of the MSB of the MSN of the 12-bit register.
A Matrix of Possibilities
Given the fact that we have a 4-bit data bus, a 12-bit memory space, and both 4-bit and 12-bit CPU registers, things can become a tad confusing when transferring data back and forth between the 4-bit and 12-bit domains.
In the early days, Joe and I tried to tie the various possibilities down and capture our thoughts in the form of notes, but we were never 100% sure that we’d covered every conceivable combination and corner case. You can only imagine our chagrin when Nils came onboard and whipped up the following handy-dandy source-to-target data width mappings graphic.
As soon as I saw this chart, I thought (a) “That’s brilliant” and (b) “Why on Earth didn’t I think of that myself?”
So, that’s the current state of play. You are now up to date with where we are at with respect to the HRRG 4-bit computer. Nils is working away furiously on his gate/register-level FPGA-based implementation, about which I will blog further in the future. In the meantime, as always, I welcome your comments, questions, and suggestions.