Programming lesson
Designing an ALU and Control Unit in Verilog: A Hands-On Lab Tutorial
Learn how to design a 32-bit ALU and a 10-bit control unit in Verilog for RISC-V. This tutorial covers case statements, testbenches, and simulation on EDA Playground.
Introduction to ALU and Control Unit Design
In this tutorial, we will walk through the design of an Arithmetic Logic Unit (ALU) and a Control Unit (CU) using Verilog, as part of a typical CDA 4205L lab assignment. These modules are the heart of a processor, handling computations and instruction decoding. By the end, you'll be able to implement your own ALU with operations like addition, subtraction, multiplication, division, shifts, and comparisons, and a CU that generates control signals for a RISC-V datapath. This is essential for understanding computer architecture and digital design.
We'll use EDA Playground with Icarus Verilog 0.9.7 as the simulator. The lab is divided into two parts: ALU design and CU design. We'll focus on the ALU in depth, providing a complete example, and then outline the CU approach.
Part 1: 32-Bit ALU Design
Setting Up the Project
First, create a new tab on EDA Playground. Choose Icarus Verilog 0.9.7 as the simulator. Name your project 'alu' and set it to Private. Add the `timescale 1ns/1ns directive at the top of your Verilog module. This sets the simulation time unit and precision.
Module Definition
Define the ALU module with inputs: a 4-bit operation code, two 32-bit signed operands A and B, and a 32-bit signed output register ALUResult. Use reg signed [31:0] ALUResult and input signed [31:0] A, B. The operation input is input [3:0] op.
module alu(
input wire [3:0] op,
input signed [31:0] A, B,
output reg signed [31:0] ALUResult
);
always @(*) begin
case(op)
4'b0010: ALUResult = A + B; // add
4'b0110: ALUResult = A - B; // sub
4'b0111: ALUResult = A * B; // mul
4'b1111: ALUResult = A / B; // div
4'b0100: ALUResult = A << B; // sll
4'b1000: ALUResult = A >> B; // srl (logical shift)
4'b1110: ALUResult = A >>> B; // sra (arithmetic shift)
4'b0000: ALUResult = A & B; // and
4'b0001: ALUResult = A | B; // or
4'b0011: ALUResult = A ^ B; // xor
4'b1000: ALUResult = (A == B) ? 32'd1 : 32'd0; // eq
4'b1001: ALUResult = (A != B) ? 32'd1 : 32'd0; // neq
4'b1010: ALUResult = (A < B) ? 32'd1 : 32'd0; // lt
4'b1011: ALUResult = (A >= B) ? 32'd1 : 32'd0; // geq
default: ALUResult = 32'd0;
endcase
end
endmoduleNote: The shift operations use the lower 5 bits of B (since 32-bit). For division, ensure B is non-zero to avoid X. The comparison results are 1 or 0.
Testbench
Create a testbench module alu_testbench with no ports. Declare regs for inputs and wires for output. Instantiate the ALU, then use an initial block to apply stimuli. Use $monitor to display changes.
module alu_testbench;
reg [3:0] op;
reg signed [31:0] A, B;
wire signed [31:0] ALUResult;
alu uut (.op(op), .A(A), .B(B), .ALUResult(ALUResult));
initial begin
$monitor($time, " op=%b A=%d B=%d result=%d", op, A, B, ALUResult);
A = 10; B = 3;
#5 op = 4'b0010; // add
#5 op = 4'b0110; // sub
#5 op = 4'b0111; // mul
#5 op = 4'b1111; // div
#5 op = 4'b0100; // sll
#5 op = 4'b1000; // srl
#5 op = 4'b1110; // sra
#5 op = 4'b0000; // and
#5 op = 4'b0001; // or
#5 op = 4'b0011; // xor
#5 op = 4'b1000; // eq (A==B?)
#5 op = 4'b1001; // neq
#5 op = 4'b1010; // lt
#5 op = 4'b1011; // geq
#5 $finish;
end
endmoduleRun the simulation. You should see outputs for each operation. Then change A and B to negative numbers (e.g., -10, 3) to test signed arithmetic. Ensure division and shift handle signs correctly.
Part 2: Control Unit Design
The Control Unit decodes the 7-bit opcode (and funct3/funct7) to produce 10 control signals: ALUSrc, MemToReg, RegWrite, MemRead, MemWrite, Branch, Jump, Jalr, and ALUOp (2 bits). For RISC-V, we map instructions as follows:
- R-type (add, sub, etc.): opcode = 7'b0110011 → ALUOp = 10, RegWrite=1, others 0.
- I-type (addi, lw): opcode = 7'b0010011 (addi) or 7'b0000011 (lw) → ALUSrc=1, RegWrite=1, MemToReg=0 for addi, MemToReg=1 for lw, MemRead=1 for lw.
- S-type (sw): opcode = 7'b0100011 → ALUSrc=1, MemWrite=1.
- B-type (beq): opcode = 7'b1100011 → Branch=1, ALUOp=01.
- J-type (jal): opcode = 7'b1101111 → Jump=1, RegWrite=1.
- Jalr: opcode = 7'b1100111 → Jalr=1, RegWrite=1.
Implement the CU as a combinational always block with a case on opcode. Assign each control signal accordingly. Test with a testbench that feeds different opcodes and monitors outputs.
Connecting to Trends: Why This Matters
Understanding ALU and CU design is crucial for building efficient processors used in modern AI accelerators, gaming consoles, and mobile devices. For example, the latest AI chips from companies like NVIDIA and AMD rely on custom ALUs for matrix operations. Even in cryptocurrency mining, specialized ALUs perform hashing. By mastering Verilog design, you're gaining skills for careers in hardware engineering, embedded systems, and chip design – fields that are booming with the rise of RISC-V open-source architecture.
Conclusion
This tutorial covered the essentials of ALU and CU design in Verilog. You learned to implement a 32-bit ALU with multiple operations, write a testbench, and simulate on EDA Playground. For the control unit, you understood how to decode opcodes to generate control signals. Practice by extending the ALU with more operations or implementing a pipelined datapath. These skills are foundational for computer architecture and digital logic design.