Simulation Using Icarus Verilog¶
Simulation is the process of creating models that mimic the behavior of the device you are designing (simulation models) and creating models to exercise the device (test benches). The simulation model need not reflect any understanding of the underlying technology, and the simulator need not know that the design is intended for any specific technology.
The Verilog simulator, in fact, is usually a different program than the synthesizer. It may even come from a different vendor. The simulator need not know of or generate netlists for the target technology, so it is possible to write one simulator that can be used to model designs intended for a wide variety of technologies. A synthesizer, on the other hand, does need to know a great deal about the target technology in order to generate efficient netlists. Synthesizers are often technology specific and come from vendors with specialized knowledge, whereas simulators are more general purpose.
Simulation models and test benches, therefore, can use the full range of Verilog features to model the intended design as clearly as possible. This is the time to test the algorithms of the design using language that is relatively easy for humans to read. The simulator, along with the test bench, can test that the clearly written model really does behave as intended, and that the intended behavior really does meet expectations.
The test benches model the world outside the design, so they are rarely destined for real hardware. They are written in Verilog simply as a matter of convenience, and sometimes they are not written in Verilog at all. The test benches are not throw-away code either, as they are used to retest the device under test as it is transformed from a simulation model to a synthesizeable description.
Compilation and Elaboration¶
Simulation of a design amounts to compiling and executing a program. The Verilog source that represents the simulation model and the test bench is compiled into an executable form and executed by a simulation engine. Internally, Icarus Verilog divides the compilation of program source to an executable form into several steps, and basic understanding of these steps helps understand the nature of failures and errors. The first step is macro preprocessing, then compilation, elaboration, optional optimizations and finally code generation. The boundary between these steps is often blurred, but this progression remains a useful model of the compilation process.
The macro preprocessing step performs textual substitutions of macros defined with “`define” statements, textual inclusion with “`include” statements, and conditional compilation by “`ifdef” and “`ifndef” statements. The macropreprocessor for Icarus Verilog is internally a separate program that can be accessed independently by using the “-E” flag to the “iverilog” command, like so:
% iverilog -E -o out.v example.v
This command causes the input Verilog file “example.v” to be preprocessed, and the output, a Verilog file without preprocessor statements, written into “out.v”. The “`include” and “`ifdef” directives in the input file are interpreted, and defined macros substituted, so that the output, a single file, is the same Verilog but with the preprocessor directives gone. All the explicitly specified source files are also combined by the preprocessor, so that the preprocessed result is a single Verilog stream.
Normally, however, the “-E” flag is not used and the preprocessed Verilog is instead sent directly to the next step, the compiler. The compiler core takes as input preprocessed Verilog and generates an internal parsed form. The parsed form is an internal representation of the Verilog source, in a format convenient for further processing, and is not accessible to the user.
The next step, elaboration, takes the parsed form, chooses the root modules, and instantiates (makes instances of) those roots. The root instances may contain instances of other modules, which may in turn contain instances of yet other modules. The elaboration process creates a hierarchy of module instances that ends with primitive gates and statements.
Note that there is a difference between a module and a module instance. A module is a type. It is a description of the contents of module instances that have its type. When a module is instantiated within another module, the module name identifies the type of the instance, and the instance name identifies the specific instance of the module. There can be many instances of any given module.
Root modules are a special case, in that the programmer does not give them instance names. Instead, the instance names of root modules are the same as the name of the module. This is valid because, due to the nature of the Verilog syntax, a module can be a root module only once, so the module name itself is a safe instance name.
Elaboration creates a hierarchy of scopes. Each module instance creates a new scope within its parent module, with each root module starting a hierarchy. Every module instance in the elaborated program has a unique scope path, a hierarchical name, that starts with its root scope and ends with its own instance name. Every named object, including variables, parameters, nets and gates, also has a hierarchical name that starts with a root scope and ends with its own base name. The compiler uses hierarchical names in error messages generated during or after elaboration, so that erroneous items can be completely identified. These hierarchical names are also used by waveform viewers that display waveform output from simulations.
The elaboration process creates from the parsed form the scope hierarchy including the primitive objects within each scope. The elaborated design then is optimized to reduce it to a more optimal, but equivalent design. The optimization step takes the fully elaborated design and transforms it to an equivalent design that is smaller or more efficient. These optimizations are, for example, forms of constant propagation and dead code elimination. Useless logic is eliminated, and constant expressions are pre-calculated. The resulting design behaves as if the optimizations were not performed, but is smaller and more efficient. The elimination (and spontaneous creation) of gates and statements only affects the programmer when writing VPI modules, which through the API have limited access to the structures of the design.
Finally, the optimized design, which is still in an internal form not accessible to users, is passed to a code generator that writes the design into an executable form. For simulation, the code generator is selected to generate the vvp format–a text format that can be executed by the simulation engine. Other code generators may be selected by the Icarus Verilog user, even third party code generators, but the vvp code generator is the default for simulation purposes.
Making and Using Libraries¶
Although simple programs may be written into a single source file, this gets inconvenient as the designs get larger. Also, writing the entire program into a single file makes it difficult for different programs to share common code. It therefore makes sense to divide large programs into several source files, and to put generally useful source code files somewhere accessible to multiple designs.
Once the program is divided into many files, the compiler needs to be told how to find the files of the program. The simplest way to do that is to list the source files on the command line or in a command file. This is for example the best way to divide up and integrate test bench code with the simulation model of the device under test.
The Macro Preprocessor¶
Another technique is to use the macro preprocessor to include library files into a main file. The include directive takes the name of a source file to include. The preprocessor inserts the entire contents of the included file in place of the include directive. The preprocessor normally looks in the current working directory (the current working directory of the running compiler, and not the directory where the source file is located) for the included file, but the “-I” switch to “iverilog” can add directories to the search locations list.
% iverilog -I/directory/to/search example.v
It is common to create include directories shared by a set of programs. The preprocessor include directive can be used by the individual programs to include the source files that it needs.
The preprocessor method of placing source code into libraries is general (arbitrary source code can be placed in the included files) but is static, in the sense that the programmer must explicitly include the desired library files. The automatic module library is a bit more constrained, but is automatic.
Automatic Module Libraries¶
A common use for libraries is to store module definitions that may be of use to a variety of programs. If modules are divided into a single module per file, and the files are named appropriately, and the compiler is told where to look, then the compiler can automatically locate library files when it finds that a module definition is missing.
For this to work properly, the library files must be Verilog source, they should contain a single module definition, and the files must be named after the module they contain. For example, if the module “AND2” is a module in the library, then it belongs in a file called “AND2.v” and that file contains only the “AND2” module. A library, then, is a directory that contains properly named and formatted source files.
% iverilog -y/library/to/search example.v
The “-y” flag to “iverilog” tells the compiler to look in the specified directory for library modules whenever the program instantiates a module that is not otherwise defined. The programmer may include several “-y” flags on the command line (or in a command file) and the compiler will search each directory in order until an appropriate library file is found to resolve the module.
Once a module is defined, either in the program or by reading a library module, the loaded definition is used from then on within the program. If the module is defined within a program file or within an included file, then the included definition is used instead of any library definition. If a module is defined in multiple libraries, then the first definition that the compiler finds is used, and later definitions are never read.
Icarus Verilog accesses automatic libraries during elaboration, after it has already preprocessed and parsed the non-library source files. Modules in libraries are not candidates for root modules, and are not even parsed unless they are instantiated in other source files. However, a library module may reference other library modules, and reading in a library module causes it to be parsed and elaborated, and further library references resolved, just like a non-library module. The library lookup and resolution process iterates until all referenced modules are resolved, or known to be missing from the libraries.
The automatic module library technique is useful for including vendor or technology libraries into a program. Many EDA vendors offer module libraries that are formatted appropriately; and with this technique, Icarus Verilog can use them for simulation.
Advanced Command Files¶
Command files were mentioned in the “Getting Started” chapter, but only briefly. In practice, Verilog programs quickly grow far beyond the usefulness of simple command line options, and even the macro preprocessor lacks the flexibility to combine source and library modules according to the advancing development process.
The main contents of a command file is a list of Verilog source files. You can name in a command file all the source files that make up your design. This is a convenient way to collect together all the files that make up your design. Compiling the design can then be reduced to a simple command line like the following:
% iverilog -c example.cf
The command file describes a configuration. That is, it lists the specific files that make up your design. It is reasonable, during the course of development, to have a set of different but similar variations of your design. These variations may have different source files but also many common source files. A command file can be written for each variation, and each command file lists the source file names used by each variation.
A configuration may also specify the use of libraries. For example, different configurations may be implementations for different technologies so may use different parts libraries. To make this work, command files may include “-y” statements. These work in command files exactly how they work on “iverilog” command line. Each “-y” flag is followed by a directory name, and the directories are searched for library modules in the order that they are listed in the command file.
The include search path can also be specified in configuration files with “+incdir+” tokens. These tokens start with the “+incdir+” string, then continue with directory paths, separated from each other with “+” characters (not spaces) for the length of the line.
Other information can be included in the command file. See the section Command File Format for complete details on what can go in a command file.
Input Data at Runtime¶
Often, it is useful to compile a program into an executable simulation, then run the simulation with various inputs. This requires some means to pass data and arguments to the compiled program each time it is executed. For example, if the design models a micro-controller, one would like to run the compiled simulation against a variety of different ROM images.
There are a variety of ways for a Verilog program to get data from the outside world into the program at run time. Arguments can be entered on the command line, and larger amounts of data can be read from files. The simplest method is to take arguments from the command line.
Consider this running example of a square root calculator
module sqrt32(clk, rdy, reset, x, .y(acc)); input clk; output rdy; input reset; input [31:0] x; output [15:0] acc; // acc holds the accumulated result, and acc2 is // the accumulated square of the accumulated result. reg [15:0] acc; reg [31:0] acc2; // Keep track of which bit I'm working on. reg [4:0] bitl; wire [15:0] bit = 1 << bitl; wire [31:0] bit2 = 1 << (bitl << 1); // The output is ready when the bitl counter underflows. wire rdy = bitl; // guess holds the potential next values for acc, // and guess2 holds the square of that guess. wire [15:0] guess = acc | bit; wire [31:0] guess2 = acc2 + bit2 + ((acc << bitl) << 1); task clear; begin acc = 0; acc2 = 0; bitl = 15; end endtask initial clear; always @(reset or posedge clk) if (reset) clear; else begin if (guess2 <= x) begin acc <= guess; acc2 <= guess2; end bitl <= bitl - 1; end endmodule
One could write the test bench as a program that passes a representative set of input values into the device and checks the output result. However, we can also write a program that takes on the command line an integer value to be used as input to the device. We can write and compile this program, then pass different input values on the run time command line without recompiling the simulation.
This example demonstrates the use of the “$value$plusargs” to access command line arguments of a simulation
module main; reg clk, reset; reg [31:0] x; wire [15:0] y; wire rdy; sqrt32 dut (clk, rdy, reset, x, y); always #10 clk = ~clk; initial begin clk = 0; reset = 1; if (! $value$plusargs("x=%d", x)) begin $display("ERROR: please specify +x=<value> to start."); $finish; end #35 reset = 0; wait (rdy) $display("y=%d", y); $finish; end // initial begin endmodule // main
The “$value$plusargs” system function takes a string pattern that describes the format of the command line argument, and a reference to a variable that receives the value. The “sqrt_plusargs” program can be compiled and executed like this:
% iverilog -osqrt_plusargs.vvp sqrt_plusargs.v sqrt.v % vvp sqrt_plusargs.vvp +x=81 y= 9
Notice that the “x=%d” string of the “$value$plusargs” function describes the format of the argument. The “%d” matches a decimal value, which in the sample run is “81”. This gets assigned to “x” by the “$value$plusargs” function, which returns TRUE, and the simulation continues from there.
If two arguments have to be passed to the testbench then the main module would be modified as follows
module main; reg clk, reset; reg [31:0] x; reg [31:0] z; wire [15:0] y1,y2; wire rdy1,rdy2; sqrt32 dut1 (clk, rdy1, reset, x, y1); sqrt32 dut2 (clk, rdy2, reset, z, y2); always #10 clk = ~clk; initial begin clk = 0; reset = 1; if (! $value$plusargs("x=%d", x)) begin $display("ERROR: please specify +x=<value> to start."); $finish; end if (! $value$plusargs("z=%d", z)) begin $display("ERROR: please specify +z=<value> to start."); $finish; end #35 reset = 0; wait (rdy1) $display("y1=%d", y1); wait (rdy2) $display("y2=%d", y2); $finish; end // initial begin endmodule // main
and the “sqrt_plusargs” program would be compiled and executed as follows:
% iverilog -osqrt_plusargs.vvp sqrt_plusargs.v sqrt.v % vvp sqrt_plusargs.vvp +x=81 +z=64 y1= 9 y2= 8
In general, the “vvp” command that executes the compiled simulation takes a few predefined argument flags, then the file name of the simulation. All the arguments after the simulation file name are extended arguments to “vvp” and are passed to the executed design. Extended arguments that start with a “+” character are accessible through the “$test$plusargs” and “$value$plusargs” system functions. Extended arguments that do not start with a “+” character are only accessible to system tasks and functions written in C using the VPI.
In the previous example, the program pulls the argument from the command line, assigns it to the variable “x”, and runs the sqrt device under test with that value. This program can take the integer square root of any single value. Of course, if you wish to test with a large number of input values, executing the program many times may become tedious.
Another technique would be to put a set of input values into a data file, and write the test bench to read the file. We can then edit the file to add new input values, then rerun the simulation without compiling it again. The advantage of this technique is that we can accumulate a large set of test input values, and run the lot as a batch.
module main; reg clk, reset; reg [31:0] data[4:0]; reg [31:0] x; wire [15:0] y; wire rdy; sqrt32 dut (clk, rdy, reset, x, y); always #10 clk = ~clk; integer i; initial begin /* Load the data set from the hex file. */ $readmemh("sqrt.hex", data); for (i = 0 ; i <= 4 ; i = i + 1) begin clk = 0; reset = 1; x = data[i]; #35 reset = 0; wait (rdy) $display("y=%d", y); end $finish; end // initial begin endmodule // main
demonstrates the use of “$readmemh” to read data samples from a file into a Verilog array. Start by putting into the file “sqrt.hex” the numbers:
51 19 1a 18 1
Then run the simulation with the command sequence:
% iverilog -osqrt_readmem.vvp sqrt_readmem.vl sqrt.vl % vvp sqrt_readmem.vvp y= 9 y= 5 y= 5 y= 4 y= 1
It is easy enough to change this program to work with larger data sets, or to change the “data.hex” file to contain different data. This technique is also common for simulating algorithms that take in larger data sets. One can extend this idea slightly by using a “$value$plusargs” statement to select the file to read.