Coverage for tsfpga/test/functional/vivado/test_building_vivado_project.py: 0%

148 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-29 20:01 +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://gitlab.com/tsfpga/tsfpga 

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

8 

9import sys 

10import unittest 

11 

12import pytest 

13 

14import tsfpga 

15from tsfpga.constraint import Constraint 

16from tsfpga.module import get_modules, get_hdl_modules 

17from tsfpga.system_utils import create_file, run_command 

18from tsfpga.vivado.build_result_checker import LessThan, TotalLuts 

19from tsfpga.vivado.project import VivadoNetlistProject, VivadoProject 

20from tsfpga.test import file_contains_string 

21 

22 

23def test_building_artyz7_project(tmp_path): 

24 build_py = tsfpga.TSFPGA_EXAMPLES / "build.py" 

25 cmd = [ 

26 sys.executable, 

27 build_py, 

28 "artyz7", 

29 "--projects-path", 

30 tmp_path / "projects", 

31 "--output-path", 

32 tmp_path / "artifacts", 

33 ] 

34 run_command(cmd, cwd=tsfpga.REPO_ROOT) 

35 assert (tmp_path / "artifacts" / "artyz7" / "artyz7.bit").exists() 

36 assert (tmp_path / "artifacts" / "artyz7" / "artyz7.bin").exists() 

37 assert (tmp_path / "artifacts" / "artyz7" / "artyz7.xsa").exists() 

38 assert (tmp_path / "artifacts" / "artyz7" / "artyz7-0.0.0.0.zip").exists() 

39 

40 

41# pylint: disable=too-many-instance-attributes 

42@pytest.mark.usefixtures("fixture_tmp_path") 

43class TestBasicProject(unittest.TestCase): 

44 

45 tmp_path = None 

46 

47 top_template = """ 

48library ieee; 

49use ieee.std_logic_1164.all; 

50use ieee.numeric_std.all; 

51 

52library common; 

53use common.attribute_pkg.all; 

54 

55library resync; 

56 

57 

58entity test_proj_top is 

59 port ( 

60 clk_in : in std_logic; 

61 input : in std_logic; 

62 clk_out : in std_logic; 

63 output : out std_logic 

64 ); 

65end entity; 

66 

67architecture a of test_proj_top is 

68 signal input_p1 : std_logic; 

69begin 

70 

71 pipe_input : process 

72 begin 

73 wait until rising_edge(clk_in); 

74 input_p1 <= input; 

75 end process; 

76 

77{code_block} 

78 

79end architecture; 

80""" 

81 

82 def setUp(self): 

83 module_folder = self.tmp_path / "modules" / "apa" 

84 

85 self.project_folder = self.tmp_path / "vivado" 

86 

87 # Default top level 

88 resync = """ 

89 code_block : entity resync.resync_level 

90 generic map ( 

91 enable_input_register => false 

92 ) 

93 port map ( 

94 data_in => input_p1, 

95 

96 clk_out => clk_out, 

97 data_out => output 

98 );""" 

99 top = self.top_template.format(code_block=resync) 

100 self.top_file = create_file(module_folder / "test_proj_top.vhd", top) 

101 

102 self.constraint_io = """ 

103set_property -dict {package_pin H16 iostandard lvcmos33} [get_ports clk_in] 

104set_property -dict {package_pin P14 iostandard lvcmos33} [get_ports input] 

105set_property -dict {package_pin K17 iostandard lvcmos33} [get_ports clk_out] 

106set_property -dict {package_pin T16 iostandard lvcmos33} [get_ports output] 

107""" 

108 self.constraint_clocks = """ 

109# 250 MHz 

110create_clock -period 4 -name clk_in [get_ports clk_in] 

111create_clock -period 4 -name clk_out [get_ports clk_out] 

112""" 

113 self.constraint_file = create_file( 

114 file=module_folder / "test_proj_pinning.tcl", 

115 contents=self.constraint_io + self.constraint_clocks, 

116 ) 

117 constraints = [Constraint(self.constraint_file)] 

118 

119 self.modules = get_hdl_modules() + get_modules(modules_folders=[module_folder.parent]) 

120 self.proj = VivadoProject( 

121 name="test_proj", 

122 modules=self.modules, 

123 part="xc7z020clg400-1", 

124 constraints=constraints, 

125 # Faster 

126 default_run_index=2, 

127 ) 

128 self.log_file = self.project_folder / "vivado.log" 

129 self.runs_folder = self.project_folder / "test_proj.runs" 

130 

131 @property 

132 def create_files(self): 

133 """ 

134 Files that should exist when the project has been created. 

135 """ 

136 return [self.project_folder / "test_proj.xpr"] 

137 

138 @property 

139 def synthesis_files(self): 

140 """ 

141 Files that should exist when the project has been synthesized. 

142 """ 

143 return [self.runs_folder / "synth_2" / "hierarchical_utilization.rpt"] 

144 

145 @property 

