Due Monday, November 23, 2026.
Accept the Lab 4 assignment and get your starter repository.
In Lab 3, you built a 32-bit ALU. In the register-file activity, you completed a register file. Now you will connect these components to a control unit and instruction memory to execute a small RISC-V program.
Your processor will execute one instruction per clock cycle. You will build a single-cycle datapath for the RV32I arithmetic subset below; loads, stores, branches, jumps, and pipelining are outside this lab’s scope.
By the end of the lab, you should be able to:
Accept the assignment using the link above, then clone your starter repository. Keep all circuit files in the same folder so Digital can find custom components.
Bring these completed files from Lab 3:
alu.digboolean-unit.digbidirectional-shifter.digadd-sub-unit.digslt-unit.digAlso bring your completed register-file.dig from the
in-class activity. Run your ALU and register-file tests before
integrating them. Save a separate copy of your earlier work.
The CPU starter provides cpu.dig with a program counter,
PC + 4 logic, instruction memory, a clock, and read-data output pins.
You will build control-unit.dig, complete
control-signals.csv, and connect the remaining datapath.
The accompanying basic-tests.s, preassembled
basic-tests.mem, ROM-ready basic-tests.csv,
basic.txt, and format.py support program
testing.
Preserve the supplied pin names and widths. The Fall
ALU control input is named ALUOp, not
ALUOperation.
Implement all 11 instructions in this table. For every instruction, x0 remains zero even when it is the destination.
| Instruction | Type | Result written to rd |
|---|---|---|
add rd, rs1, rs2 |
R | rs1 + rs2 |
sub rd, rs1, rs2 |
R | rs1 - rs2 |
and rd, rs1, rs2 |
R | Bitwise AND |
or rd, rs1, rs2 |
R | Bitwise OR |
slt rd, rs1, rs2 |
R | 1 if signed rs1 < rs2; otherwise 0 |
sll rd, rs1, rs2 |
R | Logical left shift by rs2[4:0] |
srl rd, rs1, rs2 |
R | Logical right shift by rs2[4:0] |
sra rd, rs1, rs2 |
R | Arithmetic right shift by rs2[4:0] |
addi rd, rs1, imm |
I | rs1 + sign_extend(imm) |
slti rd, rs1, imm |
I | 1 if signed rs1 < sign_extend(imm); otherwise 0 |
andi rd, rs1, imm |
I | rs1 AND sign_extend(imm) |
Arithmetic results retain the low 32 bits. The I-type immediate is a
signed 12-bit value, ranging from −2048 to 2047. Sign-extend it
for andi too. The shift instructions here use a
register for the shift amount; immediate shifts are not required. Your
ALU supports XOR, but this CPU subset does not require an XOR
instruction.
Deliverable: control-signals.csv
The control unit chooses the ALU operation, selects the second ALU operand, and enables register writeback.
| Signal | Width | Meaning |
|---|---|---|
RegWrite |
1 | Enable a register-file write on the rising edge |
ALUSrc |
1 | 0: use ReadData2; 1: use the sign-extended
immediate |
ImmSrc |
2 | Immediate format; use 00 throughout this lab |
ALUOp |
4 | ALU operation using the Lab 3 encoding |
The eight-bit control word is arranged as follows:
bit: 7 6 5 4 3 2 1 0
RegWrite ALUSrc ImmSrc[1:0] ALUOp[3:0]
Keep the ImmSrc bits at zero even for R-type
instructions, which do not use the immediate. They are retained in the
control-word format for consistency.
ALUOp |
Operation |
|---|---|
0000 |
AND |
0001 |
OR |
0010 |
ADD |
0011 |
XOR |
0100 |
SLL |
0101 |
SRL |
0110 |
SUB |
0111 |
SLT |
1000 |
SRA |
Fill every instruction row in control-signals.csv. Its
individual-bit columns are RegWrite, ALUSrc,
ImmSrc1, ImmSrc0, and ALUOp3
through ALUOp0. In DataValueForROM,
concatenate those bits in that order and prefix the result with
0b.
For example, add writes a register, uses a register as
the second operand, and selects ADD:
RegWrite = 1, ALUSrc = 0, ImmSrc = 00, ALUOp = 0010
DataValueForROM = 0b10000010
Checkpoint: Which control bit distinguishes
add from addi? Which bits distinguish
add from sub?
Deliverable: control-unit.dig
Use these input and output buses for the control unit:
| Pin | Direction | Width |
|---|---|---|
opcode |
Input | 7 |
funct3 |
Input | 3 |
funct7 |
Input | 7 |
RegWrite |
Output | 1 |
ALUSrc |
Output | 1 |
ImmSrc |
Output | 2 |
ALUOp |
Output | 4 |
The CSV represents some buses as separate bit columns; use splitters to assemble or separate the circuit buses as needed.
Build one match signal per supported instruction using comparisons and AND gates. All patterns below are binary.
| Instruction | opcode |
funct3 |
funct7 |
|---|---|---|---|
add |
0110011 |
000 |
0000000 |
sub |
0110011 |
000 |
0100000 |
and |
0110011 |
111 |
0000000 |
or |
0110011 |
110 |
0000000 |
slt |
0110011 |
010 |
0000000 |
addi |
0010011 |
000 |
Ignore |
slti |
0010011 |
010 |
Ignore |
andi |
0010011 |
111 |
Ignore |
sll |
0110011 |
001 |
0000000 |
srl |
0110011 |
101 |
0000000 |
sra |
0110011 |
101 |
0100000 |
For example:
is_add = (opcode == 0b0110011)
AND (funct3 == 0b000)
AND (funct7 == 0b0000000)
Do not check funct7 for addi,
slti, or andi. In those instructions,
bits 31–25 belong to the immediate and can vary, including when the
immediate is negative.
Use Components → Plexers → Priority Encoder and Components → Memory → ROM. Because this CPU supports 11 instructions, use a 16-input encoder with a four-bit address output and a ROM with 4 address bits and 8 data bits.
Connect match signals to encoder inputs in this order, and place the corresponding control words at the same ROM addresses:
| ROM address / encoder input | Instruction |
|---|---|
| 0 | add |
| 1 | sub |
| 2 | and |
| 3 | or |
| 4 | slt |
| 5 | addi |
| 6 | slti |
| 7 | andi |
| 8 | sll |
| 9 | srl |
| 10 | sra |
| 11–15 | Unused: store 0b00000000 |
Tie unused encoder inputs low. Connect the encoder’s num
output to the ROM address input and tie ROM sel high. Enter
your control words through the ROM editor, then split the ROM output
into the control signals.
The match signals should be mutually exclusive: a supported instruction activates exactly one. The priority encoder is selecting an address, not resolving an intended conflict between instructions.
Handle “no match” explicitly. Use the encoder’s
any signal to qualify writeback:
RegWrite = any AND ROM_data[7]
An unrecognized instruction must not write a register just because
the encoder produces the address of add. This small CPU
does not implement exception handling: for unsupported encodings,
suppress register writes and continue advancing PC by four. Other
control outputs need not have a meaningful operation when no instruction
matches.
Test all 11 instruction patterns. Also check:
add versus sub, and srl
versus sra.RegWrite must be zero.Checkpoint: If addi works with a small
positive immediate but fails with a negative immediate, which decoder
comparison might be too restrictive?
Deliverable: cpu.dig
Leave the supplied PC, PC + 4 logic, and instruction-memory connections in place. Insert your custom ALU, register file, and control unit, then add instruction-field splitters, an immediate extender, and the ALU operand mux.
| Field | Instruction bits | Width |
|---|---|---|
opcode |
6–0 | 7 |
rd |
11–7 | 5 |
funct3 |
14–12 | 3 |
rs1 |
19–15 | 5 |
rs2 |
24–20 | 5 |
funct7 |
31–25 | 7 |
| I-type immediate | 31–20 | 12 |
Connect opcode, funct3, and
funct7 to your control unit. Sign-extend the 12-bit
immediate to 32 bits by replicating instruction bit 31 into bits 31–12
of the extended value.
| Component pin | Connect to |
|---|---|
Register file ReadReg1 |
rs1 |
Register file ReadReg2 |
rs2 |
Register file WriteReg |
rd |
Register file WriteData |
ALU Result |
Register file RegWrite |
Control-unit RegWrite |
Register file Clock |
CPU clock, shared with the PC |
ALU A |
Register-file ReadData1 |
ALU B |
Output of the 32-bit ALUSrc mux |
ALU ALUOp |
Control-unit ALUOp |
For the operand-B mux, input 0 is ReadData2 and input 1
is the sign-extended immediate. Its selector is the one-bit
ALUSrc signal.
All supported instructions write the ALU result to rd;
there is no destination-register mux or memory-writeback mux in this
subset. FlagZ is not needed because this CPU has no
branches.
The in-class register file’s Clock is an ordinary input.
Connect it to the CPU clock named clk; the two
component-boundary labels do not need to match. Do not create a separate
clock for the register file.
Connect the supplied ReadData1 and
ReadData2 output pins to the register-file data
outputs. These are 32-bit values, not the five-bit register numbers
ReadReg1 and ReadReg2.
The ROM holds 1024 32-bit instructions. The PC is a byte address, but each ROM entry holds an entire four-byte instruction, so the supplied circuit uses PC bits 11–2 as the ten-bit ROM index.
ROM index = (PC >> 2) & 0x3ff
Successive instructions occupy ROM indices 0, 1, 2, and so on as the PC advances by four. The starter initializes the PC to 0; using these ten bits alone does not implement a complete 32-bit address decoder or enforce a particular memory region.
Before a rising edge, the current PC selects an instruction; decoding, register reads, immediate extension, and the ALU settle combinationally. At the rising edge:
rd is not x0.After propagation settles, the next instruction appears and its
source values reach ReadData1 and ReadData2.
These two outputs therefore describe the currently selected
instruction’s sources, not automatically the destination of the
instruction that just finished.
For an I-type instruction, bits 24–20 are immediate bits, but the
physical second read port still selects the register with that bit
pattern; the ALU ignores this read value because ALUSrc=1.
Do not force ReadData2 to zero merely because an
instruction is I-type.
Checkpoint: Trace addi x4, x0, -20
through the field splitter, immediate extender, mux, ALU, and
register-file write input before raising the clock.
Start with basic-tests.s. For your first run, you may
load the supplied basic-tests.csv directly; it already
contains this program. When you change the program, use the miniRISC-V
simulator to produce new machine code:
For a visual walkthrough of this process, see How to load instruction memory into ROM.
basic-tests.s, then copy the assembly program into
the miniRISC-V simulator.basic-tests.mem, with one instruction per
line and in program order. Keep only the hexadecimal words—do
not include addresses, comments, or disassembly.python3 format.py basic-tests.mem basic-tests.csvbasic-tests.csv in a text editor or spreadsheet
and copy all of its cells.cpu.dig so the
loaded program is retained.format.py produces rows of hexadecimal values prefixed
with 0x, arranged in groups of eight for the Digital ROM
editor. It also fills the remaining entries with zeros so that loading a
shorter program clears any old instructions. The formatter does
not assemble the .s file: after changing
the assembly, you must repeat the miniRISC-V assembly and memory-dump
steps before running the script. Avoid compressed instructions and
pseudoinstructions that expand to instructions outside this lab’s
subset.
Copy the supplied basic.txt test data into the CPU’s
green test block and run Digital’s tests. The header is:
clk ReadData1 ReadData2
A row using C for clk advances the clock
through a pulse; the expected outputs are the settled read-data values
for the instruction selected afterward. The initial row with clock 0
checks the first instruction before it executes.
Load this three-instruction program at the beginning of instruction memory:
addi x1, x0, 7
addi x2, x1, 9
add x3, x1, x2At a fresh simulation start, use a low clock and inspect these values:
| Point | Currently selected instruction | Expected observation |
|---|---|---|
| Before first rising edge | addi x1, x0, 7 |
ReadData1=0, ALU result = 7 |
| After first rising edge and settling | addi x2, x1, 9 |
x1 = 7; ReadData1=7, ALU result = 16 |
| After second rising edge and settling | add x3, x1, x2 |
x2 = 16; ReadData1=7, ReadData2=16, ALU
result = 23 |
| After third rising edge | Next ROM entry | x3 = 23; inspect register storage or read x3 with a subsequent instruction |
To observe x3 through a CPU output, append
addi x0, x3, 0: its first read port exposes x3 while the
attempted write to x0 has no effect.
Restart simulation before rerunning a program so the PC and register state are initialized consistently. Do not mix observations from separate runs.
The starter’s basic program is a starting point, not complete instruction coverage. Add instructions and matching expected outputs that exercise:
Keep your assembly, loaded ROM contents, and expected-output rows synchronized. Changing the instruction sequence without updating the ROM and test data invalidates the trace.
| Symptom | Check first |
|---|---|
| ALU tests pass, but CPU results are wrong | Instruction field ranges, ALUOp wiring, and operand-mux
input order |
| Positive immediates work but negative ones fail | Sign extension and accidental funct7 checks on I-type
instructions |
srl and sra behave identically |
Full funct7 decode and the SRA control word |
| Every instruction seems to add | Encoder connections, ROM contents, and control-word bit order |
| Tests appear one instruction ahead | Expectations after a clock pulse refer to the newly selected instruction |
| Second read output is unexpected for I-type | It reads the register selected by immediate bits 24–20 even though the ALU ignores it |
| Register writes are missing or delayed | Both PC and register file must use the same clock |
| An invalid instruction changes registers | Qualify ROM write enable with the encoder’s any
signal |
| Circuit works only on your machine | Include every custom .dig dependency and preserve
filenames |
Submit your completed work to the course’s Lab 4 assignment using its posted submission instructions. Include:
cpu.dig and control-unit.dig.control-signals.csv.register-file.dig, alu.dig, and every
custom .dig dependency they use.Before submitting, open cpu.dig from the folder you
intend to submit, verify that the custom components load, and rerun your
tests. Keep a working copy of the completed CPU.