Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • Logic Synthesis Glossary
  • Logic Synthesis Cheatsheet
  • Synthesis Foundations14
  • Logic Optimisation and Mapping20
  • Constraints and Timing15
  • Sequential, FSM and Memory19
    • Flops, Resets and Enables
    • FSM Synthesis
      • Coding Styles
      • State Encoding
      • Unreachable States
      • Output Registering
      • Yosys FSM Passes
    • Memory and Macro Inference
    • Retiming and Pipelining
  • Datapath Synthesis14
  • Area, Power and Test14
  • Verification, ECO and Handoff15
  • Flows and Debugging15
  • Interview and Judgment11
Loading lesson

←FSM Synthesis
  1. Topics
  2. Logic Synthesis
  3. Sequential, FSM and Memory
  4. FSM Synthesis
  5. Output Registering
On this page

    Part 4 of 9, lesson 9 of 19Part 4 of 9/Sequential, FSM and Memory/Lesson 9 of 19Intermediate

    Output Registering

    Mealy and Moore outputs, why registering outputs removes glitches, and the cycle it costs.

    A gate-driven pin can wobble inside the cycle while a flop-driven pin only moves at clock edges, and the difference is one cycle of latency.

    After this lesson you can: move the output decode across a flop boundary, read the before-and-after longest paths, and name the latency the move adds.

    Mealy answers now, Moore answers on the move

    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).

    yosys
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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 combinational
    yosys
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    read_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 flops
    Predict first The combinational pins are gate-driven and the registered pins are flop-driven. What does each recorded longest path end at, and how many flops does each decoder need?

    The 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.

    What the two runs printed

    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.

    The recorded registered decoder: three blue output flops whose D inputs carry the decode, with the pins flop-driven.

    The latency receipt

    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.

    pkt_2p.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    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
    endmodule

    The 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.

    Break it: drop the reset

    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.

    yosys
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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 flops

    The 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.

    Registering an output changes cycle behaviour, so an old testbench that expects same-cycle answers fails even though the gates are correct. Update the protocol timing first, then re-verify: the bench above shows the shape, checking both answers at both cycles instead of assuming either one.

    Q: the Mealy err pin glitches near state changes. What do you change, what does it cost, and how do you prove the latency?

    Weak answer: add a bigger output buffer. Strong answer: sample the decode into flops so every pin is flop-driven; here the 2-stage input-to-pin path becomes a flop-to-flop path closing at the output flop, at a cost of three flops and exactly one cycle, which the stepping bench above records as accept arriving one cycle after DONE. Follow-up: how do you keep the flops but lose the cycle? Decode from the next state instead of the state: the look-ahead accept is still flop-driven yet answers in the same cycle as the two-process pin, as the same bench records.

    Check yourself The combinational longest path runs 2 stages to a pin while the registered path runs 3 stages to a flop. Why is the registered number bigger, and why is the timing still better?

    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.

    Check yourself The break-it run shortens the path to 2 stages with the same three flops. Why is that not a free win?

    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.

    • The combinational path runs two stages to the err pin; the registered decode runs three stages to a flop and the pins are flop-driven.

    • Sampling the state costs three flops and one cycle; decoding the next state instead answers in the same cycle.

    • Every output flop still needs its reset value, or the first cycle samples garbage.

    Rule of thumb: update the protocol timing first when outputs move a cycle, then re-verify the handshake.

    How this was checked

    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.

    • Interview: run a mock interview on this topic

    Related topics

    • Up: FSM Synthesis

    • Next: Yosys FSM passes

    • Delay Models and Slack

    Previous

    60 / 139

    Next: Yosys FSM Passes

    Up next

    Up nextYosys FSM Passes

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Coding Styles
    • State Encoding