146 def build_files(self): 

147 """ 

148 Files that should exist when the project has been fully built. 

149 """ 

150 return [ 

151 self.project_folder / f"{self.proj.name}.bit", 

152 self.project_folder / f"{self.proj.name}.bin", 

153 self.runs_folder / "impl_2" / "hierarchical_utilization.rpt", 

154 ] 

155 

156 def _create_vivado_project(self): 

157 assert self.proj.create(self.project_folder) 

158 

159 for path in self.create_files: 

160 assert path.exists(), path 

161 

162 def test_create_project(self): 

163 self._create_vivado_project() 

164 

165 def test_synth_project(self): 

166 self._create_vivado_project() 

167 

168 build_result = self.proj.build(self.project_folder, synth_only=True) 

169 assert build_result.success 

170 for path in self.synthesis_files: 

171 assert path.exists(), path 

172 

173 def test_synth_should_fail_if_source_code_does_not_compile(self): 

174 create_file(self.top_file, "garbage\napa\nhest") 

175 

176 self._create_vivado_project() 

177 

178 build_result = self.proj.build(self.project_folder, synth_only=True) 

179 assert not build_result.success 

180 assert file_contains_string(self.log_file, "\nERROR: Run synth_2 failed.") 

181 

182 def test_synth_with_assert_false_should_fail(self): 

183 assert_false = """ 

184 assert false severity failure;""" 

185 

186 top = self.top_template.format(code_block=assert_false) 

187 create_file(self.top_file, top) 

188 

189 self._create_vivado_project() 

190 

191 build_result = self.proj.build(self.project_folder, synth_only=True, run_index=2) 

192 assert not build_result.success 

193 assert file_contains_string(self.log_file, 'RTL assertion: "Assertion violation."') 

194 

195 def test_synth_with_error_message_should_fail(self): 

196 # Elevate the "Generic 'X' not present" message to ERROR, then set a generic that 

197 # does not exist. This will trigger an ERROR message, which should crash the build. 

198 self.proj.tcl_sources.append( 

199 create_file( 

200 self.tmp_path / "elevate_vivado_message.tcl", 

201 contents="set_msg_config -new_severity ERROR -id {Synth 8-3819}", 

202 ) 

203 ) 

204 self.proj.static_generics["non_existing"] = 1024 

205 

206 self._create_vivado_project() 

207 

208 build_result = self.proj.build(self.project_folder, synth_only=True) 

209 assert not build_result.success 

210 assert file_contains_string(self.log_file, "\nERROR: Run synth_2 failed.") 

211 assert file_contains_string( 

212 self.log_file, "\nERROR: Vivado has reported one or more ERROR messages. See build log." 

213 ) 

214 

215 def test_build_project(self): 

216 self._create_vivado_project() 

217 

218 build_result = self.proj.build(self.project_folder, self.project_folder) 

219 assert build_result.success 

220 for path in self.synthesis_files + self.build_files: 

221 assert path.exists(), path 

222 

223 # Sanity check some of the build result 

224 assert build_result.synthesis_size["Total LUTs"] == 0, build_result.synthesis_size 

225 

226 assert build_result.implementation_size["FFs"] > 0, build_result.implementation_size 

227 assert build_result.implementation_size["FFs"] < 2000, build_result.implementation_size 

228 

229 def test_build_project_in_steps(self): 

230 self._create_vivado_project() 

231 for path in self.synthesis_files + self.build_files: 

232 assert not path.exists(), path 

233 

234 build_result = self.proj.build(self.project_folder, self.project_folder, synth_only=True) 

235 assert build_result.success 

236 for path in self.synthesis_files: 

237 assert path.exists(), path 

238 for path in self.build_files: 

239 assert not path.exists(), path 

240 

241 build_result = self.proj.build(self.project_folder, self.project_folder, from_impl=True) 

242 assert build_result.success 

243 for path in self.synthesis_files + self.build_files: 

244 assert path.exists(), path 

245 

246 def test_build_with_bad_setup_timing_should_fail(self): 

247 # Do a ridiculously wide multiplication, which Vivado can't optimize away 

248 bad_timing = """ 

249 mult_block : block 

250 signal resynced_input : std_logic; 

251 begin 

252 resync_level_inst : entity resync.resync_level 

253 generic map ( 

254 enable_input_register => false 

255 ) 

256 port map ( 

257 data_in => input_p1, 

258 

259 clk_out => clk_out, 

260 data_out => resynced_input 

261 ); 

262 

263 mult : process 

264 constant bit_pattern : std_logic_vector(32 -1 downto 0) := x"deadbeef"; 

265 variable term1, term2, term3 : unsigned(bit_pattern'range); 

266 begin 

267 wait until rising_edge(clk_out); 

268 term1 := unsigned(bit_pattern); 

269 term1(28) := resynced_input; 

270 

271 term2 := unsigned(not bit_pattern); 

272 term2(22) := resynced_input; 

273 

274 output <= xor (term1 * term2); 

275 end process; 

276 end block;""" 

