opencos-eda 0.2.51__py3-none-any.whl → 0.2.53__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.
Files changed (43) hide show
  1. opencos/commands/__init__.py +2 -0
  2. opencos/commands/build.py +1 -1
  3. opencos/commands/deps_help.py +259 -0
  4. opencos/commands/export.py +1 -1
  5. opencos/commands/flist.py +3 -0
  6. opencos/commands/lec.py +1 -1
  7. opencos/commands/open.py +2 -0
  8. opencos/commands/proj.py +1 -1
  9. opencos/commands/shell.py +1 -1
  10. opencos/commands/sim.py +32 -8
  11. opencos/commands/synth.py +1 -1
  12. opencos/commands/upload.py +3 -0
  13. opencos/commands/waves.py +13 -2
  14. opencos/deps/defaults.py +1 -0
  15. opencos/deps/deps_file.py +30 -4
  16. opencos/deps/deps_processor.py +72 -2
  17. opencos/deps_schema.py +3 -0
  18. opencos/eda.py +66 -29
  19. opencos/eda_base.py +159 -20
  20. opencos/eda_config.py +2 -2
  21. opencos/eda_config_defaults.yml +83 -3
  22. opencos/eda_extract_targets.py +1 -58
  23. opencos/tests/helpers.py +16 -0
  24. opencos/tests/test_eda.py +13 -2
  25. opencos/tests/test_tools.py +227 -6
  26. opencos/tools/cocotb.py +492 -0
  27. opencos/tools/modelsim_ase.py +67 -51
  28. opencos/tools/quartus.py +638 -0
  29. opencos/tools/questa.py +167 -88
  30. opencos/tools/questa_fse.py +10 -0
  31. opencos/tools/riviera.py +1 -0
  32. opencos/tools/verilator.py +31 -0
  33. opencos/tools/vivado.py +3 -3
  34. opencos/util.py +22 -3
  35. opencos/utils/str_helpers.py +85 -0
  36. opencos/utils/vscode_helper.py +47 -0
  37. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/METADATA +2 -1
  38. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/RECORD +43 -39
  39. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/WHEEL +0 -0
  40. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/entry_points.txt +0 -0
  41. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/licenses/LICENSE +0 -0
  42. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/licenses/LICENSE.spdx +0 -0
  43. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,638 @@
