Code coverage is extracted automatically from RTL structure: lines, branches, toggles. It answers whether the implementation ran. Functional coverage is written from the spec. It answers whether required scenarios happened.
Code coverage | Functional coverage | |
|---|---|---|
Source | RTL structure | The specification |
Written | Automatically, by the tool | By the verification engineer |
Measures | Implementation exercised | Scenarios that occurred |
100% means | All written code ran | All planned scenarios hit |
Blind to | Missing code, scenario meaning | Anything not in the coverage model |
Effort | A switch | A plan and covergroups |
The FIFO corner below is the classic proof. Both lines run, yet the killing combination never occurs.
The next listing is not run in this course's checks: the open simulators do not count covergroups. The bench at the end of the lesson counts the same bins with the course's bins model, listed on the Bins Model page of this topic.
// not-run: Verilator 5.048 ignores covergroups (COVERIGN) and Icarus 13 has none; the bins model counts it
module fifo_wr (input logic clk, input logic rst_n,
input logic push, pop, input logic full, input logic empty,
input logic [7:0] wdata, input logic [2:0] wr_ptr,
output logic [2:0] rd_ptr);
logic [7:0] mem [8];
// Line A runs on any normal push; line B on any normal pop.
// Pushes until full plus pops until empty give 100% line,
// toggle and branch coverage: yet push && pop && full NEVER OCCURS.
// BUG: simultaneous push && pop while FULL corrupts the count
// elsewhere in the design.
always_ff @(posedge clk) begin
if (push && !full)
mem[wr_ptr] <= wdata;
if (pop && !empty)
rd_ptr <= rd_ptr + 1;
end
// The functional coverage that names the gap:
covergroup cg_fifo @(posedge clk);
cp_corner : coverpoint {push, pop, full} iff (rst_n) {
bins push_pop_full = {3'b111}; // unhit means the scenario was never tried
}
endgroup
cg_fifo cg_inst = new();
endmoduleThe unhit bin points at the exact stimulus gap. Line coverage had no vocabulary to even ask the question.
Verilator measures code coverage for real, so the claim can be checked. The bench fills a small FIFO, drains it, and samples the corner bin with the bins model:
// uses: covmodel
// A 4-deep FIFO, a test that fills it and drains it, and the one corner both miss.
module fifo4 (
input logic clk, rst_n, push, pop,
input logic [7:0] wdata,
output logic [7:0] rdata,
output logic full, empty
);
logic [7:0] mem [4];
logic [1:0] wp, rp;
logic [2:0] count;
assign full = (count == 4);
assign empty = (count == 0);
assign rdata = mem[rp];
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin wp <= 0; rp <= 0; count <= 0; end
else begin
if (push && !full) begin mem[wp] <= wdata; wp <= wp + 1; end
if (pop && !empty) rp <= rp + 1;
count <= count + (push && !full) - (pop && !empty);
end
endmodule
module tb_fifo_codecov;
logic clk = 0, rst_n = 0, push = 0, pop = 0;
logic [7:0] wdata = 0, rdata;
logic full, empty;
always #5 clk = ~clk;
fifo4 dut (.*);
cm_point cp_corner = new("cp_corner {push, pop, full}");
always @(posedge clk) if (rst_n) cp_corner.sample({push, pop, full});
initial begin
cp_corner.add("push_pop_full", 3'b111, 3'b111);
repeat (2) @(negedge clk);
rst_n = 1;
repeat (5) begin @(negedge clk) push = 1; wdata = wdata + 1; end // fill, and one push while full
@(negedge clk) push = 0;
repeat (5) begin @(negedge clk) pop = 1; end // drain, and one pop while empty
@(negedge clk) pop = 0;
repeat (2) @(negedge clk);
cp_corner.report();
$display("PASS: the fill-and-drain test finished; compare the corner bin with the line coverage of fifo4");
$finish;
end
endmodule coverpoint cp_corner {push, pop, full}: 0.00% (0 of 1 bins) below goal 100
push_pop_full 0 <- hole
PASS: the fill-and-drain test finished; compare the corner bin with the line coverage of fifo4Built a second time with line coverage on (verilator --binary --coverage-line, with the FIFO in its own file fifo4.sv), the same test writes coverage.dat, and verilator_coverage --annotate-min 1 --annotate-all --annotate annot coverage.dat annotates the FIFO. Every line ran, both branches of every if included, while the corner bin above reads 0:
// // verilator_coverage annotation
module fifo4 (
input logic clk, rst_n, push, pop,
input logic [7:0] wdata,
output logic [7:0] rdata,
output logic full, empty
);
logic [7:0] mem [4];
logic [1:0] wp, rp;
logic [2:0] count;
assign full = (count == 4);
assign empty = (count == 0);
assign rdata = mem[rp];
000016 always_ff @(posedge clk or negedge rst_n)
000014 if (!rst_n) begin wp <= 0; rp <= 0; count <= 0; end
000014 else begin
000010 if (push && !full) begin mem[wp] <= wdata; wp <= wp + 1; end
000010 if (pop && !empty) rp <= rp + 1;
000014 count <= count + (push && !full) - (pop && !empty);
end
endmoduleRecorded with Verilator 5.048 on 2026-10-18. The counts at the left margin are how many times each line ran; a line that never ran would show %000000. Line coverage is complete and the FIFO's one dangerous scenario was never tried. That is the whole argument for functional coverage, measured on one design.
Functional coverage low | Functional coverage high | |
|---|---|---|
Code coverage high | Stimulus wanders; scenarios never formed | Sign-off candidate: review waivers, then close |
Code coverage low | Early bring-up: keep testing | Coverage model too thin, or dead RTL: investigate |
An assertion counter is a different construct: SystemVerilog cover property counts assertion matches, not covergroup bins. Both appear in reports, so name which one each number came from.
x_op_zero sits at 83.3 percent. What is missing?One opcode never produced a zero result: five of six asserted bins hit. Add the directed case the hole names.
Stimulus wanders without forming planned scenarios. Steer it with constraints or directed cases instead of running longer.
Counted with the course's bins model (rich/coverage/cov_model.sv, listed on The Bins Model page) on Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown. Verilator 5.048 parses covergroups with a COVERIGN warning and ignores their bins, and Icarus 13 has none, so each covergroup listing is marked not run and its bench counts the same bins with the model.
The FIFO test was also built with verilator --binary --coverage-line and annotated with verilator_coverage on 2026-10-18: every line and both sides of every branch ran while the corner bin read 0.