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);| Part | When it matters | Role |
|---|---|---|
Stmt process = seq ... endseq; | elaboration | describes which steps exist and in which order |
mkFSM(process) | elaboration, then runtime | builds the controller state and the rules that advance it |
| the actions inside the steps | runtime | read 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.
| Method | What it does |
|---|---|
start | starts a new run; it can be called only while the controller is idle |
done | True whenever the controller is idle, False while a run is active |
waitTillDone | an Action with no state update, ready only when the controller is done; use it after starting the run |
abort | stops 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| Code | Role |
|---|---|
started | testbench state, not part of the Stmt |
launch | starts the FSM exactly once |
started && controller.done | true 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;
endseqThis 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
endseq2.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.
- Does the second assignment read the initial 0 or the 3 written by the first step?
- Which value does
finishprint?
Check
The process is seq value <= 3; value <= value + 4; endseq, starting from value = 0. What does finish print?
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
endpackagePractice this concept
Use this concept in a short BSV problem as your next step.
Check the result. Follow the transitions, not the indentation.
| Point | value | Controller | What happens |
|---|---|---|---|
before start | 0 | idle | nothing |
| first step | 0 | running | value <= 3 is scheduled |
| after the first edge | 3 | running | the second step begins |
| second step | 3 | running | value + 4 schedules 7 |
| after the last edge | 7 | done | finish 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.
- Swap the two statements. The process adds 4 first and then overwrites the
register with 3, so
finishprints 3. The expressions did not change; only the control order did. - Wrap both statements in one
action ... endaction. They now form a single step that writesvaluetwice, and compilation stops with errorG0004: the rule "uses methods that conflict in parallel", two calls ofvalue.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| Form | Repeats | Use it when |
|---|---|---|
repeat (n) | exactly n times | the contract fixes an amount of work |
while (c) | while c is true at the start of each pass | the contract fixes a target state |
for (...) | while the condition holds, with register updates | you 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.
- What does
finishprint? - 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?
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
endpackagePractice 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.
- Change the initial value to
mkReg(2).repeat (4)still performs four increments, and the result is 6. - Keep
mkReg(2)and replacerepeat (4)withwhile (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;
endseqThe 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.
- Which registers change while the FSM waits at
await? - Can
finishever print aresultof 0? - Will the printed
cyclebe exactly 3?
Check
finish is guarded by started && controller.done. Can it print result=00?
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
endpackagePractice this concept
Use this concept in a short BSV problem as your next step.
Check the result. The run has four phases.
startactivates the controller whilereadyis false andresultis 0.advancekeeps incrementingcycle; the FSM stays atawait.- The firing of
advancethat seescycle == 3schedulesready <= True. The write commits at the edge, soawaitsees it on a later cycle. - The FSM passes
await, writes8'h2ain 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.
- Change
cycle == 3tocycle == 6. The printedcyclegrows by three, andresultis still2a. The latency changed; the order did not. - Restore the comparison and remove
started &&from thefinishguard.doneis true while the controller is idle, sofinishfires in the first cycle and printscycle=0andresult=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;
endparpar
a <= 1;
b <= 2;
endparBoth 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?
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);
endruleThe 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
endseqInside 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 ruleA 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;
endseqThe 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
| Topic | Key point |
|---|---|
Stmt | a description of control flow; does nothing until given to mkFSM |
mkFSM | builds the controller state and rules; returns start, done, waitTillDone, abort |
done | true whenever idle, including before the first start |
seq | one step per action; the next step reads committed values |
action ... endaction | groups writes into one step; reads see the entry state |
| Loops | repeat fixes the work, while the target state; for uses register indices |
await | holds only this FSM; other rules keep running |
par | branches may overlap; writes to one register shadow, and read/write cycles conflict, as between rules |
9. Check your understanding
-
Why does declaring a
Stmtnot change any register?Show answer
A
Stmtis a value that describes control flow during elaboration. Only the controller built bymkFSM, once started, runs the actions. -
What lets the second action in a
seqread 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.
-
Which state may keep changing while an FSM is stopped at
await?Show answer
Every register written by other rules, such as
cyclein the example. Only this FSM's own progress is held. -
Why is
parnot 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.
-
Why must a finish rule distinguish the initial idle state from a completed run?
Show answer
doneis true in both states. Without a separatestartedrecord, 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.