Project F

Verilog Lint with Verilator

Published · Updated

Hardware design can be unforgiving, so it pays to use any advantage you can get. Verilator is a Verilog simulator and C++ compiler that also supports linting: statically analysing your designs for issues. Not only can Verilator spot problems your synthesis tool might overlook, but it also runs quickly. Verilator is also great for graphics simulation with SDL.

Share your thoughts with @WillFlux on Mastodon or Twitter. If you like what I do, sponsor me. 🙏

Installing Verilator

Linux

Verilator is available in most Linux distribution repos and will run on Windows Subsystem for Linux.

For Debian and Ubuntu-based distros, you can install Verilator with apt:

apt update
apt install verilator

macOS

On macOS, you can install a recent version of Verilator via the Homebrew package manager:

brew install verilator

To install Verilator for other platforms, such as FreeBSD, see the official Verilator install guide.

Basic Linting

For a standalone module, linting is simplicity itself:

verilator --lint-only -Wall foo.v

--lint-only - tells Verilator to lint but not to generate any simulation output
-Wall - turns on extra stylistic checks

If all is well, you’ll see no messages from Verilator.

If Verilator finds a potential issue, it provides clear advice, including how to disable the warning. The Verilator manual includes the list of possible warnings.

Let’s create a simple “add” module with a couple of problems, then lint it:

`default_nettype none

module add (
    input  wire clk,
    input  wire [3:0] x,
    input  wire [3:0] y,
    output reg z,
    output reg c
    );

    always @(posedge clk) begin
        z <= x + y;
    end
endmodule
$ verilator --lint-only -Wall add.v
%Warning-WIDTH: add.v:12:11: Operator ASSIGNDLY expects 1 bits on the Assign RHS, but Assign RHS's ADD generates 4 bits.
                           : ... In instance add
   12 |         z <= x + y;
      |           ^~
                ... Use "/* verilator lint_off WIDTH */" and lint_on around source to disable this message.
%Warning-UNDRIVEN: add.v:8:16: Signal is not driven: 'c'
                             : ... In instance add
    8 |     output reg c
      |                ^
%Error: Exiting due to 2 warning(s)

The first problem is with widths: x and y are 4 bits wide, but z has no explicit width, so is only 1 bit wide.

We could ignore the width warning by doing this:

    always @(posedge clk) begin
        /* verilator lint_off WIDTH */
        z <= x + y;
        /* verilator lint_on WIDTH */
    end

But this hides the problem without doing anything about it.

Instead, we can fix it by setting the width of z to 4 bits:

    output reg [3:0] z,

While this removes the Verilator warning, it may not have fixed the problem completely.

For example, what happens if x and y are both 4'b1000? Our addition overflows, leaving z with the value 4'b0000. This example illustrates one of the limitations of linting: it can look at the widths of different signals but can’t account for all the logic applied to them.

So, as well as fixing the width of z, we can use c as a carry signal, which also resolves the “Signal is not driven” warning:

    always @(posedge clk) begin
        {c,z} <= x + y;
    end

Dependencies and Paths

What happens if a module depends on another? Verilator will search the current path for matching modules. If you want to add additional directories to the module search path, we can use -I. For example, if top.v depends on modules in the ../maths directory:

verilator --lint-only -Wall -I../maths top.v

You can use multiple -I arguments to include multiple directories.

Black Boxes and Null Modules

Most designs depend on vendor primitives or IP cores for which you don’t have the source, for example using a PLL to generate a clock. When you try to lint a module that refers to a vendor primitive, you’ll get an error such as this:

%Error: clock_pix.sv:29:5: Cannot find file containing module: 'MMCME2_BASE'
   29 |     MMCME2_BASE #(
      |     ^~~~~~~~~~~
%Error: clock_pix.sv:29:5: This may be because there's no search path specified with -I<dir>.
   29 |     MMCME2_BASE #(
      |     ^~~~~~~~~~~
        ... Looked in:
             MMCME2_BASE
             MMCME2_BASE.v
             MMCME2_BASE.sv
             obj_dir/MMCME2_BASE
             obj_dir/MMCME2_BASE.v
             obj_dir/MMCME2_BASE.sv

Your first thought is probably to find a way to exclude MMCME2_BASE from the lint. Alas, Verilog “can’t be linted without elaboration, which requires the whole design”. We can get around this by creating a null module for the primitive. A null module includes the IO but none of the logic.

For example, I’ve created a null module for Xilinx’s BUFG primitive:

module BUFG (
    input  wire logic I,
    output      logic O
    );

    // NULL MODULE

endmodule

Creating null modules is slightly tedious, but lets you lint your entire design. To use the null module ensure it’s in Verilator’s search path (see the previous section).

Linting Waivers

If you need to silence linter warnings on larger designs, or those using third-party source, then /* verilator lint_off */ comments may be impractical. Starting with Verilator 4.028, you can create waiver files to handle warnings without touching the source. To learn more, see Stefan Wallentowitz’s post Verilator Waivers.

Linting Shell Script

If you have many top modules and/or include directories you can automate checking with a Makefile or a simple shell script.

The following shell script lints all top modules in the same directory as the script:

#!/bin/sh

DIR=`dirname $0`

echo "## Linting top modules in ${DIR}"
for f in ${DIR}/top_*\.*v; do
    echo "##   Checking ${f}";
    verilator --lint-only -Wall -I${DIR} -I${DIR}/../common $f;
done

You can adjust the -I parameters to suit your setup. The glob top_*\.*v capture files with .v and .sv extensions.

What’s Next?

Take a look at FPGA Tools for more Verilator content or check out my FPGA and RISC-V Tutorials.