Coverage for tsfpga/vivado/project.py: 84%

177 statements  

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

9# Standard libraries 

10import shutil 

11from copy import deepcopy 

12 

13# First party libraries 

14from tsfpga import TSFPGA_TCL 

15from tsfpga.build_step_tcl_hook import BuildStepTclHook 

16from tsfpga.system_utils import create_file, read_file 

17 

18# Local folder libraries 

19from .build_result import BuildResult 

20from .common import run_vivado_gui, run_vivado_tcl 

21from .hierarchical_utilization_parser import HierarchicalUtilizationParser 

22from .logic_level_distribution_parser import LogicLevelDistributionParser 

23from .tcl import VivadoTcl 

24 

25 

26class VivadoProject: 

27 """ 

28 Used for handling a Xilinx Vivado HDL project 

29 """ 

30 

31 # pylint: disable=too-many-arguments,too-many-instance-attributes 

32 def __init__( 

33 self, 

34 name, 

35 modules, 

36 part, 

37 top=None, 

38 generics=None, 

39 constraints=None, 

40 tcl_sources=None, 

41 build_step_hooks=None, 

42 vivado_path=None, 

43 default_run_index=1, 

44 defined_at=None, 

45 **other_arguments, 

46 ): 

47 """ 

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

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

50 

51 Arguments: 

52 name (str): Project name. 

53 modules (list(BaseModule)): Modules that shall be included in the project. 

54 part (str): Part identification. 

55 top (str): Name of top level entity. If left out, the top level name will be 

56 inferred from the ``name``. 

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

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

59 project. These will be set in the project when it is created. 

60 

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

62 

63 The generic value shall be of type 

64 

65 * :class:`bool` (suitable for VHDL type ``boolean`` and ``std_logic``), 

66 * :class:`int` (suitable for VHDL type ``integer``, ``natural``, etc.), 

67 * :class:`float` (suitable for VHDL type ``real``), 

68 * :class:`.BitVectorGenericValue` (suitable for VHDL type ``std_logic_vector``, 

69 ``unsigned``, etc.), or 

70 * :class:`.StringGenericValue` (suitable for VHDL type ``string``). 

71 constraints (list(Constraint)): Constraints that will be applied to the project. 

72 tcl_sources (list(pathlib.Path)): A list of TCL files. Use for e.g. block design, 

73 pinning, settings, etc. 

74 build_step_hooks (list(BuildStepTclHook)): Build step hooks that will be applied to the 

75 project. 

76 vivado_path (pathlib.Path): A path to the Vivado executable. If omitted, 

77 the default location from the system PATH will be used. 

78 default_run_index (int): Default run index (synth_X and impl_X) that is set in the 

79 project. Can also use the argument to :meth:`build() <VivadoProject.build>` to 

80 specify at build-time. 

81 defined_at (pathlib.Path): Optional path to the file where you defined this 

82 project. To get a useful ``build.py --list`` message. Is useful when you have many 

83 projects set up. 

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

85 instead be passed on to 

86 

87 * :func:`BaseModule.get_synthesis_files() 

88 <tsfpga.module.BaseModule.get_synthesis_files>` 

89 * :func:`BaseModule.get_ip_core_files() 

90 <tsfpga.module.BaseModule.get_ip_core_files>` 

91 * :func:`BaseModule.get_scoped_constraints() 

92 <tsfpga.module.BaseModule.get_scoped_constraints>` 

93 * :func:`VivadoProject.pre_create` 

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

95 * :func:`VivadoProject.pre_build` 

96 * :func:`VivadoProject.post_build` 

97 

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

99 :meth:`.build`. 

100 

101 .. note:: 

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

103 """ 

104 self.name = name 

105 self.modules = modules.copy() 

106 self.part = part 

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

108 self.constraints = [] if constraints is None else constraints.copy() 

109 self.tcl_sources = [] if tcl_sources is None else tcl_sources.copy() 

110 self.build_step_hooks = [] if build_step_hooks is None else build_step_hooks.copy() 

111 self._vivado_path = vivado_path 

112 self.default_run_index = default_run_index 

113 self.defined_at = defined_at 

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

115 

116 # Will be set by child class when applicable 

117 self.is_netlist_build = False 

118 self.analyze_synthesis_timing = True 

119 self.report_logic_level_distribution = False 

120 self.ip_cores_only = False 

121 

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

123 

124 self.tcl = VivadoTcl(name=self.name) 

125 

126 def project_file(self, project_path): 

