Lab 4: Build a RISC-V CPU

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.

Learning goals

By the end of the lab, you should be able to:

Getting started

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:

Also 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.

Supported instructions

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.

Task 1: Plan the control signals

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?

Task 2: Build the instruction decoder and control ROM

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.

Decode the instruction

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.

Select the control word

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.

Check the decoder independently

Test all 11 instruction patterns. Also check:

Checkpoint: If addi works with a small positive immediate but fails with a negative immediate, which decoder comparison might be too restrictive?

Task 3: Connect the datapath

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.

Extract instruction fields

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.

Wire the register file and ALU

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.

Understand the supplied instruction memory

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.

Understand one clock cycle

Before a rising edge, the current PC selects an instruction; decoding, register reads, immediate extension, and the ALU settle combinationally. At the rising edge:

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.

Task 4: Load and test a program

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.

  1. Open basic-tests.s, then copy the assembly program into the miniRISC-V simulator.
  2. Click Assemble. Fix any assembler errors before continuing.
  3. Choose Dump Memory to view the assembled instruction words.
  4. Copy the 32-bit hexadecimal instruction words into basic-tests.mem, with one instruction per line and in program order. Keep only the hexadecimal words—do not include addresses, comments, or disassembly.
  5. Run the supplied formatter:
python3 format.py basic-tests.mem basic-tests.csv
  1. Open basic-tests.csv in a text editor or spreadsheet and copy all of its cells.
  2. In Digital, right-click the instruction-memory ROM and choose Edit. Select the cell at address 0 and paste the CSV contents. You may instead use the ROM editor’s Edit → File → Load command to load the CSV.
  3. Verify that the first few ROM words match the miniRISC-V memory dump, in order. Close the editor and save 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.

A short manual trace

Load this three-instruction program at the beginning of instruction memory:

addi x1, x0, 7
addi x2, x1, 9
add  x3, x1, x2

At 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.

Extend your testing

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.

Debugging checkpoints

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

Submission

Submit your completed work to the course’s Lab 4 assignment using its posted submission instructions. Include:

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.