Contrasting Verilog and SeqiLog Port List Customization
How using a Python-based builder pattern to construct Module contents simplifies and expands the possibilities for interfaces.
Introduction
Verilog provides myriad ways to customize RTL modules for reusability. Parameters, generic types, and generate
statements let the designer code once, and adapt module instances for different circumstances. But there is no method to specify a conditional port, or a variable number of ports. You can change the type of ports at elaboration time, but never select whether ports appear in the module header. In this post I will explain how SeqiLog solves this problem.
Verilog Lacks Port Generate
Let’s start with an example. Can a Verilog module implement both a half adder and a full adder?
A “half adder” performs 1-bit addition without a carry-in. In Verilog:
A “full adder” also performs 1-bit addition, but with a carry-in. In Verilog:
The most straightforward way to combine these two modules would be to use a Boolean parameter
and a generate-if
statement:
This will work fine, but there is an obvious inconsistency. If FULL equals 1'b0
(False), in the half_adder
branch of the generate-if the ci
(carry-in) port is never used. Though the module body reflects one particular parameterization, the port list needs to be a union of all the possible ports used in the body.
Why does Verilog have a generate
construct for the module body, but not for the port list? There is probably a good reason, but I apologize Dear Reader — I don’t know it.
You might be wondering why anybody would want to do something silly like combine a half and full adder into a single module. I admit, it is a contrived example. Not only would few people find this a valuable exercise, but more likely an RTL designer would simply use the +
operator instead of an adder submodule. I am using this example because it is simple and familiar. A slightly more compelling example might be a module that implements an AMBA AXI interface. That interface has over a hundred signals, many with parameterizable widths, on each of the AW (Write Address), W (Write Data), B (Write Response), AR (Read Address), R (Read Data), AC (Snoop Request), and CR (Snoop Response) channels. A standardized link interface module might need to come in several configurations when implementing any non-trivial fabric. Every signal that is used by at least one config must be declared for all configs, regardless of whether it is consumed. In addition, it is common practice to add debug instrumentation to the RTL that is not included in the synthesized netlist.
This limitation forces the RTL designer to resort to all sorts of workarounds and barbarous contortions. Fancy data types like unions and unpacked arrays are hardly a solution. The easiest “solution” is to just ignore the problem and tolerate dangling ports wherever they are not required.
Unfortunately, many design teams resort to using either preprocessor `ifdef … `endif
macros, or a bolt-on string template expansion tool to generate Verilog from a higher level specification. The source code ends up looking like this:
Preprocessor and template expansion commit two grave sins:
They make debugging harder, and
They inevitably lead to simulation/synthesis mismatch
If your programming language needs a text-based preprocessor (in 2024+), it is not a good language. Please do yourself a favor: accelerate through the stages of grief to acceptance.
We can do better. Python and SeqiLog are here to help.
SeqiLog Solution
SeqiLog addresses this problem by avoiding port lists altogether:
The build
method follows the Builder pattern. It is imperative, executing from start to finish, building the module body one Python statement at a time. First, it creates ports for sum (s
), carry-out (co
), a
, and b
. If FULL==True
, it creates the carry-in (ci
) port, then declares expression processes to drive the “full” sum and carry-out values. Otherwise, it will only declare expression processes to drive the “half” sum and carry-out values.
The FULL
parameter works very similar to the Verilog version. The parameter list is declared in the outer class scope.
Aside from being implemented in Python, the major difference with the Verilog version is that SeqiLog has no port statement. Ports are not special; they are declared in the build method just like all other module components. A parameter can select whether or not to declare the port. This allows maximum flexibility. The example above is relatively straightforward, but the build method can do anything. It can parse a file on disk, read an Excel spreadsheet, or do something even crazier like download an AMBA specification from the Internet.
For convenience, an IPython notebook that demonstrates this example can be found here. If you execute the code, it will produce a VCD file that can be viewed with GTKWave or Surfer:
Conclusion
In this post, we briefly explored an important limitation of Verilog port lists, how that limitation leads to unnecessary complexity in project RTL, and how SeqiLog’s build
method is much more powerful for defining module interfaces.
In the immortal words of John Ousterhout, “it’s all about complexity”. More complexity means higher costs, and lost opportunity. To reduce complexity, modules should have narrow and precise interfaces, deep implementation details, and strong encapsulation.
SeqiLog is an experimental Python meta-HDL. At the time of this writing, it is not an alternative to Verilog. Not even close. But it’s a fun hobby project. We hope to spur innovation in this space, and better interfaces is a great place to start.