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.
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).
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) |
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.
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
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
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.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
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
| 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. |
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).
The expected console output when running the testbench — showing the $monitor log, the formatted test results table, and the final pass/fail summary:
# ── 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
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 |
// 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.