127 """ 

128 Arguments: 

129 project_path (pathlib.Path): A path containing a Vivado project. 

130 Return: 

131 pathlib.Path: The project file of this project, in the given folder 

132 """ 

133 return project_path / (self.name + ".xpr") 

134 

135 def _setup_tcl_sources(self): 

136 tsfpga_tcl_sources = [ 

137 TSFPGA_TCL / "vivado_default_run.tcl", 

138 TSFPGA_TCL / "vivado_fast_run.tcl", 

139 TSFPGA_TCL / "vivado_messages.tcl", 

140 ] 

141 

142 # Add tsfpga TCL sources first. The user might want to change something in the tsfpga 

143 # settings. Conversely, tsfpga should not modify something that the user has set up. 

144 self.tcl_sources = tsfpga_tcl_sources + self.tcl_sources 

145 

146 def _setup_build_step_hooks(self): 

147 # Check that no ERROR messages have been sent by Vivado. After synthesis as well as 

148 # after implementation. 

149 self.build_step_hooks.append( 

150 BuildStepTclHook( 

151 TSFPGA_TCL / "check_no_error_messages.tcl", "STEPS.SYNTH_DESIGN.TCL.POST" 

152 ) 

153 ) 

154 self.build_step_hooks.append( 

155 BuildStepTclHook( 

156 TSFPGA_TCL / "check_no_error_messages.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE" 

157 ) 

158 ) 

159 

160 # Check the implemented timing and resource utilization via TCL build hooks. 

161 # This is different than for synthesis, where it is embedded in the build script. 

162 # This is due to Vivado limitations related to post-synthesis hooks. 

163 # Specifically, the report_utilization figures do not include IP cores when it is run in 

164 # a post-synthesis hook. 

165 self.build_step_hooks.append( 

166 BuildStepTclHook(TSFPGA_TCL / "report_utilization.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE") 

167 ) 

168 self.build_step_hooks.append( 

169 BuildStepTclHook(TSFPGA_TCL / "check_timing.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE") 

170 ) 

171 

172 if not self.analyze_synthesis_timing: 

173 # In this special case however, the synthesized design is never opened, and 

174 # report_utilization is not run by the build_vivado_project.tcl. 

175 # So in order to get a utilization report anyway we add it as a hook. 

176 # This mode is exclusively used by netlist builds, which very rarely include IP cores, 

177 # so it is acceptable that the utilization report might be erroneous with regards to 

178 # IP cores. 

179 self.build_step_hooks.append( 

180 BuildStepTclHook( 

181 TSFPGA_TCL / "report_utilization.tcl", "STEPS.SYNTH_DESIGN.TCL.POST" 

182 ) 

183 ) 

184 

185 if self.report_logic_level_distribution: 

186 # Used by netlist builds 

187 self.build_step_hooks.append( 

188 BuildStepTclHook( 

189 TSFPGA_TCL / "report_logic_level_distribution.tcl", 

190 "STEPS.SYNTH_DESIGN.TCL.POST", 

191 ) 

192 ) 

193 

194 def _create_tcl(self, project_path, ip_cache_path, all_arguments): 

195 """ 

196 Make a TCL file that creates a Vivado project 

197 """ 

198 if project_path.exists(): 

199 raise ValueError(f"Folder already exists: {project_path}") 

200 project_path.mkdir(parents=True) 

201 

202 create_vivado_project_tcl = project_path / "create_vivado_project.tcl" 

203 tcl = self.tcl.create( 

204 project_folder=project_path, 

205 modules=self.modules, 

206 part=self.part, 

207 top=self.top, 

208 run_index=self.default_run_index, 

209 generics=self.static_generics, 

210 constraints=self.constraints, 

211 tcl_sources=self.tcl_sources, 

212 build_step_hooks=self.build_step_hooks, 

213 ip_cache_path=ip_cache_path, 

214 disable_io_buffers=self.is_netlist_build, 

215 ip_cores_only=self.ip_cores_only, 

216 other_arguments=all_arguments, 

217 ) 

218 create_file(create_vivado_project_tcl, tcl) 

219 

220 return create_vivado_project_tcl 

221 

222 def create(self, project_path, ip_cache_path=None, **other_arguments): 

223 """ 

224 Create a Vivado project 

225 

226 Arguments: 

227 project_path (pathlib.Path): Path where the project shall be placed. 

228 ip_cache_path (pathlib.Path): Path to a folder where the Vivado IP cache can be 

229 placed. If omitted, the Vivado IP cache mechanism will not be enabled. 

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

231 instead be sent to 

232 

233 * :func:`BaseModule.get_synthesis_files() 

234 <tsfpga.module.BaseModule.get_synthesis_files>` 

235 * :func:`BaseModule.get_ip_core_files() 

236 <tsfpga.module.BaseModule.get_ip_core_files>` 

237 * :func:`BaseModule.get_scoped_constraints() 

238 <tsfpga.module.BaseModule.get_scoped_constraints>` 

239 * :func:`VivadoProject.pre_create` 

240 

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

242 

243 .. note:: 

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

245 Returns: 

246 bool: True if everything went well. 

247 """ 

248 print(f"Creating Vivado project in {project_path}") 

249 self._setup_tcl_sources() 

250 self._setup_build_step_hooks() 

251 

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

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

254 # Note that the modules are copied before the pre-build hooks as well, 

255 # since we do not know if we might be performing a create-only or 

256 # build-only operation. The copy does not take any significant time, so this is not 

257 # an issue. 

258 self.modules = deepcopy(self.modules) 

259 

260 # Send all available arguments that are reasonable to use in pre-create and module getter 

261 # functions. Prefer run-time values over the static. 

262 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments) 

263 all_arguments.update( 

264 generics=self.static_generics, 

265 part=self.part, 

266 ) 

267 

268 if not self.pre_create( 

269 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments 

270 ): 

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

272 return False 

273 

274 create_vivado_project_tcl = self._create_tcl( 

275 project_path=project_path, ip_cache_path=ip_cache_path, all_arguments=all_arguments 

276 ) 

277 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl) 

278 

279 def pre_create(self, **kwargs): # pylint: disable=unused-argument 

280 """ 

281 Override this function in a child class if you wish to do something useful with it. 

282 Will be called from :meth:`.create` right before the call to Vivado. 

283 

284 An example use case for this function is when TCL source scripts for the Vivado project 

285 have to be auto generated. This could e.g. be scripts that set IP repo paths based on the 

286 Vivado system PATH. 

287 

288 .. Note:: 

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

290 this mechanism. 

291 

292 Arguments: 

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

294 the ``other_arguments`` argument to :func:`VivadoProject.__init__`. 

295 

296 Return: 

297 bool: True if everything went well. 

298 """ 

299 return True 

300 

301 def _build_tcl( 

302 self, 

303 project_path, 

304 output_path, 

305 num_threads, 

306 run_index, 

307 all_generics, 

308 synth_only, 

309 from_impl, 

310 ): 

311 """ 

312 Make a TCL file that builds a Vivado project 

313 """ 

314 project_file = self.project_file(project_path) 

315 if not project_file.exists(): 

316 raise ValueError( 

317 f"Project file does not exist in the specified location: {project_file}" 

318 ) 

319 

320 build_vivado_project_tcl = project_path / "build_vivado_project.tcl" 

321 tcl = self.tcl.build( 

322 project_file=project_file, 

323 output_path=output_path, 

324 num_threads=num_threads, 

325 run_index=run_index, 

326 generics=all_generics, 

327 synth_only=synth_only, 

328 from_impl=from_impl, 

329 analyze_synthesis_timing=self.analyze_synthesis_timing, 

330 ) 

331 create_file(build_vivado_project_tcl, tcl) 

332 

333 return build_vivado_project_tcl 

334 

335 def pre_build(self, **kwargs): # pylint: disable=unused-argument 

336 """ 

337 Override this function in a child class if you wish to do something useful with it. 

338 Will be called from :meth:`.build` right before the call to Vivado. 

339 

340 Arguments: 

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

342 parameters from the user. 

343 

344 Return: 

345 bool: True if everything went well. 

346 """ 

347 return True 

348 

349 def post_build(self, **kwargs): # pylint: disable=unused-argument 

350 """ 

351 Override this function in a child class if you wish to do something useful with it. 

352 Will be called from :meth:`.build` right after the call to Vivado. 

353 

354 An example use case for this function is to encrypt the bit file, or generate any other 

355 material that shall be included in FPGA release artifacts. 

356 

357 .. Note:: 

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

359 this mechanism. 

360 

361 Arguments: 

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

363 parameters from the user. Will also include ``build_result`` with 

364 implemented/synthesized size, which can be used for asserting the expected resource 

365 utilization. 

366 

367 Return: 

368 bool: True if everything went well. 

369 """ 

