Skip to content

Latest commit

 

History

History
642 lines (508 loc) · 17.5 KB

File metadata and controls

642 lines (508 loc) · 17.5 KB

Previous | Index | Next

FSM statements

This is a comprehensive reference of all statements available in fsm functions.

Combinatorial vs Control statements

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 (i.e. statements that do not contain nested statements) can be lexically classed either as control statements or combinatorial statements.

  • Compound statements (i.e. statements containing other nested statements) can be either combinatorial or control statements.

    • If they only contain other combinatorial statements they will be combinatorial statements.

    • If they contain a mix of combinatorial and control statements, they will be control statements. In this case, the last nested statement must be a control statement.

FSM function bodies must end with a control statement.

Declaration statement (combinatorial)

A declaration statement can be used to introduce a new variable to be used within the surrounding lexical scope. It could be declared in the entity scope and used by all functions, or declared within an individual function. Declaration statements start with a type name, followed by the variable identifier, optionally followed by = and an initializer expression, and end in ;:

Fiddle with these declarations here.

  u8 a;          // Declare 8 bit unsigned integer variable 'a',
                 // but do not initialize it.
  i16 b = -'sd2; // 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.

Block statement (combinatorial or control)

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.

Expression statements (combinatorial)

Expressions can be used as statements when ending in ;. Since expression statements do not return a value, they should only be used for their side-effects (see examples below). The compiler will signal an error if a pure expression (i.e. one with no side-effects) is used in statement position.

Fiddle with these expressions here.

  p_in.read();        // Legal, as '.read()' has a side-effect of reading
                      // the input port, although the read value is discarded.

  p_out.write(1'b1);  // Legal, 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.

Assignment statements

An assignment statement updates the value of some storage location. All assignment statements are combinatorial statements.

Simple assignments (combinatorial)

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, e.g. foo = 2'd2;

  • Indexed identifier: foo[idx] e.g. foo[3] = 1'b1; or foo[a] = 3'd2;

  • Identifier with range (a slice):

    • foo[msb:lsb] e.g. foo[10+:7] = 4'd3;
    • foo[msb -: width] e.g. c[10 -: 4] = 4'd3;
    • foo[lsb +: width] e.g. c[7 +: 4] = 4'd10;
  • Structure member access, e.g. foo.bar = 4'd9;

  • Unpacking assignment (concatenation of lvalues): {foo, {bar[idx], baz.x}} - fiddle here:

    u10 a;
    u2[3] b;
    bool[3] c;
    {a, b[1], c[3]} = 13'h1abc; // 10 bits + 2 bits + 1 bit
    

Shorthand assignments (combinatorial)

All binary operators are available in the shorthand assignment form, including when the target is a concatenation or other compound lvalue (fiddle here):

  a >>= 2;
  b[2] -= a;
  {sign, abs} += 1;

Increment/Decrement statements (combinatorial)

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 (control)

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 (fiddle here):

  a = b + c;
  fence;
  d = a + e;
  fence;

Branching statements

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 (and in particular, end in) control statements. If one or more branches contains a control statement, but not all branches end in a control statement, the branch statement is invalid and yields a compile time error.

if statement (combinatorial or control)

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, and omitting the else clause results in an implicit fence, as follows (fiddle here):

  if (cond) {
    a = 2;
    b = 3;
    fence;
  }

is compiled as:

  if (cond) {
    a = 2;
    b = 3;
    fence;
  } else {
    fence;
  }

Some legal examples are (fiddle here):

  // 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;

  // Different branches can contain different numbers of control units
  if (a) {
    b = p_in_0.read();
    fence;
    b += p_in_0.read();
    fence;
  } else {
    c = p_in_0.read();
    fence;
  }

  // This if statement has an implicit else where nothing happens
  if (a) {
    b = 2;
  }

  // This if statement has an implicit else containing a single fence
  if (a) {
    b = p_in_0.read();
    fence;
    b += p_in_0.read();
    fence;
  }

Some invalid examples are (fiddle here):

  // Invalid because 'if' is control and 'else' is combinatorial
  if (a) {
    b = 2;
    fence;
  } else {
    c = 2;
  }

  // Invalid because control block must end in a control statement
  if (a) {
    b = p_in_0.read();
    fence;
    b += p_in_0.read();
  }

case statement (combinatorial or control)

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>

The selectors can be:

  • single selectors or comma-separated lists
  • values, constant expressions or variable-expressions (unlike the C switch)
  • default

The selectors are considered in a top-to-bottom order, and if the condition expression is equal to the selector, the statement is executed and no further selectors are checked. This means overlapping selectors are legal. For example (fiddle here):

  // Assume foo is u3
  case (foo) {
    3'd0, 3'd1, 3'd2: a = 0;
    bar + 3'd1: a = 1;
    default: a = 2; // could be placed anywhere in the list
  }

Case clauses can contain arbitrarily complex statements using a {} block:

  case (foo) {
    bar: {
      // code to execute if foo == bar
    }
    baz: {
      // code to execute if foo == baz
    }
    default: {
      // code to execute otherwise
    }
  }

The case statement can either have an explicit or implicit default case to catch remaining cases. If used explicitly, it can be placed anywhere in the selector list and is always evaluated last (this is the same as verilog). If used implicitly, it can be omitted and the compiler will insert a default empty combinatorial block or fence; control block, as appropriate. For example (fiddle here):

  case (foo) {
    bar: {
      a++;
      fence;
    }
    baz: {
      b++;
      fence;
    }
  }

is compiled as:

  case (foo) {
    bar: {
      a++;
      fence;
    }
    baz: {
      b++;
      fence;
    }
    default: fence;
  }

Function calls

Functions are used to encapsulate repetitive portions of FSM behaviour. All statements relating to function call handling are control statements.

The call statement (control)

To end the current control unit, and transfer control to a function on the next clock cycle, simply call it in statement position (fiddle here):

  void foo() {
    ...
    bar();  // Transfer control to function 'bar'
    ...
    fence;
  }

  void bar() {
    ...
  }

return statement (control)

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 (fiddle here).

  void foo() {
    bar();  // Call 'bar',  when it returns, loop back to the top of 'foo'.
  }

  void bar() {
    return; // Return to call site
  }

goto statement (control)

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 (fiddle here):

  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;
  }

