How to design a (better) ALU

A project log for PDP - Processor Design Principles

Distilling my experience and wisdom about the architecture, organisation and design choices of my CPUs

Yann Guidon / YGDESYann Guidon / YGDES 04/12/2018 at 23:340 Comments

As @Martian created his new architecture #Homebrew 16-bit CPU and Computer, he credited this PDP project for ideas and guidance. In return I take the opportunity to comment on his ALU, which contains typical "misunderstandngs" that I have seen in many other "amateur" designs. These are harmless in a SW simulation but their hardware implementation would be significantly slower, larger and less efficient than what can be done. So this is not at all a criticism of Martian but a little lesson in logic design which, by the way, explains HOW and WHY my own CPU are organised in their precise ways.

Note : ALU design is already well covered there : This is a MUST READ but it might focus on particular technologies and here I'll cover some required basics.

At least, Martian's ALU is very easy to understand : there is a big MUX that selects between many results of computations. One input for every opcode. It's large, the many inputs imply a very large fan-in and there is quite a lot of redundancy. See :

Another version:

So the name of this game is to factor operations.

I'll simply start with the elephant in the room : addition and subtraction.

In typical code (think C/Verilog/VHDL), you would write something like :

if opcode=ADD then result=ADD else result=SUB

if opcode=ADD
    then result=ADD
    else result=SUB

Which synthesises into one ADD block, one SUB block and a MUX2. This would be OK if the opcode signal came late but in this case, the opcode is the first thing that we know in the pipeline. The cost is pretty high, because ADD and SUB are actually very very similar circuits so only one is actually required.

The key is to remember that in 2s-complement :

C = A - B = A + (-B) = A + (~B) + 1

C = A - B = A + (-B) = A + (~B) + 1

so if you want to compute C, you need to

Oh wait, that's another addition unit added to our critical datapath... But sweat not. Since it's only 1, there is another trick : set the CARRY IN bit.

Tadah, you have it : a unit that contains only one carry chain and has the same critical datapath as a MUX-based ADD/SUB, but with

And this is just the first of many tricks in the book...

Another unit in the diagram is a "comparison" block. Which is... well, usually, a subtractor. The carry-out and/or the sign bit of the result will indicate the relative magnitude. But we already have a subtractor, right ? Just execute a SUB instruction and don't write the result back. That's it.

OK so now there is this row of XORs in the critical datapath. Is it a curse or a blessing ?

Actually it is welcome because it helps to factor other operations than the ADD/SUB. In particular, some boolean operations require one operand to be inverted : ANDN, XORN, ORN

So you only need to implement OR XOR and AND and you save even more units (and you reduce the fanout).

But wait, it doesn't stop there ! There is even more factoring to do if you are brave enough to implement your own adding circuit. And I discovered this trick in one of the historical FPGA CPUs more than 20 years ago.

A typical adder uses a "carry-propagate" circuit where two input bits of equal weight are combined to create a "generate carry" signal and a "propagate carry" signal. Both are the result of a very simple boolean operation :

See what I see ?

These operations can be factored and shared between the adder unit and the boolean unit. The only gate to implement separately is XOR... which can be derived from the already computed OR and AND ! This XOR is in turn used in the last stage of the adder.

This method is implemented in the ALU of #YGREC8  at the gate level and it works very well. Look at the VHDL source code and the logs :-)

This diagram doesn't look at all like those at the top of the log, but now you can see why and how it is more efficient.