girst: Online I usually go by Girst and today I want to talk to you about my BlinkenLights project.
By day I'm a network engineer and I work on pretty interesting stuff from in
-house data centers to really large access networks and so by night I like
to balance stuff out by working on completely useless electronics projects.
This is the Motorola 14500 it's a one -bit CPU so the data bus is
not 8 bits not even 4 bits just a single bit for the data bus.
Motorola didn't even dare to call this a CPU they simply
called it an industrial control unit in the marketing material.
The 14500 was released in 1977 that's right in between the 6502 which forms
the basis of the Commodore 64, the Apple II and many other 8 -bit homebrew home
computers and it was followed quite quickly by the Intel 8086 variant of which
forms the basis of the IBM PC and therefore most of our modern computers.
As you might imagine the 14500 is way smaller it is also way slower and it is
way less capable but the 14500 was never intended to compete with these chips.
Instead it was designed to replace relay logic.
Relay logic or often called ladder logic by the circuit diagrams they are written in.
A short overview over the specifications.
As already mentioned this is a one -bit CPU with a one -bit data bus
and unlike many other bit serial architectures which still process
one bit at a time but then combine these multiple bits into a
single machine word, the 14500 really only works on one -bit words.
It has nearly one full register. Motorola calls it a result register.
It's essentially an accumulator if you are more familiar with 65 or two -speak.
Most of the instructions actually take one of the operands from the
data bus and the other and then just add it to the result register.
However, the address bus is not hooked up to the CPU.
The CPU just hopes that the correct data appears on the data bus magically.
What this means in practice is that you, the designer of a board,
you have to bring your own address decoding logic and hook that up.
There is no full ALU. ALU is short for arithmetic and logic unit.
This is the part of the CPU that does addition, subtraction and also the logic operations.
Instead, it simply has an LU, a logic unit.
There isn't much of a point in providing addition and subtraction if your
only possible values are 0 and 1. More stuff is missing from the CPU.
For example, there is no program counter.
Just like with the data bus, the CPU just hopes that the
correct instruction is provided to it and you have to bring
your own program counter and program memory and all that stuff.
That's all quite theoretical, so let's look at the practical example.
This is what Motorola calls the minimal ICU system.
ICU, once again, the marketing name of the 14500.
Here on the left, we can see the program counter.
Usually that's just a binary counter starting at 0
and every clock cycle it increments to 1, 2, 3, etc.
And that's usually directly connected to your program memory.
Some systems use ROM for that, read -only memory.
Some very tiny ones even use RAM, meaning after you power
the device up, you have to load the program into it.
And here we can see half of the output of the program chip is connected to the CPU.
This is your instruction bus.
And down here, the other half is connected to the I
.O. and memory section, which is your address bus.
Of course, those two are connected through the data bus.
And one interesting thing, RR is the result register, our
accumulator, and the CPU actually exposes the signal on a pin.
So you can feed it back into the memory unit and address it just like any other peripheral.
One thing I wanted to mention on the previous slide, there is
no program counter in there, yet the CPU has a jump instruction.
The jump instruction simply is an output pin that
when the jump instruction is called, it makes blip!
And you can hook that up to whatever you want.
Usually you use it to reset the program counter to a known value or to
zero even, but nothing is stopping you from hooking it up to whatever.
And I've also placed the signal up here.
In this case it simply resets the circuit.
For I .O., the minimal system consists of eight inputs and eight outputs.
These share the same addresses.
And that works by looking at whether the CPU wants to do a read or a write.
And that's a signal also exposed by the CPU.
So if you read, you get redirected to the inputs, and
if you do a write, you get redirected to the outputs.
That's not happening within the CPU.
That happens usually within your own logic in the I .O. and memory section.
And you most always also have at least eight bits of scratch memory.
That's essentially a bank of external registers that
you can use to store a limited amount of variables.
Short look at the instruction set. It starts out fairly tame.
There's a load instruction.
It loads an address and stores it in the result register. Same for store.
Logically OR.
So it takes a value from the data bus and stores
it and ORs it logically to the result register.
There's also AND. And then there's X NOR. There is no XOR exclusive OR.
They instead chose to provide exclusive NOT OR.
Essentially, it's an equality check.
So if the value at the address A is equal to what is
currently stored in the result register, then a 1 is placed
in the result register, otherwise a 0. We also have a NOP.
But one thing is missing. There is no INVERSE instruction OR NOT instruction.
So how do you INVERT a value? There's a whole other set of instructions.
They call them COMPLEMENTS.
So LOAD COMPLEMENT reads the value at address A, INVERTS it, and then stores it back.
OR COMPLEMENT loads value from address A, INVERTS
it, and then ORs it to the result register.
There's also a second NOP instruction. And that's quite an interesting idea.
Remember, we are talking about app ROMs and similar devices for storing your program.
And if you only decide to program those bits that you actually need, and then
leave the rest of the ROM unprogrammed, it will usually have all Fs stored in it.
All ones.
And the NOP i nstruction, NOP F, is placed at the highest possible instruction.
So whenever you encounter invalid memory, so to
speak, you get an output signal similar to jump.
And you can use that to, for example, reset the system.
Because if you are executing invalid memory, something has gone wrong somewhere.
You can, of course, then use the other NOP as your normal NOB instruction.
But this too has an output PIN connected to it.
Here's the aforementioned jump instruction that simply blips a PIN.
Return is quite similar to that. It also has its own output PIN.
And you, once again, can hook that up to whatever you want.
Most likely it's intended to exit from a hardware stack.
SKZ is short for skip if result register is zero. This is used for flow control.
And IEN and OEN stand for input enable and output enable.
I'll talk about all three of those instructions later.
We're still missing something. Load immediate. How do you load constants?
Of course, we could do what modern ARM CPUs, for example, do and
provide a zero register that always returns the static value.
But that wastes an entry in your address table.
And you probably already have way too few of them. So here's another way.
You can use the result register as an input to the OR complement function.
So independent of what is currently stored in RR, if you
take that value and do a logic OR with the inverse of
itself, you always get a 1. So if RR is 0, you have a 0 or 1.
If RR is 1, you have 1 or 0. And that's always 1. You can
do the same trick with end complement to get a constant 0.
Back to flow control. On the left, here's a small program.
We want to check the value at address 1.
And if that is true, i .e., 1, then we want to loop back to the beginning.
We can use the skip 0 here.
We simply check load the value from A1 into the register.
If that register contains a 0, we skip the next instruction, so we don't jump.
But if A1 contains a 1, a true value, then the skip is executed, and the jump is skipped.
However, this falls apart fairly quickly.
If we want to skip more than one instructions, like in this example, we want to
use the value we just loaded previously or it was something and then store it back.
We obviously can't use skip 0 like this because skip 0 only skips one instruction.
We also can't place an additional skip 0 here in the
middle because we are modifying the result register.
So maybe the first part is skipped and the second part is executed.
That's not what we want.
This is where this alternative flow control comes in, with output enabled in this case.
This is kind of like an if -else.
Whenever you enable the output, the store instruction starts working.
And when you disable the output, the store instruction turns into a no -op.
So you run through all your code, and in the end, when you
want to talk to the real world and write that variable out
to some place, nothing happens if the output is disabled.
So it doesn't matter if you execute half a million
lines of assembly code if the output is disabled.
And you can see output, we check the value of A1. If that is true,
we enable the output, and then the code inside is truly executed.
We load a value, we or it with another one and store it back.
If instead A1 was 0, we still load the value, we still
or it with something else, but then we don't store it.
So we essentially just discard what we just computed.
And here at the end, we once again use the OR complement RR trick to force
a 1 into the result register, and then unconditionally re -enable the output.
These two lines are essentially your end if.
I guess we now have enough information to do a practical example.
So let's get back into our 1970s office.
Your co -worker is smoking next to you, of course.
And your boss comes in and hands you this diagram. This is a ladder logic circuit.
This is usually what industrial processors used to use.
You still see it sometimes today even.
To read this, this vertical line on the left is essentially a power source.
And the one on the right is essentially a drain.
And you read it from left to right, and you try to make a complete circuit.
This first symbol, the two vertical bars with a slash through it.
That's a normally closed button. So usually power flows.
If you press the button, power gets interrupted. If you let go, it continues.
And here's an emergency stop. We then have a regular stop button.
We also have a start button that works the inverse. It's normally open.
So once you press it, power flows. And once you release it, power stops.
And here on the right, the circle is an output or a relay in this case.
You can tell it's a relay because there are multiple labels that are named the same.
And once we press the start button, power can flow completely
from left to right, energizing the coil of the relay.
Once the relay coil energizes, the relay contact closes.
And now you can let go of the start button and the contacts
will stay closed because the circuit is self -sustaining.
The run signal is then also, of course, hooked up to the motor.
Now, if we want to transform this into assembly,
we first need to get an equation out of this.
Here's one way to do it.
We want to run the motor if neither the emergency
stop nor the regular stop button is pressed.
And also either the start button or the run signal must be triggered.
Once again, we do not have a NOT operation like this available in our instruction set.
So we can apply the Morgan's law.
And we can move the NOT very close to the two variables.
We can then use the end complement and similar instruction to work with those.
And finally, we also need to rearrange it so that we get rid of the parentheses.
And we can simply do this by moving this part at
the beginning and then we can walk through the code.
Here's the same code again. And now let's turn this into some assembly.
Every MC14500 program starts with CPU initialization because,
annoyingly, input and output are disabled when the CPU powers up.
So I'll always start with a NOP because the return instruction I
mentioned previously will skip the instruction that follows it.
And skipping a NOP is, of course, safe. Then we once again force a 1 into RR.
And finally, enable the input and enable the output.
The actual code is then pretty straightforward.
We load the run signal or it logically with start. This is exactly this line.
Then we end it with the inverse of the emergency stop, exactly the next line.
Once again, end it with the stop button and then store it back into the run signal.
The jump at the end loops back to the beginning, loops back
to this NOP statement, and the next cycle can continue.
Now for something else. This is the thing I talked about in the abstract.
This is the board I built. You can see it over there.
And I think we can maybe even put it on screen. And back to the slides, please.
Yeah. Once again, the CPU itself is really tiny.
It's not a large chip on the board. It's just 16 pins.
And this thing essentially plays Game of Life.
I wanted to do something with the CPU and making it blink nice was the essential role.
The whole system can address 517 words of memory.
Each word, of course, being a single bit. So that's 517 bits.
There is a double -buffered 16x16 LED matrix.
There's a hardware random number generator. Well, pseudo -random number generator.
And even an interrupt of sorts.
And I thought it would be pretty funny to make this
completely period correct to late 70s, early 80s chips.
And so I decided to make it USB -C power delivery compliant to power it.
The first part of the system is the program memory and program counter unit.
This big chip is the ROM, which stores the actual program.
And it is divided into two halves.
The lower part of it is the main memory, main program.
And it simply executes in a loop. And with each loop, a single pixel is updated.
And once it reaches the end, the interrupt automatically triggers.
This triggers a flip -flop that then switches to the upper half of memory.
And once we execute the return instruction, the flip
-flop gets reset and we continue with the main program.
Here are some bits and pieces that are also necessary.
This chip, for example, is the 14599. That's the
external register bank that provides the scratch memory.
This is another small PROM that I use as a makeshift MMU for address decoding.
And down here are the three chips that make up the linear feedback
shift register that is the pseudo -random number generator.
The sequence length is 255, which is barely enough to make one whole board out of that.
I only had space to hook up five of the eight external registers.
But frankly, that's all I needed. So that worked out fine.
And finally, here's the big star of the show. This is the main memory.
There are 512 locations.
Yet the address bus, as I previously mentioned, is only four bits wide.
That means there are 16 addresses in total.
We already used up more than half of them for the register
bank, the random number generator, the result register.
So how does this work?
I've been very careful to avoid the word RAM,
because this is volatile non -random access memory.
Essentially, the RAM chip is not connected to the
address bus, but instead to its separate counter.
This counter, once again, starts at zero.
And every time the CPU tells it to, we can increment it.
This way, we can look at the first part of memory, increment it,
look at the next one, look at the next one, look at the next one.
But for Converse Game of Life, that's not enough.
You always have your center location and then the eight neighbors around it.
We can modify the circuit. We can place two adders in between these lines.
If those adders are simply zero, if the second input of these
adders are simply zero, then it works the same as before.
But the second input of the two adders are
connected to some of the bits of our address lines.
So we can shift where we are currently looking at temporarily.
We can go one column to the right, one column to the left, one row up, one row down.
And finally, the display is double buffered, as I
said, and that works with the topmost address bit.
This is also connected to the counter, of course.
But whenever we do a store instruction, this XOR gate will swap them around.
So we always read on one plane, and when we write, it automatically goes to the other one.
And once the whole frame is done, they are automatically exchanged,
and we once again read from one plane and write to the other.
Here's how the memory map looks in practice.
We interpret the lower two bits of the address
base as a signed two -bit two -complement number.
And we also do the same for the upper two bits, as a separate number.
So if we look at address 0000, that's just the center location of our RAM,
meaning the value that is currently stored in the second counter chip.
If we add a one to that, we can look one column to the right, we can look one
column to the left, we can look a whole row in front and a whole row back.
And of course, we can do both at the same time to get the diagonals.
We don't want to subtract 32 or subtract 2, so these values here in the margin are
still free, and so we can use them to store the register bank, we can store the
result register here, and we also have a place for the random number generator.
Now to the display.
As you might have seen, there was no address left, so how is the display connected?
The display is a big shift register, 256 positions, and when you
write to the VM RAM, your data is automatically copied to the display.
So a new value is automatically shifted in, and once the interrupt
fires, the display is latched and the new image is displayed.
Of course, we need a little bit of firmware.
The main program I decided to split into two modes. The first is called simulate.
It generates one new pixel data for the next frame, and then it loops.
And the other mode is called generate, and it fills the
board with random data whenever it gets boring to look at.
At the end of the frame, again, the interrupt fires, and then we switch out which
program we run, and in this case, we simply check if the board still doing something,
or is it stuck, and if it's stuck, we switch generate, and generate a new one.
We can only jump to address zero.
This means we have to unroll all our inner loops, and this
was done simply by a makeshift assembler from a Python script.
It essentially, I define functions for all the instruction mnemonics, and they
print out the hexadecimal machine code representation, and then there's some
shell that turns it into a real binary file that I can push onto the app ROM.
Here's the same thing again. This is the simulation routine.
It counts up all the neighbors, and then decides it counts up whether
how many neighbors are lit up currently, and then decides if this
current pixel should turn on or should turn off in the next frame.
Then it advances the memory counter, and starts looping back.
This happens 256 times. Then the interrupt fires.
It checks are we still doing something. If yes, we continue simulating.
If we don't do anything, we switch to the generation
mode, and then we return from the interrupt.
The generation is extremely simple.
It takes a random value, writes it to the current
pixel, advances to the next pixel, and starts over .
Not all was smooth sailing.
I made some annoying mistakes, some silly mistakes,
and I thought it would be fun to look at them.
There are a lot of chips on this board.
On the front, there are 19. On the back, there are 16 for the shift register of the LEDs.
So, of course, I wanted to bring it up in stages and not all at once.
And even though I knew about the problem, I accidentally left some of the inputs
floating, and with 4000 series logic, that is, of course, a very bad idea.
And it happened that my clock line and my reset signal were
right next to each other on the board, and they capacitively
coupled, and whenever the clock clocked, the CPU reset.
Of course, this was only a problem during the partial bring -up.
Once all the chips were in place, this was gone, and, of course, I also
added a small pull -down resistor to fix it during the partial bringup.
Similar problem also happened with the data bus.
One of the inputs of the bus transceiver was floating, and two peripherals fighted
over the data bus, and instead of it being at zero or one, it went to half.
And this had an additional negative side effect that's essentially that
short, and it consumed a lot of power, and the chip's got pretty hot.
The second part, the second stuff I did wrong was I mixed up some of the chips.
My counter is clocked on the negative edge, so this is an edge
-triggered chip, meaning it only advances or only does something
when the clock goes from high to low or from low to high.
Some of those chips are rising edge, some of these are
falling edge, and instead of everything happening at once,
some of the stuff happened at the offbeat, so to speak.
The problem is, of course, not reading the data sheets correctly, and the fix
is to add an inverter to get the signal inverted, and then it works again.
That interrupt, as I mentioned previously, is essentially a flip -flop.
Flip -flops are also edge -triggered, so it initially didn't
matter that my interrupt signal, once the frame was over, it
went high and then stayed there for multiple instructions.
But later, I noticed that it would be smart to also reset the program counter
when you switch out programs, so you don't start in the middle of the program.
But the reset line is, of course, line -level triggered, so the interrupt signal
went high, we started resetting the counter, and then we never let go of it.
So, of course, once again, the whole thing went to a halt.
And finally, about that MMU, about that makeshift MMU from the one -time programmable run.
These are extremely obsolete chips. They're called bipolar PROMs.
They're the only ones that are small enough to fit into my special design.
And I thought, yeah, well, well, can't be too hard.
The problem is they essentially are a collection of fuses
that have to be burned through with a pretty high current
and voltage, and most modern programmers don't support that.
You either have to buy really expensive programmers or try to
get a hand at really old ones, neither of which I wanted to do.
So, in the end, I had to build my own programmer for that.
It turns out this is rather simple. You don't need any active components.
You can simply wire switches to all the pins and toggle it in.
And since there are only 16 addresses I care about, each of which only has a
single bit set, this turned out to be fairly trivial to do by hand, so to speak.
And with that, I'm done. I hope you learned something.
Maybe you found this project interesting.
We can take another look at it, for example. And that's it. *Applause*
I brought one or two of those PCBs. If someone wants one, hit me up.
I'll stay around here.
Be warned, though, they contain one error that I will have to talk you through.
Toby, thank you for the great talk. Now it's the time for QA.
So, anyone who wants to ask a question, please approach one of the microphones in the room.
And yes, microphone one, please.
Okay. First of all, wonderful talk.
I have a question, which is, at what point is a CPU not a central processing unit anymore?
girst: That's a really good question.
I think this really stretches the limit what you can still call a CPU.
And Motorola didn't even call it a CPU anymore.
They called it an industrial control unit. They invented a new word for it.
Then I guess that's answered. Thank you.
Herald: Microphone two, please.
Hello, and thank you for the talk. Did you open source the plans?
girst: I intend to.
When we go back to the last slide, this website will contain all the stuff in the future.
I haven't had time to publish it yet. Feel free to check it out.
Currently, there are just the slides and some text on it.
Guest: Thank you very much.
Herald: Mic, one, please.
Thank you. Excellent talk.
You were showing a picture of these industrial control planes very
much at the beginning that these chips were supposed to replace.
Can you talk a bit more about that?
You mean this slide, correct? Yes. Those are not chips. Those are relays.
Electromechanical relays. These were used to also build logic circuits.
As mentioned in the one example here. There.
For example, these two parts form a single relay.
And you can use that to store the state of the start button.
So you can make logic circuits completely out of relays and of course they are slow.
They consume a lot of power. So making them electronic is of course desired.
Guest: What is the clock source and how fast can you run it?
girst: The clock source is a 555 or more specifically a 556
because I'm using the other half to do a nice reset signal.
I intended to run it at 500 kilohertz.
But there's a potentiometer on it where you can go down to a few kilohertz.
So you can decide how often a new frame is generated.
Thank you. And we have a question from the internet.
Signal: So the internet wants to know whether you have considered removing the shift
register and doing multiplexing the display output in software using MCUs.
girst: The problem with not using the shift register is that
you would have to constantly manually update the display.
The shift register does that for you.
You don't have to keep track of the display anymore if you use a shift register.
I'm actually using one cheat about the late 70s early 80s components.
The shift register on the underside of the board is actually fairly modern.
Oh yeah, you can see the USB -C socket here. Those are 16 -bit LED drivers.
So they are a shift register with a constant current source.
I've done this solely so they use less board space.
Instead of one resistor per LED, there is one resistor per row of LEDs.
That's brilliant. Finally one more question.
Are there any least capable ICs that you could use for your next project?
I'm looking for them. If you know something, hit me up. Call me.
My phone number here is 4008.
Signal: And also Tobi wanted to mention that he's hanging around in this room for the next talk.
So if you want to say hi and discuss the project, please feel free to approach him.