277 

278 top = self.top_template.format(code_block=bad_timing) 

279 create_file(self.top_file, top) 

280 

281 self._create_vivado_project() 

282 

283 build_result = self.proj.build(self.project_folder, self.project_folder) 

284 assert not build_result.success 

285 assert file_contains_string( 

286 self.log_file, "\nERROR: Setup/hold timing not OK after implementation run." 

287 ) 

288 

289 assert (self.runs_folder / "impl_2" / "timing_summary.rpt").exists() 

290 

291 def test_build_with_unhandled_clock_crossing_should_fail(self): 

292 bad_resync = """ 

293 pipe_output : process 

294 begin 

295 wait until rising_edge(clk_out); 

296 output <= input_p1; 

297 end process;""" 

298 

299 top = self.top_template.format(code_block=bad_resync) 

300 create_file(self.top_file, top) 

301 

302 self._create_vivado_project() 

303 

304 build_result = self.proj.build(self.project_folder, self.project_folder) 

305 assert not build_result.success 

306 assert file_contains_string( 

307 self.log_file, "\nERROR: Unhandled clock crossing in synth_2 run." 

308 ) 

309 

310 assert (self.runs_folder / "synth_2" / "hierarchical_utilization.rpt").exists() 

311 assert (self.runs_folder / "synth_2" / "timing_summary.rpt").exists() 

312 assert (self.runs_folder / "synth_2" / "clock_interaction.rpt").exists() 

313 

314 def test_build_with_bad_pulse_width_timing_should_fail(self): 

315 # Overwrite the default constraint file 

316 constraint_clocks = """ 

317# 250 MHz, but duty cycle is only 0.2 ns 

318create_clock -period 4 -waveform {1.0 1.2} -name clk_in [get_ports clk_in] 

319create_clock -period 4 -waveform {1.0 1.2} -name clk_out [get_ports clk_out] 

320""" 

321 create_file(self.constraint_file, self.constraint_io + constraint_clocks) 

322 

323 self._create_vivado_project() 

324 

325 build_result = self.proj.build(self.project_folder, self.project_folder) 

326 assert not build_result.success 

327 assert file_contains_string( 

328 self.log_file, "\nERROR: Pulse width timing violation after implementation run." 

329 ) 

330 

331 assert (self.runs_folder / "impl_2" / "pulse_width.rpt").exists() 

332 

333 def test_build_with_bad_bus_skew_should_fail(self): 

334 resync_wide_word = """ 

335 resync_block : block 

336 signal input_word, result_word : std_logic_vector(0 to 32 - 1) := (others => '0'); 

337 

338 attribute dont_touch of result_word : signal is "true"; 

339 begin 

340 

341 input_process : process 

342 begin 

343 wait until rising_edge(clk_in); 

344 input_word <= (others => input_p1); 

345 end process; 

346 

347 result_process : process 

348 begin 

349 wait until rising_edge(clk_out); 

350 result_word <= input_word; 

351 end process; 

352 

353 end block;""" 

354 top = self.top_template.format(code_block=resync_wide_word) 

355 create_file(self.top_file, top) 

356 

357 # Overwrite the default constraint file 

358 constraint_bus_skew = """ 

359set input_word [get_cells resync_block.input_word_reg*] 

360set result_word [get_cells resync_block.result_word_reg*] 

361 

362# The constraints below are the same as e.g. resync_counter.tcl. 

363 

364# Vivado manages to solve ~1.2 ns skew, so the value below will not succeed 

365set_bus_skew -from ${input_word} -to ${result_word} 0.8 

366 

367# Set max delay to exclude the clock crossing from regular timing analysis 

368set_max_delay -datapath_only -from ${input_word} -to ${result_word} 3 

369""" 

370 create_file( 

371 file=self.constraint_file, 

372 contents=self.constraint_io + self.constraint_clocks + constraint_bus_skew, 

373 ) 

374 

375 self._create_vivado_project() 

376 

377 build_result = self.proj.build(self.project_folder, self.project_folder) 

378 assert not build_result.success 

379 assert file_contains_string( 

380 self.log_file, "\nERROR: Bus skew constraints not met after implementation run." 

381 ) 

382 

383 assert (self.runs_folder / "impl_2" / "bus_skew.rpt").exists() 

384 

385 def test_building_vivado_netlist_project(self): 

386 project = VivadoNetlistProject( 

387 name="test_proj", 

388 modules=self.modules, 

389 part="xc7z020clg400-1", 

390 # Faster 

391 default_run_index=2, 

392 build_result_checkers=[TotalLuts(LessThan(1000))], 

393 ) 

394 assert project.create(self.project_folder) 

395 

396 build_result = project.build( 

397 project_path=self.project_folder, output_path=self.project_folder 

398 ) 

399 assert build_result.success 

400 for path in self.synthesis_files: 

401 assert path.exists(), path