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

149 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-31 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 

9# Standard libraries 

10import sys 

11import unittest 

12 

13# Third party libraries 

14import pytest 

15 

16# First party libraries 

17import tsfpga 

18from tsfpga.constraint import Constraint 

19from tsfpga.examples.example_env import get_hdl_modules 

20from tsfpga.module import get_modules 

21from tsfpga.system_utils import create_file, run_command 

22from tsfpga.test import file_contains_string 

23from tsfpga.vivado.build_result_checker import LessThan, TotalLuts 

24from tsfpga.vivado.project import VivadoNetlistProject, VivadoProject 

25 

26 

27def test_building_artyz7_project(tmp_path): 

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

29 cmd = [ 

30 sys.executable, 

31 build_py, 

32 "artyz7", 

33 "--projects-path", 

34 tmp_path / "projects", 

35 "--output-path", 

36 tmp_path / "artifacts", 

37 ] 

38 run_command(cmd, cwd=tsfpga.REPO_ROOT) 

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

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

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

42 assert (tmp_path / "artifacts" / "artyz7" / "artyz7-0.0.0.zip").exists() 

43 

44 

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

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

47class TestBasicProject(unittest.TestCase): 

48 

49 tmp_path = None 

50 

51 top_template = """ 

52library ieee; 

53use ieee.std_logic_1164.all; 

54use ieee.numeric_std.all; 

55 

56library common; 

57use common.attribute_pkg.all; 

58 

59library resync; 

60 

61 

62entity test_proj_top is 

63 port ( 

64 clk_in : in std_ulogic; 

65 input : in std_ulogic; 

66 clk_out : in std_ulogic; 

67 output : out std_ulogic 

68 ); 

69end entity; 

70 

71architecture a of test_proj_top is 

72 signal input_p1 : std_ulogic; 

73begin 

74 

75 pipe_input : process 

76 begin 

77 wait until rising_edge(clk_in); 

78 input_p1 <= input; 

79 end process; 

80 

81{code_block} 

82 

83end architecture; 

84""" 

85 

86 def setUp(self): 

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

88 

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

90 

91 # Default top level 

92 resync = """ 

93 code_block : entity resync.resync_level 

94 generic map ( 

95 enable_input_register => false 

96 ) 

97 port map ( 

98 data_in => input_p1, 

99 

100 clk_out => clk_out, 

101 data_out => output 

102 );""" 

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

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

105 

106 self.constraint_io = """ 

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

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

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

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

111""" 

112 self.constraint_clocks = """ 

113# 250 MHz 

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

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

116""" 

117 self.constraint_file = create_file( 

118 file=module_folder / "test_proj_pinning.tcl", 

119 contents=self.constraint_io + self.constraint_clocks, 

120 ) 

121 constraints = [Constraint(self.constraint_file)] 

122 

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

124 self.proj = VivadoProject( 

125 name="test_proj", 

126 modules=self.modules, 

127 part="xc7z020clg400-1", 

128 constraints=constraints, 

129 # Faster 

130 default_run_index=2, 

131 ) 

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

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

134 

135 @property 

136 def create_files(self): 

137 """ 

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

139 """ 

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

141 

142 @property 

143 def synthesis_files(self): 

144 """ 

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

146 """ 

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

148 

149 @property 

150 def build_files(self): 

151 """ 

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

153 """ 

154 return [ 

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

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

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

158 ] 

159 

160 def _create_vivado_project(self): 

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

162 

163 for path in self.create_files: 

164 assert path.exists(), path 

165 

166 def test_create_project(self): 

167 self._create_vivado_project() 

168 

169 def test_synth_project(self): 

170 self._create_vivado_project() 

171 

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

173 assert build_result.success 

174 for path in self.synthesis_files: 

175 assert path.exists(), path 

176 

177 def test_synth_should_fail_if_source_code_does_not_compile(self): 

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

179 

180 self._create_vivado_project() 

181 

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

183 assert not build_result.success 

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

185 

186 def test_synth_with_assert_false_should_fail(self): 

187 assert_false = """ 

188 assert false severity failure;""" 

189 

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

191 create_file(self.top_file, top) 

192 

193 self._create_vivado_project() 

194 

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

196 assert not build_result.success 

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

198 

199 def test_synth_with_error_message_should_fail(self): 

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

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

202 self.proj.tcl_sources.append( 

203 create_file( 

204 self.tmp_path / "elevate_vivado_message.tcl", 

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

206 ) 

207 ) 

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

