Skip to content
code0.HDL

Finite state machines with StmtFSM

Writing a multi-cycle process as a Stmt, running it with mkFSM, and reading its behaviour, one committed step per action in seq, loops with repeat, while and for, waiting with await, overlapping branches with par, and finishing only after a run that was started.

In this chapter

  • Trace a seq process step by step and say which committed value each step reads
  • Choose repeat, while or for from whether the contract fixes an amount of work or a target state
  • Explain what await holds and what keeps running, and why done alone cannot end a testbench
  • Predict the G0004 error of two writes in one action and the shadowing warnings of two par branches writing one register
Concepts in this chapter
  • Finite state machines with StmtFSM
Prerequisite concepts

Rules describe hardware that reacts whenever work is enabled. Some processes are easier to state as an ordered plan: do one thing, wait for an event, repeat some work, then report that you are done. Written with plain rules, that plan spreads its "where am I" state across several registers and guards, as the phase register in State machines with rules did. The StmtFSM library lets you write the plan as sequential statements and compiles it into ordinary BSV rules.

You need registers, actions, rule guards, and the fact that register writes commit at a clock edge. Pattern matching covered runtime choices inside one cycle; this chapter spreads work over several cycles.

1. Describing and running a process

1.1 Stmt and mkFSM

A Stmt is a value that describes control flow. Building it does not perform any action. Passing it to mkFSM creates a controller: state that remembers how far the process has progressed, plus rules that advance it.

Syntax

import StmtFSM::*;

Stmt name = seq
  statement;
  statement;
endseq;

FSM controller <- mkFSM(name);
Stmt process = seq
  value <= 3;
  value <= value + 4;
endseq;

FSM controller <- mkFSM(process);
PartWhen it mattersRole
Stmt process = seq ... endseq;elaborationdescribes which steps exist and in which order
mkFSM(process)elaboration, then runtimebuilds the controller state and the rules that advance it
the actions inside the stepsruntimeread and write registers and call methods, atomically

The controller's state remembers where the process is. Your own registers remember what the process works on. Keeping the two apart explains why other registers can keep changing while an FSM waits.

Note. A Stmt that is never passed to mkFSM does nothing. Declaring Stmt p = seq v <= 9; endseq; without an FSM leaves v at its initial value.

1.2 The FSM interface

mkFSM returns an interface with four methods.

MethodWhat it does
startstarts a new run; it can be called only while the controller is idle
doneTrue whenever the controller is idle, False while a run is active
waitTillDonean Action with no state update, ready only when the controller is done; use it after starting the run
abortstops an active run without performing its remaining statements

done is already True before the first start, and it becomes True again after the last statement finishes. A finished controller can be started again.

1.3 Starting and observing completion

Because done is also true before any run, a testbench must remember that it started the controller.

Reg#(Bool) started <- mkReg(False);

rule launch (!started);
  controller.start;
  started <= True;
endrule

rule finish (started && controller.done);
  $finish(0);
endrule
CodeRole
startedtestbench state, not part of the Stmt
launchstarts the FSM exactly once
started && controller.donetrue only after a run that was started has finished

Without started, finish would see an idle controller in the first cycle and end the simulation before any work was done. Every example in this chapter uses this pattern.

2. Sequential steps with seq

2.1 One step per action

Statements inside seq ... endseq run in source order. Each action statement is a separate FSM step, and its register writes commit at the clock edge before the next step begins. A later step therefore reads what an earlier step wrote.

Syntax

seq
  action1;
  action2;
endseq

This differs from a rule, where every read sees the entry state, as Rules that share state showed. If you want several writes in one step, group them in an action ... endaction block: they then commit together, and reads inside the block still see the old values.

seq
  action
    value <= 3;
    other <= value + 4;   // reads the old value, not 3
  endaction
endseq

2.2 Example: two dependent steps

In SequentialActions.bsv, the process writes 3 to value and then adds 4. Read the value declaration, the two statements of process, and the start and finish rules.

Predict before you run.

  1. Does the second assignment read the initial 0 or the 3 written by the first step?
  2. Which value does finish print?

