digsim-logic-simulator 0.22.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- digsim/__init__.py +6 -0
- digsim/app/__main__.py +12 -0
- digsim/app/cli.py +68 -0
- digsim/app/gui/__init__.py +6 -0
- digsim/app/gui/_circuit_area.py +468 -0
- digsim/app/gui/_component_selection.py +154 -0
- digsim/app/gui/_main_window.py +163 -0
- digsim/app/gui/_top_bar.py +339 -0
- digsim/app/gui/_utils.py +26 -0
- digsim/app/gui/_warning_dialog.py +46 -0
- digsim/app/gui_objects/__init__.py +7 -0
- digsim/app/gui_objects/_bus_bit_object.py +94 -0
- digsim/app/gui_objects/_buzzer_object.py +97 -0
- digsim/app/gui_objects/_component_context_menu.py +79 -0
- digsim/app/gui_objects/_component_object.py +374 -0
- digsim/app/gui_objects/_component_port_item.py +63 -0
- digsim/app/gui_objects/_dip_switch_object.py +104 -0
- digsim/app/gui_objects/_gui_note_object.py +80 -0
- digsim/app/gui_objects/_gui_object_factory.py +80 -0
- digsim/app/gui_objects/_hexdigit_object.py +53 -0
- digsim/app/gui_objects/_image_objects.py +239 -0
- digsim/app/gui_objects/_label_object.py +97 -0
- digsim/app/gui_objects/_logic_analyzer_object.py +86 -0
- digsim/app/gui_objects/_seven_segment_object.py +131 -0
- digsim/app/gui_objects/_shortcut_objects.py +82 -0
- digsim/app/gui_objects/_yosys_object.py +32 -0
- digsim/app/gui_objects/images/AND.png +0 -0
- digsim/app/gui_objects/images/Analyzer.png +0 -0
- digsim/app/gui_objects/images/BUF.png +0 -0
- digsim/app/gui_objects/images/Buzzer.png +0 -0
- digsim/app/gui_objects/images/Clock.png +0 -0
- digsim/app/gui_objects/images/DFF.png +0 -0
- digsim/app/gui_objects/images/DIP_SWITCH.png +0 -0
- digsim/app/gui_objects/images/FlipFlop.png +0 -0
- digsim/app/gui_objects/images/IC.png +0 -0
- digsim/app/gui_objects/images/LED_OFF.png +0 -0
- digsim/app/gui_objects/images/LED_ON.png +0 -0
- digsim/app/gui_objects/images/MUX.png +0 -0
- digsim/app/gui_objects/images/NAND.png +0 -0
- digsim/app/gui_objects/images/NOR.png +0 -0
- digsim/app/gui_objects/images/NOT.png +0 -0
- digsim/app/gui_objects/images/ONE.png +0 -0
- digsim/app/gui_objects/images/OR.png +0 -0
- digsim/app/gui_objects/images/PB.png +0 -0
- digsim/app/gui_objects/images/Switch_OFF.png +0 -0
- digsim/app/gui_objects/images/Switch_ON.png +0 -0
- digsim/app/gui_objects/images/XNOR.png +0 -0
- digsim/app/gui_objects/images/XOR.png +0 -0
- digsim/app/gui_objects/images/YOSYS.png +0 -0
- digsim/app/gui_objects/images/ZERO.png +0 -0
- digsim/app/images/app_icon.png +0 -0
- digsim/app/model/__init__.py +6 -0
- digsim/app/model/_model.py +210 -0
- digsim/app/model/_model_components.py +162 -0
- digsim/app/model/_model_new_wire.py +57 -0
- digsim/app/model/_model_objects.py +155 -0
- digsim/app/model/_model_settings.py +35 -0
- digsim/app/model/_model_shortcuts.py +72 -0
- digsim/app/settings/__init__.py +8 -0
- digsim/app/settings/_component_settings.py +415 -0
- digsim/app/settings/_gui_settings.py +71 -0
- digsim/app/settings/_shortcut_dialog.py +39 -0
- digsim/circuit/__init__.py +7 -0
- digsim/circuit/_circuit.py +329 -0
- digsim/circuit/_waves_writer.py +61 -0
- digsim/circuit/components/__init__.py +26 -0
- digsim/circuit/components/_bus_bits.py +68 -0
- digsim/circuit/components/_button.py +44 -0
- digsim/circuit/components/_buzzer.py +45 -0
- digsim/circuit/components/_clock.py +54 -0
- digsim/circuit/components/_dip_switch.py +73 -0
- digsim/circuit/components/_flip_flops.py +99 -0
- digsim/circuit/components/_gates.py +246 -0
- digsim/circuit/components/_hexdigit.py +82 -0
- digsim/circuit/components/_ic.py +36 -0
- digsim/circuit/components/_label_wire.py +167 -0
- digsim/circuit/components/_led.py +18 -0
- digsim/circuit/components/_logic_analyzer.py +60 -0
- digsim/circuit/components/_mem64kbyte.py +42 -0
- digsim/circuit/components/_memstdout.py +37 -0
- digsim/circuit/components/_note.py +25 -0
- digsim/circuit/components/_on_off_switch.py +54 -0
- digsim/circuit/components/_seven_segment.py +28 -0
- digsim/circuit/components/_static_level.py +28 -0
- digsim/circuit/components/_static_value.py +44 -0
- digsim/circuit/components/_yosys_atoms.py +1353 -0
- digsim/circuit/components/_yosys_component.py +232 -0
- digsim/circuit/components/atoms/__init__.py +23 -0
- digsim/circuit/components/atoms/_component.py +280 -0
- digsim/circuit/components/atoms/_digsim_exception.py +8 -0
- digsim/circuit/components/atoms/_port.py +398 -0
- digsim/circuit/components/ic/74162.json +1331 -0
- digsim/circuit/components/ic/7448.json +834 -0
- digsim/storage_model/__init__.py +7 -0
- digsim/storage_model/_app.py +58 -0
- digsim/storage_model/_circuit.py +126 -0
- digsim/synth/__init__.py +6 -0
- digsim/synth/__main__.py +67 -0
- digsim/synth/_synthesis.py +156 -0
- digsim/utils/__init__.py +6 -0
- digsim/utils/_yosys_netlist.py +134 -0
- digsim_logic_simulator-0.22.0.dist-info/METADATA +140 -0
- digsim_logic_simulator-0.22.0.dist-info/RECORD +107 -0
- digsim_logic_simulator-0.22.0.dist-info/WHEEL +5 -0
- digsim_logic_simulator-0.22.0.dist-info/entry_points.txt +2 -0
- digsim_logic_simulator-0.22.0.dist-info/licenses/LICENSE.md +32 -0
- digsim_logic_simulator-0.22.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2024
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""All classes within digsim.storage_model namespace"""
|
|
5
|
+
|
|
6
|
+
from ._app import AppFileDataClass, GuiPositionDataClass, ModelDataClass
|
|
7
|
+
from ._circuit import CircuitDataClass, CircuitFileDataClass, ComponentDataClass, WireDataClass
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module that handles the dataclasses for application
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import asdict
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
from pydantic.dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from ._circuit import CircuitDataClass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class GuiPositionDataClass:
|
|
22
|
+
x: int = 100
|
|
23
|
+
y: int = 100
|
|
24
|
+
z: int = 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class AppFileDataClass:
|
|
29
|
+
circuit: CircuitDataClass
|
|
30
|
+
gui: dict[str, GuiPositionDataClass] = Field(default_factory=dict)
|
|
31
|
+
shortcuts: dict[str, str] = Field(default_factory=dict)
|
|
32
|
+
settings: dict[str, Any] = Field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def load(filename):
|
|
36
|
+
try:
|
|
37
|
+
with open(filename, mode="r", encoding="utf-8") as json_file:
|
|
38
|
+
app_filedata_class = AppFileDataClass(**json.load(json_file))
|
|
39
|
+
except json.JSONDecodeError as exc:
|
|
40
|
+
raise ValueError(f"Malformed JSON file: {filename} - {exc}") from exc
|
|
41
|
+
except FileNotFoundError as exc:
|
|
42
|
+
raise FileNotFoundError(f"File not found: {filename}") from exc
|
|
43
|
+
return app_filedata_class
|
|
44
|
+
|
|
45
|
+
def save(self, filename):
|
|
46
|
+
json_object = json.dumps(asdict(self), indent=4)
|
|
47
|
+
with open(filename, mode="w", encoding="utf-8") as json_file:
|
|
48
|
+
json_file.write(json_object)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ModelDataClass:
|
|
53
|
+
circuit: CircuitDataClass
|
|
54
|
+
gui: dict[str, GuiPositionDataClass]
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def from_app_file_dc(app_file_dc):
|
|
58
|
+
return ModelDataClass(circuit=app_file_dc.circuit, gui=app_file_dc.gui)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module that handles the dataclasses for circuit load/save
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import asdict
|
|
13
|
+
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
from pydantic.dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class WireDataClass:
|
|
20
|
+
src: str
|
|
21
|
+
dst: str
|
|
22
|
+
|
|
23
|
+
def connect(self, circuit):
|
|
24
|
+
src_comp_name, src_port_name = self.src.split(".")
|
|
25
|
+
dst_comp_name, dst_port_name = self.dst.split(".")
|
|
26
|
+
src_comp = circuit.get_component(src_comp_name)
|
|
27
|
+
dst_comp = circuit.get_component(dst_comp_name)
|
|
28
|
+
src_comp.port(src_port_name).wire = dst_comp.port(dst_port_name)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def list_from_port(cls, src_port):
|
|
32
|
+
wires = []
|
|
33
|
+
for port in src_port.wired_ports:
|
|
34
|
+
# Only add port on top-level components
|
|
35
|
+
if port.parent().is_toplevel():
|
|
36
|
+
wires.append(
|
|
37
|
+
WireDataClass(
|
|
38
|
+
src=f"{src_port.parent().name()}.{src_port.name()}",
|
|
39
|
+
dst=f"{port.parent().name()}.{port.name()}",
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
return wires
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ComponentDataClass:
|
|
47
|
+
"""Component data class"""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
type: str
|
|
51
|
+
display_name: str = Field(default="")
|
|
52
|
+
settings: dict = Field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
def create(self, circuit):
|
|
55
|
+
"""Factory: Create a component from a dict"""
|
|
56
|
+
if "path" in self.settings:
|
|
57
|
+
self.settings["path"] = circuit.load_path(self.settings["path"])
|
|
58
|
+
|
|
59
|
+
py_module_name = ".".join(self.type.split(".")[0:-1])
|
|
60
|
+
py_class_name = self.type.split(".")[-1]
|
|
61
|
+
|
|
62
|
+
module = importlib.import_module(py_module_name)
|
|
63
|
+
class_ = getattr(module, py_class_name)
|
|
64
|
+
component = class_(circuit=circuit, **self.settings)
|
|
65
|
+
component.set_name(self.name)
|
|
66
|
+
if self.display_name is not None:
|
|
67
|
+
component.set_display_name(self.display_name)
|
|
68
|
+
return component
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def from_component(component):
|
|
72
|
+
"""Return the component information as a dict, used when storing a circuit"""
|
|
73
|
+
module_split = type(component).__module__.split(".")
|
|
74
|
+
type_str = ""
|
|
75
|
+
for module in module_split:
|
|
76
|
+
if not module.startswith("_"):
|
|
77
|
+
type_str += f"{module}."
|
|
78
|
+
type_str += type(component).__name__
|
|
79
|
+
|
|
80
|
+
return ComponentDataClass(
|
|
81
|
+
name=component.name(),
|
|
82
|
+
display_name=component.display_name(),
|
|
83
|
+
type=type_str,
|
|
84
|
+
settings=component.settings_to_dict(),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class CircuitDataClass:
|
|
90
|
+
name: str = "unnamed"
|
|
91
|
+
components: list[ComponentDataClass] = Field(default_factory=list)
|
|
92
|
+
wires: list[WireDataClass] = Field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def from_circuit(circuit):
|
|
96
|
+
dc = CircuitDataClass(name=circuit.name)
|
|
97
|
+
toplevel_components = circuit.get_toplevel_components()
|
|
98
|
+
for comp in toplevel_components:
|
|
99
|
+
dc.components.append(ComponentDataClass.from_component(comp))
|
|
100
|
+
|
|
101
|
+
for comp in toplevel_components:
|
|
102
|
+
for port in comp.ports:
|
|
103
|
+
dc.wires.extend(WireDataClass.list_from_port(port))
|
|
104
|
+
|
|
105
|
+
return dc
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class CircuitFileDataClass:
|
|
110
|
+
circuit: CircuitDataClass
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def load(filename):
|
|
114
|
+
try:
|
|
115
|
+
with open(filename, mode="r", encoding="utf-8") as json_file:
|
|
116
|
+
dc = CircuitFileDataClass(**json.load(json_file))
|
|
117
|
+
except json.JSONDecodeError as exc:
|
|
118
|
+
raise ValueError(f"Malformed JSON file: {filename} - {exc}") from exc
|
|
119
|
+
except FileNotFoundError as exc:
|
|
120
|
+
raise FileNotFoundError(f"File not found: {filename}") from exc
|
|
121
|
+
return dc
|
|
122
|
+
|
|
123
|
+
def save(self, filename):
|
|
124
|
+
json_object = json.dumps(asdict(self), indent=4)
|
|
125
|
+
with open(filename, mode="w", encoding="utf-8") as json_file:
|
|
126
|
+
json_file.write(json_object)
|
digsim/synth/__init__.py
ADDED
digsim/synth/__main__.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""The main class module of the digsim.synth namespace"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from . import Synthesis, SynthesisException
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _synth_modules(args):
|
|
14
|
+
print("Synthesis started...")
|
|
15
|
+
for infile in args.input_files:
|
|
16
|
+
print(f" - Reading {infile}")
|
|
17
|
+
print(f"Generating {args.output_file}...")
|
|
18
|
+
start_time = time.monotonic()
|
|
19
|
+
synthesis = Synthesis(args.input_files, args.top)
|
|
20
|
+
try:
|
|
21
|
+
synthesis.synth_to_json_file(args.output_file, silent=args.silent)
|
|
22
|
+
print(f"Synthesis complete in {time.monotonic() - start_time:.2f}s")
|
|
23
|
+
except SynthesisException as exc:
|
|
24
|
+
print(f"ERROR: {str(exc)}")
|
|
25
|
+
return -1
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _list_modules(args):
|
|
30
|
+
try:
|
|
31
|
+
modules = Synthesis.list_modules(args.input_files)
|
|
32
|
+
print("Modules:")
|
|
33
|
+
print("========")
|
|
34
|
+
for idx, module in enumerate(modules):
|
|
35
|
+
print(f"{idx}: {module}")
|
|
36
|
+
print("========")
|
|
37
|
+
except SynthesisException as exc:
|
|
38
|
+
print(f"ERROR: {str(exc)}")
|
|
39
|
+
return -1
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
parser = argparse.ArgumentParser("Yosys synthesizer helper")
|
|
45
|
+
subparser = parser.add_subparsers(required=True)
|
|
46
|
+
synth_parser = subparser.add_parser("synth")
|
|
47
|
+
synth_parser.add_argument(
|
|
48
|
+
"--input-files", "-i", type=str, nargs="+", required=True, help="The verilog input files"
|
|
49
|
+
)
|
|
50
|
+
synth_parser.add_argument(
|
|
51
|
+
"--output-file", "-o", type=str, required=True, help="The json output file"
|
|
52
|
+
)
|
|
53
|
+
synth_parser.add_argument(
|
|
54
|
+
"--top", "-t", type=str, required=True, help="The verilog top module"
|
|
55
|
+
)
|
|
56
|
+
synth_parser.add_argument(
|
|
57
|
+
"--silent", "-s", action="store_true", help="Silent the yosys output"
|
|
58
|
+
)
|
|
59
|
+
synth_parser.set_defaults(func=_synth_modules)
|
|
60
|
+
list_parser = subparser.add_parser("list")
|
|
61
|
+
list_parser.add_argument(
|
|
62
|
+
"--input-files", "-i", type=str, nargs="+", required=True, help="The verilog input files"
|
|
63
|
+
)
|
|
64
|
+
list_parser.set_defaults(func=_list_modules)
|
|
65
|
+
arguments = parser.parse_args()
|
|
66
|
+
returncode = arguments.func(arguments)
|
|
67
|
+
sys.exit(returncode)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""Helper module for yosys synthesis"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import pathlib
|
|
8
|
+
import shutil
|
|
9
|
+
import site
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
import pexpect
|
|
13
|
+
import pexpect.popen_spawn
|
|
14
|
+
|
|
15
|
+
from digsim.circuit.components.atoms import DigsimException
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SynthesisException(DigsimException):
|
|
19
|
+
"""Exception class for yosys synthesis"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Synthesis:
|
|
23
|
+
"""Helper class for yosys synthesis"""
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def _pexpect_wait_for_prompt(cls, pexp):
|
|
27
|
+
index = pexp.expect(["yosys>", pexpect.EOF])
|
|
28
|
+
before_lines = pexp.before.decode("utf8").replace("\r", "").split("\n")
|
|
29
|
+
if index == 0:
|
|
30
|
+
pass
|
|
31
|
+
elif index == 1:
|
|
32
|
+
# Unexpected EOF means ERROR
|
|
33
|
+
errorline = "ERROR"
|
|
34
|
+
for line in before_lines:
|
|
35
|
+
if "ERROR" in line:
|
|
36
|
+
errorline = line
|
|
37
|
+
break
|
|
38
|
+
raise SynthesisException(errorline)
|
|
39
|
+
|
|
40
|
+
# Remove escape sequence in output
|
|
41
|
+
out_lines = []
|
|
42
|
+
for line in before_lines:
|
|
43
|
+
if line.startswith("\x1b"):
|
|
44
|
+
continue
|
|
45
|
+
out_lines.append(line)
|
|
46
|
+
return out_lines
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def _find_win_yowasp_yosys_binary():
|
|
50
|
+
try:
|
|
51
|
+
# Use getusersitepackages if this is present, as it ensures that the
|
|
52
|
+
# value is initialised properly.
|
|
53
|
+
user_site = site.getusersitepackages()
|
|
54
|
+
except AttributeError:
|
|
55
|
+
user_site = site.USER_SITE
|
|
56
|
+
scripts_path = pathlib.Path(user_site).parent / "Scripts"
|
|
57
|
+
yowasp_yosys_path = scripts_path / "yowasp-yosys.exe"
|
|
58
|
+
if yowasp_yosys_path.is_file():
|
|
59
|
+
return str(yowasp_yosys_path)
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def _pexpect_spawn_yosys(cls):
|
|
64
|
+
# Find linux binary
|
|
65
|
+
yosys_exe = shutil.which("yosys") or shutil.which("yowasp-yosys")
|
|
66
|
+
|
|
67
|
+
# No binary found
|
|
68
|
+
if yosys_exe is None:
|
|
69
|
+
# Try to find windows binary
|
|
70
|
+
yosys_exe = cls._find_win_yowasp_yosys_binary()
|
|
71
|
+
|
|
72
|
+
if yosys_exe is None:
|
|
73
|
+
raise SynthesisException("Yosys executable not found")
|
|
74
|
+
|
|
75
|
+
if sys.platform == "win32":
|
|
76
|
+
return pexpect.popen_spawn.PopenSpawn(yosys_exe)
|
|
77
|
+
|
|
78
|
+
return pexpect.spawn(yosys_exe)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def list_modules(cls, verilog_files):
|
|
82
|
+
"""List available modules in verilog files"""
|
|
83
|
+
if isinstance(verilog_files, str):
|
|
84
|
+
verilog_files = [verilog_files]
|
|
85
|
+
|
|
86
|
+
pexp = cls._pexpect_spawn_yosys()
|
|
87
|
+
cls._pexpect_wait_for_prompt(pexp)
|
|
88
|
+
pexp.sendline(f"read -sv {' '.join(verilog_files)}")
|
|
89
|
+
cls._pexpect_wait_for_prompt(pexp)
|
|
90
|
+
pexp.sendline("ls")
|
|
91
|
+
pexp.expect("\n")
|
|
92
|
+
ls_response = cls._pexpect_wait_for_prompt(pexp)
|
|
93
|
+
pexp.sendline("exit")
|
|
94
|
+
|
|
95
|
+
modules = []
|
|
96
|
+
for line in ls_response:
|
|
97
|
+
if len(line) == 0:
|
|
98
|
+
continue
|
|
99
|
+
if "modules:" in line:
|
|
100
|
+
continue
|
|
101
|
+
modules.append(line.replace("$abstract\\", "").strip())
|
|
102
|
+
return modules
|
|
103
|
+
|
|
104
|
+
def __init__(self, verilog_files, verilog_top_module):
|
|
105
|
+
if isinstance(verilog_files, str):
|
|
106
|
+
self._verilog_files = [verilog_files]
|
|
107
|
+
else:
|
|
108
|
+
self._verilog_files = verilog_files
|
|
109
|
+
self._verilog_top_module = verilog_top_module
|
|
110
|
+
self._yosys_log = []
|
|
111
|
+
|
|
112
|
+
def synth_to_json(self, silent=False):
|
|
113
|
+
"""Execute yosys with generated synthesis script"""
|
|
114
|
+
script = f"read -sv {' '.join(self._verilog_files)}; "
|
|
115
|
+
script += f"hierarchy -top {self._verilog_top_module}; "
|
|
116
|
+
script += "proc; flatten; "
|
|
117
|
+
script += "memory_dff; "
|
|
118
|
+
script += "proc; opt; techmap; opt; "
|
|
119
|
+
script += f"synth -noabc -top {self._verilog_top_module}; "
|
|
120
|
+
|
|
121
|
+
pexp = self._pexpect_spawn_yosys()
|
|
122
|
+
self._pexpect_wait_for_prompt(pexp)
|
|
123
|
+
pexp.sendline(script)
|
|
124
|
+
yosys_log = self._pexpect_wait_for_prompt(pexp)
|
|
125
|
+
for line in yosys_log:
|
|
126
|
+
self._yosys_log.append(line)
|
|
127
|
+
if silent:
|
|
128
|
+
continue
|
|
129
|
+
print("Yosys:", line)
|
|
130
|
+
pexp.sendline("write_json")
|
|
131
|
+
pexp.expect("Executing JSON backend.")
|
|
132
|
+
json_lines = self._pexpect_wait_for_prompt(pexp)
|
|
133
|
+
pexp.sendline("exit")
|
|
134
|
+
|
|
135
|
+
return "\n".join(json_lines)
|
|
136
|
+
|
|
137
|
+
def synth_to_dict(self, silent=False):
|
|
138
|
+
"""Execute yosys with generated synthesis script and return python dict"""
|
|
139
|
+
yosys_json = self.synth_to_json(silent)
|
|
140
|
+
try:
|
|
141
|
+
netlist_dict = json.loads(yosys_json)
|
|
142
|
+
except json.JSONDecodeError as exc:
|
|
143
|
+
raise SynthesisException(f"Malformed JSON output from Yosys: {exc}") from exc
|
|
144
|
+
return netlist_dict
|
|
145
|
+
|
|
146
|
+
def synth_to_json_file(self, filename, silent=False):
|
|
147
|
+
"""Execute yosys with generated synthesis script and write to file"""
|
|
148
|
+
yosys_json = self.synth_to_json(silent)
|
|
149
|
+
if yosys_json is None:
|
|
150
|
+
raise SynthesisException("Yosys synthesis failed")
|
|
151
|
+
with open(filename, mode="w", encoding="utf-8") as json_file:
|
|
152
|
+
json_file.write(yosys_json)
|
|
153
|
+
|
|
154
|
+
def get_log(self):
|
|
155
|
+
"""Get the yosys output"""
|
|
156
|
+
return self._yosys_log
|
digsim/utils/__init__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module with classes to parse a yosys netlist
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Literal, Optional, Union
|
|
11
|
+
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
from pydantic.dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
BIT_TYPE = list[Union[int, Literal["X"], Literal["x"], Literal["0"], Literal["1"]]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class NetPort:
|
|
21
|
+
parent: Union[YosysModule, YosysCell]
|
|
22
|
+
parent_name: str
|
|
23
|
+
name: str
|
|
24
|
+
bit_index: Optional[int] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Nets:
|
|
29
|
+
source: dict[int, NetPort] = Field(default_factory=dict)
|
|
30
|
+
sinks: dict[int, list[NetPort]] = Field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class YosysPort:
|
|
35
|
+
direction: str
|
|
36
|
+
bits: BIT_TYPE
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_output(self):
|
|
40
|
+
return self.direction == "output"
|
|
41
|
+
|
|
42
|
+
def is_same(self, compare_port):
|
|
43
|
+
return (compare_port.direction == self.direction) and (
|
|
44
|
+
len(compare_port.bits) == len(self.bits)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class YosysCell:
|
|
50
|
+
type: str
|
|
51
|
+
port_directions: dict[str, str] = Field(default_factory=dict)
|
|
52
|
+
connections: dict[str, BIT_TYPE] = Field(default_factory=dict)
|
|
53
|
+
hide_name: int = 0
|
|
54
|
+
parameters: dict[str, Any] = Field(default_factory=dict)
|
|
55
|
+
attributes: dict[str, Any] = Field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
def get_nets(self, name, nets):
|
|
58
|
+
for port_name, net_list in self.connections.items():
|
|
59
|
+
if not net_list:
|
|
60
|
+
# Handle empty net_list, e.g., by skipping or raising an error
|
|
61
|
+
continue
|
|
62
|
+
net = net_list[0]
|
|
63
|
+
port = NetPort(parent=self, parent_name=name, name=port_name)
|
|
64
|
+
if self.port_directions[port_name] == "input":
|
|
65
|
+
if net not in nets.sinks:
|
|
66
|
+
nets.sinks[net] = []
|
|
67
|
+
nets.sinks[net].append(port)
|
|
68
|
+
else:
|
|
69
|
+
nets.source[net] = port
|
|
70
|
+
|
|
71
|
+
def component_name(self, name):
|
|
72
|
+
"""Return a friendly name for a netlist cell"""
|
|
73
|
+
return f"{name.split('$')[-1]}_{self.component_type()}"
|
|
74
|
+
|
|
75
|
+
def component_type(self):
|
|
76
|
+
"""Return a friendly type for a netlist cell"""
|
|
77
|
+
return f"_{self.type[2:-1]}_"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class YosysNetName:
|
|
82
|
+
bits: BIT_TYPE
|
|
83
|
+
attributes: dict[str, Any] = Field(default_factory=dict)
|
|
84
|
+
hide_name: int = 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class YosysModule:
|
|
89
|
+
attributes: dict[str, Any] = Field(default_factory=dict)
|
|
90
|
+
parameter_default_values: dict[str, Any] = Field(default_factory=dict)
|
|
91
|
+
ports: dict[str, YosysPort] = Field(default_factory=dict)
|
|
92
|
+
cells: dict[str, YosysCell] = Field(default_factory=dict)
|
|
93
|
+
netnames: dict[str, YosysNetName] = Field(default_factory=dict)
|
|
94
|
+
|
|
95
|
+
def is_same_interface(self, netlist):
|
|
96
|
+
is_same = True
|
|
97
|
+
if len(netlist.ports) == len(self.ports):
|
|
98
|
+
for netlist_port_name, netlist_port in netlist.ports.items():
|
|
99
|
+
module_port = self.ports.get(netlist_port_name)
|
|
100
|
+
if module_port is None or not module_port.is_same(netlist_port):
|
|
101
|
+
# Port does not exist or has a different bitwidth
|
|
102
|
+
is_same = False
|
|
103
|
+
break
|
|
104
|
+
else:
|
|
105
|
+
# The number of ports does not match
|
|
106
|
+
is_same = False
|
|
107
|
+
return is_same
|
|
108
|
+
|
|
109
|
+
def get_nets(self):
|
|
110
|
+
nets = Nets()
|
|
111
|
+
|
|
112
|
+
for port_name, port_item in self.ports.items():
|
|
113
|
+
for bit_index, net in enumerate(port_item.bits):
|
|
114
|
+
port = NetPort(parent=self, parent_name="top", name=port_name, bit_index=bit_index)
|
|
115
|
+
if port_item.is_output:
|
|
116
|
+
if net not in nets.sinks:
|
|
117
|
+
nets.sinks[net] = []
|
|
118
|
+
nets.sinks[net].append(port)
|
|
119
|
+
else:
|
|
120
|
+
nets.source[net] = port
|
|
121
|
+
|
|
122
|
+
for cell_name, cell in self.cells.items():
|
|
123
|
+
cell.get_nets(cell_name, nets)
|
|
124
|
+
|
|
125
|
+
return nets
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class YosysNetlist:
|
|
130
|
+
creator: Optional[str] = None
|
|
131
|
+
modules: dict[str, YosysModule] = Field(default_factory=dict)
|
|
132
|
+
|
|
133
|
+
def get_modules(self):
|
|
134
|
+
return self.modules
|