If we're going to use C to simulate a CPU, we need to talk a little bit about how we can use C code (and variables) to provide the same functions as the hardware.
We've already pointed out the mapping from combinational logic to code, and from state elements to variables. Now let's talk in a bit more detail about how to do that.
To take a concrete example, let's use the registers from Figure 5.7, on page 295 of the book. Here's my adaptation of that figure:
The first thing we need to do is see how to read the figure. The
lines connecting to the box represent wires connected to it - so we
have wires with names like readRegister1
,
regWrite
, and so forth.
The wires with arrows are meant to represent data. Input data to a piece of hardware is almost always shown as coming in from the left; output data is shown as going to the right. Control wires come in the top or bottom of the box. I should mention at this point that what constitutes data, and what constitutes control, can seem (or can even be) a bit arbitrary: I for one would think a register number would be control, but it's almost universally considered as data; you can see that in this figure.
Finally, we frequently have "bundles" of wires that we consider as a unit: the five wires we need to specify a register number, or the 32 wires needed to carry a 32 bit integer from one unit to another. In order to limit the number of lines on the figure, we represent this by putting a "hash" across the line and a number, showing how many wires are represented by the wire. If there is no mark, it's a single wire.
OK, so how do we make code out of this? Each of the inputs and
outputs has to be a parameter; the inputs are normal by-value
parameters while the outputs are by-reference. So, in the case of the
register file, some good #define
s and
typedef
s and a good function prototype would be:
#define REGFIELSIZE 32 typedef unsigned int Regno; typedef uint32_t Value; typedef enum {FALSE=0, TRUE=1} Bool; void registerFile(Regno readRegister1, Regno readRegister2, Regno writeRegister, Value writeData, Bool regWrite, Value &readData1, Value &readData2);
So now, we need to actually come up with the code to implement it. From the description on Page 294, we can come up with the following code to do the register file:
void registerFile(Regno readRegister1, Regno readRegister2, Regno writeRegister, Value writeData, Bool regWrite, Value *readData1, Value *readData2) { static Value registers[REGFILESIZE] = {0}; // make sure our inputs are good assert(readRegister1 < REGFILESIZE); assert(readRegister2 < REGFILESIZE); assert(writeRegister < REGFILESIZE); // Now we do the write (if any) // A subtlety that comes up later in the text is that if you try // to read and write a register on the same cycle, the read // returns the new value. Also note that we can't write to // register 0 if (regWrite && (writeRegister != 0)) registers[writeRegister] = writeData; // And do the reads *readData1 = registers[readRegister1]; *readData2 = registers[readRegister1]; }
Now, in some cases we'll be able to use a register that is actual
state as part of the design, and sometimes we'll need to use a wire as
a "state" element (which it isn't, of course). So we might end up
having to define variables with names like readData1
and
readData2
, so we can call registerFile
with
those variables as outputs, and then call our alu
function (which we haven't described here at all, of course) using
those as inputs.