Monday, May 17, 2010

Building a CPU - Musings on the evolution of machine code

Over the weekend I got deeper into some of the more thorny parts of the design. The ALU was really rather simple. For the most part the glue logic was pretty simple. Sure, it looks big and complex, but it's mostly just a bunch of wires that connect from point a to a central routing station to point b, oh, and a few dozen control signals to tell the thing what to do.

This is all fine and good. I can by setting a certain set of inputs instruct the machine to take register o, connect it to bus b on ALU and add it to the accumulator. Great, so the basis of instructions will work.

Now, all I need to do is create an instruction set. At first I had the brilliant idea of using the most significant five bits (8 bit instructions, 2 words) This created a very nice set of 8 bit instructions. I started working out the machine code, how the arguments will be handled and how op-code+arguments will be structured. I knew I was going to somehow index into a set of EEPROMs to get the signals, or a series of signals the op-code will generate. Essentially the EEPROMs would store a table. After fidgeting around with the op-codes and their respective arguments for a while it became painfully apparent that I would either have more ROM for instruction microcode than the entire system has addressable space or I would need some very complex decoding logic to get an index into the appropriate entry into the table.

That simply was not going to work. First, it seems wrong to have several more times address space just to store instructions than the chip is capable of handling from the external world, not only that, but it would be exceptionally sparse, a huge waste of memory. Yes-- memory is cheap these days. ROM is not as cheap, but still rather cheap. The other alternative would mean I'd need a heap of TTL chips to build just the instruction decoder, which aside from requiring large piles of money would also require more power than I care to deal with (I don't really care for needing to add special high-current breakers to the breaker panel, and I don't think the apartment complex would appreciate it.)

So, something had to give. I tried squeezing some different kinds of flags into those eight bits. Hmm, no, that still doesn't work out as cleanly as I would like. I Finally broke down and decided the op-codes between instruction forms will only loosely be related. This means, for example, ld (load a register from memory or another register) would take up about half the available slots, due to the fact that it necessarily needs to move data from anywhere to almost anywhere. st (store register data to memory) is a little bit better in this regard, since the destination is only memory.

Which leaves me with machine code that is functional, but not the orthogonal work of art I had hoped for.

Control logic: Easy at first but maybe not on closer inspection

So I began work on the instruction decoder after I put together an instruction set. One of the things I knew would be a requirement is that the EEPROMs be banked. Allow me to explain: The control word is ~50 bits wide. Each micro-instruction is a control word. EEPROMs generally have an 8 bit data bus. I could fetch in series each chunk of microcode and bring it into a final register, but this complicates things and wastes precious instruction cycles. So, rather than a single EEPROM, I'll need 7. As far as the decoder is concerned, it's not a big deal. The tricky part is getting an 8 bit instruction from a 4 bit data bus. This has to be done in two cycles. Also, during this cycle the current micro-instruction must stay the same, or the decoder could lose its fetch signal. So, the decoder pulls an op-code in two cycles, and once the cycle is complete it then latches the op-code. Upon latching, the first micro-instruction of that op-code now sends its signals out to the rest of the CPU. That latch step added one clock to the fetch cycle. A necessary evil, I suppose.

The next challenge was getting some microcode in the simulation to test things out. Unfortunately the banked layout makes things rather nasty as far as just jumping into the sim's hex editor and plopping in codes (notwithstanding the fact that the control word is long enough to tax my memory of what bit handles what, despite the logical ordering of things.) This necessitates some sort of assembler, so I have begun a quick-and-dirty assembler for microcode. This is unlike any other assembly code, the microcode is highly parallel, and needs to perform several operations at once. So each "instruction" becomes a list of instructions followed by either a fetch or next. Fetch will of course read 2 words from the data bus and latch a new instruction. Next will increment an internal counter in the microcode sequencer. As it stands now, each instruction can perform 16 microcode steps. This is, obviously, subject to change as I get a better grip on the requirements of each instruction. 16 seems generous. Some of the instructions in the micro-assembler, will really be a composite of signals, some will be individual signals. Some will simply be modifiers.

I also knew that some of the instruction forms, especially in the forms [si+c], [si+oac], [si+<imm>], [si+<imm16>] would need a bit of new logic in the address bus control, since I didn't think to add the ability to combine addresses with external numbers. This also grows the control word by a few more signals.

I am very glad to be running this through a simulator before beginning the build. It allows me to catch flaws in my design and plan before wiring things up. Before simulations, I'd need to put the whole thing on a breadboard and test it out. Sure, not horrible, but definitely not as quick.

No comments: