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