Skip to content
julianspeith edited this page Aug 12, 2026 · 28 revisions

A module is a functional unit comprising gates and their connected nets. Modules allow for hierarchization of the netlist and can be nested into each other, thus creating different levels of abstraction from the original netlist. Per design, modules only really contain gates and not nets. An important distinction between gates and modules is that a net does not end at the module boundaries but actually crosses them. The module comes with a set of functions to obtain all nets that are connected to gates within that module. By default, all gates of a netlist are contained in a single module called the "top_module" if no hierarchy is defined within the netlist file. Each module comes with a unique ID and a name. Additionally, a type may be assigned to a module if desired.

Modules are where reverse engineering results are recorded. A netlist recovered from hardware arrives completely flat: every gate sits directly in the top module and nothing indicates which gates belong together. As you figure out that 128 flip-flops form the key register, or that a cluster of LUTs implements a round function, you capture that insight by moving those gates into a module. From then on the structure is part of the netlist — it survives saving the project, it can be folded into a single box in the graph view, and it can be handed to analyses that operate on modules.

This is also what dataflow analysis does automatically: create_modules() writes each recovered register back into the netlist as a module. Compared to groupings, modules are the committed form of structure — hierarchical, with pins and a boundary — while groupings are the loose, throwaway variant for hypotheses you are still testing.

Module information and management

A module can be created by calling create_module on the current netlist. When creating a new module, the user always needs to specify its parent module. In the example below, we simply use the "top_module" as a parent module. The ID cannot be changed by the user and may only be read by calling the get_id function from Python. Read and write access to the name is provided by the get_name and set_name functions respectively. Similarly, the module's type can be accessed using get_type and set_type. The get_top_module function can be executed on the netlist and returns the top module of the current netlist. Some example Python code is given below:

m = netlist.create_module("example_module", netlist.get_top_module())   # create a new module within the top module
print(m.get_name())                                                     # print the name of the module
print(m.get_type())                                                     # print the type of the module
print(m.get_id())                                                       # print the ID of the module

To retrieve a module from the netlist that corresponds to a known ID, the get_module_by_id command can be executed on the netlist. Furthermore, a module may be deleted using delete_module as shown below. When deleting a module, all gates assigned to that module are handed over to its parent module.

m = netlist.get_module_by_id(3)   # get the module with ID 3 from the netlist
netlist.delete_module(m)          # delete the module

In addition, a module can also be part of a grouping itself. While details on how to assign a grouping can be found here, the easiest way to retrieve the current grouping of a module is given by the get_grouping function.

m = netlist.get_module_by_id(3)   # get the module with ID 3
grouping = m.get_grouping()       # get the current grouping of the module

Managing gates, parent-, and submodules

Gates can be added to a module by calling assign_gate and removed by using remove_gate. For bulk operations, assign_gates and remove_gates take a list of gates and are considerably faster than calling the single-gate variants in a loop. Furthermore, the user can check whether a gate, module, or net is part of a module by resorting to contains_gate, contains_module, and contains_net. Finally, a list of contained gates and submodules is returned by the functions get_gates and get_submodules. A short example is given below:

m = netlist.create_module("example_module", netlist.get_top_module())   # create a new module within the top module
m.assign_gate(netlist.get_gate_by_id(1))                                # assign gate with ID 1 to module
m.contains_gate(netlist.get_gate_by_id(4))                              # return 'false'
gates = m.get_gates()                                                   # return a list containing only the gate with ID 1
m.remove_gate(gates[0])                                                 # remove gate with ID 1 from module

A module's parent module can be obtained using get_parent_module. For the top module, this will return None. Furthermore, the gates of a module may also be accessed by ID using get_gate_by_id.

top = netlist.get_top_module()                     # get the top module of the netlist
m = netlist.create_module("example_module", top)   # create a new submodule of the top module 
m.get_parent_module()                              # return the top module
top.get_parent_module()                            # return None
top.get_gate_by_id(4)                              # get gate with ID 4 from top module if present within the module

