garageworks 0.2.0.dev299555241357__tar.gz
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.
- garageworks-0.2.0.dev299555241357/PKG-INFO +10 -0
- garageworks-0.2.0.dev299555241357/garageworks/__init__.py +1 -0
- garageworks-0.2.0.dev299555241357/garageworks/bridge/__init__.py +1 -0
- garageworks-0.2.0.dev299555241357/garageworks/bridge/aved_bridge.py +85 -0
- garageworks-0.2.0.dev299555241357/garageworks/bridge/cocotb_bridge.py +93 -0
- garageworks-0.2.0.dev299555241357/garageworks/bridge/conv_cocotb_to_fpga.py +426 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/__init__.py +3 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/cli.py +293 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/config.py +322 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/jobs.py +282 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/lock.py +111 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/mcp_server.py +134 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/orchestrate.py +190 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/setup_wizard.py +185 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/shell.py +61 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/stages.py +397 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/status.py +429 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/sudoers.py +73 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/templates/Makefile.gw +59 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/templates/target.mk.tmpl +15 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/templates/target_bridge.py +13 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/templates/tb_base.py +12 -0
- garageworks-0.2.0.dev299555241357/garageworks/gwscript/timing.py +199 -0
- garageworks-0.2.0.dev299555241357/garageworks.egg-info/PKG-INFO +10 -0
- garageworks-0.2.0.dev299555241357/garageworks.egg-info/SOURCES.txt +29 -0
- garageworks-0.2.0.dev299555241357/garageworks.egg-info/dependency_links.txt +1 -0
- garageworks-0.2.0.dev299555241357/garageworks.egg-info/entry_points.txt +3 -0
- garageworks-0.2.0.dev299555241357/garageworks.egg-info/requires.txt +6 -0
- garageworks-0.2.0.dev299555241357/garageworks.egg-info/top_level.txt +1 -0
- garageworks-0.2.0.dev299555241357/pyproject.toml +27 -0
- garageworks-0.2.0.dev299555241357/setup.cfg +4 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: garageworks
|
|
3
|
+
Version: 0.2.0.dev299555241357
|
|
4
|
+
Requires-Python: >=3.9
|
|
5
|
+
Requires-Dist: cocotb==1.9.2
|
|
6
|
+
Requires-Dist: cocotb-bus==0.3.0
|
|
7
|
+
Requires-Dist: cocotb-test==0.2.6
|
|
8
|
+
Requires-Dist: cocotbext-axi==0.1.26
|
|
9
|
+
Requires-Dist: libcst==1.8.6
|
|
10
|
+
Requires-Dist: pytest==8.4.2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# garageworks/__init__.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# garageworks/bridge/__init__.py
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# See LICENSE file for details.
|
|
2
|
+
# written by Kaz Yoshii <kazutomo.yoshii@gmail.com>
|
|
3
|
+
|
|
4
|
+
import os, sys, time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import json
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
import pyaved
|
|
11
|
+
|
|
12
|
+
class AVED_Bridge:
|
|
13
|
+
is_fpga = True
|
|
14
|
+
|
|
15
|
+
def readAddrMap(self):
|
|
16
|
+
fn = os.getenv("PARAMFN")
|
|
17
|
+
if fn is None:
|
|
18
|
+
raise RuntimeError("Environment variable PARAMFN is not set")
|
|
19
|
+
|
|
20
|
+
with open(fn) as f:
|
|
21
|
+
data = json.load(f, object_hook=lambda d: SimpleNamespace(**d))
|
|
22
|
+
|
|
23
|
+
return data
|
|
24
|
+
|
|
25
|
+
def aved_write32(self, addr, v):
|
|
26
|
+
self.aved.write32(addr, v)
|
|
27
|
+
|
|
28
|
+
def aved_read32(self, addr):
|
|
29
|
+
v = self.aved.read32(addr)
|
|
30
|
+
return v
|
|
31
|
+
|
|
32
|
+
def __init__(self, base=0x1100000, dev="b1:00.0", logfn = "output.log"):
|
|
33
|
+
self.base = base
|
|
34
|
+
|
|
35
|
+
self.p = self.readAddrMap()
|
|
36
|
+
|
|
37
|
+
self.aved = pyaved.AVED()
|
|
38
|
+
self.aved.open(dev)
|
|
39
|
+
|
|
40
|
+
log_path = Path(logfn).resolve()
|
|
41
|
+
self.log = logging.getLogger("tblog")
|
|
42
|
+
self.log.setLevel(logging.INFO)
|
|
43
|
+
self.log.propagate = False
|
|
44
|
+
self.log.handlers.clear()
|
|
45
|
+
handler = logging.FileHandler(log_path, mode="w")
|
|
46
|
+
handler.setLevel(logging.INFO)
|
|
47
|
+
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
48
|
+
self.log.addHandler(handler)
|
|
49
|
+
|
|
50
|
+
self.log.info("FPGA Simulation Environment Initialized!\n")
|
|
51
|
+
|
|
52
|
+
def writelog(self, txt):
|
|
53
|
+
print(txt, end='')
|
|
54
|
+
with open(self.outputfn, "a") as f:
|
|
55
|
+
f.write(txt)
|
|
56
|
+
|
|
57
|
+
def checkOffset(self, offset, align=8):
|
|
58
|
+
assert (offset % align) == 0, f"offset should be aligned with {align} bytes : {offset}"
|
|
59
|
+
|
|
60
|
+
def writeWord(self, offset, data):
|
|
61
|
+
self.checkOffset(offset, align=4)
|
|
62
|
+
self.aved_write32(self.base + offset, data)
|
|
63
|
+
|
|
64
|
+
def readWord(self, offset):
|
|
65
|
+
self.checkOffset(offset, align=4)
|
|
66
|
+
return self.aved_read32(self.base + offset)
|
|
67
|
+
|
|
68
|
+
def expectWord(self, offset, ref, msg = ""):
|
|
69
|
+
v = self.readWord(offset)
|
|
70
|
+
assert v == ref, msg
|
|
71
|
+
|
|
72
|
+
def softReset(self, maxloopcnt = 1000):
|
|
73
|
+
cycles = self.p.reset_cycles
|
|
74
|
+
self.writeWord(self.p.soft_reset_rw, 1)
|
|
75
|
+
reset_done = 0
|
|
76
|
+
loopcnt = 0
|
|
77
|
+
while reset_done == 0:
|
|
78
|
+
reset_done = self.readWord(self.p.soft_reset_rw)
|
|
79
|
+
if loopcnt > maxloopcnt:
|
|
80
|
+
raise RuntimeError("Reached maxloopcnt. Something wrong")
|
|
81
|
+
loopcnt += 1
|
|
82
|
+
return loopcnt
|
|
83
|
+
|
|
84
|
+
def setup(self):
|
|
85
|
+
pass
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# See LICENSE file for details.
|
|
2
|
+
# written by Kaz Yoshii <kazutomo.yoshii@gmail.com>
|
|
3
|
+
|
|
4
|
+
import os, sys, random
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import json
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
import cocotb_test.simulator, pytest
|
|
11
|
+
import cocotb
|
|
12
|
+
from cocotb.clock import Clock
|
|
13
|
+
from cocotb.triggers import RisingEdge, Timer
|
|
14
|
+
from cocotb.regression import TestFactory
|
|
15
|
+
from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiLiteRam, AxiResp
|
|
16
|
+
import warnings
|
|
17
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
18
|
+
|
|
19
|
+
class COCOTB_Bridge:
|
|
20
|
+
is_fpga = False
|
|
21
|
+
|
|
22
|
+
def readParams(self):
|
|
23
|
+
fn = os.getenv("PARAMFN")
|
|
24
|
+
if fn is None:
|
|
25
|
+
raise RuntimeError("Environment variable PARAMFN is not set")
|
|
26
|
+
|
|
27
|
+
with open(fn) as f:
|
|
28
|
+
data = json.load(f, object_hook=lambda d: SimpleNamespace(**d))
|
|
29
|
+
|
|
30
|
+
return data
|
|
31
|
+
|
|
32
|
+
async def setup(self):
|
|
33
|
+
cocotb.start_soon(Clock(self.dut.s_axi_aclk, 10, units="ns").start())
|
|
34
|
+
|
|
35
|
+
bus = AxiLiteBus.from_prefix(self.dut, "S_AXI")
|
|
36
|
+
self.axi_master = AxiLiteMaster(
|
|
37
|
+
bus,
|
|
38
|
+
self.dut.s_axi_aclk, # lower-case clock
|
|
39
|
+
self.dut.s_axi_aresetn, # lower-case reset
|
|
40
|
+
reset_active_level=0 # because aresetn is active-low
|
|
41
|
+
)
|
|
42
|
+
await self.reset_dut()
|
|
43
|
+
|
|
44
|
+
def __init__(self, dut, logfn = "output.log"):
|
|
45
|
+
self.dut = dut
|
|
46
|
+
|
|
47
|
+
self.p = self.readParams()
|
|
48
|
+
|
|
49
|
+
log_path = Path(logfn).resolve()
|
|
50
|
+
self.log = logging.getLogger("tblog")
|
|
51
|
+
self.log.setLevel(logging.INFO)
|
|
52
|
+
self.log.propagate = False
|
|
53
|
+
self.log.handlers.clear()
|
|
54
|
+
handler = logging.FileHandler(log_path, mode="w")
|
|
55
|
+
handler.setLevel(logging.INFO)
|
|
56
|
+
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
57
|
+
self.log.addHandler(handler)
|
|
58
|
+
|
|
59
|
+
self.log.info("Cocotb Test Initialized!\n")
|
|
60
|
+
|
|
61
|
+
async def writeWord(self, addr, data):
|
|
62
|
+
b = bytearray(data.to_bytes(4, byteorder="little"))
|
|
63
|
+
await self.axi_master.write(addr, b)
|
|
64
|
+
|
|
65
|
+
async def readWord(self, addr):
|
|
66
|
+
b = await self.axi_master.read(addr, 4)
|
|
67
|
+
v = int.from_bytes(b, byteorder="little")
|
|
68
|
+
return v
|
|
69
|
+
|
|
70
|
+
async def expectWord(self, addr, ref, msg = ""):
|
|
71
|
+
v = await self.readWord(addr)
|
|
72
|
+
assert v == ref, msg
|
|
73
|
+
|
|
74
|
+
async def softReset(self, maxloopcnt = 1000):
|
|
75
|
+
cycles = self.p.reset_cycles
|
|
76
|
+
await self.writeWord(self.p.soft_reset_rw, 1)
|
|
77
|
+
reset_done = 0
|
|
78
|
+
loopcnt = 0
|
|
79
|
+
while reset_done == 0:
|
|
80
|
+
reset_done = await self.readWord(self.p.soft_reset_rw)
|
|
81
|
+
if loopcnt > maxloopcnt:
|
|
82
|
+
raise RuntimeError("Reached maxloopcnt. Something wrong")
|
|
83
|
+
loopcnt += 1
|
|
84
|
+
return loopcnt
|
|
85
|
+
|
|
86
|
+
# obsolute below
|
|
87
|
+
async def reset_dut(self, cycles=5):
|
|
88
|
+
self.dut.s_axi_aresetn.value = 0 # assert (active-low)
|
|
89
|
+
for _ in range(cycles):
|
|
90
|
+
await RisingEdge(self.dut.s_axi_aclk)
|
|
91
|
+
self.dut.s_axi_aresetn.value = 1 # deassert
|
|
92
|
+
for _ in range(2):
|
|
93
|
+
await RisingEdge(self.dut.s_axi_aclk)
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Convert a set of AXI cocotb testbench files into FPGA testbench files.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python conv_cocotb_to_fpga.py tb_filter.py r2bridge.py subtest1.py
|
|
7
|
+
|
|
8
|
+
Each input file `foo.py` is written as `fpga_foo.py` (default: same
|
|
9
|
+
directory as input, or --outdir if given). Local imports between files
|
|
10
|
+
in the same batch are rewritten to match (e.g. `from r2bridge import
|
|
11
|
+
R2Bridge` -> `from fpga_r2bridge import R2Bridge`) so the converted
|
|
12
|
+
files import each other correctly.
|
|
13
|
+
|
|
14
|
+
To overwrite existing output files, use the --force option.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import libcst as cst
|
|
23
|
+
import libcst.matchers as m
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def fpga_name(stem: str) -> str:
|
|
27
|
+
"""Map a module/file stem to its converted-output equivalent name."""
|
|
28
|
+
return f"fpga_{stem}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CocotbToFpgaTransformer(cst.CSTTransformer):
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
fpga: str,
|
|
35
|
+
local_stems: frozenset[str] = frozenset(),
|
|
36
|
+
drop_setup: bool = True,
|
|
37
|
+
):
|
|
38
|
+
self.sim = 'COCOTB'
|
|
39
|
+
self.fpga = fpga
|
|
40
|
+
self.local_stems = local_stems
|
|
41
|
+
self.drop_setup = drop_setup
|
|
42
|
+
self.generated_main = False
|
|
43
|
+
self._bridge_class_depth: list[bool] = []
|
|
44
|
+
|
|
45
|
+
def _module_name(self, node: cst.BaseExpression) -> str:
|
|
46
|
+
"""Return dotted module name from LibCST Name/Attribute node."""
|
|
47
|
+
if isinstance(node, cst.Name):
|
|
48
|
+
return node.value
|
|
49
|
+
if isinstance(node, cst.Attribute):
|
|
50
|
+
left = self._module_name(node.value)
|
|
51
|
+
return f"{left}.{node.attr.value}"
|
|
52
|
+
return ""
|
|
53
|
+
|
|
54
|
+
# -------------------------
|
|
55
|
+
# Imports
|
|
56
|
+
# -------------------------
|
|
57
|
+
def leave_Import(
|
|
58
|
+
self,
|
|
59
|
+
original_node: cst.Import,
|
|
60
|
+
updated_node: cst.Import,
|
|
61
|
+
) -> cst.Import | cst.RemovalSentinel:
|
|
62
|
+
kept = []
|
|
63
|
+
for alias in updated_node.names:
|
|
64
|
+
name = alias.name
|
|
65
|
+
# remove: import cocotb
|
|
66
|
+
if isinstance(name, cst.Name) and name.value == "cocotb":
|
|
67
|
+
continue
|
|
68
|
+
kept.append(alias.with_changes(comma=cst.MaybeSentinel.DEFAULT))
|
|
69
|
+
if not kept:
|
|
70
|
+
return cst.RemoveFromParent()
|
|
71
|
+
return updated_node.with_changes(names=kept)
|
|
72
|
+
|
|
73
|
+
def leave_ImportFrom(
|
|
74
|
+
self,
|
|
75
|
+
original_node: cst.ImportFrom,
|
|
76
|
+
updated_node: cst.ImportFrom,
|
|
77
|
+
) -> cst.ImportFrom | cst.RemovalSentinel:
|
|
78
|
+
if updated_node.module is None:
|
|
79
|
+
return updated_node
|
|
80
|
+
|
|
81
|
+
module_code = self._module_name(updated_node.module)
|
|
82
|
+
|
|
83
|
+
# remove: from cocotb... import ...
|
|
84
|
+
if module_code == "cocotb" or module_code.startswith("cocotb."):
|
|
85
|
+
return cst.RemoveFromParent()
|
|
86
|
+
|
|
87
|
+
# e.g.,
|
|
88
|
+
# from garageworks.bridge.cocotb_bridge import COCOTB_Bridge
|
|
89
|
+
# ->
|
|
90
|
+
# from garageworks.bridge.aved_bridge import AVED_Bridge
|
|
91
|
+
if module_code == "garageworks.bridge.cocotb_bridge":
|
|
92
|
+
return updated_node.with_changes(
|
|
93
|
+
module=cst.Attribute(
|
|
94
|
+
value=cst.Attribute(
|
|
95
|
+
value=cst.Name("garageworks"),
|
|
96
|
+
attr=cst.Name("bridge"),
|
|
97
|
+
),
|
|
98
|
+
attr=cst.Name(f"{self.fpga.lower()}_bridge"),
|
|
99
|
+
),
|
|
100
|
+
names=[
|
|
101
|
+
cst.ImportAlias(
|
|
102
|
+
name=cst.Name(f"{self.fpga}_Bridge"),
|
|
103
|
+
comma=cst.MaybeSentinel.DEFAULT,
|
|
104
|
+
)
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Batch-local import rewrite, e.g.
|
|
109
|
+
# from r2bridge import R2Bridge
|
|
110
|
+
# ->
|
|
111
|
+
# from fpga_r2bridge import R2Bridge
|
|
112
|
+
# (also covers helper modules like subtest1.py)
|
|
113
|
+
if module_code in self.local_stems:
|
|
114
|
+
return updated_node.with_changes(
|
|
115
|
+
module=cst.Name(fpga_name(module_code)),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return updated_node
|
|
119
|
+
|
|
120
|
+
# -------------------------
|
|
121
|
+
# async / await conversion
|
|
122
|
+
# -------------------------
|
|
123
|
+
def leave_Await(
|
|
124
|
+
self,
|
|
125
|
+
original_node: cst.Await,
|
|
126
|
+
updated_node: cst.Await,
|
|
127
|
+
) -> cst.BaseExpression:
|
|
128
|
+
# await rev.readWord(...)
|
|
129
|
+
# ->
|
|
130
|
+
# rev.readWord(...)
|
|
131
|
+
return updated_node.expression
|
|
132
|
+
|
|
133
|
+
# -------------------------
|
|
134
|
+
# Function conversion
|
|
135
|
+
# -------------------------
|
|
136
|
+
def leave_FunctionDef(
|
|
137
|
+
self,
|
|
138
|
+
original_node: cst.FunctionDef,
|
|
139
|
+
updated_node: cst.FunctionDef,
|
|
140
|
+
) -> cst.FunctionDef:
|
|
141
|
+
decorators = []
|
|
142
|
+
is_cocotb_test = False
|
|
143
|
+
for dec in updated_node.decorators:
|
|
144
|
+
if self._is_cocotb_test_decorator(dec):
|
|
145
|
+
is_cocotb_test = True
|
|
146
|
+
continue
|
|
147
|
+
decorators.append(dec)
|
|
148
|
+
|
|
149
|
+
# async def -> def
|
|
150
|
+
new_node = updated_node.with_changes(
|
|
151
|
+
asynchronous=None,
|
|
152
|
+
decorators=decorators,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if is_cocotb_test:
|
|
156
|
+
self.generated_main = True
|
|
157
|
+
# async def sim_simple(dut):
|
|
158
|
+
# ->
|
|
159
|
+
# def main():
|
|
160
|
+
new_node = new_node.with_changes(
|
|
161
|
+
name=cst.Name("main"),
|
|
162
|
+
params=cst.Parameters(),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Inside a *Bridge subclass, __init__(self, cocotb_dut) drops the
|
|
166
|
+
# cocotb_dut param, since the FPGA-side bridge takes none.
|
|
167
|
+
in_bridge_class = (
|
|
168
|
+
self._bridge_class_depth and self._bridge_class_depth[-1]
|
|
169
|
+
)
|
|
170
|
+
if in_bridge_class and updated_node.name.value == "__init__":
|
|
171
|
+
params = list(updated_node.params.params)
|
|
172
|
+
if len(params) >= 2:
|
|
173
|
+
# keep self and all params after cocotb_dut
|
|
174
|
+
new_params_list = [params[0]] + params[2:]
|
|
175
|
+
new_params = updated_node.params.with_changes(
|
|
176
|
+
params = new_params_list
|
|
177
|
+
)
|
|
178
|
+
new_node = new_node.with_changes(params=new_params)
|
|
179
|
+
|
|
180
|
+
return new_node
|
|
181
|
+
|
|
182
|
+
def _is_cocotb_test_decorator(self, dec: cst.Decorator) -> bool:
|
|
183
|
+
expr = dec.decorator
|
|
184
|
+
# @cocotb.test
|
|
185
|
+
if m.matches(
|
|
186
|
+
expr,
|
|
187
|
+
m.Attribute(
|
|
188
|
+
value=m.Name("cocotb"),
|
|
189
|
+
attr=m.Name("test"),
|
|
190
|
+
),
|
|
191
|
+
):
|
|
192
|
+
return True
|
|
193
|
+
# @cocotb.test()
|
|
194
|
+
if m.matches(
|
|
195
|
+
expr,
|
|
196
|
+
m.Call(
|
|
197
|
+
func=m.Attribute(
|
|
198
|
+
value=m.Name("cocotb"),
|
|
199
|
+
attr=m.Name("test"),
|
|
200
|
+
)
|
|
201
|
+
),
|
|
202
|
+
):
|
|
203
|
+
return True
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
# -------------------------
|
|
207
|
+
# Class definition: rebase project bridge classes
|
|
208
|
+
# e.g. class R2Bridge(COCOTB_Bridge): -> class R2Bridge(AVED_Bridge):
|
|
209
|
+
# -------------------------
|
|
210
|
+
def _is_bridge_base(self, base_value: cst.BaseExpression) -> bool:
|
|
211
|
+
name = self._module_name(base_value)
|
|
212
|
+
return name == "COCOTB_Bridge" or name.endswith("Bridge")
|
|
213
|
+
|
|
214
|
+
def visit_ClassDef(self, node: cst.ClassDef) -> None:
|
|
215
|
+
is_bridge_class = any(
|
|
216
|
+
self._is_bridge_base(base.value) for base in node.bases
|
|
217
|
+
)
|
|
218
|
+
self._bridge_class_depth.append(is_bridge_class)
|
|
219
|
+
|
|
220
|
+
def leave_ClassDef(
|
|
221
|
+
self,
|
|
222
|
+
original_node: cst.ClassDef,
|
|
223
|
+
updated_node: cst.ClassDef,
|
|
224
|
+
) -> cst.ClassDef:
|
|
225
|
+
self._bridge_class_depth.pop()
|
|
226
|
+
|
|
227
|
+
new_bases = []
|
|
228
|
+
changed = False
|
|
229
|
+
for base in updated_node.bases:
|
|
230
|
+
if m.matches(base.value, m.Name("COCOTB_Bridge")):
|
|
231
|
+
new_bases.append(
|
|
232
|
+
base.with_changes(value=cst.Name(f"{self.fpga}_Bridge"))
|
|
233
|
+
)
|
|
234
|
+
changed = True
|
|
235
|
+
else:
|
|
236
|
+
new_bases.append(base)
|
|
237
|
+
if changed:
|
|
238
|
+
return updated_node.with_changes(bases=new_bases)
|
|
239
|
+
return updated_node
|
|
240
|
+
|
|
241
|
+
# -------------------------
|
|
242
|
+
# Class replacement (generalized to any *Bridge subclass call)
|
|
243
|
+
# -------------------------
|
|
244
|
+
def leave_Call(
|
|
245
|
+
self,
|
|
246
|
+
original_node: cst.Call,
|
|
247
|
+
updated_node: cst.Call,
|
|
248
|
+
) -> cst.Call:
|
|
249
|
+
def drop_cocotb_dut(args):
|
|
250
|
+
args = list(args)
|
|
251
|
+
if (
|
|
252
|
+
args
|
|
253
|
+
and args[0].keyword is None
|
|
254
|
+
and isinstance(args[0].value, cst.Name)
|
|
255
|
+
and args[0].value.value == "cocotb_dut"
|
|
256
|
+
):
|
|
257
|
+
return args[1:]
|
|
258
|
+
return args
|
|
259
|
+
|
|
260
|
+
if isinstance(updated_node.func, cst.Name):
|
|
261
|
+
name = updated_node.func.value
|
|
262
|
+
|
|
263
|
+
# COCOTB_Bridge(cocotb_dut) -> AVED_Bridge()
|
|
264
|
+
if name == "COCOTB_Bridge":
|
|
265
|
+
return updated_node.with_changes(
|
|
266
|
+
func=cst.Name(f"{self.fpga}_Bridge"),
|
|
267
|
+
args=[],
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# R2Bridge(cocotb_dut, ...) -> R2Bridge(...)
|
|
271
|
+
if name.endswith("Bridge") and name != f"{self.fpga}_Bridge":
|
|
272
|
+
return updated_node.with_changes(
|
|
273
|
+
args=drop_cocotb_dut(updated_node.args)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Inside a *Bridge subclass, super().__init__(cocotb_dut, ...) -> super().__init__(...)
|
|
277
|
+
in_bridge_class = (
|
|
278
|
+
self._bridge_class_depth and self._bridge_class_depth[-1]
|
|
279
|
+
)
|
|
280
|
+
if in_bridge_class and m.matches(
|
|
281
|
+
updated_node.func,
|
|
282
|
+
m.Attribute(
|
|
283
|
+
value=m.Call(func=m.Name("super")),
|
|
284
|
+
attr=m.Name("__init__"),
|
|
285
|
+
),
|
|
286
|
+
):
|
|
287
|
+
return updated_node.with_changes(
|
|
288
|
+
args=drop_cocotb_dut(updated_node.args)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return updated_node
|
|
292
|
+
|
|
293
|
+
# -------------------------
|
|
294
|
+
# Optional setup removal
|
|
295
|
+
# -------------------------
|
|
296
|
+
def leave_SimpleStatementLine(
|
|
297
|
+
self,
|
|
298
|
+
original_node: cst.SimpleStatementLine,
|
|
299
|
+
updated_node: cst.SimpleStatementLine,
|
|
300
|
+
) -> cst.SimpleStatementLine | cst.RemovalSentinel:
|
|
301
|
+
if self.drop_setup and len(updated_node.body) == 1:
|
|
302
|
+
stmt = updated_node.body[0]
|
|
303
|
+
# Drop:
|
|
304
|
+
# await rev.setup()
|
|
305
|
+
# After await-removal, this becomes:
|
|
306
|
+
# rev.setup()
|
|
307
|
+
if m.matches(
|
|
308
|
+
stmt,
|
|
309
|
+
m.Expr(
|
|
310
|
+
value=m.Call(
|
|
311
|
+
func=m.Attribute(
|
|
312
|
+
attr=m.Name("setup"),
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
),
|
|
316
|
+
):
|
|
317
|
+
return cst.RemoveFromParent()
|
|
318
|
+
return updated_node
|
|
319
|
+
|
|
320
|
+
# -------------------------
|
|
321
|
+
# Add main entry point
|
|
322
|
+
# -------------------------
|
|
323
|
+
def leave_Module(
|
|
324
|
+
self,
|
|
325
|
+
original_node: cst.Module,
|
|
326
|
+
updated_node: cst.Module,
|
|
327
|
+
) -> cst.Module:
|
|
328
|
+
if not self.generated_main:
|
|
329
|
+
return updated_node
|
|
330
|
+
|
|
331
|
+
main_block = cst.parse_statement(
|
|
332
|
+
'''
|
|
333
|
+
if __name__ == "__main__":
|
|
334
|
+
main()
|
|
335
|
+
'''
|
|
336
|
+
)
|
|
337
|
+
return updated_node.with_changes(
|
|
338
|
+
body=list(updated_node.body) + [main_block]
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def convert_code(
|
|
343
|
+
source: str,
|
|
344
|
+
fpga: str,
|
|
345
|
+
local_stems: frozenset[str] = frozenset(),
|
|
346
|
+
drop_setup: bool = True,
|
|
347
|
+
) -> str:
|
|
348
|
+
module = cst.parse_module(source)
|
|
349
|
+
transformed = module.visit(
|
|
350
|
+
CocotbToFpgaTransformer(
|
|
351
|
+
fpga=fpga,
|
|
352
|
+
local_stems=local_stems,
|
|
353
|
+
drop_setup=drop_setup,
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
return transformed.code
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def main() -> None:
|
|
360
|
+
parser = argparse.ArgumentParser(
|
|
361
|
+
description="Convert a batch of restricted cocotb testbench files "
|
|
362
|
+
"to FPGA testbench files."
|
|
363
|
+
)
|
|
364
|
+
parser.add_argument(
|
|
365
|
+
"inputs",
|
|
366
|
+
nargs="+",
|
|
367
|
+
type=Path,
|
|
368
|
+
help="Input cocotb files, e.g. tb_filter.py r2bridge.py subtest1.py",
|
|
369
|
+
)
|
|
370
|
+
parser.add_argument(
|
|
371
|
+
"--outdir",
|
|
372
|
+
type=Path,
|
|
373
|
+
default=None,
|
|
374
|
+
help="Directory for converted output files "
|
|
375
|
+
"(default: same directory as each input file).",
|
|
376
|
+
)
|
|
377
|
+
parser.add_argument(
|
|
378
|
+
"--fpga",
|
|
379
|
+
default="AVED",
|
|
380
|
+
help="FPGA backend name (default: %(default)s)",
|
|
381
|
+
)
|
|
382
|
+
parser.add_argument(
|
|
383
|
+
"--keep-setup",
|
|
384
|
+
action="store_true",
|
|
385
|
+
help="Keep setup() calls instead of removing them.",
|
|
386
|
+
)
|
|
387
|
+
parser.add_argument(
|
|
388
|
+
"--force",
|
|
389
|
+
action="store_true",
|
|
390
|
+
help="Overwrite output files if they already exist.",
|
|
391
|
+
)
|
|
392
|
+
args = parser.parse_args()
|
|
393
|
+
|
|
394
|
+
# Batch-wide set of local module stems, so local imports between
|
|
395
|
+
# files in this same invocation get rewritten consistently
|
|
396
|
+
# (e.g. "r2bridge", "subtest1").
|
|
397
|
+
local_stems = frozenset(p.stem for p in args.inputs)
|
|
398
|
+
|
|
399
|
+
# Resolve output paths up front and check for collisions before
|
|
400
|
+
# writing anything.
|
|
401
|
+
output_paths = []
|
|
402
|
+
for in_path in args.inputs:
|
|
403
|
+
outdir = args.outdir if args.outdir is not None else in_path.parent
|
|
404
|
+
out_path = outdir / f"{fpga_name(in_path.stem)}{in_path.suffix}"
|
|
405
|
+
if out_path.exists() and not args.force:
|
|
406
|
+
raise FileExistsError(
|
|
407
|
+
f"{out_path} already exists. Use --force to overwrite."
|
|
408
|
+
)
|
|
409
|
+
output_paths.append(out_path)
|
|
410
|
+
|
|
411
|
+
for in_path, out_path in zip(args.inputs, output_paths):
|
|
412
|
+
source = in_path.read_text()
|
|
413
|
+
converted = convert_code(
|
|
414
|
+
source=source,
|
|
415
|
+
fpga=args.fpga,
|
|
416
|
+
local_stems=local_stems,
|
|
417
|
+
drop_setup=False,
|
|
418
|
+
# drop_setup=not args.keep_setup,
|
|
419
|
+
)
|
|
420
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
421
|
+
out_path.write_text(converted)
|
|
422
|
+
print(f"{in_path} -> {out_path}")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
if __name__ == "__main__":
|
|
426
|
+
main()
|