Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
    • Digital Logic for RTL Engineers
    • Number Systems for RTL Engineers
    • Verilog Fundamentals
    • Verilog Operators and Types
    • SystemVerilog for RTL
    • Simulation and Testbenches
      • Minimal but Robust Testbench Structure
      • Clock and Reset Best Practices
      • Self-Checking and Observability
      • Useful Runtime Instrumentation
      • Verification Quality Gates
      • First Project: a 4-bit ALU
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹Simulation and Testbenches
  1. Topics
  2. RTL Design
  3. RTL Foundations
  4. Simulation and Testbenches
  5. First Project: a 4-bit ALU

Part 1 · Foundations · Beginner

First Project: a 4-bit ALU

A hands-on mini project: specify, write and prove a 4-bit ALU with flags, checked on all 2048 input combinations and synthesised without a latch.

On this page

    Why this matters

    A 4-bit ALU with eight operations has only 2048 possible inputs, so it can be proven on every one of them. Most first ALUs still ship a latch on a flag or swap carry and overflow on subtract, because the spec never said what each flag means for each op.

    After this lesson you can: write a frozen spec for a small combinational block, build it as one always_comb with defaults, prove it exhaustively against a differently written model, and show it synthesises with no latch.

    A block you can test completely

    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.

    The specification

    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.

    The block diagram

    The ALU as built: three units see the operands, the op code drives one result select that also picks C and V, and shared flag logic forms Z and N from the result

    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 RTL, line by line

    The code is in the Testbench section below. Read it in three parts.

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

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

    3. 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:

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

    Proving it on all 2048 inputs

    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:

    Diagram
    $ 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:

    Diagram
    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 wrong

    Done when

    The project is finished when every row below is true, and you have run each command yourself.

    Check

    How

    Done when

    Clean compile

    iverilog -g2012 -Wall on the design and bench

    No output at all

    Every case passes

    vvp sim.out

    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 synth, then stat

    Only gates in the cell list, no latch cell

    The synthesis check, trimmed to the lines that matter:

    Diagram
    $ 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:

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

    Common mistakes

    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

    Common pitfall

    Carry and overflow are different answers for SUB. A design that reports the borrow as overflow flags 0 − 1 as an overflow; one that reads C as overflow calls −8 − 1 fine. The broken design above made the first swap and failed 112 of the 2048 cases. Decide what C means for SUB, write it in the spec table, and give V its own sign rule.

    Check yourself op = SUB, a = 0101, b = 1011. Give y and the four flags.

    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.

    Check yourself All 2048 cases pass. Name a bug this bench still cannot find.

    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.

    Stretch goals

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

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

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

    Project files

    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

    alu4.sv

    The design: the 4-bit ALU with Z, N, C and V flags

    tb_alu4.sv

    The exhaustive self-checking bench and its integer reference model

    run.sh

    Diagram
    #!/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 count

    Expected output: the line a correct design makes the simulation print at the end of its run.

    Diagram
    PASS: alu4 matches the integer model on all 2048 cases (8 ops x 16 a x 16 b)

    Testbench

    The design and the exhaustive bench in one listing.

    alu4.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // ---- 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
    

    Remember

    • Write a value for every output in every row of the spec before writing RTL; a blank cell becomes a latch or an argument.

    • Give every output a default at the top of always_comb, then let each case item override only what it defines.

    • C is the unsigned answer and V the signed one; for SUB they are independent, so define C (borrow or no borrow) explicitly.

    Rule of thumb: a block is done when a bench that you have seen fail now passes on every input, and synthesis shows only the cells you expected.

    How this was checked

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

    Keep going

    • Practice: Simple ALU with add, sub, AND, OR, XOR

    • Code lab: 4-bit ALU slice

    • Interview: run a mock interview on this topic

    Related topics

    • Overflow detection patterns

    • Self-checking and observability

    Previous

    36 / 350

    Next: Combinational Design

    Up next

    Up nextCombinational Design

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Minimal but Robust Testbench Structure
    • Clock and Reset Best Practices
    • Self-Checking and Observability