Python 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:
1import sys
2from pathlib import Path
3
4from hdl_registers.generator.python.accessor import PythonAccessorGenerator
5from hdl_registers.generator.python.pickle import PythonPickleGenerator
6from hdl_registers.parser.toml import from_toml
7
8THIS_DIR = Path(__file__).parent
9
10
11def main(output_folder: Path) -> None:
12 """
13 Create register Python artifacts from the TOML example file.
14 """
15 register_list = from_toml(
16 name="example",
17 toml_file=THIS_DIR.parent.parent / "user_guide" / "toml" / "toml_format.toml",
18 )
19
20 PythonPickleGenerator(register_list=register_list, output_folder=output_folder).create()
21
22 PythonAccessorGenerator(register_list=register_list, output_folder=output_folder).create()
23
24
25if __name__ == "__main__":
26 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:

Example register printout with many fields of different types.
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.3.1-dev.
3# Code generator PythonAccessorGenerator version 0.1.0.
4# Generated 2025-04-01 11:34 from file toml_format.toml at Git commit 228a22928e9a.
5# Register hash bb6496bff8f74abb3efe59e498c2b2a3f7b4149a.
6# ------------------------------------------------------------------------------
7
8import pickle
9from dataclasses import dataclass
10from enum import Enum
11from pathlib import Path
12from typing import TYPE_CHECKING
13from termcolor import colored
14
15from hdl_registers.field.numerical_interpretation import to_unsigned_binary
16from tsfpga.math_utils import to_binary_nibble_string, to_hex_byte_string
17
18if TYPE_CHECKING:
19 from hdl_registers.generator.python.register_accessor_interface import (
20 PythonRegisterAccessorInterface,
21 )
22 from hdl_registers.register_list import RegisterList
23
24
25THIS_DIR = Path(__file__).parent
26
27
28class ExampleAccessor:
29 """
30 Class to read/write registers and fields of the ``example`` register list.
31 """
32
33 def __init__(self, register_accessor: "PythonRegisterAccessorInterface"):
34 """
35 Arguments:
36 register_accessor: Object that implements register access methods in your system.
37 Must have the methods ``read_register`` and ``write_register``
38 with the same interface and behavior as the interface class
39 :class:`.PythonRegisterAccessorInterface`.
40 It is highly recommended that your accessor class inherits from that class.
41 """
42 self._register_accessor = register_accessor
43
44 pickle_path = THIS_DIR / "example.pickle"
45 if not pickle_path.exists():
46 raise FileNotFoundError(
47 f"Could not find the pickle file {pickle_path}, "
48 "make sure this artifact is generated."
49 )
50
51 with pickle_path.open("rb") as file_handle:
52 self._register_list: "RegisterList" = pickle.load(file_handle)
53
54 def _read_register(self, register_index: int) -> int:
55 """
56 Read a register value via the register accessor provided by the user.
57 We perform sanity checks in both directions here, since this is the glue logic between
58 two interfaces.
59 """
60 if not 0 <= register_index <= 9:
61 raise ValueError(
62 f"Register index {register_index} out of range. This is likely an internal error."
63 )
64
65 register_value = self._register_accessor.read_register(
66 register_list_name="example", register_address=4 * register_index
67 )
68 if not 0 <= register_value < 2**32:
69 raise ValueError(
70 f'Register read value "{register_value}" from accessor is out of range.'
71 )
72
73 return register_value
74
75 def _write_register(self, register_index: int, register_value: int) -> None:
76 """
77 Write a register value via the register accessor provided by the user.
78 We perform sanity checks in both directions here, since this is the glue logic between
79 two interfaces.
80 """
81 if not 0 <= register_index <= 9:
82 raise ValueError(
83 f"Register index {register_index} out of range. This is likely an internal error."
84 )
85
86 if not 0 <= register_value < 2**32:
87 raise ValueError(f'Register write value "{register_value}" is out of range.')
88
89 self._register_accessor.write_register(
90 register_list_name="example",
91 register_address=4 * register_index,
92 register_value=register_value,
93 )
94
95 def read_conf(self) -> "ExampleConfValue":
96 """
97 Read the 'conf' register.
98 """
99 index = self._register_list.get_register_index(
100 register_name="conf",
101 register_array_name=None,
102 register_array_index=None,
103 )
104 value = self._read_register(register_index=index)
105 register = self._register_list.get_register(register_name="conf", register_array_name=None)
106 return ExampleConfValue(
107 enable=register.fields[0].get_value(register_value=value),
108 direction=ExampleConfValue.Direction(
109 register.fields[1].get_value(register_value=value).name
110 ),
111 )
112
113 def write_conf(self, register_value: "ExampleConfValue") -> None:
114 """
115 Write the whole 'conf' register.
116
117 Arguments:
118 register_value: Object with the field values that shall be written.
119 """
120 index = self._register_list.get_register_index(
121 register_name="conf",
122 register_array_name=None,
123 register_array_index=None,
124 )
125 register = self._register_list.get_register(register_name="conf", register_array_name=None)
126 integer_value = 0
127 integer_value += register.fields[0].set_value(field_value=register_value.enable)
128 integer_value += register.fields[1].set_value(
129 field_value=register.fields[1].get_element_by_name(register_value.direction.value)
130 )
131 self._write_register(register_index=index, register_value=integer_value)
132
133 def write_conf_enable(self, field_value: "int") -> None:
134 """
135 Write the 'enable' field in the 'conf' register.
136 Will read-modify-write the ``conf`` register, setting the ``enable`` field
137 to the provided value, while leaving the other fields at their previous values.
138
139 Arguments:
140 field_value: The field value to be written.
141 Single bit. Range: 0-1.
142 """
143 register_value = self.read_conf()
144 register_value.enable = field_value
145 self.write_conf(register_value=register_value)
146
147 def write_conf_direction(self, field_value: "ExampleConfValue.Direction") -> None:
148 """
149 Write the 'direction' field in the 'conf' register.
150 Will read-modify-write the ``conf`` register, setting the ``direction`` field
151 to the provided value, while leaving the other fields at their previous values.
152
153 Arguments:
154 field_value: The field value to be written.
155 Enumeration. Possible values: DATA_IN, HIGH_Z, DATA_OUT.
156 """
157 register_value = self.read_conf()
158 register_value.direction = field_value
159 self.write_conf(register_value=register_value)
160
161 def read_status(self) -> "int":
162 """
163 Read the 'status' register.
164 Return type is a plain unsigned integer since the register has no fields.
165 """
166 index = self._register_list.get_register_index(
167 register_name="status",
168 register_array_name=None,
169 register_array_index=None,
170 )
171 value = self._read_register(register_index=index)
172 return value
173
174 def read_channels_read_address(self, array_index: int) -> "int":
175 """
176 Read the 'read_address' register within the 'channels' register array.
177 Return type is a plain unsigned integer since the register has no fields.
178
179 Arguments:
180 array_index: Register array iteration index (range 0-3).
181 """
182 index = self._register_list.get_register_index(
183 register_name="read_address",
184 register_array_name="channels",
185 register_array_index=array_index,
186 )
187 value = self._read_register(register_index=index)
188 return value
189
190 def write_channels_read_address(self, register_value: "int", array_index: int) -> None:
191 """
192 Write the whole 'read_address' register within the 'channels' register array.
193
194 Arguments:
195 register_value: Value to be written.
196 Is a plain unsigned integer since the register has no fields.
197 array_index: Register array iteration index (range 0-3).
198 """
199 index = self._register_list.get_register_index(
200 register_name="read_address",
201 register_array_name="channels",
202 register_array_index=array_index,
203 )
204 self._write_register(register_index=index, register_value=register_value)
205
206 def write_channels_conf(
207 self, register_value: "ExampleChannelsConfValue", array_index: int
208 ) -> None:
209 """
210 Write the whole 'conf' register within the 'channels' register array.
211
212 Arguments:
213 register_value: Object with the field values that shall be written.
214 array_index: Register array iteration index (range 0-3).
215 """
216 index = self._register_list.get_register_index(
217 register_name="conf",
218 register_array_name="channels",
219 register_array_index=array_index,
220 )
221 register = self._register_list.get_register(
222 register_name="conf", register_array_name="channels"
223 )
224 integer_value = 0
225 integer_value += register.fields[0].set_value(field_value=register_value.enable)
226 integer_value += register.fields[1].set_value(field_value=register_value.tuser)
227 self._write_register(register_index=index, register_value=integer_value)
228
229 def write_channels_conf_enable(self, field_value: "int", array_index: int) -> None:
230 """
231 Write the 'enable' field in the 'conf' register within the 'channels' register array.
232 Will write the ``conf`` register, setting the ``enable`` field to the
233 provided value, and all other fields to their default values.
234
235 Arguments:
236 field_value: The field value to be written.
237 Single bit. Range: 0-1.
238 array_index: Register array iteration index (range 0-3).
239 """
240 index = self._register_list.get_register_index(
241 register_name="conf",
242 register_array_name="channels",
243 register_array_index=array_index,
244 )
245
246 register = self._register_list.get_register(
247 register_name="conf", register_array_name="channels"
248 )
249 register_value = 0
250 register_value += register.fields[0].set_value(field_value)
251 register_value += register.fields[1].default_value_uint << register.fields[1].base_index
252
253 self._write_register(register_index=index, register_value=register_value)
254
255 def write_channels_conf_tuser(self, field_value: "int", array_index: int) -> None:
256 """
257 Write the 'tuser' field in the 'conf' register within the 'channels' register array.
258 Will write the ``conf`` register, setting the ``tuser`` field to the
259 provided value, and all other fields to their default values.
260
261 Arguments:
262 field_value: The field value to be written.
263 Unsigned bit vector. Width: 8. Range: 0 - 255.
264 array_index: Register array iteration index (range 0-3).
265 """
266 index = self._register_list.get_register_index(
267 register_name="conf",
268 register_array_name="channels",
269 register_array_index=array_index,
270 )
271
272 register = self._register_list.get_register(
273 register_name="conf", register_array_name="channels"
274 )
275 register_value = 0
276 register_value += register.fields[0].default_value_uint << register.fields[0].base_index
277 register_value += register.fields[1].set_value(field_value)
278
279 self._write_register(register_index=index, register_value=register_value)
280
281 def print_registers(self, no_color: bool = False) -> None:
282 """
283 Print all registers and their values to STDOUT.
284
285 Arguments:
286 no_color: Disable color output.
287 Can also be achieved by setting the environment variable ``NO_COLOR`` to any value.
288 """
289 print(
290 colored("Register", color="light_yellow", no_color=no_color)
291 + " 'conf' "
292 + colored("." * 63, color="dark_grey", no_color=no_color)
293 + " (index 0, address 0):"
294 )
295 register_value = self.read_conf()
296 print(register_value.to_string(indentation=" ", no_color=no_color))
297 print()
298
299 print(
300 colored("Register", color="light_yellow", no_color=no_color)
301 + " 'status' "
302 + colored("." * 61, color="dark_grey", no_color=no_color)
303 + " (index 1, address 4):"
304 )
305 register_value = self.read_status()
306 formatted_value = _format_unsigned_number(value=register_value, width=32)
307 print(f" {formatted_value}\n")
308
309 print(
310 colored("Register", color="light_yellow", no_color=no_color)
311 + " 'channels[0].read_address' "
312 + colored("." * 43, color="dark_grey", no_color=no_color)
313 + " (index 2, address 8):"
314 )
315 register_value = self.read_channels_read_address(array_index=0)
316 formatted_value = _format_unsigned_number(value=register_value, width=32)
317 print(f" {formatted_value}\n")
318
319 print(
320 colored("Register", color="light_yellow", no_color=no_color)
321 + " 'channels[0].conf' "
322 + colored("." * 51, color="dark_grey", no_color=no_color)
323 + " (index 3, address 12):"
324 )
325 print(" Not readable.\n")
326
327 print(
328 colored("Register", color="light_yellow", no_color=no_color)
329 + " 'channels[1].read_address' "
330 + colored("." * 43, color="dark_grey", no_color=no_color)
331 + " (index 4, address 16):"
332 )
333 register_value = self.read_channels_read_address(array_index=1)
334 formatted_value = _format_unsigned_number(value=register_value, width=32)
335 print(f" {formatted_value}\n")
336
337 print(
338 colored("Register", color="light_yellow", no_color=no_color)
339 + " 'channels[1].conf' "
340 + colored("." * 51, color="dark_grey", no_color=no_color)
341 + " (index 5, address 20):"
342 )
343 print(" Not readable.\n")
344
345 print(
346 colored("Register", color="light_yellow", no_color=no_color)
347 + " 'channels[2].read_address' "
348 + colored("." * 43, color="dark_grey", no_color=no_color)
349 + " (index 6, address 24):"
350 )
351 register_value = self.read_channels_read_address(array_index=2)
352 formatted_value = _format_unsigned_number(value=register_value, width=32)
353 print(f" {formatted_value}\n")
354
355 print(
356 colored("Register", color="light_yellow", no_color=no_color)
357 + " 'channels[2].conf' "
358 + colored("." * 51, color="dark_grey", no_color=no_color)
359 + " (index 7, address 28):"
360 )
361 print(" Not readable.\n")
362
363 print(
364 colored("Register", color="light_yellow", no_color=no_color)
365 + " 'channels[3].read_address' "
366 + colored("." * 43, color="dark_grey", no_color=no_color)
367 + " (index 8, address 32):"
368 )
369 register_value = self.read_channels_read_address(array_index=3)
370 formatted_value = _format_unsigned_number(value=register_value, width=32)
371 print(f" {formatted_value}\n")
372
373 print(
374 colored("Register", color="light_yellow", no_color=no_color)
375 + " 'channels[3].conf' "
376 + colored("." * 51, color="dark_grey", no_color=no_color)
377 + " (index 9, address 36):"
378 )
379 print(" Not readable.\n")
380
381
382def get_accessor(register_accessor: "PythonRegisterAccessorInterface") -> ExampleAccessor:
383 """
384 Factory function to create an accessor object.
385 """
386 return ExampleAccessor(register_accessor=register_accessor)
387
388
389def _format_unsigned_number(value: int, width: int, include_decimal: bool = True) -> str:
390 """
391 Format a string representation of the provided ``value``.
392 Suitable for printing field and register values.
393 """
394 result = ""
395 if include_decimal:
396 result += f"unsigned decimal {value}, "
397
398 hex_string = to_hex_byte_string(value=value, result_width_bits=width)
399 result += f"hexadecimal {hex_string}, "
400
401 binary_string = to_binary_nibble_string(value=value, result_width_bits=width)
402 result += f"binary {binary_string}"
403
404 return result
405
406
407@dataclass
408class ExampleConfValue:
409 """
410 Represents the field values of the 'conf' register.
411 Uses native Python types for easy handling.
412 """
413
414 class Direction(Enum):
415 """
416 Enumeration elements for the ``direction`` field.
417 """
418
419 DATA_IN = "data_in"
420 HIGH_Z = "high_z"
421 DATA_OUT = "data_out"
422
423 # Single bit. Range: 0-1.
424 enable: int
425 # Enumeration. Possible values: DATA_IN, HIGH_Z, DATA_OUT.
426 direction: Direction
427
428 def to_string(self, indentation: str = "", no_color: bool = False) -> str:
429 """
430 Get a string of the field values, suitable for printing.
431
432 Arguments:
433 indentation: Optionally indent each field value line.
434 no_color: Disable color output.
435 Can also be achieved by setting the environment variable ``NO_COLOR`` to any value.
436 """
437 values = []
438
439 value = str(self.enable)
440 field_name = colored("enable", color="light_cyan", no_color=no_color)
441 values.append(f"{indentation}{field_name}: {value}")
442
443 enum_index = list(self.Direction).index(self.direction)
444 value = f"{self.direction.name} ({enum_index})"
445 field_name = colored("direction", color="light_cyan", no_color=no_color)
446 values.append(f"{indentation}{field_name}: {value}")
447
448 return "\n".join(values)
449
450
451@dataclass
452class ExampleChannelsConfValue:
453 """
454 Represents the field values of the 'conf' register within the 'channels' register array.
455 Uses native Python types for easy handling.
456 """
457
458 # Single bit. Range: 0-1.
459 enable: int
460 # Unsigned bit vector. Width: 8. Range: 0 - 255.
461 tuser: int
462
463 def to_string(self, indentation: str = "", no_color: bool = False) -> str:
464 """
465 Get a string of the field values, suitable for printing.
466
467 Arguments:
468 indentation: Optionally indent each field value line.
469 no_color: Disable color output.
470 Can also be achieved by setting the environment variable ``NO_COLOR`` to any value.
471 """
472 values = []
473
474 value = str(self.enable)
475 field_name = colored("enable", color="light_cyan", no_color=no_color)
476 values.append(f"{indentation}{field_name}: {value}")
477
478 value_comment = _format_unsigned_number(value=self.tuser, width=8, include_decimal=False)
479 value = f"{self.tuser} ({value_comment})"
480 field_name = colored("tuser", color="light_cyan", no_color=no_color)
481 values.append(f"{indentation}{field_name}: {value}")
482
483 return "\n".join(values)