Check

The process is seq value <= 3; value <= value + 4; endseq, starting from value = 0. What does finish print?

Your answer

SequentialActions.bsv
package SequentialActions;

import StmtFSM::*;

(* synthesize *)
module mkTop(Empty);
  Reg#(UInt#(8)) value <- mkReg(0);

  // Each action becomes a separate sequential FSM step.
  Stmt process = seq
    value <= 3;
    // The second step reads the value committed by the first step.
    value <= value + 4;
  endseq;

  FSM controller <- mkFSM(process);
  Reg#(Bool) started <- mkReg(False);

  rule start (!started);
    controller.start;
    started <= True;
  endrule

  rule finish (started && controller.done);
    $display("value=%0d", value);
    $finish(0);
  endrule
endmodule

endpackage

Practice this concept

Use this concept in a short BSV problem as your next step.

Check the result. Follow the transitions, not the indentation.

PointvalueControllerWhat happens
before start0idlenothing
first step0runningvalue <= 3 is scheduled
after the first edge3runningthe second step begins
second step3runningvalue + 4 schedules 7
after the last edge7donefinish reads 7

The printed 7 shows both the order of the steps and the clock edge between them. Had the second step read the initial 0, the result would have been 4.

Try it yourself.

  1. Swap the two statements. The process adds 4 first and then overwrites the register with 3, so finish prints 3. The expressions did not change; only the control order did.
  2. Wrap both statements in one action ... endaction. They now form a single step that writes value twice, and compilation stops with error G0004: the rule "uses methods that conflict in parallel", two calls of value.write. One atomic action may write a register only once.

3. Repetition and conditions

3.1 repeat, while, and for

Loop statements inside a Stmt repeat FSM steps over several cycles. This is different from a loop in a function, which elaboration unfolds into hardware within one cycle, as in Variables, loops, and the context of a literal.

Syntax

repeat (count) statement
while (condition) statement
for (init; condition; update) statement
FormRepeatsUse it when
repeat (n)exactly n timesthe contract fixes an amount of work
while (c)while c is true at the start of each passthe contract fixes a target state
for (...)while the condition holds, with register updatesyou need an index register

In a Stmt, the parts of a for are actions, so the index is a register written with <=.

for (i <= 0; i < 3; i <= i + 1)
  acc <= acc + 10;

Starting from acc = 0, this adds 10 three times; when the FSM is done, i is 3 and acc is 30.

3.2 Example: increment four times

RepeatLoop.bsv repeats one increment of count. Find the initial value of count, the repeat count, and the single register write.

Predict before you run.

  1. What does finish print?
  2. How many increments commit, and what are the intermediate values?

Check

count starts at 0 and the FSM runs repeat (4) seq count <= count + 1; endseq. What does finish print?

Your answer

RepeatLoop.bsv
package RepeatLoop;

import StmtFSM::*;

(* synthesize *)
module mkTop(Empty);
  Reg#(UInt#(8)) count <- mkReg(0);

  Stmt process = seq
    // The repeat count determines how many times the body executes.
    repeat (4) seq
      count <= count + 1;
    endseq
  endseq;

  FSM controller <- mkFSM(process);
  Reg#(Bool) started <- mkReg(False);

  rule start (!started);
    controller.start;
    started <= True;
  endrule

  rule finish (started && controller.done);
    $display("count=%0d", count);
    $finish(0);
  endrule
endmodule

endpackage

Practice this concept

Use this concept in a short BSV problem as your next step.

Check the result. count goes 0 -> 1 -> 2 -> 3 -> 4, and finish prints 4. Each arrow is one run of the body and one committed write. The controller reports done only after the fourth write has committed, so the printed value checks the loop control and the register update together.

Try it yourself.

  1. Change the initial value to mkReg(2). repeat (4) still performs four increments, and the result is 6.
  2. Keep mkReg(2) and replace repeat (4) with while (count < 4). The loop now stops at the target state after two increments, and the result is 4.

3.3 if inside a Stmt

if and else choose which statements the FSM runs next. The condition is evaluated when the FSM reaches the if, after earlier steps have committed.

