Small bytes: Program counter

Small bytes: Program counter

One of the current projects I’m tinkering with is an 8-bit computer. It’s something I’ve wanted to build for several years, and now that I’ve finished so many other projects, I finally have the bandwidth to take this on. I’m aware that Ben Eater has done this, and I’m not intending to compete with his amazing website and tutorial. My goal is to do this myself based on the things I’ve learned teaching microcontrollers and digital systems for the past 8 years. I also hope to learn new things as I make design decisions (and I’m sure, plenty of mistakes) along the way.

The final project is going to be quite large and take me a while to complete, but in the meantime as I complete a subset of the computer I’d like to write a smaller post about it.

I’m going to point out front and center that the vast majority of the microcontroller and assembly knowledge I have is based on the AVR 8-bit architecture. Therefore, you may notice lots of similarities with the AVR instruction set.

What is a program counter?

A program counter is the part of any processor that keeps track of the next address in program memory to point to. A very simple computer program with a few instructions and no branching or jumping logic would start by pointing to address 0. That address in program memory would be asserted, causing that program instruction to be entered into an instruction register. Then the instruction decoder will determine what to do during that cycle. At the end of the instruction cycle, the program counter will increment to 1. This process will repeat until the final instruction is concluded. So at a very basic level, a program counter is simply a counter.

However, a sophisticated computer or microcontroller is capable of executing branching and jumping instructions. When I say branch, I mean that an offset will be added to the current address and then loaded into the register containing the next address. When I say jump, I mean that a particular location can be loaded into the register, regardless of the current address.

The program counter I designed is capable of incrementing, doing conditional branches (based on four conditions, described below), and doing a jump.

Design decisions

Considering this was the first module I built, I’m not terribly surprised it made me consider lots of aspects of my design and make quite a few decisions, and change a few others that I’d thought were mostly settled.

Instruction size

I was initially planning on designing a pretty basic computer without any conditional instructions. Then I realized that it wouldn’t actually be that onerous to design in branching and jumping instructions, and it wouldn’t complicate my instruction set too much. One of my non-electronic hobbies is sewing, and my mother always told me that whenever I’m not sure if I should spend an extra 5 (or 10, or 60) minutes fixing or improving a small thing in a big project (such as a quilt), then I should consider that the finished project is going to be something I have to look at forever. So fix or improve the small thing now and feel better about the project forever. So I decided to use 16-bit instructions, make my instruction set more robust, and design around that now. This way my computer can be capable of doing more things when and if I decide to actually use it in the future.

Program counter size

Because I settled on 16-bit instructions, I had to consider where I’ll be storing my program memory, most likely the 27C1024 (link to datasheet PDF) EPROM. This has a 16-bit address space (65,536 addresses) so I could theoretically design a 16-bit program counter. Two things bugged me about that.

First, I wondered what would happen if I wrote the most ambitious code ever that used all 65,536 instructions, and then my program counter incremented back to 0 again. I decided to use an instruction of 0xFFFF (the default, unprogrammed value stored in an EPROM chip) to indicate an “end of program” instruction that will inhibit the clock and stop everything. If I limit myself to 12 bits of addressable memory, then I can guarantee I will eventually reach an end of program instruction and have the clock stop without the program counter going out of bounds of memory space.

Second, I considered that all of the hardware I’m using is based on 4-bits. The MUX chip I’m using is a quad chip. The adder I’m using is a 4-bit adder. The register I decided to use is a 4-bit register. So using all 16 bits of program memory is going to require an annoyingly large footprint. 12 bits seems like a good tradeoff between spending the extra time now to make this design more robust and not hating my life when I build it.

Memory address register

When I was making my decisions about how many bits to make the program counter, I considered what I’d use to store the memory address. Initially when I had considered 8-bit everything, I had settled on the 74194 (link to datasheet PDF). Then when I considered 16 bits for the program counter, I thought about using the 74374 (link to datasheet PDF), as I was seduced by the adjective “octal.” Even when I settled on 12 bits, the octal flip-flop seemed like a good choice for the design. I don’t need to shift the contents of the memory address register, nor do I really need mode select logic, as I will only want to parallel load when the clock ticks.

When I first built the prototype with the 74374, I had issues with the circuit functioning the way I wanted it to. (Translation: it didn’t work.) One issue was that I couldn’t clear the memory address register to get a clean slate for performing I/O tests. There’s no asynchronous clear on the 74374! I decided that the asynchronous clear on the 74194 made up for the increase in footprint size and decided to use it anyway. I went nuclear (IYKYK) and thankfully the design worked perfectly with the 74194 chip.

Instruction set

When coming up with conditional logic for the branch and jump multiplexers, I had to consider what my instruction set would be. I’d already decided on a 16-bit instruction, and had already made some block diagrams and preliminary schematics for some of the ALU instructions. All of my ALU instructions have B0 (the most significant instruction bit) equal to zero. So my I/O, control flow, and miscellaneous instructions have B0 equal to one. This artifact is totally arbitrary and based on my very preliminary thoughts of having a 4-bit opcode and 16 instructions, and the ALU instructions were the first that came to mind, hence having MSBs of zero. (I now have 22 instructions and a non-fixed-width opcode.)

All of the branch instructions and the jump instructions start with 11, which will differentiate them from any of my I/O instructions (which start with 10). At some point when I figure out how to build any appropriate decoding logic for those instructions, I’m sure I’ll be utilizing that fact to prevent branch/jump instructions from executing I/O operations. (Similarly, the MSB of 1 will prevent the ALU instructions from being executed.)

Branch instructions

