Coverage

Line, branch, toggle, assertion and covergroup coverage: collect it, merge runs, render a report.

Coverage is compiled into the simulation model. You choose the coverage types on the ryusim compile line. The model counts hits while it runs and writes them to an LCOV-format file when it exits.

Example: two runs, one merged report

The design is a 4-bit counter. Save it as counter.sv:

module counter ( input logic clk, input logic rst, input logic en, output logic [3:0] count ); always_ff @(posedge clk) begin if (rst) count <= '0; else if (en) count <= count + 1; end endmodule

The testbench enables the counter only when the +enable plusarg is given, so two runs of the same model exercise different code. Save it as tb.sv:

module tb; logic clk = 0, rst = 1, en = 0; logic [3:0] count; counter dut (.clk, .rst, .en, .count); always #5 clk = ~clk; initial begin @(negedge clk) rst = 0; en = $test$plusargs("enable"); repeat (3) @(negedge clk); $display("count=%0d", count); $finish; end endmodule

Compile with line, branch and toggle coverage:

ryusim compile --top tb --coverage --coverage-branch --coverage-toggle tb.sv counter.sv

The compile log confirms each type and counts the instrumented points:

[3/4] Generating C++ simulation code... Coverage collection enabled Branch coverage enabled Toggle coverage enabled [coverage] census: 29 point(s) registered; excluded: line=0 branch=0 toggle=0 fsm=0 functional=0 assertion=0

Run the model twice. Each run writes coverage.info in the current directory, so rename it after each run:

obj_dir/build/tb_sim mv coverage.info idle.info
obj_dir/build/tb_sim +enable mv coverage.info enabled.info

Compare the summary lines for counter.sv. The idle run never reaches the increment, so it hits 3 of 4 lines, 2 of 3 branches and none of the 8 counter bit transitions:

sed -n '/^SF:counter.sv/,/^end_of_record/p' idle.info | grep -E '^(LF|LH|BRF|BRH|TGF|TGH):'
LF:4 LH:3 BRF:3 BRH:2 TGF:8 TGH:0

Merge the two runs:

ryusim coverage merge -o merged.info idle.info enabled.info
RyuSim - Merging 2 coverage file(s) Output: merged.info Merge complete: merged.info

The merged file has every line and branch of the counter covered. Hit counts are summed across the inputs:

sed -n '/^SF:counter.sv/,/^end_of_record/p' merged.info | grep -E '^(DA|LF|LH|BRF|BRH):'
DA:8,8 DA:9,2 DA:10,6 DA:11,3 LF:4 LH:4 BRF:3 BRH:3

Add --verbose to ryusim coverage merge to print per-file parsing and line and branch totals. Its --strict option is accepted but has no effect in 2.1.16. The CLI reference lists the merge options.

HTML report

ryusim coverage html runs genhtml from the lcov package, which you install separately (sudo apt install lcov on Ubuntu and Debian). Without it the command stops with Error: genhtml command not found. (see Troubleshooting).

ryusim coverage html -o coverage_html merged.info
RyuSim - Generating HTML coverage report Input: merged.info Output: coverage_html Report generated: coverage_html Open coverage_html/index.html in a browser to view

This block is not run by the site's CI, which has no lcov installed; the output above is from lcov 2.0. The report shows line coverage per source file. Each covergroup gets its own page, linked from a tile on the index page. genhtml does not render toggle, FSM or assertion records. With genhtml 2.x, RyuSim adds --ignore-errors format,source --synthesize-missing so those records do not stop it. --title sets the report title (default RyuSim Coverage Report).

You can also run genhtml on the file yourself. genhtml 2.x rejects the RyuSim record types unless you pass the same --ignore-errors format.

Coverage types

Any of these options turns coverage on. Line coverage is always collected once coverage is on, and covergroups are recorded with any of them.

OptionRecordsRecords in the file
--coverageLine coverageDA
--coverage-lineLine coverage. The output is the same as with --coverage.DA
--coverage-branchTaken counts for if/else and case branchesBRDA
--coverage-toggle0→1 and 1→0 transitions of each signal bitTGDA
--coverage-fsmStates and transitions of detected state machines. Partial: the records are written but no hits are counted in 2.1.16 (see Known limits).FSMDA, FSMTDA
--coverage-assertOutcome counts of each assert property: PASS, FAIL, VACUOUS, DISABLEDASSERTA
--coverage-functionalCovergroupsCGDA and the other CG records

