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.

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

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