Search and Replacement of more complex Structures: Clock Gating¶
Often, reoccurring instances or structures should be replaced by a certain structure. In the following, this is explained for the replacement of plain flip-flops with a flip-flop plus clock-gate structure.
Info: Before using this notebook, clone the openMSP430 repository and update the openMSP_hdl_path variable in the box below accordingly.
Replacing all instances of a certain type with a certain structure¶
- Execute the cell below to read the
openMSP430design, load the circuit and set everything up as usual.
In [ ]:
Copied!
from pathlib import Path
import netlist_carpentry
from netlist_carpentry.scripts.script_builder import build_and_execute
def gen_nl_and_read():
yosys_script_path = Path("files/openMSP430/openMSP430_synthesis_script.sh")
# Path within the openMSP430 submodule
openMSP_hdl_path = Path("../../../openmsp430/core/rtl/verilog/*.v")
nl_path = Path("files/openMSP430/openMSP430.json")
top = "openMSP430"
techmap_path = Path("files/openMSP430/pmux2mux_techmap.v")
build_and_execute(yosys_script_path, [openMSP_hdl_path], nl_path, top=top, techmap_paths=[techmap_path])
return netlist_carpentry.read_json(nl_path)
circuit = gen_nl_and_read()
module = circuit.top
# Write back unchanged circuit for comparison
netlist_carpentry.write(circuit, "output/openMSP430_unchanged.v", overwrite=True)
from pathlib import Path
import netlist_carpentry
from netlist_carpentry.scripts.script_builder import build_and_execute
def gen_nl_and_read():
yosys_script_path = Path("files/openMSP430/openMSP430_synthesis_script.sh")
# Path within the openMSP430 submodule
openMSP_hdl_path = Path("../../../openmsp430/core/rtl/verilog/*.v")
nl_path = Path("files/openMSP430/openMSP430.json")
top = "openMSP430"
techmap_path = Path("files/openMSP430/pmux2mux_techmap.v")
build_and_execute(yosys_script_path, [openMSP_hdl_path], nl_path, top=top, techmap_paths=[techmap_path])
return netlist_carpentry.read_json(nl_path)
circuit = gen_nl_and_read()
module = circuit.top
# Write back unchanged circuit for comparison
netlist_carpentry.write(circuit, "output/openMSP430_unchanged.v", overwrite=True)
- In addition, read the Verilog file that contains the module representing the clock gate.
- Execute the cell below to load the clock gate structure and convert it into a
Circuitobject.
In [ ]:
Copied!
clock_gate_path = "files/openMSP430/clock_gating/clock_gate.v"
cg_circuit = netlist_carpentry.read(clock_gate_path)
cg_module = cg_circuit.first
cg_ports = {port.name for port in cg_module.ports.values()}
print(f"Module {cg_module.name} has these ports: {', '.join(cg_ports)}")
clock_gate_path = "files/openMSP430/clock_gating/clock_gate.v"
cg_circuit = netlist_carpentry.read(clock_gate_path)
cg_module = cg_circuit.first
cg_ports = {port.name for port in cg_module.ports.values()}
print(f"Module {cg_module.name} has these ports: {', '.join(cg_ports)}")
- The goal is to find all flip-flops in the circuit (e.g. either recursively or by simply iterating over the instances of each module) and to replace them with a flip-flop plus clock-gate structure.
- Different types of flip-flops have different type descriptors in Yosys-generated netlists, but all have "dff" in their name.
- In Netlist Carpentry, each type of flip-flop is represented by a single instance type "§dff", unifying the different types generated by Yosys.
- The structural differences are handled when reading the Yosys-generated netlist.
- Execute the cell below to collect the paths to each flip-flop in a module definition.
Info: In the previous notebook, all flip-flop instances across the design were collected with their full instance paths in the design.
This time, only the flip-flops in the module definition are required.
If the flip-flops in the module definition are modified, they are also updated everywhere the module is instantiated.
Info: Flip-flops may have a data width of more than 1 bit.
This can be seen in the output of the cell below.
Accordingly, the number of flip-flops does not directly represent the total number of bits to store.
For this reason, the total number of flip-flops along with the total data width of all flip-flops is printed at the bottom of the output.
With Instance.split(), an n-bit wide instance can be split into n 1-bit wide instances for easier handling.
In [ ]:
Copied!
from typing import List, Set
from netlist_carpentry import Circuit, Module
def _check_not_found_and_update(dff_paths: List[str], already_found_ffs: Set[str], dff_data: str):
if dff_data not in already_found_ffs:
dff_paths.append(dff_data)
already_found_ffs.add(dff_data)
def collect_dffs(circuit: Circuit, module: Module) -> List[str]:
already_found_ffs = set()
dff_data_strs = []
for inst in module.instances_by_types.get("§dff", []):
path_and_width = (inst.raw_path, inst.ports["D"].width)
_check_not_found_and_update(dff_data_strs, already_found_ffs, path_and_width)
for m_inst in module.submodules:
submodule = circuit.get_module(m_inst.instance_type)
for path in collect_dffs(circuit, submodule):
_check_not_found_and_update(dff_data_strs, already_found_ffs, path)
return dff_data_strs
dff_paths = collect_dffs(circuit, circuit.top)
print("These are the direct instance paths of all flip-flops (and their data widths) in the module definitions:")
print("\n".join(f"\t{path} ({width} bit wide)" for path, width in dff_paths))
total_stored_bits = sum(width for _, width in dff_paths)
print(f"The design contains {len(dff_paths)} flip-flops (in regards to module definitions) with a total of {total_stored_bits} bits to store!")
from typing import List, Set
from netlist_carpentry import Circuit, Module
def _check_not_found_and_update(dff_paths: List[str], already_found_ffs: Set[str], dff_data: str):
if dff_data not in already_found_ffs:
dff_paths.append(dff_data)
already_found_ffs.add(dff_data)
def collect_dffs(circuit: Circuit, module: Module) -> List[str]:
already_found_ffs = set()
dff_data_strs = []
for inst in module.instances_by_types.get("§dff", []):
path_and_width = (inst.raw_path, inst.ports["D"].width)
_check_not_found_and_update(dff_data_strs, already_found_ffs, path_and_width)
for m_inst in module.submodules:
submodule = circuit.get_module(m_inst.instance_type)
for path in collect_dffs(circuit, submodule):
_check_not_found_and_update(dff_data_strs, already_found_ffs, path)
return dff_data_strs
dff_paths = collect_dffs(circuit, circuit.top)
print("These are the direct instance paths of all flip-flops (and their data widths) in the module definitions:")
print("\n".join(f"\t{path} ({width} bit wide)" for path, width in dff_paths))
total_stored_bits = sum(width for _, width in dff_paths)
print(f"The design contains {len(dff_paths)} flip-flops (in regards to module definitions) with a total of {total_stored_bits} bits to store!")
- To retrieve the objects from the instance paths, use the Circuit object to retrieve the module (the string part left of the
.) and then the instance with the corresponding name (the string part right of the.). - Execute the cell below to retrieve flip-flop objects from the instance paths and store them in a set of flip-flop instances.
Info: For simplicity reasons, only flip-flops with a data width of 1 bit are considered in this notebook.
Otherwise, the clock-gate must also be adjusted for flip-flops of larger widths.
This would exceed the scope of this notebook.
In [ ]:
Copied!
from netlist_carpentry import Instance
ffs: Set[Instance] = set()
module_inst_paths = [path.split('.') for path, _ in dff_paths]
for module_name, inst_name in module_inst_paths:
ff = circuit.get_module(module_name).get_instance(inst_name)
if ff.parameters["WIDTH"] == 1:
ffs.add(ff)
print(f"Found {len(ffs)} flip-flop objects with a data width of 1 bit out of a total of {len(dff_paths)} flip-flops.")
from netlist_carpentry import Instance
ffs: Set[Instance] = set()
module_inst_paths = [path.split('.') for path, _ in dff_paths]
for module_name, inst_name in module_inst_paths:
ff = circuit.get_module(module_name).get_instance(inst_name)
if ff.parameters["WIDTH"] == 1:
ffs.add(ff)
print(f"Found {len(ffs)} flip-flop objects with a data width of 1 bit out of a total of {len(dff_paths)} flip-flops.")
- To insert and connect a new structure, the interface of the structure previously located there must be extracted first.
- With regard to the Clock-Gate, this is applicable to the port names of the new structure that correspond to the original structure.
- This means the port names of the Clock-Gate must be mapped to ports of the original flip-flop.
- In this specific case, the flip-flop itself remains in the circuit, but its connections are modified, so that it no longer receives its clock signal from the original source, but rather as a gated signal generated by the inserted clock gate.
- Execute the cell below to set the mappings accordingly.
In [ ]:
Copied!
# These are the connections that should be copied from the DFF and set at the Clock-Gate instance
cg_to_dff_map = {
# Key: Port name of the Clock-Gate
# Value: Corresponding port name of the DFF
"D": "D",
"Q": "Q",
"EN": "EN",
"CLK_I": "CLK"
}
# This is the Clock-Gate-to-Flip-Flop connection, where the original connection should be overwritten
new_connection = {
# Key: Port name of the Clock-Gate
# Value: Corresponding port name of the DFF
"GC_O": "CLK"
}
print("These are the ports, where the original connection of the flip-flop should be copied and set at the Clock-Gate instance:")
for port_name_cg, port_name_dff in cg_to_dff_map.items():
print(f"\tPort {port_name_cg} of the Clock-Gate will be connected to the wire at Port {port_name_dff} of the flip-flop.")
print("This is the port of the Clock-Gate, which will now drive a port of the DFF:")
for port_name_cg, port_name_dff in new_connection.items():
print(f"\tPort {port_name_cg} of the Clock-Gate will now drive Port {port_name_dff} of the flip-flop.")
# These are the connections that should be copied from the DFF and set at the Clock-Gate instance
cg_to_dff_map = {
# Key: Port name of the Clock-Gate
# Value: Corresponding port name of the DFF
"D": "D",
"Q": "Q",
"EN": "EN",
"CLK_I": "CLK"
}
# This is the Clock-Gate-to-Flip-Flop connection, where the original connection should be overwritten
new_connection = {
# Key: Port name of the Clock-Gate
# Value: Corresponding port name of the DFF
"GC_O": "CLK"
}
print("These are the ports, where the original connection of the flip-flop should be copied and set at the Clock-Gate instance:")
for port_name_cg, port_name_dff in cg_to_dff_map.items():
print(f"\tPort {port_name_cg} of the Clock-Gate will be connected to the wire at Port {port_name_dff} of the flip-flop.")
print("This is the port of the Clock-Gate, which will now drive a port of the DFF:")
for port_name_cg, port_name_dff in new_connection.items():
print(f"\tPort {port_name_cg} of the Clock-Gate will now drive Port {port_name_dff} of the flip-flop.")
- For each matching flip-flop, a clock gate instance must be implemented.
- Each clock gate instance must be connected to the flip-flop accordingly, where each input of the clock gate is connected to a corresponding port of the flip-flop, as specified in the
cg_to_dff_mapdictionary. - The output port of the clock gate instance must then also be connected to the clock port of the flip-flop, replacing the former clock input signal.
- Execute the cell below to let Jupyter know the definition of the function that implements and connects the clock-gates.
In [ ]:
Copied!
def add_single_clock_gate(module: Module, ff_inst: Instance):
# Create a new Clock-Gate instance with a matching interface
cg_name = ff_inst.name + "_CG"
cg_inst = module.create_instance(cg_module, cg_name)
# Copying connections from the flip-flop to the Clock-Gate as specified in the cg_to_dff_map mapping
for cg_port_name, dff_port_name in cg_to_dff_map.items():
dff_port = ff_inst.ports[dff_port_name]
for idx, seg in dff_port:
cg_inst.connect_modify(cg_port_name, seg.ws_path, dff_port.direction, idx)
# Adding wire and connecting the gated clock signal to the flip-flop
for cg_port_name, dff_port_name in new_connection.items():
dff_port = ff_inst.ports[dff_port_name]
for idx, seg in dff_port:
module.disconnect(ff_inst.ports[dff_port_name])
module.connect(cg_inst.ports[cg_port_name], ff_inst.ports[dff_port_name])
# In case the flip-flops does not have an enable signal connected to its port, tie the signal at the enable port to 1
if cg_inst.ports["EN"][0].is_unconnected:
cg_inst.ports["EN"][0].tie_signal('1')
def add_single_clock_gate(module: Module, ff_inst: Instance):
# Create a new Clock-Gate instance with a matching interface
cg_name = ff_inst.name + "_CG"
cg_inst = module.create_instance(cg_module, cg_name)
# Copying connections from the flip-flop to the Clock-Gate as specified in the cg_to_dff_map mapping
for cg_port_name, dff_port_name in cg_to_dff_map.items():
dff_port = ff_inst.ports[dff_port_name]
for idx, seg in dff_port:
cg_inst.connect_modify(cg_port_name, seg.ws_path, dff_port.direction, idx)
# Adding wire and connecting the gated clock signal to the flip-flop
for cg_port_name, dff_port_name in new_connection.items():
dff_port = ff_inst.ports[dff_port_name]
for idx, seg in dff_port:
module.disconnect(ff_inst.ports[dff_port_name])
module.connect(cg_inst.ports[cg_port_name], ff_inst.ports[dff_port_name])
# In case the flip-flops does not have an enable signal connected to its port, tie the signal at the enable port to 1
if cg_inst.ports["EN"][0].is_unconnected:
cg_inst.ports["EN"][0].tie_signal('1')
- Iterate over all found flip-flops and implement a clock gate instance for each flip-flop.
- Execute the cell below to add a clock gate instance to each flip-flop of the circuit design.
In [ ]:
Copied!
for ff in ffs:
# Get the module name, which is the first element in the ff's instance path
module_name = ff.path.get(0)
module = circuit.get_module(module_name)
add_single_clock_gate(module, ff)
for ff in ffs:
# Get the module name, which is the first element in the ff's instance path
module_name = ff.path.get(0)
module = circuit.get_module(module_name)
add_single_clock_gate(module, ff)
- Finally, add the clock gate module to the circuit design, such that the implemented instances also have a module implementation.
- Execute the cell below to add the created clock gate module to the circuit design.
In [ ]:
Copied!
circuit.add_module(cg_module)
circuit.add_module(cg_module)
- After all modifications are finished, the circuit object can be transformed back to a Verilog representation.
- Execute the cell below to transform the circuit object into a textual representation in Verilog syntax and write it into a file.
In [ ]:
Copied!
netlist_carpentry.write(circuit, "output/openMSP430_with_simple_cgs.v", overwrite=True)
netlist_carpentry.write(circuit, "output/openMSP430_with_simple_cgs.v", overwrite=True)
Executing a Logical Equivalence Check on the Optimized Circuit design¶
- To prove the logical equivalence of the original design and the optimized design, the EQY tool is used once again.
- Execute the cell below to generate a
.eqyscript, which is then executed using EQY to prove the logical equivalence of both designs. - The output of the EQY tool is printed to the console.
Warning: Currently, EQY is unable to prove equivalence, which is probably due to a bug in the EQY partitioning algorithm.
EQY is unable under certain circumstances to match the clock signal of the original design to the clock signal in the design with clock gates.
This is indicated by the fact that EQY is not able to map the clock of the original design to the clock of the modified design, probably because EQY is sometimes confused by the gated clock signal.
In [ ]:
Copied!
from netlist_carpentry.scripts.equivalence_checking import EquivalenceChecking
base_name = 'openMSP430'
original_file_path = f'output/{base_name}_unchanged.v'
modified_file_path = f'output/{base_name}_with_simple_cgs.v'
eqy_dir = f'{base_name}/eqy'
eqy_script_path = f'{eqy_dir}/generated_eqy_script.eqy'
eqy_tool_wrapper = EquivalenceChecking([original_file_path], base_name, [modified_file_path], base_name, eqy_script_path)
eqy_tool_wrapper.run_eqy(f"{eqy_dir}/out", overwrite=True)
from netlist_carpentry.scripts.equivalence_checking import EquivalenceChecking
base_name = 'openMSP430'
original_file_path = f'output/{base_name}_unchanged.v'
modified_file_path = f'output/{base_name}_with_simple_cgs.v'
eqy_dir = f'{base_name}/eqy'
eqy_script_path = f'{eqy_dir}/generated_eqy_script.eqy'
eqy_tool_wrapper = EquivalenceChecking([original_file_path], base_name, [modified_file_path], base_name, eqy_script_path)
eqy_tool_wrapper.run_eqy(f"{eqy_dir}/out", overwrite=True)