This is an extension to Ben Eater's 8-bit breadboard computer project. I have immensely enjoyed following Ben's videos on how to build a breadboard computer and building my own version of it. If you have not been following along with Ben then I highly recommend watching his videos before reading - this project will make very little sense without the context.
After building the computer it quickly became clear that if I had to manually toggle in a program via DIP switches every time I turned it on then it would not see much use at all. Even toggling in just 16 bytes over and over again gets old very fast. Also I was planning to extend the memory to 256 bytes and that was only going to be useful if somehow programs could be loaded and saved.
What can I do with it?- Save a program from breadboard computer RAM
- Load a program into the breadboard computer RAM
- Auto-run a saved program when the breadboard computer starts up
- Inspect and modify memory contents via a serial connection
- Program the breadboard computer in assembly language
- Disassemble memory contents
- Single-step instruction by instruction
- Set breakpoints and run up to breakpoint
Here's a video demonstrating these features. Please excuse the video quality - I'm by no means a professional video content creator:
How does it work?I had a few goals in mind while making this project:
- As little as possible hardware required to build it
- As few changes as possible to the breadboard computer itself
In the beginning I though about using an EEPROM to store programs and then some kind of logic to transfer programs from the EEPROM to the breadboard computer's RAM. However, coming up with the transfer logic proved to be more complicated than I could handle (I'm more of a software guy even though I really enjoy working close to hardware). I happen to be a big Arduino fan so at some point I started thinking of replacing the transfer logic with an Arduino. It took some time to convince myself that using an Arduino was not cheating (after all even the Arduino UNO is much more powerful than the breadboard computer itself) but I'm happy with the result so I have made my peace.
So what does the Arduino need to be able to do? Well, it needs to be able to read data from memory and write data to memory. The easiest and least intrusive (to the breadboard computer) way to do that is to use the same interface that the memory module already has: the bus and control signals. The Arduino digital I/O pins are bi-directional, so connecting 8 of them directly to the bus allows the Arduino to read from and write to the bus. To make the RAM module read from or write to the bus all that is necessary is to set the MI/RI/RO signals accordingly. Now those signals are usually driven by the control logic's EEPROMs so having the Arduino control them too would lead to conflicts and possible short-circuit situations. However, the AT28C16 EEPROMS used by Ben have a Chip Enable (CE) input that puts all data outputs into a high-z state - which then allows the Arduino to manipulate the signals. To read out the RAM, the Arduino needs to do the following:
- set the EEPROM's CE signal high (i.e. disable the control logic)
- output the first address to be read onto the bus
- set the MI signal high and wait for a clock low-high transition
- set MI low and RO high and wait for a clock low-high transition
- read the data byte from the bus
- repeat from step 2 for all 16 addresses
- set the EEPROM's CE signal low (i.e. re-enable the control logic)
And that's it. Writing RAM contents is very similar, the Arduino just needs to write to the bus and set RI high instead of RO. There are of course some technical issues to be solved but the above basic mechanism is the heart of this project.
What changes do I need to make to the breadboard computer?There are two changes that need to be made:
- Put pull-down resistors on the control logic EEPROM's data outputs
- Disable (or continually reset) the ring counter while the EEPROM is disabled
Pull-down resistors
The pull-down resistors are required because once the EEPROM is disabled, the pull-up resistors of the logic gates that the control signals (such as AO/AI/EO...) are connected to will pull those signals high. That would mean that multiple registers would be writing to the bus, causing conflicts.
The pull-up resistors on the inputs of 74LS gates are around 10k. So the pull-down resistors need to be small enough to get the voltage down into low territory. For most of the signal lines I used 3.3 kOhm resistors. However, there are two exceptions: first, the "SU" signal is connected to 8 exclusive OR gates, meaning that the effective pull-up resistor is 10kOhm/8 = 1.25kOhm. A pull-down resistor would have to be significantly less than 1k to pull this low. Luckily, the SU (subtract) signal does not control any interaction with the bus so we can just ignore it and not have a pull-down resistor. Secondly, the CE (counter enable) needed a 1k pull-down resistor - larger values caused random program counter behavior in some cases.
I found it easiest to add the pull-down resistors on the breadboard that holds all the blue LEDs, i.e. between the LED anodes (which are connected to the EEPROM outputs) and GND.
[I couldn't fit the resistors in here for the HLT/MI/RI signals so I added those at other locations on the breadboard]
Ring counter reset
The other modification is resetting the ring counter. Technically this is not really necessary to allow saving/loading of programs but it allows for a smooth transfer of control from the Arduino back to the control logic. The point is to keep the ring counter at 0 as long as CE is high (i.e. the control logic is disabled). When the Arduino switches CE back to low (enabling the control logic), the ring counter will be at zero and the breadboard computer starts executing the next instruction.
For my build I didn't need to do anything for this because I am using one EEPROM output to reset the ring counter. This improves performance by resetting the ring counter as soon as an instruction is finished. It also automatically gives me the ring counter reset when the control logic EEPROM gets disabled: the pull-down resistor on the EEPROM's output will pull the signal low which resets the ring counter.
If you're using Ben's implementation of a fixed 5-step ring counter, I think the following extension to his reset circuit should reset the counter when if CE is high (click the left/right arrows below to switch between Ben's original circuit and the extended version):
As you can see it requires 3 more NAND gates, i.e. one more 74LS00 chip. Note that I have not tested this approach but it should work as far as I can see.
This modification is not absolutely necessary - you can leave it out in the beginning. Loading and saving as well as the monitor/assembler/disassembler will still work just fine. However, any action that requires transfer of control from the Arduino to the control logic will not work. Most notably that is auto-running saved programs at startup as well as single-stepping and running in the debugger.
How do I set up the Arduino?Upload the sketch from the GIT archive to the Arduino and connect the Arduino to the breadboard computer as follows:
- Arduino 5V pin (not Vin!) to 5V rail on the breadboard
- Arduino GND pin to GND pin on the breadboard
- Arduino digital pins 2-9 to bus 0-7
- Arduino digital pin 10 to control logic EEPROM output that controls RO
- Arduino digital pin 11 to control logic EEPROM output that controls RI
- Arduino digital pin 12 to control logic EEPROM output that controls MI
- Arduino analog pin 0 to CLOCK signal
- Arduino analog pin 3 to CE (pin 18) of all control logic EEPROMs and via a 10k resistor to +5v
Apart from that you will need to wire the Arduino's analog 1 and analog 2 input pins to the DIP switches and push buttons as shown in the schematics (for more details see the attached Fritzing file).
For an absolute minimal (but still functional) version, you can do the following:
- Add 3.3kOhm pull-down resistors to the EEPROM output pins that control AO, CO, EO, IO, RO
- Skip the "Ring counter reset" instructions above
- Do the Arduino to breadboard computer wiring as shown above (you can leave out the 10k resistor in the last step if you want)
- Connect both analog pin 1 and analog pin 2 to GND
To use the load/save buttons, all you need to do is connect the buttons, DIP switches and associated resistors to analog pin 1 and 2 according to the schematics.
To use the autostart feature, the Arduino needs to keep the the program counter at and ring counter at 0 during reset and while the program is transferred. The pull-up resistor between analog pin 3 and +5V keeps the control logic disabled (and therefore the program counter at 0) while the Arduino resets. For the ring counter, follow the "Ring counter reset" instructions above.
How do I load and save programs?The minimal setup above will allow you to control the Arduino via the serial interface. To communicate with the Arduino you will need a terminal program such as Putty or TeraTerm. The serial monitor in the Arduino software will work too but the separation between input and output area in the serial monitor makes it a bit clunky in this scenario.
- Turn on the breadboard computer
- Connect a PC to the Arduino via the USB cable
- Start the terminal program and configure to 9600 baud, 8 bits, no parity, 1 stop bit
- Press ESC in the terminal window to enter the Monitor mode
- You should see a "." as a command prompt
- Type "h" and press enter to get a list of supported commands
With the minimal setup, you should be able to use the "m", "M", "C", "l" and "s" commands. These allow you to see memory contents, modify memory contents and load and save programs.
To save or load a program via the button:
- Turn off the breadboard computer's clock
- Set the DIP switches to select the file number under which the data should be saved.
- Press the "save" or "load" button. The LED connected to CE will come on, indicating that the Arduino has taken control
- Turn on the breadboard computer's clock. You should see the Arduino cycling through the addresses (watch the LEDs of the memory address register)
- Wait until the bus LEDs stop blinking and the memory address register's LEDs and show 1111
- Turn off the breadboard computer's clock. The LED connected to CE will turn off, indicating that control has been returned to the control logic
To automatically run a program at startup (make sure you have all the necessary circuitry in place), just set the DIP switches to the file number under which the program is saved and turn on the breadboard computer (or press the reset button). There are two special cases: If all DIP switches are off then the computer starts up regularly, without autostart. If all DIP switches are on then the Arduino goes into Monitor mode directly at startup.
How do I use the assembler and disassembler?To use the assembler/disassembler and debugger features you will first need to alter the program on the Arduino to conform to your specific setup. Find the section in the source code that defines the opcodes_4bit structure:
struct opcodes_struct opcodes_4bit [] =
{
{"NOP ", B00000000, 0, false},
{"LDA ", B00010000, 2, true},
...
{".OR ", B11111110, 0, true}, // set start address
{".BY ", B11111111, 0, true}, // define one byte of data
{NULL, 0, 0, false}
};
Each line specifies one opcode:
- First field is the mnemonic ("LDA"). For immediate addressing, include a "#" in the mnemonic. So what Ben calls "LDI" would be called "LDA #" here. Can you tell I grew up programming 6510 assembler on a C64?
- Second field is the opcode itself. The lower four bits should always be 0. (except for the special opcodes .OR and .BY, see below)
- Third field is the number of cycles that the opcode takes to execute (this is in addition to the fetch cycles). For example, in my implementation LDA has opcode 0001 and takes a total of four cycles to execute, two of which are the fetch cycle. If you followed Ben's instructions (where all opcodes use 5 cycles) then this should always be 3.
- Last field specifies whether this opcode requires an argument. For example LDA requires an argument but OUT does not.
You will need to adjust this list to reflect the opcodes you have implemented for your breadboard computer. The last two lines are special opcodes used by the assembler and should be left as they are.
After entering all your opcodes, upload the software to the Arduino. Connect your terminal and go into Monitor mode (either by pressing ESC in the terminal window or by setting all DIP switches to on). You should now be able to disassemble your program. Typing just "d" in the monitor will start disassembling at address 0.
The assembler is minimal but works pretty well. Type "a" to start assembling at address 0. Conventions are:
- If a line starts does not start with a whitespace then it must start with a label definition. Labels must start with an alphabetic character followed by alphanumeric characters, can be at most 3 characters long and are case sensitive.
- After the first whitespace in a line, the assembler expects a mnemonic (LDA, STA, OUT...).
- Special mnemonic ".BY" directly specifies a data byte to be stored at the current location.
- Special mnemonic ".OR" tells the assembler to continue assembling at a new address.
- If an argument starts with a alphabetic character then it is assumed to be a label.
- Any numeric argument is expected to be a decimal number. To specify hexadecimal, precede the argument with a "$". For example, to load hexadecimal FF number into the A register, use "LDA #$FF".
- Anything after a ";" is assumed to be a comment and ignored.
For example, the Fibonacci code can be entered as follows:
rst LDA #0 ; x = 0
STA x
LDA #1 ; y = 1
STA y
lp ADD x ; z = y + x
STA z
JC rst ; restart if overflow
OUT ; print z
LDA y ; x = y
STA x
LDA z ; y = z
STA y
JMP lp ; loop
x .BY 0
y .BY 0
z .BY 0
What are the limitations?To save RAM space on the Arduino, the assembler works as a 1-pass assembler (otherwise the Arduino would have to buffer all the source code). The assembler writes the opcodes to the breadboard computer's memory as they are entered. This means that assembling is slowed down by the clock speed of the breadboard computer. If you copy-and-paste text into the terminal window, that can lead to lost characters as the Arduino can't keep up with characters coming in at 9600 baud (because it is spending too much time waiting for the breadboard computer's clock). To work around that either reduce the baud rate or use TeraTerm which provides a setting to specify a delay between characters sent. The other workaround is to increase the clock speed on the breadboard computer. My clock goes up to 160kHz and at that speed I can copy-and-paste code at 9600 baud without problems.
In its default configuration the Arduino sketch can handle clock frequencies on the breadboard computer of up to about 1-2kHz (maybe a bit more). Note that Ben's clock in its default configuration does not go faster than 500Hz. If your clock is faster, look for the #ifdef FAST_IO
switch in the code. Turning on FAST_IO should make the Arduino work with clock speeds of up to 250kHz. I have tested it up to 160kHz. It would probably be possible to support higher speeds by implementing the time-critical loops directly in assembler but honestly a 160kHz clock speed already feels too fast on the breadboard computer with its otherwise limited capabilities. Make sure to read the corresponding comments in the code before turning on FAST_IO.
The Arduino has 1k of EEPROM and therefore can hold 1024/16=64 different programs. Actually it is 63 since 16 bytes are reserved to save configuration data. That's not a lot but probably enough to hold all programs you can come up with. Only the programs number 0-15 of those can be selected via the dip switches (1-14 for autostart) but the "s" and "l" commands will work with the full 0-62 range.
It looks a bit messy. Can you neaten it up?Yes! In my final version here I actually just used the bare Atmega 328P chip (together with a 16MHz crystal and capacitors) instead of an Arduino UNO. The voltage regulator on the UNO is not necessary here since the Arduino is directly using the 5V from our power supply anyways. The only loss is that we now have to use a separate USB-to-serial converter (FTDI or similar) to talk to the Atmega. The overall look of that fits in much better with the rest of the breadboard computer:
Another optimization is to remove the bootloader from the Arduino/Atmega. That gets rid of the 2 second delay while the Arduino bootloader starts up. I will post instructions on how to do that if people are interested. Let me know in the comments!
Comments