Writing RTL
Hierarchy and interfaces
Real designs are made of smaller designs. A module builds its children in its constructor and wires them to its own signals there; by the time _logic runs, the hierarchy exists and each module only talks about its own wires.
Instantiating a child
A child is created like any object, with parent_module=self, and its ports are connected to the parent's wires with connect.
class Datapath(Module):
def __init__(self, global_config, name, parent_module):
Module.__init__(self, global_config, name, parent_module=parent_module)
self.din = Wire(my_module=self, name='din', direction='input', width=16)
self.dout = Wire(my_module=self, name='dout', direction='output', width=16)
self.stored = Wire(my_module=self, name='stored', direction='internal', width=16)
# A child, and its ports joined to this module's wires.
self.fifo = FIFO(global_config=global_config, name='fifo',
parent_module=self, word_length=16, depth=16)
self.fifo.wr_data.connect(self.din)
self.fifo.rd_data.connect(self.stored)
self.build()
def _logic(self):
self.add_comb(self.dout, default_value=self.stored) # our OWN wireA module may only speak about its own wires
_logic - self.fifo.rd_data - is not allowed, for the same reason it is not allowed in Verilog or VHDL. Connect the child port to a wire of your own in the constructor and use that wire. It is the single most common surprise for people arriving from software.Parameters through the hierarchy
A parameter is a ModuleParam: it becomes a real parameter or generic in the generated HDL, and it is also an ordinary number at build time, so it can size arrays and drive loops. Keep a plain copy when you need the number itself for arithmetic.
self.depth = ModuleParam(self, name='DEPTH', value=depth, default_value=16)
self._depth = depth # for elaboration-time maths
for i in range(self._depth):
...When a module's structure differs per parameter - a different number of stages, say - give the instance its own entity name with module_name=f'Name_{param}', so two shapes never share one HDL module.
Interfaces
A handful of signals that always travel together - data, valid, and the enable coming back - is an interface. The library ships the common ones, and an interface is declared on a module in one line, with a direction, and connected end to end in one more.
from rtllibpack import BasicValidEnableIface
class Stage(Module):
def __init__(self, global_config, name, parent_module):
Module.__init__(self, global_config, name, parent_module=parent_module)
self.up = BasicValidEnableIface(self, 'up', 'input', width=10)
self.down = BasicValidEnableIface(self, 'down', 'output', width=10)
self.build()
def _logic(self):
self.add_comb(self.down.fields['data'], default_value=self.up.fields['data'])
self.add_comb(self.down.fields['valid'], default_value=self.up.fields['valid'])
self.add_comb(self.up.fields['enable'], default_value=self.down.fields['enable'])The fields are ordinary wires - iface.fields['data'] - so everything on the previous page applies to them. Two interfaces of the same kind connect to each other directly, which is how a pipeline is assembled from stages without naming every signal twice.
Clock and reset
You do not declare a clock or a reset. Every module that has a flop in it gets them, with the names your project settings ask for, and they are connected down the hierarchy for you. What you choose is what they are called and whether reset is active low - project settings, under house style.