seq
  x <= 7;
  if (x > 5) x <= 100; else x <= 1;
endseq

The if reads the committed 7, so x ends at 100.

4. Waiting with await

4.1 Holding one control point

await (condition) keeps the FSM at its current position until the condition is true. Later statements do not run, and the FSM does not report done. Other rules in the module keep running, including the one that will eventually make the condition true.

Syntax

await (condition);

Caution. If nothing ever makes the condition true, the FSM waits forever. A finish rule guarded by done then never fires, and the simulation ends only when the execution time limit stops it.

4.2 Example: wait for readiness, then write the result

AwaitCompletion.bsv has a free-running rule advance that counts cycles and sets ready when it sees cycle == 3. The FSM waits for ready and then writes 8'h2a to result. Read advance first, then the await, then the result write, then finish.

Predict before you run.

  1. Which registers change while the FSM waits at await?
  2. Can finish ever print a result of 0?
  3. Will the printed cycle be exactly 3?

Check

finish is guarded by started && controller.done. Can it print result=00?

Your answer

AwaitCompletion.bsv
package AwaitCompletion;

import StmtFSM::*;

(* synthesize *)
module mkTop(Empty);
  Reg#(UInt#(4)) cycle <- mkReg(0);
  Reg#(Bool) ready <- mkReg(False);
  Reg#(Bit#(8)) result <- mkReg(0);

  rule advance;
    cycle <= cycle + 1;
    if (cycle == 3) ready <= True;
  endrule

  Stmt process = seq
    // The FSM remains at await until ready is true.
    await (ready);
    result <= 8'h2a;
  endseq;

  FSM controller <- mkFSM(process);
  Reg#(Bool) started <- mkReg(False);

  rule start (!started);
    controller.start;
    started <= True;
  endrule

  // done becomes true only after the final action completes.
  rule finish (started && controller.done);
    $display("cycle=%0d result=%02h", cycle, result);
    $finish(0);
  endrule
endmodule

endpackage

Practice this concept

Use this concept in a short BSV problem as your next step.

Check the result. The run has four phases.

  1. start activates the controller while ready is false and result is 0.
  2. advance keeps incrementing cycle; the FSM stays at await.
  3. The firing of advance that sees cycle == 3 schedules ready <= True. The write commits at the edge, so await sees it on a later cycle.
  4. The FSM passes await, writes 8'h2a in the next step, and reports done only after that write has committed.

The printed result is 2a, never 0. The printed cycle is well past 3, because advance kept running while the FSM moved through its steps. Do not compute exact latency from the number of source lines: generated control steps and other rules decide the cycle placement. The stable contract is the order: readiness, then the result write, then done.

Try it yourself.

  1. Change cycle == 3 to cycle == 6. The printed cycle grows by three, and result is still 2a. The latency changed; the order did not.
  2. Restore the comparison and remove started && from the finish guard. done is true while the controller is idle, so finish fires in the first cycle and prints cycle=0 and result=00. This is the premature-finish symptom from section 1.3, reproduced on purpose.

5. Parallel branches with par

5.1 par ... endpar

The branches of a par block may make progress at the same time. The par statement finishes only when every branch has finished.

Syntax

par
  branch1;
  branch2;
endpar
par
  a <= 1;
  b <= 2;
endpar

Both branches write different registers, so they are independent; after the par, a is 1 and b is 2.

5.2 par follows normal scheduling

Every action inside a branch becomes a generated rule, and par does not change how rules are scheduled. Two branches that write x and do not read it are not a conflict but the shadowing case: bsc compiles the design with warnings G0036 and G0117, naming generated rules such as f_action_l7c28 (built from the source line and column), and only the write later in the schedule survives. Branches in which each reads what the other writes are a genuine conflict and cannot share a cycle. Both cases behave exactly as two rules did in Rules that share state. If exact cycle-level overlap is part of your design contract, check the scheduling warnings or write explicit rules.

Check

An FSM runs par x <= 1; x <= 2; endpar starting from x = 0. What does bsc report, and what does x hold when the FSM is done?