370 return True 

371 

372 def build( 

373 self, 

374 project_path, 

375 output_path=None, 

376 run_index=None, 

377 generics=None, 

378 synth_only=False, 

379 from_impl=False, 

380 num_threads=12, 

381 **pre_and_post_build_parameters, 

382 ): 

383 """ 

384 Build a Vivado project 

385 

386 Arguments: 

387 project_path (pathlib.Path): A path containing a Vivado project. 

388 output_path (pathlib.Path): Results (bit file, ...) will be placed here. 

389 run_index (int): Select Vivado run (synth_X and impl_X) to build with. 

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

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

392 

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

394 

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

396 synth_only (bool): Run synthesis and then stop. 

397 from_impl (bool): Run the ``impl`` steps and onward on an existing synthesized design. 

398 num_threads (int): Number of parallel threads to use during run. 

399 pre_and_post_build_parameters: Optional further arguments. Will not be used by tsfpga, 

400 but will instead be sent to 

401 

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

403 * :func:`VivadoProject.pre_build` 

404 * :func:`VivadoProject.post_build` 

405 

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

407 

408 .. note:: 

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

410 

411 Return: 

412 :class:`.build_result.BuildResult`: Result object with build information. 

413 """ 

414 synth_only = synth_only or self.is_netlist_build 

415 

416 if output_path is None and not synth_only: 

417 raise ValueError("Must specify output_path when doing an implementation run") 

418 

419 if synth_only: 

420 print(f"Synthesizing Vivado project in {project_path}") 

421 else: 

422 print(f"Building Vivado project in {project_path}, placing artifacts in {output_path}") 

423 

424 # Combine to all available generics. Prefer run-time values over static. 

425 all_generics = copy_and_combine_dicts(self.static_generics, generics) 

426 

427 # Run index is optional to specify at build-time 

428 run_index = self.default_run_index if run_index is None else run_index 

429 

430 # Send all available information to pre- and post build functions. Prefer build-time values 

431 # over the static arguments. 

432 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters) 

433 all_parameters.update( 

434 project_path=project_path, 

435 output_path=output_path, 

436 run_index=run_index, 

437 generics=all_generics, 

438 synth_only=synth_only, 

439 from_impl=from_impl, 

440 num_threads=num_threads, 

441 ) 

442 

443 # The pre-build hooks (either project pre-build hook or any of the module's pre-build hooks) 

444 # might have side effects. E.g. change some register constants. So we make a deep copy of 

445 # the module list before any of these hooks are called. Note that the modules are copied 

446 # before the pre-create hook as well, since we do not know if we might be performing a 

447 # create-only or build-only operation. The copy does not take any significant time, so this 

448 # is not an issue. 

449 self.modules = deepcopy(self.modules) 

450 

451 result = BuildResult(self.name) 

452 

453 for module in self.modules: 

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

455 print( 

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

457 ) 

458 result.success = False 

459 return result 

460 

461 # Make sure register packages are up to date 

462 module.create_regs_vhdl_package() 

463 

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

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

466 result.success = False 

467 return result 

468 

469 build_vivado_project_tcl = self._build_tcl( 

470 project_path=project_path, 

471 output_path=output_path, 

472 num_threads=num_threads, 

473 run_index=run_index, 

474 all_generics=all_generics, 

475 synth_only=synth_only, 

476 from_impl=from_impl, 

477 ) 

478 

479 if not run_vivado_tcl(self._vivado_path, build_vivado_project_tcl): 

480 result.success = False 

481 return result 

482 

483 result.synthesis_size = self._get_size(project_path, f"synth_{run_index}") 

484 if self.report_logic_level_distribution: 

485 result.logic_level_distribution = self._get_logic_level_distribution( 

486 project_path, f"synth_{run_index}" 

487 ) 

488 

489 if not synth_only: 

490 impl_folder = project_path / f"{self.name}.runs" / f"impl_{run_index}" 

491 shutil.copy2(impl_folder / f"{self.top}.bit", output_path / f"{self.name}.bit") 

492 shutil.copy2(impl_folder / f"{self.top}.bin", output_path / f"{self.name}.bin") 

493 result.implementation_size = self._get_size(project_path, f"impl_{run_index}") 

494 

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

496 all_parameters.update(build_result=result) 

497 

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

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

500 result.success = False 

501 

502 return result 

503 

504 def open(self, project_path): 

