The receiver's three outputs split across the two kinds. Ready watches only the state: high in IDLE, low everywhere else. Accept watches only the state too: high in DONE. Those are Moore outputs; they move only when the machine moves. Err watches the state and an input: high in CRC while crc_ok stays low. That is a Mealy output; it answers inside the cycle, the moment the input changes, without waiting for a state move.
The combinational answer has a physical shadow, and it falls on every gate-driven pin, not only the Mealy one. The state register holds three bits that never arrive at the decode gates at exactly the same instant: routing skews them, so during a transition the decoder briefly sees a wrong code and ready, accept and err can all wobble before they settle. The encoding lesson recorded how often that happens: the HDR to PAYLOAD hop flips two bits at once, so every packet walks through at least one skewed code. Err carries a second shadow on top: it also watches the crc_ok input, so a mid-cycle input change moves the pin with no state move at all. A downstream flop sampling any of these pins near a state change can catch the wobble. Registering the outputs removes the whole class: the pin is driven by a flop, so it changes only at clock edges, after the decode has had the full cycle to settle.
To measure the output path alone, the scripts below lift the output decode out of the machine with the state presented as an input. Running ltp over the full receiver would only report the state feedback loop, which drowns the output arc; the isolated decoders put the exact decode from the two-process listing under the probe. The combinational decoder is the two-process output block: ready, accept and err straight from state and crc_ok, with err the only Mealy output among the three. The registered decoder is the three-process output block: the same three decodes sampled into flops under the synchronous reset. Both scripts measure with ltp -noff: by default ltp walks through flops, so the flag stops the walk at flip-flop boundaries and reports the decode alone (check with yosys -h ltp).
read_verilog <<EOV
module out_mealy (input wire [2:0] state, input wire crc_ok, output wire ready, output wire accept, output wire err);
assign ready = (state == 3'd0);
assign accept = (state == 3'd4);
assign err = (state == 3'd3) && !crc_ok;
endmodule
EOV
hierarchy -top out_mealy
proc
opt_clean
ltp -noff
stat
write_json net_mealy.json
select -assert-none t:$dff t:$dffe
log PASS: mealy decoder is pure combinationalread_verilog <<EOV
module out_reg (input wire clk, input wire rst, input wire [2:0] state, input wire crc_ok,
output reg ready, output reg accept, output reg err);
always @(posedge clk) begin
if (rst) begin ready <= 1'b1; accept <= 1'b0; err <= 1'b0; end
else begin ready <= (state == 3'd0); accept <= (state == 3'd4); err <= (state == 3'd3) && !crc_ok; end
end
endmodule
EOV
hierarchy -top out_reg
proc
opt_clean
ltp -noff
stat
write_json net_reg.json
select -assert-count 3 t:$dff
log PASS: registered decoder holds outputs in three flopsThe combinational path runs 2 stages and ends at the err pin, with no flops at all. The registered path runs 3 stages and ends at a flop, and the decoder needs three flops, one per output pin.
The combinational recording is short enough to quote whole: Longest topological path in out_mealy (length=2):, stepping from a state bit through the equality test and the AND gate to the err pin. Every stage sits between an input and a pin, so every input change and every state skew reaches the pin inside the cycle. The registered recording reads Longest topological path in out_reg (length=3):: with ltp -noff the walk stops at the flop boundary, so the decode ends at a flop instead of a pin, with the reset selection mux adding one stage on the way in. The pin itself has left the combinational world entirely.
Output pin | Combinational driver | Registered driver |
|---|---|---|
ready | $logic_not | $dff |
accept | $eq | $dff |
err | $logic_and | $dff |
The driver table is read back from the two recorded netlists, and it is the structural half of the glitch argument. Every combinational pin is driven by a gate, so every pin can wobble mid-cycle. Every registered pin is driven by a flop Q, so no pin moves except at a clock edge. The price sits in the flop column: the combinational decoder asserts zero flops and the registered decoder asserts three, one per pin, exactly the three extra flops the coding-styles lesson measured between the two-process and three-process receivers.
Cleaner timing always has a price, and for sampling the state it is exactly one clock cycle. The two-process pins track the state: accept rises in the same cycle the machine enters DONE. The three-process pins track the previous cycle: the output block samples the DONE code after the state has already moved, so accept rises one cycle later. The testbench below steps all three receivers through one identical packet and checks the offset at the decisive cycle, and its PASS line is the recorded receipt.
module pkt_2p (input wire clk, input wire rst, input wire start, input wire byte_valid, input wire last_byte, input wire crc_ok,
output wire ready, output wire accept, output wire 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;
case (state)
IDLE: if (start) next = HDR;
HDR: if (byte_valid) next = PAYLOAD;
PAYLOAD: if (byte_valid && last_byte) next = CRC;
CRC: if (crc_ok) next = DONE; else next = IDLE;
DONE: next = IDLE;
default: next = IDLE;
endcase
end
assign ready = (state == IDLE);
assign accept = (state == DONE);
assign err = (state == CRC) && !crc_ok;
endmodule
module pkt_3p (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;
case (state)
IDLE: if (start) next = HDR;
HDR: if (byte_valid) next = PAYLOAD;
PAYLOAD: if (byte_valid && last_byte) next = CRC;
CRC: if (crc_ok) next = DONE; else next = IDLE;
DONE: next = IDLE;
default: next = IDLE;
endcase
end
always @(posedge clk) begin
if (rst) begin ready <= 1'b1; accept <= 1'b0; err <= 1'b0; end
else begin ready <= (state == IDLE); accept <= (state == DONE); err <= (state == CRC) && !crc_ok; end
end
endmodule
module pkt_3pla (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;
case (state)
IDLE: if (start) next = HDR;
HDR: if (byte_valid) next = PAYLOAD;
PAYLOAD: if (byte_valid && last_byte) next = CRC;
CRC: if (crc_ok) next = DONE; else next = IDLE;
DONE: next = IDLE;
default: next = IDLE;
endcase
end
always @(posedge clk) begin
if (rst) begin ready <= 1'b1; accept <= 1'b0; err <= 1'b0; end
else begin ready <= (next == IDLE); accept <= (next == DONE); err <= (state == CRC) && !crc_ok; end
end
endmoduleThe bench carries all three receivers: the two-process and three-process listings plus a look-ahead variant. At the cycle the machine enters DONE, the two-process and look-ahead accepts are already high while the three-process accept is still low; one cycle later the three-process accept arrives as all three machines return to IDLE. The third receiver samples next instead of state — look-ahead Moore output, flop-driven yet answering in the same cycle, as the bench records. Only the Moore outputs take this shortcut: ready and accept decode from next, while err stays on state, because err also watches crc_ok and predicting an input is guessing, not look-ahead. Register where the protocol allows it, and re-verify the handshake the cycle the outputs move: an old testbench expecting same-cycle answers fails against correct gates.
The registered script above resets its three flops to the idle values. The script below deletes the reset branch and records what changes. The decode is identical, the flop count is identical, and the longest path shortens by exactly the reset selection mux.
read_verilog <<EOV
module out_reg_norst (input wire clk, input wire [2:0] state, input wire crc_ok,
output reg ready, output reg accept, output reg err);
always @(posedge clk) begin
ready <= (state == 3'd0); accept <= (state == 3'd4); err <= (state == 3'd3) && !crc_ok;
end
endmodule
EOV
hierarchy -top out_reg_norst
proc
opt_clean
ltp -noff
stat
write_json net_norst.json
select -assert-count 3 t:$dff
log PASS: unreset decoder still holds three flopsThe recorded path without reset runs 2 stages against 3 with reset, and both need the same three flops. The shorter path is real, and so is the trap: without the reset branch the three flops power up unknown, and no assertion in either script can warn about it because unknown power-up is a simulation behaviour, not a synthesis error. Every output flop added for timing still needs its reset value, or the first cycle out of power-up samples garbage through a freshly cleaned-up path.
The registered path carries the reset selection mux on the way to the flop, which the combinational cloud never had. Timing is still better because the decode closes inside the cycle at a flop while the pin leaves the combinational world: downstream sees flop-to-flop instead of input-to-output.
The reset branch is gone, so all three flops power up unknown and the first active cycle samples garbage. Synthesis reports nothing because unknown power-up is a simulation behaviour; only the reset values prevent it.
Path reports, netlists, driver table, and flop counts come from recorded Yosys 0.70+post runs of both isolated decoders, each measured with ltp -noff. The one-cycle latency and the look-ahead same-cycle answer come from the recorded stepping bench over all three receivers; the unreset variant is a third recorded run. The glitch argument is structural reasoning over recorded pin drivers, not a measured waveform.