Note that get_gates, get_submodules, get_nets, contains_gate, and contains_module operate on the module's direct content by default. Each accepts a recursive flag to include everything in nested submodules as well — which is almost always what you want once a hierarchy exists:

m.get_gates()                       # only gates directly assigned to m
m.get_gates(None, True)             # all gates in m and all of its submodules
m.get_submodules(None, True)        # the entire subtree of modules below m

For navigating the hierarchy, get_parent_modules returns the whole chain of ancestors up to the top module, is_top_module identifies the root, and get_submodule_depth gives the module's distance from it. is_parent_module_of and is_submodule_of test the relationship between two modules directly, each with an optional recursive flag.

m.is_top_module()          # False for anything but the top module
m.get_submodule_depth()    # 0 for the top module, 1 for its direct children, ...
m.is_submodule_of(top, True)

A module can be moved elsewhere in the hierarchy with set_parent_module. Its gates move with it, and HAL recomputes the affected module boundaries automatically.

m.set_parent_module(other_module)   # re-parent the module

Managing nets

The nets contained in a module are dynamically assigned depending on the gates belonging to the module. Nets can either be inputs, outputs, or internal to the module. Note that a net may also fall into multiple of these categories at the same time. Input nets are all nets that have a destination within the module and are either a global input to the netlist or have at least one of their sources outside of the module. Similarly, output nets have a source within the module and are either a global netlist output or have at least one destination outside of the module. Internal nets do have at least one source and one destination within the module, but may have additional outside connections. Hence, a net with sources and destinations both inside and outside of the module is regarded as input, output, and internal. To access those nets, the functions get_input_nets, get_output_nets, and get_internal_nets each return a respective list of nets. To get all nets that have at least one source or one destination within the module, get_nets may be used. Additionally, to check whether a net is an input, output, or internal net, the functions is_input_net, is_output_net, is_internal_net are provided.

nets = m.get_nets()                     # get all nets of the module
input_nets = m.get_input_nets()         # get all input nets of the module
internal_nets = m.get_internal_nets()   # get all internal nets of the module

The input and output nets are effectively the interface of whatever you have isolated into the module, and inspecting them is usually the first thing to do after creating one. A recovered register whose input nets include a signal you already identified as a clock enable, or whose output nets lead straight to a global output, tells you a lot about its role in the design.

Because net assignment is derived from the gates in the module, it is recomputed automatically whenever gates are added or removed. update_nets forces this recomputation and is only needed if the netlist was modified in a way HAL could not observe.

Managing pins

Similar to gate types, modules also feature pins through which they are connected to surrounding circuitry. Each ModulePin is assigned an ID unique within the owning module, a name, a direction, a type, and a net. Usually, module pins are automatically assigned whenever gates are added or removed to/from the respective module. Manual creation of module pins is usually not desirable and hence strongly discouraged. However, names and types of the pins may be changed on demand.

The pins of a module can be retrieved using get_pins by optionally providing a filter function. If only the names of the pins are required, get_pin_names may be used. Similar short-hand functions are provided to only retrieve input or output pins (including inout pins): get_input_pins, get_input_pin_names, get_output_pins, and get_output_pin_names.

pins = m.get_pins()                                                     # get all pins of module m
input_pins = m.get_input_pins()                                         # get input pins of module m
data_pins = m.get_pins(lambda p: p.get_type() == hal_py.PinType.data)   # get pins of type 'data' of module m

Furthermore, pins can be retrieved using their ID, name, or the associated net using get_pin_by_id, get_pin_by_name, and get_pin_by_net.

pin_by_id = m.get_pin_by_id(3)            # get pin with ID 3 of module m
pin_by_name = m.get_pin_by_name("A")      # get pin with name "A" of module m
some_net = netlist.get_net_by_id(5)       # get net with ID 5
pin_by_net = m.get_pin_by_net(some_net)   # get pin associated with net with ID 5 of module m

The ID, name, direction, type, and net associated with a pin can be retrieved using get_id, get_name, get_direction, get_type, and get_net of class ModulePin respectively. To change the name or type of a module pin, use set_pin_name and set_pin_type provided by the Module class.

