Coverage for tsfpga/yosys/project.py: 95%

273 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-09 00:40 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project, a project platform for modern FPGA development. 

5# https://tsfpga.com 

6# https://github.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9from __future__ import annotations 

10 

11import os 

12import sys 

13import tempfile 

14from contextlib import contextmanager 

15from copy import deepcopy 

16from pathlib import Path 

17from threading import Lock 

18from typing import TYPE_CHECKING, Any, NoReturn 

19 

20from vunit.ui import VUnit 

21 

22from tsfpga.generics import ( 

23 BitVectorGenericValue, 

24 GenericValue, 

25 GenericValues, 

26 StringGenericValue, 

27) 

28from tsfpga.hdl_file import HdlFile 

29from tsfpga.system_utils import ( 

30 copy_and_combine_dicts, 

31 create_directory, 

32 create_file, 

33 read_file, 

34) 

35 

36from .build_result import YosysBuildResult 

37from .common import run_ghdl, run_yosys, to_yosys_path 

38from .utilization_parser import ( 

39 YosysIntelUtilizationParser, 

40 YosysMicrochipUtilizationParser, 

41 YosysUtilizationParser, 

42 YosysXilinxUtilizationParser, 

43) 

44 

45if TYPE_CHECKING: 

46 from collections.abc import Iterator 

47 

48 from tsfpga.module import BaseModule 

49 from tsfpga.module_list import ModuleList 

50 from tsfpga.vivado.build_result_checker import SizeChecker 

51 

52 

53class YosysNetlistBuild: 

54 """ 

55 Used for handling a synthesis-only (netlist) build of a design, using Yosys with the 

56 ``ghdl-yosys-plugin`` as the VHDL front end. 

57 

58 Since this is a netlist build, there is no implementation (place & route) step, and hence 

59 no bitstream is produced. 

60 This is a great tool for getting quick feedback on the resource utilization of a design, or 

61 a sub-component of a design, during development. 

62 

63 The ``top`` is typically a VHDL entity, in which case all of its VHDL dependencies are found 

64 automatically via the compile order. 

65 It can also be a Verilog/SystemVerilog module (or the design can have no VHDL at all), in 

66 which case any VHDL entities that shall be instantiated from it (or from other VHDL 

67 entities) must be explicitly listed using the ``vhdl_entities`` argument. 

68 

69 .. note:: 

70 Requires GHDL, Yosys, and the ``ghdl-yosys-plugin`` module to be installed and 

71 available. 

72 See the `ghdl-yosys-plugin documentation 

73 <https://github.com/ghdl/ghdl-yosys-plugin>`__ for installation instructions. 

74 """ 

75 

76 #: Will always be ``True`` for this class, since it is a netlist build. 

77 is_netlist_build = True 

78 

79 #: The parser used to interpret the Yosys utilization report. 

80 #: The base class uses the one that reports only raw cell counts, since the plain ``synth`` 

81 #: command does not target any specific architecture. 

82 #: Overridden by the architecture-specific subclasses (:class:`.YosysXilinxNetlistBuild`, 

83 #: :class:`.YosysIntelNetlistBuild`, :class:`.YosysMicrochipNetlistBuild`). 

84 _utilization_parser: type[YosysUtilizationParser] = YosysUtilizationParser 

85 

86 #: The Yosys ``synth*`` command used to synthesize the design. 

87 #: The base class uses the generic ``synth`` command, which does not target any specific 

88 #: architecture. 

89 #: Overridden by the architecture-specific subclasses. Can also be set when subclassing, e.g. 

90 #: to ``"synth_intel_alm"`` to target an architecture that has no subclass of its own. 

91 _synth_command: str = "synth" 

92 

93 #: Further arguments to the ``synth*`` command, placed before its ``-top`` argument. 

94 #: Can be set when subclassing, e.g. to pass an option that this class has no argument for. 

95 #: Note that this is a tuple, so that it is never modified in place and shared between 

96 #: objects. A subclass that adds arguments in its constructor assigns a new tuple instead. 

97 _synth_arguments: tuple[str, ...] = () 

98 

99 #: Whether the ``_synth_command`` needs an explicit ``-flatten`` flag appended to it in order 

100 #: to flatten the design before synthesis (see :meth:`._get_synth_command`). 

101 #: This is the case for e.g. the ``synth`` and ``synth_xilinx`` commands. 

102 #: Some ``synth_*`` commands (e.g. ``synth_intel`` and ``synth_microchip``) instead flatten 

103 #: the design by default, and do not accept a ``-flatten`` flag. Such subclasses shall set 

104 #: this attribute to ``False``. 

105 _needs_explicit_flatten_flag: bool = True 

106 

107 def __init__( # noqa: PLR0913, PLR0917 

108 self, 

109 name: str, 

110 modules: ModuleList, 

111 top: str | None = None, 

112 vhdl_entities: list[str] | None = None, 

113 generics: GenericValues | None = None, 

114 build_result_checkers: list[SizeChecker] | None = None, 

115 vhdl_standard: str = "08", 

116 ghdl_path: Path | None = None, 

117 yosys_path: Path | None = None, 

118 ghdl_plugin_path: Path | None = None, 

119 ghdl_prefix: Path | None = None, 

120 defined_at: Path | None = None, 

121 **other_arguments: Any, # noqa: ANN401 

122 ) -> None: 

