-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathtop_level_cli.py
247 lines (191 loc) · 9.92 KB
/
top_level_cli.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import os
import sys
import shutil
import logging
import tempfile
import argparse
from amaranth import Elaboratable
from amaranth._unused import MustUse
from luna import configure_default_logging
from luna.gateware.platform import get_appropriate_platform, configure_toolchain
from luna_soc.generate import Generate, Introspect
def top_level_cli(fragment, *pos_args, **kwargs):
""" Runs a default CLI that assists in building and running SoC gateware.
If the user's options resulted in the board being programmed, this returns the fragment
that was programmed onto the board. Otherwise, it returns None.
Parameters:
fragment -- The design to be built; or a callable that returns a fragment,
such as a Elaborable type. If the latter is provided, any keyword or positional
arguments not specified here will be passed to this callable.
"""
# Disable UnusedElaboarable warnings until we decide to build things.
# This is sort of cursed, but it keeps us categorically from getting UnusedElaborable warnings
# if we're not actually buliding.
MustUse._MustUse__silence = True
# Configure logging.
configure_default_logging()
logging.getLogger().setLevel(logging.DEBUG)
# If this isn't a fragment directly, interpret it as an object that will build one.
name = fragment.__name__ if callable(fragment) else fragment.__class__.__name__
if callable(fragment):
fragment = fragment(*pos_args, **kwargs)
# Make sure this fragment represents a SoC design.
if not hasattr(fragment, "soc"):
logging.error(f"Provided fragment '{name}' is not a SoC design")
sys.exit(0)
# Configure command arguments.
parser = argparse.ArgumentParser(description=f"Gateware generation/upload script for '{name}' gateware.")
parser.add_argument('--output', '-o', metavar='filename', help="Build and output a bitstream to the given file.")
parser.add_argument('--erase', '-E', action='store_true',
help="Clears the relevant FPGA's flash before performing other options.")
parser.add_argument('--upload', '-U', action='store_true',
help="Uploads the relevant design to the target hardware. Default if no options are provided.")
parser.add_argument('--flash', '-F', action='store_true',
help="Flashes the relevant design to the target hardware's configuration flash.")
parser.add_argument('--dry-run', '-D', action='store_true',
help="When provided as the only option; builds the relevant bitstream without uploading or flashing it.")
parser.add_argument('--keep-files', action='store_true',
help="Keeps the local files in the default `build` folder.")
parser.add_argument('--fpga', metavar='part_number',
help="Overrides build configuration to build for a given FPGA. Useful if no FPGA is connected during build.")
parser.add_argument('--console', metavar="port",
help="Attempts to open a convenience 115200 8N1 UART console on the specified port immediately after uploading.")
# Configure SoC command arguments.
parser.add_argument('--generate-c-header', action='store_true',
help="If provided, a C header file for this design's SoC will be printed to the stdout. Other options ignored.")
parser.add_argument('--generate-ld-script', action='store_true',
help="If provided, a C linker script for design's SoC memory regions will be printed to the stdout. Other options ignored.")
parser.add_argument('--generate-memory-x', action='store_true',
help="If provided, a Rust linker script for design's SoC memory regions will be printed to the stdout. Other options ignored.")
parser.add_argument('--generate-svd', action='store_true',
help="If provided, a SVD description of this design's SoC will be printed to the stdout. Other options ignored.")
parser.add_argument('--get-fw-address', action='store_true',
help="If provided, the utility will print the address firmware should be loaded to to stdout. Other options ignored.")
parser.add_argument('--log-resources', action='store_true',
help="If provided, the utility will print a summary of the design's SoC memory map and interrupts. Other options ignored.")
# Parse command arguments.
args = parser.parse_args()
# If we have no other options set, build and upload the relevant file.
if (args.output is None and not args.flash and not args.erase and not args.dry_run):
args.upload = True
# Once the device is flashed, it will self-reconfigure, so we
# don't need an explicitly upload step; and it implicitly erases
# the flash, so we don't need an erase step.
if args.flash:
args.erase = False
args.upload = False
# Select platform.
platform = get_appropriate_platform()
if platform is None:
logging.error("Failed to identify a supported platform")
sys.exit(1)
# If we have a toolchain override, apply it to our platform.
toolchain = os.getenv("LUNA_TOOLCHAIN")
if toolchain:
platform.toolchain = toolchain
if args.fpga:
platform.device = args.fpga
# If we've been asked to generate a C header, generate -only- that.
if args.generate_c_header:
logging.info("Generating C header for SoC")
Generate(fragment.soc).c_header(platform_name=platform.name, file=None)
sys.exit(0)
# If we've been asked to generate C linker region info, generate -only- that.
if args.generate_ld_script:
logging.info("Generating C linker region info script for SoC")
Generate(fragment.soc).ld_script(file=None)
sys.exit(0)
# If we've been asked to generate Rust linker region info, generate -only- that.
if args.generate_memory_x:
logging.info("Generating Rust linker region info script for SoC")
Generate(fragment.soc).memory_x(file=None)
sys.exit(0)
# If we've been asked to generate a SVD description of the design, generate -only- that.
if args.generate_svd:
logging.info("Generating SVD description for SoC")
Generate(fragment.soc).svd(file=None)
sys.exit(0)
# If we've been asked for the address firmware should be loaded, generate _only_ that.
if args.get_fw_address:
print(f"0x{Introspect(fragment.soc).main_ram_address():08x}")
sys.exit(0)
# If we've been asked to generate a log of the design's resources, generate -only- that.
if args.log_resources:
Introspect(fragment.soc).log_resources()
sys.exit(0)
# If we'be been asked to erase the FPGA's flash before performing other options, do that.
if args.erase:
logging.info("Erasing flash...")
platform.toolchain_erase()
logging.info("Erase complete.")
# Build the relevant files.
build_dir = "build" if args.keep_files else tempfile.mkdtemp()
try:
build(args, fragment, platform, build_dir)
# Return the fragment we're working with, for convenience.
if args.upload or args.flash:
return fragment
# Clean up any directories created during the build process.
finally:
if not args.keep_files:
shutil.rmtree(build_dir)
return None
# - build commands ------------------------------------------------------------
def build(args, fragment, platform, build_dir):
""" Top-level build command. Invokes the build steps for each artifact
to be generated."""
join_text = "and uploading gateware to attached" if args.upload else "for"
logging.info(f"Building {join_text} {platform.name}...")
logging.info(f"Build directory: {build_dir}")
# Allow SoC to perform any pre-elaboration steps it wants.
# This allows it to e.g. build a BIOS or equivalent firmware.
_build_pre(args, fragment, platform, build_dir)
# Generate the design bitstream.
products = _build_gateware(args, fragment, platform, build_dir)
# Perform post-build steps
_build_post(args, fragment, platform, build_dir, products)
def _build_pre(args, fragment, platform, build_dir):
""" Perform any pre-elaboration steps for the design. e.g. build a
BIOS or equivalent firmware."""
# Call SoC build function if it has one
soc_build = getattr(fragment.soc, "build", None)
if callable(soc_build):
logging.info("Performing SoC build steps")
soc_build(name="soc", build_dir=build_dir)
def _build_gateware(args, fragment, platform, build_dir):
""" Build gateware for the design."""
# Configure toolchain.
if not configure_toolchain(platform):
logging.info(f"Failed to configure the toolchain for: {platform.toolchain}")
logging.info(f"Continuing anyway.")
# Now that we're actually building, re-enable Unused warnings.
MustUse._MustUse__silence = False
# Perform the build.
products = platform.build(
fragment,
do_program=args.upload,
build_dir=build_dir
)
# Disable UnusedElaboarable warnings again.
MustUse._MustUse__silence = True
logging.info(f"{'Upload' if args.upload else 'Build'} complete.")
return products
def _build_post(args, fragment, platform, build_dir, products):
""" Perform post-build steps."""
# If we're flashing the FPGA's flash, do so.
if args.flash:
logging.info("Programming flash...")
platform.toolchain_flash(products)
logging.info("Programming complete.")
# If we're outputting a file, write it.
if args.output:
bitstream = products.get("top.bit")
with open(args.output, "wb") as f:
f.write(bitstream)
# If we're expecting a console, open one.
if args.console:
import serial.tools.miniterm
# Clear our arguments, so they're not parsed by miniterm.
del sys.argv[1:]
# Run miniterm with our default port and baudrate.
serial.tools.miniterm.main(default_port=args.console, default_baudrate=115200)