A 4-bit ALU with a 3-bit opcode has 16 x 16 x 8 = 2048 possible inputs. That is small enough to test every one of them in one short simulation, so this project ends with proof, not with a feeling that it works. You will freeze a specification, draw the block, write it as one always_comb, and check it against a reference model written a different way.
The project reuses what Foundations taught: two's complement, the C and V flags, a self-checking bench and a quality gate. Each step links back instead of repeating it.
Freeze the spec before writing RTL. Inputs a and b are 4 bits, op is 3 bits, and every one of the eight codes is used. The outputs are a 4-bit result y and four flags.
op | Name | y | C | V |
|---|---|---|---|---|
000 | ADD | a + b, low 4 bits | carry out of bit 3 | signed overflow of a + b |
001 | SUB | a − b, low 4 bits | 1 = no borrow (unsigned a ≥ b) | signed overflow of a − b |
010 | AND | bitwise AND | 0 | 0 |
011 | OR | bitwise OR | 0 | 0 |
100 | XOR | bitwise XOR | 0 | 0 |
101 | SLT | 0001 if signed a < signed b, else 0000 | 0 | 0 |
110 | SHL | a shifted left by 1, 0 enters | a[3], the bit shifted out | 0 |
111 | SHR | a shifted right by 1, 0 enters | a[0], the bit shifted out | 0 |
For every op, Z is 1 when y is 0000 and N is y[3]. Writing a value for every flag in every row is the point: a blank cell becomes a latch or a disagreement between two engineers. The C convention for SUB (1 means no borrow) matches the one in Overflow detection patterns, where the flag formulas are derived.
Read it top to bottom: every unit computes all the time, and the op code only chooses which answer leaves.
Three units, always busy: the adder and subtractor, the logic gates and the shifter all see a and b on every input change.
One select: the case (op) statement is a mux. It picks y, and also C and V, because their meaning depends on the op.
Shared flags: Z and N depend only on y, so they sit after the select and are written once.
The code is in the Testbench section below. Read it in three parts.
Shared arithmetic, outside the case. {add_c, add_y} = {1'b0, a} + {1'b0, b} widens both operands to 5 bits so the carry lands in add_c. Subtraction is a + ~b + 1 with the same trick. The V formulas are the sign rules: same signs in and a different sign out for ADD; different signs in and the sign of a lost for SUB. SLT is N XOR V of the subtraction, which stays correct even when a − b overflows.
The unit, one always_comb. Defaults come first: y, c and v get 0 before the case, so every path assigns every output and no latch can form. Each case item then overrides only what that op defines.
Flags after the select. z and n are continuous assignments on y, so no op can forget them.
The default item is unreachable, because all eight codes are listed; it is kept so that an unknown op in simulation gives a defined 0000 rather than holding a stale value. A unique case would ask the simulator to report overlapping or missing items, but this version of Icarus Verilog ignores it and prints a note instead:
alu4_unique.sv:27: vvp.tgt sorry: Case unique/unique0 qualities are ignored.A note like that fails the "no warnings" gate, so this project uses a plain case with defaults and a default item. Verilator and synthesis tools do honour unique.
The bench loops over an 11-bit number (3 opcode bits plus 4 + 4 operand bits) and splits it into {op, a, b}, so every combination appears exactly once. Count the bits before you size the loop: a 12-bit loop here would run every case twice and report twice the work it did. The reference model is written differently on purpose: it works in plain integers, finds the signed value with (a > 7) ? ua - 16 : ua instead of $signed, detects carry as ua + ub > 15 and overflow as a sum outside −8 to 7, and shifts by multiplying or dividing by 2. A mistake would have to be made twice, in two different styles, to slip through.
Running it:
$ iverilog -g2012 -Wall -o sim.out alu4.sv tb_alu4.sv
$ vvp sim.out
PASS: alu4 matches the integer model on all 2048 cases (8 ops x 16 a x 16 b)The compile line printed nothing: no warnings. A bench that has never failed proves little, so break the design once and watch it fail. Setting V for SUB to the borrow (v = ~sub_c) gives:
MISMATCH op=1 a=0000 b=0001: y=1111 want 1111, ZNCV=0101 want 0100
MISMATCH op=1 a=0000 b=0010: y=1110 want 1110, ZNCV=0101 want 0100
...
FATAL: tb_alu4.sv:38: FAIL: 112 of 2048 cases wrongThe project is finished when every row below is true, and you have run each command yourself.
Check | How | Done when |
|---|---|---|
Clean compile |
| No output at all |
Every case passes |
| The PASS line with 2048 cases |
Flags defined for every op | The spec table above | No blank C or V cell; Z and N assigned outside the case |
Bench can fail | Break one flag on purpose | MISMATCH lines and a FAIL |
No latch | Yosys | Only gates in the cell list, no latch cell |
The synthesis check, trimmed to the lines that matter:
$ yosys -p "read_verilog -sv alu4.sv; synth -top alu4; stat"
...
No latch inferred for signal `\alu4.\c' from process `\alu4.$proc$alu4.sv:23$12'.
No latch inferred for signal `\alu4.\y' from process `\alu4.$proc$alu4.sv:23$12'.
No latch inferred for signal `\alu4.\v' from process `\alu4.$proc$alu4.sv:23$12'.
...
115 cells
8 $_ANDNOT_
31 $_AND_
44 $_NAND_
3 $_NOR_
12 $_ORNOT_
11 $_OR_
4 $_XNOR_
2 $_XOR_All 115 cells are gates; there is no $_DLATCH_ line. Delete the c = 1'b0; default and the same command stops at the process step:
ERROR: Latch inferred for signal `\alu4.\c' from always_comb process `\alu4.$proc$alu4_latch.sv:23$12'.C is then missing on the AND, OR, XOR and SLT paths. Because the block is written as always_comb, Yosys treats the latch as an error rather than building it. Why that happens is covered in The complete-assignment pattern.
C reads the bits as unsigned and V reads them as signed, so for SUB one can be set without the other. Two subtractions from the spec, as the RTL computes them:
a − b | y | Unsigned view | Signed view | C | V |
|---|---|---|---|---|---|
0000 − 0001 | 1111 | 0 − 1 borrows | 0 − 1 = −1 fits | 0 | 0 |
1000 − 0001 | 0111 | 8 − 1 = 7, no borrow | −8 − 1 = −9 does not fit; y reads +7 | 1 | 1 |
y = 0101 + 0100 + 1 = 1010. Z = 0, N = 1. Unsigned, 5 − 11 borrows, so C = 0. Signed, 5 − (−5) = 10 does not fit in −8 to 7, so V = 1. The bench run on these inputs gives exactly y = 1010 and ZNCV = 0101.
A mistake in the specification itself. If you and the reference model both believe C for SUB means borrow, the RTL and the model agree and the bench passes. Exhaustive testing proves the RTL matches the model; a review of the spec table proves the model matches what the user needs.
Add an op. Replace SHR with an arithmetic shift right (the sign bit is copied in) or widen op to 4 bits and add it. Update the spec table, the reference model and the case count. See Barrel shifter, rotate and arithmetic shift.
Register the outputs. Put y and the flags in flops so the next block sees a clean value at the clock edge, and move the checks one cycle later. See D flip-flop with asynchronous reset.
Add a valid/ready wrapper. Accept {op, a, b} when in_valid && in_ready and hold the result until the consumer takes it. See Valid/ready flow control.
The complete listing below contains every file of this project, each starting at a // ---- file: <name> ---- line. Save each part under its name in one folder, then run the script.
File | What it holds |
|---|---|
| The design: the 4-bit ALU with Z, N, C and V flags |
| The exhaustive self-checking bench and its integer reference model |
run.sh
#!/bin/sh
set -e
iverilog -g2012 -Wall -o sim.out alu4.sv tb_alu4.sv
vvp sim.out
yosys -q -p "read_verilog -sv alu4.sv; synth -top alu4; stat" # optional: no latch, cell countExpected output: the line a correct design makes the simulation print at the end of its run.
PASS: alu4 matches the integer model on all 2048 cases (8 ops x 16 a x 16 b)The design and the exhaustive bench in one listing.
// ---- file: alu4.sv ----
module alu4 (
input logic [3:0] a, b,
input logic [2:0] op,
output logic [3:0] y,
output logic z, n, c, v
);
localparam logic [2:0] ADD = 3'd0, SUB = 3'd1, AND = 3'd2, OR = 3'd3,
XOR = 3'd4, SLT = 3'd5, SHL = 3'd6, SHR = 3'd7;
// Shared arithmetic: one extra bit on the left catches the carry out.
logic [3:0] add_y, sub_y;
logic add_c, sub_c, add_v, sub_v, lt;
logic a_msb, a_lsb;
assign a_msb = a[3];
assign a_lsb = a[0];
assign {add_c, add_y} = {1'b0, a} + {1'b0, b};
assign {sub_c, sub_y} = {1'b0, a} + {1'b0, ~b} + 5'd1; // a - b; sub_c = 1 means no borrow
assign add_v = (a[3] == b[3]) && (add_y[3] != a[3]); // same signs in, other sign out
assign sub_v = (a[3] != b[3]) && (sub_y[3] != a[3]); // different signs in, sign of a lost
assign lt = sub_y[3] ^ sub_v; // signed a < b: N xor V of a - b
// The unit: one case picks the result, C and V for each op.
always_comb begin
y = 4'd0; // defaults first: no path can leave an output unassigned
c = 1'b0;
v = 1'b0;
case (op)
ADD: begin y = add_y; c = add_c; v = add_v; end
SUB: begin y = sub_y; c = sub_c; v = sub_v; end
AND: y = a & b;
OR: y = a | b;
XOR: y = a ^ b;
SLT: y = {3'b000, lt}; // 1 when signed a < b, else 0
SHL: begin y = a << 1; c = a_msb; end // C = the bit shifted out
SHR: begin y = a >> 1; c = a_lsb; end // logical: a 0 enters at the top
default: y = 4'd0; // all 8 codes are listed; kept for X on op
endcase
end
// Flag logic shared by every op.
assign z = (y == 4'd0);
assign n = y[3];
endmodule
Tools: Icarus Verilog 13.0, Yosys 0.69+post (git sha1 143eb14f9), Verilator 5.048; run on 2026-10-17. Commands: iverilog -g2012 -Wall -o sim.out alu4.sv tb_alu4.sv && vvp sim.out; yosys -p "read_verilog -sv alu4.sv; synth -top alu4; stat"; verilator --lint-only -Wall alu4.sv.
Result: the compile prints nothing, the bench prints the PASS line for 2048 cases (each case once; an earlier 12-bit loop ran every case twice and reported 4096), Yosys reports No latch inferred for c, y and v and 115 gate cells with no latch cell, and Verilator lint is clean.
Variants, each run with the same commands: alu4_bad.sv (v = ~sub_c for SUB) fails 112 of 2048 cases with the MISMATCH lines shown; alu4_unique.sv (unique case) makes Icarus print the sorry note shown; alu4_latch.sv (no c default) makes Yosys stop with the Latch inferred ERROR shown. The SUB examples and the check-yourself answer come from a small directed bench on the same alu4.sv.
Scope and limits: functional, zero-delay and exhaustive over 2-state inputs; it does not check X or Z inputs, timing, or area on a real library (Yosys generic gates only).