SystemC to FPGA design flow

1 Synthesis flow outline

The synthesis route leads from the RTL description of the circuit in SystemC, via translation to Verilog performed by CoCentric, compilation of the Verilog source with FPGA CompilerII to EDIF and placement and routing with Webpack. In the first step, SystemC sources are translated to Verilog files. The translation is performed by invoking compile_systemc command of Synopsys CoCentric SystemC compiler. In the process of translation, the general structure of the modules is preserved. Each SystemC module (declared as SC_MODULE(module_name)) is translated into a separate Verilog module and placed in the module_name.v file. Each process within a module (declared as SC_METHOD(process)) is translated to an always block labeled with the process name. Also, the variables and ports present in the SystemC code are preserved under the same name in the Verilog source. In the second step, the Verilog files are compiled by Synopsys FPGA CompilerII into the optimized network of Spartan2 primitives: look up tables, multiplexers, memories, etc. This network can be exported in the EDIF format for interfacing with place and route tools and in Verilog (or VHDL) for functional simulation. Once the project is compiled, the user may invoke analysis tools available in FPGA CompilerII to analyze and debug the design. It is possible to view the hierarchical schematics of the design and access detailed timing information. Finally, Webpack suite is used to place and route the design exported from FPGACompilerII in EDIF format, generate the FPGA programming file and program the device.

Figure: Synthesis flow

2 Synthesis flow details

2.1 CoCentric

To translate SystemC sources to Verilog, Synopsys CoCentric SystemC compiler is used. This compiler supports only selected constructs of the standard language, a subset of SystemC Version 1.0. Therefore, before the compilation is performed, the SystemC sources need to be modified to comply with the requirements of CoCentric. These requirements are discussed in Section "Synthesizable SystemC" After the source have been modified, Synopsys Design Compiler (DC) shell must be invoked with the command:
dc_shell
The DC after a number of information messages about version and environment presents user with a command prompt. The SystemC sources can be compiled by entering at the prompt the command:
compile_systemc -rtl -rtl_format verilog -cpp_options "-DOPT1 -DOPT2" and.cpp
The options inform the compiler that the SystemC description is an RTL description of the design and that the desired output format is Verilog. Additionally, the C++ preprocessor, which is run on the file source.cpp before compilation will be called with options -DOPT1 -DOPT2. The compilation produces a number of information messages, as well as warnings and errors. Finally, a single number is printed corresponding to the exit status of compilation: ``1'' on success and ``0'' on error. A sequence of compile_systemc commands for a number SystemC files can be for efficiency put together in a DC script file, which can be then executed with the command:
dc_shell -f script
The figures below show an example of translation of a file and.cpp (with header and.h) containing two modules: AND2 and AND3 (2- and 3-input AND gate). As mentioned in Section "Synthesis flow outline", translation produces one verilog file containing one module for each SystemC module in the translated file. Note how the names of the modules and variables are preserved in the translation process. Also, the sensitivity list of the always block corresponds exactly to the sensitivity list of the SystemC process and the always block itself is labeled with the name of SystemC process (and_process).
Figure: and.h
#include <systemc.h>

SC_MODULE(AND2)
{
  sc_in< bool > a, b;
  sc_out< bool > r;

  void and_process();

  SC_CTOR(AND2) {
    SC_METHOD(and_process);
    sensitive << a << b;
  }
};
SC_MODULE(AND3)
{
  sc_in< bool > a, b, c;
  sc_out< bool > r;

  void and_process();

  SC_CTOR(AND3) {
    SC_METHOD(and_process);
    sensitive << a << b << c;
  }
};
Figure: and.cpp




#include "and.h"

void AND2::and_process()
{
  bool a_tmp, b_tmp, r_tmp;

  a_tmp = a.read();
  b_tmp = b.read();
  r_tmp = a_tmp && b_tmp;
  r.write( r_tmp );
}

void AND3::and_process()
{
  bool a_tmp, b_tmp, c_tmp, r_tmp;

  a_tmp = a.read();
  b_tmp = b.read();
  c_tmp = c.read();
  r_tmp = a_tmp && b_tmp && c_tmp;
  r.write( r_tmp );
}