123 """ 

124 Class constructor. Performs a shallow copy of the mutable arguments, so that the user 

125 can e.g. append items to their list after creating an object. 

126 

127 Arguments: 

128 name: Project name. 

129 modules: Modules that shall be included in the build. 

130 Only VHDL source files are considered, since the build uses the GHDL front end. 

131 top: Name of top level entity. 

132 If left out, the top level name will be inferred from the ``name``. 

133 Is typically a VHDL entity, but can also be a Verilog/SystemVerilog module -- see 

134 ``vhdl_entities`` below. 

135 vhdl_entities: Only used if ``top`` is not a VHDL entity (i.e. if it is a 

136 Verilog/SystemVerilog module, or if the design has no VHDL at all). 

137 A list of the names of the VHDL entities that shall be made available for 

138 instantiation from the non-VHDL top level (or from other VHDL entities), since 

139 there is in that case no single VHDL top level to automatically find these 

140 dependencies from. 

141 Not used, and shall be left as ``None``, if ``top`` is a VHDL entity, since in 

142 that case all of its VHDL dependencies are found automatically via the compile 

143 order. 

144 generics: A dict with generics values (name: value). Use this parameter 

145 for "static" generics that do not change between multiple builds of this 

146 project. 

147 

148 Compare to the build-time generic argument in :meth:`build`. 

149 

150 The generic value shall be of type :data:`.GenericValue`. 

151 build_result_checkers: 

152 Checkers that will be executed after a successful build. Is used to automatically 

153 check that e.g. resource utilization is not greater than expected. 

154 vhdl_standard: The VHDL standard that shall be used by GHDL when analyzing the 

155 source files (e.g. ``"93"`` or ``"08"``). 

156 ghdl_path: Path to the GHDL executable. 

157 If omitted, the default location from the system PATH will be used. 

158 yosys_path: Path to the Yosys executable. 

159 If omitted, the default location from the system PATH will be used. 

160 ghdl_plugin_path: Path to the ``ghdl-yosys-plugin`` module (typically named 

161 ``ghdl.so``). 

162 Can be left out if the plugin is already available to Yosys without explicitly 

163 loading it (e.g. if it has been installed in the Yosys plugin directory). 

164 ghdl_prefix: Value to set the ``GHDL_PREFIX`` environment variable to when running 

165 Yosys with the ``ghdl-yosys-plugin``. The plugin is loaded as part of the Yosys 

166 process, and can not find the GHDL standard libraries (``std``, ``ieee``, ...) 

167 on its own the way the standalone ``ghdl`` executable can. 

168 Corresponds to the "library prefix" printed by ``ghdl --disp-config``. 

169 If left out, this is auto-detected by calling ``ghdl --disp-config`` against 

170 the executable given by ``ghdl_path`` above. 

171 Set explicitly to override the auto-detected value, or if auto-detection fails. 

172 defined_at: Optional path to the file where you defined this project. 

173 To get a useful ``build_fpga.py --list`` message. Is useful when you have many 

174 projects set up. 

175 other_arguments: Optional further arguments. Will not be used by tsfpga, but will 

176 instead be passed on to 

177 

178 * :func:`BaseModule.get_synthesis_files() 

179 <tsfpga.module.BaseModule.get_synthesis_files>` 

180 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>` 

181 * :func:`YosysNetlistBuild.pre_create` 

182 * :func:`YosysNetlistBuild.pre_build` 

183 * :func:`YosysNetlistBuild.post_build` 

184 

185 along with further arguments supplied at build-time to :meth:`.create` and 

186 :meth:`.build`. 

187 

188 .. note:: 

189 This is a "kwargs" style argument. You can pass any number of named arguments. 

190 """ 

191 self.name = name 

192 self.modules = modules.copy() 

193 self.top = name + "_top" if top is None else top 

194 self.vhdl_entities = [] if vhdl_entities is None else list(vhdl_entities) 

195 self.static_generics = {} if generics is None else generics.copy() 

196 self.build_result_checkers = ( 

197 [] if build_result_checkers is None else build_result_checkers.copy() 

198 ) 

199 self.defined_at = defined_at 

200 self.other_arguments = None if other_arguments is None else other_arguments.copy() 

201 

202 self._vhdl_standard = vhdl_standard 

203 self._ghdl_path = ghdl_path 

204 self._yosys_path = yosys_path 

205 self._ghdl_plugin_path = ghdl_plugin_path 

206 self._ghdl_prefix = ghdl_prefix 

207 

208 # Lazily created/cached. See '_get_vunit_project'. 

209 self._vunit_proj: VUnit | None = None 

210 # Owns the VUnit project's throwaway output directory: holding it here ties that 

211 # directory's lifetime to this object's. 

212 self._vunit_output_dir: tempfile.TemporaryDirectory[str] | None = None 

213 

214 def project_file(self, project_path: Path) -> Path: 

215 """ 

216 Arguments: 

217 project_path: A path containing a Yosys netlist build. 

218 

219 Return: 

220 The Yosys command script of this build, in the given folder. 

221 """ 

222 return project_path / f"{self.name}.ys" 

223 

224 def _get_ghdl_workdir(self, project_path: Path) -> Path: 

225 return project_path / "ghdl" 

226 

227 def _get_vunit_project(self) -> VUnit: 