some_pin = m.get_pin_by_id(1)              # get pin with ID 1 of module m
pin_name = some_pin.get_name()             # get the name of the pin
pin_direction = some_pin.get_direction()   # get the direction of the pin
pin_type = some_pin.get_type()             # get the type of the pin
m.set_pin_name(some_pin, "new name")       # set the name of the pin to "new name"

Managing pin groups

A so called ModulePinGroup is used to enable multi-bit pins. This matters more than it may sound: grouping eight single-bit pins into an ordered group A is what lets you treat them as one 8-bit value and reason about the module at the word level -- asking whether it computes A + B rather than tracking eight unrelated bits. The BooleanFunctionDecorator turns a pin group directly into a multi-bit Boolean function variable, and the Simple ALU example project builds its entire SMT check on this.

Since the bit order within a group determines the meaning of that value, it must be correct -- recovering it from the surrounding circuitry (carry chains, shift registers) is what the bitorder propagation plugin is for. On creation, every pin is automatically assigned to a pin group that contains just that very pin. If needed, this can be prevented using the create_group flag of the create_pin function. However, in the end HAL requires each pin to be part of a group to avoid memory inconsistency. Hence, when disabling auto-assignment to a pin group on pin creation, the user must ensure that the respective pin is manually assigned to a pin group later on.

Pin groups can be created using create_pin_group and providing a name for the group and an ordered list of pins to be assigned to the group. Optionally, a direction, type, order (ascending or descending), and start_index can be provided. Pins can then be accesses either directly or through their group. All pin groups of a module can be accessed using get_pin_groups. As for module pins, pin groups can also be retrieved by their unique ID or name using get_pin_group_by_id and get_pin_group_by_name. The class ModulePinGroup offers functions to retrieve ID (get_id), name (get_name), direction (get_direction), type (get_type), order (is_ascending), and start index (get_start_index) of a pin group. The pins of the group are returned by get_pins. To get a pin at a certain index of the group, get_pin_at_index may be used. To get the index of a given pin within a group, the function get_index is provided.

m.create_pin_group("A", [a0, a1], hal_py.PinDirection.input)   # assign both pins to a new pin group with name "A"
groups = m.get_pin_groups()                                    # get all pin groups of the gate type
a = m.get_pin_group_by_name("A")                               # get pin group named "A" of the gate type
pins = a.get_pins()                                            # get all pins belonging to group "A" of the gate type as an ordered list

The class Module provides additional functions to facilitate more advanced options to handle pin groups. Module pin groups can be deleted using delete_pin_group, which will result in every pin previously belonging to the group being assigned to a new pin group only containing that one pin. In addition, pins may also be assigned to a group after creation of the group using assign_pin_to_group. The order of pins within a group can be changed using move_pin_within_group. Finally, a pin may be removed from a group using remove_pin_from_group. Note that the indices within a pin group are always continuous. Removing a pin will cause the index gap being closed by all subsequent pins resulting in their indices shifting by 1.

The name, type, and direction of an existing group can be changed with set_pin_group_name, set_pin_group_type, and set_pin_group_direction, and a group can be repositioned among the module's pin groups using move_pin_group.

some_group = m.get_pin_group_by_id(3)            # get pin group with ID 3 of module m; assume the group to contain pins "A(0)", "A(2)", and "A(3)"
pin_a1 = m.get_pin_by_name("A(1)")               # get the pin "A(1)" currently missing from the pin group
m.assign_pin_to_group(some_group, pin_a1)        # assign "A(1)" to the pin group; it will be appended to the back of the group
m.move_pin_within_group(some_group, pin_a1, 1)   # move pin to correct position within group
pin_a3 = m.get_pin_by_name("A(3)")               # get the pin "A(3)" currently belonging to the pin group
m.remove_pin_from_group(some_group, pin_a3)      # remove "A(3)" from the pin group
m.delete_pin_group(some_group)                   # delete the entire pin group

Clone this wiki locally