Figure: AND2.v


module AND2( a, b, r );
  input a;
  wire a;
  input b;
  wire b;
  output r;
  reg r;
  reg a_tmp, b_tmp, r_tmp;

  always @( a or b )
  begin : and_process
    a_tmp = a;
    b_tmp = b;
    r_tmp = a && b;
    r = r_tmp;
  end
endmodule

Figure: AND3.v
module AND3( a, b, c, r );
  input a;
  wire a;
  input b;
  wire b;
  output r;
  reg r;
  reg a_tmp, b_tmp, c_tmp, r_tmp;

  always @( a or b or c )
  begin : and_process
    a_tmp = a;
    b_tmp = b;
    c_tmp = c;
    r_tmp = a && b && c;
    r = r_tmp;
  end
endmodule

For the remote access to CoCentric compiler, a script is prepared that will transfer the SystemC project files to the remote host, execute an appropriate DesignCompiler script to translate them to Verilog and transfer the Verilog files back.

2.2 FPGA CompilerII

The Verilog files produced by CoCentric form a project, which can be created and compiled by FPGA CompilerII. FPGA Compiler's graphical Design Wizard can be used for assisted project creation and compilation. In this scenario, the user is asked in the series of message boxes to select the source files, target architecture and determine basic optimization objectives (target clock rate, area/time optimization, etc). Then, the project is automatically created, compiled and the resulting netlist is exported. The details of the DesignWizard synthesis flow, as well as more advanced flows, both in graphical and command-line environments can be found in [3] in Chapters 2, 3 and 5.

2.3 Webpack

The final processing of the project is performed by the tools from Xilinx Webpack suite. The shell script (MapEdif.bat) is provided that calls appropriate tools. The script is called with a single argument - the project name, e.g. MapEdif.bat project. It assumes that the EDIF file project.edf is present in the current directory, as well as the UCF file project.ucf. The UCF file is needed to associate ports of the top-level design with pads of the FPGA device. It is an ASCII file familiar to the Webpack users (edited with "Edit UCF file" command of Webpack). It can be edited with any text editor. The port assignment constraints are specified by the lines in the following format:
NET "DATA<0>" LOC = "P99";
This constraint associates bit 0 of the input bus DATA to the pin P99 of the FPGA device. The content of MapEdif.bat script illustrates the steps of the mapping process. First, the EDIF network is translated into Webpack internal format with the command ngdbuild. Then, the network is mapped to the target Spartan architecture. In this case the mapping is trivial, since FPGA CompilerII already produced the network of Spartan primitives. Then, the network is placed and routed, and finally, the programming file for FPGA is produced. Additionally, a timing report is generated in file <project>.twr. Now, the ``Device Programming'' tool can be started to program the FPGA with the programming file. It is also possible to use ModelSim to simulate the design synthesized by FPGA CompilerII. For this purpose, FCII can export the synthesized network in Verilog format. The detailed description of the Webpack suite tools can be found in [2].

3 Synthesizable SystemC

3.1 Language

CoCentric SystemC compiler imposes certain constraints on the SystemC code, which are not present in the language. In particular, in the discussed synthesis flow, SystemC sources need to comply with the requirements set by CoCentric for RTL description. The detailed discussion of these requirements can be found in the document [1]. Most of the combinatorial modules can be made synthesizable simply by replacing the description of the functionality with SC_THREAD, which suggests behavioral description, with SC_METHOD, which corresponds with RTL description. The necessary steps are the following:
  1. Replace in the constructor of the module SC_THREAD(thread_name)
    with SC_METHOD(thread_name).
  2. Delete from the source of the thread (method, from now on) while(true) statement, leaving the loop's body unchanged.
  3. Delete from the source of the thread wait() statements.