--coverage on its own collects line coverage only. BRDA records need --coverage-branch and TGDA records need --coverage-toggle; neither is implied by --coverage.

The coverage file

The model writes coverage.info in the directory the simulation runs in. --coverage-file sets another name or path at compile time. At startup the model writes the file with every count at zero, and at exit it overwrites it with the counts. A run that dies before exit leaves the zeroed file.

The file is LCOV tracefile text. Per source file (SF: … end_of_record) it carries the standard LCOV line and branch records plus RyuSim's own records. A count of - means the point was never reached. ryusim coverage merge writes 0 in its place.

RecordFields
DADA:<line>,<hits>. Summary: LF (lines found), LH (lines hit).
BRDABRDA:<line>,<block>,<branch>,<taken>. Summary: BRF, BRH.
TGDATGDA:<line>,<signal>,<bit>,<0to1|1to0>,<hits>. Summary: TGF, TGH.
FSMDAFSMDA:<line>,<state variable>,<state>,<hits>. Summary: FSMF, FSMH.
FSMTDAFSMTDA:<line>,<state variable>,<from>,<to>,<hits>. Summary: FSMTF, FSMTH.
ASSERTAASSERTA:<line>,<label>,<outcome>,<hits>. Summary: ASSERTF, ASSERTH.
EXCLEXCL:<first line>,<last line>,<types>, one per excluded region. Summary: EXCLF (regions), EXCLL (lines).
CGDACGDA:<covergroup>,<instance>,<coverpoint or cross>,<bin>,<hits>,<at_least>. These come before the first SF:. CGSUM and CGISUM carry the type and instance percentages; CGTMD, CGIMD and CGXB carry options and excluded bins.

The CG record layout may still change. The merger matches covergroup bins by name and writes * as the instance.

Covergroups

Covergroups (IEEE 1800-2023 clause 19) run natively and write their bins to the same coverage file. get_coverage() and get_inst_coverage() return the percentages during the run. The clause 19.9 system tasks work too: $get_coverage returns the overall percentage, $set_coverage_db_name changes the file the run writes at exit, and $load_coverage_db reads the bins of an earlier run into this one. The compliance matrix lists which parts of clause 19 are supported.

This covergroup has explicit bins, an illegal bin and a cross with a binsof ignore:

covergroup cg_mode @(posedge clk); cp_mode: coverpoint mode { bins idle = {0}; bins active[] = {[1:2]}; illegal_bins bad = {3}; } cp_err: coverpoint err; x: cross cp_mode, cp_err { ignore_bins no_err_idle = binsof(cp_mode.idle) && binsof(cp_err) intersect {1}; } endgroup

A run that visits modes 0, 1 and 2 with err held at 0 writes these bin records:

CGDA:cg_mode,cg_mode,cp_mode,idle,1,1 CGDA:cg_mode,cg_mode,cp_mode,active[1],1,1 CGDA:cg_mode,cg_mode,cp_mode,active[2],1,1 CGDA:cg_mode,cg_mode,cp_mode,bad,0,1 CGDA:cg_mode,cg_mode,cp_err,auto[0],3,1 CGDA:cg_mode,cg_mode,cp_err,auto[1],0,1 CGDA:cg_mode,cg_mode,x,<idle,auto[0]>,1,1 CGDA:cg_mode,cg_mode,x,<active[1],auto[0]>,1,1 CGDA:cg_mode,cg_mode,x,<active[2],auto[0]>,1,1

Excluding code

Comments in the source exclude regions from coverage:

The compile's [coverage] census line counts excluded points by type, and the file records each region as EXCL.

With cocotb

Put the coverage options in the compile options of your cocotb flow; the cocotb guide shows where they go for the Makefile, the Python Runner and pytest. The model writes coverage.info in the directory the simulation runs in, which that guide gives for each flow. Coverage options are compiled in, so delete the build directory after you change them.

Known limits in 2.1.16