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
« 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# --------------------------------------------------------------------------------------------------
9# Standard libraries
10import sys
11import unittest
13# Third party libraries
14import pytest
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
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()
45# pylint: disable=too-many-instance-attributes
46@pytest.mark.usefixtures("fixture_tmp_path")
47class TestBasicProject(unittest.TestCase):
49 tmp_path = None
51 top_template = """
52library ieee;
53use ieee.std_logic_1164.all;
54use ieee.numeric_std.all;
56library common;
57use common.attribute_pkg.all;
59library resync;
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;
71architecture a of test_proj_top is
72 signal input_p1 : std_ulogic;
73begin
75 pipe_input : process
76 begin
77 wait until rising_edge(clk_in);
78 input_p1 <= input;
79 end process;
81{code_block}
83end architecture;
84"""
86 def setUp(self):
87 module_folder = self.tmp_path / "modules" / "apa"
89 self.project_folder = self.tmp_path / "vivado"
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,
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)
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)]
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"
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"]
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"]
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 ]
160 def _create_vivado_project(self):
161 assert self.proj.create(self.project_folder)
163 for path in self.create_files:
164 assert path.exists(), path
166 def test_create_project(self):
167 self._create_vivado_project()
169 def test_synth_project(self):
170 self._create_vivado_project()
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
177 def test_synth_should_fail_if_source_code_does_not_compile(self):
178 create_file(self.top_file, "garbage\napa\nhest")
180 self._create_vivado_project()
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.")
186 def test_synth_with_assert_false_should_fail(self):
187 assert_false = """
188 assert false severity failure;"""
190 top = self.top_template.format(code_block=assert_false)
191 create_file(self.top_file, top)
193 self._create_vivado_project()
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."')
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
210 self._create_vivado_project()
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 )
219 def test_build_project(self):
220 self._create_vivado_project()
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
227 # Sanity check some of the build result
228 assert build_result.synthesis_size["Total LUTs"] == 0, build_result.synthesis_size
230 assert build_result.implementation_size["FFs"] > 0, build_result.implementation_size
231 assert build_result.implementation_size["FFs"] < 2000, build_result.implementation_size
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
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
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
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,
263 clk_out => clk_out,
264 data_out => resynced_input
265 );
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;
275 term2 := unsigned(not bit_pattern);
276 term2(22) := resynced_input;
278 output <= xor (term1 * term2);
279 end process;
280 end block;"""
282 top = self.top_template.format(code_block=bad_timing)
283 create_file(self.top_file, top)
285 self._create_vivado_project()
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 )
293 assert (self.runs_folder / "impl_2" / "timing_summary.rpt").exists()
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;"""
303 top = self.top_template.format(code_block=bad_resync)
304 create_file(self.top_file, top)
306 self._create_vivado_project()
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 )
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()
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)
327 self._create_vivado_project()
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 )
335 assert (self.runs_folder / "impl_2" / "pulse_width.rpt").exists()
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');
342 attribute dont_touch of result_word : signal is "true";
343 begin
345 input_process : process
346 begin
347 wait until rising_edge(clk_in);
348 input_word <= (others => input_p1);
349 end process;
351 result_process : process
352 begin
353 wait until rising_edge(clk_out);
354 result_word <= input_word;
355 end process;
357 end block;"""
358 top = self.top_template.format(code_block=resync_wide_word)
359 create_file(self.top_file, top)
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*]
366# The constraints below are the same as e.g. resync_counter.tcl.
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
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 )
379 self._create_vivado_project()
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 )
387 assert (self.runs_folder / "impl_2" / "bus_skew.rpt").exists()
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)
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