corel_mmio 8.0.0__tar.gz
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.
- corel_mmio-8.0.0/LICENSE +21 -0
- corel_mmio-8.0.0/PKG-INFO +81 -0
- corel_mmio-8.0.0/README.md +60 -0
- corel_mmio-8.0.0/pyproject.toml +111 -0
- corel_mmio-8.0.0/setup.cfg +4 -0
- corel_mmio-8.0.0/src/corel_mmio/__init__.py +5 -0
- corel_mmio-8.0.0/src/corel_mmio/bytes.py +108 -0
- corel_mmio-8.0.0/src/corel_mmio/cache.py +54 -0
- corel_mmio-8.0.0/src/corel_mmio/device.py +102 -0
- corel_mmio-8.0.0/src/corel_mmio/doc.py +83 -0
- corel_mmio-8.0.0/src/corel_mmio/eval.py +51 -0
- corel_mmio-8.0.0/src/corel_mmio/interface/__init__.py +1 -0
- corel_mmio-8.0.0/src/corel_mmio/interface/message.py +125 -0
- corel_mmio-8.0.0/src/corel_mmio/interface/mock.py +287 -0
- corel_mmio-8.0.0/src/corel_mmio/interface/protocol.py +316 -0
- corel_mmio-8.0.0/src/corel_mmio/mapping.py +148 -0
- corel_mmio-8.0.0/src/corel_mmio/memmap/__init__.py +3 -0
- corel_mmio-8.0.0/src/corel_mmio/memmap/array.py +265 -0
- corel_mmio-8.0.0/src/corel_mmio/memmap/block.py +274 -0
- corel_mmio-8.0.0/src/corel_mmio/memmap/field.py +430 -0
- corel_mmio-8.0.0/src/corel_mmio/memmap/file.py +264 -0
- corel_mmio-8.0.0/src/corel_mmio/memmap/memmap.py +677 -0
- corel_mmio-8.0.0/src/corel_mmio/memmap/mode.py +50 -0
- corel_mmio-8.0.0/src/corel_mmio/memmap/register.py +524 -0
- corel_mmio-8.0.0/src/corel_mmio/memmap/type.py +544 -0
- corel_mmio-8.0.0/src/corel_mmio/memmap/type_serialization.py +52 -0
- corel_mmio-8.0.0/src/corel_mmio/memmap/value.py +377 -0
- corel_mmio-8.0.0/src/corel_mmio/memory.py +43 -0
- corel_mmio-8.0.0/src/corel_mmio/py.typed +0 -0
- corel_mmio-8.0.0/src/corel_mmio/signal.py +68 -0
- corel_mmio-8.0.0/src/corel_mmio.egg-info/PKG-INFO +81 -0
- corel_mmio-8.0.0/src/corel_mmio.egg-info/SOURCES.txt +40 -0
- corel_mmio-8.0.0/src/corel_mmio.egg-info/dependency_links.txt +1 -0
- corel_mmio-8.0.0/src/corel_mmio.egg-info/requires.txt +10 -0
- corel_mmio-8.0.0/src/corel_mmio.egg-info/top_level.txt +1 -0
- corel_mmio-8.0.0/tests/test_bytes.py +62 -0
- corel_mmio-8.0.0/tests/test_device.py +106 -0
- corel_mmio-8.0.0/tests/test_eval.py +63 -0
- corel_mmio-8.0.0/tests/test_interface.py +143 -0
- corel_mmio-8.0.0/tests/test_mapping.py +129 -0
- corel_mmio-8.0.0/tests/test_memory.py +97 -0
- corel_mmio-8.0.0/tests/test_signal.py +65 -0
corel_mmio-8.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Deutsches Elektronen-Synchrotron DESY
|
|
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,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: corel_mmio
|
|
3
|
+
Version: 8.0.0
|
|
4
|
+
Summary: Common Memory-Mapped IO operations and in-memory representations.
|
|
5
|
+
Author-email: Jason Watson <jason.watson@desy.de>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: repository, https://gitlab.desy.de/corel/corel-mmio
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: typing-extensions~=4.12
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: ipython; extra == "dev"
|
|
14
|
+
Requires-Dist: pre-commit~=4.1; extra == "dev"
|
|
15
|
+
Requires-Dist: mypy~=1.15; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest~=8.3; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-asyncio~=0.23; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
19
|
+
Requires-Dist: coverage>=7.4.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# <img align="right" src="misc/logo.png" height="200"></a>
|
|
23
|
+
|
|
24
|
+
# corel-mmio
|
|
25
|
+
|
|
26
|
+
[](https://gitlab.desy.de/corel/corel-mmio) [](https://gitlab.desy.de/corel/corel-mmio)
|
|
27
|
+
|
|
28
|
+
Common Memory-Mapped Input/Output (MMIO) operations and in-memory
|
|
29
|
+
representations, to aid in the development of packages which communicate with
|
|
30
|
+
hardware containing a memory-mapped address space.
|
|
31
|
+
|
|
32
|
+
This package is part of the [corel](https://gitlab.desy.de/corel) project ecosystem,
|
|
33
|
+
which is a collection of Python packages for the cooperative development of
|
|
34
|
+
software and firmware for memory mapped FPGAs, microcontrollers and other hardware.
|
|
35
|
+
|
|
36
|
+
* Supported Python versions: 3.10+
|
|
37
|
+
* Supported platforms: Linux, OSX
|
|
38
|
+
* Source: <https://gitlab.desy.de/corel/corel-mmio>
|
|
39
|
+
* Pypi: <https://pypi.org/project/corel-mmio>
|
|
40
|
+
* Documentation: TODO
|
|
41
|
+
* License: [MIT](LICENSE)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
* Unification of the interfaces involved in the interaction between software
|
|
47
|
+
and Memory-Mapped Input/Output (MMIO) hardware.
|
|
48
|
+
* Object-oriented description of a memory map address space, providing
|
|
49
|
+
register address access and validation and supporting multiple layers of
|
|
50
|
+
submaps.
|
|
51
|
+
* Convenient asynchronous read/write methods attached to the memory-mapped
|
|
52
|
+
registers via an abstract "interface" class defined through the structural
|
|
53
|
+
subtyping of Python `Protocol`.
|
|
54
|
+
* Supports the development of hardware drivers which can be reused in different
|
|
55
|
+
bench setups, where the underlying protocol (message format or communication
|
|
56
|
+
protocol) changes, but the memory map of the device (and therefore the
|
|
57
|
+
device operation) is common.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## Acknowledgements
|
|
61
|
+
|
|
62
|
+
Concepts in corel-mmio have been inspired by the
|
|
63
|
+
[hdl_registers](https://hdl-registers.com/) package. However, corel-mmio
|
|
64
|
+
deliberately avoids a dependency on hdl_registers, so that it may be used in
|
|
65
|
+
software architectures which strive to minimise dependencies.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
This package is installable via pip from PyPI:
|
|
71
|
+
```
|
|
72
|
+
pip install corel-mmio
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This package is installable via pip directly from this repository
|
|
76
|
+
(via https or ssh), e.g.:
|
|
77
|
+
```
|
|
78
|
+
pip install 'git+https://gitlab.desy.de/corel/corel-mmio.git'
|
|
79
|
+
```
|
|
80
|
+
For developers, follow the usual `git clone` procedure and see the
|
|
81
|
+
[Makefile](Makefile) for development commands.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# <img align="right" src="misc/logo.png" height="200"></a>
|
|
2
|
+
|
|
3
|
+
# corel-mmio
|
|
4
|
+
|
|
5
|
+
[](https://gitlab.desy.de/corel/corel-mmio) [](https://gitlab.desy.de/corel/corel-mmio)
|
|
6
|
+
|
|
7
|
+
Common Memory-Mapped Input/Output (MMIO) operations and in-memory
|
|
8
|
+
representations, to aid in the development of packages which communicate with
|
|
9
|
+
hardware containing a memory-mapped address space.
|
|
10
|
+
|
|
11
|
+
This package is part of the [corel](https://gitlab.desy.de/corel) project ecosystem,
|
|
12
|
+
which is a collection of Python packages for the cooperative development of
|
|
13
|
+
software and firmware for memory mapped FPGAs, microcontrollers and other hardware.
|
|
14
|
+
|
|
15
|
+
* Supported Python versions: 3.10+
|
|
16
|
+
* Supported platforms: Linux, OSX
|
|
17
|
+
* Source: <https://gitlab.desy.de/corel/corel-mmio>
|
|
18
|
+
* Pypi: <https://pypi.org/project/corel-mmio>
|
|
19
|
+
* Documentation: TODO
|
|
20
|
+
* License: [MIT](LICENSE)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
* Unification of the interfaces involved in the interaction between software
|
|
26
|
+
and Memory-Mapped Input/Output (MMIO) hardware.
|
|
27
|
+
* Object-oriented description of a memory map address space, providing
|
|
28
|
+
register address access and validation and supporting multiple layers of
|
|
29
|
+
submaps.
|
|
30
|
+
* Convenient asynchronous read/write methods attached to the memory-mapped
|
|
31
|
+
registers via an abstract "interface" class defined through the structural
|
|
32
|
+
subtyping of Python `Protocol`.
|
|
33
|
+
* Supports the development of hardware drivers which can be reused in different
|
|
34
|
+
bench setups, where the underlying protocol (message format or communication
|
|
35
|
+
protocol) changes, but the memory map of the device (and therefore the
|
|
36
|
+
device operation) is common.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Acknowledgements
|
|
40
|
+
|
|
41
|
+
Concepts in corel-mmio have been inspired by the
|
|
42
|
+
[hdl_registers](https://hdl-registers.com/) package. However, corel-mmio
|
|
43
|
+
deliberately avoids a dependency on hdl_registers, so that it may be used in
|
|
44
|
+
software architectures which strive to minimise dependencies.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
This package is installable via pip from PyPI:
|
|
50
|
+
```
|
|
51
|
+
pip install corel-mmio
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This package is installable via pip directly from this repository
|
|
55
|
+
(via https or ssh), e.g.:
|
|
56
|
+
```
|
|
57
|
+
pip install 'git+https://gitlab.desy.de/corel/corel-mmio.git'
|
|
58
|
+
```
|
|
59
|
+
For developers, follow the usual `git clone` procedure and see the
|
|
60
|
+
[Makefile](Makefile) for development commands.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=77",
|
|
4
|
+
"setuptools_scm[toml]>=8",
|
|
5
|
+
]
|
|
6
|
+
build-backend = "setuptools.build_meta"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "corel_mmio"
|
|
10
|
+
version = "8.0.0"
|
|
11
|
+
description = "Common Memory-Mapped IO operations and in-memory representations."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
authors = [
|
|
17
|
+
{name = "Jason Watson", email = "jason.watson@desy.de"},
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"typing-extensions~=4.12", # TODO: replace for Python 3.12
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"ipython",
|
|
26
|
+
"pre-commit~=4.1",
|
|
27
|
+
"mypy~=1.15",
|
|
28
|
+
"pytest~=8.3",
|
|
29
|
+
"pytest-asyncio~=0.23",
|
|
30
|
+
"pytest-cov>=4.1.0",
|
|
31
|
+
"coverage>=7.4.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
repository = "https://gitlab.desy.de/corel/corel-mmio"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.uv]
|
|
41
|
+
required-version = ">=0.6.0"
|
|
42
|
+
default-groups = []
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
minversion = "8"
|
|
46
|
+
xfail_strict = true
|
|
47
|
+
addopts = ["-ra", "--strict-config", "--strict-markers"]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
asyncio_mode = "strict"
|
|
50
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
51
|
+
log_cli_level = "DEBUG"
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 90
|
|
55
|
+
target-version = "py310"
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
extend-select = [
|
|
59
|
+
"I", # isort
|
|
60
|
+
"N", # pep8 naming
|
|
61
|
+
"D", # pydocstyle
|
|
62
|
+
"NPY", # numpy
|
|
63
|
+
"PT", # pytest
|
|
64
|
+
"UP", # pyupgrade
|
|
65
|
+
"COM", # flake8-commas
|
|
66
|
+
"ISC", # implicit string concat rules
|
|
67
|
+
"ICN", # import name conventions
|
|
68
|
+
"G", # logging
|
|
69
|
+
]
|
|
70
|
+
ignore = [
|
|
71
|
+
"COM812", # incompatible with ruff format
|
|
72
|
+
"ISC001", # incompatible with ruff format
|
|
73
|
+
"G004", # premature optimization, f-strings are more convenient
|
|
74
|
+
"D401", # don't force imperative mood
|
|
75
|
+
"D105", # don't force docstring for magic methods
|
|
76
|
+
"PT013", # useful for pytest.approx
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint.pydocstyle]
|
|
80
|
+
convention = "numpy"
|
|
81
|
+
ignore-decorators = [ # Functions with these decorators don't have to have docstrings.
|
|
82
|
+
"typing.overload", # Default configuration
|
|
83
|
+
# The next four are all variations on override, so child classes don't have to repeat parent classes' docstrings.
|
|
84
|
+
"overrides.override",
|
|
85
|
+
"overrides.overrides",
|
|
86
|
+
"typing.override",
|
|
87
|
+
"typing_extensions.override",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.ruff.lint.per-file-ignores]
|
|
91
|
+
# no documentation linting for test files
|
|
92
|
+
"**/tests/**" = ["D"]
|
|
93
|
+
"**/tests_*.py" = ["D"]
|
|
94
|
+
"*test*.py" = ["D"]
|
|
95
|
+
|
|
96
|
+
[tool.ruff.format]
|
|
97
|
+
quote-style = "double"
|
|
98
|
+
indent-style = "space"
|
|
99
|
+
skip-magic-trailing-comma = false
|
|
100
|
+
line-ending = "auto"
|
|
101
|
+
|
|
102
|
+
[tool.isort]
|
|
103
|
+
profile = "black"
|
|
104
|
+
line_length = 90
|
|
105
|
+
|
|
106
|
+
[[tool.mypy.overrides]]
|
|
107
|
+
module = "tests.*"
|
|
108
|
+
allow_untyped_defs = true
|
|
109
|
+
allow_incomplete_defs = true
|
|
110
|
+
allow_untyped_calls = true
|
|
111
|
+
disable_error_code = ["var-annotated", "has-type", "abstract"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Tools related to bytes of the memory map and data."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"word_increments",
|
|
8
|
+
"pad_data",
|
|
9
|
+
"flip_byteorder",
|
|
10
|
+
"flip_integer_byteorder",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def word_increments(
|
|
15
|
+
start_address: int,
|
|
16
|
+
byte_length: int,
|
|
17
|
+
bus_width: int,
|
|
18
|
+
auto_increment: bool,
|
|
19
|
+
) -> Iterator[tuple[int, int]]:
|
|
20
|
+
"""
|
|
21
|
+
Generator to increment over word addresses.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
start_address : int
|
|
26
|
+
Address in the memory map to start from.
|
|
27
|
+
byte_length : int
|
|
28
|
+
Total number of bytes to increment over.
|
|
29
|
+
bus_width : int
|
|
30
|
+
Byte width of the register bus of the memory map.
|
|
31
|
+
auto_increment : bool
|
|
32
|
+
Automatically increment the register address when writing/reading more
|
|
33
|
+
than the bus length (4 bytes).
|
|
34
|
+
|
|
35
|
+
Yields
|
|
36
|
+
------
|
|
37
|
+
current_address: int
|
|
38
|
+
Address the generator is currently at.
|
|
39
|
+
remaining_word_bytes
|
|
40
|
+
Number of bytes left in the current word.
|
|
41
|
+
"""
|
|
42
|
+
current_address = start_address
|
|
43
|
+
remaining_byte_length = byte_length
|
|
44
|
+
word_start_address = (start_address // bus_width) * bus_width
|
|
45
|
+
while remaining_byte_length > 0:
|
|
46
|
+
remaining_word_bytes = bus_width - (current_address % bus_width)
|
|
47
|
+
if remaining_word_bytes > remaining_byte_length:
|
|
48
|
+
remaining_word_bytes = remaining_byte_length
|
|
49
|
+
|
|
50
|
+
yield current_address, remaining_word_bytes
|
|
51
|
+
|
|
52
|
+
current_address += remaining_word_bytes
|
|
53
|
+
if not auto_increment:
|
|
54
|
+
current_address = word_start_address + (current_address % bus_width)
|
|
55
|
+
remaining_byte_length -= remaining_word_bytes
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def pad_data(
|
|
59
|
+
data: bytes | bytearray,
|
|
60
|
+
padded_byte_length: int | None,
|
|
61
|
+
) -> bytearray:
|
|
62
|
+
"""
|
|
63
|
+
Pad the start of a bytearray to have the desired length.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
data : bytearray
|
|
68
|
+
Data to be padded.
|
|
69
|
+
padded_byte_length : int or None
|
|
70
|
+
Desired length to pad the data to.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
data_padded : bytearray
|
|
75
|
+
Padded data.
|
|
76
|
+
"""
|
|
77
|
+
if padded_byte_length is None:
|
|
78
|
+
return bytearray(data)
|
|
79
|
+
|
|
80
|
+
byte_length = len(data)
|
|
81
|
+
if padded_byte_length < len(data):
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"Data (len: {byte_length}) is longer than requested "
|
|
84
|
+
f"byte_length: {padded_byte_length}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
data_padded = bytearray(padded_byte_length - len(data))
|
|
88
|
+
data_padded.extend(data)
|
|
89
|
+
return data_padded
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def flip_byteorder(
|
|
93
|
+
data_bytes: bytes, from_: Literal["little", "big"], to_: Literal["little", "big"]
|
|
94
|
+
) -> bytes:
|
|
95
|
+
"""Reorder the bytes from one endianness into another."""
|
|
96
|
+
data_int = int.from_bytes(data_bytes, byteorder=from_)
|
|
97
|
+
return data_int.to_bytes(len(data_bytes), byteorder=to_)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def flip_integer_byteorder(
|
|
101
|
+
value: int,
|
|
102
|
+
n_bytes: int,
|
|
103
|
+
from_: Literal["little", "big"],
|
|
104
|
+
to_: Literal["little", "big"],
|
|
105
|
+
) -> int:
|
|
106
|
+
"""Flip the byteorder of the integer."""
|
|
107
|
+
value_bytes = value.to_bytes(n_bytes, byteorder=from_)
|
|
108
|
+
return int.from_bytes(value_bytes, byteorder=to_)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Local cache of the hardware memory map register values."""
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from typing_extensions import TypeVar
|
|
7
|
+
|
|
8
|
+
from corel_mmio.mapping import SliceableMutableMapping
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from corel_mmio.memmap.memmap import MemoryMap
|
|
12
|
+
from corel_mmio.memmap.register import Register
|
|
13
|
+
|
|
14
|
+
TType = TypeVar("TType")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Cache:
|
|
18
|
+
"""
|
|
19
|
+
Local cache of the hardware memory map register values.
|
|
20
|
+
|
|
21
|
+
Useful for tracking the values written to write-only registers, so that
|
|
22
|
+
unintentional overwrites can be avoided when writing to fields which
|
|
23
|
+
share that same register.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, size: int):
|
|
27
|
+
self._size = size
|
|
28
|
+
self._bus = SliceableMutableMapping(size=size, mapping=defaultdict(lambda: 0))
|
|
29
|
+
|
|
30
|
+
def push_address(self, word_address: int, value: int) -> None:
|
|
31
|
+
"""Update the cache with the value of a register at an address."""
|
|
32
|
+
self._bus[word_address] = value
|
|
33
|
+
|
|
34
|
+
def push_register(self, register: "Register", value: int) -> None:
|
|
35
|
+
"""Update the cache with the value of a register."""
|
|
36
|
+
self.push_address(register.word_address, value)
|
|
37
|
+
|
|
38
|
+
def pull_address(self, word_address: int) -> int:
|
|
39
|
+
"""Obtain the currently cached value of a register at an address."""
|
|
40
|
+
return self._bus[word_address]
|
|
41
|
+
|
|
42
|
+
def pull_register(self, register: "Register") -> int:
|
|
43
|
+
"""Obtain the currently cached value of a register."""
|
|
44
|
+
return self.pull_address(register.word_address)
|
|
45
|
+
|
|
46
|
+
def pull_fields(self, register: "Register[TType]") -> dict[str, TType]:
|
|
47
|
+
"""Obtain the currently cached values of the fields in a register."""
|
|
48
|
+
register_value = self.pull_register(register)
|
|
49
|
+
return register.extract_field_values(register_value)
|
|
50
|
+
|
|
51
|
+
def update_from_defaults(self, memmap: "MemoryMap") -> None:
|
|
52
|
+
"""Update the cache with the default value of each register in the memory map."""
|
|
53
|
+
for register in memmap.registers:
|
|
54
|
+
self.push_register(register, register.default_value)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Containers related to device addressing."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Iterator
|
|
4
|
+
from enum import Flag
|
|
5
|
+
from functools import cache, reduce
|
|
6
|
+
from operator import or_ as _or_
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
|
|
9
|
+
from typing_extensions import Self # TODO: in Python 3.11 use typing.Self
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DeviceFlag",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T", bound="DeviceFlag")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DeviceFlag(Flag):
|
|
19
|
+
"""
|
|
20
|
+
Flag enum for indicating the sub-device involved in an operation.
|
|
21
|
+
|
|
22
|
+
Used when addressing multiple devices in a single operation, which is then
|
|
23
|
+
executed either sequentially or simultaneously (broadcast).
|
|
24
|
+
|
|
25
|
+
This class is intended as a baseclass to be inherited, to be defined for
|
|
26
|
+
the specific sub-devices of a project.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# noinspection PyTypeChecker
|
|
30
|
+
@property
|
|
31
|
+
def value(self) -> int:
|
|
32
|
+
"""The value of the Enum member."""
|
|
33
|
+
return super().value
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def name(self) -> str:
|
|
37
|
+
"""The name of the Enum member."""
|
|
38
|
+
class_name = self.__class__.__name__
|
|
39
|
+
return super().name or str(self).replace(f"{class_name}.", "")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def solo(self) -> bool:
|
|
43
|
+
"""True if the addressed device is solo."""
|
|
44
|
+
n = self.value
|
|
45
|
+
return n > 0 and (n & (n - 1)) == 0
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
@cache
|
|
49
|
+
def distinct(cls) -> list[Self]:
|
|
50
|
+
"""Obtain a list of the distinct devices."""
|
|
51
|
+
# TODO: unnecessary in 3.11?
|
|
52
|
+
return [n for n in cls if n.solo]
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
@cache
|
|
56
|
+
def n_distinct(cls) -> int:
|
|
57
|
+
"""Number of distinct devices."""
|
|
58
|
+
return len(cls.distinct())
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def validate_bit_string(cls, string: str) -> None:
|
|
62
|
+
"""Validate that a 32 digit bit string is valid for indicating the device."""
|
|
63
|
+
n = cls.n_distinct()
|
|
64
|
+
error_msg = f"Must be a bit string of at-most length {n}"
|
|
65
|
+
|
|
66
|
+
if len(string) > n:
|
|
67
|
+
raise ValueError(error_msg)
|
|
68
|
+
|
|
69
|
+
# Test that the string is a binary
|
|
70
|
+
try:
|
|
71
|
+
int(string, 2)
|
|
72
|
+
except ValueError:
|
|
73
|
+
raise ValueError(error_msg)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_bit_string(cls, string: str) -> Self:
|
|
77
|
+
"""Create from a bit string notation."""
|
|
78
|
+
cls.validate_bit_string(string)
|
|
79
|
+
return cls(int(string, 2))
|
|
80
|
+
|
|
81
|
+
def to_bit_string(self) -> str:
|
|
82
|
+
"""Convert to a bit string notation."""
|
|
83
|
+
n = self.n_distinct()
|
|
84
|
+
return f"{self.value:0{n}b}"
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def join(cls, devices: Iterable[Self]) -> Self:
|
|
88
|
+
"""Join multiple devices into a single flag."""
|
|
89
|
+
return cls(reduce(_or_, devices, cls(0)))
|
|
90
|
+
|
|
91
|
+
def split(self) -> Iterator[Self]:
|
|
92
|
+
"""Split into solo devices."""
|
|
93
|
+
for device in self.distinct():
|
|
94
|
+
if device in self:
|
|
95
|
+
yield device
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
@cache
|
|
99
|
+
def permutations(cls) -> list[Self]:
|
|
100
|
+
"""Obtain a list of all possible permutations of the devices."""
|
|
101
|
+
n = cls.n_distinct()
|
|
102
|
+
return [cls(v) for v in range(2**n)]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Utils for documentation."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import MISSING
|
|
4
|
+
from typing import (
|
|
5
|
+
TYPE_CHECKING,
|
|
6
|
+
Generic,
|
|
7
|
+
Literal,
|
|
8
|
+
TypeVar,
|
|
9
|
+
overload,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from typing_extensions import Self
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
Owner = TypeVar("Owner")
|
|
18
|
+
Value = TypeVar("Value")
|
|
19
|
+
|
|
20
|
+
___all__ = [
|
|
21
|
+
"DocDescriptor",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DocDescriptor(Generic[Owner, Value]):
|
|
26
|
+
"""Descriptor to add runtime-retrievable documentation to a dataclass field."""
|
|
27
|
+
|
|
28
|
+
# noinspection PyShadowingBuiltins
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
doc: str,
|
|
33
|
+
default: Value | Literal["MISSING"] = MISSING, # type: ignore[assignment]
|
|
34
|
+
):
|
|
35
|
+
self._doc = doc
|
|
36
|
+
self._default = default
|
|
37
|
+
|
|
38
|
+
if self._doc:
|
|
39
|
+
self.__doc__ = doc
|
|
40
|
+
|
|
41
|
+
def __set_name__(self, owner: type[Owner], name: str) -> None:
|
|
42
|
+
self._private_name = "_" + name
|
|
43
|
+
self._public_name = name
|
|
44
|
+
|
|
45
|
+
@overload
|
|
46
|
+
def __get__(self, obj: None, obj_type: type[Owner] | None) -> Self: ...
|
|
47
|
+
|
|
48
|
+
@overload
|
|
49
|
+
def __get__(self, obj: Owner, obj_type: type[Owner] | None = None) -> Value: ...
|
|
50
|
+
|
|
51
|
+
def __get__(
|
|
52
|
+
self, obj: Owner | None, obj_type: type[Owner] | None = None
|
|
53
|
+
) -> Value | Self:
|
|
54
|
+
# if obj is None:
|
|
55
|
+
# return self
|
|
56
|
+
|
|
57
|
+
if not hasattr(obj, self._private_name):
|
|
58
|
+
raise AttributeError(f"'{self._private_name[1:]}' has not been set")
|
|
59
|
+
|
|
60
|
+
value: Value = getattr(obj, self._private_name)
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
def __set__(self, obj: Owner, value: Value | Self) -> None:
|
|
64
|
+
if value == self:
|
|
65
|
+
if self._default is not MISSING:
|
|
66
|
+
value = self._default # type: ignore[assignment]
|
|
67
|
+
else:
|
|
68
|
+
raise TypeError(
|
|
69
|
+
f"TypeError: {obj.__class__.__name__}.__init__() missing "
|
|
70
|
+
f"required positional argument: '{self._public_name}'"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
setattr(obj, self._private_name, value)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def doc(self) -> str:
|
|
77
|
+
"""Get the documentation string for this descriptor."""
|
|
78
|
+
return self._doc
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def default(self) -> Value | Literal["MISSING"]:
|
|
82
|
+
"""Get the default value for this descriptor."""
|
|
83
|
+
return self._default
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Safe evaluation of expressions."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"UnsupportedClassError",
|
|
8
|
+
"safe_eval_expr",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
_T = TypeVar("_T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UnsupportedClassError(ValueError):
|
|
15
|
+
"""Raised when a class is not supported."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def safe_eval_expr(expr: str, supported: dict[str, type[_T]]) -> _T:
|
|
19
|
+
"""Safely evaluates an expression of the form ClassName(arg=value)."""
|
|
20
|
+
tree = ast.parse(expr, mode="eval")
|
|
21
|
+
|
|
22
|
+
# Ensure it's a function call
|
|
23
|
+
if not isinstance(tree.body, ast.Call):
|
|
24
|
+
raise ValueError("Expression must be a function call")
|
|
25
|
+
|
|
26
|
+
# Ensure it's a valid function name
|
|
27
|
+
if not isinstance(tree.body.func, ast.Name):
|
|
28
|
+
raise ValueError("Invalid function call")
|
|
29
|
+
|
|
30
|
+
class_name = tree.body.func.id
|
|
31
|
+
|
|
32
|
+
# Ensure it's a supported class
|
|
33
|
+
if class_name not in supported:
|
|
34
|
+
raise UnsupportedClassError(f"Class {class_name} is not allowed")
|
|
35
|
+
|
|
36
|
+
# Extract positional arguments
|
|
37
|
+
args = []
|
|
38
|
+
for arg in tree.body.args:
|
|
39
|
+
if not isinstance(arg, ast.Constant): # Only allow simple constants
|
|
40
|
+
raise ValueError("Only constant positional arguments are allowed")
|
|
41
|
+
args.append(arg.value)
|
|
42
|
+
|
|
43
|
+
# Extract keyword arguments
|
|
44
|
+
kwargs = {}
|
|
45
|
+
for kw in tree.body.keywords:
|
|
46
|
+
if not isinstance(kw.value, ast.Constant) or not isinstance(kw.arg, str):
|
|
47
|
+
raise ValueError("Only constant keyword arguments are allowed")
|
|
48
|
+
kwargs[kw.arg] = kw.value.value
|
|
49
|
+
|
|
50
|
+
# Instantiate the class safely
|
|
51
|
+
return supported[class_name](*args, **kwargs)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Interface definitions between the PC software and remote memory-mapped hardware."""
|