The 58-line block below synthesises with exit status 0, and the last line of the check pass says Found and reported 0 problems. Yet the netlist has 18 flip-flops where the RTL describes 21, four latches nobody asked for, and a status bit tied to 0.
Every one of those facts is in the log. Synthesis has to understand each line of RTL to build it, so it notices things a quick simulation does not. Read the log and the stat table before you commit, in the same order every time.
Steps 1 to 3 are warnings and are easy to find with grep. Step 4 is not a warning at all, and step 5 needs a number you worked out yourself. The seeded block shows why both are needed.
An event counter with four registers. Software enables it, sets a threshold, reads the count and reads a status register with an overflow bit and an error bit. Each bug below is one that reaches real code reviews.
// yosys: latch
// Event counter with a threshold interrupt and four registers.
// addr 0: ctrl {irq_en, enable} addr 1: thresh addr 2: count (read only)
// addr 3: status {err, ovf}, a write clears it addr 4-7: unmapped, a write sets err
module evt_unit (
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [1:0] addr,
input wire [7:0] wdata,
input wire evt,
output reg [7:0] rdata,
output wire irq,
output reg [3:0] level,
output wire busy
);
reg [1:0] ctrl;
reg [7:0] thresh, count;
reg ovf, err, hit_q;
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
ctrl <= 2'b00; thresh <= 8'd0;
end else if (wr_en) begin
if (addr == 2'd0) ctrl <= wdata[1:0];
if (addr == 2'd1) thresh <= wdata;
if (addr == 2'd3) ovf <= 1'b0;
end
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
count <= 8'd0; ovf <= 1'b0;
end else if (ctrl[0] && evt) begin
count <= count + 8'd1;
if (count == 8'hff) ovf <= 1'b1;
end
always @(posedge clk or negedge rst_n)
if (!rst_n) err <= 1'b0;
else if (wr_en && addr == 4) err <= 1'b1;
always @(posedge clk or negedge rst_n)
if (!rst_n) hit_q <= 1'b0;
else hit_q <= (count >= thresh);
always @(*)
case (addr)
2'd0: rdata = {6'd0, ctrl};
2'd1: rdata = thresh;
2'd2: rdata = count;
default: rdata = {6'd0, err, ovf};
endcase
always @(*)
if (ctrl[0]) level = count[7:4];
assign irq = ctrl[1] & (count >= thresh);
endmoduleSynthesise it and keep the log, then pull out every warning:
yosys -p "read_verilog evt_unit.v; synth -top evt_unit; check" > before.log
grep -i warning before.logWarning: Latch inferred for signal `\evt_unit.\level' from process `\evt_unit.$proc$evt_unit.v:54$19': $auto$proc_dlatch.cc:547:proc_dlatch$60
Warning: multiple conflicting drivers for evt_unit.\ovf:
Warning: Wire evt_unit.\busy is used but has no driver.
Warning: Driver-driver conflict for \ovf between cell $auto$ff.cc:337:slice$99.Q and constant 1'0 in evt_unit: Resolved using constant.
ABC: Warning: The network is combinational (run "fraig" or "fraig_sweep").
Warnings: 4 unique messages, 4 totalThe last line is the summary: four unique warnings. The ABC line is the internal logic optimiser talking about its own script; it appears for every design, including the clean one later, and says nothing about your RTL. Now take the findings one by one.
The first warning names the signal, level, and line 54, the second always @(*). When ctrl[0] is 0 nothing assigns level, so it must hold its value: four latches, one per bit, which stat lists as $_DLATCH_P_. Fix: assign level on every path.
The check pass inside synth lists both flops that drive ovf: one from the register-write block (clear on a write to address 3) and one from the counter block (set on wrap).
2.5. Executing CHECK pass (checking for obvious problems).
Checking module evt_unit...
Warning: multiple conflicting drivers for evt_unit.\ovf:
port Q[0] of cell $procdff$97 ($dff)
port Q[0] of cell $procdff$80 ($adff)
Warning: Wire evt_unit.\busy is used but has no driver.
Found and reported 2 problems.
...
3. Executing CHECK pass (checking for obvious problems).
Checking module evt_unit...
Found and reported 0 problems.Real hardware cannot have two flops driving one wire, and simulation lets the last writer win, so the two disagree. Later the optimiser settles it on its own: the fourth warning says the conflict was Resolved using constant. The status bit now always reads 0. Notice the final check: by then the conflict is gone from the netlist, so it reports 0 problems. Fix: one block owns each register; put set and clear in the same block with a stated priority.
Wire evt_unit.\busy is used but has no driver. An output port counts as used. Someone declared the port and never wrote the logic. The netlist drives it with a constant, so the chip is never busy. Fix: drive it, or delete the port.
No warning mentions err. The clue is an optimiser message about a cell name, which the proc step ties back to a signal:
Creating register for signal `\evt_unit.\err' using process `\evt_unit.$proc$evt_unit.v:38$11'.
created $adff cell `$procdff$70' with positive edge clock and positive level reset.
...
Handling D = Q on $procdff$70 ($adff) from module evt_unit (removing D path).
Setting constant 0-bit at position 0 on $procdff$70 ($dlatch) from module evt_unit.Handling D = Q means the flop's next value is always its current value, so it can never change and is replaced by its reset value, 0. The cause is width: addr is 2 bits, so addr == 4 is never true. Yosys does not warn about the comparison; a lint run does. Fix: make addr 3 bits, as the address map says.
hit_q is computed every cycle but nothing reads it: irq uses the combinational compare instead. Its only trace in the log is a count:
Creating register for signal `\evt_unit.\hit_q' using process `\evt_unit.$proc$evt_unit.v:42$15'.
created $adff cell `$procdff$65' with positive edge clock and positive level reset.
...
2.4. Executing OPT_CLEAN pass (remove unused cells and wires).
Finding unused cells or wires in module \evt_unit..
Removed 14 unused cells and 53 unused wires.There is no warning, because removing unused logic is the optimiser's job. The bug is that irq comes straight from a comparator instead of from a flop, a glitchy, timing-hungry output. Only the flop count in stat gives it away.
Count the state bits from the declarations before you look: ctrl 2, thresh 8, count 8, then ovf, err and hit_q 1 each. That is 21 flops and 0 latches.
Cell in stat | Before | After the fixes | What the change tells you |
|---|---|---|---|
| 18 | 20 | Before: only ctrl, thresh and count survived. ovf and err are back after the fixes. |
| 0 | 1 | hit_q: no enable, because it updates every cycle |
| 4 | 0 | The four bits of level |
All cells | 102 | 120 | More logic after the fixes, because more logic is now used |
A flop count lower than yours means logic was removed: something is unread, constant or driven twice. A higher count means storage you did not intend, such as a combinational block that became a latch or an array built from flops instead of a RAM. Any latch in a design without latches in its specification is a bug.
Do not stop at the last line of the log. Run check -assert straight after proc, before the optimiser has had a chance to tidy problems away, and the run fails instead of passing:
yosys -p "read_verilog evt_unit.v; hierarchy -top evt_unit; proc; check -assert"
...
Warning: multiple conflicting drivers for evt_unit.\ovf:
port Q[0] of cell $procdff$80 ($adff)
port Q[0] of cell $procdff$97 ($dff)
Warning: Wire evt_unit.\busy is used but has no driver.
ERROR: Found 2 problems in 'check -assert'.That command exits with status 1, so it can gate a commit or a CI job.
The comments mark the five fixes. The status bits are now set and cleared in one block each, and irq comes from the registered hit_q.
// yosys: ok
// Event counter with a threshold interrupt and four registers.
// addr 0: ctrl {irq_en, enable} addr 1: thresh addr 2: count (read only)
// addr 3: status {err, ovf}, a write clears it addr 4-7: unmapped, a write sets err
module evt_unit (
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [2:0] addr, // fix 4: three bits reach 4-7
input wire [7:0] wdata,
input wire evt,
output reg [7:0] rdata,
output wire irq,
output reg [3:0] level,
output wire busy
);
reg [1:0] ctrl;
reg [7:0] thresh, count;
reg ovf, err, hit_q;
wire clr_status = wr_en && addr == 3'd3;
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
ctrl <= 2'b00; thresh <= 8'd0;
end else if (wr_en) begin
if (addr == 3'd0) ctrl <= wdata[1:0];
if (addr == 3'd1) thresh <= wdata;
end
always @(posedge clk or negedge rst_n) // fix 2: one block owns ovf
if (!rst_n) begin
count <= 8'd0; ovf <= 1'b0;
end else begin
if (ctrl[0] && evt) count <= count + 8'd1;
if (ctrl[0] && evt && count == 8'hff) ovf <= 1'b1;
else if (clr_status) ovf <= 1'b0;
end
always @(posedge clk or negedge rst_n)
if (!rst_n) err <= 1'b0;
else if (wr_en && addr[2]) err <= 1'b1;
else if (clr_status) err <= 1'b0;
always @(posedge clk or negedge rst_n)
if (!rst_n) hit_q <= 1'b0;
else hit_q <= (count >= thresh);
always @(*)
case (addr)
3'd0: rdata = {6'd0, ctrl};
3'd1: rdata = thresh;
3'd2: rdata = count;
3'd3: rdata = {6'd0, err, ovf};
default: rdata = 8'd0;
endcase
always @(*) level = ctrl[0] ? count[7:4] : 4'd0; // fix 1: every path assigns
assign busy = ctrl[0] & ~ovf; // fix 3: the output is driven
assign irq = ctrl[1] & hit_q; // fix 5: registered interrupt
endmoduleyosys -p "read_verilog evt_unit_fixed.v; synth -top evt_unit; check" > after.log
grep -i "warning\|latch inferred" after.log
No latch inferred for signal `\evt_unit.\level' from process `\evt_unit.$proc$evt_unit_fixed.v:57$21'.
No latch inferred for signal `\evt_unit.\rdata' from process `\evt_unit.$proc$evt_unit_fixed.v:48$20'.
ABC: Warning: The network is combinational (run "fraig" or "fraig_sweep").=== evt_unit ===
...
120 cells
5 $_ANDNOT_
30 $_AND_
20 $_DFFE_PN0P_
1 $_DFF_PN0_
38 $_NAND_
1 $_NOR_
2 $_NOT_
16 $_ORNOT_
7 $_XOR_No warnings from Yosys, both combinational processes confirmed latch-free, 21 flops as counted, and the same check -assert command now exits with status 0.
$_DFFE_PN0P_ drops from 20 to 12. What do you suspect, and where do you look?Eight flops are gone, the width of one 8-bit register: thresh or count. A register vanishes without a warning when nothing reads it (removed as unused) or when its next value can never change (D = Q, then constant). Search the log for Handling D = Q and Setting constant, then check which reads of thresh and count the refactor changed. In this block thresh is read by the compare and by rdata, so look for a dropped read path.
Because the optimiser had already resolved the double driver on ovf by tying it to a constant. The problem was gone from the netlist, so the last check had nothing to report. The check that ran right after proc found both problems; run check -assert there to make them fatal.
The fixed block with a self-checking bench: register writes, the one-cycle delay of the registered interrupt, level, busy, err on an unmapped write, overflow after 256 events, and clearing the status register.
// Event counter with a threshold interrupt and four registers.
// addr 0: ctrl {irq_en, enable} addr 1: thresh addr 2: count (read only)
// addr 3: status {err, ovf}, a write clears it addr 4-7: unmapped, a write sets err
module evt_unit (
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [2:0] addr, // fix 4: three bits reach 4-7
input wire [7:0] wdata,
input wire evt,
output reg [7:0] rdata,
output wire irq,
output reg [3:0] level,
output wire busy
);
reg [1:0] ctrl;
reg [7:0] thresh, count;
reg ovf, err, hit_q;
wire clr_status = wr_en && addr == 3'd3;
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
ctrl <= 2'b00; thresh <= 8'd0;
end else if (wr_en) begin
if (addr == 3'd0) ctrl <= wdata[1:0];
if (addr == 3'd1) thresh <= wdata;
end
always @(posedge clk or negedge rst_n) // fix 2: one block owns ovf
if (!rst_n) begin
count <= 8'd0; ovf <= 1'b0;
end else begin
if (ctrl[0] && evt) count <= count + 8'd1;
if (ctrl[0] && evt && count == 8'hff) ovf <= 1'b1;
else if (clr_status) ovf <= 1'b0;
end
always @(posedge clk or negedge rst_n)
if (!rst_n) err <= 1'b0;
else if (wr_en && addr[2]) err <= 1'b1;
else if (clr_status) err <= 1'b0;
always @(posedge clk or negedge rst_n)
if (!rst_n) hit_q <= 1'b0;
else hit_q <= (count >= thresh);
always @(*)
case (addr)
3'd0: rdata = {6'd0, ctrl};
3'd1: rdata = thresh;
3'd2: rdata = count;
3'd3: rdata = {6'd0, err, ovf};
default: rdata = 8'd0;
endcase
always @(*) level = ctrl[0] ? count[7:4] : 4'd0; // fix 1: every path assigns
assign busy = ctrl[0] & ~ovf; // fix 3: the output is driven
assign irq = ctrl[1] & hit_q; // fix 5: registered interrupt
endmodule
Tool: Yosys 0.69+post (git sha1 143eb14f9), run on 2026-10-17. Commands: yosys -p "read_verilog evt_unit.v; synth -top evt_unit; check" > before.log; the same for evt_unit_fixed.v > after.log; grep -i warning on both; yosys -p "read_verilog <file>; hierarchy -top evt_unit; proc; check -assert" on both (exit 1 seeded, exit 0 fixed).
Result: every log and stat line on the page is copied from those runs, trimmed with ... marks. Seeded: 4 unique warnings, 18 $_DFFE_PN0P_ and 4 $_DLATCH_P_ in 102 cells, final check 0 problems. Fixed: no Yosys warnings (only the ABC note), 20 $_DFFE_PN0P_ and 1 $_DFF_PN0_ in 120 cells. That hit_q ($procdff$65) is removed by the first OPT_CLEAN was confirmed with proc; opt_clean; select -list t:$adff t:$dff.
The statement that lint flags the addr == 4 comparison was checked with Verilator 5.048 --lint-only -Wall (WIDTHEXPAND at line 40); that run did not flag the double driver on ovf. The bench for the fixed block was run with Icarus Verilog 13.0 and prints PASS; it is a directed check of the listed behaviours, not a proof.
Scope and limits: generic gates with no library, so cell counts are structural, not area or timing; other synthesis tools word and order their messages differently, and may resolve a double driver differently or refuse it.