228 if self._vunit_proj is None: 

229 # VUnit is used only to calculate the compile order of the VHDL source files. 

230 # Cleanup errors are ignored because this is scratch space: failing to delete it 

231 # must not fail a build. 

232 self._vunit_output_dir = tempfile.TemporaryDirectory( 

233 prefix="tsfpga_yosys_vunit_", ignore_cleanup_errors=True 

234 ) 

235 argv = [ 

236 "--output-path", 

237 self._vunit_output_dir.name, 

238 "--log-level", 

239 "error", 

240 "--no-color", 

241 ] 

242 

243 with _suppress_stdout(): 

244 # Note that VUnit's builtins are not compiled, since that is only done when 

245 # 'add_vhdl_builtins' is called. They would add dozens of VUnit-internal VHDL 

246 # files to the compile order. 

247 self._vunit_proj = VUnit.from_argv(argv=argv) 

248 

249 for module in self.modules: 

250 vunit_library = self._vunit_proj.add_library( 

251 library_name=module.library_name, allow_duplicate=True 

252 ) 

253 for hdl_file in module.get_synthesis_files( 

254 include_verilog_files=False, include_systemverilog_files=False 

255 ): 

256 vunit_library.add_source_file(hdl_file.path) 

257 

258 return self._vunit_proj 

259 

260 def _find_vhdl_source_file(self, entity_name: str) -> tuple[BaseModule, HdlFile] | None: 

261 """ 

262 Arguments: 

263 entity_name: Name of a VHDL entity. 

264 

265 Return: A tuple ``(module, hdl_file)`` for the VHDL source file that defines the given 

266 entity name, or ``None`` if no such VHDL source file is found in the modules of 

267 this build. 

268 """ 

269 matches = [ 

270 (module, hdl_file) 

271 for module in self.modules 

272 for hdl_file in module.get_synthesis_files( 

273 include_verilog_files=False, include_systemverilog_files=False 

274 ) 

275 if hdl_file.path.stem == entity_name 

276 ] 

277 

278 if len(matches) > 1: 

279 raise ValueError(f'Found multiple VHDL source files for entity "{entity_name}".') 

280 

281 return matches[0] if matches else None 

282 

283 def _find_top_vhdl_source_file(self) -> tuple[BaseModule, HdlFile] | None: 

284 """ 

285 Return: The module and source file of the ``top``, if it is a VHDL entity. 

286 ``None`` otherwise, e.g. if it is a Verilog/SystemVerilog module. 

287 """ 

288 return self._find_vhdl_source_file(entity_name=self.top) 

289 

290 @property 

291 def _top_is_vhdl(self) -> bool: 

292 """ 

293 Whether the ``top`` is a VHDL entity, as opposed to e.g. a Verilog/SystemVerilog module. 

294 """ 

295 return self._find_top_vhdl_source_file() is not None 

296 

297 def _get_vhdl_files_in_compile_order(self) -> list[tuple[str, str]]: 

298 """ 

299 Return: A list of tuples ``(file_path, library_name)`` in the order they need to be 

300 analyzed by GHDL. 

301 """ 

302 vunit_proj = self._get_vunit_project() 

303 top_level_match = self._find_top_vhdl_source_file() 

304 

305 if top_level_match is None: 

306 # The 'top' is not a VHDL entity (e.g. it is a Verilog/SystemVerilog module, or the 

307 # design has no VHDL at all). There is no single VHDL top level to compute an 

308 # implementation subset relative to, so analyze all the VHDL source files in the 

309 # modules of this build instead. 

310 compile_order = vunit_proj.get_compile_order() 

311 else: 

312 # Look up the source file directly by its (unique, resolved) path, rather than 

313 # reconstructing a file name pattern, since the VHDL file ending can be either 

314 # '.vhd', '.vhdl' or '.vho' (see 'HdlFile.file_endings_mapping'). 

315 _, top_hdl_file = top_level_match 

316 top_source_file = vunit_proj.get_source_file(file_name=top_hdl_file.path.resolve()) 

317 compile_order = vunit_proj.get_implementation_subset(source_files=[top_source_file]) 

318 

319 return [ 

320 (Path(source_file.name).resolve().as_posix(), source_file.library.name) 

321 for source_file in compile_order 

322 ] 

323 

324 def _get_verilog_source_files(self) -> list[Path]: 

325 """ 

326 Return: A list of paths to all the Verilog and SystemVerilog source files (not headers) 

327 found in the modules of this build. 

328 These are read directly by Yosys, bypassing GHDL entirely, and may be instantiated 

329 from the VHDL top level as unbound components with a matching name. 

330 """ 

331 source_types = (HdlFile.Type.VERILOG_SOURCE, HdlFile.Type.SYSTEMVERILOG_SOURCE) 

332 

333 return [ 

334 hdl_file.path 

335 for module in self.modules 

336 for hdl_file in module.get_synthesis_files(include_vhdl_files=False) 

337 if hdl_file.type in source_types 

338 ] 

339 

340 def _get_verilog_include_directories(self) -> list[Path]: 

341 """ 

342 Return: A sorted list of the unique directories that contain Verilog and SystemVerilog 

343 header files in the modules of this build. 

344 Used so that Yosys can resolve ```` `include ```` directives in the source files. 

345 """ 

346 header_types = (HdlFile.Type.VERILOG_HEADER, HdlFile.Type.SYSTEMVERILOG_HEADER) 

347 

348 return sorted( 

349 { 

350 hdl_file.path.parent 

351 for module in self.modules 

352 for hdl_file in module.get_synthesis_files(include_vhdl_files=False) 

353 if hdl_file.type in header_types 

354 } 

355 ) 

356 

357 def _get_read_verilog_command(self) -> str | None: 

358 """ 

359 Return: A Yosys ``read_verilog`` command that reads all the Verilog and SystemVerilog 

360 source files found in the modules of this build, or ``None`` if there are no such 

361 files. 

362 """ 

363 verilog_files = self._get_verilog_source_files() 

364 if not verilog_files: 

365 return None 

366 

367 # Note the asymmetry: file paths below are quoted, since Yosys splits unquoted command 

368 # arguments on whitespace, which would otherwise break for paths containing spaces 

369 # (e.g. common on Windows). 

370 # Include directories can NOT be quoted, however. Yosys' 'read_verilog' takes the 

371 # directory as everything after the "-I" in the token, so a quoted path ends up 

372 # containing the quote characters and never matches a real directory. 

373 # This means that include directories containing spaces are not supported by Yosys. 

374 include_flags = " ".join( 

375 f"-I{to_yosys_path(directory)}" for directory in self._get_verilog_include_directories() 

376 ) 

377 file_arguments = " ".join(f'"{to_yosys_path(file_path)}"' for file_path in verilog_files) 

378 

379 # The '-sv' flag enables the SystemVerilog parser, which is a superset of Verilog and 

380 # hence works fine for plain Verilog files as well. 

381 command = f"read_verilog -sv {include_flags} {file_arguments}" 

382 return " ".join(command.split()) 

383 

384 def create( 

385 self, 

386 project_path: Path, 

387 **other_arguments: Any, # noqa: ANN401 

388 ) -> bool: 

389 """ 

390 Analyze all the VHDL source files with GHDL, so that the design is ready to be 

391 elaborated and synthesized by :meth:`.build`. 

392 

393 Arguments: 

394 project_path: Path where the GHDL analysis result shall be placed. 

395 other_arguments: Optional further arguments. Will not be used by tsfpga, but will 

396 instead be sent to 

397 

398 * :func:`BaseModule.get_synthesis_files() 

399 <tsfpga.module.BaseModule.get_synthesis_files>` 

400 * :func:`YosysNetlistBuild.pre_create` 

401 

402 along with further ``other_arguments`` supplied to :meth:`.__init__`. 

403 

404 Return: 

405 True if everything went well. 

406 """ 

407 print(f"Creating Yosys netlist build {self.name} in {project_path}") 

408 

409 # The pre-create hook might have side effects. E.g. change some register constants. 

410 # So we make a deep copy of the module list before the hook is called. 

411 self.modules = deepcopy(self.modules) 

412 

413 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments) 

414 if not self.pre_create(project_path=project_path, **all_arguments): 

415 print("ERROR: Project pre-create hook returned False. Failing the build.") 

416 return False 

417 

418 return self._analyze_vhdl(workdir=self._get_ghdl_workdir(project_path=project_path)) 

419 

420 def _analyze_vhdl(self, workdir: Path) -> bool: 

421 """ 

422 Analyze all the VHDL source files into a GHDL work library, so that the design can be 

423 elaborated by the ``ghdl-yosys-plugin``. 

424 

425 Arguments: 

426 workdir: The GHDL work library location. Will be emptied first, so that the result 

427 reflects the source files as they are right now. 

428 

429 Return: 

430 True if everything went well. 

431 """ 

432 create_directory(workdir, empty=True) 

433 

434 for file_path, library_name in self._get_vhdl_files_in_compile_order(): 

435 arguments = [ 

436 "-a", 

437 f"--std={self._vhdl_standard}", 

438 f"--workdir={workdir}", 

439 f"-P={workdir}", 

440 f"--work={library_name}", 

441 file_path, 

442 ] 

443 

444 if not run_ghdl(ghdl_path=self._ghdl_path, arguments=arguments, cwd=workdir): 

445 print(f'ERROR: GHDL analysis failed for "{self.name}".') 

446 return False 

447 

448 return True 

449 

450 def pre_create( 

451 self, 

452 **kwargs: Any, # noqa: ANN401, ARG002 

453 ) -> bool: 

454 """ 

455 Override this function in a subclass if you wish to do something useful with it. 

456 Will be called from :meth:`.create` right before the GHDL analysis is started. 

457 

458 .. Note:: 

459 This default method does nothing. Shall be overridden by project that utilize 

460 this mechanism. 

461 

462 Arguments: 

463 kwargs: Will have all the :meth:`.create` parameters in it, as well as everything in 

464 the ``other_arguments`` argument to :func:`YosysNetlistBuild.__init__`. 

465 

466 Return: 

467 True if everything went well. 

468 """ 

469 return True 

470 

471 def _get_synth_command(self) -> str: 

472 # The design is flattened so that the produced utilization report contains the 

473 # primitive counts for the whole design, and not just the top level. 

474 flatten_flag = " -flatten" if self._needs_explicit_flatten_flag else "" 

475 arguments = "".join(f" {argument}" for argument in self._synth_arguments) 

476 return f"{self._synth_command}{arguments} -top {self.top}{flatten_flag}" 

477 

478 def _get_ghdl_elaborate_command( 

479 self, workdir: Path, entity_name: str, library_name: str, generic_arguments: str 

480 ) -> str: 

481 # Note: Unlike the paths used in '_get_read_verilog_command' and '_get_yosys_script', 

482 # the 'workdir' path below is deliberately *not* quoted. The 'ghdl' command, provided by 

483 # the 'ghdl-yosys-plugin', tokenizes its own argument line by naive whitespace splitting 

484 # and does not strip surrounding quotes, so quoting here would actually break paths that 

485 # contain spaces even worse than leaving them unquoted (verified empirically). This is a 

486 # limitation of the plugin, not something that can be worked around from this side. 

487 parts = [ 

488 "ghdl", 

489 f"--std={self._vhdl_standard}", 

490 f"--workdir={workdir}", 

491 f"-P={workdir}", 

492 f"--work={library_name}", 

493 ] 

494 if generic_arguments: 

495 parts.append(generic_arguments) 

496 parts.append(entity_name) 

497 

498 return " ".join(parts) 

499 

500 def _get_ghdl_commands( 

501 self, 

502 workdir: Path, 

503 all_generics: GenericValues, 

504 ) -> list[str]: 

505 """ 

506 Return: A list of Yosys ``ghdl`` commands that elaborate the VHDL entities of this 

507 build, making them available to Yosys. 

508 """ 

509 top_level_match = self._find_top_vhdl_source_file() 

510 if top_level_match is not None: 

511 # The 'top' is a VHDL entity: elaborate it directly. GHDL will pull in everything it 

512 # depends on, including any Verilog/SystemVerilog modules read by 

513 # '_get_read_verilog_command', which are bound by name to unbound VHDL component 

514 # instantiations. 

515 top_level_module, _ = top_level_match 

516 entities = [(self.top, top_level_module)] 

517 

518 # Project generics target the project top level, which is this VHDL entity. 

519 generic_arguments = " ".join( 

520 f"-g{name}={_get_ghdl_generic_value(value)}" for name, value in all_generics.items() 

521 ) 

522 else: 

523 # The 'top' is a Verilog/SystemVerilog module (or the design has no VHDL at all). 

524 # Elaborate each of the explicitly listed 'vhdl_entities' individually, so that they 

525 # become available (under their own entity name) for Yosys's 'hierarchy' pass to 

526 # bind to instantiations from the Verilog/SystemVerilog top level (or from other 

527 # VHDL entities). Any entity that ends up unused is pruned by Yosys. 

528 entities = [] 

529 for entity_name in self.vhdl_entities: 

530 match = self._find_vhdl_source_file(entity_name=entity_name) 

531 if match is None: 

532 raise ValueError( 

533 f'Could not find a VHDL source file for entity "{entity_name}" ' 

534 '(listed in "vhdl_entities").' 

535 ) 

536 module, _ = match 

537 entities.append((entity_name, module)) 

538 

539 # The project generics target the Verilog/SystemVerilog top level, where they are 

540 # applied as parameters by '_get_hierarchy_command'. They must not be passed to the 

541 # entities listed in 'vhdl_entities', which are submodules that generally do not 

542 # declare them. Per-entity generics are not supported. 

543 generic_arguments = "" 

544 

545 return [ 

546 self._get_ghdl_elaborate_command( 

547 workdir=workdir, 

548 entity_name=entity_name, 

549 library_name=module.library_name, 

550 generic_arguments=generic_arguments, 

551 ) 

552 for entity_name, module in entities 

553 ] 

554 

555 def _get_hierarchy_command(self, all_generics: GenericValues) -> str | None: 

556 """ 

557 Return: A Yosys ``hierarchy`` command that sets the top level parameters, when ``top`` is 

558 a Verilog/SystemVerilog module. ``None`` when there is nothing to set, or when 

559 ``top`` is a VHDL entity (where generics are instead passed to GHDL). 

560 """ 

561 if not all_generics or self._top_is_vhdl: 

562 return None 

563 

564 # Note that the 'synth' command runs 'hierarchy' itself, but the parameter values set 

565 # here are preserved by that later run. 

566 parameters = " ".join( 

567 f"-chparam {name} {_get_verilog_parameter_value(value)}" 

568 for name, value in all_generics.items() 

569 ) 

570 return f"hierarchy -top {self.top} {parameters}" 

571 

572 def _get_yosys_script( 

573 self, 

574 workdir: Path, 

575 all_generics: GenericValues, 

576 utilization_report_file: Path, 

577 ) -> str: 

578 commands = [] 

579 

580 # Read any Verilog/SystemVerilog source files before elaborating the VHDL, so that Yosys 

581 # can bind the unbound components in the VHDL design to the modules read here. 

582 read_verilog_command = self._get_read_verilog_command() 

583 if read_verilog_command is not None: 

584 commands.append(read_verilog_command) 

585 

586 commands += self._get_ghdl_commands(workdir=workdir, all_generics=all_generics) 

587 

588 hierarchy_command = self._get_hierarchy_command(all_generics=all_generics) 

589 if hierarchy_command is not None: 

590 commands.append(hierarchy_command) 

591 

592 commands += [ 

593 self._get_synth_command(), 

594 f'tee -o "{to_yosys_path(utilization_report_file)}" stat', 

595 ] 

596 

597 return "\n".join(commands) + "\n" 

598 

599 def build( 

600 self, 

601 project_path: Path, 

602 output_path: Path | None = None, 

603 generics: GenericValues | None = None, 

604 **pre_and_post_build_parameters: Any, # noqa: ANN401 

605 ) -> YosysBuildResult: 

606 """ 

607 Synthesize the design with Yosys. 

608 

609 Arguments: 

610 project_path: A path containing the result of a call to :meth:`.create`. 

611 output_path: The utilization report, and any other artifacts, will be placed here. 

612 Will default to ``project_path`` if not set. 

613 generics: A dict with generics values (`dict(name: value)`). Use for run-time 

614 generics, i.e. values that can change between each build of this project. 

615 Compare to the create-time generics argument in :meth:`.__init__`. 

616 The generic value types follow the same rules as for :meth:`.__init__`. 

617 pre_and_post_build_parameters: Optional further arguments. Will not be used by 

618 tsfpga, but will instead be sent to 

619 

620 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>` 

621 * :func:`YosysNetlistBuild.pre_build` 

622 * :func:`YosysNetlistBuild.post_build` 

623 

624 along with further ``other_arguments`` supplied to :meth:`.__init__`. 

625 

626 .. note:: 

627 This is a "kwargs" style argument. You can pass any number of named arguments. 

628 

629 Return: 

630 Result object with build information. 

631 """ 

632 workdir = self._get_ghdl_workdir(project_path=project_path) 

633 if not workdir.exists(): 

634 raise ValueError( 

635 f'Project "{self.name}" does not exist in the specified location: {project_path}. ' 

636 "Call 'create' before 'build'." 

637 ) 

638 

639 output_path = project_path if output_path is None else output_path 

640 create_directory(output_path, empty=False) 

641 

642 print(f"Synthesizing Yosys netlist build {self.name} in {project_path}") 

643 

644 all_generics = copy_and_combine_dicts(self.static_generics, generics) 

645 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters) 

646 all_parameters.update( 

647 project_path=project_path, output_path=output_path, generics=all_generics 

648 ) 

649 

650 # See 'create' for the rationale of doing this copy here as well. 

651 self.modules = deepcopy(self.modules) 

652 

653 result = YosysBuildResult(name=self.name) 

654 

655 for module in self.modules: 

656 if not module.pre_build(project=self, **all_parameters): 

657 print( 

658 f"ERROR: Module {module.name} pre-build hook returned False. Failing the build." 

659 ) 

660 result.success = False 

661 return result 

662 

663 # Make sure register packages are up to date. 

664 module.create_register_synthesis_files() 

665 

666 if not self.pre_build(**all_parameters): 

667 print("ERROR: Project pre-build hook returned False. Failing the build.") 

668 result.success = False 

669 return result 

670 

671 # The hooks above, and the register generation, may have changed the VHDL source files. 

672 # A typical example is a 'pre_build' hook that updates register constants based on the 

673 # build-time generics. 

674 # Hence the design must be analyzed again here, rather than relying on what 'create' 

675 # analyzed, which would elaborate stale sources. 

676 # The cached VUnit project is dropped as well, since the set of source files, and their 

677 # compile order, may have changed along with them. 

678 self._vunit_proj = None 

679 self._vunit_output_dir = None 

680 

681 if not self._analyze_vhdl(workdir=workdir): 

682 result.success = False 

683 return result 

684 

685 utilization_report_file = output_path / f"{self.name}_utilization.txt" 

686 script = self._get_yosys_script( 

687 workdir=workdir, 

688 all_generics=all_generics, 

689 utilization_report_file=utilization_report_file, 

690 ) 

691 

692 script_file = self.project_file(project_path=output_path) 

693 create_file(script_file, script) 

694 

695 if not run_yosys( 

696 yosys_path=self._yosys_path, 

697 ghdl_plugin_path=self._ghdl_plugin_path, 

698 script_file=script_file, 

699 cwd=output_path, 

700 ghdl_path=self._ghdl_path, 

701 ghdl_prefix=self._ghdl_prefix, 

702 ): 

703 print(f'ERROR: Yosys synthesis failed for "{self.name}".') 

704 result.success = False 

705 return result 

706 

707 result.synthesis_size = self._get_size(utilization_report_file=utilization_report_file) 

708 result.success = self._check_size(build_result=result) 

709 

710 # Send the result object, along with everything else, to the post-build function. 

711 all_parameters.update(build_result=result) 

712 

713 if not self.post_build(**all_parameters): 

714 print("ERROR: Project post-build hook returned False. Failing the build.") 

715 result.success = False 

716 

717 return result 

718 

719 def pre_build( 

720 self, 

721 **kwargs: Any, # noqa: ANN401, ARG002 

722 ) -> bool: 

723 """ 

724 Override this function in a subclass if you wish to do something useful with it. 

725 Will be called from :meth:`.build` right before the call to Yosys. 

726 

727 Arguments: 

728 kwargs: Will have all the :meth:`.build` parameters in it. Including additional 

729 parameters from the user. 

730 

731 Return: 

732 True if everything went well. 

733 """ 

734 return True 

735 

736 def post_build( 

737 self, 

738 **kwargs: Any, # noqa: ANN401, ARG002 

739 ) -> bool: 

740 """ 

741 Override this function in a subclass if you wish to do something useful with it. 

742 Will be called from :meth:`.build` right after the call to Yosys. 

743 

744 .. Note:: 

745 This default method does nothing. Shall be overridden by project that utilize 

746 this mechanism. 

747 

748 Arguments: 

749 kwargs: Will have all the :meth:`.build` parameters in it. Including additional 

750 parameters from the user. Will also include ``build_result``. 

751 

752 Return: 

753 True if everything went well. 

754 """ 

755 return True 

756 

757 def _get_size(self, utilization_report_file: Path) -> dict[str, int]: 

758 return self._utilization_parser.get_size(report=read_file(utilization_report_file)) 

759 

760 def _check_size(self, build_result: YosysBuildResult) -> bool: 

761 success = True 

762 for build_result_checker in self.build_result_checkers: 

763 checker_result = build_result_checker.check(build_result) 

764 success = success and checker_result 

765 

766 return success 

767 

768 def open( 

769 self, 

770 project_path: Path, 

771 ) -> NoReturn: 

772 """ 

773 Not implemented. A Yosys netlist build has no GUI to open. 

774 """ 

775 raise NotImplementedError("Yosys netlist build can not be opened") 

776 

777 def __str__(self) -> str: 

778 result = f"{self.name}\n" 

779 

780 if self.defined_at is not None: 

781 result += f"Defined at: {self.defined_at.resolve()}\n" 

782 

783 result += f"Type: {self.__class__.__name__}\n" 

784 result += f"Top level: {self.top}\n" 

785 

786 generics = self._dict_to_string(self.static_generics) if self.static_generics else "-" 

787 result += f"Generics: {generics}\n" 

788 

789 if self.other_arguments: 

790 result += f"Arguments: {self._dict_to_string(self.other_arguments)}\n" 

791 

792 return result 

793 

794 @staticmethod 

795 def _dict_to_string(data: dict[str, Any]) -> str: 

796 return ", ".join([f"{name}={value}" for name, value in data.items()]) 

797 

798 

799class YosysXilinxNetlistBuild(YosysNetlistBuild): 

800 """ 

801 Used for handling a Yosys netlist build that targets Xilinx primitives (LUTs, FDs, 

802 RAMBs, DSP48s, ...), using the Yosys ``synth_xilinx`` command. 

803 

804 Since the produced utilization report uses the same resource naming convention as the 

805 Vivado utilization report, the checkers in :mod:`.vivado.build_result_checker` can be used 

806 directly to check e.g. the LUT or RAMB count of the design. 

807 """ 

808 

809 _utilization_parser = YosysXilinxUtilizationParser 

810 _synth_command = "synth_xilinx" 

811 

812 def __init__( 

813 self, 

814 family: str | None = None, 

815 **kwargs: Any, # noqa: ANN401 

816 ) -> None: 

817 """ 

818 Arguments: 

819 family: Optionally target a specific Xilinx device family (e.g. ``"xc7"``). 

820 See the Yosys ``synth_xilinx`` command documentation for valid values. 

821 kwargs: Further arguments accepted by :meth:`.YosysNetlistBuild.__init__`. 

822 """ 

823 super().__init__(**kwargs) 

824 

825 if family is not None: 

826 self._synth_arguments = (*self._synth_arguments, "-family", family) 

827 

828 

829class YosysIntelNetlistBuild(YosysNetlistBuild): 

830 """ 

831 Used for handling a Yosys netlist build that targets Intel (Altera) primitives 

832 (``*_lcell_comb``, ``dffeas``, ``altsyncram``, ...), using the Yosys ``synth_intel`` command. 

833 

834 .. note:: 

835 Targets the MAX10, Cyclone IV, Cyclone IV E and Cyclone 10 LP families. 

836 For ALM-based Intel devices (Cyclone V, Cyclone 10 GX) the ``synth_intel_alm`` command 

837 shall be used instead, which is not covered by this class. Subclass 

838 :class:`.YosysNetlistBuild` and set its ``_synth_command`` to ``"synth_intel_alm"`` for 

839 that, though note that no aggregated resource counts will be available in that case. 

840 

841 Since the produced utilization report uses the resource names ``"Total LUTs"``, ``"FFs"``, 

842 ``"Block RAMs"`` and ``"DSP Blocks"``, the corresponding checkers in 

843 :mod:`.vivado.build_result_checker` can be used directly. 

844 """ 

845 

846 _utilization_parser = YosysIntelUtilizationParser 

847 _synth_command = "synth_intel" 

848 _needs_explicit_flatten_flag = False 

849 

850 def __init__( 

851 self, 

852 family: str | None = None, 

853 **kwargs: Any, # noqa: ANN401 

854 ) -> None: 

855 """ 

856 Arguments: 

857 family: Optionally target a specific Intel device family (e.g. ``"cycloneiv"``). 

858 See the Yosys ``synth_intel`` command documentation for valid values. 

859 kwargs: Further arguments accepted by :meth:`.YosysNetlistBuild.__init__`. 

860 """ 

861 super().__init__(**kwargs) 

862 

863 if family is not None: 

864 self._synth_arguments = (*self._synth_arguments, "-family", family) 

865 

866 

867class YosysMicrochipNetlistBuild(YosysNetlistBuild): 

868 """ 

869 Used for handling a Yosys netlist build that targets Microchip primitives 

870 (``CFG*``, ``SLE``, ``RAM1K20``, ``MACC_PA``, ...), using the Yosys ``synth_microchip`` 

871 command. 

872 

873 .. note:: 

874 Targets the PolarFire family, which is the only one currently supported by the Yosys 

875 ``synth_microchip`` command. 

876 

877 Since the produced utilization report uses the resource names ``"Total LUTs"``, ``"FFs"``, 

878 ``"Block RAMs"`` and ``"DSP Blocks"``, the corresponding checkers in 

879 :mod:`.vivado.build_result_checker` can be used directly. 

880 """ 

881 

882 _utilization_parser = YosysMicrochipUtilizationParser 

883 _synth_command = "synth_microchip" 

884 _needs_explicit_flatten_flag = False 

885 

886 def __init__( 

887 self, 

888 family: str | None = None, 

889 discard_ffinit: bool = False, 

890 **kwargs: Any, # noqa: ANN401 

891 ) -> None: 

892 """ 

893 Arguments: 

894 family: Optionally target a specific Microchip device family. 

895 See the Yosys ``synth_microchip`` command documentation for valid values. 

896 discard_ffinit: The Yosys ``synth_microchip`` command will raise an error if the 

897 design contains a flip-flop with an initial value that can not be legalized to 

898 a supported flip-flop type (which is a common occurrence, since e.g. VHDL signals 

899 initialized to a default value result in flip-flops with an initial value). 

900 Set this to ``True`` to instead discard the initial value and let synthesis 

901 proceed. Corresponds to the Yosys ``-discard-ffinit`` flag. 

902 kwargs: Further arguments accepted by :meth:`.YosysNetlistBuild.__init__`. 

903 """ 

904 super().__init__(**kwargs) 

905 

906 if family is not None: 

907 self._synth_arguments = (*self._synth_arguments, "-family", family) 

908 

909 if discard_ffinit: 

910 self._synth_arguments = (*self._synth_arguments, "-discard-ffinit") 

911 

912 

913def _get_ghdl_generic_value( 

914 value: GenericValue, 

915) -> str: 

916 """ 

917 Convert a generic value of a native Python type (or one of the tsfpga generic value 

918 wrapper classes) to a string suitable for the ``-g<name>=<value>`` argument of the 

919 ``ghdl-yosys-plugin`` ``ghdl`` command. 

920 """ 

921 # Note that bool is a sub-class of int in Python, so check for bool must be first. 

922 if isinstance(value, bool): 

923 # The plugin does not recognize "1"/"0" as boolean literals, only "true"/"false". 

924 return "true" if value else "false" 

925 

926 if isinstance(value, int): 

927 return str(value) 

928 

929 if isinstance(value, float): 

930 return str(value) 

931 

932 if isinstance(value, BitVectorGenericValue): 

933 return value.value 

934 

935 if isinstance(value, StringGenericValue): 

936 return value.value 

937 

938 message = f'Unsupported type for generic. Got type="{type(value)}", value="{value}".' 

939 if isinstance(value, str): 

940 message += ( 

941 " Please use either of the explicit types StringGenericValue or BitVectorGenericValue." 

942 ) 

943 

944 raise TypeError(message) 

945 

946 

947#: Guards the global ``sys.stdout`` swap in :func:`._suppress_stdout`. 

948#: Netlist builds are created from VUnit test-runner worker threads (see 

949#: :class:`.BuildProjectList`), which installs its own object as the global ``sys.stdout``. 

950#: Without this lock, two threads entering the swap concurrently interleave their 

951#: save/restore, and the last one out installs an already-closed file as ``sys.stdout``, 

952#: breaking every subsequent print in the process. 

953_SUPPRESS_STDOUT_LOCK = Lock() 

954 

955 

956@contextmanager 

957def _suppress_stdout() -> Iterator[None]: 

958 """ 

959 Suppress the (very chatty) printouts made by VUnit when creating a project. 

960 

961 Note that this swaps the process-global ``sys.stdout``, so it is serialized with a lock and 

962 the critical section is kept as small as possible. 

963 """ 

964 with _SUPPRESS_STDOUT_LOCK, Path(os.devnull).open("w") as devnull: 

965 old_stdout = sys.stdout 

966 sys.stdout = devnull 

967 try: 

968 yield 

969 finally: 

970 sys.stdout = old_stdout 

971 

972 

973def _get_verilog_parameter_value(value: GenericValue) -> str: 

974 """ 

975 Convert a generic value of a native Python type (or one of the tsfpga generic value 

976 wrapper classes) to a string suitable for the ``-chparam`` argument of the Yosys 

977 ``hierarchy`` command. 

978 """ 

979 if isinstance(value, bool): 

980 # Note that this must be before the 'int' check below, since 'bool' is a subclass of it. 

981 return "1" if value else "0" 

982 

983 if isinstance(value, BitVectorGenericValue): 

984 return f"{value.length}'b{value.value}" 

985 

986 if isinstance(value, (int, float)): 

987 return str(value) 

988 

989 message = ( 

990 "Yosys can not set string parameters of a Verilog/SystemVerilog top level. " 

991 f'Got type="{type(value)}", value="{value}".' 

992 ) 

993 raise TypeError(message)