Lyra Tutorial
From Lyra
Contents |
Introduction
This document provides a tutorial on the hardware modeling language Lyra and its accompanying simulator. The tutorial is intended to give users new to Lyra a quick start. For details of Lyra and its simulator, readers are recommended to refer to the User's Guide.
This tutorial goes through a working example to explain how to model a design with Lyra and the usage of Lyra simulator.
Example: Greatest Common Divisor
The greatest common divisor (gcd) is the largest positive integer that divides two non-zero integers without remainder. The gcd of a and b is written as gcd(a, b). For example, gcd(12, 18) = 6, gcd(1071, 1029) = 21.
In this working example, a gcd calculator modeled by Lyra is presented. The calculator takes as inputs two non-zero integers a and b, and produces gcd(a,b) as the output.
Algorithm
Euclidean algorithm is used to calculate the gcd. It performs repeated subtraction to find the gcd of two integers and the following pseudo-code shows the algorithm.
function gcd(a, b)
if a = 0 return b
while b ≠ 0
if a > b
a := a − b
else
b := b − a
return a
Modeling in Lyra
Basic concepts and terminologies of Lyra are briefly explained along the way of modeling the gcd calculator.
Module
A design is described in Lyra using the concept of a module. A module can be considered as two parts, the port declarations and the module body. The port declarations represent the external interface to the module. The module body can contain instances of sub-modules, registers, rendezvous, finite state machines. A special highest level module, named toplevel, is always required in modeling a design.
In this example, we can model the design with three modules: one module (named gcd) for calculating the gcd of two integers, one module (named test) working as a test bench for the gcd module , and a toplevel module which instantiates gcd and test as sub-modules and connects the two sub-modules with rendezvous.
Module and Port declarations
Each module starts with the following:
module module_name (declaration of ports)
A port declaration can be generally specified as:
port_type <data_type> port_name
- gcd module
module gcd (in <(uint, uint)> ops, out <uint> result)
{
//module body
}
- test module
module test (out <(uint, uint)> ops, in <uint> result)
{
/*
module body
*/
}
- toplevel module
module toplevel
{
//module body
}
The module_name and port_name can be any identifier. An identifier is a combination of alphanumeric characters, the first being a letter of the alphabet or an underline, and the remaining being any letter of the alphabet, any numeric digit, or the underline.
Two port types are shown in this example:
- in port receiving data
- out port transmitting data
Two data types are shown in this example:
- uint is an unsigned integer type.
- (uint, uint) is a tuple type formed by two uint data types.
More port types and data types supported by Lyra can be found in the User's Guide.
Note that toplevel module does not have any port declaration. Since it is the root of module hierarchy, it has no external connections.
The module body is enclosed by two curly parentheses "{" and "}". Lines beginning with "//" are considered as comments and do not have any effect on the behavior of the program. Another type of comment, known as block comment, is also supported and it discards everything between the "/*" characters and the first appearance of the "*/" characters.
Rendezvous
Rendezvous are used to connect the ports of sub-modules and the ports of FSMs in a module for different modes of communication. Lyra supports three types of rendezvous:
- rendv is a bi-party rendezvous. It implements one to one communication. rendv connects two sets of ports which represent the two different sides of the communication. The two sets of ports must have the same data type but the opposite port directions. The rule of occurrence of a rendv is that exactly one party from each set should participate.
- barrier is a multiparty rendezvous. It implements group synchronization. barrier can occur with or without data communication. With data communication, it connects one output port to multiple input ports and all ports have the same data type. Without data communication, it connects a set of sync ports. sync is a port type without data transfer and it is only used for synchronization. The occurrence of a barrier requires all connected parties to participate.
In this example, rendv type of rendezvous are used in the toplevel module to connect the ports of sub-modules. They are declared as the following:
rendv ops, result(1);
"ops" and "result" are rendezvous names and they can be any identifiers. Rendezvous can optionally carry a weight enclosed by "(" and ")". The weight indicates the firing priority of the rendezvous and it guides the scheduling of Lyra simulator. In the Lyra simulator, the higher the weight means the higher the priority to fire this rendezvous. In this example, both rendezvous have the same weight since the default weight is one.
Sub-modules
A sub-module declaration instantiates a module inside the parent module, and connects the ports of the sub-module to the rendezvous declared in the parent module, or the ports of the parent module. There are two ways to connect the ports of a sub-module, positional association and named association. The toplevel module in our example illustrates the sub-module declarations and their connections to rendezvous declared in the parent (toplevel) module.
module toplevel
{
rendv ops, result(1); //Rendezvous that connects the test and gcd modules.
/* positional association */
test test1(ops, result); //Instantiating the test module as test1.
/* named association */
gcd gcd1(.ops(ops), .result(result)); //Instantiating the gcd module as gcd1.
}
Internal State Variables
Internal state variables are registers. A register declaration is specified as the following:
reg data_type register_name ;
reg is the keyword for register declaration. Internal state variables can be initialized by the init declaration. In this example, two internal state variables for holding the two input integers are declared in the gcd module and the two registers are initialized with value zero.
module gcd (in <(uint, uint)> ops, out <uint> result)
{
reg uint op_a, op_b;
init {
op_a = 0;
op_b = 0;
}
}
Finite State Machines (FSMs)
FSMs represent processes. The declaration of an FSM contains the ports, temporary variables and states. FSMs are declared inside modules and there can be several FSMs in one module.
In this example, gcd module contains an FSM named gcd_fsm calculating the gcd value according to the above algorithm and test module contains an FSM named test_fsm generating inputs and monitoring outputs for gcd module. The graph representations of these processes are also given below to help readers visualize the FSMs.
| FSM in gcd module | |
module gcd(in <(uint, uint)> ops, out <uint> result)
{
reg uint op_a, op_b;
| |
| FSM in test module | |
module test(out<(uint, uint)> ops, in <uint> result)
{
|
There are no explicit port declarations in the above FSMs since they both connect to the ports of their owner modules.
val in test_fsm is the keyword of declaring temporary variables. A temporary variable does not have memory. Here, a temporary variable named gcd is used to monitor the calculation result from the gcd module.
An FSM can have multiple states. init is the keyword for the initial state and followed with a state name. There must be exactly one initial state in an FSM. The other states in an FSM are regular states and they start with the state keyword and followed with a state name.
Every state declaration contains a list of semantic statements. A goto statement creates a state transition edge in the FSM from the current state to the target of goto. A when statement specifies the condition of a state transition and the actions when the transition occurs. A assignment statement uses the "=" operator to assign values to state variables or temporary values. Assignment to state variables have delayed update, e.g. (op_a, op_b) = ops.read(). While assignments to temporary variables have instantaneous update, e.g. gcd = result.read().
The port_name.read() and port_name.write() are semantic operators for port access. In the above test_fsm, result.read() reads the data received from port result and ops.write(12, 18) writes the data to port ops for transmitting.
For details of semantic statements and semantic operators, please refer to the User's Guide.
Functions
Lyra uses functions to help structure code. The declaration of a function requires a function name, a return type, a list of arguments, and the function body. The function body may contain declaration of temporary variables, and a list of regular semantic statements. The control statements cannot be used in functions. A return statement must exist at the end of every function with a non-void return type.
In order to illustrate the usage of a function, another version of gcd module is given below. It uses a function named sub to perform the subtraction of two integers.
function uint sub (uint a, uint b)
{
val uint ret;
ret = a - b;
return ret;
}
module gcd(in <(uint, uint)> ops, out <uint> result)
{
reg uint op_a, op_b;
init {
op_a = 0;
op_b = 0;
}
fsm gcd_fsm
{
init start:
{
when (ops)
{
(op_a, op_b) = ops.read();
goto calc;
}
}
state calc:
{
when (op_a == 0)
{
goto write_b;
}
when (op_b == 0)
{
goto write_a;
}
when (op_a != 0 && op_b != 0)
{
when (op_a > op_b)
{
op_a = sub(op_a, op_b);//calling the sub function
goto calc;
}
when (op_b >= op_a)
{
op_b = sub(op_b, op_a);//calling the sub function
goto calc;
}
}
}
state write_b:
{
when (result)
{
result.write(op_b);
goto start;
}
}
state write_a:
{
when (result)
{
result.write(op_a);
goto start;
}
}
}
}
Lyra also allows users to define external functions to boost its capabilities. The external functions are implemented in C++ and the prototypes of these external functions are declared in Lyra. In this example, an external function _print_ is defined to display the gcd result. A file librfsm.cpp contains the C++ implementation of the external function and the core part of the code is shown as below.
void _print_(const vector<const Value *> *parms, Value *result)
{
print_object_list(std::cout, " ", parms);
std::cout << std::endl;
}
The external function is then compiled into a shared library which is loaded by the Lyra simulator. The following command compiles the above C++ file librfsm.cpp to the default Lyra library librfsm.so.
g++ -Wl,-soname,librfsm.so -o librfsm.so -O -fpic -shared librfsm.cpp -I <path to Lyra headers>
The prototype of the external function _print_ is declared in a file librfsm.rfm as following.
function void _print_(...);
The Lyra compiler uses the above prototype declaration to check the types of the arguments and the return value at compile time, and perform necessary type casting for those at simulation time. To provide the prototype to the Lyra compiler, the file librfsm.rfm is included in the file gcd.rfm which contains all three modules in this example.
Until now, the gcd calculator implementation is completed in Lyra. The complete code in file gcd.rfm is as the following.
#include "librfsm.rfm"
module test(out<(uint, uint)> ops, in <uint> result)
fsm test_fsm
{
val uint gcd;
init s1:
{
when (ops)
{
ops.write(12, 18); //sending two integers 12 and 18 to the gcd module
goto s2;
}
}
state s2:
{
when (result)
{
gcd = result.read(); //fetching the result of gcd(12, 18) from the gcd module
_print_("gcd(12, 18) = ", gcd);
goto s3;
}
}
state s3:
{
when (ops)
{
ops.write(1071, 1029);//sending two integers 1071 and 1029 to the gcd module
goto s4;
}
}
state s4:
{
when (result)
{
gcd = result.read(); //fetching the result of gcd(1071, 1029) from the gcd module
_print_("gcd(1071, 1029) = ", gcd);
goto s1;
}
}
}
}
module gcd(in <(uint, uint)> ops, out <uint> result)
{
reg uint op_a, op_b;
init {
op_a = 0;
op_b = 0;
}
fsm gcd_fsm
{
init start:
{
when (ops)
{
/* read the inputs: two integers from test module */
(op_a, op_b) = ops.read();
goto calc;
}
}
state calc:
{
when (op_a == 0)
{
goto write_b;
}
when (op_b == 0)
{
goto write_a;
}
when (op_a != 0 && op_b != 0)
{
when (op_a > op_b)
{
op_a = op_a - op_b;
goto calc;
}
when (op_b >= op_a)
{
op_b = op_b - op_a;
goto calc;
}
}
}
state write_b:
{
when (result)
{
result.write(op_b);
goto start;
}
}
state write_a:
{
when (result)
{
result.write(op_a);
goto start;
}
}
}
}
module toplevel
{
rendv ops, result(1); //Rendezvous that connects the test and gcd modules.
/* positional association */
test test1(ops, result); //Instantiating the test module as test1.
/* named association */
gcd gcd1(.ops(ops), .result(result)); //Instantiating the gcd module as gcd1.
}
Simulation
The Lyra simulator has a Tcl-based interactive interface. The simulator has the following command line syntax.
Usage: sim.tcl [-v] [-d num] [-L library path] [-l library name]
[-I include path] [file name]
-v Verbose mode
-d Set debug message level to num, default is 10
-L Set the path to shared library files
-l Specify the names of shared library files
-I Specify the path to included source files
To invoke the Lyra simulator, one can type as follows:
../build/bin/sim.tcl -L ../libso/ -I ../libso/ -l rfsm
Here, we suppose that the default shared library librfsm.so and its source files are located under directory libso/ and our working directory is rfsm/tutorial/ which contains the example file gcd.rfm. Users may need to change the above relative paths according to their directories. The library names will be prefixed by lib and suffixed by .so automatically. Since the library named librfsm.so is always loaded by default, "-l rfsm" in the above command can be actually omitted in this case.
The simulator supports a list of commands. For the complete list of commands, please refer to the User's Guide. The most often used commands are presented through the simulation of our example gcd calculator.
- load_design
It loads a new Lyra program into the simulator. In order to load our example, one can type
load_design gcd.rfm True
If the program is successfully loaded, then a "True" will be displayed in the interface; otherwise, a "False" will be given and it is generally accompanied with some error messages.
- step
It simulates the design for n steps and n equals one when omitted. If one types "step 40" after loading the design gcd.rfm, the following will be displayed in the interface.
step 40 gcd(12, 18) = 6 gcd(1071, 1029) = 21 40
The first two lines are due to the external function _print_ which prints out the gcd calculation results. The last line indicates the number of steps has been simulated.
- list_reg
It lists the names of registers in the design.
list_reg test1.test_fsm gcd1.op_a gcd1.op_b gcd1.gcd_fsm
gcd1.op_a and gcd1.op_b are registers defined by users. test1.test_fsm and gcd1.gcd_fsm are registers holding the current states in the two FSMs.
- list_mach
It lists the names of FSMs in the design.
list_mach test1.test_fsm gcd1.gcd_fsm
- list_rendv
It lists the names of rendezvous in the design.
list_rendv ops result
- show_reg
It shows the type information of the named register(s). All the register types are shown if the name is omitted.
show_reg reg string test1.test_fsm reg uint gcd1.op_a reg uint gcd1.op_b reg string gcd1.gcd_fsm
- show_mach
It shows the state diagram of the named FSM(s). All the FSM state diagrams are shown if the name is omitted. The following command shows the gcd_fsm state diagram.
show_mach gcd1.gcd_fsm
machine gcd1.gcd_fsm
(input<(uint, uint) > ops, output<uint > result)
{
states:
calc has 4 edges
calc -> write_b:
when ((gcd1.op_a==0))
calc -> write_a:
when ((gcd1.op_b==0))
calc -> calc:
when (((gcd1.op_a!=0)&&(gcd1.op_b!=0)), (gcd1.op_a>gcd1.op_b))
gcd1.op_a = sub(gcd1.op_a, gcd1.op_b);
calc -> calc:
when (((gcd1.op_a!=0)&&(gcd1.op_b!=0)), (gcd1.op_b>=gcd1.op_a))
gcd1.op_b = sub(gcd1.op_b, gcd1.op_a);
write_b has 1 edges
write_b -> start:
when (result)
result.write(gcd1.op_b);
write_a has 1 edges
write_a -> start:
when (result)
result.write(gcd1.op_a);
start has 1 edges
start -> calc:
when (ops)
#100 = ops.read();
gcd1.op_a = #100.$1;
gcd1.op_b = #100.$2;
}
- show_rendv
It shows the connectivity of the named rendezvous. All rendezvous connectivity are shown if the name is omitted.
show_rendv
rendv ops = {gcd1.gcd_fsm.ops : test1.test_fsm.ops}
rendv result = {test1.test_fsm.result : gcd1.gcd_fsm.result}
- print_reg
It prints the value of the named register(s). The following gives the register values right after the design is loaded.
print_reg test1.test_fsm string s1 gcd1.op_a uint 0 gcd1.op_b uint 0 gcd1.gcd_fsm string start
Then we run one step simulation and the changes of registers can be seen as follows:
test1.test_fsm string s2 gcd1.op_a uint 12 gcd1.op_b uint 18 gcd1.gcd_fsm string calc
- print_mach
It prints the current state of the named FSM(s).
print_mach test1.test_fsm @ s2 gcd1.gcd_fsm @ calc
- list_ready_rendv
It prints all rendezvous that may be ready to fire in the current step. The following shows the ready rendezvous right after loading the design.
list_ready_rendv ops
- list_fired_rendv
It prints all rendezvous fired in the last step. The following shows the fired rendezvous after running one step simulation.
list_fired_rendv ops



