TD4 4-bit DIY CPU – Part 6
Having now successfully built my own version of the TD4 4-bit CPU in Part 5, I’m now chewing over some of the ways I’d like to try to expand it.
- Part 1 – Introduction, Discussion and Analysis
- Part 2 – Building and Hardware
- Part 3 – Programming and Simple Programs
- Part 4 – Some hardware enhancements
- Part 5 – My own PCB version
- Part 6 – Replacing the ROM with a microcontroller
- Part 7 – Creating an Arduino “assembler” for the TD4
- Part 8 – Extending the address space to 5-bits and an Arduino ROM PCB
I already have a list of others extended projects at the end of Part 4, so I might be drawing on some of them for inspiration moving forward. Many of these are very similar projects, but with a completely different architecture. But really at this stage rather than build a different, more capable, 4-bit CPU from someone else’s design, I’m interested in seeing how far the TD4 design can go. So, ultimately, like all my projects, the fun here is in the reinventing and learning on the way.
One of the questions I have is can I replace the DIP switches with something that can provide the data in a better way? This would be particularly critical if I expand the address space in the future. A ROM is the obvious option, but something more dynamic might be an interesting experiment too.
This post looks at options for replacing the DIP switches with microcontrollers.
Now I feel like I really ought to state right up front that this is a pretty ludicrous thing to do.
At the more charitable end of the endeavor I’m using a 16MHz 8-bit AVR microcontroller with 2kB of RAM to serve up 16 8-bit values to a 10Hz 4-bit CPU.
At the most extreme end I’m using a 125MHz, dual-core, 32-bit ARM Cortex M0+ CPU with 264 kB of RAM running an entire interpreted programming environment requiring (probably) millions of lines of low-level code to implement it, to do the same thing.
So why bother? Well – why not?
TD4 without the ROM
To interface to a microcontroller, I’m after two things:
- Ability to read the 4 address lines.
- Ability to drive the 8 data lines.
The best place to get at these signals is on the interface to the ROM itself – the 74HC540 octal line driver, and 74HC154 4-to-8 line decoder.
Conveniently, these signals can be broken out quite easily on my board as shown below.
The pink shaded area shows which components are needed for a ROM-less build. The two yellow highlights show where headers should be soldered to permit access to the address lines (top) and data lines (bottom).
In this build, the following components are omitted from the full board:
- 74HC154
- 74HC540
- 16x 8-way DIP switches
- 128x small signal diodes
- 8x 10k pull-up resistors
I’ve used 6-way and 10-way pin header sockets to allow me to patch in a microcontroller. This allows for each header to conveniently include 5V and GND too. I’ve included the USB socket for power to the PCB but expect I’ll probably power the board via these 5V and GND links from the microcontroller.
Using Arduino
The natural choice here is to use one of the older Arduino boards, as these are all 5V IO which makes interfacing with the 4-bit CPU fairly straight forward.
Using Arduino direct PORTIO should also make it pretty trivial to read address lines and write the data. I’ve configured the connections as follows:
TD4 SignalArduino GPIOArduino PORTIOA0A0PORTC:0A1A1PORTC:1A2A2PORTC:2A3A3PORTC:3D0D8PORTB:0D1D9PORTB:1D2D10PORTB:2D3D11PORTB:3D4D4PORTD:4D5D5PORTD:5D6D6PORTD:6D7D7PORTD:7I’m avoiding D0/D1 (PORTD[0:1]) and D13 as they all have other hardware attached (serial port and LED in this case).
Accessing the data corresponding to any specific address is as simple as follows:
uint8_t ROM[16];loop:
unt8_t addr = PINC & 0x0F
PORTB = (PORTD & ~(0x0F)) | (ROM[addr] & 0x0F);
PORTD = (PORTD & ~(0xF0)) | (ROM[addr] & 0xF0);
The code could be simplified if I didn’t mind trashing whatever is configured for the other GPIO pins via the PORTIO, but it is good practice to preserve those values when only writing to a subset of the IO ports.
In the final code below, I’ve included a toggle for A5 which allows me to do some timing measurements too.
uint8_t ROM[16] = {0xB1, 0x01, 0xB2, 0x51,
0xB4, 0x01, 0xB8, 0x51,
0xB4, 0x01, 0xB2, 0x51,
0xF0, 0x00, 0x00, 0x00
};
void setup() {
DDRB |= 0x0F;
DDRD |= 0xF0;
DDRC |= 0x20;
}
int toggle;
void loop() {
if (toggle == 0) {
toggle = 1;
PORTC |= 0x20;
} else {
toggle = 0;
PORTC &= ~(0x20);
}
uint8_t addr = PINC & 0x0F;
PORTB = (PORTD & ~(0x0F)) | (ROM[addr] & 0x0F);
PORTD = (PORTD & ~(0xF0)) | (ROM[addr] & 0xF0);
}
Running the code in a loop like this gives a scan frequency of around 500kHz and a response time of something like 2-3 uS for each read. That seems pretty responsive and I’m sure will be fine for a 10Hz CPU. And it is – it works great!
Using Circuitpython
One thing that would be really nice is a workflow that allows more of a “direct save to the CPU” approach to programming it. One option is to use a more modern microcontroller that supports a filesystem.
The obvious choice here is a 32-bit microcontroller that supports Circuitpython. But will IO in Circuitpython be fast enough to respond to the CPU? There is one obvious way to find out – give it a try.
There is another complication too – most Circuitpython boards run at 3.3V not 5V so that needs to be addressed too.
Level Shifting
I’m going to use a 74LVC245. The Adafruit product page puts it best:
“essentially: connect VCC to your logic level you want to convert to (say 3.3V), Ground connects to Ground. Wire OE (output enable) to ground to enable the device and DIR (direction) to VCC. Then digital logic on the A pins up to 5V will appear on the B pins shifted down to the VCC logic.”
This is an 8-way bi-directional bus transceiver and should be powered by 3V3, then the direction pin will determine the direction of the conversion as shown[ below.
Two devices will be required. The address lines will need a 5V to 3V3 conversion; the data lines will need 3V3 o 5V.
Here is how I’ve wired these up for a Raspberry Pi Pico:
The Pico is connected as follows:
- INPUT: GPIO 10-13 = A0-A3
- OUTPUT: GPIO 2-9 = D7-D0 (not the ordering!)
CircuitPython ROM
The basic algorithm will be as follows:
ROM = [16 command byte values]LOOP:
Read four address lines
Set data lines from ROM[address]
For performance reasons it would be best to optimise both the reading of the address lines and the writing of the data lines, ideally into a single access. But as this is for a CPU that runs at a maximum of 10Hz, so for now, I’m just going with simple and see how it goes.
import boardimport digitalio
ROM = [
0xB1, 0x01, 0xB2, 0x51,
0xB4, 0x01, 0xB8, 0x51,
0xB4, 0x01, 0xB2, 0x51,
0xF0, 0x00, 0x00, 0x00
]
Tpin = digitalio.DigitalInOut(board.GP21)
Tpin.direction = digitalio.Direction.OUTPUT
A0pin = digitalio.DigitalInOut(board.GP10)
A1pin = digitalio.DigitalInOut(board.GP11)
A2pin = digitalio.DigitalInOut(board.GP12)
A3pin = digitalio.DigitalInOut(board.GP13)
D0pin = digitalio.DigitalInOut(board.GP2)
D0pin.direction = digitalio.Direction.OUTPUT
D1pin = digitalio.DigitalInOut(board.GP3)
D1pin.direction = digitalio.Direction.OUTPUT
D2pin = digitalio.DigitalInOut(board.GP4)
D2pin.direction = digitalio.Direction.OUTPUT
D3pin = digitalio.DigitalInOut(board.GP5)
D3pin.direction = digitalio.Direction.OUTPUT
D4pin = digitalio.DigitalInOut(board.GP6)
D4pin.direction = digitalio.Direction.OUTPUT
D5pin = digitalio.DigitalInOut(board.GP7)
D5pin.direction = digitalio.Direction.OUTPUT
D6pin = digitalio.DigitalInOut(board.GP8)
D6pin.direction = digitalio.Direction.OUTPUT
D7pin = digitalio.DigitalInOut(board.GP9)
D7pin.direction = digitalio.Direction.OUTPUT
def doOutput (data):
if (data & 0x01):
D0pin.value = True
else:
D0pin.value = False
if (data & 0x02):
D1pin.value = True
else:
D1pin.value = False
if (data & 0x04):
D2pin.value = True
else:
D2pin.value = False
if (data & 0x08):
D3pin.value = True
else:
D3pin.value = False
if (data & 0x10):
D4pin.value = True
else:
D4pin.value = False
if (data & 0x20):
D5pin.value = True
else:
D5pin.value = False
if (data & 0x40):
D6pin.value = True
else:
D6pin.value = False
if (data & 0x80):
D7pin.value = True
else:
D7pin.value = False
while True:
Tpin.value = True
addr = 0
if (A0pin.value == True):
addr = addr + 1
if (A1pin.value == True):
addr = addr + 2
if (A2pin.value == True):
addr = addr + 4
if (A3pin.value == True):
addr = addr + 8
Tpin.value = False
doOutput(ROM[addr])
I’ve included a timing pin to GPIO21 so I can see how long it takes to access the IO.
It turns out that it takes something of the order of 50-60uS to read the four address lines and something in the region of 70-80uS to write out the 8 data lines. The above simple Circuitpython code to do this is running with a frequency of around 7kHz.
Now at this point I ought to be reading through the datasheets for the ICs used in the CPU to check response times and timing tolerances so see if this is ok. But I didn’t bother with any of that as it all appears to work!
Conclusion
The Circuitpython is obviously a lot slower than the Arduino running optimised PORTIO code, even though the Circuitpython is running on a 125MHz processor compared to the Arduino’s 16MHz. Of course, if performance was critical then switching to direct GPIO access in C on the Pico would be a lot faster again. Even just having a way to do a single block-access of GPIO would probably make quite a difference.
But for this application, either as they are seem to work absolutely fine.
The ability to quickly edit the ROM contents is pretty useful with the Circuitpython. But I am now wondering how difficult it would be to have some kind of uploader to the Arduino over the serial port. There are only 16 bytes to transfer after all.
In fact it might even be possible to create a simple interactive assembler that allows code to be typed in over the serial port using proper word-based op-codes (like ADD, IN, OUT, etc). At the very least a simple serial port interface to type in numeric values would be relatively straight forward I think. It might also be possible to allow the microcontroller to reset the CPU too.
I’m not sure the added complications of logic shifting, etc, make it worth carrying on with a Pico version at this stage, so I think improving the Arduino is probably the way to go for now.
Kevin
#4bit #arduinoUno #circuitpython #portio #raspberryPiPico #td4





