4-Bit ALU Verilog Code with Testbench
An arithmetic logic unit (ALU) is the block inside every processor that turns an operation code into a result. Below is complete, runnable 4-bit ALU Verilog code that selects add, subtract, AND, OR, XOR, or set-less-than from a single 3-bit opcode, exposes carry and zero flags, and ships with a testbench that exercises every operation. Copy it into a file, compile it with Icarus Verilog, and read the simulation log to confirm each result by hand.
// 4-bit ALU: add/sub/and/or/xor/slt selected by a 3-bit opcode.
module alu4 (
input wire [3:0] a,
input wire [3:0] b,
input wire [2:0] opcode,
output reg [3:0] y,
output reg carry,
output wire zero
);
localparam OP_ADD = 3'b000,
OP_SUB = 3'b001,
OP_AND = 3'b010,
OP_OR = 3'b011,
OP_XOR = 3'b100,
OP_SLT = 3'b101;
reg [4:0] ext; // one extra bit to hold carry / borrow
always @(*) begin
y = 4'b0000; // defaults keep the block purely combinational
carry = 1'b0;
ext = 5'b00000;
case (opcode)
OP_ADD: begin
ext = {1'b0, a} + {1'b0, b};
y = ext[3:0];
carry = ext[4];
end
OP_SUB: begin
ext = {1'b0, a} - {1'b0, b};
y = ext[3:0];
carry = ext[4]; // 1 => a borrow was needed
end
OP_AND: y = a & b;
OP_OR : y = a | b;
OP_XOR: y = a ^ b;
OP_SLT: y = ($signed(a) < $signed(b)) ? 4'd1 : 4'd0;
default: y = 4'b0000;
endcase
end
assign zero = (y == 4'b0000);
endmodule
// Testbench: one representative vector per operation.
module tb_alu4;
reg [3:0] a, b;
reg [2:0] opcode;
wire [3:0] y;
wire carry, zero;
alu4 dut (.a(a), .b(b), .opcode(opcode), .y(y), .carry(carry), .zero(zero));
initial begin
a=4'b0110; b=4'b0101; opcode=3'b000; #1
$display("ADD a=%b b=%b -> y=%b (%0d) carry=%b zero=%b", a,b,y,y,carry,zero);
a=4'b1001; b=4'b1000; opcode=3'b000; #1
$display("ADD a=%b b=%b -> y=%b (%0d) carry=%b zero=%b", a,b,y,y,carry,zero);
a=4'b0111; b=4'b0010; opcode=3'b001; #1
$display("SUB a=%b b=%b -> y=%b (%0d) carry=%b zero=%b", a,b,y,y,carry,zero);
a=4'b0010; b=4'b0101; opcode=3'b001; #1
$display("SUB a=%b b=%b -> y=%b (%0d) carry=%b zero=%b", a,b,y,y,carry,zero);
a=4'b1100; b=4'b1010; opcode=3'b010; #1
$display("AND a=%b b=%b -> y=%b (%0d) carry=%b zero=%b", a,b,y,y,carry,zero);
a=4'b1100; b=4'b1010; opcode=3'b011; #1
$display("OR a=%b b=%b -> y=%b (%0d) carry=%b zero=%b", a,b,y,y,carry,zero);
a=4'b1100; b=4'b1010; opcode=3'b100; #1
$display("XOR a=%b b=%b -> y=%b (%0d) carry=%b zero=%b", a,b,y,y,carry,zero);
a=4'b1111; b=4'b0001; opcode=3'b101; #1
$display("SLT a=%b b=%b -> y=%b (%0d) carry=%b zero=%b", a,b,y,y,carry,zero);
a=4'b0011; b=4'b0011; opcode=3'b100; #1
$display("XOR a=%b b=%b -> y=%b (%0d) carry=%b zero=%b", a,b,y,y,carry,zero);
$finish;
end
endmodule
ADD a=0110 b=0101 -> y=1011 (11) carry=0 zero=0 ADD a=1001 b=1000 -> y=0001 (1) carry=1 zero=0 SUB a=0111 b=0010 -> y=0101 (5) carry=0 zero=0 SUB a=0010 b=0101 -> y=1101 (13) carry=1 zero=0 AND a=1100 b=1010 -> y=1000 (8) carry=0 zero=0 OR a=1100 b=1010 -> y=1110 (14) carry=0 zero=0 XOR a=1100 b=1010 -> y=0110 (6) carry=0 zero=0 SLT a=1111 b=0001 -> y=0001 (1) carry=0 zero=0 XOR a=0011 b=0011 -> y=0000 (0) carry=0 zero=1 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpjh96swit/alu4_demo.v:73: $finish called at 9 (1s)
Build and run it. Save the listing as alu4_demo.v and run a single command: iverilog -g2005 -o sim alu4_demo.v && vvp sim. Icarus elaborates the alu4 design together with its testbench, and vvp prints one line per vector. No waveform viewer is required to follow this example, though you can add $dumpfile and $dumpvars later if you want to inspect it in GTKWave.
How the ALU is wired
The module takes two 4-bit operands, a and b, and a 3-bit opcode. It produces a 4-bit result y, a carry flag, and a zero flag. The six operations are given symbolic names with localparam so the case statement reads like a table instead of a wall of binary literals. Keeping the opcode as a named constant also means you can renumber operations later by editing one line.
| Opcode | Operation | Result | Carry flag |
|---|---|---|---|
000 | ADD | a + b | carry out of bit 3 |
001 | SUB | a – b | borrow out of bit 3 |
010 | AND | a & b | 0 |
011 | OR | a | b | 0 |
100 | XOR | a ^ b | 0 |
101 | SLT | (a < b) ? 1 : 0 | 0 |
The arithmetic path is the only part that needs care. Adding two 4-bit numbers can produce a 5-bit answer, so the code zero-extends both operands into a 5-bit vector ext before adding. Bit 4 of that vector is the carry out, and the low four bits are the result. Subtraction reuses the same trick: ext holds a – b, and bit 4 becomes the borrow indicator. The logic operations (AND, OR, XOR) are bitwise and need no extra width, so they drive y directly and leave carry at its default of zero.
Set-less-than (slt) compares the two operands and returns 1 when a is less than b, otherwise 0. The comparison uses the $signed system function so that 1111 is read as negative one rather than fifteen, which matches how slt behaves in real instruction sets. Change $signed to a plain comparison if you want an unsigned less-than instead.
Two details keep the block synthesizable and glitch-free. First, the always block begins by assigning defaults to y, carry, and ext; without those defaults a missing case branch would infer a latch. Second, the zero flag is a continuous assignment that simply tests whether y is all zeros, so it always tracks the current result. The default branch of the case guards against an undefined opcode by forcing the result to zero.
Verify it with a self-checking testbench
Printing values is useful, but a testbench that checks itself is what you want before trusting a design. The version below keeps a small reference model written as a Verilog function, drives both a negative and a positive operand pair through all six opcodes, and compares the hardware output against the model. If every vector matches it prints a single pass line; any mismatch is flagged with the exact inputs. This is the pattern you scale up later with randomized stimulus.
// 4-bit ALU (same design) with a self-checking testbench.
module alu4 (
input wire [3:0] a,
input wire [3:0] b,
input wire [2:0] opcode,
output reg [3:0] y,
output reg carry,
output wire zero
);
localparam OP_ADD = 3'b000,
OP_SUB = 3'b001,
OP_AND = 3'b010,
OP_OR = 3'b011,
OP_XOR = 3'b100,
OP_SLT = 3'b101;
reg [4:0] ext;
always @(*) begin
y = 4'b0000;
carry = 1'b0;
ext = 5'b00000;
case (opcode)
OP_ADD: begin ext = {1'b0,a} + {1'b0,b}; y = ext[3:0]; carry = ext[4]; end
OP_SUB: begin ext = {1'b0,a} - {1'b0,b}; y = ext[3:0]; carry = ext[4]; end
OP_AND: y = a & b;
OP_OR : y = a | b;
OP_XOR: y = a ^ b;
OP_SLT: y = ($signed(a) < $signed(b)) ? 4'd1 : 4'd0;
default: y = 4'b0000;
endcase
end
assign zero = (y == 4'b0000);
endmodule
// Self-checking testbench: compare the DUT against a reference model.
module tb_alu4_check;
reg [3:0] a, b;
reg [2:0] opcode;
wire [3:0] y;
wire carry, zero;
reg [3:0] exp;
integer errors, i;
alu4 dut (.a(a), .b(b), .opcode(opcode), .y(y), .carry(carry), .zero(zero));
function [3:0] model(input [3:0] av, input [3:0] bv, input [2:0] op);
case (op)
3'b000: model = av + bv;
3'b001: model = av - bv;
3'b010: model = av & bv;
3'b011: model = av | bv;
3'b100: model = av ^ bv;
3'b101: model = ($signed(av) < $signed(bv)) ? 4'd1 : 4'd0;
default: model = 4'b0000;
endcase
endfunction
task check;
begin
exp = model(a, b, opcode);
if (y !== exp) begin
errors = errors + 1;
$display("MISMATCH op=%b a=%b b=%b got=%b exp=%b", opcode, a, b, y, exp);
end else begin
$display("ok op=%b a=%b b=%b -> y=%b zero=%b", opcode, a, b, y, zero);
end
end
endtask
initial begin
errors = 0;
for (i = 0; i < 6; i = i + 1) begin
opcode = i[2:0];
a = 4'b1011; b = 4'b0110; #1 check;
a = 4'b0100; b = 4'b0100; #1 check;
end
if (errors == 0) $display("RESULT: ALL CHECKS PASSED");
else $display("RESULT: %0d MISMATCH(ES)", errors);
$finish;
end
endmodule
ok op=000 a=1011 b=0110 -> y=0001 zero=0 ok op=000 a=0100 b=0100 -> y=1000 zero=0 ok op=001 a=1011 b=0110 -> y=0101 zero=0 ok op=001 a=0100 b=0100 -> y=0000 zero=1 ok op=010 a=1011 b=0110 -> y=0010 zero=0 ok op=010 a=0100 b=0100 -> y=0100 zero=0 ok op=011 a=1011 b=0110 -> y=1111 zero=0 ok op=011 a=0100 b=0100 -> y=0100 zero=0 ok op=100 a=1011 b=0110 -> y=1101 zero=0 ok op=100 a=0100 b=0100 -> y=0000 zero=1 ok op=101 a=1011 b=0110 -> y=0001 zero=0 ok op=101 a=0100 b=0100 -> y=0000 zero=1 RESULT: ALL CHECKS PASSED /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpjb9qbzs3/alu4_selfcheck.v:81: $finish called at 12 (1s)
Common mistakes to avoid
These are the errors that trip up most newcomers writing ALU code:
- Dropping the carry bit. If you write
y = a + bwithyonly four bits wide, the carry is silently discarded and you can never detect overflow. Always compute into a wider vector and slice off the top bit. - Using the wrong assignment type. Inside a combinational
always @(*)block, use blocking assignments (=), as shown here. Non-blocking (<=) belongs in clocked sequential blocks, not in combinational logic. - Forgetting the default assignments. Leaving
yorcarryunassigned on some path makes the synthesizer infer a latch, which turns a clean combinational ALU into a timing hazard. Assign defaults at the top of the block. - Confusing signed and unsigned compare. Plain
<treats operands as unsigned, so1111looks larger than0001. Wrap operands in$signedwhen you need a signed slt, and be deliberate about which you want. - Mismatched opcode widths. If the opcode port is narrower than the number of operations you encode, the high operations alias onto low ones. Size the opcode to cover every case value.
Quick check
Why is the internal ext vector 5 bits wide and not 4?
Adding or subtracting two 4-bit numbers can produce a result that needs a fifth bit. That extra bit (bit 4) captures the carry out on ADD and the borrow on SUB, while bits 3:0 form the 4-bit result.
Why does the always block assign defaults to y and carry before the case?
So the block stays fully combinational. If a branch left an output undriven, the tool would infer a latch; assigning defaults first guarantees every output has a value on every path.
What does wrapping the operands in $signed change about the slt result?
It makes the comparison signed, so 1111 is treated as -1 rather than 15. Without $signed the comparison is unsigned and 1111 would be the largest value.
Which assignment style belongs in a combinational always @(*) block?
Blocking assignments (=). Non-blocking assignments (<=) are reserved for clocked sequential logic such as flip-flops in an always @(posedge clk) block.
Share your question in comments or talk to our mentor team for batch guidance.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses