Half Adder — Design & Testbench
Complete half adder implementation at four abstraction levels — gate, data flow, behavioral, and UDP — each paired with a comprehensive self-checking testbench, waveform output, and simulation log.
➕ Introduction & Theory
A half adder is the most fundamental arithmetic circuit in digital design. It adds two single-bit binary numbers and produces two outputs: a sum bit and a carry bit. It is called a “half” adder because it does not accept a carry input — it can only add two bits, not three (for three-bit addition, a full adder is needed).
📊 Truth Table & Boolean Equations
The half adder’s behaviour is completely described by this truth table. With only two 1-bit inputs, there are exactly four possible input combinations:
| a | b | sum | carry | Meaning |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0+0 = 0, no carry |
| 0 | 1 | 1 | 0 | 0+1 = 1, no carry |
| 1 | 0 | 1 | 0 | 1+0 = 1, no carry |
| 1 | 1 | 0 | 1 | 1+1 = 10₂ (sum=0, carry=1) |
· = AND (Conjunction)
No simplification possible — already minimal.
Notice the key pattern: sum is 1 whenever exactly one of the inputs is 1 (XOR behaviour), while carry is 1 only when both inputs are 1 (AND behaviour). When both inputs are 1, the binary result is 10₂ — sum bit is 0, carry bit is 1.
🔌 Circuit Diagram
🔗 Gate Level Implementation
The gate level model directly instantiates primitive gate cells — the most explicit description, mirroring the circuit diagram one-to-one.
// ============================================================ // Module : half_adder_gate // Level : Gate Level (Structural) // Function : sum = a XOR b // carry = a AND b // ============================================================ `timescale 1ns/1ps `default_nettype none module half_adder_gate ( input a, // first addend input b, // second addend output sum, // a XOR b output carry // a AND b ); // Instantiate built-in gate primitives xor g_sum (sum, a, b); // output first, then inputs and g_carry (carry, a, b); endmodule `default_nettype wire
⚡ Data Flow Implementation
The data flow model uses continuous assign statements — more concise than gate level while remaining synthesizable. The logic equations map directly to assign statements.
// ============================================================ // Module : half_adder_dataflow // Level : Data Flow // Equations: sum = a ^ b | carry = a & b // ============================================================ `timescale 1ns/1ps `default_nettype none module half_adder_dataflow ( input a, input b, output sum, output carry ); // Continuous assignments — both active simultaneously assign sum = a ^ b; // XOR: 1 when inputs differ assign carry = a & b; // AND: 1 only when both are 1 endmodule `default_nettype wire
🧠 Behavioral Implementation
The behavioral model uses an always @(*) block — procedural combinational logic. The sensitivity list @(*) ensures the block re-evaluates whenever any input changes.
// ============================================================ // Module : half_adder_behavioral // Level : Behavioral // Uses always @(*) for combinational logic description // ============================================================ `timescale 1ns/1ps `default_nettype none module half_adder_behavioral ( input a, input b, output reg sum, // reg because driven inside always block output reg carry ); always @(*) begin // re-evaluate any time a or b changes sum = a ^ b; // blocking — combinational intent carry = a & b; end endmodule `default_nettype wire
always @(*) combinational blocks, use blocking assignments. Non-blocking is for sequential flip-flop logic. Using <= here would still simulate correctly for a half adder but is a style error that confuses readers and tools.
🧬 UDP Implementation
The UDP (User-Defined Primitive) model defines the half adder’s behaviour using truth tables — two separate UDPs, one for each output, then combined in a wrapper module.
// ============================================================ // UDPs for Half Adder — one UDP per output // (UDPs can have only one output — so two are needed) // ============================================================ `timescale 1ns/1ps // ── UDP 1: XOR for SUM ──────────────────────────────────────── primitive udp_xor (out, a, b); output out; input a, b; table // a b : out 0 0 : 0; // 0⊕0=0 0 1 : 1; // 0⊕1=1 1 0 : 1; // 1⊕0=1 1 1 : 0; // 1⊕1=0 x 0 : x; // unknown a, b=0 → x 0 x : x; // a=0, unknown b → x 1 x : x; x 1 : x; x x : x; endtable endprimitive // ── UDP 2: AND for CARRY ────────────────────────────────────── primitive udp_and (out, a, b); output out; input a, b; table // a b : out 0 ? : 0; // any a=0 → carry=0 ? 0 : 0; // any b=0 → carry=0 1 1 : 1; // both 1 → carry=1 x 1 : x; // unknown a, b=1 → x 1 x : x; // a=1, unknown b → x endtable endprimitive // ── Wrapper module using both UDPs ──────────────────────────── `default_nettype none module half_adder_udp ( input a, b, output sum, carry ); udp_xor u_sum (sum, a, b); udp_and u_carry (carry, a, b); endmodule `default_nettype wire
🧪 Comprehensive Testbench
The testbench is a non-synthesizable module that instantiates the DUT (Device Under Test), applies all four possible input combinations, compares outputs against a pre-computed golden reference, and reports pass/fail for each test case. It is designed to be self-checking — no manual waveform inspection needed.
// ============================================================ // Testbench : half_adder_tb // DUT : half_adder_gate (swap module name to test others) // Strategy : Exhaustive — all 4 input combinations (2^2) // Checking : Golden reference table, $fatal on first failure // ============================================================ `timescale 1ns/1ps `default_nettype none module half_adder_tb; // ── DUT ports ───────────────────────────────────────────── reg a, b; wire sum, carry; // ── DUT instantiation (gate level) ──────────────────────── half_adder_gate dut ( .a (a ), .b (b ), .sum (sum ), .carry(carry) ); // ── Waveform dump ────────────────────────────────────────── initial begin $dumpfile("half_adder.vcd"); $dumpvars(0, half_adder_tb); end // ── Test tracking ────────────────────────────────────────── integer pass_cnt = 0; integer fail_cnt = 0; integer test_num = 0; // ── Self-checking task ───────────────────────────────────── task check; input exp_sum, exp_carry; begin test_num = test_num + 1; #1; // allow outputs to settle if (sum === exp_sum && carry === exp_carry) begin $display(" PASS [%0d] a=%b b=%b | sum=%b carry=%b", test_num, a, b, sum, carry); pass_cnt = pass_cnt + 1; end else begin $display(" FAIL [%0d] a=%b b=%b | got sum=%b carry=%b | exp sum=%b carry=%b", test_num, a, b, sum, carry, exp_sum, exp_carry); fail_cnt = fail_cnt + 1; end end endtask // ── Stimulus and checking ────────────────────────────────── initial begin $display(""); $display("================================================"); $display(" Half Adder Testbench — Exhaustive Verification"); $display("================================================"); $display(" Test a b | sum carry"); $display(" --------------------------------"); // Initialise a = 1'b0; b = 1'b0; #10; // ── Test 1: a=0, b=0 → sum=0, carry=0 ───────────────── a = 1'b0; b = 1'b0; check(1'b0, 1'b0); // ── Test 2: a=0, b=1 → sum=1, carry=0 ───────────────── a = 1'b0; b = 1'b1; check(1'b1, 1'b0); // ── Test 3: a=1, b=0 → sum=1, carry=0 ───────────────── a = 1'b1; b = 1'b0; check(1'b1, 1'b0); // ── Test 4: a=1, b=1 → sum=0, carry=1 ───────────────── a = 1'b1; b = 1'b1; check(1'b0, 1'b1); // ── Summary ──────────────────────────────────────────── $display(" --------------------------------"); $display(" Results: %0d / %0d PASS | %0d FAIL", pass_cnt, test_num, fail_cnt); $display("================================================"); $display(""); if (fail_cnt == 0) $display(" ✅ ALL TESTS PASSED"); else $fatal(1, " ❌ %0d TEST(S) FAILED", fail_cnt); #10; $finish; end // ── Continuous monitoring ────────────────────────────────── initial $monitor(" @%0t a=%b b=%b sum=%b carry=%b", $time, a, b, sum, carry); endmodule `default_nettype wire
Testbench Design Decisions
| Decision | Rationale |
|---|---|
| Exhaustive stimulus | With only 4 input combinations (2²), full exhaustive testing is trivial and complete. No random or directed subset needed. |
| #1 settle delay | Applied inside the check task before sampling outputs. Allows propagation through combinational logic before reading. |
| === not == | Case equality catches x values — if a bug causes the output to be x, == would evaluate to x (treated as false), but === explicitly checks that expected matches actual including x/z. |
| $fatal on failure | Non-zero exit code signals failure to CI systems. More reliable than manual log inspection for automated regression testing. |
| $dumpvars | VCD file enables post-simulation waveform viewing in GTKWave or any waveform viewer — essential for debugging failures. |
| $monitor | Automatically logs every value change — gives a time-ordered audit trail without manually placing $display everywhere. |
📈 Simulation Waveform
The waveform shows all four input combinations applied sequentially. Each transition demonstrates one row of the truth table. The outputs respond immediately (combinational, zero simulation delay).
💻 Simulation Console Output
The expected console output when running the testbench — showing the $monitor log, the formatted test results table, and the final pass/fail summary:
How to Run the Simulation
# ── Icarus Verilog (iverilog) — free, open-source ───────────── iverilog -o half_adder_sim \ half_adder_gate.v half_adder_tb.v vvp half_adder_sim # View waveform: gtkwave half_adder.vcd # ── Verilator (fast simulation, C++ backend) ────────────────── verilator --binary -j 0 half_adder_gate.v half_adder_tb.v \ --top-module half_adder_tb ./obj_dir/Vhalf_adder_tb # ── ModelSim / Questa ───────────────────────────────────────── vlog half_adder_gate.v half_adder_tb.v vsim -c half_adder_tb -do "run -all; quit" # ── Cadence Xcelium ─────────────────────────────────────────── xrun half_adder_gate.v half_adder_tb.v \ -top half_adder_tb -access +rwc # ── To test other implementations, swap the DUT file: # half_adder_dataflow.v → change module name in TB to half_adder_dataflow # half_adder_behavioral.v → change module name in TB to half_adder_behavioral # half_adder_udp.v → change module name in TB to half_adder_udp
🔬 Design Analysis & Comparison
All four implementations are functionally equivalent — they produce identical outputs for all input combinations. The differences lie in style, verbosity, and intended use case:
| Implementation | Lines of code | Synthesizable | Simulation speed | Best for |
|---|---|---|---|---|
| Gate Level | ~15 | ✅ Yes | Fastest | Standard cell library modelling, post-synthesis netlists |
| Data Flow | ~14 | ✅ Yes | Fast | RTL design — most common style for combinational logic |
| Behavioral | ~17 | ✅ Yes | Fast | Complex combinational logic, algorithmic descriptions |
| UDP | ~40 | ⚠️ Depends | Fastest | Heavily-used primitives in cell libraries needing speed |
Extension: Building a Full Adder from Two Half Adders
// A full adder accepts a carry-in and adds three bits // Architecture: FA = HA(a,b) → HA(sum1, cin) → carry = c1 OR c2 `timescale 1ns/1ps `default_nettype none module full_adder ( input a, b, cin, output sum, cout ); wire sum1, c1, c2; // Stage 1: half adder on primary inputs half_adder_gate ha1 (.a(a), .b(b), .sum(sum1), .carry(c1)); // Stage 2: half adder on intermediate sum + carry-in half_adder_gate ha2 (.a(sum1), .b(cin), .sum(sum), .carry(c2)); // Output carry: at most one of c1/c2 can be 1 at a time or g_or (cout, c1, c2); endmodule `default_nettype wire // Truth table for reference: // a b cin | sum cout // 0 0 0 | 0 0 // 0 0 1 | 1 0 // 0 1 0 | 1 0 // 0 1 1 | 0 1 // 1 0 0 | 1 0 // 1 0 1 | 0 1 // 1 1 0 | 0 1 // 1 1 1 | 1 1 ← 1+1+1 = 3 = 11₂
half_adder_gate are connected by internal wires to build a higher-level circuit. This bottom-up approach scales to any complexity: two full adders make a 2-bit adder, four make a 4-bit adder, and so on — each level verified independently before integration.
