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.

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:

Python code that parses the example TOML file and generates Python register artifacts.
 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.

Click to expand/collapse code.
Example Python pickle re-creation script
 1# ------------------------------------------------------------------------------
 2# This file is automatically generated by hdl-registers version 7.3.1-dev.
 3# Code generator PythonPickleGenerator version 1.0.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 pathlib import Path
10from typing import TYPE_CHECKING
11
12if TYPE_CHECKING:
13        from hdl_registers.register_list import RegisterList
14
15THIS_DIR = Path(__file__).parent
16PICKLE_FILE = THIS_DIR / "example.pickle"
17
18
19class Example:
20    """
21    Instantiate this class to get the ``RegisterList`` object for the
22    ``example`` register list.
23    """
24
25    def __new__(cls):
26        """
27        Recreate the ``RegisterList`` object from binary pickle.
28        """
29        with PICKLE_FILE.open("rb") as file_handle:
30            return pickle.load(file_handle)
31
32
33def get_register_list() -> "RegisterList":
34    """
35    Return a ``RegisterList`` object with the registers/constants from the
36    ``example`` register list.
37    Recreated from a Python pickle file.
38    """
39    with PICKLE_FILE.open("rb") as file_handle:
40        return pickle.load(file_handle)

Accessor code

The class below is generated by the PythonAccessorGenerator for performing register and field operations on a target device. Interesting things to notice:

  1. Register read/write operations use a generated Python dataclass to represent a register with field value members. See e.g. read_config and write_config methods, and the ExampleConfigValue type in the generated code.

  2. Field values are represented using native Python types.

  1. A bit field is represented as a Python int.

  2. A bit vector field WITHOUT fractional bits, whether signed or unsigned, is represented as a Python int.

  3. A bit vector field WITH fractional bits, whether signed or unsigned, is represented as a Python float.

  4. An enumeration field is represented using a custom Python Enum type. See e.g. ExampleConfigValue.Direction.

  5. An integer field, whether signed or unsigned, is represented as a Python int.

  1. In cases where the register has no fields, the read and write use a plain integer instead. See e.g. read_status.

  2. Each writeable register has methods to write

    1. The entire register value (e.g. write_config), which takes a dataclass value as argument.

    2. 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:

../../_images/print_registers.png

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

  1. Generating artifacts as shown here.

  2. Adding the generated Python module to your Python PATH.

  3. Instantiating the object, by either

  1. Class instantiation, e.g. accessor = ExampleAccessor(register_accessor=register_accessor), where “Example” in the class name is the name of the register list.

  2. Running the convenience function accessor = get_accessor(register_accessor=register_accessor) where the register list name is not needed anywhere.

Example Python accessor class
  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)