17 KiB
Verilog and SystemVerilog Generation
Verilog and SystemVerilog are critical components of the hardware design tool ecosystem, but generating syntactically correct Verilog that is acceptable by a wide range of tools is a challenge -- and generating "good looking" output even more so. This document describes CIRCT's approach and support for generating Verilog and SystemVerilog, some of the features and capabilities provided, and information about the internal layering of the related subsystems.
Why is this hard?
One of the goals of CIRCT is to insulate "front end" authors from the details of Verilog generation. We would like to see innovation at the authoring level, and the problems in that space are quite different than the challenges of creating syntactically correct Verilog.
Further, the Verilog/SystemVerilog languages were primarily designed to be a human-authored programming language, and have evolved over the years with many new and exciting features. At the same time, the industry is full of critical EDA tools - but these have mixed support for different language features. Open source tools in particular have mixed support for new features. Different emission styles also impact simulator performance and have many other considerations. We would like clients of CIRCT to be insulated from this complexity where possible.
Beyond the capabilities of different tools, in many cases the output of CIRCT is run through various "linters" that look for antipatterns or possible bugs in the output. While it is difficult to work with arbitrary 3rd party linters, we would like the output of CIRCT-based tools to be as "lint clean by definition" as possible.
Finally, our goal is for the generated Verilog to be as readable and polished as possible - some users of CIRCT generate IP that is sold to customers, and the quality of the generated Verilog directly reflects on the quality of the corresponding products. This means that small details, including indentation and use of the correct idioms is important.
Baseline Assumptions
Circt assumes, as a baseline, that tools support the subset of verilog specified in IEEE 1364.1-2002 "IEEE Standard for Verilog Register Transfer Level Synthesis". Although this standard is deprecated and no replacement has been created for System Verilog, it provides a reasonable, established, and defined subset of the verilog specification which all tools should support. Circt may assume more features than specified in that document (e.g. system verilog constructs), but should be willing to consider lowering to simpler implementations when justified to support a major tool and when such lowering is possible. Supporting tools which do not meet this baseline will require substantial justification.
Controlling output style with LoweringOptions
The primary interface to control the output style from a CIRCT-based tool is
through the circt::LoweringOptions
structure. It contains a number of properties (e.g. emittedLineLength
or
disallowLocalVariables
) that affect lowering and emission of Verilog -- in
this case, what length of lines the emitter should aim for (e.g. 80 columns
wide, 120 wide, etc), and whether the emitter is allowed to use automatic logic
declarations in nested blocks or not.
The defaults in LoweringOptions
are set up to generate aesthetically pleasing
output, and to use the modern features of SystemVerilog where possible. Client
tools and frontends can change these, e.g. if they need to generate standard
Verilog for older tools.
Command line tools generally provide a --lowering-options=
flag that
allows end-users to override the defaults or the front-end provided features.
If you're using firtool
for example, you can pass
--lowering-options=emittedLineLength=200
to change the line length. This can
be useful for experimentation, or when a frontend doesn't have other ways to
control the output.
The current set of "tool capability" Lowering Options is:
noAlwaysComb
(default=false
). If true, emitssv.alwayscomb
as Verilogalways @(*)
statements. Otherwise, print them asalways_comb
.exprInEventControl
(default=false
). If true, expressions are allowed in the sensitivity list ofalways
statements, otherwise they are forced to be simple wires. Some EDA tools rely on these being simple wires.disallowPackedArrays
(default=false
). If true, eliminate packed arrays for tools that don't support them (e.g. Yosys).disallowPackedStructAssignments
(default=false
). If true, eliminate packed struct assignments in favor of a wire + assignments to the individual fields.disallowLocalVariables
(default=false
). If true, do not emit SystemVerilog locally scoped "automatic" or logic declarations - emit top level wire and reg's instead.verifLabels
(default=false
). If true, verification statements likeassert
,assume
, andcover
will always be emitted with a label. If the statement has no label in the IR, a generic one will be created. Some EDA tools require verification statements to be labeled.
The current set of "style" Lowering Options is:
emittedLineLength
(default=90
). This is the target width of lines in an emitted Verilog source file in columns.locationInfoStyle
(default=plain
). This option controls emitted location information style. The available styles are:plain
:// perf/regress/AndNot.fir:3:10, :7:{10,17}
wrapInAtSquareBracket
:// @[perf/regress/AndNot.fir:3:10, :7:{10,17}]
none
: (no comment emitted)
disallowPortDeclSharing
(default=false
). If true, emit one port per declaration. Instead ofinput a,\n b
this will produceinput a,\n input b
. When false, ports are emitted using the same declaration when possible.printDebugInfo
(default=false
). If true, emit additional debug information (e.g. inner symbols) into comments.disallowMuxInlining
(default=false
). If true, every mux expression is spilled to a wire. This is used to avoid emitting deeply nested mux expressions to improve readability.wireSpillingHeuristic
(default=spillNone
). This controls extra wire spilling performed in PrepareForEmission to improve readability and debuggability. It is possible to combine several heuristics by specifyingwireSpillingHeuristic
multiple times. (e.g.wireSpillingHeuristic=spillLargeTermsWithNamehints,wireSpillingHeuristic=spillAllMux
).spillLargeTermsWithNamehints
: If spillLargeTermsWithNamehints is specified, expressions with meaningful namehints (i.e. names which start with "_") are spilled to wires. For a namehint with "_" prefix, if the term size is greater thanwireSpillingNamehintTermLimit
(default=3), then the expression is spilled.
emitWireInPorts
(default=false
). Emitswire
in port lists rather than relying on 'default_nettype'. For instance, instead ofinput a
this option would emit that port asinput wire a
.emitBindComments
(default=false
). Emits a comment wherever an instance or interface instance is not printed, because it was emitted as a bind elsewhere.omitVersionComment
(default=false
). Avoids emitting a version comment (e.g.// Generated by CIRCT ...
) at the top of each verilog file.
The current set of "lint warnings fix" Lowering Options is:
explicitBitcast
(default=false
). If true, add an explicit bitcast for avoiding bitwidth mismatch lint warnings (e.g.8'(a+b)
).disallowExpressionInliningInPorts
(default=false
). If true, every expression passed to an instance port is driven by a wire. Some lint tools dislike expressions being inlined into input ports so this option avoids such warnings.caseInsensitiveKeywords
(default=false
). If true, then check for collisions with Verilog keywords insensitively. E.g., this will treat a variable calledWIRE
as a collision with the keyword and rename it toWIRE_0
(or similar). When set tofalse
, thenWIRE
will not be renamed.fixUpEmptyModules
(default=false
). If true, then add a dummy wire to empty modules since some vendor tools consider empty modules as a blackbox and raise synthesis errors.
Recommended LoweringOptions
by Target
This section presents a list of recommended LoweringOptions
for various tools.
Unlike LLVM, CIRCT doesn't use target triples to specify options. Instead,
depending on the target you may use, you'll need to manually specify the
appropriate options. Please refer ToolsWorkarounds
for the more details of each issue.
Questa
For Questa, we recommend using the emitWireInPorts
option. This option is
helpful because Questa emits warnings when ports do not have net types such as
wire
, reg
, or logic
.
Spyglass
We suggest using two options for Spyglass: explicitBitcast
and
disallowExpressionInliningInPorts
. Spyglass implements its own width rules,
and explicitBitcast
helps to ensure that the widths are correctly inferred.
Additionally, disallowExpressionInliningInPorts
helps to surpass NoExprInPorts
lint warnings.
Verilator
For Verilator, we recommend using the locationInfoStyle=wrapInAtSquareBracket
and disallowLocalVariables
options. Verilator treats comments such as // verilator ..
as verilator metadata. If your file location starts with "verilator",
the location string will be recognized as verilator metadata, but it will cause
a complication error. The locationInfoStyle=wrapInAtSquareBracket
option helps
to wrap file locations in @[..]
to avoid this issue. Additionally, Verilator
sometimes infers automatic logic variables as latches and emits warnings if these
variables are conditionally assigned in procedural statements, so we recommend
using disallowLocalVariables
.
Vivado
For Vivado, we recommend using the mitigateVivadoArrayIndexConstPropBug
option.
Vivado (at least 2020.2, 2021.2, and 2022.2) has a bug in constant propagation,
and this option helps to create a wire with the desired behavior.
Yosys
For Yosys, we recommend using the disallowLocalVariables
and disallowPackedArrays
options. Yosys doesn't parse automatic
variables, so disallowLocalVariables
is
required. Additionally, Yosys doesn't accept packed arrays, so we suggest using
disallowPackedArrays
.
Specifying LoweringOptions
in a front-end HDL tool
The circt::LoweringOptions
struct itself
is very simple: it projects each of the lowering options as a boolean, integer
or other property. This allows C++ code to set up and query these properties
with a natural and easy to use API.
That said, this struct is merely a convenience the actual truth is encoded into
the IR as a circt.loweringOptions
string attribute on the top level
builtin.module
declaration. Any frontend can set these options by setting
this attribute on the IR that they generate.
Adding new Lowering Options
Adding new LoweringOptions
is pretty easy, but we want to be able to scale to
having lots of these and want them to remain as consistent as we can. Please
follow these guidelines when adding new things:
-
Don't use
LoweringOptions
to change the semantics of IR nodes in ExportVerilog. Instead, add new IR nodes to model the different semantic concepts that you need, and queryLoweringOptions
to decide what construct to lower to. -
Make the default setting of the flag generate modern and clean SystemVerilog code. Flags should make the output more conservative/verbose/boring.
-
Name boolean options with active verb and reuse the existing ones (e.g.
disallow
) where possible. Try to make new options consistently named. -
Consider making
ExportVerilog
reject invalid constructs with an error message - a compiler bug that causes CIRCT to generate the wrong construct is pretty certain to be better diagnosed by CIRCT itself than by the EDA tool that consumes the output. -
Keep this documentation up to date.
Using the Verilog Exporter in a PassManager pipeline
When building a new compiler, you get to decide what order to run passes in,
and the order of passes can greatly affect the quality of the generated IR.
There are many ways to do this, but we'd recommend you follow the example of a
well maintained tool in CIRCT (e.g. firtool
). Something like this as the end
of your pipeline should work well:
// Optional: perform general cleanups and structure the modules in a
// consistent way.
auto &modulePM = pm.nest<hw::HWModuleOp>();
modulePM.addPass(sv::createHWCleanupPass());
modulePM.addPass(createCSEPass());
modulePM.addPass(createSimpleCanonicalizerPass());
// Required: Legalize unsupported operations within modules. Do not run
// passes after this that aren't aware of LoweringOptions.
modulePM.addPass(sv::createHWLegalizeModulesPass());
// Optional: Tidy up the IR to improve Verilog emission quality.
modulePM.addPass(sv::createPrettifyVerilogPass());
// Actually export the module.
exportVerilog(theModule, ...);
Signal naming
ExportVerilog checks all signal (and instance) names for keyword conflicts and duplicated names. Whenever these conditions are encountered, ExportVerilog will change the name to avoid the conflict.
Ops with explicit names
The sv.reg
operation, sv.wire
operation, and the various instance operations
in the hw dialect have a name
attribute which gets used.
Out-of-line expressions ("temporaries")
Expressions are sometimes not emitted inlined (out-of-line) for a variety of
reasons. These wire (or automatic logic
) names or existence is not
guaranteed to be stable. (Meaning they could change for any reason.) They,
therefore, should not be relied upon for anything but local waveform debugging.
In general, any name with prefixed with an underscore should not be relied upon.
These names come from the following sources, listed here in proirity order:
- The
sv.namehint
dialect attribute (which can be attached to any operation). - If ExportVerilog has a rule to derive a name based on the operation and
operands, it will do so. (e.g.
hw.extract
operations get name_<operandName>_<highBit>to<lowBit>
as of writing). _T
or_T_x
whereinx
is a number.
exportVerilog
Internals
It turns out that producing syntactically correct Verilog that is also pretty is really hard. As such, we've taken a few steps to improve separation of concerns and thus simplify the implementation of Verilog emission. It is important to understand the division of responsibilities between these components when adding new features or fixing bugs in the existing code.
In particular, we split responsibilities between three major components, which are run in this order:
- The optional
PrettifyVerilog
pass. It is never required for correctness, but has a major impact on "prettiness", and should always be used in practice. - The mandatory [
PrepareForEmission
logic] that is built into theexportVerilog
function, and is thus mandatory. - The core
ExportVerilog
logic, which handles printing out of Verilog source code.
The first two of these are highly parameterized on LoweringOptions
, and we're
trying to minimize the complexity in the last one. Let's discuss each of them
in inverse order.
The core ExportVerilog
logic
The core of ExportVerilog
walks the IR and prints out syntactically correct
Verilog to an llvm::raw_ostream
.
TODO: Talk about line splitting, preorder traversal, NameCollector, etc. Cross block references always go through a temporary. This doesn't support cyclic graph region references in the top level of a hw.module.
Because the Prepass logic has already been run, it knows it doesn't have to handle invalid output - it will have already been lowered by Prepass or other things earlier in the pipeline. As such, it can just diagnose any invalid things that may have slipped through with an error.
The PrepareForEmission
logic built into ExportVerilog
This functionality is a logically distinct lowering pass that happens to run as part of ExportVerilog (so it cannot be forgotten or drift away from the exporter). It is structured as a lowering pass that rewrites invalid constructs as lower level ones, e.g. injecting explicit wires in places to break cyclic combinational logic circuits or duplicating array indexing operations into the same block as the uses (so they'll be sure to be inlined).
PrepareForEmission
also collects some information about local names that are
used by the emitter later.
PrettifyVerilog
Internals
PrettifyVerilog
is an optional pass that is run right before the Verilog
emitter. It introduces prettier but non-canonical forms of expressions that
align with user expectations: for example, we print "a-1" instead of "a+255".
When in SystemVerilog mode, this pass sinks expressions into nested regions (e.g. into procedural if regions) whenever it can to encourage inline emission of subexpressions. It also moves instances to the end of the module, which eliminates temporaries and makes the output more predictable.