Also, one of the most common requirements, not explicitly mentioned in the Synopsys document, is the exact equivalence of types and sizes of variables in assignment and comparison. Some examples follow:
  1. It is not possible to compare a variable of sc_bv<> type with integer. Statements of the type
    sc_bv<1> bv;
    if(bv == 1) ...
    
    need to be replaced with
    sc_bv<1> bv;
    sc_uint<1> ui = bv;
    if(ui == 1) ...
    
    or
    if(bv[0] == 1) ...
    
  2. Variables need to have the same bit-lengths for assignment operator. E.g.
    sc_bv<4> bv4;
    sc_bv<8> bv8;
    
    bv8 = bv4;
    
    has to be replaced with
    sc_bv<4> bv4, zero4b(0) /* 4-bit long vector of zeros */;
    sc_bv<8> bv8;
    
    bv8 = (zero4b, bv4); /* concatenate bv4 with zeros */
    
For examples of synthesizable description of sequential logic and other modules, see [1].

3.2 Project structure

In the regular SystemC flow, the structure of the top-level design is determined in function sc_main by declaration of appropriate modules and connecting them with signals. Since CoCentric does not support sc_main function, it is necessary to create an additional SystemC module, which will reflect the structure of the top-level design, i.e. instantiate the modules and create the connections. The creation of such hierarchical module is discussed in [1] on page 2-26. The fact that the top-level module replaces sc_main function means that the the main project file (containing sc_main) no longer needs to be translated. Instead, the new top-level module file is translated to Verilog with CoCentric. Naturally, it is still possible to simulate thus modified design. Instead of describing the structure of the entire design in sc_main, it is sufficient to instantiate only one module - the newly created top-level module. An example of the modified sc_main is shown below. The example assumes that the top-level module named TOPLEVEL was defined in toplevel.h.

Figure: sc_main with top-level module
#include "toplevel.h"

int sc_main(int argc, char *argv[]){
  // instantiate top-level design
  TOPLEVEL design("design");

  // connect inputs and outputs of the top-level design
  sc_signal< sc_uint<32> > in1, in2, out;
  sc_signal< bool > clk;

  design.in1(in1);
  design.in2(in2);
  design.out(out);
  design.clk(clk);

  // define traces
  sc_trace_file *tf;
  tf = sc_create_vcd_trace_file("design");

  // trace in/out
  sc_trace(tf, in1, "in1");
  sc_trace(tf, in2, "in2");
  sc_trace(tf, out, "out");
  sc_trace(tf, clk, "clk");

  // trace internal sc_signals of the design
  sc_trace(tf, design.bus1, "design.bus1");
  sc_trace(tf, design.bus2, "design.bus2");

  ...
}

3.3 Tips

Incorrect system behavior can be caused by unexpected appearence after synthesis of registers in the places, where strictly combinational logic was designed. The most common cause of such surprise is forgetting to specify a value for a variable in some conditions. The compiler assumes, that in these conditions the value of the variable remains unchanged (the same as the current value), and therefore a register is necessary to store the value. Consider the example of the switch statement:
      sc_uint<2> in = input.read();
      switch( in ){
        case 0 : output = 1; break;
        case 1 : output = 1; break;
        case 2 : output = 2; break;
      }
or the equivalent if statements:
      if( in==0 || in==1 ) output = 1;
      else if( in == 2 ) output = 2;
Since the value of the output for the input value 3 is not specified, the value will have to remain the same as it was previously. To remember that previous value, the output variable will have to be registered. To avoid this pitfall, it is necessary to always specify the value of a combinational variable for all input conditions, or supply the default value. The default value in the above example can be supplied by adding to the case statement the line
         default : output = 0; break;
It can also be supplied by setting the output to the default value before any other assignment, e.g.
      sc_uint<2> in = input.read();

      output = 0; /* default value */
      switch( in ){
        case 0 : output = 1; break;
        case 1 : output = 1; break;
        case 2 : output = 2; break;
      }
An unexpected registered variable can often be identified by an unexpected clock signal appearing in the timing report of FPGA CompilerII.

Bibliography

1 ``Describing Synthesizable RTL in SystemC''

2 Webpack documentation Chapter "Development System Reference Guide".

3 ``FPGA CompilerII User Guide''
Last updated: Thu, 6 May 2004 09:10:23 +0200