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