The four branch conditions I decided on are:

  • BREQ: branch if equal, the branch will only execute if the zero flag in the status register is equal to one,
  • BRNE: branch if not equal, the branch will only execute if the zero flag in the status register is equal to zero,
  • BRSH: branch if same or higher, the branch will only execute if the carry flag in the status register is equal to zero, and
  • BRLO: branch if lower, the branch will only execute if the carry flag in the status register is equal to one.

Note that these are all unsigned branches. Will I regret that later? Maybe! But for now it certainly makes my instruction set smaller and far less complicated. Each branch instruction starts with B0B1B2 = 111. Then the next two bits (B3B4) dictate which branch (EQ = 00, NE = 01, SH = 11, LO = 10), these binary values were determined by considering how to use conditional logic with a MUX and an XOR gate to enable branching, as shown in the schematic below. Then B5 is equal to zero to differentiate these instructions from NOP or END instructions. The remaining 10 bits will store the branch offset (which I’ll sometimes refer to as k). (Why 10 bits? For one thing, to prevent overflowing program memory space. For another thing, that’s all the bits I had left.)

Note that a branch can only be used to increase the value in the memory address register, and not decrease it. This is an issue I will have to work out in any software I write. I’m actually only realizing this as I write this post, and my brain hasn’t had sufficient time to work through possible implications of this yet. While jump instructions can be used to go backward, they are not conditional. This may be something I need to reconsider as I continue with my design. (I think it will be OK, as I can always conditionally branch to a jump instruction. This may or may not keep me up at night as this conundrum occupies a small area of my brain’s back burner.)

Jump instruction

There’s only one jump instruction. I need to make sure that it’s different enough from the branches to prevent it from triggering a branch, while also making sure it cannot be confused for a NOP or END instruction. I settled with the first four bits B0B1B2B3 = 1100. Then the remaining 12 bits will store the jump target (which I’ll sometimes refer to as k).

Because k is a 12-bit value, a jump instruction will be capable of going anywhere in program memory.

Schematic

Below is a schematic of the working program counter. All inputs starting with B and then a number are bits of the program instruction and will eventually be connected from the instruction register.

Schematic of the program counter, which is described in the following paragraphs.

The schematic looks a lot scarier than it is. The MUX in the upper left differentiates between a branch (+k) and simple increment (+1). This controls one of the inputs of the adder. If the control bit is 0, then the A input on the adder is 1, and the program counter will increment by one. If the control bit is 1, then the A input will increment by k, the value stored in the branch offset. The branch offset input is connected in my prototype to a DIP switch containing the program instruction. In the final design it’ll be connected to the 10 least-significant instruction register output bits.

Here’s a truth table of that MUX control bit. (It’s not exhaustive, considering there are 8 inputs and therefore 256 different possible outputs. Most of the inputs are irrelevant.) A zero output means the program counter will increment by 1 or jump. A one output means the program counter will increment by k. The first row in the truth table is meant to represent all of the combinations where B0B1B2B5 does not equal to 1110 (all of the non-branch instructions).

B0B1B2B5B3B4ZCout
xxxx0
1110000x0
1110001x1
1110010x1
1110011x0
111010x00
111010x11
111011x01
111011x10

To be honest, that’s the part of the circuit I’m proudest of, as I considered it to be a clever use of conditional logic to make a branch happen only when those particular inputs occurred.

The second input to the adder is the current address stored in the memory address register. Therefore the branching logic adds an offset rather than going to an absolute location.

The sum of the adder is one of the inputs to another multiplexer. The other MUX input is the jump target (k). If the instruction starts with 110 (as expected for a jump instruction), then the memory address register will load k into the register when the program counter increment (PC.INC) control signal is received. Otherwise, it will load the sum of the adder into the memory address register.

Hardware

As mentioned, the memory address register uses the 74194 chip. Other hardware includes (each link goes to the datasheet PDF):

  • Multiplexers: 74157 (quad 2-to-1 MUX), this was useful as each of the four MUXes on the chip shares a control bit,
  • AND gates: 7411 (triple 3-input AND), this saved me lots of hassle due to the relatively large fan-in of each AND gate, and
  • Adder: 74283 (4-bit fast adder with carry), connected together to create a 12-bit adder.

Working circuit

The video below demonstrates the circuit as built on a breadboard. I know there’s a ratsnest of wires that makes things hard to see. On the left is a DIP switch representing the 16-bit instruction. All the way to the right is a set of bar LEDs that light up with the current value of the memory address register. I demonstrate regular increments, a branch, a jump, and a reset.

Future changes

When this is finally integrated into the rest of the computer, some changes will need to be made.

The DIP switch will need to be replaced with connections to the instruction register. For now, a DIP switch is a great way to prototype and work out that my design works.

The output of the memory address register will go to EPROM address pins instead of a set of LEDs. (I envision using a ZIF socket to house the EPROM so a program can easily be swapped out, unlike a regular socket which may damage pins with repeated insertions and removals.)

The toggle switches currently used to represent the values of the Z and C flags will eventually connect to the status register.

The pushbutton used to increment the memory address register will eventually be replaced by a control signal output by the finite state machine.

The pushbutton used to clear the memory address register may or may not be replaced by a control signal, or be part of conditional logic including a control signal. I’m not sure yet.

What’s next?

I don’t think this project lends itself to a simple answer of: what part of this design should I do next? Feedback circuits are all an ouroboros, so there’s no real beginning or end. I’d like to do another “easy” component, so it’s likely I’ll deal with ALU instruction decoding next. But at some point I’m going to need to knuckle down and build the finite state machine. But do I build the FSM before I have a good feel for how all of my instruction decoding hardware works? However, if I don’t build the FSM, can I truly feel confident about my instruction decoding hardware?

While I’m not entirely sure what part of the computer I’ll work on next, I’m looking forward to seeing how everything turns out over time! I only hope my office doesn’t get completely overtaken by breadboards filled with computer modules in the meantime.