Verilog Lint with Verilator
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.
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
Adjust the -I
parameters to suit your setup. The glob top_*\.*v
selects .v
and .sv
files.
What’s Next?
Take a look at FPGA Tools for more Verilator content or check out my FPGA and RISC-V Tutorials.
Get in touch on Mastodon, Bluesky, or X. If you enjoy my work, please sponsor me. 🙏