Functional coverage answers the question "what have we actually tested?" It lets you define a coverage model based on your verification plan — specific values, ranges, transitions, and cross-combinations — and tracks which ones your testbench has exercised.
Code coverage is automatically extracted by the simulator — it tells you which lines and branches of RTL were reached. But reaching a line of code is not the same as testing the right scenario. Functional coverage is a user-defined metric driven by the verification plan:
| Aspect | Code Coverage | Functional Coverage |
|---|---|---|
| Source | Extracted from RTL automatically | Written by verification engineer |
| Measures | Which RTL statements were executed | Which design scenarios were exercised |
| 100% means | All code was touched | All planned scenarios were hit |
| Misses | Legal corner cases not in the code path | Bugs in the coverage model itself |
A covergroup is like a class — you define it once and can instantiate it multiple times. It can live in a class, module, or interface. The sampling trigger can be a clock edge, an event, or the manual sample() call:
// Covergroup triggered by a clock edge covergroup cg_transfer @(posedge clk); cp_addr: coverpoint addr; // auto bins for all values of addr cp_data: coverpoint data[7:0]; // only lower 8 bits endgroup // Covergroup inside a class — uses sample() for manual control class uart_cov_model; covergroup cg_uart; cp_baud: coverpoint baud_rate; cp_par: coverpoint parity; endgroup function new(); cg_uart = new(); // instantiate the covergroup endfunction function void sample_transaction(uart_pkt pkt); baud_rate = pkt.baud; parity = pkt.par; cg_uart.sample(); // explicit sample call endfunction endclass
Without explicit bins, the simulator creates automatic bins. For a 4-bit signal that gives 16 auto-bins. You can override with meaningful named bins:
covergroup cg_opcode @(posedge clk); cp_op: coverpoint opcode { // Named single-value bins bins add = {4'b0000}; bins sub = {4'b0001}; bins mul = {4'b0010}; // Range bin — set of values, one bin bins alu = {[4'b0011:4'b0111]}; // Array bin — one bin PER value in range ([] suffix) bins load[] = {[4'b1000:4'b1011]}; // Wildcard bin — matches any value with X in that position bins store_any = {4'b11??}; // Default bin — catches everything not matched above bins others = default; } endgroup // ignore_bins — values that should NOT count as covered // illegal_bins — values that should NEVER occur (causes error if seen) covergroup cg_state @(posedge clk); cp_st: coverpoint state { bins valid_states[] = {IDLE, RUN, PAUSE, DONE}; ignore_bins rsvd = {3'b101, 3'b110}; illegal_bins bad_state = {3'b111}; } endgroup
cross tracks combinations of values across two or more coverpoints. It creates N×M bins automatically and reveals which pairs were never exercised:
covergroup cg_rw_size @(posedge clk); // Individual coverpoints cp_rw: coverpoint rw { bins read = {1'b0}; bins write = {1'b1}; } cp_size: coverpoint xfer_size { bins byte_sz = {2'b00}; bins half_sz = {2'b01}; bins word_sz = {2'b10}; bins dword_sz = {2'b11}; } // Cross: 2 x 4 = 8 bins automatically // Checks: was a WRITE with DWORD size ever done? etc. cx_rw_size: cross cp_rw, cp_size; endgroup // Exclude specific cross bins (e.g., invalid combinations) covergroup cg_cross_filtered @(posedge clk); cp_rw: coverpoint rw; cp_size: coverpoint xfer_size; cx: cross cp_rw, cp_size { ignore_bins no_dword_read = binsof(cp_rw.read) && binsof(cp_size.dword_sz); } endgroup
Transition bins capture sequences of values — not just individual values but the order in which they occur. Ideal for state machine coverage:
covergroup cg_fsm_trans @(posedge clk); cp_state: coverpoint state { // Simple state values bins idle = {IDLE}; bins run = {RUN}; bins pause = {PAUSE}; bins done = {DONE}; // Transition bins — 2-step bins idle2run = (IDLE => RUN); bins run2pause = (RUN => PAUSE); bins pause2run = (PAUSE => RUN); bins run2done = (RUN => DONE); // Multi-step transition — full normal flow bins full_flow = (IDLE => RUN => DONE); // Error recovery path bins err_recovery = (RUN => PAUSE => RUN => DONE); } endgroup
The iff keyword attaches a guard condition to a coverpoint or the entire covergroup. Sampling only occurs when the guard is true:
covergroup cg_bus @(posedge clk); // Only sample this coverpoint when the bus is enabled cp_addr: coverpoint addr iff (bus_enable) { bins low = {[0:15]}; bins mid = {[16:239]}; bins hi = {[240:255]}; } // Only sample write data during actual write transactions cp_wdata: coverpoint wdata[7:0] iff (bus_enable && !rnw); endgroup // iff on the entire covergroup (at instantiation) covergroup cg_rd @(posedge clk) iff (rd_valid); cp_rdata: coverpoint rdata; endgroup
By default a covergroup is sampled at the triggering event. The sample() method lets you control when to sample from procedural code — useful when you're inside a UVM component and want to sample after transaction processing:
class my_coverage; logic [7:0] addr_v; logic [1:0] size_v; logic rnw_v; // No automatic trigger — sample() controls when covergroup cg_xfer; cp_addr: coverpoint addr_v { bins low[] = {[0:63]}; bins high[] = {[192:255]}; } cp_size: coverpoint size_v; cp_rnw: coverpoint rnw_v; cx_size_rnw: cross cp_size, cp_rnw; endgroup function new(); cg_xfer = new(); endfunction // Called from monitor write() or scoreboard function void sample(bus_item txn); addr_v = txn.addr; size_v = txn.size; rnw_v = txn.rnw; cg_xfer.sample(); // manual trigger endfunction endclass
// $get_coverage() returns overall coverage percentage (0.0–100.0) // cg_xfer.get_coverage() returns coverage for one instance // cp_addr.get_coverage() returns coverage for one coverpoint initial begin wait(sim_done); $display("Overall coverage: %.1f%%", $get_coverage()); $display("Transfer cg: %.1f%%", cov_inst.cg_xfer.get_coverage()); end // Coverage option — set goals and names per-instance covergroup cg_with_goal @(posedge clk); option.name = "cg_ctrl"; // name in report option.goal = 95; // target 95% (default 100) option.at_least = 2; // bin needs 2 hits to count option.per_instance = 1; // report each instance separately cp_ctrl: coverpoint ctrl_reg; endgroup
A complete coverage model for a UART transmitter, capturing baud rates, parity modes, data values, and the full state transition sequence:
typedef enum logic [1:0] {PAR_NONE=0, PAR_ODD=1, PAR_EVEN=2} parity_e; typedef enum logic [2:0] { ST_IDLE=0, ST_START=1, ST_DATA=2, ST_PARITY=3, ST_STOP=4 } uart_state_e; class uart_coverage; int baud_v; parity_e par_v; logic [7:0] data_v; uart_state_e state_v; covergroup cg_uart_frame; // Baud rate coverage — named bins for each standard rate cp_baud: coverpoint baud_v { bins b9600 = {9600}; bins b115200 = {115200}; bins b921600 = {921600}; } // Parity mode cp_parity: coverpoint par_v { bins none = {PAR_NONE}; bins odd = {PAR_ODD}; bins even = {PAR_EVEN}; } // Data value coverage — corner cases + all-ones/all-zeros cp_data: coverpoint data_v { bins all_zero = {8'h00}; bins all_one = {8'hFF}; bins low[] = {[8'h01:8'h0F]}; bins mid[] = {[8'h10:8'hEF]}; bins high[] = {[8'hF0:8'hFE]}; } // State machine transition coverage cp_state_trans: coverpoint state_v { bins normal_frame = (ST_IDLE => ST_START => ST_DATA => ST_STOP => ST_IDLE); bins parity_frame = (ST_IDLE => ST_START => ST_DATA => ST_PARITY => ST_STOP => ST_IDLE); } // Cross: was each baud rate tested with each parity mode? cx_baud_par: cross cp_baud, cp_parity; endgroup function new(); cg_uart_frame = new(); endfunction function void sample_frame(uart_transaction txn); baud_v = txn.baud_rate; par_v = txn.parity; data_v = txn.data; state_v = txn.state; cg_uart_frame.sample(); endfunction endclass