rgscore 0.14.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rgscore/__init__.py +3 -0
- rgscore/model/__init__.py +0 -0
- rgscore/model/register.py +584 -0
- rgscore/model/reglist.py +154 -0
- rgscore/model/sfix.py +26 -0
- rgscore-0.14.2.dist-info/METADATA +51 -0
- rgscore-0.14.2.dist-info/RECORD +10 -0
- rgscore-0.14.2.dist-info/WHEEL +5 -0
- rgscore-0.14.2.dist-info/licenses/LICENSE +21 -0
- rgscore-0.14.2.dist-info/top_level.txt +1 -0
rgscore/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import reduce
|
|
8
|
+
from typing import Any, Self, final
|
|
9
|
+
|
|
10
|
+
from bitstring import BitArray
|
|
11
|
+
from i2c_api import I2CMaster, RegisterAddress
|
|
12
|
+
from pydantic import TypeAdapter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RLink(ABC):
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def read(self, addr: int, address_bus_width: int, value_width: int) -> BitArray | None:
|
|
18
|
+
"""
|
|
19
|
+
Reads raw register value (of `value_width` bits) from address `addr` assuming that
|
|
20
|
+
address bus is `address_bus_width`. If no register exist at this address, then returns None.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def write(self, addr: int, address_bus_width: int, value: BitArray) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Writes register value `value` to address `addr` assuming that address bus is `address_bus_width`.
|
|
27
|
+
If register does exist at this address, then do write and return True, otherwise return False.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@final
|
|
32
|
+
class RLinkI2C(RLink):
|
|
33
|
+
def __init__(self, i2c: I2CMaster, device_address: int):
|
|
34
|
+
self.i2c = i2c
|
|
35
|
+
self.device_address = device_address
|
|
36
|
+
|
|
37
|
+
def read(self, addr: int, address_bus_width: int, width: int) -> BitArray | None:
|
|
38
|
+
bytes_to_read = int(width / 8)
|
|
39
|
+
if (width % 8) > 0:
|
|
40
|
+
bytes_to_read += 1
|
|
41
|
+
|
|
42
|
+
data = self.i2c.read_register(
|
|
43
|
+
address=self.device_address,
|
|
44
|
+
register=RegisterAddress(addr, address_bus_width),
|
|
45
|
+
num_bytes=bytes_to_read,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if data is None:
|
|
49
|
+
return None
|
|
50
|
+
else:
|
|
51
|
+
return BitArray(data[0:width])
|
|
52
|
+
|
|
53
|
+
def write(self, addr: int, address_bus_width: int, value: BitArray) -> bool:
|
|
54
|
+
return self.i2c.write_register(
|
|
55
|
+
address=self.device_address,
|
|
56
|
+
register=RegisterAddress(addr, address_bus_width),
|
|
57
|
+
data=value,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class EnumValue:
|
|
63
|
+
name: str
|
|
64
|
+
value: float
|
|
65
|
+
desc: str = ""
|
|
66
|
+
|
|
67
|
+
def __post_init__(self):
|
|
68
|
+
self.value = float(self.value)
|
|
69
|
+
|
|
70
|
+
def __repr__(self) -> str:
|
|
71
|
+
return f"{self.value} = {self.name} # {self.desc}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class FieldDef:
|
|
76
|
+
name: str
|
|
77
|
+
offset: int
|
|
78
|
+
signed: str
|
|
79
|
+
width: int
|
|
80
|
+
fractional: int
|
|
81
|
+
rw: bool
|
|
82
|
+
desc: str = ""
|
|
83
|
+
enum_values: list[EnumValue] | None = None
|
|
84
|
+
|
|
85
|
+
def __post_init__(self):
|
|
86
|
+
self.__validate__()
|
|
87
|
+
|
|
88
|
+
def __validate__(self):
|
|
89
|
+
if self.signed not in ("U", "S"):
|
|
90
|
+
raise ValueError("Invalid signed value")
|
|
91
|
+
if self.offset < 0:
|
|
92
|
+
raise ValueError("Field offset must be a positive integer")
|
|
93
|
+
|
|
94
|
+
if self.enum_values is not None:
|
|
95
|
+
vlen = len(self.enum_values)
|
|
96
|
+
if len({v.name for v in self.enum_values}) < vlen or len({v.value for v in self.enum_values}) < vlen:
|
|
97
|
+
raise ValueError("List of valid values contains duplicate entries")
|
|
98
|
+
else:
|
|
99
|
+
self.valid_name_values = {v.name: v for v in self.enum_values}
|
|
100
|
+
self.valid_value_values = {v.value: v for v in self.enum_values}
|
|
101
|
+
else:
|
|
102
|
+
self.valid_name_values = {}
|
|
103
|
+
self.valid_value_values = {}
|
|
104
|
+
|
|
105
|
+
def copy(self) -> Self:
|
|
106
|
+
return dataclasses.replace(self)
|
|
107
|
+
|
|
108
|
+
def range(self) -> tuple[float, float]:
|
|
109
|
+
if self.signed == "U":
|
|
110
|
+
return (0.0, (pow(2, self.width) - 1) / pow(2, self.fractional))
|
|
111
|
+
else:
|
|
112
|
+
return (
|
|
113
|
+
-(pow(2, self.width - 1) / pow(2, self.fractional)),
|
|
114
|
+
(pow(2, self.width - 1) - 1) / pow(2, self.fractional),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def end_offset(self) -> int:
|
|
118
|
+
return self.offset + self.width - 1
|
|
119
|
+
|
|
120
|
+
def idxs(self) -> list[int]:
|
|
121
|
+
return list(range(self.offset, self.end_offset() + 1))
|
|
122
|
+
|
|
123
|
+
def format(self, value: float) -> str:
|
|
124
|
+
return f"{int(value)}" if self.fractional == 0 else f"{value}"
|
|
125
|
+
|
|
126
|
+
def __repr__(self):
|
|
127
|
+
rw = "rw" if self.rw else "ro"
|
|
128
|
+
return f"{self.name}@[{self.end_offset()}:{self.offset}]{self.signed}{self.width}.{self.fractional}#{rw}"
|
|
129
|
+
|
|
130
|
+
def read_raw(self, data: BitArray) -> BitArray:
|
|
131
|
+
return data[(len(data) - self.end_offset() - 1) : (len(data) - self.offset)]
|
|
132
|
+
|
|
133
|
+
def next_up_value(self, data: BitArray) -> float | int:
|
|
134
|
+
return self.__next(data, 1)
|
|
135
|
+
|
|
136
|
+
def next_down_value(self, data: BitArray) -> float | int:
|
|
137
|
+
return self.__next(data, -1)
|
|
138
|
+
|
|
139
|
+
def __get_next_raw_field(self, data: BitArray, step: int) -> BitArray:
|
|
140
|
+
raw_field = self.read_raw(data)
|
|
141
|
+
try:
|
|
142
|
+
if self.signed == "S":
|
|
143
|
+
next_in_val = raw_field.int + step
|
|
144
|
+
return BitArray(f"int:{len(raw_field)}={next_in_val}")
|
|
145
|
+
else: # unsigned
|
|
146
|
+
next_in_val = raw_field.uint + step
|
|
147
|
+
return BitArray(f"uint:{len(raw_field)}={next_in_val}")
|
|
148
|
+
except ValueError:
|
|
149
|
+
# we are here if next value is out of range for this field
|
|
150
|
+
return raw_field
|
|
151
|
+
|
|
152
|
+
def __next(self, data: BitArray, step: int) -> float | int:
|
|
153
|
+
return self.__read_from_raw_field(self.__get_next_raw_field(data, step))
|
|
154
|
+
|
|
155
|
+
def __read_from_raw_field(self, raw_field: BitArray) -> float | int:
|
|
156
|
+
if self.fractional == 0:
|
|
157
|
+
return raw_field.int if self.signed == "S" else raw_field.uint
|
|
158
|
+
else:
|
|
159
|
+
return (raw_field.int if self.signed == "S" else raw_field.uint) / pow(2, self.fractional)
|
|
160
|
+
|
|
161
|
+
def read(self, data: BitArray) -> float | int:
|
|
162
|
+
return self.__read_from_raw_field(self.read_raw(data))
|
|
163
|
+
|
|
164
|
+
def write(self, data: BitArray, value: float) -> None:
|
|
165
|
+
new_val = int(value * pow(2, self.fractional))
|
|
166
|
+
tspec = ("int:" if self.signed == "S" else "uint:") + f"{self.width}={new_val}"
|
|
167
|
+
data.overwrite(len(data) - self.end_offset() - 1, BitArray(tspec))
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def value_of(field_definition: str) -> "FieldDef":
|
|
171
|
+
m = Register.field_def_re.match(field_definition)
|
|
172
|
+
if m:
|
|
173
|
+
end_offset = int(m.group("end"))
|
|
174
|
+
|
|
175
|
+
start_group = m.group("start")
|
|
176
|
+
start_offset = int(start_group) if start_group else end_offset
|
|
177
|
+
|
|
178
|
+
implied_width = end_offset - start_offset + 1
|
|
179
|
+
width = int(m.group("w"))
|
|
180
|
+
if implied_width != width:
|
|
181
|
+
raise ValueError(f"Inconsistent field width {implied_width} != {width}")
|
|
182
|
+
rw = m.group("rw")
|
|
183
|
+
rw = rw if rw else "rw"
|
|
184
|
+
return FieldDef(
|
|
185
|
+
name=m.group("name"),
|
|
186
|
+
offset=start_offset,
|
|
187
|
+
signed=m.group("s"),
|
|
188
|
+
width=width,
|
|
189
|
+
fractional=int(m.group("f")),
|
|
190
|
+
rw=(rw == "rw"),
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
raise ValueError(f"Invalid register field definition [{field_definition}]")
|
|
194
|
+
|
|
195
|
+
def with_description(self, desc: str) -> Self:
|
|
196
|
+
self.desc = desc
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
def with_enum(self, values: list[EnumValue]) -> Self:
|
|
200
|
+
if len({v.name for v in values}) != len(values):
|
|
201
|
+
raise ValueError("Duplicate name in enum values")
|
|
202
|
+
if len({v.value for v in values}) != len(values):
|
|
203
|
+
raise ValueError("Duplicate value in enum values")
|
|
204
|
+
self.enum_values = values
|
|
205
|
+
self.__validate__()
|
|
206
|
+
return self
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Register:
|
|
210
|
+
numeric_type_re = re.compile(r"^\s*(?P<s>U|S)(?P<w>\d+)\.(?P<f>\d+)\s*$")
|
|
211
|
+
rw_re = re.compile(r"^\s*(?P<rw>rw|ro)\s*$")
|
|
212
|
+
field_def_re = re.compile(
|
|
213
|
+
r"^(?P<name>[a-zA-Z_]+[0-9]*)@\[(?P<end>\d+)(:(?P<start>\d+))?\](?P<s>U|S)(?P<w>\d+)\.(?P<f>\d+)(#(?P<rw>rw|ro))?$"
|
|
214
|
+
)
|
|
215
|
+
register_name_re = re.compile("^[a-zA-Z]+[a-zA-Z0-9_]*$")
|
|
216
|
+
|
|
217
|
+
def __init__(
|
|
218
|
+
self,
|
|
219
|
+
bit_len: int,
|
|
220
|
+
model: list[FieldDef] | None = None,
|
|
221
|
+
address: int | None = None,
|
|
222
|
+
address_bus_width_bytes: int = 1,
|
|
223
|
+
name: str | None = None,
|
|
224
|
+
link_provider: Callable[[], RLink] | None = None,
|
|
225
|
+
desc: str = "",
|
|
226
|
+
):
|
|
227
|
+
"""
|
|
228
|
+
Register in an object that encapsulates array of bits and field definitions and provides read and write
|
|
229
|
+
access to these fields. Fields are defined in the model
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
bit_len: width of register in bits
|
|
233
|
+
model: model of register fields
|
|
234
|
+
address: register address (optional)
|
|
235
|
+
address_bus_width_bytes: width of address bus in bytes
|
|
236
|
+
name: register name (optional)
|
|
237
|
+
link_provider: provider of link to the external storage (optional)
|
|
238
|
+
desc: register description (optional)
|
|
239
|
+
"""
|
|
240
|
+
if name is not None and not Register.register_name_re.match(name):
|
|
241
|
+
raise ValueError("Invalid register name.")
|
|
242
|
+
|
|
243
|
+
if bit_len <= 0:
|
|
244
|
+
raise ValueError("Register width must be greater than zero")
|
|
245
|
+
self.data: BitArray = BitArray.from_zeros(bit_len)
|
|
246
|
+
self._originial_data = BitArray.from_zeros(bit_len)
|
|
247
|
+
self.__width = bit_len
|
|
248
|
+
self.__desc = desc
|
|
249
|
+
|
|
250
|
+
self._model = (
|
|
251
|
+
[FieldDef.value_of(f"val@[{bit_len - 1}:0]U{bit_len}.0")] if model is None else [m.copy() for m in model]
|
|
252
|
+
)
|
|
253
|
+
self._model.sort(key=lambda fd: -fd.offset)
|
|
254
|
+
|
|
255
|
+
if self._model == []:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
"Register model must not be empty list. Leaving it None will create an implicit field though."
|
|
258
|
+
)
|
|
259
|
+
self._fields_by_name: dict[str, FieldDef] = {fd.name: fd for fd in self._model}
|
|
260
|
+
if len(self._fields_by_name) != len(self._model):
|
|
261
|
+
raise ValueError("Duplicate field names in the model")
|
|
262
|
+
self.__validate_model_wrt_width(self._model, self.width)
|
|
263
|
+
self.__validate_fields_overlapp(self._model)
|
|
264
|
+
|
|
265
|
+
self.address = address
|
|
266
|
+
self.address_bus_width_bytes = address_bus_width_bytes
|
|
267
|
+
self.name = name
|
|
268
|
+
self.__link_provider: Callable[[], RLink] | None = link_provider
|
|
269
|
+
self.linked_address: int | None = address
|
|
270
|
+
|
|
271
|
+
def __validate_model_wrt_width(self, model: list[FieldDef], width: int):
|
|
272
|
+
for m in model:
|
|
273
|
+
if m.end_offset() >= width or m.offset < 0:
|
|
274
|
+
raise ValueError(f'Field "{m.name}" extends outside of register width.')
|
|
275
|
+
|
|
276
|
+
def __validate_fields_overlapp(self, model: list[FieldDef]):
|
|
277
|
+
fields_idxs = reduce(lambda x, y: x + y, [m.idxs() for m in model], [])
|
|
278
|
+
if len(set(fields_idxs)) != len(fields_idxs):
|
|
279
|
+
raise ValueError("Overlapping fields in register definition.")
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def desc(self):
|
|
283
|
+
return self.__desc
|
|
284
|
+
|
|
285
|
+
@desc.setter
|
|
286
|
+
def desc(self, desc: str):
|
|
287
|
+
self.__desc = desc
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def width(self) -> int:
|
|
291
|
+
return self.__width
|
|
292
|
+
|
|
293
|
+
@width.setter
|
|
294
|
+
def width(self, new_width: int):
|
|
295
|
+
self.__validate_model_wrt_width(self._model, new_width)
|
|
296
|
+
self.__width = new_width
|
|
297
|
+
self._originial_data = BitArray.from_zeros(new_width)
|
|
298
|
+
self.data = BitArray.from_zeros(new_width)
|
|
299
|
+
|
|
300
|
+
def replace_model(self, new_model: list[FieldDef]):
|
|
301
|
+
field_names: set[str] = {fd.name for fd in new_model}
|
|
302
|
+
if len(field_names) != len(new_model):
|
|
303
|
+
raise ValueError("Duplicate field names in the model")
|
|
304
|
+
self.__validate_fields_overlapp(new_model)
|
|
305
|
+
self.__validate_model_wrt_width(new_model, self.width)
|
|
306
|
+
self._model.clear()
|
|
307
|
+
self._model.extend(new_model)
|
|
308
|
+
self._model.sort(key=lambda fd: -fd.offset)
|
|
309
|
+
|
|
310
|
+
def copy(self) -> "Register":
|
|
311
|
+
register_copy = Register(
|
|
312
|
+
bit_len=self.width,
|
|
313
|
+
model=[fd.copy() for fd in self._model],
|
|
314
|
+
address=self.address,
|
|
315
|
+
name=self.name,
|
|
316
|
+
link_provider=self.__link_provider,
|
|
317
|
+
)
|
|
318
|
+
register_copy.data = self.data.copy()
|
|
319
|
+
register_copy._originial_data = self._originial_data.copy()
|
|
320
|
+
return register_copy
|
|
321
|
+
|
|
322
|
+
def is_changed(self) -> bool:
|
|
323
|
+
"""
|
|
324
|
+
Returns True or False indicating of register data has changed since it was loaded. This method is only usefull
|
|
325
|
+
when external storage is linked to the register and read() and write() methods are called.
|
|
326
|
+
"""
|
|
327
|
+
return self._originial_data != self.data
|
|
328
|
+
|
|
329
|
+
def get_def(self) -> str:
|
|
330
|
+
"""Returns human-readable register definition."""
|
|
331
|
+
return (
|
|
332
|
+
"Register"
|
|
333
|
+
+ ("" if self.name is None else f" name:{self.name}")
|
|
334
|
+
+ ("" if self.address is None else f" addr:0x{self.address:X}")
|
|
335
|
+
+ ("" if self.desc == "" else f' desc:"{self.desc}"')
|
|
336
|
+
+ f" width:{self.width} fields:{self._model}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def __repr__(self):
|
|
340
|
+
retval = "Register" if self.name is None else self.name
|
|
341
|
+
if self.address is not None:
|
|
342
|
+
retval += f"[0x{self.address:0{self.address_bus_width_bytes * 2}X}]"
|
|
343
|
+
return (
|
|
344
|
+
f"{retval}("
|
|
345
|
+
+ ", ".join(
|
|
346
|
+
[f"{field_def.name}={field_def.format(field_def.read(self.data))}" for field_def in self._model]
|
|
347
|
+
)
|
|
348
|
+
+ ")"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def to_dict_def(self) -> dict:
|
|
352
|
+
"""Exports register definition as dict."""
|
|
353
|
+
return {
|
|
354
|
+
"name": self.name,
|
|
355
|
+
"address": None if self.address is None else f"0x{self.address:X}",
|
|
356
|
+
"address_bus_width_bytes": self.address_bus_width_bytes,
|
|
357
|
+
"width": self.width,
|
|
358
|
+
"desc": self.desc,
|
|
359
|
+
"fields": [dataclasses.asdict(field_def) for field_def in self._model],
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
def to_json_def(self, indent: int | None = None) -> str:
|
|
363
|
+
"""Exports register definition as json text."""
|
|
364
|
+
return json.dumps(self.to_dict_def(), indent=indent)
|
|
365
|
+
|
|
366
|
+
@staticmethod
|
|
367
|
+
def from_dict_def(data: dict) -> "Register":
|
|
368
|
+
fd_adapter = TypeAdapter(FieldDef)
|
|
369
|
+
return Register(
|
|
370
|
+
bit_len=data["width"],
|
|
371
|
+
name=data["name"],
|
|
372
|
+
desc=data["desc"] if "desc" in data else "",
|
|
373
|
+
address=None if data["address"] is None else int(data["address"], 16),
|
|
374
|
+
address_bus_width_bytes=data["address_bus_width_bytes"],
|
|
375
|
+
model=[fd_adapter.validate_python(f) for f in data["fields"]],
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
def from_json_def(json_str: str) -> "Register":
|
|
380
|
+
"""Imports register definition from json text and returns instance of Register."""
|
|
381
|
+
return Register.from_dict_def(json.loads(json_str))
|
|
382
|
+
|
|
383
|
+
def set_field_value(self, field: str, value: float | str) -> float | int:
|
|
384
|
+
"""
|
|
385
|
+
Sets value to a field or raises exception if unable to do so. If value is str, then it is used as a look up
|
|
386
|
+
into associated enum if any and found number value is used. If data in the register model in memory actually
|
|
387
|
+
changed as a result of calling this method, then calling `Register::is_changed()` starts returning True.
|
|
388
|
+
"""
|
|
389
|
+
field_def = self._fields_by_name.get(field)
|
|
390
|
+
if field_def is None:
|
|
391
|
+
raise ValueError(f'There is no field "{field}" in this register.')
|
|
392
|
+
if not field_def.rw:
|
|
393
|
+
raise ValueError(f'Field "{field}" is not writable.')
|
|
394
|
+
|
|
395
|
+
def get_value() -> float:
|
|
396
|
+
if isinstance(value, str):
|
|
397
|
+
num_val = field_def.valid_name_values.get(value)
|
|
398
|
+
if num_val is None:
|
|
399
|
+
raise ValueError(f"Invalid enum value [{value}] for field {field}")
|
|
400
|
+
else:
|
|
401
|
+
return num_val.value
|
|
402
|
+
else:
|
|
403
|
+
return value
|
|
404
|
+
|
|
405
|
+
value = get_value()
|
|
406
|
+
min, max = field_def.range()
|
|
407
|
+
if min <= value <= max:
|
|
408
|
+
field_def.write(self.data, value)
|
|
409
|
+
return field_def.read(self.data)
|
|
410
|
+
else:
|
|
411
|
+
raise ValueError(f'Value {value} is out of range [{field_def.range()}] for field "{field}"')
|
|
412
|
+
|
|
413
|
+
def get_field_value(self, field: str) -> float | int:
|
|
414
|
+
field_def = self._fields_by_name.get(field)
|
|
415
|
+
if field_def is None:
|
|
416
|
+
raise ValueError(f'There is no field "{field}" in this register.')
|
|
417
|
+
else:
|
|
418
|
+
return field_def.read(self.data)
|
|
419
|
+
|
|
420
|
+
def get_next_up_field_value(self, field: str) -> float | int:
|
|
421
|
+
field_def = self._fields_by_name.get(field)
|
|
422
|
+
if field_def is None:
|
|
423
|
+
raise ValueError(f'There is no field "{field}" in this register.')
|
|
424
|
+
else:
|
|
425
|
+
return field_def.next_up_value(self.data)
|
|
426
|
+
|
|
427
|
+
def get_next_down_field_value(self, field: str) -> float | int:
|
|
428
|
+
field_def = self._fields_by_name.get(field)
|
|
429
|
+
if field_def is None:
|
|
430
|
+
raise ValueError(f'There is no field "{field}" in this register.')
|
|
431
|
+
else:
|
|
432
|
+
return field_def.next_down_value(self.data)
|
|
433
|
+
|
|
434
|
+
def get_field_value_raw(self, field: str) -> BitArray:
|
|
435
|
+
field_def = self._fields_by_name.get(field)
|
|
436
|
+
if field_def is None:
|
|
437
|
+
raise ValueError(f'There is no field "{field}" in this register.')
|
|
438
|
+
else:
|
|
439
|
+
return field_def.read_raw(self.data)
|
|
440
|
+
|
|
441
|
+
def get_field_values(self) -> dict[str, float]:
|
|
442
|
+
return {m.name: self.get_field_value(m.name) for m in self._model}
|
|
443
|
+
|
|
444
|
+
def get_field_definition(self, field: str) -> FieldDef:
|
|
445
|
+
return self._fields_by_name[field]
|
|
446
|
+
|
|
447
|
+
def get_field_names(self) -> list[str]:
|
|
448
|
+
return [m.name for m in self._model]
|
|
449
|
+
|
|
450
|
+
def link(self, link: RLink, register_address: int | None = None) -> None:
|
|
451
|
+
"""
|
|
452
|
+
Links register to external store. If optional address is provided, then it will be used to access external store
|
|
453
|
+
even if register has address in its definition. If optional address is not provided, then address defined in the
|
|
454
|
+
register will be used. If no addresses are given and present in the internal register definition then ValueError
|
|
455
|
+
will be raised.
|
|
456
|
+
|
|
457
|
+
:param link: link to external store
|
|
458
|
+
:param register_address: address of register in external store
|
|
459
|
+
"""
|
|
460
|
+
if register_address is not None:
|
|
461
|
+
self.linked_address = register_address
|
|
462
|
+
if self.linked_address is None:
|
|
463
|
+
raise ValueError(
|
|
464
|
+
"To link register to external store address must be either set in the register "
|
|
465
|
+
"or provided during linking."
|
|
466
|
+
)
|
|
467
|
+
self.__link_provider = lambda: link
|
|
468
|
+
|
|
469
|
+
def set_link_provider(self, link_provider: Callable[[], RLink]) -> None:
|
|
470
|
+
self.__link_provider = link_provider
|
|
471
|
+
|
|
472
|
+
def read(self) -> None:
|
|
473
|
+
"""
|
|
474
|
+
Reads register data from associated link (external store). If no link is configured, then this is a no-op.
|
|
475
|
+
If external store store (link) is configure and read operation fails for any reason than RuntimeError is raised.
|
|
476
|
+
Once data is read from the external store, calling `Register::is_changed()` starts returning False until
|
|
477
|
+
data is modified by calling `Register::set_field_value(...)`.
|
|
478
|
+
"""
|
|
479
|
+
if self.__link_provider is not None:
|
|
480
|
+
assert self.linked_address is not None
|
|
481
|
+
raw_data = self.__link_provider().read(self.linked_address, self.address_bus_width_bytes, self.width)
|
|
482
|
+
if raw_data is None:
|
|
483
|
+
raise RuntimeError(f"There is no register at the address {self.linked_address}.")
|
|
484
|
+
self.data = raw_data
|
|
485
|
+
self._originial_data = raw_data.copy()
|
|
486
|
+
|
|
487
|
+
def write(self, read_back: bool = False, if_changed_only: bool = False) -> bool:
|
|
488
|
+
"""
|
|
489
|
+
Writes register data to external store configured when `Register::link(...)` method is called.
|
|
490
|
+
Once data is written to the external store, calling `Register::is_changed()` starts returning False until
|
|
491
|
+
data is modified by calling `Register::set_field_value(...)`.
|
|
492
|
+
|
|
493
|
+
:param read_back: If True, then immediately after write operation read it back. If False, then do just write op.
|
|
494
|
+
:param if_changed_only: If True, then check if register in memory model changed and if so then do write,
|
|
495
|
+
otherwise do not write (return False). If False (default), then always write.
|
|
496
|
+
:return: True or False if write operation was successful or not.
|
|
497
|
+
"""
|
|
498
|
+
if self.__link_provider is None or (if_changed_only and self._originial_data == self.data):
|
|
499
|
+
return False
|
|
500
|
+
else:
|
|
501
|
+
assert self.linked_address is not None
|
|
502
|
+
write_success = self.__link_provider().write(self.linked_address, self.address_bus_width_bytes, self.data)
|
|
503
|
+
if read_back:
|
|
504
|
+
self.read()
|
|
505
|
+
if write_success:
|
|
506
|
+
self._originial_data = self.data.copy()
|
|
507
|
+
return write_success
|
|
508
|
+
|
|
509
|
+
def mk_embedding_class(self, link_provider: Callable[[], RLink], auto_sync: bool) -> Any:
|
|
510
|
+
register = self.copy()
|
|
511
|
+
register.set_link_provider(link_provider)
|
|
512
|
+
|
|
513
|
+
attrs = {
|
|
514
|
+
"_read": register.read,
|
|
515
|
+
"_write": register.write,
|
|
516
|
+
"__repr__": register.__repr__,
|
|
517
|
+
"__call__": lambda _: register.data,
|
|
518
|
+
}
|
|
519
|
+
for field_name in self.get_field_names():
|
|
520
|
+
|
|
521
|
+
def mk_accessor(fname, register, auto_sync):
|
|
522
|
+
def field_accessor(sself, new_value=None) -> int | float:
|
|
523
|
+
if new_value is None:
|
|
524
|
+
if auto_sync:
|
|
525
|
+
register.read()
|
|
526
|
+
return register.get_field_value(fname)
|
|
527
|
+
else:
|
|
528
|
+
retval = register.set_field_value(fname, new_value)
|
|
529
|
+
if auto_sync:
|
|
530
|
+
register.write()
|
|
531
|
+
return retval
|
|
532
|
+
|
|
533
|
+
return field_accessor
|
|
534
|
+
|
|
535
|
+
def mk_raw_accessor(fname, register):
|
|
536
|
+
def field_accessor(sself) -> BitArray:
|
|
537
|
+
if auto_sync:
|
|
538
|
+
register.read()
|
|
539
|
+
return register.get_field_value_raw(fname)
|
|
540
|
+
|
|
541
|
+
return field_accessor
|
|
542
|
+
|
|
543
|
+
accessor = mk_accessor(field_name, register, auto_sync)
|
|
544
|
+
attrs[field_name] = property(fget=accessor, fset=accessor)
|
|
545
|
+
attrs[field_name + "_raw"] = property(fget=mk_raw_accessor(field_name, register))
|
|
546
|
+
|
|
547
|
+
return type(self.name, (), attrs)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def Field(
|
|
551
|
+
name: str, offset: int, numeric_type: str, rw: str = "rw", desc: str = "", values: list[EnumValue] | None = None
|
|
552
|
+
) -> FieldDef:
|
|
553
|
+
"""
|
|
554
|
+
Convinience method for creating instance of `FieldDef`.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
name: field name
|
|
558
|
+
offset: field offset from the right
|
|
559
|
+
numeric_type: fixed point number type, e.g. U5.7 or S3.0 etc.
|
|
560
|
+
rw: "rw" (default) or "ro" - read/write or read/only
|
|
561
|
+
desc: optiona field description
|
|
562
|
+
values: optional enum values
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
Instance of `FieldDef`.
|
|
566
|
+
"""
|
|
567
|
+
nt_m = Register.numeric_type_re.match(numeric_type)
|
|
568
|
+
if nt_m is None:
|
|
569
|
+
raise ValueError(f"Invalid numeric type: {numeric_type}")
|
|
570
|
+
|
|
571
|
+
rw_m = Register.rw_re.match(rw)
|
|
572
|
+
if rw_m is None:
|
|
573
|
+
raise ValueError(f"Invalid rw: {rw}")
|
|
574
|
+
|
|
575
|
+
return FieldDef(
|
|
576
|
+
name=name,
|
|
577
|
+
offset=offset,
|
|
578
|
+
width=int(nt_m.group("w")),
|
|
579
|
+
signed=nt_m.group("s"),
|
|
580
|
+
fractional=int(nt_m.group("f")),
|
|
581
|
+
rw=(rw == "rw"),
|
|
582
|
+
desc=desc,
|
|
583
|
+
enum_values=values,
|
|
584
|
+
)
|
rgscore/model/reglist.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
from rgscore.model.register import Register, RLink
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RegList:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
store: RLink | None = None,
|
|
12
|
+
on_change_callback: Callable[[Self], None] = lambda _: None,
|
|
13
|
+
registers: list[Register] | None = None,
|
|
14
|
+
):
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.registers: list[Register] = []
|
|
17
|
+
self._reg_names: set[str] = set()
|
|
18
|
+
self._reg_addresses: set[int] = set()
|
|
19
|
+
self._reg_by_names: dict[str, Register] = {}
|
|
20
|
+
self._reg_by_address: dict[int, Register] = {}
|
|
21
|
+
self._store = store
|
|
22
|
+
self._on_change_callback = on_change_callback
|
|
23
|
+
|
|
24
|
+
if registers is not None:
|
|
25
|
+
self.add(registers)
|
|
26
|
+
|
|
27
|
+
def add(self, rs: Register | list[Register]):
|
|
28
|
+
self.__add(rs, call_on_change_callback=True)
|
|
29
|
+
|
|
30
|
+
def __add(self, rs: Register | list[Register], call_on_change_callback: bool):
|
|
31
|
+
rs = [rs] if isinstance(rs, Register) else rs
|
|
32
|
+
if len(rs) == 0:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
for r in rs:
|
|
36
|
+
if r.address is None:
|
|
37
|
+
raise ValueError("To be added Register must have an address")
|
|
38
|
+
|
|
39
|
+
if r.name is None:
|
|
40
|
+
# create implicit register name if register does not an explicit one
|
|
41
|
+
r.name = f"R{r.address}"
|
|
42
|
+
|
|
43
|
+
if r.name in self._reg_names:
|
|
44
|
+
raise ValueError(f"Register under a name [{r.name}] is already in the set")
|
|
45
|
+
|
|
46
|
+
if r.address in self._reg_addresses:
|
|
47
|
+
raise ValueError(f"Register at this address [0x{r.address:04X}] is already in the set")
|
|
48
|
+
|
|
49
|
+
if self._store is not None:
|
|
50
|
+
r.link(self._store)
|
|
51
|
+
|
|
52
|
+
self.registers.append(r)
|
|
53
|
+
self._reg_names.add(r.name)
|
|
54
|
+
self._reg_addresses.add(r.address)
|
|
55
|
+
self._reg_by_names[r.name] = r
|
|
56
|
+
self._reg_by_address[r.address] = r
|
|
57
|
+
|
|
58
|
+
self.registers.sort(key=lambda r: r.address)
|
|
59
|
+
if call_on_change_callback:
|
|
60
|
+
self._on_change_callback(self)
|
|
61
|
+
|
|
62
|
+
def get_register_by_name(self, name: str) -> Register | None:
|
|
63
|
+
return self._reg_by_names.get(name)
|
|
64
|
+
|
|
65
|
+
def get_register_by_address(self, address: int) -> Register | None:
|
|
66
|
+
return self._reg_by_address.get(address)
|
|
67
|
+
|
|
68
|
+
def delete_register_by_name(self, name: str):
|
|
69
|
+
self.__delete_register_by_name(name, call_on_change_callback=True)
|
|
70
|
+
|
|
71
|
+
def __delete_register_by_name(self, name: str, call_on_change_callback: bool):
|
|
72
|
+
"""
|
|
73
|
+
Removes register with a given name from this regList. If register with this name not found, then do nothing.
|
|
74
|
+
"""
|
|
75
|
+
if name in self._reg_names:
|
|
76
|
+
register = self._reg_by_names[name]
|
|
77
|
+
self._reg_names.remove(name)
|
|
78
|
+
|
|
79
|
+
assert register.address is not None
|
|
80
|
+
self._reg_addresses.remove(register.address)
|
|
81
|
+
del self._reg_by_names[name]
|
|
82
|
+
del self._reg_by_address[register.address]
|
|
83
|
+
|
|
84
|
+
idx = [r.name for r in self.registers].index(name)
|
|
85
|
+
del self.registers[idx]
|
|
86
|
+
if call_on_change_callback:
|
|
87
|
+
self._on_change_callback(self)
|
|
88
|
+
|
|
89
|
+
def update_register_def(self, original_register: Register, new_register: Register) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Updates register in the regList given the original one and a new one which is expected to replace original.
|
|
92
|
+
"""
|
|
93
|
+
if new_register.address is None:
|
|
94
|
+
raise ValueError("To be added Register must have an address")
|
|
95
|
+
|
|
96
|
+
if new_register.name is None:
|
|
97
|
+
raise ValueError("To be added Register must have a name")
|
|
98
|
+
|
|
99
|
+
if new_register.name != original_register.name and new_register.name in self._reg_names:
|
|
100
|
+
raise ValueError(f"Register under a name [{new_register.name}] is already in the set")
|
|
101
|
+
|
|
102
|
+
if new_register.address != original_register.address and new_register.address in self._reg_addresses:
|
|
103
|
+
raise ValueError(f"Register at this address [{new_register.address}] is already in the set")
|
|
104
|
+
|
|
105
|
+
assert original_register.name is not None
|
|
106
|
+
self.__delete_register_by_name(original_register.name, call_on_change_callback=False)
|
|
107
|
+
self.__add(new_register, call_on_change_callback=False)
|
|
108
|
+
self._on_change_callback(self)
|
|
109
|
+
|
|
110
|
+
def clear(self):
|
|
111
|
+
"""Removes all registers from the list."""
|
|
112
|
+
self.registers.clear()
|
|
113
|
+
self._reg_names.clear()
|
|
114
|
+
self._reg_addresses.clear()
|
|
115
|
+
self._reg_by_names.clear()
|
|
116
|
+
self._reg_by_address.clear()
|
|
117
|
+
self._on_change_callback(self)
|
|
118
|
+
|
|
119
|
+
def read_all(self) -> None:
|
|
120
|
+
"""Reads all registers in the regList from linked store if any provided."""
|
|
121
|
+
for r in self.registers:
|
|
122
|
+
r.read()
|
|
123
|
+
|
|
124
|
+
def write_all(self) -> None:
|
|
125
|
+
"""Writes all registers in the regList to linked store if any provided."""
|
|
126
|
+
for r in self.registers:
|
|
127
|
+
r.write()
|
|
128
|
+
|
|
129
|
+
def to_json_def(self, indent: int | None = None) -> str:
|
|
130
|
+
"""Exports register definition contained within regList as json text."""
|
|
131
|
+
return json.dumps(
|
|
132
|
+
{
|
|
133
|
+
"class": "RegList",
|
|
134
|
+
"version": 1,
|
|
135
|
+
"registers": [r.to_dict_def() for r in self.registers],
|
|
136
|
+
},
|
|
137
|
+
indent=indent,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def from_json_def(json_str: str) -> "RegList":
|
|
142
|
+
"""Imports a RegList definition from a json string returning new instance of RegList."""
|
|
143
|
+
return RegList().load_from_json_def(json_str)
|
|
144
|
+
|
|
145
|
+
def load_from_json_def(self, json_str: str) -> Self:
|
|
146
|
+
data = json.loads(json_str)
|
|
147
|
+
if data["class"] != "RegList":
|
|
148
|
+
raise ValueError("Provided json document does not represent regList")
|
|
149
|
+
elif data["version"] != 1:
|
|
150
|
+
raise ValueError(f"Provided regList json has unsupported version {data['version']}")
|
|
151
|
+
else:
|
|
152
|
+
self.clear()
|
|
153
|
+
self.add([Register.from_dict_def(d) for d in data["registers"]])
|
|
154
|
+
return self
|
rgscore/model/sfix.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from functools import cache, reduce
|
|
2
|
+
|
|
3
|
+
from bitstring import BitArray, Bits
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@cache
|
|
7
|
+
def __2pow(n) -> float | int:
|
|
8
|
+
return pow(2, n)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __to_bit_array(data: int | Bits) -> Bits:
|
|
12
|
+
return BitArray(f"uint:8={data}") if isinstance(data, int) else data
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def U(en: int, bit_arrays: list[Bits | int]) -> float | int:
|
|
16
|
+
return reduce(
|
|
17
|
+
lambda acc, v: acc + v, [__to_bit_array(a) for a in bit_arrays], BitArray.from_zeros(0)
|
|
18
|
+
).uint / __2pow(en)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def S(en: int, bit_arrays: list[Bits | int]) -> float | int:
|
|
22
|
+
return reduce(
|
|
23
|
+
lambda acc, v: acc + v,
|
|
24
|
+
[__to_bit_array(a) for a in bit_arrays],
|
|
25
|
+
BitArray.from_zeros(0),
|
|
26
|
+
).int / __2pow(en)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rgscore
|
|
3
|
+
Version: 0.14.2
|
|
4
|
+
Summary: Python API for describing and accessing hardware registers
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: bitstring<6.0.0,>=5.0.0
|
|
9
|
+
Requires-Dist: i2c-api>=0.20.0
|
|
10
|
+
Requires-Dist: pydantic>=2.14.1
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# RGSCore
|
|
14
|
+
|
|
15
|
+
[](https://github.com/Brainless-Software/rgscore/blob/master/LICENSE)
|
|
16
|
+
[](https://pypi.org/project/rgscore/)
|
|
17
|
+
[](https://github.com/Brainless-Software/rgscore/actions/workflows/test.yml)
|
|
18
|
+
[](https://brainless-software.github.io/rgscore/)
|
|
19
|
+
|
|
20
|
+
Python library that provides classes for representing, defining and accessing registers in the embedded devices.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from rgscore import Register, RegList, FieldDef
|
|
26
|
+
|
|
27
|
+
reglist = RegList(
|
|
28
|
+
registers=[
|
|
29
|
+
Register(
|
|
30
|
+
bit_len=8,
|
|
31
|
+
name="Foo",
|
|
32
|
+
address=0x01,
|
|
33
|
+
model=[FieldDef.value_of("a@[3:0]U4.1#ro")],
|
|
34
|
+
),
|
|
35
|
+
Register(
|
|
36
|
+
bit_len=8,
|
|
37
|
+
name="Bar",
|
|
38
|
+
address=0x02,
|
|
39
|
+
desc="Very important register",
|
|
40
|
+
model=[FieldDef.value_of("b1@[3:0]U4.1#ro"), FieldDef.value_of("b2@[5:4]S2.0#rw")],
|
|
41
|
+
),
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
To add this library to your project do
|
|
47
|
+
|
|
48
|
+
```shell
|
|
49
|
+
uv add rgscore
|
|
50
|
+
```
|
|
51
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
rgscore/__init__.py,sha256=1pBK-h5SYK-imPk_VGgFBd7FiZGqpUlIGsRXQuAI4hs,146
|
|
2
|
+
rgscore/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
rgscore/model/register.py,sha256=aIjQWDElcborFTNXVtQDbRHLBlk7vJBkwtHbD4tBFwg,23005
|
|
4
|
+
rgscore/model/reglist.py,sha256=YMHvDaXZOlwTZpUiuPNm_w1K6DJJedw9QhqzoV4c1AI,5968
|
|
5
|
+
rgscore/model/sfix.py,sha256=-2QSD6MztIq6AcWmfRTyZRVoEYW591Hq-ycVmiS6vmI,675
|
|
6
|
+
rgscore-0.14.2.dist-info/licenses/LICENSE,sha256=UBiXDGNSvplE3Bx8SPXgZpdje80HPyWokuB6Tqq4EPg,1071
|
|
7
|
+
rgscore-0.14.2.dist-info/METADATA,sha256=SCNXwBd60OxNBNJ4HIcm05Qomk07uCGL0-XC7ZX5bjA,1570
|
|
8
|
+
rgscore-0.14.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
9
|
+
rgscore-0.14.2.dist-info/top_level.txt,sha256=YnGfUkhZWwW6x_lwk0i-QwGjZWKpLAme_jjaG6_A0Z8,8
|
|
10
|
+
rgscore-0.14.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitri Priimak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rgscore
|