Yosys handles a finite-state machine in named stages: detection marks the state register without changing the circuit, extraction packs the marked machine into one cell, optimisation trims it with a cleanup pass, recoding reassigns the state codes, and mapping unpacks the cell back into flops and gates. The built-in help for the meta pass, checked in this build with yosys -h fsm, lists exactly this order.
Every stage is pattern-based, which means every stage can decline. Detection only marks registers it recognises: a plain or asynchronous-reset flop feeding a multiplexer tree, with no initial value and no reset-like behaviour. The three scripts below differ in exactly one feature each, and each one quotes the detector's own log lines alongside the cell counts.
read_verilog <<EOV
module pkt_rx (input wire clk, input wire start, input wire byte_valid, input wire last_byte, input wire crc_ok,
output reg ready, output reg accept, output reg err);
localparam [2:0] IDLE = 3'd0, HDR = 3'd1, PAYLOAD = 3'd2, CRC = 3'd3, DONE = 3'd4;
reg [2:0] state, next;
initial state = IDLE;
always @(posedge clk) state <= next;
always @(*) begin
next = state; ready = 1'b0; accept = 1'b0; err = 1'b0;
case (state)
IDLE: begin ready = 1'b1; if (start) next = HDR; end
HDR: begin if (byte_valid) next = PAYLOAD; end
PAYLOAD: begin if (byte_valid && last_byte) next = CRC; end
CRC: begin if (crc_ok) next = DONE; else begin next = IDLE; err = !crc_ok; end end
DONE: begin accept = 1'b1; next = IDLE; end
default: next = IDLE;
endcase
end
endmodule
EOV
hierarchy -top pkt_rx
proc
opt_clean
fsm_detect
fsm_extract
tee -o coarse_init.txt stat t:$fsm t:$dff t:$dffe t:$adff t:$sdff
select -assert-none t:$fsm
log PASS: initialised register is refused by detectionread_verilog <<EOV
module pkt_rx (input wire clk, input wire start, input wire byte_valid, input wire last_byte, input wire crc_ok,
output reg ready, output reg accept, output reg err);
localparam [2:0] IDLE = 3'd0, HDR = 3'd1, PAYLOAD = 3'd2, CRC = 3'd3, DONE = 3'd4;
reg [2:0] state, next;
always @(posedge clk) state <= next;
always @(*) begin
next = state; ready = 1'b0; accept = 1'b0; err = 1'b0;
case (state)
IDLE: begin ready = 1'b1; if (start) next = HDR; end
HDR: begin if (byte_valid) next = PAYLOAD; end
PAYLOAD: begin if (byte_valid && last_byte) next = CRC; end
CRC: begin if (crc_ok) next = DONE; else begin next = IDLE; err = !crc_ok; end end
DONE: begin accept = 1'b1; next = IDLE; end
default: next = IDLE;
endcase
end
endmodule
EOV
hierarchy -top pkt_rx
proc
opt_clean
fsm_detect
fsm_extract
tee -o coarse_noreset.txt stat t:$fsm t:$dff t:$dffe t:$adff t:$sdff
select -assert-none t:$fsm
log PASS: reset-free register is refused as self-resettingread_verilog <<EOV
module pkt_rx (input wire clk, input wire rst, input wire start, input wire byte_valid, input wire last_byte, input wire crc_ok,
output reg ready, output reg accept, output reg err);
localparam [2:0] IDLE = 3'd0, HDR = 3'd1, PAYLOAD = 3'd2, CRC = 3'd3, DONE = 3'd4;
reg [2:0] state, next;
always @(posedge clk) if (rst) state <= IDLE; else state <= next;
always @(*) begin
next = state; ready = 1'b0; accept = 1'b0; err = 1'b0;
case (state)
IDLE: begin ready = 1'b1; if (start) next = HDR; end
HDR: begin if (byte_valid) next = PAYLOAD; end
PAYLOAD: begin if (byte_valid && last_byte) next = CRC; end
CRC: begin if (crc_ok) next = DONE; else begin next = IDLE; err = !crc_ok; end end
DONE: begin accept = 1'b1; next = IDLE; end
default: next = IDLE;
endcase
end
endmodule
EOV
hierarchy -top pkt_rx
proc
opt_clean
fsm_detect
fsm_extract
tee -o coarse_reset.txt stat t:$fsm t:$dff t:$dffe t:$adff t:$sdff
select -assert-count 1 t:$fsm
log PASS: reset register is detected and extractedOnly the synchronous-reset register is marked, extracting to one $fsm cell. The initialised register is refused for its initial value on top of the self-resetting reading, and the reset-free register is refused as self-resetting only; the table below quotes both log lines in full.
Register | Detector log | $fsm cells | State driver |
|---|---|---|---|
With initial |
| 0 | $dff x1 |
Reset-free |
| 0 | $dff x1 |
Sync reset | marked and extracted | 1 | $dff x1 |
Read the first row as the correction this lesson exists to make: the initialised register is refused because it carries an initial value, and only secondarily as self-resetting. An initial value is a simulation construct the detector will not reason about, so the register stays a plain flop no matter how clean the case statement looks. The second row isolates the other reason: remove the initial line but keep the machine reset-free, and the detector still declines, this time only as self-resetting. With no reset line anywhere, the always-taken arcs home read as a reset of the machine's own making.
The third row is the shape the whole topic uses: add the synchronous reset and the machine is found and extracted into one cell. Note the extraction detail the coarse counts preserve: after packing, the design holds one $fsm cell next to one leftover $dff, which keeps generating the original state code until the optimisation passes remove it. That leftover is why the pass list runs fsm_opt with opt_clean and then fsm_opt again.
There is a second way to stay invisible. The detector only considers registers driven by a plain $dff or an asynchronous-reset $adff cell. An enabled state register folds its enable mux into a $dffe under a full optimisation, and a $dffe driver is not on the detector's list. The two scripts below run the identical enabled receiver twice: once with a full opt that folds, once with the folding disabled.
read_verilog <<EOV
module pkt_rx (input wire clk, input wire en, input wire start, input wire byte_valid, input wire last_byte, input wire crc_ok,
output reg ready, output reg accept, output reg err);
localparam [2:0] IDLE = 3'd0, HDR = 3'd1, PAYLOAD = 3'd2, CRC = 3'd3, DONE = 3'd4;
reg [2:0] state, next;
always @(posedge clk) if (en) state <= next;
always @(*) begin
next = state; ready = 1'b0; accept = 1'b0; err = 1'b0;
case (state)
IDLE: begin ready = 1'b1; if (start) next = HDR; end
HDR: begin if (byte_valid) next = PAYLOAD; end
PAYLOAD: begin if (byte_valid && last_byte) next = CRC; end
CRC: begin if (crc_ok) next = DONE; else begin next = IDLE; err = !crc_ok; end end
DONE: begin accept = 1'b1; if (start) next = HDR; else next = IDLE; end
endcase
end
endmodule
EOV
hierarchy -top pkt_rx
proc
opt
fsm_detect
fsm_extract
tee -o coarse_enfull.txt stat t:$fsm t:$dff t:$dffe t:$adff t:$sdff
select -assert-count 1 t:$dffe
select -assert-none t:$fsm
log PASS: folded enable hides the machine from detectionread_verilog <<EOV
module pkt_rx (input wire clk, input wire en, input wire start, input wire byte_valid, input wire last_byte, input wire crc_ok,
output reg ready, output reg accept, output reg err);
localparam [2:0] IDLE = 3'd0, HDR = 3'd1, PAYLOAD = 3'd2, CRC = 3'd3, DONE = 3'd4;
reg [2:0] state, next;
always @(posedge clk) if (en) state <= next;
always @(*) begin
next = state; ready = 1'b0; accept = 1'b0; err = 1'b0;
case (state)
IDLE: begin ready = 1'b1; if (start) next = HDR; end
HDR: begin if (byte_valid) next = PAYLOAD; end
PAYLOAD: begin if (byte_valid && last_byte) next = CRC; end
CRC: begin if (crc_ok) next = DONE; else begin next = IDLE; err = !crc_ok; end end
DONE: begin accept = 1'b1; if (start) next = HDR; else next = IDLE; end
endcase
end
endmodule
EOV
hierarchy -top pkt_rx
proc
opt -nodffe -nosdff
fsm_detect
fsm_extract
tee -o coarse_enclean.txt stat t:$fsm t:$dff t:$dffe t:$adff t:$sdff
select -assert-count 1 t:$fsm
log PASS: unfolded enable lets detection mark the machineOptimisation | State driver | Detector log | $fsm cells |
|---|---|---|---|
Full opt | $dffe x1 | no detection line | 0 |
opt -nodffe -nosdff | $dff x1 |
| 1 |
The full-opt row is the quiet one: the enable folds into one $dffe and the detector prints no line at all, because the driver is not a cell type it considers. Silence means skipped, not clean. The second row keeps the enable as gates around a plain $dff, the detector marks the register, and extraction packs one $fsm cell. That is why the stock flow runs opt -nodffe -nosdff before its own detection step: so enables are not yet folded when the detector looks.
With detection understood, the script below runs the complete pass list on the synchronous-reset receiver: detect, extract, optimise twice around a cleanup, recode to binary, map, then technology mapping and reporting. The assertions pin every handover: one $fsm cell after extraction, none after mapping, three library flops at the end.
read_verilog <<EOV
module pkt_rx (input wire clk, input wire rst, input wire start, input wire byte_valid, input wire last_byte, input wire crc_ok,
output reg ready, output reg accept, output reg err);
localparam [2:0] IDLE = 3'd0, HDR = 3'd1, PAYLOAD = 3'd2, CRC = 3'd3, DONE = 3'd4;
reg [2:0] state, next;
always @(posedge clk) if (rst) state <= IDLE; else state <= next;
always @(*) begin
next = state; ready = 1'b0; accept = 1'b0; err = 1'b0;
case (state)
IDLE: begin ready = 1'b1; if (start) next = HDR; end
HDR: begin if (byte_valid) next = PAYLOAD; end
PAYLOAD: begin if (byte_valid && last_byte) next = CRC; end
CRC: begin if (crc_ok) next = DONE; else begin next = IDLE; err = !crc_ok; end end
DONE: begin accept = 1'b1; next = IDLE; end
default: next = IDLE;
endcase
end
endmodule
EOV
hierarchy -top pkt_rx
proc
opt_clean
fsm_detect
fsm_extract
fsm_opt
opt_clean
fsm_opt
select -assert-count 1 t:$fsm
fsm_recode -encoding binary
fsm_map
select -assert-none t:$fsm
tee -o coarse_pipe.txt stat
techmap
opt_clean
dfflibmap -liberty course.lib
tee -o abc_pipe.txt abc -liberty course.lib -constr typical.constr -D 1000
tee -o stat_pipe.txt stat -liberty course.lib
write_json net_pipe.json
select -assert-count 3 t:DFF
log PASS: binary pipeline maps the receiver to three flopsStage | Evidence |
|---|---|
After extract | one $fsm cell, and the coarse stat still shows the leftover state driver beside it |
After recode and map | no $fsm cells: the machine is flops and gates again, in the chosen code |
After ABC and stat | 18 gates, area 42, delay 506.03 ps, chip area 66, 3 state flops |
The final numbers match the binary run of the state-encoding lesson field for field: 18 gates, area 42, delay 506.03 ps, chip area 66. That agreement is a cross-check, not a coincidence: same receiver, same binary code, same library and load, so the pipeline reproduces the earlier result exactly. The drawing below is the recorded mapped netlist from this run, and the flowchart after it names the stages the assertions pinned.
The reset-free machine is a legitimate design that the detector declines for a defensible reason. When the machine really does return home along unconditional arcs and the designer wants it recognised anyway, the detector offers an explicit override flag. The script below reruns the refused machine with that flag and nothing else changed.
read_verilog <<EOV
module pkt_rx (input wire clk, input wire start, input wire byte_valid, input wire last_byte, input wire crc_ok,
output reg ready, output reg accept, output reg err);
localparam [2:0] IDLE = 3'd0, HDR = 3'd1, PAYLOAD = 3'd2, CRC = 3'd3, DONE = 3'd4;
reg [2:0] state, next;
always @(posedge clk) state <= next;
always @(*) begin
next = state; ready = 1'b0; accept = 1'b0; err = 1'b0;
case (state)
IDLE: begin ready = 1'b1; if (start) next = HDR; end
HDR: begin if (byte_valid) next = PAYLOAD; end
PAYLOAD: begin if (byte_valid && last_byte) next = CRC; end
CRC: begin if (crc_ok) next = DONE; else begin next = IDLE; err = !crc_ok; end end
DONE: begin accept = 1'b1; next = IDLE; end
default: next = IDLE;
endcase
end
endmodule
EOV
hierarchy -top pkt_rx
proc
opt_clean
fsm_detect -ignore-self-reset
fsm_extract
tee -o coarse_ignore.txt stat t:$fsm t:$dff t:$dffe t:$adff t:$sdff
select -assert-count 1 t:$fsm
log PASS: overriding the self-reset check marks the machineVariant | $fsm cells | State driver |
|---|---|---|
Reset-free, plain detect | 0 | $dff x1 |
Reset-free, override flag | 1 | $dff x1 |
The recorded run marks the register and extracts one $fsm cell, so the flag does exactly what it promises. Treat it as an override, not a default: it silences a check that also catches genuinely degenerate machines, so use it for a machine whose home arcs are intentional and keep the plain detection for everything else. The assertion pins the single extracted cell, matching the synchronous-reset run's count through a different door.
The initialised register carries both an initial value and the self-resetting reading; only removing the initial value and adding a real reset fixes it, because the detector never marks initialised registers. The reset-free register carries only the self-resetting reading, so either a synchronous reset or the explicit override flag marks it.
Silence means the driver folded into $dffe, which the detector never considers, so there is nothing to report and no $fsm cell follows. The stock flow runs opt -nodffe -nosdff before detecting so enables stay as gates around a plain $dff, exactly the recorded second run.
Every verdict, count, ABC figure, chip area, and the netlist come from seven recorded Yosys 0.70+post runs: detection with initial, without reset, and with sync reset; the enable-folding pair; the full binary pipeline with ABC against course.lib (typical.constr); and the override flag. Pass order follows yosys -h fsm in this build. Absolute areas are teaching units.