Coverage for hdl_registers/register_list.py: 100%
158 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-29 22:03 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-29 22:03 +0000
1# --------------------------------------------------------------------------------------------------
2# Copyright (c) Lukas Vik. All rights reserved.
3#
4# This file is part of the hdl_registers project, a HDL register generator fast enough to be run
5# in real time.
6# https://hdl-registers.com
7# https://gitlab.com/hdl_registers/hdl_registers
8# --------------------------------------------------------------------------------------------------
10# Standard libraries
11import copy
12import datetime
13import hashlib
14import re
15from pathlib import Path
16from shutil import copy2
18# Third party libraries
19from tsfpga import DEFAULT_FILE_ENCODING
20from tsfpga.git_utils import get_git_commit, git_commands_are_available
21from tsfpga.svn_utils import get_svn_revision_information, svn_commands_are_available
22from tsfpga.system_utils import create_directory, create_file, read_file
24# Local folder libraries
25from . import __version__
26from .constant import Constant
27from .register import Register
28from .register_array import RegisterArray
29from .register_c_generator import RegisterCGenerator
30from .register_cpp_generator import RegisterCppGenerator
31from .register_html_generator import RegisterHtmlGenerator
32from .register_python_generator import RegisterPythonGenerator
33from .register_vhdl_generator import RegisterVhdlGenerator
36class RegisterList:
38 """
39 Used to handle the registers of a module. Also known as a register map.
40 """
42 def __init__(self, name, source_definition_file):
43 """
44 Arguments:
45 name (str): The name of this register list. Typically the name of the module that uses
46 it.
47 source_definition_file (pathlib.Path): The TOML source file that defined this
48 register list. Will be displayed in generated source code and documentation
49 for traceability.
50 """
51 self.name = name
52 self.source_definition_file = source_definition_file
54 self.register_objects = []
55 self.constants = []
57 @classmethod
58 def from_default_registers(cls, name, source_definition_file, default_registers):
59 """
60 Factory method. Create a RegisterList object from a plain list of registers.
62 Arguments:
63 name (str): The name of this register list.
64 source_definition_file (pathlib.Path): The source file that defined this
65 register list. Will be displayed in generated source code and documentation
66 for traceability.
68 Can be set to ``None`` if this information does not make sense in the current
69 use case.
70 default_registers (list(Register)): These registers will be inserted in the
71 register list.
72 """
73 register_list = cls(name=name, source_definition_file=source_definition_file)
74 register_list.register_objects = copy.deepcopy(default_registers)
75 return register_list
77 def append_register(self, name, mode, description):
78 """
79 Append a register to this list.
81 Arguments:
82 name (str): The name of the register.
83 mode (str): A valid register mode.
84 description (str): Textual register description.
85 Return:
86 Register: The register object that was created.
87 """
88 if self.register_objects:
89 index = self.register_objects[-1].index + 1
90 else:
91 index = 0
93 register = Register(name, index, mode, description)
94 self.register_objects.append(register)
96 return register
98 def append_register_array(self, name, length, description):
99 """
100 Append a register array to this list.
102 Arguments:
103 name (str): The name of the register array.
104 length (int): The number of times the register sequence shall be repeated.
105 Return:
106 RegisterArray: The register array object that was created.
107 """
108 if self.register_objects:
109 base_index = self.register_objects[-1].index + 1
110 else:
111 base_index = 0
112 register_array = RegisterArray(
113 name=name, base_index=base_index, length=length, description=description
114 )
116 self.register_objects.append(register_array)
117 return register_array
119 def get_register(self, name):
120 """
121 Get a register from this list. Will only find single registers, not registers in a
122 register array. Will raise exception if no register matches.
124 Arguments:
125 name (str): The name of the register.
126 Return:
127 Register: The register.
128 """
129 for register_object in self.register_objects:
130 if isinstance(register_object, Register) and register_object.name == name:
131 return register_object
133 raise ValueError(f'Could not find register "{name}" within register list "{self.name}"')
135 def get_register_array(self, name):
136 """
137 Get a register array from this list. Will raise exception if no register array matches.
139 Arguments:
140 name (str): The name of the register array.
141 Return:
142 RegisterArray: The register array.
143 """
144 for register_object in self.register_objects:
145 if isinstance(register_object, RegisterArray) and register_object.name == name:
146 return register_object
148 raise ValueError(
149 f'Could not find register array "{name}" within register list "{self.name}"'
150 )
152 def get_register_index(
153 self, register_name, register_array_name=None, register_array_index=None
154 ):
155 """
156 Get the zero-based index within the register list for the specified register.
158 Arguments:
159 register_name (str): The name of the register.
160 register_array_name (str): If the register is within a register array, the name
161 of the array must be specified.
162 register_array_name (str): If the register is within a register array, the array
163 iteration index must be specified.
165 Return:
166 int: The index.
167 """
168 if register_array_name is None and register_array_index is None:
169 # Target is plain register
170 register = self.get_register(register_name)
172 return register.index
174 # Target is in register array
175 register_array = self.get_register_array(register_array_name)
176 register_array_start_index = register_array.get_start_index(register_array_index)
178 register = register_array.get_register(register_name)
179 register_index = register.index
181 return register_array_start_index + register_index
183 def add_constant(self, name, value, description=None):
184 """
185 Add a constant. Will be available in the generated packages and headers.
187 Arguments:
188 name (str): The name of the constant.
189 value (bool, int, str): The constant value.
190 description (str): Textual description for the constant.
191 Return:
192 Constant: The constant object that was created.
193 """
194 constant = Constant(name=name, value=value, description=description)
195 self.constants.append(constant)
196 return constant
198 def get_constant(self, name):
199 """
200 Get a constant from this list. Will raise exception if no constant matches.
202 Arguments:
203 name (str): The name of the constant.
204 Return:
205 Constant: The constant.
206 """
207 for constant in self.constants:
208 if constant.name == name:
209 return constant
211 raise ValueError(f'Could not find constant "{name}" within register list "{self.name}"')
213 def create_vhdl_package(self, output_path):
214 """
215 Create a VHDL package file with register and field definitions.
217 This function assumes that the ``output_path`` folder already exists. This assumption makes
218 it slightly faster than the other functions that use ``create_file()``. Necessary since this
219 one is often used in real time (before simulations, etc..) and not in one-off scenarios
220 like the others (when making a release).
222 In order to save time, there is a mechanism to only generate the VHDL file when necessary.
223 A hash of this register list object will be written to the file along with all the register
224 definitions. This hash will be inspected and compared, and the VHDL file will only be
225 generated again if something has changed.
227 Arguments:
228 output_path (pathlib.Path): Result will be placed here.
229 """
230 vhd_file = output_path / (self.name + "_regs_pkg.vhd")
232 self_hash = self._hash()
233 if self._should_create_vhdl_package(vhd_file, self_hash):
234 self._create_vhdl_package(vhd_file, self_hash)
236 def _should_create_vhdl_package(self, vhd_file, self_hash):
237 if not vhd_file.exists():
238 return True
239 if (self_hash, __version__) != self._find_hash_and_version_of_existing_vhdl_package(
240 vhd_file
241 ):
242 return True
243 return False
245 @staticmethod
246 def _find_hash_and_version_of_existing_vhdl_package(vhd_file):
247 """
248 Returns `None` if nothing found, otherwise the matching strings in a tuple.
249 """
250 regexp = re.compile(r"\n-- Register hash ([0-9a-f]+), generator version (\S+)\.\n")
251 existing_file_content = read_file(vhd_file)
252 match = regexp.search(existing_file_content)
253 if match is None:
254 return None
255 return match.group(1), match.group(2)
257 def _create_vhdl_package(self, vhd_file, self_hash):
258 print(f"Creating VHDL register package {vhd_file}")
259 # Add a header line with the hash
260 generated_info = self.generated_source_info() + [
261 f"Register hash {self_hash}, generator version {__version__}."
262 ]
263 register_vhdl_generator = RegisterVhdlGenerator(self.name, generated_info)
264 with open(vhd_file, "w", encoding=DEFAULT_FILE_ENCODING) as file_handle:
265 file_handle.write(
266 register_vhdl_generator.get_package(self.register_objects, self.constants)
267 )
269 def create_c_header(self, output_path, file_name=None):
270 """
271 Create a C header file with register and field definitions.
273 Arguments:
274 output_path (pathlib.Path): Result will be placed here.
275 file_name (str): Optionally specify an explicit file name.
276 """
277 file_name = f"{self.name}_regs.h" if file_name is None else file_name
278 output_file = output_path / file_name
280 register_c_generator = RegisterCGenerator(self.name, self.generated_source_info())
281 create_file(
282 output_file, register_c_generator.get_header(self.register_objects, self.constants)
283 )
285 def create_cpp_interface(self, output_path):
286 """
287 Create a C++ class interface header file, with register and field definitions. The
288 interface header contains only virtual methods.
290 Arguments:
291 output_path (pathlib.Path): Result will be placed here.
292 """
293 output_file = output_path / ("i_" + self.name + ".h")
294 register_cpp_generator = RegisterCppGenerator(self.name, self.generated_source_info())
295 create_file(
296 output_file, register_cpp_generator.get_interface(self.register_objects, self.constants)
297 )
299 def create_cpp_header(self, output_path):
300 """
301 Create a C++ class header file.
303 Arguments:
304 output_path (pathlib.Path): Result will be placed here.
305 """
306 output_file = output_path / (self.name + ".h")
307 register_cpp_generator = RegisterCppGenerator(self.name, self.generated_source_info())
308 create_file(output_file, register_cpp_generator.get_header(self.register_objects))
310 def create_cpp_implementation(self, output_path):
311 """
312 Create a C++ class implementation file.
314 Arguments:
315 output_path (pathlib.Path): Result will be placed here.
316 """
317 output_file = output_path / (self.name + ".cpp")
318 register_cpp_generator = RegisterCppGenerator(self.name, self.generated_source_info())
319 create_file(output_file, register_cpp_generator.get_implementation(self.register_objects))
321 def create_html_page(self, output_path):
322 """
323 Create a documentation HTML page with register and field information. Will include the
324 tables created by :meth:`.create_html_register_table` and
325 :meth:`.create_html_constant_table`.
327 Arguments:
328 output_path (pathlib.Path): Result will be placed here.
329 """
330 register_html_generator = RegisterHtmlGenerator(self.name, self.generated_source_info())
332 html_file = output_path / (self.name + "_regs.html")
333 create_file(
334 html_file, register_html_generator.get_page(self.register_objects, self.constants)
335 )
337 stylesheet = register_html_generator.get_page_style()
338 stylesheet_file = output_path / "regs_style.css"
339 if (not stylesheet_file.exists()) or read_file(stylesheet_file) != stylesheet:
340 # Create the file only once. This mechanism could be made more smart, but at the moment
341 # there is no use case. Perhaps there should be a separate stylesheet for each
342 # HTML file?
343 create_file(stylesheet_file, stylesheet)
345 def create_html_register_table(self, output_path):
346 """
347 Create documentation HTML table with register and field information.
349 Arguments:
350 output_path (pathlib.Path): Result will be placed here.
351 """
352 output_file = output_path / (self.name + "_register_table.html")
353 register_html_generator = RegisterHtmlGenerator(self.name, self.generated_source_info())
354 create_file(output_file, register_html_generator.get_register_table(self.register_objects))
356 def create_html_constant_table(self, output_path):
357 """
358 Create documentation HTML table with constant information.
360 Arguments:
361 output_path (pathlib.Path): Result will be placed here.
362 """
363 output_file = output_path / (self.name + "_constant_table.html")
364 register_html_generator = RegisterHtmlGenerator(self.name, self.generated_source_info())
365 create_file(output_file, register_html_generator.get_constant_table(self.constants))
367 def create_python_class(self, output_path):
368 """
369 Save a python class with all register and constant information.
371 Arguments:
372 output_path (pathlib.Path): Result will be placed here.
373 """
374 register_python_generator = RegisterPythonGenerator(self.name, self.generated_source_info())
375 register_python_generator.create_class(register_list=self, output_folder=output_path)
377 def copy_source_definition(self, output_path):
378 """
379 Copy the source file that created this register list. If no source file is set, nothing will
380 be copied.
382 Arguments:
383 output_path (pathlib.Path): Result will be placed here.
384 """
385 if self.source_definition_file is not None:
386 create_directory(output_path, empty=False)
387 copy2(self.source_definition_file, output_path)
389 @staticmethod
390 def generated_info():
391 """
392 Return:
393 list(str): Line(s) informing the user that a file is automatically generated.
394 """
395 return ["This file is automatically generated by hdl_registers."]
397 def generated_source_info(self):
398 """
399 Return:
400 list(str): Line(s) informing the user that a file is automatically generated, containing
401 info about the source of the generated register information.
402 """
403 # Default to the user's current working directory
404 directory = Path(".")
406 time_info = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
408 file_info = ""
409 if self.source_definition_file is not None:
410 directory = self.source_definition_file.parent
411 file_info = f" from file {self.source_definition_file.name}"
413 commit_info = ""
414 if git_commands_are_available(directory):
415 commit_info = f" at commit {get_git_commit(directory)}"
416 elif svn_commands_are_available(directory):
417 commit_info = f" at revision {get_svn_revision_information(directory)}"
419 info = f"Generated {time_info}{file_info}{commit_info}."
421 return self.generated_info() + [info]
423 def _hash(self):
424 """
425 Get a hash of this object representation. SHA1 is the fastest method according to e.g.
426 http://atodorov.org/blog/2013/02/05/performance-test-md5-sha1-sha256-sha512/
427 Result is a lowercase hexadecimal string.
428 """
429 return hashlib.sha1(repr(self).encode()).hexdigest()
431 def __repr__(self):
432 return f"""{self.__class__.__name__}(\
433name={self.name},\
434source_definition_file={repr(self.source_definition_file)},\
435register_objects={','.join([repr(register_object) for register_object in self.register_objects])},\
436constants={','.join([repr(constant) for constant in self.constants])},\
437)"""