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.

OpcodeOperationResultCarry flag
000ADDa + bcarry out of bit 3
001SUBa – bborrow out of bit 3
010ANDa & b0
011ORa | b0
100XORa ^ b0
101SLT(a < b) ? 1 : 00

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 + b with y only 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 y or carry unassigned 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, so 1111 looks larger than 0001. Wrap operands in $signed when 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.

Have questions about this topic?
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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD