Coverage for apio/scons/plugin_xilinx.py: 100%

77 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-09 01:55 +0000

1# -*- coding: utf-8 -*- 

2# -- This file is part of the Apio project 

3# -- (C) 2016-2018 FPGAwars 

4# -- Author Jesús Arroyo 

5# -- License GPLv2 

6# -- Derived from: 

7# ---- Platformio project 

8# ---- (C) 2014-2016 Ivan Kravets <me@ikravets.com> 

9# ---- License Apache v2 

10 

11"""Apio scons plugin for the xilinx architecture.""" 

12 

13# pylint: disable=duplicate-code 

14 

15import os 

16from pathlib import Path 

17from SCons.Script import Builder, Action 

18from SCons.Builder import BuilderBase, CompositeBuilder 

19from apio.common.common_util import SRC_SUFFIXES 

20from apio.scons.apio_env import ApioEnv 

21from apio.scons.plugin_base import PluginBase, ArchPluginInfo 

22from apio.scons.plugin_util import ( 

23 verilator_lint_action, 

24 has_testbench_name, 

25 announce_testbench_action, 

26 source_files_issue_scanner_action, 

27 iverilog_action, 

28 basename, 

29 make_verilator_config_builder, 

30 get_define_flags, 

31) 

32 

33 

34class PluginXilinx(PluginBase): 

35 """Apio scons plugin for the Xilinx architecture.""" 

36 

37 def __init__(self, apio_env: ApioEnv): 

38 # -- Call parent constructor. 

39 super().__init__(apio_env) 

40 

41 # -- Cache values. 

42 yosys_path = Path(apio_env.params.environment.yosys_path) 

43 self.yosys_lib_dir = yosys_path / "xilinx" 

44 self.sim_lib_files = [yosys_path / "xilinx" / "cells_sim.v"] 

45 # -- For lint, also pass the black-box declarations of the primitives 

46 # -- that have no simulation model in cells_sim.v (PLLE2_*, MMCME2_*, 

47 # -- etc.); without them verilator fails with MODMISSING on any design 

48 # -- that instantiates one. The two files declare disjoint modules. 

49 self.lint_lib_files = self.sim_lib_files + [ 

50 yosys_path / "xilinx" / "cells_xtra.v" 

51 ] 

52 

53 def plugin_info(self) -> ArchPluginInfo: 

54 """Return plugin specific parameters.""" 

55 return ArchPluginInfo( 

56 constrains_file_suffix=".xdc", 

57 pnr_file_suffix=".frames", 

58 bitstream_file_suffix=".bit", 

59 ) 

60 

61 # @overrides 

62 def synth_builder(self) -> BuilderBase | CompositeBuilder: 

63 """Creates and returns the synth builder.""" 

64 

65 # -- Keep short references. 

66 apio_env = self.apio_env 

67 params = apio_env.params 

68 xilinx_params = params.fpga_info.xilinx_params 

69 

70 # -- The yosys synth builder. 

71 return Builder( 

72 action=( 

73 # -- yosys-extra-options goes INSIDE the synth_xilinx command 

74 # -- (like the other architectures do with synth_ice40/ecp5/ 

75 # -- gowin), so synth flags such as -nodsp work; it used to 

76 # -- land after write_json, where it did nothing useful. 

77 'yosys -p "synth_xilinx -arch {0} -top {1} {2}; ' 

78 'write_json $TARGET " ' 

79 "{3} -DSYNTHESIZE {4} $SOURCES" 

80 ).format( 

81 xilinx_params.yosys_arch, 

82 params.apio_env_params.top_module, 

83 " ".join(params.apio_env_params.yosys_extra_options), 

84 "" if params.verbosity.all or params.verbosity.synth else "-q", 

85 get_define_flags(apio_env), 

86 ), 

87 suffix=".json", 

88 src_suffix=SRC_SUFFIXES, 

89 source_scanner=self.verilog_src_scanner, 

90 ) 

91 

92 # @overrides 

93 def pnr_builder(self) -> BuilderBase | CompositeBuilder: 

94 """Creates and returns the pnr builder.""" 

95 

96 # -- Keep short references. 

97 apio_env = self.apio_env 

98 params = apio_env.params 

99 xilinx_params = params.fpga_info.xilinx_params 

100 

101 # -- We use an emmiter to add to the builder a second output file. 

102 def emitter(target, source, env): 

103 _ = env # Unused 

104 target.append(apio_env.target + ".pnr") 

105 return target, source 

106 

107 # -- Get params. 

108 chipdb_dir = Path(apio_env.params.environment.xilinx_chipdb_path) 

109 package_name = xilinx_params.package 

110 

111 # -- The chipdb is one to one with the package name. 

112 chipdb_file_path = chipdb_dir / f"{package_name}.bin" 

113 

114 # -- Find the path of the report_xilinx utility that is used to 

115 # -- generate the report file hardware.pnr. It's embedded in the Apio 

116 # -- source tree. 

117 

118 # -- Get the full path of this file (plugin_xilinx.py) 

119 current_python_file = Path(__file__) 

120 

121 # -- The parent folder is the apio root folder 

122 apio_root = current_python_file.parent.parent 

123 

124 # -- Add the report_xilinx.py folder to the path. As of Aug 2026, 

125 # -- nextpnr-xilinx doesn't support the --report flag so we workaround 

126 # -- by running a report generation script on a --post-route trigger. 

127 # -- 

128 # -- TODO: Clean it up once nextpnr-xilinx gets the --report flag. 

129 report_py = apio_root / "scons/report_xilinx.py" 

130 

131 report_file = apio_env.target + ".pnr" 

132 

133 # -- Print action (platform independent) 

134 def print_report_var(target, source, env): 

135 _ = (target, source, env) # Unused 

136 print(f"[Setting APIO_XILINX_REPORT_FILE={report_file}]") 

137 return 0 # success 

138 

139 # -- Create the builder 

140 return Builder( 

141 action=[ 

142 # -- Action 1: Inform user about the env setting. 

143 Action( 

144 print_report_var, 

145 strfunction=lambda target, source, env: "", # hide 

146 ), 

147 # -- Action 2: The actual nextpnr. 

148 ( 

149 "nextpnr-xilinx --chipdb {0} --xdc {1} --json $SOURCE " 

150 "--fasm $TARGET " 

151 "--post-route {2} " 

152 "{3} " 

153 "{4}" 

154 ).format( 

155 chipdb_file_path, 

156 self.constrain_file(), 

157 report_py, 

158 # -- Honor --verbose-pnr like the other archs (ice40/ecp5/ 

159 # -- gowin), which show the full nextpnr log (fmax, 

160 # -- critical path). The xilinx plugin used to hard-code 

161 # -- -q. 

162 ( 

163 "" 

164 if params.verbosity.all or params.verbosity.pnr 

165 else "-q" 

166 ), 

167 " ".join(params.apio_env_params.nextpnr_extra_options), 

168 ), 

169 ], 

170 suffix=".fasm", 

171 src_suffix=".json", 

172 emitter=emitter, 

173 # -- Pass to the report_xiling.py script the path of its output 

174 # -- file. 

175 ENV={ 

176 **os.environ, 

177 "APIO_XILINX_REPORT_FILE": report_file, 

178 }, 

179 ) 

180 

181 # @overrides 

182 def bitstream_pre_builder(self) -> BuilderBase | CompositeBuilder: 

183 """Creates and returns the pre-bitstream builder.""" 

184 

185 # -- Keep short references. 

186 apio_env = self.apio_env 

187 params = apio_env.params 

188 xilinx_params = params.fpga_info.xilinx_params 

189 

190 part1 = f"{xilinx_params.package}-{xilinx_params.speed}" 

191 prjxray_db = Path(apio_env.params.environment.xilinx_prjxray_db_path) 

192 prjxray_db = prjxray_db / xilinx_params.family 

193 

