LLM-FSM: Finite-State Reasoning for RTL Code Generation
This skill enables Claude to translate natural-language hardware specifications into correct register transfer-level (RTL) implementations by decomposing the problem through a structured YAML intermediate representation of the underlying finite-state machine (FSM). Rather than attempting direct spec-to-Verilog generation (which degrades sharply as FSM complexity grows), this approach first extracts the FSM topology -- states, transitions, inputs, outputs, and conditions -- into a machine-readable YAML format, then compiles that representation into synthesizable Verilog. This two-stage pipeline (Spec -> YAML -> RTL) significantly reduces structural errors like missing transitions and incorrect timing semantics, based on findings from the LLM-FSM benchmark of 1,000 FSM problems across varying complexity tiers.
When to Use
- When the user provides a natural-language description of a hardware controller, protocol handler, or sequencer and wants Verilog/SystemVerilog output
- When the user has a state diagram or state table and needs it translated to synthesizable RTL
- When the user asks to implement an FSM-based module (e.g., UART controller, SPI master, bus arbiter, traffic light controller)
- When the user provides a specification document describing state-dependent behavior and asks for hardware code
- When debugging an existing FSM implementation -- extract the intended FSM structure from the spec before comparing to the code
- When the user wants to verify that an RTL implementation matches a natural-language specification by reconstructing the FSM
Key Technique
The core insight from the LLM-FSM paper is that direct specification-to-RTL generation fails at scale. Even the strongest LLMs drop from ~90% accuracy on simple FSMs (4-14 states) to ~66% on complex ones (27-59 states). The critical failure modes are: (1) missing or extra state transitions, (2) incorrect cycle-level timing semantics, (3) malformed output logic, and (4) syntax errors in the generated code. These errors compound as FSM complexity grows because the LLM must simultaneously reason about graph structure, signal assignments, and HDL syntax.
The two-stage pipeline mitigates this by separating concerns. Stage 1 (Spec -> YAML) focuses purely on FSM reasoning: identifying states, enumerating all transitions with their conditions, and mapping inputs to outputs per state. Stage 2 (YAML -> RTL) is a deterministic or near-deterministic compilation step that maps the structured YAML into a standard Verilog FSM template. This decomposition exploits the fact that LLMs are better at structured extraction than simultaneous reasoning-and-coding, and the YAML intermediate representation serves as a verifiable checkpoint.
Test-time scaling through multi-trace sampling further improves reliability. Generating multiple candidate solutions (k=8-16) and selecting the best one via syntax checking and functional verification consistently outperforms single-shot generation. For critical hardware, generate several candidates and verify each against the specification.
Step-by-Step Workflow
Parse the specification for FSM indicators. Read the natural-language spec and identify: reset behavior, named states or operational modes, conditional transitions ("when X occurs, move to state Y"), input signals, output signals, and timing requirements (synchronous/asynchronous, clock edge sensitivity).
Extract the FSM into structured YAML. Create a YAML document with these required fields:
module_name: the Verilog module name
clock and reset: clock/reset signal names and polarity (active-high/low, posedge/negedge)
inputs: list of input signals with bit widths
outputs: list of output signals with bit widths and default values
states: enumeration of all states with a designated reset state
transitions: for each state, a list of {condition, next_state, outputs} tuples ordered by priority
output_type: Moore (outputs depend only on state) vs Mealy (outputs depend on state + inputs)
Validate the FSM graph for completeness. Check that: every state is reachable from the reset state, every state has at least one outgoing transition (including self-loops for hold conditions), there are no orphan states, and a default/else transition exists for each state to prevent latches.
Select the RTL coding style. Use a three-always-block pattern for synchronous FSMs: one block for state register update, one for next-state combinational logic, one for output combinational logic. This is the most portable and synthesis-friendly pattern.
Generate the Verilog module. Compile the YAML into Verilog using the three-always-block template: declare state encoding (one-hot or binary based on state count), implement the state register with synchronous reset, write the next-state logic as a case statement with priority-ordered conditions, and write the output logic.
Add default assignments to prevent latches. At the top of every combinational always block, assign default values to all outputs and next_state before the case statement. This is the single most common source of synthesis bugs in FSM code.
Verify signal completeness. Confirm that every input signal mentioned in the spec appears in at least one transition condition, every output signal is assigned in every state (either explicitly or via defaults), and bit widths match the specification.
Generate a basic testbench. Create a testbench that exercises: the reset sequence, at least one path through every state, boundary conditions on input signals, and the longest path through the FSM graph.
If the FSM has >20 states, decompose hierarchically. Split into sub-FSMs by identifying phases (initialization, operation, error handling, shutdown). Each phase becomes a sub-module with its own local FSM, and a top-level FSM manages phase transitions. This keeps each individual FSM within the complexity range where LLM generation is reliable.
Review for common RTL pitfalls. Check for: blocking vs non-blocking assignment correctness (<= in sequential, = in combinational), complete sensitivity lists (use always @(*) for combinational), no multiple drivers on the same signal, and correct reset initialization of all state-dependent registers.
Concrete Examples
Example 1: SPI Master Controller
User: "Write a Verilog module for an SPI master that transmits 8 bits MSB-first. It should have an idle state waiting for a start signal, then clock out 8 bits on MOSI synchronized to SCK, then return to idle with a done signal."
Approach:
- Extract FSM structure from the description
- Build the YAML intermediate representation
- Compile to three-always-block Verilog
YAML intermediate:
module_name: spi_master
clock: {name: clk, edge: posedge}
reset: {name: rst_n, active: low}
inputs:
- {name: start, width: 1}
- {name: data_in, width: 8}
outputs:
- {name: mosi, width: 1, default: 0}
- {name: sck, width: 1, default: 0}
- {name: done, width: 1, default: 0}
states: [IDLE, LOAD, SHIFT, DONE_ST]
reset_state: IDLE
output_type: mealy
transitions:
IDLE:
- {condition: "start", next: LOAD, outputs: {}}
- {condition: "default", next: IDLE, outputs: {}}
LOAD:
- {condition: "default", next: SHIFT, outputs: {mosi: "shift_reg[7]"}}
SHIFT:
- {condition: "bit_cnt == 7", next: DONE_ST, outputs: {sck: "~sck_r", mosi: "shift_reg[7]"}}
- {condition: "default", next: SHIFT, outputs: {sck: "~sck_r", mosi: "shift_reg[7]"}}
DONE_ST:
- {condition: "default", next: IDLE, outputs: {done: 1}}
Output:
module spi_master (
input wire clk,
input wire rst_n,
input wire start,
input wire [7:0] data_in,
output reg mosi,
output reg sck,
output reg done
);
localparam IDLE = 2'd0,
LOAD = 2'd1,
SHIFT = 2'd2,
DONE_ST = 2'd3;
reg [1:0] state, next_state;
reg [7:0] shift_reg;
reg [2:0] bit_cnt;
// State register
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
shift_reg <= 8'd0;
bit_cnt <= 3'd0;
end else begin
state <= next_state;
case (next_state)
LOAD: shift_reg <= data_in;
SHIFT: if (sck) shift_reg <= {shift_reg[6:0], 1'b0};
default: shift_reg <= shift_reg;
endcase
if (state == SHIFT)
bit_cnt <= bit_cnt + 3'd1;
else
bit_cnt <= 3'd0;
end
end
// Next-state logic
always @(*) begin
next_state = state;
case (state)
IDLE: next_state = start ? LOAD : IDLE;
LOAD: next_state = SHIFT;
SHIFT: next_state = (bit_cnt == 3'd7) ? DONE_ST : SHIFT;
DONE_ST: next_state = IDLE;
endcase
end
// Output logic
always @(*) begin
mosi = 1'b0;
sck = 1'b0;
done = 1'b0;
case (state)
SHIFT: begin
mosi = shift_reg[7];
sck = bit_cnt[0]; // Toggle SCK
end
DONE_ST: done = 1'b1;
endcase
end
endmodule
Example 2: Traffic Light Controller
User: "Implement a traffic light controller. Green for 30 cycles, yellow for 5, red for 30. Emergency input forces immediate red. After emergency clears, return to green."
Approach:
- Identify 4 states: GREEN, YELLOW, RED, EMERGENCY
- Build YAML with timer-based transitions
- Generate Verilog with counter logic
YAML intermediate:
module_name: traffic_light
clock: {name: clk, edge: posedge}
reset: {name: rst, active: high}
inputs:
- {name: emergency, width: 1}
outputs:
- {name: light, width: 2, default: 0} # 00=off, 01=green, 10=yellow, 11=red
states: [GREEN, YELLOW, RED, EMERG]
reset_state: RED
output_type: moore
transitions:
GREEN:
- {condition: "emergency", next: EMERG, outputs: {light: "2'b11"}}
- {condition: "timer == 29", next: YELLOW, outputs: {light: "2'b01"}}
- {condition: "default", next: GREEN, outputs: {light: "2'b01"}}
YELLOW:
- {condition: "emergency", next: EMERG, outputs: {light: "2'b11"}}
- {condition: "timer == 4", next: RED, outputs: {light: "2'b10"}}
- {condition: "default", next: YELLOW, outputs: {light: "2'b10"}}
RED:
- {condition: "emergency", next: EMERG, outputs: {light: "2'b11"}}
- {condition: "timer == 29", next: GREEN, outputs: {light: "2'b11"}}
- {condition: "default", next: RED, outputs: {light: "2'b11"}}
EMERG:
- {condition: "!emergency", next: GREEN, outputs: {light: "2'b11"}}
- {condition: "default", next: EMERG, outputs: {light: "2'b11"}}
Output: A three-always-block Verilog module with a 5-bit timer that resets on state transitions, priority checking for the emergency input in every state's next-state logic, and Moore-style output assignment based solely on current state.
Example 3: Hierarchical Decomposition for Complex Protocol
User: "Implement an I2C master controller with support for start, address, write data, read data with ACK/NACK, repeated start, and stop conditions."
Approach:
- Recognize this exceeds 20 states -- decompose hierarchically
- Identify phases: IDLE, START, ADDRESS, WRITE, READ, STOP
- Create a top-level FSM managing phase transitions
- Implement each phase as a sub-FSM module
- Generate YAML and Verilog for each sub-module independently
Top-level YAML:
module_name: i2c_master_top
states: [IDLE, START, ADDR, WRITE, READ, REP_START, STOP]
reset_state: IDLE
transitions:
IDLE:
- {condition: "go", next: START}
START:
- {condition: "start_done", next: ADDR}
ADDR:
- {condition: "addr_done && rw == 0", next: WRITE}
- {condition: "addr_done && rw == 1", next: READ}
# ... each phase sub-FSM handles its own internal states
Each sub-module (e.g., i2c_start_gen, i2c_addr_phase) is a self-contained FSM with <10 states, keeping every module within the reliable generation range.
Best Practices
- Do: Always produce the YAML intermediate before writing Verilog. This catches missing transitions and ambiguous spec language before they become RTL bugs.
- Do: Use the three-always-block pattern (state register, next-state combinational, output combinational) as the default FSM coding style. It is universally supported by synthesis tools and easiest to verify.
- Do: Add default assignments at the top of every combinational block before the case statement. This single practice eliminates the most common class of FSM synthesis bugs (inferred latches).
- Do: Decompose any FSM with more than ~20 states into hierarchical sub-FSMs. LLM accuracy on FSMs with 27+ states drops by 15-25 percentage points.
- Avoid: Generating Verilog directly from a complex natural-language spec without the YAML extraction step. Direct generation has significantly higher error rates for FSMs beyond trivial complexity.
- Avoid: Using
always @(posedge clk) for combinational next-state or output logic. This introduces unintended pipeline stages and breaks the FSM timing model.
- Avoid: Omitting self-loop transitions for states that should hold. Every state must have a defined behavior for every possible input combination, or synthesis will infer latches.
Error Handling
| Error Type |
Symptom |
Fix |
| Missing transitions |
Simulation hangs in a state or synthesis warns about latches |
Add explicit default/else transitions for every state in the YAML before generating RTL |
| Timing mismatch |
Outputs appear one cycle early/late |
Verify Moore vs Mealy classification; Moore outputs are registered (one cycle delay), Mealy are combinational (same cycle) |
| Unreachable states |
Dead code warnings from synthesis |
Trace the state graph from reset_state; remove any state with no incoming path |
| Multiple drivers |
Synthesis error on signal assignment |
Ensure each output is assigned in exactly one always block; do not split output logic across blocks |
| Reset incompleteness |
Simulation starts in unknown state |
Initialize all registers (state, counters, shift registers) in the reset branch of the sequential always block |
| Syntax errors |
Compilation failures in generated Verilog |
Validate parentheses, semicolons, and begin/end blocks; use a linting pass before delivery |
Limitations
- Datapath-heavy designs: This technique optimizes for control-dominant FSMs. Designs where the complexity is in arithmetic datapath operations (ALUs, DSP pipelines) rather than state transitions will not benefit from FSM decomposition.
- Asynchronous FSMs: The approach assumes synchronous, single-clock-domain FSMs. Asynchronous or multi-clock designs require additional considerations (clock domain crossing, metastability) not covered here.
- Analog/mixed-signal: FSM extraction does not apply to analog behavioral descriptions.
- Ambiguous specifications: If the natural-language spec is genuinely ambiguous about state behavior, the YAML extraction step will surface the ambiguity but cannot resolve it -- ask the user for clarification.
- Very large FSMs (>50 states): Even with hierarchical decomposition, extremely large FSMs may require iterative refinement. Generate, simulate, identify failing paths, and fix incrementally.
Reference
LLM-FSM: Scaling Large Language Models for Finite-State Reasoning in RTL Code Generation -- Wu et al., 2026. Key takeaway: the Spec -> YAML -> RTL two-stage pipeline outperforms direct generation, and FSM complexity scaling reveals that structured intermediate representations are essential for reliable hardware code generation by LLMs.
1---2name: llm-fsm-scaling-finite-state-reasoning3description: Generate correct RTL (Verilog/SystemVerilog) implementations of finite-state machines from natural-language specifications using a structured YAML intermediate representation. Use when the user asks to: 'generate Verilog for this state machine', 'convert this FSM spec to RTL', 'write a hardware controller from this description', 'implement this protocol as a state machine in Verilog', 'create an FSM module from this specification', 'translate this state diagram to synthesizable code'.4---56# LLM-FSM: Finite-State Reasoning for RTL Code Generation78This skill enables Claude to translate natural-language hardware specifications into correct register transfer-level (RTL) implementations by decomposing the problem through a structured YAML intermediate representation of the underlying finite-state machine (FSM). Rather than attempting direct spec-to-Verilog generation (which degrades sharply as FSM complexity grows), this approach first extracts the FSM topology -- states, transitions, inputs, outputs, and conditions -- into a machine-readable YAML format, then compiles that representation into synthesizable Verilog. This two-stage pipeline (Spec -> YAML -> RTL) significantly reduces structural errors like missing transitions and incorrect timing semantics, based on findings from the LLM-FSM benchmark of 1,000 FSM problems across varying complexity tiers.910## When to Use1112- When the user provides a natural-language description of a hardware controller, protocol handler, or sequencer and wants Verilog/SystemVerilog output13- When the user has a state diagram or state table and needs it translated to synthesizable RTL14- When the user asks to implement an FSM-based module (e.g., UART controller, SPI master, bus arbiter, traffic light controller)15- When the user provides a specification document describing state-dependent behavior and asks for hardware code16- When debugging an existing FSM implementation -- extract the intended FSM structure from the spec before comparing to the code17- When the user wants to verify that an RTL implementation matches a natural-language specification by reconstructing the FSM1819## Key Technique2021**The core insight from the LLM-FSM paper is that direct specification-to-RTL generation fails at scale.** Even the strongest LLMs drop from ~90% accuracy on simple FSMs (4-14 states) to ~66% on complex ones (27-59 states). The critical failure modes are: (1) missing or extra state transitions, (2) incorrect cycle-level timing semantics, (3) malformed output logic, and (4) syntax errors in the generated code. These errors compound as FSM complexity grows because the LLM must simultaneously reason about graph structure, signal assignments, and HDL syntax.2223**The two-stage pipeline mitigates this by separating concerns.** Stage 1 (Spec -> YAML) focuses purely on FSM reasoning: identifying states, enumerating all transitions with their conditions, and mapping inputs to outputs per state. Stage 2 (YAML -> RTL) is a deterministic or near-deterministic compilation step that maps the structured YAML into a standard Verilog FSM template. This decomposition exploits the fact that LLMs are better at structured extraction than simultaneous reasoning-and-coding, and the YAML intermediate representation serves as a verifiable checkpoint.2425**Test-time scaling through multi-trace sampling further improves reliability.** Generating multiple candidate solutions (k=8-16) and selecting the best one via syntax checking and functional verification consistently outperforms single-shot generation. For critical hardware, generate several candidates and verify each against the specification.2627## Step-by-Step Workflow28291. **Parse the specification for FSM indicators.** Read the natural-language spec and identify: reset behavior, named states or operational modes, conditional transitions ("when X occurs, move to state Y"), input signals, output signals, and timing requirements (synchronous/asynchronous, clock edge sensitivity).30312. **Extract the FSM into structured YAML.** Create a YAML document with these required fields:32 - `module_name`: the Verilog module name33 - `clock` and `reset`: clock/reset signal names and polarity (active-high/low, posedge/negedge)34 - `inputs`: list of input signals with bit widths35 - `outputs`: list of output signals with bit widths and default values36 - `states`: enumeration of all states with a designated reset state37 - `transitions`: for each state, a list of `{condition, next_state, outputs}` tuples ordered by priority38 - `output_type`: Moore (outputs depend only on state) vs Mealy (outputs depend on state + inputs)39403. **Validate the FSM graph for completeness.** Check that: every state is reachable from the reset state, every state has at least one outgoing transition (including self-loops for hold conditions), there are no orphan states, and a default/else transition exists for each state to prevent latches.41424. **Select the RTL coding style.** Use a three-always-block pattern for synchronous FSMs: one block for state register update, one for next-state combinational logic, one for output combinational logic. This is the most portable and synthesis-friendly pattern.43445. **Generate the Verilog module.** Compile the YAML into Verilog using the three-always-block template: declare state encoding (one-hot or binary based on state count), implement the state register with synchronous reset, write the next-state logic as a case statement with priority-ordered conditions, and write the output logic.45466. **Add default assignments to prevent latches.** At the top of every combinational always block, assign default values to all outputs and next_state before the case statement. This is the single most common source of synthesis bugs in FSM code.47487. **Verify signal completeness.** Confirm that every input signal mentioned in the spec appears in at least one transition condition, every output signal is assigned in every state (either explicitly or via defaults), and bit widths match the specification.49508. **Generate a basic testbench.** Create a testbench that exercises: the reset sequence, at least one path through every state, boundary conditions on input signals, and the longest path through the FSM graph.51529. **If the FSM has >20 states, decompose hierarchically.** Split into sub-FSMs by identifying phases (initialization, operation, error handling, shutdown). Each phase becomes a sub-module with its own local FSM, and a top-level FSM manages phase transitions. This keeps each individual FSM within the complexity range where LLM generation is reliable.535410. **Review for common RTL pitfalls.** Check for: blocking vs non-blocking assignment correctness (`<=` in sequential, `=` in combinational), complete sensitivity lists (use `always @(*)` for combinational), no multiple drivers on the same signal, and correct reset initialization of all state-dependent registers.5556## Concrete Examples5758**Example 1: SPI Master Controller**5960User: "Write a Verilog module for an SPI master that transmits 8 bits MSB-first. It should have an idle state waiting for a start signal, then clock out 8 bits on MOSI synchronized to SCK, then return to idle with a done signal."6162Approach:631. Extract FSM structure from the description642. Build the YAML intermediate representation653. Compile to three-always-block Verilog6667YAML intermediate:68```yaml69module_name: spi_master70clock: {name: clk, edge: posedge}71reset: {name: rst_n, active: low}72inputs:73 - {name: start, width: 1}74 - {name: data_in, width: 8}75outputs:76 - {name: mosi, width: 1, default: 0}77 - {name: sck, width: 1, default: 0}78 - {name: done, width: 1, default: 0}79states: [IDLE, LOAD, SHIFT, DONE_ST]80reset_state: IDLE81output_type: mealy82transitions:83 IDLE:84 - {condition: "start", next: LOAD, outputs: {}}85 - {condition: "default", next: IDLE, outputs: {}}86 LOAD:87 - {condition: "default", next: SHIFT, outputs: {mosi: "shift_reg[7]"}}88 SHIFT:89 - {condition: "bit_cnt == 7", next: DONE_ST, outputs: {sck: "~sck_r", mosi: "shift_reg[7]"}}90 - {condition: "default", next: SHIFT, outputs: {sck: "~sck_r", mosi: "shift_reg[7]"}}91 DONE_ST:92 - {condition: "default", next: IDLE, outputs: {done: 1}}93```9495Output:96```verilog97module spi_master (98 input wire clk,99 input wire rst_n,100 input wire start,101 input wire [7:0] data_in,102 output reg mosi,103 output reg sck,104 output reg done105);106107 localparam IDLE = 2'd0,108 LOAD = 2'd1,109 SHIFT = 2'd2,110 DONE_ST = 2'd3;111112 reg [1:0] state, next_state;113 reg [7:0] shift_reg;114 reg [2:0] bit_cnt;115116 // State register117 always @(posedge clk or negedge rst_n) begin118 if (!rst_n) begin119 state <= IDLE;120 shift_reg <= 8'd0;121 bit_cnt <= 3'd0;122 end else begin123 state <= next_state;124 case (next_state)125 LOAD: shift_reg <= data_in;126 SHIFT: if (sck) shift_reg <= {shift_reg[6:0], 1'b0};127 default: shift_reg <= shift_reg;128 endcase129 if (state == SHIFT)130 bit_cnt <= bit_cnt + 3'd1;131 else132 bit_cnt <= 3'd0;133 end134 end135136 // Next-state logic137 always @(*) begin138 next_state = state;139 case (state)140 IDLE: next_state = start ? LOAD : IDLE;141 LOAD: next_state = SHIFT;142 SHIFT: next_state = (bit_cnt == 3'd7) ? DONE_ST : SHIFT;143 DONE_ST: next_state = IDLE;144 endcase145 end146147 // Output logic148 always @(*) begin149 mosi = 1'b0;150 sck = 1'b0;151 done = 1'b0;152 case (state)153 SHIFT: begin154 mosi = shift_reg[7];155 sck = bit_cnt[0]; // Toggle SCK156 end157 DONE_ST: done = 1'b1;158 endcase159 end160161endmodule162```163164**Example 2: Traffic Light Controller**165166User: "Implement a traffic light controller. Green for 30 cycles, yellow for 5, red for 30. Emergency input forces immediate red. After emergency clears, return to green."167168Approach:1691. Identify 4 states: GREEN, YELLOW, RED, EMERGENCY1702. Build YAML with timer-based transitions1713. Generate Verilog with counter logic172173YAML intermediate:174```yaml175module_name: traffic_light176clock: {name: clk, edge: posedge}177reset: {name: rst, active: high}178inputs:179 - {name: emergency, width: 1}180outputs:181 - {name: light, width: 2, default: 0} # 00=off, 01=green, 10=yellow, 11=red182states: [GREEN, YELLOW, RED, EMERG]183reset_state: RED184output_type: moore185transitions:186 GREEN:187 - {condition: "emergency", next: EMERG, outputs: {light: "2'b11"}}188 - {condition: "timer == 29", next: YELLOW, outputs: {light: "2'b01"}}189 - {condition: "default", next: GREEN, outputs: {light: "2'b01"}}190 YELLOW:191 - {condition: "emergency", next: EMERG, outputs: {light: "2'b11"}}192 - {condition: "timer == 4", next: RED, outputs: {light: "2'b10"}}193 - {condition: "default", next: YELLOW, outputs: {light: "2'b10"}}194 RED:195 - {condition: "emergency", next: EMERG, outputs: {light: "2'b11"}}196 - {condition: "timer == 29", next: GREEN, outputs: {light: "2'b11"}}197 - {condition: "default", next: RED, outputs: {light: "2'b11"}}198 EMERG:199 - {condition: "!emergency", next: GREEN, outputs: {light: "2'b11"}}200 - {condition: "default", next: EMERG, outputs: {light: "2'b11"}}201```202203Output: A three-always-block Verilog module with a 5-bit timer that resets on state transitions, priority checking for the emergency input in every state's next-state logic, and Moore-style output assignment based solely on current state.204205**Example 3: Hierarchical Decomposition for Complex Protocol**206207User: "Implement an I2C master controller with support for start, address, write data, read data with ACK/NACK, repeated start, and stop conditions."208209Approach:2101. Recognize this exceeds 20 states -- decompose hierarchically2112. Identify phases: IDLE, START, ADDRESS, WRITE, READ, STOP2123. Create a top-level FSM managing phase transitions2134. Implement each phase as a sub-FSM module2145. Generate YAML and Verilog for each sub-module independently215216Top-level YAML:217```yaml218module_name: i2c_master_top219states: [IDLE, START, ADDR, WRITE, READ, REP_START, STOP]220reset_state: IDLE221transitions:222 IDLE:223 - {condition: "go", next: START}224 START:225 - {condition: "start_done", next: ADDR}226 ADDR:227 - {condition: "addr_done && rw == 0", next: WRITE}228 - {condition: "addr_done && rw == 1", next: READ}229 # ... each phase sub-FSM handles its own internal states230```231232Each sub-module (e.g., `i2c_start_gen`, `i2c_addr_phase`) is a self-contained FSM with <10 states, keeping every module within the reliable generation range.233234## Best Practices235236- **Do:** Always produce the YAML intermediate before writing Verilog. This catches missing transitions and ambiguous spec language before they become RTL bugs.237- **Do:** Use the three-always-block pattern (state register, next-state combinational, output combinational) as the default FSM coding style. It is universally supported by synthesis tools and easiest to verify.238- **Do:** Add default assignments at the top of every combinational block before the case statement. This single practice eliminates the most common class of FSM synthesis bugs (inferred latches).239- **Do:** Decompose any FSM with more than ~20 states into hierarchical sub-FSMs. LLM accuracy on FSMs with 27+ states drops by 15-25 percentage points.240- **Avoid:** Generating Verilog directly from a complex natural-language spec without the YAML extraction step. Direct generation has significantly higher error rates for FSMs beyond trivial complexity.241- **Avoid:** Using `always @(posedge clk)` for combinational next-state or output logic. This introduces unintended pipeline stages and breaks the FSM timing model.242- **Avoid:** Omitting self-loop transitions for states that should hold. Every state must have a defined behavior for every possible input combination, or synthesis will infer latches.243244## Error Handling245246| Error Type | Symptom | Fix |247|---|---|---|248| **Missing transitions** | Simulation hangs in a state or synthesis warns about latches | Add explicit default/else transitions for every state in the YAML before generating RTL |249| **Timing mismatch** | Outputs appear one cycle early/late | Verify Moore vs Mealy classification; Moore outputs are registered (one cycle delay), Mealy are combinational (same cycle) |250| **Unreachable states** | Dead code warnings from synthesis | Trace the state graph from reset_state; remove any state with no incoming path |251| **Multiple drivers** | Synthesis error on signal assignment | Ensure each output is assigned in exactly one always block; do not split output logic across blocks |252| **Reset incompleteness** | Simulation starts in unknown state | Initialize all registers (state, counters, shift registers) in the reset branch of the sequential always block |253| **Syntax errors** | Compilation failures in generated Verilog | Validate parentheses, semicolons, and `begin/end` blocks; use a linting pass before delivery |254255## Limitations256257- **Datapath-heavy designs**: This technique optimizes for control-dominant FSMs. Designs where the complexity is in arithmetic datapath operations (ALUs, DSP pipelines) rather than state transitions will not benefit from FSM decomposition.258- **Asynchronous FSMs**: The approach assumes synchronous, single-clock-domain FSMs. Asynchronous or multi-clock designs require additional considerations (clock domain crossing, metastability) not covered here.259- **Analog/mixed-signal**: FSM extraction does not apply to analog behavioral descriptions.260- **Ambiguous specifications**: If the natural-language spec is genuinely ambiguous about state behavior, the YAML extraction step will surface the ambiguity but cannot resolve it -- ask the user for clarification.261- **Very large FSMs (>50 states)**: Even with hierarchical decomposition, extremely large FSMs may require iterative refinement. Generate, simulate, identify failing paths, and fix incrementally.262263## Reference264265[LLM-FSM: Scaling Large Language Models for Finite-State Reasoning in RTL Code Generation](https://arxiv.org/abs/2602.07032v1) -- Wu et al., 2026. Key takeaway: the Spec -> YAML -> RTL two-stage pipeline outperforms direct generation, and FSM complexity scaling reveals that structured intermediate representations are essential for reliable hardware code generation by LLMs.