Python code generator
The Python code generator creates an accessor class for reading, writing and printing register and
field values on a target device.
The class will have a named method for each of these operations on each register and field.
The methods use native Python types (e.g. int
, float
, Enum
) to represent values that
are read or written.
This means that the user never has to do any bit slicing or casting.
PythonPickleGenerator
saves a Pythonpickle
file containing theRegisterList
, along with a small wrapper file to recreate the object.PythonAccessorGenerator
creates a Python class with methods to read/write/print each register and field.
These can be used in a Python-based system test environment to conveniently perform register read/writes on the target device.
The artifacts are generated like this:
1# Standard libraries
2import sys
3from pathlib import Path
4
5# First party libraries
6from hdl_registers.generator.python.accessor import PythonAccessorGenerator
7from hdl_registers.generator.python.pickle import PythonPickleGenerator
8from hdl_registers.parser.toml import from_toml
9
10THIS_DIR = Path(__file__).parent
11
12
13def main(output_folder: Path):
14 """
15 Create register Python artifacts from the TOML example file.
16 """
17 register_list = from_toml(
18 name="example",
19 toml_file=THIS_DIR.parent.parent / "user_guide" / "toml" / "toml_format.toml",
20 )
21
22 PythonPickleGenerator(register_list=register_list, output_folder=output_folder).create()
23
24 PythonAccessorGenerator(register_list=register_list, output_folder=output_folder).create()
25
26
27if __name__ == "__main__":
28 main(output_folder=Path(sys.argv[1]))
Pickle code
The script below is generated by the PythonPickleGenerator
and can be used to re-create
the RegisterList
object from the Python pickle
file.
Accessor code
The class below is generated by the PythonAccessorGenerator
for performing register and field operations on a target device.
Interesting things to notice:
Register read/write operations use a generated Python
dataclass
to represent a register with field value members. See e.g.read_config
andwrite_config
methods, and theExampleConfigValue
type in the generated code.Field values are represented using native Python types.
A bit field is represented as a Python
int
.A bit vector field WITHOUT fractional bits, whether signed or unsigned, is represented as a Python
int
.A bit vector field WITH fractional bits, whether signed or unsigned, is represented as a Python
float
.An enumeration field is represented using a custom Python
Enum
type. See e.g.ExampleConfigValue.Direction
.An integer field, whether signed or unsigned, is represented as a Python
int
.
In cases where the register has no fields, the read and write use a plain integer instead. See e.g.
read_status
.Each writeable register has methods to write
The entire register value (e.g.
write_config
), which takes a dataclass value as argument.Individual field values (e.g.
write_config_direction
), which takes the native Python field representation as argument.
Field write methods will either read-modify-write or plain write the register, depending on what the register mode allows.
Printing values
Each register value type has a to_string
method to convert the field values to a
human-readable string.
The accessor class also implements the print_registers
method, which will print all register
and field values:
Note how only readable registers are printed. Field values are presented firstly as their native type, but also as their decimal, hexadecimal and binary encodings.
Color
By default, color output is enabled, since it improves readability when a lot of information
is presented.
It can be turned of by setting the no_color
argument to print_registers
, or by setting
the environment variable NO_COLOR
to any value.
Register accessor interface
The code generated by PythonAccessorGenerator
performs
the address calculation, mode logic, bit slicing, and type casting for you.
It can not, however, implement the actual reading and writing of register values on your
target device.
This must be implemented by the user, given the methods of access that are available
in your environment (SSH, telnet, UART, etc).
Hdl-registers can not implement this in a generic way that is useful in all environments, since
device setup is so different in different projects.
The class constructor takes a register_accessor
argument of type
PythonRegisterAccessorInterface
.
This object must be constructed by the user, must inherit the
PythonRegisterAccessorInterface
class, and must implement the read_register
and
write_register
methods.
See the API documentation
for more details.
Code
Example code generated by PythonAccessorGenerator
.
Using the generated code is done by
Generating artifacts as shown here.
Adding the generated Python module to your Python PATH.
Instantiating the object, by either
Class instantiation, e.g.
accessor = ExampleAccessor(register_accessor=register_accessor)
, where “Example” in the class name is the name of the register list.Running the convenience function
accessor = get_accessor(register_accessor=register_accessor)
where the register list name is not needed anywhere.
1# ------------------------------------------------------------------------------
2# This file is automatically generated by hdl-registers version 7.0.2-dev.
3# Code generator PythonAccessorGenerator version 0.1.0.
4# Generated 2025-01-21 20:52 from file toml_format.toml at commit 3c3e6c67d817.
5# Register hash 4df9765ebb584803b583628671e4659579eb85f4.
6# ------------------------------------------------------------------------------
7
8# Standard libraries
9import pickle
10from dataclasses import dataclass
11from enum import Enum
12from pathlib import Path
13from typing import TYPE_CHECKING
14from termcolor import colored
15
16# Third party libraries
17from hdl_registers.field.numerical_interpretation import to_unsigned_binary
18from tsfpga.math_utils import to_binary_nibble_string, to_hex_byte_string
19
20if TYPE_CHECKING:
21 # Third party libraries
22 from hdl_registers.generator.python.register_accessor_interface import (
23 PythonRegisterAccessorInterface,
24 )
25 from hdl_registers.register_list import RegisterList
26
27
28THIS_DIR = Path(__file__).parent
29
30
31class ExampleAccessor:
32 """
33 Class to read/write registers and fields of the ``example`` register list.
34 """
35
36 def __init__(self, register_accessor: "PythonRegisterAccessorInterface"):
37 """
38 Arguments:
39 register_accessor: Object that implements register access methods in your system.
40 Must have the methods ``read_register`` and ``write_register``
41 with the same interface and behavior as the interface class
42 :class:`.PythonRegisterAccessorInterface`.
43 It is highly recommended that your accessor class inherits from that class.
44 """
45 self._register_accessor = register_accessor
46
47 pickle_path = THIS_DIR / "example.pickle"
48 if not pickle_path.exists():
49 raise FileNotFoundError(
50 f"Could not find the pickle file {pickle_path}, "
51 "make sure this artifact is generated."
52 )
53
54 with pickle_path.open("rb") as file_handle:
55 self._register_list: "RegisterList" = pickle.load(file_handle)
56
57 def _read_register(self, register_index: int) -> int:
58 """
59 Read a register value via the register accessor provided by the user.
60 We perform sanity checks in both directions here, since this is the glue logic between
61 two interfaces.
62 """
63 if not 0 <= register_index <= 9:
64 raise ValueError(
65 f"Register index {register_index} out of range. This is likely an internal error."
66 )
67
68 register_value = self._register_accessor.read_register(
69 register_list_name="example", register_address=4 * register_index
70 )
71 if not 0 <= register_value < 2**32:
72 raise ValueError(
73 f'Register read value "{register_value}" from accessor is out of range.'
74 )
75
76 return register_value
77
78 def _write_register(self, register_index: int, register_value: int) -> None:
79 """
80 Write a register value via the register accessor provided by the user.
81 We perform sanity checks in both directions here, since this is the glue logic between
82 two interfaces.
83 """
84 if not 0 <= register_index <= 9:
85 raise ValueError(
86 f"Register index {register_index} out of range. This is likely an internal error."
87 )
88
89 if not 0 <= register_value < 2**32:
90 raise ValueError(f'Register write value "{register_value}" is out of range.')
91
92 self._register_accessor.write_register(
93 register_list_name="example",
94 register_address=4 * register_index,
95 register_value=register_value,
96 )
97
98 def read_config(self) -> "ExampleConfigValue":
99 """
100 Read the 'config' register.
101 """
102 index = self._register_list.get_register_index(
103 register_name="config",
104 register_array_name=None,
105 register_array_index=None,
106 )
107 value = self._read_register(register_index=index)
108 register = self._register_list.get_register(
109 register_name="config", register_array_name=None
110 )
111 return ExampleConfigValue(
112 enable=register.fields[0].get_value(register_value=value),
113 direction=ExampleConfigValue.Direction(
114 register.fields[1].get_value(register_value=value).name
115 ),
116 )
117
118 def write_config(self, register_value: "ExampleConfigValue") -> None:
119 """
120 Write the whole 'config' register.
121
122 Arguments:
123 register_value: Object with the field values that shall be written.
124 """
125 index = self._register_list.get_register_index(
126 register_name="config",
127 register_array_name=None,
128 register_array_index=None,
129 )
130 register = self._register_list.get_register(
131 register_name="config", register_array_name=None
132 )
133 integer_value = 0
134 integer_value += register.fields[0].set_value(field_value=register_value.enable)
135 integer_value += register.fields[1].set_value(
136 field_value=register.fields[1].get_element_by_name(register_value.direction.value)
137 )
138 self._write_register(register_index=index, register_value=integer_value)
139
140 def write_config_enable(self, field_value: "int") -> None:
141 """
142 Write the 'enable' field in the 'config' register.
143 Will read-modify-write the ``config`` register, setting the ``enable`` field
144 to the provided value, while leaving the other fields at their previous values.
145
146 Arguments:
147 field_value: The field value to be written.
148 Single bit. Range: 0-1.
149 """
150 register_value = self.read_config()
151 register_value.enable = field_value
152 self.write_config(register_value=register_value)
153
154 def write_config_direction(self, field_value: "ExampleConfigValue.Direction") -> None:
155 """
156 Write the 'direction' field in the 'config' register.
157 Will read-modify-write the ``config`` register, setting the ``direction`` field
158 to the provided value, while leaving the other fields at their previous values.
159
160 Arguments:
161 field_value: The field value to be written.
162 Enumeration. Possible values: DATA_IN, HIGH_Z, DATA_OUT.
163 """
164 register_value = self.read_config()
165 register_value.direction = field_value
166 self.write_config(register_value=register_value)
167
168 def read_status(self) -> "int":
169 """
170 Read the 'status' register.
171 Return type is a plain unsigned integer since the register has no fields.
172 """
173 index = self._register_list.get_register_index(
174 register_name="status",
175 register_array_name=None,
176 register_array_index=None,
177 )
178 value = self._read_register(register_index=index)
179 return value
180
181 def read_channels_read_address(self, array_index: int) -> "int":
182 """
183 Read the 'read_address' register within the 'channels' register array.
184 Return type is a plain unsigned integer since the register has no fields.
185
186 Arguments:
187 array_index: Register array iteration index (range 0-3).
188 """
189 index = self._register_list.get_register_index(
190 register_name="read_address",
191 register_array_name="channels",
192 register_array_index=array_index,
193 )
194 value = self._read_register(register_index=index)
195 return value
196
197 def write_channels_read_address(self, register_value: "int", array_index: int) -> None:
198 """
199 Write the whole 'read_address' register within the 'channels' register array.
200
201 Arguments:
202 register_value: Value to be written.
203 Is a plain unsigned integer since the register has no fields.
204 array_index: Register array iteration index (range 0-3).
205 """
206 index = self._register_list.get_register_index(
207 register_name="read_address",
208 register_array_name="channels",
209 register_array_index=array_index,
210 )
211 self._write_register(register_index=index, register_value=register_value)
212
213 def write_channels_config(
214 self, register_value: "ExampleChannelsConfigValue", array_index: int
215 ) -> None:
216 """
217 Write the whole 'config' register within the 'channels' register array.
218
219 Arguments:
220 register_value: Object with the field values that shall be written.
221 array_index: Register array iteration index (range 0-3).
222 """
223 index = self._register_list.get_register_index(
224 register_name="config",
225 register_array_name="channels",
226 register_array_index=array_index,
227 )
228 register = self._register_list.get_register(
229 register_name="config", register_array_name="channels"
230 )
231 integer_value = 0
232 integer_value += register.fields[0].set_value(field_value=register_value.enable)
233 integer_value += register.fields[1].set_value(field_value=register_value.tuser)
234 self._write_register(register_index=index, register_value=integer_value)
235
236 def write_channels_config_enable(self, field_value: "int", array_index: int) -> None:
237 """
238 Write the 'enable' field in the 'config' register within the 'channels' register array.
239 Will write the ``config`` register, setting the ``enable`` field to the
240 provided value, and all other fields to their default values.
241
242 Arguments:
243 field_value: The field value to be written.
244 Single bit. Range: 0-1.
245 array_index: Register array iteration index (range 0-3).
246 """
247 index = self._register_list.get_register_index(
248 register_name="config",
249 register_array_name="channels",
250 register_array_index=array_index,
251 )
252
253 register = self._register_list.get_register(
254 register_name="config", register_array_name="channels"
255 )
256 register_value = 0
257 register_value += register.fields[0].set_value(field_value)
258 register_value += register.fields[1].default_value_uint << register.fields[1].base_index
259
260 self._write_register(register_index=index, register_value=register_value)
261
262 def write_channels_config_tuser(self, field_value: "int", array_index: int) -> None:
263 """
264 Write the 'tuser' field in the 'config' register within the 'channels' register array.
265 Will write the ``config`` register, setting the ``tuser`` field to the
266 provided value, and all other fields to their default values.
267
268 Arguments:
269 field_value: The field value to be written.
270 Unsigned bit vector. Width: 8. Range: 0 - 255.
271 array_index: Register array iteration index (range 0-3).
272 """
273 index = self._register_list.get_register_index(
274 register_name="config",
275 register_array_name="channels",
276 register_array_index=array_index,
277 )
278
279 register = self._register_list.get_register(
280 register_name="config", register_array_name="channels"
281 )
282 register_value = 0
283 register_value += register.fields[0].default_value_uint << register.fields[0].base_index
284 register_value += register.fields[1].set_value(field_value)
285
286 self._write_register(register_index=index, register_value=register_value)
287
288 def print_registers(self, no_color: bool = False) -> None:
289 """
290 Print all registers and their values to STDOUT.
291
292 Arguments:
293 no_color: Disable color output.
294 Can also be achieved by setting the environment variable ``NO_COLOR`` to any value.
295 """
296 print(
297 colored("Register", color="light_yellow", no_color=no_color)
298 + " 'config' "
299 + colored("." * 61, color="dark_grey", no_color=no_color)
300 + " (index 0, address 0):"
301 )
302 register_value = self.read_config()
303 print(register_value.to_string(indentation=" ", no_color=no_color))
304 print()
305
306 print(
307 colored("Register", color="light_yellow", no_color=no_color)
308 + " 'status' "
309 + colored("." * 61, color="dark_grey", no_color=no_color)
310 + " (index 1, address 4):"
311 )
312 register_value = self.read_status()
313 formatted_value = _format_unsigned_number(value=register_value, width=32)
314 print(f" {formatted_value}\n")
315
316 print(
317 colored("Register", color="light_yellow", no_color=no_color)
318 + " 'channels[0].read_address' "
319 + colored("." * 43, color="dark_grey", no_color=no_color)
320 + " (index 2, address 8):"
321 )
322 register_value = self.read_channels_read_address(array_index=0)
323 formatted_value = _format_unsigned_number(value=register_value, width=32)
324 print(f" {formatted_value}\n")
325
326 print(
327 colored("Register", color="light_yellow", no_color=no_color)
328 + " 'channels[0].config' "
329 + colored("." * 49, color="dark_grey", no_color=no_color)
330 + " (index 3, address 12):"
331 )
332 print(" Not readable.\n")
333
334 print(
335 colored("Register", color="light_yellow", no_color=no_color)
336 + " 'channels[1].read_address' "
337 + colored("." * 43, color="dark_grey", no_color=no_color)
338 + " (index 4, address 16):"
339 )
340 register_value = self.read_channels_read_address(array_index=1)
341 formatted_value = _format_unsigned_number(value=register_value, width=32)
342 print(f" {formatted_value}\n")
343
344 print(
345 colored("Register", color="light_yellow", no_color=no_color)
346 + " 'channels[1].config' "
347 + colored("." * 49, color="dark_grey", no_color=no_color)
348 + " (index 5, address 20):"
349 )
350 print(" Not readable.\n")
351
352 print(
353 colored("Register", color="light_yellow", no_color=no_color)
354 + " 'channels[2].read_address' "
355 + colored("." * 43, color="dark_grey", no_color=no_color)
356 + " (index 6, address 24):"
357 )
358 register_value = self.read_channels_read_address(array_index=2)
359 formatted_value = _format_unsigned_number(value=register_value, width=32)
360 print(f" {formatted_value}\n")
361
362 print(
363 colored("Register", color="light_yellow", no_color=no_color)
364 + " 'channels[2].config' "
365 + colored("." * 49, color="dark_grey", no_color=no_color)
366 + " (index 7, address 28):"
367 )
368 print(" Not readable.\n")
369
370 print(
371 colored("Register", color="light_yellow", no_color=no_color)
372 + " 'channels[3].read_address' "
373 + colored("." * 43, color="dark_grey", no_color=no_color)
374 + " (index 8, address 32):"
375 )
376 register_value = self.read_channels_read_address(array_index=3)
377 formatted_value = _format_unsigned_number(value=register_value, width=32)
378 print(f" {formatted_value}\n")
379
380 print(
381 colored("Register", color="light_yellow", no_color=no_color)
382 + " 'channels[3].config' "
383 + colored("." * 49, color="dark_grey", no_color=no_color)
384 + " (index 9, address 36):"
385 )
386 print(" Not readable.\n")
387
388
389def get_accessor(register_accessor: "PythonRegisterAccessorInterface") -> ExampleAccessor:
390 """
391 Factory function to create an accessor object.
392 """
393 return ExampleAccessor(register_accessor=register_accessor)
394
395
396def _format_unsigned_number(value: int, width: int, include_decimal: bool = True) -> str:
397 """
398 Format a string representation of the provided ``value``.
399 Suitable for printing field and register values.
400 """
401 result = ""
402 if include_decimal:
403 result += f"unsigned decimal {value}, "
404
405 hex_string = to_hex_byte_string(value=value, result_width_bits=width)
406 result += f"hexadecimal {hex_string}, "
407
408 binary_string = to_binary_nibble_string(value=value, result_width_bits=width)
409 result += f"binary {binary_string}"
410
411 return result
412
413
414@dataclass
415class ExampleConfigValue:
416 """
417 Represents the field values of the 'config' register.
418 Uses native Python types for easy handling.
419 """
420
421 class Direction(Enum):
422 """
423 Enumeration elements for the ``direction`` field.
424 """
425
426 DATA_IN = "data_in"
427 HIGH_Z = "high_z"
428 DATA_OUT = "data_out"
429
430 # Single bit. Range: 0-1.
431 enable: int
432 # Enumeration. Possible values: DATA_IN, HIGH_Z, DATA_OUT.
433 direction: Direction
434
435 def to_string(self, indentation: str = "", no_color: bool = False) -> str:
436 """
437 Get a string of the field values, suitable for printing.
438
439 Arguments:
440 indentation: Optionally indent each field value line.
441 no_color: Disable color output.
442 Can also be achieved by setting the environment variable ``NO_COLOR`` to any value.
443 """
444 values = []
445
446 value = str(self.enable)
447 field_name = colored("enable", color="light_cyan", no_color=no_color)
448 values.append(f"{indentation}{field_name}: {value}")
449
450 enum_index = list(self.Direction).index(self.direction)
451 value = f"{self.direction.name} ({enum_index})"
452 field_name = colored("direction", color="light_cyan", no_color=no_color)
453 values.append(f"{indentation}{field_name}: {value}")
454
455 return "\n".join(values)
456
457
458@dataclass
459class ExampleChannelsConfigValue:
460 """
461 Represents the field values of the 'config' register within the 'channels' register array.
462 Uses native Python types for easy handling.
463 """
464
465 # Single bit. Range: 0-1.
466 enable: int
467 # Unsigned bit vector. Width: 8. Range: 0 - 255.
468 tuser: int
469
470 def to_string(self, indentation: str = "", no_color: bool = False) -> str:
471 """
472 Get a string of the field values, suitable for printing.
473
474 Arguments:
475 indentation: Optionally indent each field value line.
476 no_color: Disable color output.
477 Can also be achieved by setting the environment variable ``NO_COLOR`` to any value.
478 """
479 values = []
480
481 value = str(self.enable)
482 field_name = colored("enable", color="light_cyan", no_color=no_color)
483 values.append(f"{indentation}{field_name}: {value}")
484
485 value_comment = _format_unsigned_number(value=self.tuser, width=8, include_decimal=False)
486 value = f"{self.tuser} ({value_comment})"
487 field_name = colored("tuser", color="light_cyan", no_color=no_color)
488 values.append(f"{indentation}{field_name}: {value}")
489
490 return "\n".join(values)