505 """ 

506 Open the project in Vivado GUI. 

507 

508 Arguments: 

509 project_path (pathlib.Path): A path containing a Vivado project. 

510 

511 Returns: 

512 bool: True if everything went well. 

513 """ 

514 return run_vivado_gui(self._vivado_path, self.project_file(project_path)) 

515 

516 def _get_size(self, project_path, run): 

517 """ 

518 Reads the hierarchical utilization report and returns the top level size 

519 for the specified run. 

520 """ 

521 report_as_string = read_file( 

522 project_path / f"{self.name}.runs" / run / "hierarchical_utilization.rpt" 

523 ) 

524 return HierarchicalUtilizationParser.get_size(report_as_string) 

525 

526 def _get_logic_level_distribution(self, project_path, run): 

527 """ 

528 Reads the hierarchical utilization report and returns the top level size 

529 for the specified run. 

530 """ 

531 report_as_string = read_file( 

532 project_path / f"{self.name}.runs" / run / "logical_level_distribution.rpt" 

533 ) 

534 return LogicLevelDistributionParser.get_table(report_as_string) 

535 

536 def __str__(self): 

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

538 

539 if self.defined_at is not None: 

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

541 

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

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

544 

545 if self.static_generics: 

546 generics = self._dict_to_string(self.static_generics) 

547 else: 

548 generics = "-" 

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

550 

551 if self.other_arguments: 

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

553 

554 return result 

555 

556 @staticmethod 

557 def _dict_to_string(data): 

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

559 

560 

561class VivadoNetlistProject(VivadoProject): 

562 """ 

563 Used for handling Vivado build of a module without top level pinning. 

564 """ 

565 

566 def __init__(self, analyze_synthesis_timing=False, build_result_checkers=None, **kwargs): 

567 """ 

568 Arguments: 

569 analyze_synthesis_timing (bool): Enable analysis of the synthesized design's timing. 

570 This will make the build flow open the design, and check for unhandled clock 

571 crossings and pulse width violations. 

572 Enabling it will add significant build time (can be as much as +40%). 

573 Also, in order for clock crossing check to work, the clocks have to be created 

574 using a constraint file. 

575 build_result_checkers (list(SizeChecker, MaximumLogicLevel)): 

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

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

578 kwargs: Further arguments accepted by :meth:`.VivadoProject.__init__`. 

579 """ 

580 super().__init__(**kwargs) 

581 

582 self.is_netlist_build = True 

583 self.analyze_synthesis_timing = analyze_synthesis_timing 

584 self.report_logic_level_distribution = True 

585 self.build_result_checkers = [] if build_result_checkers is None else build_result_checkers 

586 

587 def build(self, **kwargs): # pylint: disable=arguments-differ 

588 """ 

589 Build the project. 

590 

591 Arguments: 

592 kwargs: All arguments as accepted by :meth:`.VivadoProject.build`. 

593 """ 

594 result = super().build(**kwargs) 

595 result.success = result.success and self._check_size(result) 

596 

597 return result 

598 

599 def _check_size(self, build_result): 

600 if not build_result.success: 

601 print(f"Can not do post_build check for {self.name} since it did not succeed.") 

602 return False 

603 

604 success = True 

605 for build_result_checker in self.build_result_checkers: 

606 checker_result = build_result_checker.check(build_result) 

607 success = success and checker_result 

608 

609 return success 

610 

611 

612class VivadoIpCoreProject(VivadoProject): 

613 """ 

614 A Vivado project that is only used to generate simulation models of IP cores. 

615 """ 

616 

617 def __init__(self, **kwargs): 

618 """ 

619 Arguments: 

620 kwargs: Arguments as accepted by :meth:`.VivadoProject.__init__`. 

621 """ 

622 super().__init__(**kwargs) 

623 

624 self.ip_cores_only = True 

625 

626 def build(self, **kwargs): # pylint: disable=arguments-differ 

627 """ 

628 Not implemented. 

629 """ 

630 raise NotImplementedError("IP core project can not be built") 

631 

632 

633def copy_and_combine_dicts(dict_first, dict_second): 

634 """ 

635 Will prefer values in the second dict, in case the same key occurs in both. 

636 Will return ``None`` if both are ``None``. 

637 """ 

638 if dict_first is None and dict_second is None: 

639 return None 

640 

641 if dict_first is None: 

642 return dict_second.copy() 

643 

644 if dict_second is None: 

645 return dict_first.copy() 

646 

647 result = dict_first.copy() 

648 result.update(dict_second) 

649 return result