209 

210 self._create_vivado_project() 

211 

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

213 assert not build_result.success 

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

215 assert file_contains_string( 

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

217 ) 

218 

219 def test_build_project(self): 

220 self._create_vivado_project() 

221 

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

223 assert build_result.success 

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

225 assert path.exists(), path 

226 

227 # Sanity check some of the build result 

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

229 

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

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

232 

233 def test_build_project_in_steps(self): 

234 self._create_vivado_project() 

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

236 assert not path.exists(), path 

237 

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

239 assert build_result.success 

240 for path in self.synthesis_files: 

241 assert path.exists(), path 

242 for path in self.build_files: 

243 assert not path.exists(), path 

244 

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

246 assert build_result.success 

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

248 assert path.exists(), path 

249 

250 def test_build_with_bad_setup_timing_should_fail(self): 

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

252 bad_timing = """ 

253 mult_block : block 

254 signal resynced_input : std_ulogic; 

255 begin 

256 resync_level_inst : entity resync.resync_level 

257 generic map ( 

258 enable_input_register => false 

259 ) 

260 port map ( 

261 data_in => input_p1, 

262 

263 clk_out => clk_out, 

264 data_out => resynced_input 

265 ); 

266 

267 mult : process 

268 constant bit_pattern : std_ulogic_vector(32 -1 downto 0) := x"deadbeef"; 

269 variable term1, term2, term3 : u_unsigned(bit_pattern'range); 

270 begin 

271 wait until rising_edge(clk_out); 

272 term1 := unsigned(bit_pattern); 

273 term1(28) := resynced_input; 

274 

275 term2 := unsigned(not bit_pattern); 

276 term2(22) := resynced_input; 

277 

278 output <= xor (term1 * term2); 

279 end process; 

280 end block;""" 

281 

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

283 create_file(self.top_file, top) 

284 

285 self._create_vivado_project() 

286 

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

288 assert not build_result.success 

289 assert file_contains_string( 

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

291 ) 

292 

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

294 

295 def test_build_with_unhandled_clock_crossing_should_fail(self): 

296 bad_resync = """ 

297 pipe_output : process 

298 begin 

299 wait until rising_edge(clk_out); 

300 output <= input_p1; 

301 end process;""" 

302 

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

304 create_file(self.top_file, top) 

305 

306 self._create_vivado_project() 

307 

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

309 assert not build_result.success 

310 assert file_contains_string( 

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

312 ) 

313 

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

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

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

317 

318 def test_build_with_bad_pulse_width_timing_should_fail(self): 

319 # Overwrite the default constraint file 

320 constraint_clocks = """ 

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

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

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

324""" 

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

326 

327 self._create_vivado_project() 

328 

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

330 assert not build_result.success 

331 assert file_contains_string( 

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

333 ) 

334 

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

336 

337 def test_build_with_bad_bus_skew_should_fail(self): 

338 resync_wide_word = """ 

339 resync_block : block 

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

341 

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

343 begin 

344 

345 input_process : process 

346 begin 

347 wait until rising_edge(clk_in); 

348 input_word <= (others => input_p1); 

349 end process; 

350 

351 result_process : process 

352 begin 

353 wait until rising_edge(clk_out); 

354 result_word <= input_word; 

355 end process; 

356 

357 end block;""" 

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

359 create_file(self.top_file, top) 

360 

361 # Overwrite the default constraint file 

362 constraint_bus_skew = """ 

363set input_word [get_cells resync_block.input_word_reg*] 

364set result_word [get_cells resync_block.result_word_reg*] 

365 

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

367 

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

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

370 

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

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

373""" 

374 create_file( 

375 file=self.constraint_file, 

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

377 ) 

378 

379 self._create_vivado_project() 

380 

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

382 assert not build_result.success 

383 assert file_contains_string( 

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

385 ) 

386 

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

388 

389 def test_building_vivado_netlist_project(self): 

390 project = VivadoNetlistProject( 

391 name="test_proj", 

392 modules=self.modules, 

393 part="xc7z020clg400-1", 

394 # Faster 

395 default_run_index=2, 

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

397 ) 

398 assert project.create(self.project_folder) 

399 

400 build_result = project.build( 

401 project_path=self.project_folder, output_path=self.project_folder 

402 ) 

403 assert build_result.success 

404 for path in self.synthesis_files: 

405 assert path.exists(), path