Looping statements

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 loop statement (control) - (fiddle here)

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; // an implicit 'else fence;' is inserted by the compiler
}

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 loops

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. Note in particular that an implicit fence is always inserted at the end of the loop body and so does not need to be written.

do loop (control) - (fiddle here)

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;
    }
  }

while loop (control) - (fiddle here)

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 loop (control) - (fiddle here)

For loops follow the common syntax:

  for (<init> ; <cond> ; <step>) {
    <body>
  }

where <init> can be a list of zero or more instances of either assignment statements or simple variable declarations with initializers separated by comma, <cond> is an optional expression, <step> is a list of zero or more comma separated assignment statements, 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;
        }
      }
    }
  }

break statement (control)

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.

continue statement (control)

The continue statement can be used similarly to the C language equivalent to continue at the end of the loop body. This means that inside a do or while loop, the continue statement performs the condition check, and on the next clock cycle transfers control either to the beginning of the loop body, or the statement after the loop. Inside a for loop, the continue statement also executes the <step> statements before performing the condition check. Inside a loop loop, continue unconditionally transfers control to the beginning of the loop body on the next cycle.

let headers (control)

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 (<init>) <stmt>

The form of <init> is the same as in the case of the for loop. The <stmt> following the let header must be a loop, do,while or for statement. The let statement is syntactic sugar for:

  {
    <init>
    <body>
  }

The canonical use case is to aid with do loops to construct the equivalent of a rear-testing for loop (fiddle here):

  // 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);
  }

Previous | Index | Next