This is a comprehensive reference of all statements available in fsm
functions.
Statements can broadly be categorized as either combinatorial statements, or control statements. This distinction is used to determine control unit boundaries for the purposes of determining which statements execute together in a single clock cycle. Let it suffice for now that a linear sequence of combinatorial statements, followed by a single control statement is executed in a single clock cycle. See the section on control flow conversion for details.
Simple statements that do not contain nested statements can be lexically classed either as control statements or combinatorial statements. Compound statements that do contain other statements can be either combinatorial statements, if they only contain other combinatorial statements, or control statements if they contain a mix of combinatorial and control statements. If a compound statement contains a mix of combinatorial and control statements, then the last nested statement must be a control statement.
FSM function bodies must end with a control statement.
The simples statements, which do not influence control flow are as follows.
A declaration statement can be used to introduce a new variable to be used
within the surrounding lexical scope. Declaration statements start with a type
name, followed by the variable identifier, optionally followed by =
and and
initializer expression, and end in ;
:
u8 a; // Declare 8 bit unsigned integer variable 'a',
// but do not initialize it.
i16 b = -2; // Declare 16 bit signed integer variable 'b',
// and initialize it to -2;
foo_t bar; // Declare variable 'bar' of type 'foo_t', where 'foo_t' is either
// a typedef or the name of a struct
Note that while this looks like a local variable, the storage is in fact statically allocated. This means that care must be taken when using local variables in recursive functions. The C language equivalent of the declaration of variable b above is:
static int16_t b;
b = -2;
Array variables cannot be declared using declarations statements, they must be declared in the design entity scope.
Declaration statements are always combinatorial statements.
A {}
block can be used to introduce a new lexical scope at any time. This can
be useful to scope local variable declarations, of group statements together for
clarity.
a = 2;
{
u8 tmp = a ^ c;
b = tmp + tmp << 2;
}
c = b;
A block statement is either a combinatorial statement or a control statement, depending on its contents.
Any expression can be used as a statement when followed by a ;
. Expressions in
statement position can only achieve anything through heir side-effect. The
compiler will signal an error if a pure expression is used in statement
position.
p_in.read(); // Valid, as '.read()' has a side-effect of reading
// the input port. The read value is discarded.
p_out.write(1'b1); // Valid, writing to a port is a side-effect
a + b; // Compiler error: This is a pure expression with no
// side-effects.
Expression statements are always combinatorial statements. Note that function calls in statement positions are not expression statements and are described below.
An assignment statement updates the value of some storage location. All assignment statements are combinatorial statements.
The simplest assignment statements have the usual form, using the =
sign to
delimit the target of the assignment (lvalue), and the expression that yields
the value to be assigned. Similarly to the Verilog language, the left hand side
of an assignment can be either one of:
- Simple identifier:
foo
- Indexed identifier:
foo[idx]
- Identifier with range (a slice):
foo[msb:lsb]
(foo[msb -: width]
andfoo[lsb +: width]
are also supported) - Structure member access:
foo.bar
- Concatenation formed from other valid lvalues (also known as unpacking
assignment):
{foo, {bar[idx], baz.x}}
Here are some examples:
a = 2'd2;
b[a] = 3'd2;
c[a+:4] = 8'd42;
d.bar = 4'd9;
{a, b[1], c[3]} = 13'h1abc;
All binary operators are available in the shorthand assignment form, including when the target is a concatenation or other compound lvalue:
a >>= 2;
b[2] -= a;
{sign, abs} += 1;
As a further shorthand, increment or decrement by 1 can be expressed using the
++
and --
notation. Note however that these operations are not expressions,
but proper statements (they do not yield a value), and as such can only be used
when standing alone in statement position. All lvalues are valid.
a++;
{sign, abs}--;
The fence
statement is the simplest control statement, and is used to indicate
the end of a control unit. All combinatorial statements before a fence
statement will belong to the current control unit (which also includes the
fence
statement itself), and will execute in the current clock cycle. On the
next clock cycle, control is transferred to the statements following the fence
statement. The following example takes 2 cycles to execute:
a = b + c;
fence;
d = a + e;
fence;
Control flow branches can be achieved with the if
and case
statements. These
branching statements are combinatorial statements if all branches contain only
combinatorial statements, and they are control statements if all branches
contain control statements. Mixing combinatorial and control branches in the
same statement is invalid and yields a compile time error. That is to say that
either all branches must be combinatorial statement or all branches must be
control statements. In the case of a control if
and control case
statements,
all branches must end in a control statement (this is the same as with control
{}
blocks).
The common if
statement can be used to perform a 2-way branch:
if (condition) <then-statement> else <else-statement>
The else clause is optional. If a control if
statement does not contain an
else clause, then the compiler automatically inserts a single fence
statement.
Which is to say that:
if (cond) {
a = 2;
b = 3;
fence;
}
is compiled as:
if (cond) {
a = 2;
b = 3;
fence;
} else {
fence;
}
Some legal examples are:
// Combinatorial if statement:
if (a) {
b = 2;
} else {
c = 3;
}
// Control if statement:
if (a) {
b = p_in_0.read();
fence;
} else {
c = p_in_0.read();
fence;
}
// The above is the same as:
if (a) {
b = p_in_0.read();
} else {
c = p_in_0.read();
}
fence;
// But the following does not have a simple equivalent:
if (a) {
b = p_in_0.read();
fence;
b += p_in_0.read();
fence;
} else {
c = p_in_0.read();
fence;
}
fence;
// Combinatorial if without else clause:
if (a) {
b = 2;
}
// Control if without else clause:
if (a) {
b = p_in_0.read();
fence;
b += p_in_0.read();
fence;
}
Some invalid examples are:
// Invalid due to mismatched combinatorial/control branches
if (a) {
b = 2;
fence;
} else {
c = 2;
}
// Invalid: Control block must end in a control statement
if (a) {
b = p_in_0.read();
fence;
b += p_in_0.read();
}
Multi-way branches can be constructed using the case
statement. This multi-way
branch is more similar to the analogous Verilog case
statement, and less
similar to the C switch
statement. The general syntax is:
case (<cond>) {
<case-clauses>
}
Where each case clause is of the form:
<selector> : <statement>
There can be a single default
selector. Other selectors are comma separated
lists of expressions, which are evaluated in a top to bottom order, and the
statements of the first clause with a selector equal to the condition expression
are executed. As opposed to the C switch
statement, the selectors do not need
to be constant expressions:
// Assume foo is an u3
case (foo) {
3'd0, 3'd1, 3'd2: a = 0;
bar + 3'd1: a = 1;
default: a = 2;
}
Case clauses can contain arbitrarily complex statements using a block:
case (foo) {
bar: {
// If foo == bar
}
baz: {
// If foo == baz
}
default: {
// Otherwise
}
}
Similarly to the control if
statement without an else
clause, an implicit
fence
statement is inserted by the compiler if the default
clause is omitted
from a control case
statement:
case (foo) {
bar: {
a++;
fence;
}
baz: {
b++;
fence;
}
}
is compiled as:
case (foo) {
bar: {
a++;
fence;
}
baz: {
b++;
fence;
}
default: fence;
}
Functions are used to encapsulate repetitive portions of FSM behaviour. All statements relating to function call handling are control statements.
To end the current control unit, and transfer control to a function on the next clock cycle, simply call it in statement position:
void foo() {
...
bar(); // Transfer control to function 'bar'
...
fence;
}
void bar() {
...
}
The return
statement can be used to end the control unit and transfer control
back to the call site for the next clock cycle. As mentioned in the description
of FSMs, functions do not return automatically when they reach the
end of the function body. Without a return
statement, control is transferred
back to the top of the function.
void foo() {
bar(); // Call 'bar', when it returns, loop back to the top of 'foo'.
}
void bar() {
return; // Return to call site
}
The goto
statement can be used to perform a tail call to a function. This
statement ends the current control unit, transfers control to the target
function, but does not push a return stack entry, and hence the callee will
return to the site of the preceding function call. One use of goto
is to
eliminate wasted cycles where there is no work to be done other than returning
to an outer function:
void a() {
b();
}
void b() {
c();
return;
}
void c() {
return;
}
The body of function b in the example above takes 2 cycles to execute, one
cycle to perform the call, and one cycle to perform the return. This can be
reduced to a single cycle by using goto
, causing c to return directly to the
call site of b inside a:
void b() {
goto c;
}
All statements in this section are control statements. The bodies of all loops
must be {}
blocks, even if they contain only a single statement.
The fundamental looping construct is the infinite loop, introduced with the
loop
keyword. The body of a loop
must end in a control statement. To exit
the infinite loop, use the break
, return
, or goto
statements:
u8 acc = 0;
loop {
acc ^= p_in.read();
if (acc == 0)
break;
}
The loop
keyword ends the current control unit and introduces the loop body,
so the above code would take 1 clock cycle to perform the initialization of
acc and enter the loop, and from then on the loop body would execute once
every cycle (assuming no flow control stalls on p_in), until acc becomes 0.
Structured do
, while
, and for
loops are syntactic sugar and are rewritten
by the compiler in terms of the primitive loop
statement. When determining the
cycle behaviour of these structured loops, consider their rewriting. Given that
the rewritings introduce control statements after the loop body, structured
loops need not have a control statement at the end of their body.
The common rear testing do
loop is written as:
do {
<body>
} while (<cond>);
where <body> is a list of statements, and <cond> is an expression. This is rewritten by the compiler to:
loop {
<body>
if (<cond>) {
fence;
} else {
break;
}
}
The syntax of the front testing while
loop is as follows:
while (<cond>) {
<body>
}
where <cond> is an expression, and <body> is a list of statements. This is rewritten by the compiler to:
if (<cond>) {
loop {
<body>
if (<cond>) {
fence;
} else {
break;
}
}
}
For loops follow the common syntax:
for (<init> ; <cond> ; <step>) {
<body>
}
where <init> is either a single assignment statement or a single variable
declaration with an initializer expression, <cond> is an expression,
<step> is an assignment statement, and <body> is a list of statements. The
rewriting of a for
loop in terms of loop
is:
{
<init>;
if (<cond>) {
loop {
<body>
<step>;
if (<cond>) {
fence;
} else {
break;
}
}
}
}
The break
statement can be used to immediately terminate the innermost active
loop and transfer control to the statement following the loop on the next clock
cycle.
The let
keyword can be used to introduce a list of variable declarations
together with initializers (separated by ,
) to a new scope established by a
following loop statement:
let (declarations-with-initializers>) <stmt>
The <stmt>
following the let
header must be a loop
, do
,while
or for
statement. The let
statement is syntactic sugar for:
{
<declarations-with-initializers>
<body>
}
The canonical use case is to aid with do
loops to construct the equivalent of
a rear testing for
loop:
// Loop 8 times using a 3 bit loop variable
let (u3 i = 0) do {
foo[i] = 0;
i++;
} while (i);
This is equivalent to:
{
u3 i = 0;
do {
foo[i] = 0;
i++;
} while (i);
}