Your answer

6. Common mistakes

6.1 Finishing on done without having started

// Wrong: done is already true before start
rule finish (controller.done);
  $finish(0);
endrule

// Right: require evidence that a run began
rule finish (started && controller.done);
  $finish(0);
endrule

The wrong version ends the simulation in the first cycle with initial values. In the await example it prints cycle=0 and result=00.

6.2 Putting dependent writes in one step

// Wrong: both writes see the entry value 0
seq
  action
    value <= 3;
    other <= value + 4;   // 4, not 7
  endaction
endseq

// Right: separate steps, so the second reads the committed 3
seq
  value <= 3;
  other <= value + 4;     // 7
endseq

Inside one action, every read sees the entry state. A dependency on a new value needs a step boundary.

6.3 Declaring a Stmt and never running it

// Wrong: describes a process, runs nothing
Stmt process = seq v <= 9; endseq;

// Right: build a controller and start it
Stmt process = seq v <= 9; endseq;
FSM controller <- mkFSM(process);
// ... and call controller.start from a rule

A Stmt is only a description. Without mkFSM and start, no register changes.

6.4 Choosing repeat when the contract is a target state

// Wrong when the goal is "reach 4": from 2 it ends at 6
repeat (4) count <= count + 1;

// Right for a target state
while (count < 4) count <= count + 1;

repeat (n) counts passes, not results. Decide whether the contract fixes an amount of work or a final state before choosing the loop form.

6.5 Writing one register from two par branches

// Wrong: one branch's write of x shadows the other's
par
  x <= 1;
  x <= 2;
endpar

// Right: give each branch its own state, or order them
seq
  x <= 1;
  x <= 2;
endseq

The wrong version compiles with warnings G0036 and G0117, and one of the writes is lost. par only says the branches may overlap; it does not make two writes to one register meaningful.

7. When to use StmtFSM

StmtFSM suits testbenches, initialization sequences, and low-throughput controllers, where a readable process matters more than the last cycle of performance. Waits, loops, and completion stay in one place.

Explicit rules are better when throughput, resource conflicts, or exact overlap must be engineered by hand. A designer who knows two operations are safe can combine loop bookkeeping and useful work in one cycle, while generic FSM compilation may use an extra step to stay safe. Both forms compile to hardware; the choice is between structured clarity and direct control of cycles.

8. Summary

TopicKey point
Stmta description of control flow; does nothing until given to mkFSM
mkFSMbuilds the controller state and rules; returns start, done, waitTillDone, abort
donetrue whenever idle, including before the first start
seqone step per action; the next step reads committed values
action ... endactiongroups writes into one step; reads see the entry state
Loopsrepeat fixes the work, while the target state; for uses register indices
awaitholds only this FSM; other rules keep running
parbranches may overlap; writes to one register shadow, and read/write cycles conflict, as between rules

9. Check your understanding

  1. Why does declaring a Stmt not change any register?

    Show answer

    A Stmt is a value that describes control flow during elaboration. Only the controller built by mkFSM, once started, runs the actions.

  2. What lets the second action in a seq read the first action's result?

    Show answer

    The two actions are separate FSM steps. The first step's write commits at the clock edge before the second step is evaluated.

  3. Which state may keep changing while an FSM is stopped at await?

    Show answer

    Every register written by other rules, such as cycle in the example. Only this FSM's own progress is held.

  4. Why is par not a guarantee that two branches fire in the same cycle?

    Show answer

    Each branch action becomes a generated rule and is scheduled like any other rule. A later branch's write to the same register shadows an earlier one, and branches in which each reads what the other writes conflict and cannot share a cycle.

  5. Why must a finish rule distinguish the initial idle state from a completed run?

    Show answer

    done is true in both states. Without a separate started record, the finish rule fires before the FSM has done any work.

The Practice problems below ask you to build and repair FSMs: a Stmt that contains only noAction, a repeated body that stops one increment short of its required count, and an FSM that reports completion before its awaited work has produced a result.

Checkpoint

Run the example and compare it with your prediction

Sources

Licences and notices for the sources this site follows