Most of the time we do not give much thought about the steps that our EDA tools have to go through to run a simple testbench simulation. Starting from a couple HDL files we end up somehow with a simulation executable. This blogpo will try to explain how the compilation and the elaboration steps are generally related to tools such as VCS, DSIM and Vivado. Much of the examples provided here are referring to verilog/systemverilog tools but much of the same principles can be extended to VHDL tools.
The basic concepts:
Starting from the preprocessing step, it is where each source file resolves its macros, defines and include declarations into its intended text replacements. This step generally means generating intermediate temporal source files which funnel into the compilation step.
The compilation is the step in which a program receives a description written in a language that is converted into a particular output. In software terms, a C compiler converts C language into assembly machine code. In our case, the compilation step takes the HDL code and converts it into a binary representation; this representation is later used in the elaboration step.
The elaboration step usually takes the compiled top module in hierarchy and then resolves from top to bottom each module instance; if any submodule has any parameter or configuration provided, it is here where it is resolved. The output of the elaboration step concludes with a simulation executable.
Preprocessing
On this step the following directives are resolve:
1. Include directive: this directive is expanded and replaced in this step by the exact content of the target file. There is no analysis or inspection done here, it is an exact copy of the file.
// The following line would expand on this step by inserting the content of the file to this file. // Precaution what files you include by this directive it may cause // compiling issues when the same definitions are processed twice if the same file is included from multiple sources. `include “testbench_wrapper.sv”
2. Define directive: this directive sets a variable which is usually written in all caps. The definition could be anything from number variables to written text. It doesn’t hold any typing, the preprocessor only looks at the places where this define is being cast and replaced it with the define value.
// The following define exist but it is replaced with “” if it is used in code `define SYNTHESIS `ifndef SYNTHESIS // The following procedure is either erased or kept according the existence of the definition of SYNTHESIS // NUM_CORES would be replaced with 4 instead in the preprocessing step `define NUM_CORES 4 core_cfg core_cfgs [NUM_CORES]; // 4 ← NUM_CORES `endif
3. Macro directive: it functions as a define directive but is more powerful. The typical structure starts with a template where you set variables that are defined every time you call said macro. The method to use it is similar to how a function is being called. The variables of the macro are only resolved on this step.
`define REG_BLOCK_PATH (REG_NAME) tb.sub_path.regblock.REG_NAME Logic [31:0] csr_val; // the following macro expands to tb.sub_path.regblock.uart_status Assign csr_val = `REG_BLOCK_PATH(uart_status);
Take into account that define and macro directives require precedent definitions before being used. Generally it is a good idea to have most of these definitions being concentrated in a single or several files which are read by the preprocessor first before going through the other files that use them.
Typically we don’t call the preprocessor directly when we do the compilation because this step is usually already baked-in the compilation step.
Compilation
Possibly the most important step. This step parses and processes the file previously treated by the preprocessor and converts the unit blocks described by the HDL definitions to a binary representation. There is not a standard binary representation across vendors and it depends on the tool version too. These structures are compatible within the same tool-generated version. This stage is where dependency issues appear and is important to plan how to compile each file of our project.
There are two strategies to follow according to the needs of the overall project:
- Smaller size projects tend to follow the strategy 1 which consists of passing all the HDL files of the project in a single batch and then generating a single binary object or directory where most of the results live on. This tends to be rather easy to implement, but usually the compilation time tends to increase rapidly as the number of HDL files pile up so every recompilation requires compiling every single file again.
- That’s where the second strategy gains importance in the project day to day usage. It saves time as most of the project is already compiled when small changes are done.
Regarding the second strategy, some tools have support for automatic incremental compilations, but in case that manual control is needed or is the only option the project has to be compiled using third party tools to manage the compilation project or in-house make scripts.
Elaboration
This step is pretty straightforward. It consists of targeting a single top unit, most likely a top testbench or more, at starting solving from top to bottom the configuration overrides. The elaboration also attaches the verification code (also compiled in binary objects) to the simulation executable.
Tools examples
The following examples show how the compilation steps are done in diverse vendor tools.
Synopsys
## vlog makes the preprocessing and compilation steps for verilog/systemverilog files vlogan -sv <path_to_file>/design.sv <path_to_file>/tb.sv ## vcs makes the elaboration steps. It takes the binary structures created from the compilation step previously and build it on top of it the simulation executable vcs -s <name_of_elaboration> top_tb -timescale 1ns/1ps ## run simulation work_dir/simv
Vivado
## load vivado program binaries path to the shell environment source <vivado_installation_path>vivado_settings.sh ## xvlog makes the preprocessing and compilation steps for verilog/systemverilog files xvlog -sv <path_to_file>/design.sv <path_to_file>/tb.sv ## xelab makes the elaboration steps. It takes the binary structures created from the compilation step previously and build it on top of it the simulation executable xelab -s <name_of_elaboration> top_tb -timescale 1ns/1ps ## xsim call the executable elaborated from the previous step xsim <name_of_elaboration> -runall
DSIM
## compiles a library based of the systemverilog files dvlcom -lib 'lib_name' -sv <path_to_file>/design.sv <path_to_file>/tb.sv dsim -top lib_name.top_tb -L lib_name
Código de ejemplo
// Simple CPU design in Verilog module simple_cpu ( input clk, input reset, output [7:0] acc, // Accumulator register output output [7:0] pc // Program counter output ); // Define instruction set parameter NOP = 4'b0000; // No Operation parameter LDA = 4'b0001; // Load Accumulator parameter ADD = 4'b0010; // Add to Accumulator parameter SUB = 4'b0011; // Subtract from Accumulator parameter STA = 4'b0100; // Store Accumulator parameter JMP = 4'b0101; // Jump to Address parameter HLT = 4'b1111; // Halt CPU // CPU Registers reg [7:0] accumulator; // Accumulator register reg [7:0] program_counter; // Program Counter reg [7:0] memory [0:255]; // Memory (256 x 8 bits) reg [7:0] instruction; // Current instruction reg [3:0] opcode; // Operation code reg [7:0] operand; // Operand (data or address) // Assign outputs assign acc = accumulator; assign pc = program_counter; // CPU Reset always @(posedge clk or posedge reset) begin if (reset) begin program_counter <= 8'b0; accumulator <= 8'b0; end else begin // Fetch instruction instruction <= memory[program_counter]; opcode <= instruction[7:4]; operand <= instruction[3:0]; // Increment program counter program_counter <= program_counter + 1; // Decode and execute instruction case (opcode) NOP: ; // Do nothing LDA: accumulator <= memory[operand]; // Load value from memory ADD: accumulator <= accumulator + memory[operand]; // Add to accumulator SUB: accumulator <= accumulator - memory[operand]; // Subtract from accumulator STA: memory[operand] <= accumulator; // Store accumulator to memory JMP: program_counter <= operand; // Jump to address HLT: program_counter <= program_counter; // Halt CPU default: ; // Undefined operation endcase end end endmodule function App() { return ( <div className="App"> <header className= "App-header"> <img src={logo} className="App-logo" alt="logo"/> <p> Edit <code>src/App.tsx</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn reactjs </a> </header> </div> ); } //ejemplo de posible sintaxis en verilog // Comentario de una sola línea `define DATA_WIDTH 8 // Macro definida module ExampleModule ( input wire clk, // Señal de reloj input wire reset, // Señal de reinicio input wire [`DATA_WIDTH-1:0] in_data, // Entrada de datos output reg [`DATA_WIDTH-1:0] out_data // Salida de datos ); // Declaración de registros y cables reg [`DATA_WIDTH-1:0] accumulator; // Acumulador wire enable; // Asignación continua assign enable = (in_data > 0) ? 1'b1 : 1'b0; // Bloque inicial initial begin out_data = 0; accumulator = 0; end // Bloque siempre always @(posedge clk or negedge reset) begin if (!reset) begin // Reiniciar valores accumulator <= 0; out_data <= 0; end else if (enable) begin // Acumular y actualizar salida accumulator <= accumulator + in_data; out_data <= accumulator; end end endmodule // Otro módulo con instanciación module TopModule; // Señales internas reg clk; reg reset; reg [`DATA_WIDTH-1:0] in_data; wire [`DATA_WIDTH-1:0] out_data; // Instancia del módulo ExampleModule uut ( .clk(clk), .reset(reset), .in_data(in_data), .out_data(out_data) ); // Generador de reloj initial begin clk = 0; forever #5 clk = ~clk; // Invertir cada 5 unidades de tiempo end // Proceso de prueba initial begin reset = 0; in_data = 0; // Reinicio del sistema #10 reset = 1; // Probar datos de entrada #10 in_data = 8'b00000001; // Valor binario #10 in_data = 8'hFF; // Valor hexadecimal #10 in_data = 8'd128; // Valor decimal #10 in_data = 8'o77; // Valor octal // Finalizar simulación #50 $finish; end endmodule