194 return Builder( 

195 action="fasm2frames --part {0} --db-root {1} " 

196 " $SOURCE > $TARGET ".format( 

197 part1, 

198 prjxray_db, 

199 ), 

200 suffix=".frames", 

201 src_suffix=".fasm", 

202 ) 

203 

204 # @overrides 

205 def bitstream_builder(self) -> BuilderBase | CompositeBuilder: 

206 """Creates and returns the bitstream builder.""" 

207 

208 # -- Keep short references. 

209 apio_env = self.apio_env 

210 params = apio_env.params 

211 xilinx_params = params.fpga_info.xilinx_params 

212 part1 = f"{xilinx_params.package}-{xilinx_params.speed}" 

213 

214 prjxray_db = Path(apio_env.params.environment.xilinx_prjxray_db_path) 

215 prjxray_db = prjxray_db / xilinx_params.family 

216 part_file = prjxray_db / part1 / "part.yaml" 

217 

218 return Builder( 

219 action="xc7frames2bit --part_file {0} --part_name {1} " 

220 "--frm_file " 

221 "$SOURCE --output_file $TARGET".format( 

222 part_file, 

223 part1, 

224 ), 

225 suffix=".bit", 

226 src_suffix=".frames", 

227 ) 

228 

229 # @overrides 

230 def testbench_compile_builder(self) -> BuilderBase | CompositeBuilder: 

231 """Creates and returns the testbench compile builder.""" 

232 

233 # -- Keep short references. 

234 apio_env = self.apio_env 

235 params = apio_env.params 

236 

237 # -- Sanity checks 

238 assert apio_env.targeting_one_of("sim", "test") 

239 assert params.target.HasField("sim") or params.target.HasField("test") 

240 

241 # -- We use a generator because we need a different action 

242 # -- string for sim and test. 

243 def action_generator(target, source, env, for_signature): 

244 _ = (source, env, for_signature) # Unused 

245 # Extract testbench name from target file name. 

246 testbench_file = str(target[0]) 

247 assert has_testbench_name(testbench_file), testbench_file 

248 testbench_name = basename(testbench_file) 

249 

250 # Construct the actions list. 

251 action = [ 

252 # -- Print a testbench title. 

253 announce_testbench_action(), 

254 # -- Scan source files for issues. 

255 source_files_issue_scanner_action(), 

256 # -- Perform the actual test or sim compilation. 

257 iverilog_action( 

258 apio_env, 

259 verbose=params.verbosity.all, 

260 vcd_output_name=testbench_name, 

261 is_interactive=apio_env.targeting_one_of("sim"), 

262 lib_dirs=[self.yosys_lib_dir], 

263 lib_files=self.sim_lib_files, 

264 ), 

265 ] 

266 return action 

267 

268 # -- The testbench compiler builder. 

269 return Builder( 

270 # -- Dynamic action string generator. 

271 generator=action_generator, 

272 suffix=".out", 

273 src_suffix=SRC_SUFFIXES, 

274 source_scanner=self.verilog_src_scanner, 

275 ) 

276 

277 # @overrides 

278 def lint_config_builder(self) -> BuilderBase: 

279 """Creates and returns the lint config builder.""" 

280 

281 # -- Sanity checks 

282 assert self.apio_env.targeting_one_of("lint") 

283 

284 # -- Make the builder. 

285 return make_verilator_config_builder( 

286 self.yosys_lib_dir, 

287 rules_to_supress=[ 

288 "SPECIFYIGN", 

289 ], 

290 ) 

291 

292 # @overrides 

293 def lint_builder(self) -> BuilderBase | CompositeBuilder: 

294 """Creates and returns the lint builder.""" 

295 

296 return Builder( 

297 action=verilator_lint_action( 

298 self.apio_env, 

299 lib_dirs=[self.yosys_lib_dir], 

300 lib_files=self.lint_lib_files, 

301 ), 

302 src_suffix=SRC_SUFFIXES, 

303 source_scanner=self.verilog_src_scanner, 

304 )