1
+ ''' opencos.tools.quartus - Used by opencos.eda commands with --tool=quartus
2
+
3
+ Contains classes for ToolQuartus, and command handlers for synth, build, flist.
4
+ Used for Intel FPGA synthesis, place & route, and bitstream generation.
5
+ '''
6
+
7
+ # pylint: disable=R0801 # (setting similar, but not identical, self.defines key/value pairs)
8
+
9
+ import os
10
+ import re
11
+ import shlex
12
+ import shutil
13
+ import subprocess
14
+
15
+ from pathlib import Path
16
+
17
+ from opencos import util, eda_base
18
+ from opencos.eda_base import Tool
19
+ from opencos.commands import (
20
+ CommandSynth, CommandBuild, CommandFList, CommandProj, CommandUpload, CommandOpen
21
+ )
22
+
23
+ class ToolQuartus(Tool):
24
+ '''ToolQuartus used by opencos.eda for --tool=quartus'''
25
+
26
+ _TOOL = 'quartus'
27
+ _EXE = 'quartus_sh'
28
+
29
+ quartus_year = None
30
+ quartus_release = None
31
+ quartus_base_path = ''
32
+ quartus_exe = ''
33
+
34
+ def __init__(self, config: dict):
35
+ super().__init__(config=config)
36
+ self.args.update({
37
+ 'part': 'EP4SGX230KF40C2',
38
+ 'family': 'Stratix IV',
39
+ })
40
+ self.args_help.update({
41
+ 'part': 'Device used for commands: synth, build.',
42
+ 'family': 'FPGA family for Quartus (e.g., Stratix IV, Arria 10, etc.)',
43
+ })
44
+
45
+ def get_versions(self) -> str:
46
+ if self._VERSION:
47
+ return self._VERSION
48
+
49
+ path = shutil.which(self._EXE)
50
+ if not path:
51
+ self.error("Quartus not in path, need to install or add to $PATH",
52
+ f"(looked for '{self._EXE}')")
53
+ else:
54
+ self.quartus_exe = path
55
+ self.quartus_base_path, _ = os.path.split(path)
56
+
57
+
58
+
59
+ # Get version based on install path name or by running quartus_sh --version
60
+ util.debug(f"quartus path = {self.quartus_exe}")
61
+ m = re.search(r'(\d+)\.(\d+)', self.quartus_exe)
62
+ if m:
63
+ version = m.group(1) + '.' + m.group(2)
64
+ self._VERSION = version
65
+ else:
66
+ # Try to get version by running quartus_sh --version
67
+ try:
68
+ result = subprocess.run(
69
+ [self.quartus_exe, '--version'],
70
+ capture_output=True, text=True, timeout=10, check=False
71
+ )
72
+ version_match = re.search(r'Version (\d+\.\d+)', result.stdout)
73
+ if version_match:
74
+ self._VERSION = version_match.group(1)
75
+ else:
76
+ self.error("Could not determine Quartus version")
77
+ except (
78
+ subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError
79
+ ):
80
+ self.error("Could not determine Quartus version")
81
+
82
+ if self._VERSION:
83
+ numbers_list = self._VERSION.split('.')
84
+ self.quartus_year = int(numbers_list[0])
85
+ self.quartus_release = int(numbers_list[1])
86
+ else:
87
+ self.error(f"Quartus version not found, quartus path = {self.quartus_exe}")
88
+ return self._VERSION
89
+
90
+ def set_tool_defines(self) -> None:
91
+ self.defines['OC_TOOL_QUARTUS'] = None
92
+ def_year_release = f'OC_TOOL_QUARTUS_{self.quartus_year:02d}_{self.quartus_release:d}'
93
+ self.defines[def_year_release] = None
94
+
95
+ # Code can be conditional on Quartus versions
96
+ versions = ['20.1', '21.1', '22.1', '23.1', '24.1', '25.1']
97
+
98
+ def version_compare(v1, v2):
99
+ v1_parts = [int(x) for x in v1.split('.')]
100
+ v2_parts = [int(x) for x in v2.split('.')]
101
+ l = max(len(v1_parts), len(v2_parts))
102
+ v1_parts += [0] * (l - len(v1_parts))
103
+ v2_parts += [0] * (l - len(v2_parts))
104
+ return (v1_parts > v2_parts) - (v1_parts < v2_parts)
105
+
106
+ for ver in versions:
107
+ str_ver = ver.replace('.', '_')
108
+ cmp = version_compare(self._VERSION, ver)
109
+ if cmp <= 0:
110
+ self.defines[f'OC_TOOL_QUARTUS_{str_ver}_OR_OLDER'] = None
111
+ if cmp >= 0:
112
+ self.defines[f'OC_TOOL_QUARTUS_{str_ver}_OR_NEWER'] = None
113
+
114
+ util.debug(f"Setup tool defines: {self.defines}")
115
+
116
+
117
+ class CommandSynthQuartus(CommandSynth, ToolQuartus):
118
+ '''CommandSynthQuartus is a command handler for: eda synth --tool=quartus'''
119
+
120
+ def __init__(self, config: dict):
121
+ CommandSynth.__init__(self, config)
122
+ ToolQuartus.__init__(self, config=self.config)
123
+ # add args specific to this tool
124
+ self.args.update({
125
+ 'gui': False,
126
+ 'tcl-file': "synth.tcl",
127
+ 'sdc': "",
128
+ 'qsf': "",
129
+ })
130
+ self.args_help.update({
131
+ 'gui': 'Run Quartus in GUI mode',
132
+ 'tcl-file': 'name of TCL file to be created for Quartus',
133
+ 'sdc': 'SDC constraints file',
134
+ 'qsf': 'Quartus Settings File (.qsf)',
135
+ })
136
+
137
+ def do_it(self) -> None:
138
+ CommandSynth.do_it(self)
139
+
140
+ if self.is_export_enabled():
141
+ return
142
+
143
+ # create TCL
144
+ tcl_file = os.path.abspath(
145
+ os.path.join(self.args['work-dir'], self.args['tcl-file'])
146
+ )
147
+
148
+ self.write_tcl_file(tcl_file=tcl_file)
149
+
150
+ # execute Quartus synthesis
151
+ command_list = [
152
+ self.quartus_exe, '-t', tcl_file
153
+ ]
154
+ if not util.args['verbose']:
155
+ command_list.append('-q')
156
+
157
+ # Add artifact tracking
158
+ util.artifacts.add_extension(
159
+ search_paths=self.args['work-dir'], file_extension='qpf',
160
+ typ='project', description='Quartus Project File'
161
+ )
162
+ util.artifacts.add_extension(
163
+ search_paths=self.args['work-dir'], file_extension='qsf',
164
+ typ='project', description='Quartus Settings File'
165
+ )
166
+ util.artifacts.add_extension(
167
+ search_paths=self.args['work-dir'], file_extension='rpt',
168
+ typ='report', description='Quartus Synthesis Report'
169
+ )
170
+
171
+ self.exec(self.args['work-dir'], command_list)
172
+ util.info(f"Synthesis done, results are in: {self.args['work-dir']}")
173
+
174
+ def write_tcl_file(self, tcl_file: str) -> None: # pylint: disable=too-many-locals,too-many-branches
175
+ '''Writes synthesis capable Quartus tcl file to filepath 'tcl_file'.'''
176
+
177
+ top = self.args['top']
178
+ part = self.args['part']
179
+ family = self.args['family']
180
+
181
+ tcl_lines = [
182
+ "# Quartus Synthesis Script",
183
+ "load_package flow",
184
+ f"project_new {top} -overwrite",
185
+ f"set_global_assignment -name FAMILY \"{family}\"",
186
+ f"set_global_assignment -name DEVICE {part}",
187
+ f"set_global_assignment -name TOP_LEVEL_ENTITY {top}",
188
+ ]
189
+
190
+ # Create a symbolic link or copy the include file to resolve lib/ paths
191
+ for incdir in self.incdirs:
192
+ if 'lib' in incdir:
193
+ lib_defines = os.path.join(incdir, 'oclib_defines.vh')
194
+ if os.path.exists(lib_defines):
195
+ # Copy the include file to the work directory as lib/oclib_defines.vh
196
+ work_lib_dir = os.path.join(self.args['work-dir'], 'lib')
197
+ os.makedirs(work_lib_dir, exist_ok=True)
198
+ work_lib_defines = os.path.join(work_lib_dir, 'oclib_defines.vh')
199
+
200
+ shutil.copy2(lib_defines, work_lib_defines)
201
+
202
+ # Add the copied file as a source
203
+ rel_path = os.path.relpath(
204
+ work_lib_defines, self.args['work-dir']
205
+ ).replace('\\', '/')
206
+ tcl_lines.append(
207
+ f"set_global_assignment -name VERILOG_FILE \"{rel_path}\""
208
+ )
209
+ break
210
+ # Add source files (convert to relative paths and use forward slashes)
211
+ for f in self.files_v:
212
+ rel_path = os.path.relpath(f, self.args['work-dir']).replace('\\', '/')
213
+ tcl_lines.append(f"set_global_assignment -name VERILOG_FILE \"{rel_path}\"")
214
+ for f in self.files_sv:
215
+ rel_path = os.path.relpath(f, self.args['work-dir']).replace('\\', '/')
216
+ tcl_lines.append(f"set_global_assignment -name SYSTEMVERILOG_FILE \"{rel_path}\"")
217
+ for f in self.files_vhd:
218
+ rel_path = os.path.relpath(f, self.args['work-dir']).replace('\\', '/')
219
+ tcl_lines.append(f"set_global_assignment -name VHDL_FILE \"{rel_path}\"")
220
+
221
+ # Add include directories - Quartus needs the base directory where "lib/" can be found
222
+ project_root = None
223
+ for incdir in self.incdirs:
224
+ tcl_lines.append(f"set_global_assignment -name SEARCH_PATH \"{incdir}\"")
225
+ # Find the project root (where lib/ directory exists)
226
+ if 'lib' in incdir and os.path.exists(incdir):
227
+ parent_dir = os.path.dirname(incdir)
228
+ if os.path.exists(os.path.join(parent_dir, 'lib')):
229
+ project_root = parent_dir
230
+
231
+ # Set the project root as the main search path so "lib/oclib_defines.vh" resolves correctly
232
+ if project_root:
233
+ tcl_lines.append(
234
+ f"set_global_assignment -name SEARCH_PATH \"{project_root}\""
235
+ )
236
+ # Add the lib directory as an include path
237
+ lib_path = os.path.join(project_root, 'lib').replace('\\', '/')
238
+ tcl_lines.append(
239
+ f"set_global_assignment -name USER_LIBRARIES \"{lib_path}\""
240
+ )
241
+
242
+ # Add all include directories as user libraries for better include resolution
243
+ for incdir in self.incdirs:
244
+ if os.path.exists(incdir):
245
+ tcl_lines.append(
246
+ f"set_global_assignment -name USER_LIBRARIES \"{incdir}\""
247
+ )
248
+
249
+ # Add defines
250
+ for key, value in self.defines.items():
251
+ if value is None:
252
+ tcl_lines.append(f"set_global_assignment -name VERILOG_MACRO \"{key}\"")
253
+ else:
254
+ tcl_lines.append(f"set_global_assignment -name VERILOG_MACRO \"{key}={value}\"")
255
+
256
+ # Add constraints
257
+ if self.args['sdc']:
258
+ tcl_lines.append(f"set_global_assignment -name SDC_FILE \"{self.args['sdc']}\"")
259
+ elif self.files_sdc:
260
+ for sdc_file in self.files_sdc:
261
+ tcl_lines.append(f"set_global_assignment -name SDC_FILE \"{sdc_file}\"")
262
+
263
+ tcl_lines += [
264
+ "# Run synthesis",
265
+ "execute_flow -analysis_and_elaboration",
266
+ "execute_flow -compile",
267
+ "project_close"
268
+ ]
269
+
270
+ with open(tcl_file, 'w', encoding='utf-8') as ftcl:
271
+ ftcl.write('\n'.join(tcl_lines))
272
+
273
+
274
+ class CommandBuildQuartus(CommandBuild, ToolQuartus):
275
+ '''CommandBuildQuartus is a command handler for: eda build --tool=quartus'''
276
+
277
+ def __init__(self, config: dict):
278
+ CommandBuild.__init__(self, config)
279
+ ToolQuartus.__init__(self, config=self.config)
280
+ # add args specific to this tool
281
+ self.args.update({
282
+ 'gui': False,
283
+ 'proj': False,
284
+ 'resynth': False,
285
+ 'reset': False,
286
+ })
287
+
288
+ def do_it(self):
289
+ # add defines for this job
290
+ self.set_tool_defines()
291
+ self.write_eda_config_and_args()
292
+
293
+ # create FLIST
294
+ flist_file = os.path.join(self.args['work-dir'],'build.flist')
295
+ util.debug(f"CommandBuildQuartus: {self.args['top-path']=}")
296
+
297
+ eda_path = eda_base.get_eda_exec('flist')
298
+ command_list = [
299
+ eda_path, 'flist',
300
+ '--tool=' + self.args['tool'],
301
+ self.args['top-path'],
302
+ '--force',
303
+ '--out=' + flist_file,
304
+ '--no-quote-define',
305
+ '--no-quote-define-value',
306
+ '--no-escape-define-value',
307
+ '--equal-define',
308
+ '--bracket-quote-path',
309
+ # Enhanced prefixes for better Quartus integration
310
+ '--prefix-incdir=' + shlex.quote("set_global_assignment -name SEARCH_PATH "),
311
+ '--prefix-define=' + shlex.quote("set_global_assignment -name VERILOG_MACRO "),
312
+ '--prefix-sv=' + shlex.quote("set_global_assignment -name SYSTEMVERILOG_FILE "),
313
+ '--prefix-v=' + shlex.quote("set_global_assignment -name VERILOG_FILE "),
314
+ '--prefix-vhd=' + shlex.quote("set_global_assignment -name VHDL_FILE "),
315
+ '--emit-rel-path', # Use relative paths for better portability
316
+ ]
317
+ for key,value in self.defines.items():
318
+ if value is None:
319
+ command_list += [ f"+define+{key}" ]
320
+ else:
321
+ command_list += [ shlex.quote(f"+define+{key}={value}") ]
322
+
323
+ cwd = util.getcwd()
324
+ util.debug(f"CommandBuildQuartus: {cwd=}")
325
+
326
+ # Write out a .sh command for debug
327
+ command_list = util.ShellCommandList(command_list, tee_fpath='run_eda_flist.log')
328
+ util.write_shell_command_file(dirpath=self.args['work-dir'], filename='run_eda_flist.sh',
329
+ command_lists=[command_list], line_breaks=True)
330
+
331
+ self.exec(cwd, command_list, tee_fpath=command_list.tee_fpath)
332
+
333
+ if self.args['job-name'] == "":
334
+ self.args['job-name'] = self.args['design']
335
+ project_dir = 'project.'+self.args['job-name']
336
+
337
+ # Create a simple Quartus build TCL script
338
+ build_tcl_file = os.path.join(self.args['work-dir'], 'build.tcl')
339
+ with open(build_tcl_file, 'w', encoding='utf-8') as ftcl:
340
+ ftcl.write(f'''# Quartus Build Script
341
+ load_package flow
342
+ project_new {self.args['design']} -overwrite
343
+ set_global_assignment -name FAMILY "{self.args['family']}"
344
+ set_global_assignment -name DEVICE {self.args['part']}
345
+ set_global_assignment -name TOP_LEVEL_ENTITY {self.args['top']}
346
+
347
+ # Source the flist file
348
+ source [lindex $argv 1]
349
+
350
+ # Run synthesis and compile
351
+ execute_flow -compile
352
+ project_close
353
+ ''')
354
+
355
+ # launch Quartus build
356
+ command_list = [self.quartus_exe, '-t', build_tcl_file, project_dir, flist_file]
357
+ if not util.args['verbose']:
358
+ command_list.append('-q')
359
+
360
+ # Write out a .sh command for debug
361
+ command_list = util.ShellCommandList(command_list, tee_fpath=None)
362
+ util.write_shell_command_file(dirpath=self.args['work-dir'], filename='run_quartus.sh',
363
+ command_lists=[command_list], line_breaks=True)
364
+
365
+ # Add artifact tracking for build
366
+ util.artifacts.add_extension(
367
+ search_paths=self.args['work-dir'], file_extension='sof',
368
+ typ='bitstream', description='Quartus SRAM Object File (bitstream)'
369
+ )
370
+ util.artifacts.add_extension(
371
+ search_paths=self.args['work-dir'], file_extension='pof',
372
+ typ='bitstream', description='Quartus Programmer Object File'
373
+ )
374
+ util.artifacts.add_extension(
375
+ search_paths=self.args['work-dir'], file_extension='fit.rpt',
376
+ typ='report', description='Quartus Fitter Report'
377
+ )
378
+ util.artifacts.add_extension(
379
+ search_paths=self.args['work-dir'], file_extension='sta.rpt',
380
+ typ='report', description='Quartus Timing Analyzer Report'
381
+ )
382
+
383
+ self.exec(cwd, command_list, tee_fpath=command_list.tee_fpath)
384
+ util.info(f"Build done, results are in: {self.args['work-dir']}")
385
+
386
+
387
+ class CommandFListQuartus(CommandFList, ToolQuartus):
388
+ '''CommandFListQuartus is a command handler for: eda flist --tool=quartus'''
389
+
390
+ def __init__(self, config: dict):
391
+ CommandFList.__init__(self, config=config)
392
+ ToolQuartus.__init__(self, config=self.config)
393
+
394
+
395
+ class CommandProjQuartus(CommandProj, ToolQuartus):
396
+ '''CommandProjQuartus is a command handler for: eda proj --tool=quartus'''
397
+
398
+ def __init__(self, config: dict):
399
+ CommandProj.__init__(self, config)
400
+ ToolQuartus.__init__(self, config=self.config)
401
+ # add args specific to this tool
402
+ self.args.update({
403
+ 'gui': True,
404
+ 'tcl-file': "proj.tcl",
405
+ })
406
+ self.args_help.update({
407
+ 'gui': 'Open Quartus in GUI mode (always True for proj)',
408
+ 'tcl-file': 'name of TCL file to be created for Quartus project',
409
+ })
410
+
411
+ def do_it(self):
412
+ # add defines for this job
413
+ self.set_tool_defines()
414
+ self.write_eda_config_and_args()
415
+
416
+ # create TCL
417
+ tcl_file = os.path.abspath(os.path.join(self.args['work-dir'], self.args['tcl-file']))
418
+
419
+ part = self.args['part']
420
+ family = self.args['family']
421
+ top = self.args['top']
422
+
423
+ tcl_lines = [
424
+ "# Quartus Project Creation Script",
425
+ "load_package flow",
426
+ f"project_new {top}_proj -overwrite",
427
+ f"set_global_assignment -name FAMILY \"{family}\"",
428
+ f"set_global_assignment -name DEVICE {part}",
429
+ f"set_global_assignment -name TOP_LEVEL_ENTITY {top}",
430
+ ]
431
+
432
+ # Add source files
433
+ for f in self.files_v:
434
+ rel_path = os.path.relpath(f, self.args['work-dir']).replace('\\', '/')
435
+ tcl_lines.append(f"set_global_assignment -name VERILOG_FILE \"{rel_path}\"")
436
+ for f in self.files_sv:
437
+ rel_path = os.path.relpath(f, self.args['work-dir']).replace('\\', '/')
438
+ tcl_lines.append(f"set_global_assignment -name SYSTEMVERILOG_FILE \"{rel_path}\"")
439
+ for f in self.files_vhd:
440
+ rel_path = os.path.relpath(f, self.args['work-dir']).replace('\\', '/')
441
+ tcl_lines.append(f"set_global_assignment -name VHDL_FILE \"{rel_path}\"")
442
+
443
+ # Add include directories
444
+ for incdir in self.incdirs:
445
+ tcl_lines.append(f"set_global_assignment -name SEARCH_PATH \"{incdir}\"")
446
+
447
+ # Add defines
448
+ for key, value in self.defines.items():
449
+ if value is None:
450
+ tcl_lines.append(f"set_global_assignment -name VERILOG_MACRO \"{key}\"")
451
+ else:
452
+ tcl_lines.append(f"set_global_assignment -name VERILOG_MACRO \"{key}={value}\"")
453
+
454
+ # Add constraints if available
455
+ if self.files_sdc:
456
+ for sdc_file in self.files_sdc:
457
+ tcl_lines.append(f"set_global_assignment -name SDC_FILE \"{sdc_file}\"")
458
+
459
+ tcl_lines += [
460
+ "project_close",
461
+ f"project_open {top}_proj"
462
+ ]
463
+
464
+ with open(tcl_file, 'w', encoding='utf-8') as ftcl:
465
+ ftcl.write('\n'.join(tcl_lines))
466
+
467
+ # execute Quartus in GUI mode
468
+ command_list = [
469
+ self.quartus_exe, '-t', tcl_file
470
+ ]
471
+ if not util.args['verbose']:
472
+ command_list.append('-q')
473
+
474
+ self.exec(self.args['work-dir'], command_list)
475
+ util.info(f"Project created and opened in: {self.args['work-dir']}")
476
+
477
+
478
+ class CommandUploadQuartus(CommandUpload, ToolQuartus):
479
+ '''CommandUploadQuartus is a command handler for: eda upload --tool=quartus'''
480
+
481
+ def __init__(self, config: dict):
482
+ CommandUpload.__init__(self, config)
483
+ ToolQuartus.__init__(self, config=self.config)
484
+ # add args specific to this tool
485
+ self.args.update({
486
+ 'sof-file': "",
487
+ 'cable': "1",
488
+ 'device': "1",
489
+ 'list-cables': False,
490
+ 'list-devices': False,
491
+ 'list-sof-files': False,
492
+ 'tcl-file': "upload.tcl",
493
+ 'log-file': "upload.log",
494
+ })
495
+ self.args_help.update({
496
+ 'sof-file': 'SOF file to upload (auto-detected if not specified)',
497
+ 'cable': 'Cable number to use for programming',
498
+ 'device': 'Device number on the cable',
499
+ 'list-cables': 'List available programming cables',
500
+ 'list-devices': 'List available devices on cable',
501
+ 'list-sof-files': 'List available SOF files',
502
+ 'tcl-file': 'name of TCL file to be created for upload',
503
+ 'log-file': 'log file for upload operation',
504
+ })
505
+
506
+ def do_it(self): # pylint: disable=too-many-branches,too-many-statements,too-many-locals
507
+ # add defines for this job
508
+ self.set_tool_defines()
509
+ self.write_eda_config_and_args()
510
+
511
+ sof_file = None
512
+ if self.args['sof-file']:
513
+ if os.path.isfile(self.args['sof-file']):
514
+ sof_file = self.args['sof-file']
515
+ else:
516
+ self.error(f"Specified SOF file does not exist: {self.args['sof-file']}")
517
+
518
+ # Auto-discover SOF file if not specified
519
+ if not sof_file and not self.args['list-cables'] and not self.args['list-devices']:
520
+ sof_files = []
521
+ util.debug(f"Looking for SOF files in {os.path.abspath('.')}")
522
+ for root, _, files in os.walk("."):
523
+ for f in files:
524
+ if f.endswith(".sof"):
525
+ fullpath = os.path.abspath(os.path.join(root, f))
526
+ sof_files.append(fullpath)
527
+ util.info(f"Found SOF file: {fullpath}")
528
+
529
+ if len(sof_files) == 1:
530
+ sof_file = sof_files[0]
531
+ elif len(sof_files) > 1:
532
+ if self.args['list-sof-files']:
533
+ util.info("Multiple SOF files found:")
534
+ for sf in sof_files:
535
+ util.info(f" {sf}")
536
+ return
537
+ self.error("Multiple SOF files found, please specify --sof-file")
538
+ elif not sof_files:
539
+ if self.args['list-sof-files']:
540
+ util.info("No SOF files found")
541
+ return
542
+ self.error("No SOF files found")
543
+
544
+ # Generate TCL script
545
+ script_file = Path(self.args['tcl-file'])
546
+
547
+ try:
548
+ with script_file.open("w", encoding="utf-8") as fout:
549
+ fout.write('load_package quartus_pgm\n')
550
+
551
+ if self.args['list-cables']:
552
+ fout.write('foreach cable [get_hardware_names] {\n')
553
+ fout.write(' puts "Cable: $cable"\n')
554
+ fout.write('}\n')
555
+
556
+ if self.args['list-devices']:
557
+ cable_idx = int(self.args["cable"]) - 1
558
+ fout.write(f'set cable [lindex [get_hardware_names] {cable_idx}]\n')
559
+ fout.write('foreach device [get_device_names -hardware_name $cable] {\n')
560
+ fout.write(' puts "Device: $device"\n')
561
+ fout.write('}\n')
562
+
563
+ if sof_file:
564
+ cable_idx2 = int(self.args["cable"]) - 1
565
+ device_idx = int(self.args["device"]) - 1
566
+ fout.write(f'set cable [lindex [get_hardware_names] {cable_idx2}]\n')
567
+ device_cmd = (
568
+ f'set device [lindex [get_device_names -hardware_name $cable] {device_idx}]'
569
+ )
570
+ fout.write(device_cmd)
571
+ fout.write('set_global_assignment -name USE_CONFIGURATION_DEVICE OFF\n')
572
+ fout.write('execute_flow -compile\n')
573
+ fout.write(f'quartus_pgm -c $cable -m jtag -o "p;{sof_file}@$device"\n')
574
+
575
+ except Exception as exc:
576
+ self.error(f"Cannot create {script_file}: {exc}")
577
+
578
+ if sof_file:
579
+ util.info(f"Programming with SOF file: {sof_file}")
580
+ else:
581
+ util.info("Listing cables/devices only")
582
+
583
+ # Execute Quartus programmer
584
+ command_list = [
585
+ self.quartus_exe, '-t', str(script_file)
586
+ ]
587
+ if not util.args['verbose']:
588
+ command_list.append('-q')
589
+
590
+ self.exec(self.args['work-dir'], command_list)
591
+ util.info("Upload operation completed")
592
+
593
+
594
+ class CommandOpenQuartus(CommandOpen, ToolQuartus):
595
+ '''CommandOpenQuartus is a command handler for: eda open --tool=quartus'''
596
+
597
+ def __init__(self, config: dict):
598
+ CommandOpen.__init__(self, config)
599
+ ToolQuartus.__init__(self, config=self.config)
600
+ # add args specific to this tool
601
+ self.args.update({
602
+ 'file': "",
603
+ 'gui': True,
604
+ })
605
+ self.args_help.update({
606
+ 'file': 'Quartus project file (.qpf) to open (auto-detected if not specified)',
607
+ 'gui': 'Open Quartus in GUI mode (always True for open)',
608
+ })
609
+
610
+ def do_it(self):
611
+ if not self.args['file']:
612
+ util.info("Searching for Quartus project...")
613
+ found_file = False
614
+ all_files = []
615
+ for root, _, files in os.walk("."):
616
+ for file in files:
617
+ if file.endswith(".qpf"):
618
+ found_file = os.path.abspath(os.path.join(root, file))
619
+ util.info(f"Found project: {found_file}")
620
+ all_files.append(found_file)
621
+ self.args['file'] = found_file
622
+ if len(all_files) > 1:
623
+ all_files.sort(key=os.path.getmtime)
624
+ self.args['file'] = all_files[-1]
625
+ util.info(f"Choosing: {self.args['file']} (newest)")
626
+
627
+ if not self.args['file']:
628
+ self.error("Couldn't find a QPF Quartus project to open")
629
+
630
+ projdir = os.path.dirname(self.args['file'])
631
+
632
+ command_list = [
633
+ self.quartus_exe, self.args['file']
634
+ ]
635
+
636
+ self.write_eda_config_and_args()
637
+ self.exec(projdir, command_list)
638
+ util.info(f"Opened Quartus project: {self.args['file']}")