nortl 1.4.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.
- nortl/__init__.py +85 -0
- nortl/components/__init__.py +8 -0
- nortl/components/channel.py +132 -0
- nortl/components/timer.py +73 -0
- nortl/core/__init__.py +40 -0
- nortl/core/checker.py +135 -0
- nortl/core/common/__init__.py +4 -0
- nortl/core/common/access.py +25 -0
- nortl/core/common/debug.py +6 -0
- nortl/core/common/naming_helper.py +33 -0
- nortl/core/constructs/__init__.py +13 -0
- nortl/core/constructs/condition.py +143 -0
- nortl/core/constructs/fork_join.py +84 -0
- nortl/core/constructs/loop.py +138 -0
- nortl/core/engine.py +575 -0
- nortl/core/exceptions.py +139 -0
- nortl/core/manager/__init__.py +6 -0
- nortl/core/manager/scratch_manager.py +128 -0
- nortl/core/manager/signal_manager.py +71 -0
- nortl/core/modifiers.py +136 -0
- nortl/core/module.py +181 -0
- nortl/core/operations.py +834 -0
- nortl/core/parameter.py +88 -0
- nortl/core/process.py +451 -0
- nortl/core/protocols.py +628 -0
- nortl/core/renderers/__init__.py +0 -0
- nortl/core/renderers/operations/__init__.py +34 -0
- nortl/core/renderers/operations/arithmetics.py +38 -0
- nortl/core/renderers/operations/base.py +111 -0
- nortl/core/renderers/operations/comparison.py +44 -0
- nortl/core/renderers/operations/logic.py +38 -0
- nortl/core/renderers/operations/misc.py +26 -0
- nortl/core/renderers/operations/slice.py +30 -0
- nortl/core/signal.py +878 -0
- nortl/core/state.py +201 -0
- nortl/py.typed +0 -0
- nortl/renderer/__init__.py +5 -0
- nortl/renderer/mermaid_renderer.py +38 -0
- nortl/renderer/networkx_renderer.py +29 -0
- nortl/renderer/verilog_renderer.py +325 -0
- nortl/renderer/verilog_utils/__init__.py +6 -0
- nortl/renderer/verilog_utils/formatter.py +29 -0
- nortl/renderer/verilog_utils/process.py +226 -0
- nortl/renderer/verilog_utils/structural.py +146 -0
- nortl/renderer/verilog_utils/utils.py +23 -0
- nortl/utils/__init__.py +0 -0
- nortl/utils/parse_utils.py +37 -0
- nortl/utils/templates/testbench.sv +41 -0
- nortl/utils/test_wrapper.py +218 -0
- nortl/utils/type_aliases.py +15 -0
- nortl/verilog_library/__init__.py +74 -0
- nortl/verilog_library/nortl_clock_gate.sv +20 -0
- nortl/verilog_library/nortl_count_down_timer.sv +50 -0
- nortl/verilog_library/nortl_delay.sv +66 -0
- nortl/verilog_library/nortl_edge_detector.sv +34 -0
- nortl/verilog_library/nortl_sync.sv +28 -0
- nortl-1.4.0.dist-info/METADATA +105 -0
- nortl-1.4.0.dist-info/RECORD +60 -0
- nortl-1.4.0.dist-info/WHEEL +4 -0
- nortl-1.4.0.dist-info/licenses/LICENSE +11 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
from typing import Deque, Dict, List, Literal, Optional, Tuple, Union
|
|
3
|
+
|
|
4
|
+
from .utils import VerilogRenderable, to_verilog_renderable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VerilogAssignment:
|
|
8
|
+
def __init__(self, tgt: Union[str, VerilogRenderable], src: Union[str, VerilogRenderable]) -> None:
|
|
9
|
+
self.tgt = to_verilog_renderable(tgt)
|
|
10
|
+
self.src = to_verilog_renderable(src)
|
|
11
|
+
self.operator = '<='
|
|
12
|
+
|
|
13
|
+
def render(self) -> str:
|
|
14
|
+
return f'{self.tgt} {self.operator} {self.src};'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VerilogBlock:
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self.items: Deque[VerilogRenderable] = deque([])
|
|
20
|
+
|
|
21
|
+
def render(self) -> str:
|
|
22
|
+
content: List[str] = ['begin']
|
|
23
|
+
content.append(self.render_items())
|
|
24
|
+
content.append('end')
|
|
25
|
+
|
|
26
|
+
return '\n'.join(content)
|
|
27
|
+
|
|
28
|
+
def render_items(self) -> str:
|
|
29
|
+
content: List[str] = []
|
|
30
|
+
content.extend(item.render() for item in self.items)
|
|
31
|
+
return '\n'.join(content)
|
|
32
|
+
|
|
33
|
+
def add(self, item: VerilogRenderable) -> None:
|
|
34
|
+
self.items.append(item)
|
|
35
|
+
|
|
36
|
+
def __len__(self) -> int:
|
|
37
|
+
return len(self.items)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class VerilogCase:
|
|
41
|
+
def __init__(self, tgt_signal: str) -> None:
|
|
42
|
+
self.tgt_signal = tgt_signal
|
|
43
|
+
self.cases: Dict[str, VerilogBlock] = {}
|
|
44
|
+
self.compress_output = True
|
|
45
|
+
|
|
46
|
+
def add_case(self, value: str, block: Optional[VerilogBlock] = None) -> None:
|
|
47
|
+
if block is None:
|
|
48
|
+
self.cases[value] = VerilogBlock()
|
|
49
|
+
else:
|
|
50
|
+
self.cases[value] = block
|
|
51
|
+
|
|
52
|
+
def add_item(self, case: str, item: VerilogRenderable) -> None:
|
|
53
|
+
if case not in self.cases:
|
|
54
|
+
self.add_case(case)
|
|
55
|
+
|
|
56
|
+
self.cases[case].add(item)
|
|
57
|
+
|
|
58
|
+
def compress_cases(self) -> Dict[str, str]:
|
|
59
|
+
new_cases: Dict[str, str] = {}
|
|
60
|
+
|
|
61
|
+
for case, block in self.cases.items():
|
|
62
|
+
if len(block) != 0:
|
|
63
|
+
old_case = ''
|
|
64
|
+
new_block = block.render()
|
|
65
|
+
|
|
66
|
+
for existing_case, existing_block in new_cases.items():
|
|
67
|
+
if existing_block == new_block:
|
|
68
|
+
old_case = existing_case
|
|
69
|
+
|
|
70
|
+
if old_case == '':
|
|
71
|
+
new_cases[case] = new_block
|
|
72
|
+
else:
|
|
73
|
+
del new_cases[old_case]
|
|
74
|
+
new_cases[f'{case}, {old_case}'] = new_block
|
|
75
|
+
|
|
76
|
+
return new_cases
|
|
77
|
+
|
|
78
|
+
def render(self) -> str:
|
|
79
|
+
content: List[str] = []
|
|
80
|
+
|
|
81
|
+
if all(len(block) == 0 for block in self.cases.values()):
|
|
82
|
+
return ''
|
|
83
|
+
|
|
84
|
+
content.append(f'case ({self.tgt_signal})')
|
|
85
|
+
|
|
86
|
+
if self.compress_output:
|
|
87
|
+
for case, block in self.compress_cases().items():
|
|
88
|
+
content.append(f'{case}: {block}')
|
|
89
|
+
else:
|
|
90
|
+
for case, vblock in self.cases.items():
|
|
91
|
+
if len(vblock) != 0:
|
|
92
|
+
content.append(f'{case}: {vblock.render()}')
|
|
93
|
+
|
|
94
|
+
content.append('endcase')
|
|
95
|
+
|
|
96
|
+
return '\n'.join(content)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class VerilogIf:
|
|
100
|
+
def __init__(self, condition: Union[VerilogRenderable, str]) -> None:
|
|
101
|
+
self.condition: VerilogRenderable = to_verilog_renderable(condition)
|
|
102
|
+
self.true_branch = VerilogBlock()
|
|
103
|
+
self.false_branch = VerilogBlock()
|
|
104
|
+
|
|
105
|
+
def render(self) -> str:
|
|
106
|
+
if self.condition.render() == '1':
|
|
107
|
+
return self.true_branch.render_items()
|
|
108
|
+
|
|
109
|
+
content = [f'if ({self.condition})']
|
|
110
|
+
content.append(self.true_branch.render())
|
|
111
|
+
|
|
112
|
+
if len(self.false_branch) > 0:
|
|
113
|
+
content.append('else ')
|
|
114
|
+
content.append(self.false_branch.render())
|
|
115
|
+
|
|
116
|
+
return '\n'.join(content)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class VerilogProcess:
|
|
120
|
+
def __init__(self) -> None:
|
|
121
|
+
self.sensitivity: List[Tuple[Literal['posedge', 'negedge'], VerilogRenderable]] = []
|
|
122
|
+
self.content = VerilogBlock()
|
|
123
|
+
self.render_sensitivity = True
|
|
124
|
+
self.block_label = 'always'
|
|
125
|
+
|
|
126
|
+
def _add(self, item: VerilogRenderable) -> None:
|
|
127
|
+
self.content.add(item)
|
|
128
|
+
|
|
129
|
+
def add_sensitivity(self, edge: Literal['posedge', 'negedge'], signal: VerilogRenderable) -> None:
|
|
130
|
+
self.sensitivity.append((edge, signal))
|
|
131
|
+
|
|
132
|
+
def render(self) -> str:
|
|
133
|
+
content = []
|
|
134
|
+
|
|
135
|
+
line = f'{self.block_label}'
|
|
136
|
+
|
|
137
|
+
if self.render_sensitivity:
|
|
138
|
+
if self.sensitivity == []:
|
|
139
|
+
line = f'{line} @(*)'
|
|
140
|
+
else:
|
|
141
|
+
sens_lst = []
|
|
142
|
+
for edge, signal in self.sensitivity:
|
|
143
|
+
sens_lst.append(f'{edge} {signal}')
|
|
144
|
+
line = f'{line} @({" or ".join(sens_lst)})'
|
|
145
|
+
|
|
146
|
+
content.append(line)
|
|
147
|
+
|
|
148
|
+
content.append(self.content.render())
|
|
149
|
+
|
|
150
|
+
return '\n'.join(content)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class AlwaysFF(VerilogProcess):
|
|
154
|
+
def __init__(self, clk: Union[VerilogRenderable, str], reset: Union[VerilogRenderable, str]) -> None:
|
|
155
|
+
super().__init__()
|
|
156
|
+
self.clk = clk
|
|
157
|
+
self.reset = reset
|
|
158
|
+
|
|
159
|
+
self.behavior = VerilogIf(to_verilog_renderable(reset))
|
|
160
|
+
self._add(self.behavior)
|
|
161
|
+
|
|
162
|
+
self.block_label = 'always_ff'
|
|
163
|
+
|
|
164
|
+
def add_reset(self, item: VerilogRenderable) -> None:
|
|
165
|
+
self.behavior.true_branch.add(item)
|
|
166
|
+
|
|
167
|
+
def add(self, item: VerilogRenderable) -> None:
|
|
168
|
+
self.behavior.false_branch.add(item)
|
|
169
|
+
|
|
170
|
+
def render(self) -> str:
|
|
171
|
+
self.add_sensitivity('posedge', to_verilog_renderable(self.clk))
|
|
172
|
+
self.add_sensitivity('posedge', to_verilog_renderable(self.reset))
|
|
173
|
+
|
|
174
|
+
return super().render()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class AlwaysComb(VerilogProcess):
|
|
178
|
+
def __init__(self) -> None:
|
|
179
|
+
super().__init__()
|
|
180
|
+
self.block_label = 'always_comb'
|
|
181
|
+
self.render_sensitivity = False
|
|
182
|
+
|
|
183
|
+
def add(self, item: VerilogRenderable) -> None:
|
|
184
|
+
self._add(item)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class AlwaysLatch(VerilogProcess):
|
|
188
|
+
def __init__(self, latch_en: VerilogRenderable) -> None:
|
|
189
|
+
super().__init__()
|
|
190
|
+
|
|
191
|
+
self.behavior = VerilogIf(latch_en)
|
|
192
|
+
self._add(self.behavior)
|
|
193
|
+
|
|
194
|
+
self.block_label = 'always_latch'
|
|
195
|
+
self.render_sensitivity = False
|
|
196
|
+
|
|
197
|
+
def add(self, item: VerilogRenderable) -> None:
|
|
198
|
+
self.behavior.true_branch.add(item)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class VerilogPrint:
|
|
202
|
+
def __init__(self, line: str, *args: str) -> None:
|
|
203
|
+
self.line = line
|
|
204
|
+
self.args = args
|
|
205
|
+
|
|
206
|
+
def render(self) -> str:
|
|
207
|
+
line = f'$display("{self.line}", {",".join(self.args)});'
|
|
208
|
+
return line
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class VerilogPrintf:
|
|
212
|
+
def __init__(self, fname: str, line: str, *args: str) -> None:
|
|
213
|
+
self.fname = fname
|
|
214
|
+
self.line = line
|
|
215
|
+
self.args = args
|
|
216
|
+
|
|
217
|
+
def render(self) -> str:
|
|
218
|
+
ret = []
|
|
219
|
+
ret.append('begin')
|
|
220
|
+
ret.append('int fd;')
|
|
221
|
+
ret.append(f'fd = $fopen({self.fname}, "w+");')
|
|
222
|
+
ret.append(f'$fdisplay("{self.line}", {",".join(self.args)});')
|
|
223
|
+
ret.append('$fclose(fd);')
|
|
224
|
+
ret.append('end')
|
|
225
|
+
|
|
226
|
+
return '\n'.join(ret)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
from .formatter import VerilogFormatter
|
|
4
|
+
from .utils import VerilogRenderable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VerilogDeclaration:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
verilog_type: str,
|
|
11
|
+
name: Union[str, List[str]],
|
|
12
|
+
width: Union[int, str, VerilogRenderable] = 0,
|
|
13
|
+
connections: Optional[Dict[str, str]] = None,
|
|
14
|
+
params: Optional[Dict[str, str]] = None,
|
|
15
|
+
members: Optional[Union[List[str], Dict[str, int]]] = None,
|
|
16
|
+
):
|
|
17
|
+
self.verilog_type = verilog_type
|
|
18
|
+
self.name = name
|
|
19
|
+
self.connections = connections
|
|
20
|
+
self.params = params
|
|
21
|
+
self.members = members
|
|
22
|
+
self.width = width
|
|
23
|
+
|
|
24
|
+
def add_member(self, name: str, value: Optional[Union[int]] = None) -> None:
|
|
25
|
+
if self.members is None:
|
|
26
|
+
if value is not None:
|
|
27
|
+
self.members = {}
|
|
28
|
+
else:
|
|
29
|
+
self.members = []
|
|
30
|
+
|
|
31
|
+
if isinstance(self.members, list):
|
|
32
|
+
if value is not None:
|
|
33
|
+
raise RuntimeError('Cannot add an encoded member to an enum without all items to be encoded!')
|
|
34
|
+
self.members.append(name)
|
|
35
|
+
else:
|
|
36
|
+
if value is None:
|
|
37
|
+
raise RuntimeError('Cannot add an un-encoded member to an enum where all items are encoded!')
|
|
38
|
+
self.members[name] = value
|
|
39
|
+
|
|
40
|
+
def add_parameter(self, name: str, value: Union[str, int]) -> None:
|
|
41
|
+
if self.params is None:
|
|
42
|
+
self.params = {}
|
|
43
|
+
|
|
44
|
+
if isinstance(value, int):
|
|
45
|
+
self.params[name] = str(value)
|
|
46
|
+
else:
|
|
47
|
+
self.params[name] = value
|
|
48
|
+
|
|
49
|
+
def add_connection(self, src: str, tgt: str) -> None:
|
|
50
|
+
if self.connections is None:
|
|
51
|
+
self.connections = {}
|
|
52
|
+
|
|
53
|
+
self.connections[src] = tgt
|
|
54
|
+
|
|
55
|
+
def render(self) -> str: # noqa: C901
|
|
56
|
+
content: List[str] = []
|
|
57
|
+
connection_lst = []
|
|
58
|
+
param_lst = []
|
|
59
|
+
|
|
60
|
+
if self.connections is not None:
|
|
61
|
+
connection_lst = [f'.{x}({y})' for x, y in self.connections.items()]
|
|
62
|
+
if self.params is not None:
|
|
63
|
+
param_lst = [f'.{x}({y})' for x, y in self.params.items()]
|
|
64
|
+
|
|
65
|
+
name_str = ''
|
|
66
|
+
|
|
67
|
+
content.append(self.verilog_type)
|
|
68
|
+
|
|
69
|
+
if isinstance(self.name, list):
|
|
70
|
+
name_str = ', '.join(self.name)
|
|
71
|
+
else:
|
|
72
|
+
name_str = self.name
|
|
73
|
+
|
|
74
|
+
if isinstance(self.width, int):
|
|
75
|
+
if self.width > 1:
|
|
76
|
+
content.append(f'[{self.width - 1}:0]')
|
|
77
|
+
else:
|
|
78
|
+
content.append(f'[{self.width}-1:0]')
|
|
79
|
+
|
|
80
|
+
if self.verilog_type.startswith('enum'):
|
|
81
|
+
if self.members is None:
|
|
82
|
+
raise RuntimeError(f'Tried to create enum {name_str} without any values. Something is wrong here.')
|
|
83
|
+
|
|
84
|
+
if isinstance(self.members, List):
|
|
85
|
+
item_str = ', '.join(self.members)
|
|
86
|
+
else:
|
|
87
|
+
item_str = ', '.join([f'{item} = {value}' for item, value in self.members.items()])
|
|
88
|
+
|
|
89
|
+
content.append(f'{{{item_str}}}')
|
|
90
|
+
|
|
91
|
+
if self.params is not None:
|
|
92
|
+
content.append(f'#({", ".join(param_lst)})')
|
|
93
|
+
|
|
94
|
+
content.append(name_str)
|
|
95
|
+
|
|
96
|
+
if not any(t in self.verilog_type for t in ['wire', 'logic', 'reg']) and not self.verilog_type.startswith(
|
|
97
|
+
'enum'
|
|
98
|
+
): # other net types are out of scope for now
|
|
99
|
+
content.append(f'({", ".join(connection_lst)})')
|
|
100
|
+
|
|
101
|
+
return ' '.join(content)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class VerilogModule:
|
|
105
|
+
def __init__(self, name: str) -> None:
|
|
106
|
+
self.name = name
|
|
107
|
+
self.ports: List[VerilogDeclaration] = []
|
|
108
|
+
self.parameters: Dict[str, Optional[Union[int, str]]] = {}
|
|
109
|
+
self.signals: List[VerilogDeclaration] = []
|
|
110
|
+
self.instances: List[VerilogDeclaration] = []
|
|
111
|
+
self.functionals: List[VerilogRenderable] = []
|
|
112
|
+
|
|
113
|
+
def render(self) -> str:
|
|
114
|
+
content = []
|
|
115
|
+
line = f'module {self.name} '
|
|
116
|
+
|
|
117
|
+
if len(self.parameters) != 0:
|
|
118
|
+
params = []
|
|
119
|
+
for p, val in self.parameters.items():
|
|
120
|
+
if val is None:
|
|
121
|
+
params.append(f'parameter {p}')
|
|
122
|
+
else:
|
|
123
|
+
params.append(f'parameter {p} = {val}')
|
|
124
|
+
|
|
125
|
+
line = f'{line} #(\n{",\n".join(params)}) '
|
|
126
|
+
|
|
127
|
+
ports = [p.render() for p in self.ports]
|
|
128
|
+
|
|
129
|
+
line = f'{line} ({",\n".join(ports)});'
|
|
130
|
+
|
|
131
|
+
content.append(line)
|
|
132
|
+
|
|
133
|
+
content.extend([item.render() + ';' for item in self.signals + self.instances])
|
|
134
|
+
content.extend([item.render() for item in self.functionals])
|
|
135
|
+
|
|
136
|
+
content.append('endmodule')
|
|
137
|
+
|
|
138
|
+
code = '\n'.join(content)
|
|
139
|
+
|
|
140
|
+
return VerilogFormatter(code).format()
|
|
141
|
+
|
|
142
|
+
def get_instance(self, name: str) -> VerilogDeclaration:
|
|
143
|
+
for item in self.instances:
|
|
144
|
+
if item.name == name:
|
|
145
|
+
return item
|
|
146
|
+
raise RuntimeError(f'Could not find an instance named {name}.')
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import Protocol, Union
|
|
2
|
+
|
|
3
|
+
from nortl.core.operations import RawText
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VerilogRenderable(Protocol):
|
|
7
|
+
"""Protocol for Verilog Renderable.
|
|
8
|
+
|
|
9
|
+
Compared to the [nortl.core.Renderable][nortl.core.operations.Renderable], it doesn't require the [OperationTrait Mixin][nortl.core.operations.OperationTrait] and `target` argument for `render()`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def render(self) -> str: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def to_verilog_renderable(value: Union[str, VerilogRenderable]) -> VerilogRenderable:
|
|
16
|
+
"""Convert value to Verilog Renderable.
|
|
17
|
+
|
|
18
|
+
Compared to [nortl.core.to_renderable][nortl.core.operations.to_renderable], this also supports strings.
|
|
19
|
+
"""
|
|
20
|
+
if isinstance(value, str):
|
|
21
|
+
return RawText(value)
|
|
22
|
+
else:
|
|
23
|
+
return value
|
nortl/utils/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Optional, Tuple
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
'parse_int',
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_int(string: str) -> Tuple[int, Optional[int]]:
|
|
9
|
+
"""Parses an integer from a string.
|
|
10
|
+
|
|
11
|
+
For numbers in binary, octal or hexadecimal representation, it infers the width.
|
|
12
|
+
|
|
13
|
+
Arguments:
|
|
14
|
+
string: Integer value as a string.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Tuple of the value and inferred width.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
# Parse binary, octal or hexadecimal number
|
|
22
|
+
if string.startswith('0b'):
|
|
23
|
+
value = int(string, 2)
|
|
24
|
+
width = len(string.split('0b', maxsplit=1)[1])
|
|
25
|
+
elif string.startswith('0o'):
|
|
26
|
+
value = int(string, 8)
|
|
27
|
+
width = len(string.split('0o', maxsplit=1)[1]) * 3
|
|
28
|
+
elif string.startswith('0x'):
|
|
29
|
+
value = int(string, 16)
|
|
30
|
+
width = len(string.split('0x', maxsplit=1)[1]) * 4
|
|
31
|
+
else:
|
|
32
|
+
value = int(string)
|
|
33
|
+
width = None
|
|
34
|
+
except ValueError:
|
|
35
|
+
raise ValueError(f"Unable to create Const from '{string}', failed to convert it to integer.")
|
|
36
|
+
|
|
37
|
+
return value, width
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module testbench();
|
|
2
|
+
|
|
3
|
+
logic CLK, RST;
|
|
4
|
+
|
|
5
|
+
initial begin
|
|
6
|
+
CLK = 0;
|
|
7
|
+
while(1)
|
|
8
|
+
begin
|
|
9
|
+
#(10);
|
|
10
|
+
CLK = ~CLK;
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
logic finish, passed, timeout;
|
|
15
|
+
|
|
16
|
+
my_engine DUT(
|
|
17
|
+
.RST_ASYNC_I(RST),
|
|
18
|
+
.CLK_I(CLK),
|
|
19
|
+
.timeout(timeout),
|
|
20
|
+
.passed(passed),
|
|
21
|
+
.finish(finish)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
initial begin
|
|
25
|
+
RST = 1;
|
|
26
|
+
#(100);
|
|
27
|
+
RST = 0;
|
|
28
|
+
|
|
29
|
+
@(posedge finish);
|
|
30
|
+
|
|
31
|
+
// variable dump section
|
|
32
|
+
|
|
33
|
+
$display("timeout=%d;\n", timeout);
|
|
34
|
+
$display("errors=%d;\n", DUT.error_ctr);
|
|
35
|
+
$display("passed=%d;\n", passed);
|
|
36
|
+
|
|
37
|
+
$finish();
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
endmodule
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import subprocess
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from inspect import currentframe, getframeinfo
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from tempfile import TemporaryDirectory
|
|
7
|
+
from types import FrameType
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from nortl.core import CoreEngine
|
|
12
|
+
from nortl.core.constructs import Condition, ElseCondition, Fork
|
|
13
|
+
from nortl.core.modifiers import UnregisteredRead
|
|
14
|
+
from nortl.core.operations import to_renderable
|
|
15
|
+
from nortl.core.protocols import Renderable
|
|
16
|
+
from nortl.renderer.verilog_renderer import VerilogRenderer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NoRTLTestBase(ABC):
|
|
20
|
+
"""Base class for noRTL tests with Verilog simulation.
|
|
21
|
+
|
|
22
|
+
Subclasses must implement:
|
|
23
|
+
- build_engine(): Create and configure the engine to be tested
|
|
24
|
+
- generate_testbench(): Generate the Verilog testbench file
|
|
25
|
+
|
|
26
|
+
The simulation is automatically run before each test method.
|
|
27
|
+
Test methods can access:
|
|
28
|
+
- self.engine: The engine instance
|
|
29
|
+
- self.simulation_result: The subprocess result from simulation
|
|
30
|
+
- self.simulation_output: Parsed simulation output (string)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def init_sequence(self) -> CoreEngine:
|
|
35
|
+
"""This function must initialize the engine that is to be tested. In the most trivial case, this means, that an instance of CoreEngine is created.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
CoreEngine: Initialized engine
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def verify_final_state(self, engine: CoreEngine) -> None:
|
|
43
|
+
"""This state-sequence is called after the dut and testbench threads have ended. It can be used to verify the final state of the dut."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def the_testbench(self, engine: CoreEngine) -> None:
|
|
47
|
+
"""The state sequence that is run in parallel to the engine-under-Test. This may use the self.assert* functions to verify the behavior.
|
|
48
|
+
|
|
49
|
+
Arguments:
|
|
50
|
+
engine (CoreEngine): engine instance
|
|
51
|
+
"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def dut(self, engine: CoreEngine) -> None:
|
|
56
|
+
"""The dut function is the state sequence that is to be tested. It is executed in parallel to the testbench.
|
|
57
|
+
|
|
58
|
+
A call to self.assert* is not forbidden but not advised to create a good seperation between dut and test code.
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def get_test_engine(self) -> CoreEngine:
|
|
63
|
+
self.engine = self.init_sequence()
|
|
64
|
+
|
|
65
|
+
self.finish_flag = self.engine.define_output('finish', 1, 0)
|
|
66
|
+
self.state_flag = self.engine.define_output('passed', 1, 0)
|
|
67
|
+
self.error_ctr = self.engine.define_local('error_ctr', 16, 0)
|
|
68
|
+
self.timeout = self.engine.define_output('timeout', 1, 0)
|
|
69
|
+
ctr = self.engine.define_local('timeout_ctr', 16, 0)
|
|
70
|
+
|
|
71
|
+
self.engine.sync()
|
|
72
|
+
|
|
73
|
+
# FIXME: Introduce Timeout-timer
|
|
74
|
+
with Fork(self.engine, 'DUT') as t_dut:
|
|
75
|
+
self.dut(self.engine)
|
|
76
|
+
with Fork(self.engine, 'testbench') as t_tb:
|
|
77
|
+
self.the_testbench(self.engine)
|
|
78
|
+
with Fork(self.engine, 'timeout') as t_timeout:
|
|
79
|
+
self.engine.sync()
|
|
80
|
+
self.engine.set(ctr, ctr + 1)
|
|
81
|
+
with Condition(self.engine, ctr == 0xFFFF):
|
|
82
|
+
self.engine.set(self.timeout, 1)
|
|
83
|
+
t_timeout.finish()
|
|
84
|
+
|
|
85
|
+
self.engine.sync()
|
|
86
|
+
|
|
87
|
+
self.engine.wait_for((t_dut.finished & t_tb.finished) | t_timeout.finished)
|
|
88
|
+
|
|
89
|
+
with Condition(self.engine, t_timeout.finished):
|
|
90
|
+
t_dut.cancel()
|
|
91
|
+
t_tb.cancel()
|
|
92
|
+
if not hasattr(self, 'expect_timeout'):
|
|
93
|
+
self.assertTrue(0) # Show that we had a timeout
|
|
94
|
+
else:
|
|
95
|
+
self.assertTrue(self.expect_timeout)
|
|
96
|
+
with ElseCondition(self.engine):
|
|
97
|
+
t_timeout.cancel()
|
|
98
|
+
|
|
99
|
+
self.verify_final_state(self.engine)
|
|
100
|
+
|
|
101
|
+
self.engine.sync()
|
|
102
|
+
|
|
103
|
+
self.finish_simulation()
|
|
104
|
+
|
|
105
|
+
return self.engine
|
|
106
|
+
|
|
107
|
+
def increment_error_ctr(self) -> None:
|
|
108
|
+
self.engine.sync()
|
|
109
|
+
|
|
110
|
+
with Condition(self.engine, self.error_ctr != 0xFFFF):
|
|
111
|
+
self.engine.set(self.error_ctr, self.error_ctr + 1)
|
|
112
|
+
with ElseCondition(self.engine):
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
self.engine.sync()
|
|
116
|
+
|
|
117
|
+
def finish_simulation(self) -> None:
|
|
118
|
+
self.engine.sync()
|
|
119
|
+
|
|
120
|
+
with Condition(self.engine, self.error_ctr == 0):
|
|
121
|
+
self.engine.set(self.state_flag, 1)
|
|
122
|
+
with ElseCondition(self.engine):
|
|
123
|
+
self.engine.set(self.state_flag, 0)
|
|
124
|
+
|
|
125
|
+
self.engine.set(self.finish_flag, 1)
|
|
126
|
+
self.engine.sync()
|
|
127
|
+
|
|
128
|
+
def print_line(self, frame: FrameType) -> None:
|
|
129
|
+
fi = getframeinfo(frame)
|
|
130
|
+
if fi.code_context is None:
|
|
131
|
+
return
|
|
132
|
+
code_context = fi.code_context[0]
|
|
133
|
+
code_context = re.sub('#.*', '', code_context)
|
|
134
|
+
code_context = code_context.lstrip().rstrip()
|
|
135
|
+
self.engine.print(f'Assertion \\"{code_context}\\" failed at {fi.filename}:{frame.f_lineno}')
|
|
136
|
+
|
|
137
|
+
def assertTrue(self, condition: Renderable | int) -> None: # noqa: N802
|
|
138
|
+
with Condition(self.engine, UnregisteredRead(~to_renderable(condition))):
|
|
139
|
+
frame = currentframe()
|
|
140
|
+
if frame is not None:
|
|
141
|
+
frame = frame.f_back
|
|
142
|
+
if frame is not None:
|
|
143
|
+
self.print_line(frame)
|
|
144
|
+
self.increment_error_ctr()
|
|
145
|
+
self.finish_simulation()
|
|
146
|
+
with ElseCondition(self.engine):
|
|
147
|
+
pass
|
|
148
|
+
self.engine.sync()
|
|
149
|
+
|
|
150
|
+
def assertEqual(self, value1: Renderable | int, value2: Renderable | int) -> None: # noqa: N802
|
|
151
|
+
with Condition(self.engine, UnregisteredRead(to_renderable(value1) != to_renderable(value2))):
|
|
152
|
+
frame = currentframe()
|
|
153
|
+
if frame is not None:
|
|
154
|
+
frame = frame.f_back
|
|
155
|
+
if frame is not None:
|
|
156
|
+
self.print_line(frame)
|
|
157
|
+
|
|
158
|
+
self.engine.print('Left-hand side: 0x%h', to_renderable(value1))
|
|
159
|
+
self.engine.print('Right-hand side: 0x%h', to_renderable(value2))
|
|
160
|
+
self.increment_error_ctr()
|
|
161
|
+
self.finish_simulation()
|
|
162
|
+
with ElseCondition(self.engine):
|
|
163
|
+
pass
|
|
164
|
+
self.engine.sync()
|
|
165
|
+
|
|
166
|
+
def test_compile_and_run(self) -> None:
|
|
167
|
+
"""Automatically executed fixture.
|
|
168
|
+
|
|
169
|
+
Process:
|
|
170
|
+
1. Builds the engine
|
|
171
|
+
2. Generates and compiles Verilog
|
|
172
|
+
3. Runs the simulation
|
|
173
|
+
4. Makes results available to test methods
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
# Create engine from derived class
|
|
177
|
+
self.engine = self.get_test_engine()
|
|
178
|
+
|
|
179
|
+
with TemporaryDirectory() as tempdir_str:
|
|
180
|
+
tmp_path = Path(tempdir_str)
|
|
181
|
+
|
|
182
|
+
# Generate Verilog files
|
|
183
|
+
self.verilog_file: Path = tmp_path / 'engine.sv'
|
|
184
|
+
self.testbench_file: Path = Path(__file__).parent / 'templates' / 'testbench.sv'
|
|
185
|
+
|
|
186
|
+
renderer = VerilogRenderer(self.engine)
|
|
187
|
+
with open(self.verilog_file, 'w') as fptr:
|
|
188
|
+
fptr.write(renderer.render())
|
|
189
|
+
|
|
190
|
+
# Compile with Icarus Verilog
|
|
191
|
+
self.compiled_file: Path = tmp_path / 'engine.vvp'
|
|
192
|
+
compile_result = subprocess.run(
|
|
193
|
+
['iverilog', '-g2005-sv', '-o', str(self.compiled_file), str(self.verilog_file), str(self.testbench_file)],
|
|
194
|
+
capture_output=True,
|
|
195
|
+
text=True,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if compile_result.returncode != 0:
|
|
199
|
+
pytest.fail(f'Verilog compilation failed:\n{compile_result.stderr}')
|
|
200
|
+
|
|
201
|
+
# Run simulation automatically
|
|
202
|
+
self._run_simulation()
|
|
203
|
+
self.simulation_output: str = self.simulation_result.stdout
|
|
204
|
+
|
|
205
|
+
res = re.findall(r'passed=\s*\d+', self.simulation_output)[0]
|
|
206
|
+
print(self.simulation_output)
|
|
207
|
+
|
|
208
|
+
pass_state = int(res.split('=')[1]) == 1
|
|
209
|
+
print(pass_state)
|
|
210
|
+
|
|
211
|
+
if not pass_state:
|
|
212
|
+
pytest.fail('Verilog simulation discovered errors')
|
|
213
|
+
|
|
214
|
+
def _run_simulation(self) -> None:
|
|
215
|
+
"""Internal method to execute the simulation."""
|
|
216
|
+
cmd = ['vvp', str(self.compiled_file)]
|
|
217
|
+
|
|
218
|
+
self.simulation_result = subprocess.run(cmd, capture_output=True, text=True)
|