diff --git a/README.md b/README.md index f9181d2a..781a7572 100644 --- a/README.md +++ b/README.md @@ -139,4 +139,14 @@ some legacy features from the early versions were removed in release 4.0.0, the This had some restrictions, so a new methodology based on the `list` type was introduced in version 0.9. The old array based behaviour was removed in this version +This release also improved the features for write-only register: +- field write operations no longer have a hard-code base value of `0` in the register, the base register value is configurable via the `write_initial_state` property +- the `write_fields` methods also uses the `write_initial_state` property to allow other bits in the register to be set if needed, rather than a hardcoded `0` +- A new `single_write` context manager can be used to set multiple field values in a single write with a configurable initial register state + +The read/write register class has similar feature: +- A new `single_write` context manager that does not use a read before the write to set multiple field values in a single write with a configurable initial register state +- A new `write_all_fields_without_read` method tht will write all the writable fields with the `write_initial_state` property to allow other bits in the register to be set if needed + + diff --git a/docs/generated_package/callbacks.rst b/docs/generated_package/callbacks.rst new file mode 100644 index 00000000..1f7fa286 --- /dev/null +++ b/docs/generated_package/callbacks.rst @@ -0,0 +1,113 @@ +Callbacks +********* + +The Register Access Layer will typically interfaced to a driver that +allows accesses the chip. + +.. tip:: The simulator generated with the register model can be used as an alternative to + a hardware connection + +In order to operate the register access layer typically requires the following: + +- A callback for a single register write, this not required if there is no writable register in + the register access layer +- A callback for a single register read, this not required if there is no writable register in + the register access layer + +In addition the register access layer can make use of block operations where a block of the +address space is read in a single transaction. Not all drivers support these + +The examples of these two methods are included within the generated register +access layer package so that it can be used from the console: + +.. code-block:: python + + def read_addr_space(addr: int, width: int, accesswidth: int) -> int: + """ + Callback to simulate the operation of the package, everytime the read is called, it will + request the user input the value to be read back. + + Args: + addr: Address to write to + width: Width of the register in bits + accesswidth: Minimum access width of the register in bits + + Returns: + value inputted by the used + """ + assert isinstance(addr, int) + assert isinstance(width, int) + assert isinstance(accesswidth, int) + return input('value to read from address:0x%X' % addr) + + def write_addr_space(addr: int, width: int, accesswidth: int, data: int) -> None: + """ + Callback to simulate the operation of the package, everytime the write is called, it will + print out the result. + + Args: + addr: Address to write to + width: Width of the register in bits + accesswidth: Minimum access width of the register in bits + data: value to be written to the register + + Returns: + None + """ + assert isinstance(addr, int) + assert isinstance(width, int) + assert isinstance(accesswidth, int) + assert isinstance(data, int) + print('write data:0x%X to address:0x%X' % (data, addr)) + +In a real system these call backs will be connected to a driver. + +In addition there is also an option to use ``async`` callbacks if the package is built +``asyncoutput`` set to True. + +Callback Set +============ + +The callbacks are passed into the register access layer using either: + +* ``NormalCallbackSet`` for standard python function callbacks +* ``AsyncCallbackSet`` for async python function callbacks, these are called from the library using + ``await`` + +Legacy Block Callback and Block Access +====================================== + +.. versionchanged:: 0.9.0 + + Previous versions of PeakRDL Python used the python ``array.array`` for efficiently moving blocks + of data. This was changed in version 0.9.0 in order to accommodate memories which were larger + than 64 bit wide which could not be supported as the array type only support entries of up to + 64 bit. + + .. warning:: + The developers apologise for making a breaking change, however, not being able to fully the + systemRDL specification was determined to be a major limitation that needed to be addressed. + + It could have left this as a future compatibility mode before making a breaking change but + that would just delay the pain it was felt to be better to get as many users onto the new + API as soon as possible whilst PeakRDL Python is in beta. + + If you really want to just keep on with the array based interface and make only minimal changes + to existing code, there are two simple steps: + + 1. The northbound interfaces that are provided by the generated package expect lists of integers + rather than array. The old interfaces can be retained by using the ``legacy_block_access`` + build option. + 2. The southbound interfaces into the callbacks again need to use lists for the + ``read_block_callback`` and ``write_block_callback`` methods. If you want to continue to use + the old scheme use the following callback classes which are part of the callbacks: + * ``NormalCallbackSetLegacy`` for standard python function callbacks + * ``AsyncCallbackSetLegacy`` for async python function callbacks, these are called from the library using ``await`` + +.. versionchanged:: 3.0.0 + + The ``legacy_block_access`` will now default to ``False`` + +.. versionchanged:: 4.0.0 + + The ``legacy_block_access`` was removed, the ``NormalCallbackSetLegacy`` and ``AsyncCallbackSetLegacy`` are no longer supported \ No newline at end of file diff --git a/docs/generated_package/output_structure.rst b/docs/generated_package/output_structure.rst new file mode 100644 index 00000000..825a29a9 --- /dev/null +++ b/docs/generated_package/output_structure.rst @@ -0,0 +1,123 @@ +Output Structure +**************** + +PeakRDL Python generates a python Register Access Layer (RAL) from a System RDL design. The +generated python code is contained in a package, structured as shown below. The ``root_name`` +will be based on the top level address map name with the package was generated + +| ```` +| ├── ``lib`` +| ├── ``lib_test`` +| ├── ``sim_lib`` +| ├── ``reg_model`` +| │ └── ``.py`` +| ├── ``sim`` +| │ └── ``.py`` +| └── ``tests`` +| └── ``test_.py`` + +In the folder structure above: + +- ``.py`` - This is the register access layer code for the design +- ``test_.py`` - This is a set of autogenerated unittests to verify the register access layer (The unit test generation can be skipped, see ``skip_test_case_generation``) +- ``lib`` - This is a package of base classes used by the register access layer (The copy of this can be skipped, see :ref:`skipping-lib-copy`) +- ``lib_test`` - This is a package of base classes autogenerated unit tests (The copy of this can be skipped, see :ref:`skipping-lib-copy`) +- ``sim_lib`` - This is a package of base classes used by the register access layer simulator (The copy of this can be skipped, see :ref:`skipping-lib-copy`) + +.. versionchanged:: 2.0.0 + + The ``reg_model`` was changed in version 2.0.0 to split it out into multiple modules rather + than building the whole register model in a single python module. This helps avoid + excessively large files which helps speed up the generation and loading time. + +.. versionchanged:: 3.0.0 + + The auto-generated unit tests were changed in version 3.0.0 to split to make use + of a test library, which significantly reduced the size of the generated code. + +Top Level Classes +================= + +.. versionchanged:: 2.0.0 + + A new class aliases were added to the ``reg_model`` and ``sim`` packages to allow the register + model and simulator to be imported more easily. See the example below using ``RegModel`` and + ``Simulator``. + +Class Names +=========== + +.. versionchanged:: 2.0.0 + + In order to reduce duplication within the generated model a hashing algortihm is applied to the + nodes in the design to determine which nodes are unique. This hash is appended to the name + of all the python classes in the register model + +.. versionadded:: 2.1.0 + + There are two possible algorithms for the hashing, this is selectable by the user: + + * The builtin python ``hash`` function, this is fast but is a salted hash so changes hashes export to + export + * Use the ``SHA256`` hash from the python ``hashlib`` standard library, this may slow down the export + of large register models but will be consistent, therefore is useful if the resultant code is being + checked into a version control system (such as GIT) and the differences are being reviewed + + .. tip:: + + Use the default builtin ``hash`` option unless you need to use the alternative + +Unit Tests +========== + +In addition to the Register Model a set od unit tests are made that test that the register model has been +built correctly + +Running the Unit Tests +---------------------- + +There are many ways to run Python Unit tests. A good place to start is the ``unittest`` module +included in the Python standard installation. + + +Simulator +========= + +PeakRDL Python also generates an simulator, this can be used to test and develop using the +generated package. The simulator is used in a the examples shown earlier in this section. The +simulator has the option to attach a callback to the read and write operations of either a +register or field. In addition there is a ``value`` property that allows access to the register +or field content, this allows the contents to be accessed or updated without activating the +callbacks, this is intended to allow the simulator to be extended with behaviour that is not +fully described by the systemRDL. + +.. warning:: The PeakRDL Python simulator is not intended to replace an RTL simulation of the + design. It does not simulate the hardware, it is intended as a simple tool for + development and testing of the python wrappers or code that uses them. + +.. _skipping-lib-copy: + +Skipping the Library Copy +========================= + +In some cases it may be desirable to skip the copy of the library from the generated pacakge. +This may be useful in development of PeakRDL-python. It can also be used to avoid distributing +licensed code. + +.. warning:: If the libraries are not distributed with the package, it is very important to + ensure users download the matching version of the PeakRDL-python package to use it + +Autoformatting +============== + +The generated code is not perfect it often has lots of spare black lines, over time this will +improve but the quickest way to resolve these issue is to include an autoformatter +post-generation. Previous versions of peakrdl-python included the option to run an autoformatter +to clean up the generated code. This had two issues: + +* It created maintenance issues when the autoformatter changed +* The choice of autoformatter is an individual one, rather than force an autoformatter on people + it is better to let people choose their own. + +peakrdl-python uses the Black `Black `_ in the CI tests to check +that the generated code is compatible with an autoformatter. \ No newline at end of file diff --git a/docs/generated_package.rst b/docs/generated_package/using_ral.rst similarity index 50% rename from docs/generated_package.rst rename to docs/generated_package/using_ral.rst index 9db75934..fabd8f6c 100644 --- a/docs/generated_package.rst +++ b/docs/generated_package/using_ral.rst @@ -1,198 +1,5 @@ -Generated Package -***************** - -Output Structure -================ -PeakRDL Python generates a python Register Access Layer (RAL) from a System RDL design. The -generated python code is contained in a package, structured as shown below. The ``root_name`` -will be based on the top level address map name with the package was generated - -| ```` -| ├── ``lib`` -| ├── ``lib_test`` -| ├── ``sim_lib`` -| ├── ``reg_model`` -| │ └── ``.py`` -| ├── ``sim`` -| │ └── ``.py`` -| └── ``tests`` -| └── ``test_.py`` - -In the folder structure above: - -- ``.py`` - This is the register access layer code for the design -- ``test_.py`` - This is a set of autogenerated unittests to verify the register access layer (The unit test generation can be skipped, see ``skip_test_case_generation``) -- ``lib`` - This is a package of base classes used by the register access layer (The copy of this can be skipped, see :ref:`skipping-lib-copy`) -- ``lib_test`` - This is a package of base classes autogenerated unit tests (The copy of this can be skipped, see :ref:`skipping-lib-copy`) -- ``sim_lib`` - This is a package of base classes used by the register access layer simulator (The copy of this can be skipped, see :ref:`skipping-lib-copy`) - -.. versionchanged:: 2.0.0 - - The ``reg_model`` was changed in version 2.0.0 to split it out into multiple modules rather - than building the whole register model in a single python module. This helps avoid - excessively large files which helps speed up the generation and loading time. - -.. versionchanged:: 3.0.0 - - The auto-generated unit tests were changed in version 3.0.0 to split to make use - of a test library, which significantly reduced the size of the generated code. - -Top Level Classes ------------------ - -.. versionchanged:: 2.0.0 - - A new class aliases were added to the ``reg_model`` and ``sim`` packages to allow the register - model and simulator to be imported more easily. See the example below using ``RegModel`` and - ``Simulator``. - -Class Names ------------ - -.. versionchanged:: 2.0.0 - - In order to reduce duplication within the generated model a hashing algortihm is applied to the - nodes in the design to determine which nodes are unique. This hash is appended to the name - of all the python classes in the register model - -.. versionadded:: 2.1.0 - - There are two possible algorithms for the hashing, this is selectable by the user: - - * The builtin python ``hash`` function, this is fast but is a salted hash so changes hashes export to - export - * Use the ``SHA256`` hash from the python ``hashlib`` standard library, this may slow down the export - of large register models but will be consistent, therefore is useful if the resultant code is being - checked into a version control system (such as GIT) and the differences are being reviewed - - .. tip:: - - Use the default builtin ``hash`` option unless you need to use the alternative - - - -Running the Unit Tests -====================== - -There are many ways to run Python Unit tests. A good place to start is the ``unittest`` module -included in the Python standard installation. - -Callbacks -========= - -The Register Access Layer will typically interfaced to a driver that -allows accesses the chip. - -.. tip:: The simulator generated with the register model can be used as an alternative to - a hardware connection - -In order to operate the register access layer typically requires the following: - -- A callback for a single register write, this not required if there is no writable register in - the register access layer -- A callback for a single register read, this not required if there is no writable register in - the register access layer - -In addition the register access layer can make use of block operations where a block of the -address space is read in a single transaction. Not all drivers support these - -The examples of these two methods are included within the generated register -access layer package so that it can be used from the console: - -.. code-block:: python - - def read_addr_space(addr: int, width: int, accesswidth: int) -> int: - """ - Callback to simulate the operation of the package, everytime the read is called, it will - request the user input the value to be read back. - - Args: - addr: Address to write to - width: Width of the register in bits - accesswidth: Minimum access width of the register in bits - - Returns: - value inputted by the used - """ - assert isinstance(addr, int) - assert isinstance(width, int) - assert isinstance(accesswidth, int) - return input('value to read from address:0x%X' % addr) - - def write_addr_space(addr: int, width: int, accesswidth: int, data: int) -> None: - """ - Callback to simulate the operation of the package, everytime the write is called, it will - print out the result. - - Args: - addr: Address to write to - width: Width of the register in bits - accesswidth: Minimum access width of the register in bits - data: value to be written to the register - - Returns: - None - """ - assert isinstance(addr, int) - assert isinstance(width, int) - assert isinstance(accesswidth, int) - assert isinstance(data, int) - print('write data:0x%X to address:0x%X' % (data, addr)) - -In a real system these call backs will be connected to a driver. - -In addition there is also an option to use ``async`` callbacks if the package is built -``asyncoutput`` set to True. - -Callback Set ------------- - -The callbacks are passed into the register access layer using either: - -* ``NormalCallbackSet`` for standard python function callbacks -* ``AsyncCallbackSet`` for async python function callbacks, these are called from the library using - ``await`` - -Legacy Block Callback and Block Access --------------------------------------- - -.. versionchanged:: 0.9.0 - - Previous versions of PeakRDL Python used the python ``array.array`` for efficiently moving blocks - of data. This was changed in version 0.9.0 in order to accommodate memories which were larger - than 64 bit wide which could not be supported as the array type only support entries of up to - 64 bit. - - .. warning:: - The developers apologise for making a breaking change, however, not being able to fully the - systemRDL specification was determined to be a major limitation that needed to be addressed. - - It could have left this as a future compatibility mode before making a breaking change but - that would just delay the pain it was felt to be better to get as many users onto the new - API as soon as possible whilst PeakRDL Python is in beta. - - If you really want to just keep on with the array based interface and make only minimal changes - to existing code, there are two simple steps: - - 1. The northbound interfaces that are provided by the generated package expect lists of integers - rather than array. The old interfaces can be retained by using the ``legacy_block_access`` - build option. - 2. The southbound interfaces into the callbacks again need to use lists for the - ``read_block_callback`` and ``write_block_callback`` methods. If you want to continue to use - the old scheme use the following callback classes which are part of the callbacks: - * ``NormalCallbackSetLegacy`` for standard python function callbacks - * ``AsyncCallbackSetLegacy`` for async python function callbacks, these are called from the library using ``await`` - -.. versionchanged:: 3.0.0 - - The ``legacy_block_access`` will now default to ``False`` - -.. versionchanged:: 4.0.0 - - The ``legacy_block_access`` was removed, the ``NormalCallbackSetLegacy`` and ``AsyncCallbackSetLegacy`` are no longer supported - Using the Register Access Layer -=============================== +******************************* The register access layer package is intended to integrated into another piece of code. That code could be a simple test script for blinking an LED on a @@ -206,7 +13,7 @@ registers: This can be described with the following systemRDL code: -.. literalinclude :: ../example/simulating_callbacks/chip_with_a_GPIO.rdl +.. literalinclude :: ../../example/simulating_callbacks/chip_with_a_GPIO.rdl :language: systemrdl This systemRDL code can be built using the command line tool as follows (assuming it is stored in @@ -225,16 +32,20 @@ incorporating a RED circle to represent the LED. The chip simulator has read and equivalent to those offered by a hardware device driver), in this case they use the simulator provided by PeakRDL Python. -.. literalinclude :: ../example/simulating_callbacks/flashing_the_LED.py +.. literalinclude :: ../../example/simulating_callbacks/flashing_the_LED.py :language: python +.. warning:: In most cases when writing to a read-write register, PeakRDL Python will use a read modify write operation. + There are some cases where this is not desirable, for example in the case of a register where a read + triggers an action in the hardware e.g. incrementing a counter. + Enumerated Fields ------------------ +================= Enumerations are a good practice to implicitly encode that have special meanings which can not be easily understood from the field name. The SystemRDL enumerations are implemented using python -.. literalinclude :: ../example/enumerated_fields/enumerated_fields.rdl +.. literalinclude :: ../../example/enumerated_fields/enumerated_fields.rdl :language: systemrdl This systemRDL code can be built using the command line tool as follows (assuming it is stored in @@ -251,17 +62,17 @@ The following example shows the usage of the enumeration enumerated class is needed. This can be retrieved from the field itself with the ``enum_cls`` property -.. literalinclude :: ../example/enumerated_fields/demo_enumerated_fields.py +.. literalinclude :: ../../example/enumerated_fields/demo_enumerated_fields.py :language: python Array Access ------------- +============ SystemRDL supports multi-dimensional arrays, the following example shows an definition with an 1D and 3D array with various methods to access individual elements of the array and use of the iterators to walk through elements in loops -.. literalinclude :: ../example/array_access/array_access.rdl +.. literalinclude :: ../../example/array_access/array_access.rdl :language: systemrdl This systemRDL code can be built using the command line tool as follows (assuming it is stored in @@ -271,14 +82,19 @@ a file called ``array_access.rdl``): peakrdl python array_access.rdl -o . -.. literalinclude :: ../example/array_access/demo_array_access.py +.. literalinclude :: ../../example/array_access/demo_array_access.py :language: python +Write Only Registers +==================== + +TBA + Optimised Access ----------------- +================ Working with individual registers -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +--------------------------------- Each time the ``read`` or ``write`` method for a register field is accessed the hardware is read and or written (a write to a field will normally require a preceding read). When accessing multiple @@ -286,39 +102,46 @@ fields in the same register, it may be desirable to use one of the optimised acc Consider the following example of an GPIO block with 4 GPIO pins (configured in a single register): -.. literalinclude :: ../example/optimised_access/optimised_access.rdl +.. literalinclude :: ../../example/optimised_access/optimised_access.rdl :language: systemrdl In the to configure gpio_0 and gpio_1 whilst leaving the other two unaffected it can be done in two methods: -* using the ``write_fields`` method of the register +* using the ``write_fields`` method of the register, which carries out a single read and write of any field specified + in the arguments * using the register context manager Both demonstrated in the following code example: -.. literalinclude :: ../example/optimised_access/demo_optimised_access.py +.. literalinclude :: ../../example/optimised_access/demo_optimised_access.py :language: python +Avoiding the Read on a Read/Write Register +------------------------------------------ + +TBA + + Working with registers arrays -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +----------------------------- In many systems it is more efficient to read and write in block operations rather than using individual register access. Consider the following example of an GPIO block with 8 GPIO pins (configured in a 8 registers): -.. literalinclude :: ../example/optimised_access/optimised_array_access.rdl +.. literalinclude :: ../../example/optimised_access/optimised_array_access.rdl :language: systemrdl In order to configure all the GPIOs a range of operations are shown with the use of the context managers to make more efficient operations -.. literalinclude :: ../example/optimised_access/demo_optimised_array_access.py +.. literalinclude :: ../../example/optimised_access/demo_optimised_array_access.py :language: python Walking the Structure ---------------------- +===================== The following two example show how to use the generators within the register access layer package to traverse the structure. @@ -326,7 +149,7 @@ package to traverse the structure. Both examples use the following register set which has a number of features to demonstrate the structures -.. literalinclude :: ../example/tranversing_address_map/chip_with_registers.rdl +.. literalinclude :: ../../example/tranversing_address_map/chip_with_registers.rdl :language: systemrdl This systemRDL code can be built using the command line tool as follows (assuming it is stored in @@ -338,14 +161,14 @@ a file called ``chip_with_registers.rdl``): Traversing without Unrolling Loops -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +---------------------------------- The first example is reading all the readable registers from the register map and writing them into a JSON file. To exploit the capabilities of a JSON file the arrays of registers and register files must be converted to python lists, therefore the loops must not be unrolled, the array objects are accessed directly. -.. literalinclude :: ../example/tranversing_address_map/dumping_register_state_to_json_file.py +.. literalinclude :: ../../example/tranversing_address_map/dumping_register_state_to_json_file.py :language: python This will create a JSON file as follows: @@ -430,17 +253,17 @@ This will create a JSON file as follows: } Traversing without Unrolling Loops -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +---------------------------------- The second example is setting every register in the address map back to its default values. In this case the loops are unrolled to conveniently access all the register without needing to worry if they are in an array or not. -.. literalinclude :: ../example/tranversing_address_map/reseting_registers.py +.. literalinclude :: ../../example/tranversing_address_map/reseting_registers.py :language: python Exposing User Defined Properties --------------------------------- +================================ SystemRDL allows properties to be added to any component (Field, Memory, Register, Register File, Address Map), so called *User Defined Properties (UDP)*. @@ -452,7 +275,7 @@ There are two methods to expose user defined properties: Consider the following systemRDL example with a user defined property: ``component_usage`` -.. literalinclude :: ../example/user_defined_properties/user_defined_properties.rdl +.. literalinclude :: ../../example/user_defined_properties/user_defined_properties.rdl :language: systemrdl User Defined Properties are not automatically included they must be specified, as shown: @@ -476,7 +299,7 @@ In the following case all UDPs are included, except the ones used by PeakRDL pyt The user defined properties are stored in a ``udp`` property of all component in the generated register access and can be accessed as follows: -.. literalinclude :: ../example/user_defined_properties/demo_user_defined_properties.py +.. literalinclude :: ../../example/user_defined_properties/demo_user_defined_properties.py :language: python .. versionadded:: 2.0.0 @@ -521,7 +344,7 @@ names used in the generated python code for node. In this case following names w * name of the register will be ``overridden_reg_a`` rather than ``reg_a`` * the name of the field will be ``overridden_field_a`` rather than ``field_a`` -.. literalinclude :: ../example/overridden_names/overridden_names.rdl +.. literalinclude :: ../../example/overridden_names/overridden_names.rdl :language: systemrdl Name lookup @@ -532,7 +355,7 @@ User Defined Property), attributes can be accessed using the ``get_child_by_syst method of any object in the register model. The following example shows both methods to access the field from the example above -.. literalinclude :: ../example/overridden_names/demo_over_ridden_names.py +.. literalinclude :: ../../example/overridden_names/demo_over_ridden_names.py :language: python Hidden Elements @@ -594,46 +417,3 @@ PeakRDL Python supports hiding elements of the based on a regular expression. regfiles, address maps or memories with the name ``RSVD``, the regular expression must match on the full name e.g. ``(?:[\w_\[\]]+\.)+RSVD`` - -Autoformatting -============== - -The generated code is not perfect it often has lots of spare black lines, over time this will -improve but the quickest way to resolve these issue is to include an autoformatter -post-generation. Previous versions of peakrdl-python included the option to run an autoformatter -to clean up the generated code. This had two issues: - -* It created maintenance issues when the autoformatter changed -* The choice of autoformatter is an individual one, rather than force an autoformatter on people - it is better to let people choose their own. - -peakrdl-python uses the Black `Black `_ in the CI tests to check -that the generated code is compatible with an autoformatter. - - -Simulator -========= - -PeakRDL Python also generates an simulator, this can be used to test and develop using the -generated package. The simulator is used in a the examples shown earlier in this section. The -simulator has the option to attach a callback to the read and write operations of either a -register or field. In addition there is a ``value`` property that allows access to the register -or field content, this allows the contents to be accessed or updated without activating the -callbacks, this is intended to allow the simulator to be extended with behaviour that is not -fully described by the systemRDL. - -.. warning:: The PeakRDL Python simulator is not intended to replace an RTL simulation of the - design. It does not simulate the hardware, it is intended as a simple tool for - development and testing of the python wrappers or code that uses them. - -.. _skipping-lib-copy: - -Skipping the Library Copy -========================= - -In some cases it may be desirable to skip the copy of the library from the generated pacakge. -This may be useful in development of PeakRDL-python. It can also be used to avoid distributing -licensed code. - -.. warning:: If the libraries are not distributed with the package, it is very important to - ensure users download the matching version of the PeakRDL-python package to use it diff --git a/docs/index.rst b/docs/index.rst index 856a098c..3e72e4f7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -133,10 +133,9 @@ attempts to set the output state. .. toctree:: :hidden: :maxdepth: 2 - :caption: usage + :caption: Usage installation - generated_package api_components api command_line @@ -145,14 +144,23 @@ attempts to set the output state. .. toctree:: :hidden: :maxdepth: 2 - :caption: developer notes + :caption: Generated Package + + generated_package/output_structure + generated_package/callbacks + generated_package/using_ral + +.. toctree:: + :hidden: + :maxdepth: 2 + :caption: Developer notes design_decisions design_tools .. toctree:: :hidden: - :caption: other + :caption: Other genindex diff --git a/src/peakrdl_python/lib/async_register_and_field.py b/src/peakrdl_python/lib/async_register_and_field.py index 3221012f..e37e66cc 100644 --- a/src/peakrdl_python/lib/async_register_and_field.py +++ b/src/peakrdl_python/lib/async_register_and_field.py @@ -22,7 +22,7 @@ from enum import Enum from typing import Union, Optional, TypeVar, cast from collections.abc import AsyncGenerator, Iterator, Iterable -from abc import ABC, abstractmethod +from abc import ABC from contextlib import asynccontextmanager import sys from warnings import warn @@ -34,6 +34,7 @@ from .base_register import BaseReg, BaseRegArray, RegisterWriteVerifyError from .base_field import FieldEnum, FieldSizeProps, FieldMiscProps, \ _FieldReadOnlyFramework, _FieldWriteOnlyFramework, FieldType +from .field_encoding import SystemRDLEnum # pylint: disable=duplicate-code if sys.version_info >= (3, 11): @@ -103,6 +104,42 @@ def fields(self) -> \ """ yield from iter(self) + @property + def bitmask(self) -> int: + bitmask = 0 + for field in iter(self): + bitmask |= field.bitmask + + return bitmask + + def register_value(self, **kwargs: Union[int, 'SystemRDLEnum']) -> int: + self._check_kwargs_field_names(**kwargs) + register_value = 0 + for field_name, field_value in kwargs.items(): + field = getattr(self, field_name) + register_value &= field.inverse_bitmask + # pylint: disable=protected-access + int_value = field._int_value(field_value) + register_value |= field._register_bits(int_value) + # pylint: enable=protected-access + + return register_value + + def inferred_default_register_value(self) -> int: + + register_value = 0 + for field in self.fields: + default_value = field.default + if default_value is None: + continue + register_value &= field.inverse_bitmask + # pylint: disable=protected-access + int_default_value = field._int_value(default_value) + register_value |= field._register_bits(int_default_value) + # pylint: enable=protected-access + + return register_value + class RegAsyncReadOnly(AsyncReg, ABC): """ @@ -152,7 +189,8 @@ async def single_read(self) -> AsyncGenerator[Self]: self.__in_context_manager = False async def read(self) -> int: - """Asynchronously read value from the register + """ + Asynchronously read value from the register Returns: The value from register @@ -214,10 +252,10 @@ def _is_readable(self) -> bool: def _is_writeable(self) -> bool: return False - -class RegAsyncWriteOnly(AsyncReg, ABC): +#pylint:disable-next=invalid-name +class __RegWritable(AsyncReg, ABC): """ - class for an asynchronous write only register + base class for a writable register (either RegAsyncWriteOnly or RegAsyncReadWrite) """ __slots__: list[str] = [] @@ -227,8 +265,7 @@ def __init__(self, *, address: int, logger_handle: str, inst_name: str, - parent: Union[AsyncAddressMap, AsyncRegFile, - WritableAsyncMemory]): + parent: Union[AsyncAddressMap, AsyncRegFile, WritableAsyncMemory]): super().__init__(address=address, logger_handle=logger_handle, @@ -236,8 +273,25 @@ def __init__(self, *, parent=parent) # pylint: enable=too-many-arguments, duplicate-code + # pylint:disable=duplicate-code + @property + def writable_fields(self) -> Iterator[Union['FieldAsyncWriteOnly', 'FieldAsyncReadWrite']]: + """ + generator that produces has all the writable fields within the register + """ + + def is_writable(field: Union['FieldAsyncReadOnly', + 'FieldAsyncWriteOnly', + 'FieldAsyncReadWrite']) -> \ + TypeGuard[Union['FieldAsyncWriteOnly', 'FieldAsyncReadWrite']]: + return isinstance(field, (FieldAsyncWriteOnly, FieldAsyncReadWrite)) + + return filter(is_writable, self.fields) + # pylint:enable=duplicate-code + async def write(self, data: int) -> None: - """Asynchronously writes a value to the register + """ + Writes a value to the register Args: data: data to be written @@ -247,51 +301,148 @@ async def write(self, data: int) -> None: permissible values for the register TypeError: if the type of data is wrong """ - # this method check the types and range checks the data self._validate_data(data=data) - # pylint: disable=duplicate-code self._logger.info(f'Writing data:0x{data:X} to 0x{self.address:X}') - # pylint: enable=duplicate-code if self._callbacks.write_callback is not None: - await self._callbacks.write_callback(addr=self.address, - width=self.width, - accesswidth=self.accesswidth, - data=data) + width=self.width, + accesswidth=self.accesswidth, + data=data) elif self._callbacks.write_block_callback is not None: - await self._callbacks.write_block_callback(addr=self.address, width=self.width, accesswidth=self.accesswidth, data=[data]) + # pylint:disable=duplicate-code else: - # pylint: disable-next=duplicate-code raise RuntimeError('This function does not have a useable callback') + + + +class RegAsyncWriteOnly(__RegWritable, ABC): + """ + class for an asynchronous write only register + """ + + __slots__: list[str] = ['__in_context_manager', '__register_state', '__write_initial_state'] + + # pylint: disable=too-many-arguments, duplicate-code, useless-parent-delegation + def __init__(self, *, + address: int, + logger_handle: str, + inst_name: str, + parent: Union[AsyncAddressMap, AsyncRegFile, + WritableAsyncMemory]): + + super().__init__(address=address, + logger_handle=logger_handle, + inst_name=inst_name, + parent=parent) + + self.__in_context_manager: bool = False + self.__register_state: int = 0 + self.__write_initial_state: int = 0 + + # pylint: enable=too-many-arguments, duplicate-code + + async def write(self, data: int) -> None: + """ + Writes a value to the register + + Args: + data: data to be written + + Raises: + ValueError: if the value provided is outside the range of the + permissible values for the register + TypeError: if the type of data is wrong + RegisterWriteVerifyError: the read back data after the write does not match the + expected value + RuntimeError: if the read verify after write is set when inside a context manager + """ + + + if self.__in_context_manager: + self.__register_state = data + else: + await super().write(data) + @property - def writable_fields(self) -> Iterator[Union['FieldAsyncWriteOnly', 'FieldAsyncReadWrite']]: + def write_initial_state(self) -> int: """ - generator that produces has all the writable fields within the register + This property defines the initial state of the register when using: + - the `write_fields` method + - the `write` method on any field in the register + + By default this is initialised to `0`, however, advanced usages may include: + - A hidden field within the register which must not be set to `0` + + This property behaves accessed when inside the `single_write` when it returns the shadowed + state of the register which has not yet been writen back (the initial state plus any write + that has occurred). However, setting the property will still update the initial state not + the shadowed state. """ - def is_writable(field: Union['FieldAsyncReadOnly', - 'FieldAsyncWriteOnly', - 'FieldAsyncReadWrite']) -> \ - TypeGuard[Union['FieldAsyncWriteOnly', 'FieldAsyncReadWrite']]: - return isinstance(field, (FieldAsyncWriteOnly, FieldAsyncReadWrite)) + # This function by its nature is very similar to the non-async version + # pylint: disable=duplicate-code - return filter(is_writable, self.fields) + if self.__in_context_manager: + return self.__register_state + return self.__write_initial_state - @abstractmethod - async def write_fields(self, **kwargs) -> None: # type: ignore[no-untyped-def] + @write_initial_state.setter + def write_initial_state(self, initial_state: int) -> None: + # This function by its nature is very similar to the non-async version + # pylint: disable=duplicate-code + + # this method check the types and range checks the data + self._validate_data(data=initial_state) + self.__write_initial_state = initial_state + + async def write_fields(self, **kwargs: Union[int, SystemRDLEnum]) -> None: """ - Do an async write to the register, updating any field included in - the arguments + Do a single write to the register based on the fields provided, all fields must be + specified to use this method + + Any parts of the register not set with this method will be based on the + `write_initial_state` property + """ + self._check_kwargs_field_names(**kwargs) + + async with self.single_write(initial_state=self.write_initial_state) as reg: + for field_name, field_value in kwargs.items(): + if field_name not in reg.systemrdl_python_child_name_map.values(): + raise ValueError(f'{field_name} is not a member of the register') + + field = getattr(reg, field_name) + await field.write(field_value) + + @asynccontextmanager + async def single_write(self, initial_state: int) -> AsyncGenerator[Self]: + """ + Context manager to allow multiple field write operations to be carried out with a + single write at the end, based on a specified initial state of the register. """ + # This function by its nature is very similar to the non-async version + # pylint: disable=duplicate-code + + # this method check the types and range checks the data + self._validate_data(data=initial_state) + + self.__register_state = initial_state + self.__in_context_manager = True + # this try/finally is needed to make sure that in the event of an exception + # the state flags are not left incorrectly set + try: + yield self + finally: + self.__in_context_manager = False + await self.write(self.__register_state) @property def _is_readable(self) -> bool: @@ -302,13 +453,15 @@ def _is_writeable(self) -> bool: return True -class RegAsyncReadWrite(RegAsyncReadOnly, RegAsyncWriteOnly, ABC): +class RegAsyncReadWrite(RegAsyncReadOnly, __RegWritable, ABC): """ class for an async read and write only register """ - __slots__: list[str] = ['__in_read_write_context_manager', '__in_read_context_manager', - '__register_state'] + __slots__: list[str] = ['__in_read_context_manager', + '__in_write_context_manager', + '__register_state', + '__write_initial_state'] # pylint: disable=too-many-arguments, duplicate-code def __init__(self, *, @@ -322,64 +475,72 @@ def __init__(self, *, inst_name=inst_name, parent=parent) - self.__in_read_write_context_manager: bool = False self.__in_read_context_manager: bool = False + self.__in_write_context_manager: bool = False self.__register_state: Optional[int] = None + self.__write_initial_state = 0 # pylint: enable=too-many-arguments, duplicate-code @asynccontextmanager async def single_read_modify_write(self, verify: bool = False) -> AsyncGenerator[Self]: """ - Context manager to allow multiple field reads/write to be done with a single set of - field operations + Context manager to allow multiple field reads/write to be done with a single register read + and write Args: - verify (bool): very the write with a read afterwards - skip_write (bool): skip the write back at the end - + verify (bool): verify the write with a read afterwards """ - # pylint: disable=duplicate-code - if self.__in_read_context_manager: - raise RuntimeError('using the `single_read_modify_write` context manager within the ' - 'single_read` is not permitted') - # pylint: enable=duplicate-code - - self.__register_state = await self.read() - - # pylint: disable=duplicate-code - self.__in_read_write_context_manager = True - # this try/finally is needed to make sure that in the event of an exception - # the state flags are not left incorrectly set - try: - yield self - finally: - self.__in_read_write_context_manager = False - # pylint: enable=duplicate-code - await self.write(self.__register_state, verify) - - # clear the register states at the end of the context manager - self.__register_state = None + initial_state = await self.read() + async with self.single_write(initial_state=initial_state, verify=verify) as reg: + yield reg @asynccontextmanager async def single_read(self) -> AsyncGenerator[Self]: """ Context manager to allow multiple field reads with a single register read """ + # This function by its nature is very similar to the non-async version # pylint: disable=duplicate-code - if self.__in_read_write_context_manager: + if self.__in_read_context_manager: + raise RuntimeError('using the `single_read` context manager double layered is ' + 'not permitted') + + if self.__in_write_context_manager: raise RuntimeError('using the `single_read` context manager within the ' - 'single_read_modify_write` is not permitted') + 'single_read_modify_write` or `single_write` is not permitted') self.__in_read_context_manager = True - # pylint: enable=duplicate-code try: async with super().single_read() as reg: yield reg finally: - # pylint: disable=duplicate-code self.__in_read_context_manager = False - async def write(self, data: int, verify: bool = False) -> None: + @asynccontextmanager + async def single_write(self, initial_state: int, verify: bool = False) -> AsyncGenerator[Self]: + """ + Context manager to allow multiple field updates with a single register write when the + context manager exits + """ + # This function is by its nature very similar to the non-async version + # pylint: disable=duplicate-code + if self.__in_write_context_manager or self.__in_read_context_manager: + raise RuntimeError('using the `single_write` context manager within the ' + 'single_read_modify_write` or `single_read` is not permitted. ' + 'Similarly, using the `single_write` context manager within itself ' + 'is not permitted') + self.__in_write_context_manager = True + self.__register_state = initial_state + try: + yield self + finally: + self.__in_write_context_manager = False + await self.write(self.__register_state, verify=verify) + + # clear the register states at the end of the context manager + self.__register_state = None + + async def write(self, data: int, verify: bool = False) -> None: # pylint: disable=arguments-differ """ Writes a value to the register @@ -393,16 +554,21 @@ async def write(self, data: int, verify: bool = False) -> None: TypeError: if the type of data is wrong RegisterWriteVerifyError: the read back data after the write does not match the expected value + RuntimeError: if the read verify after write is set when inside a context manager """ + # This function is by its nature very similar to the non-async version + # pylint: disable=duplicate-code if self.__in_read_context_manager: - # pylint: disable=duplicate-code raise RuntimeError('writes within the single read context manager are not permitted') - if self.__in_read_write_context_manager: - # pylint: disable=duplicate-code + if self.__in_write_context_manager: if self.__register_state is None: raise RuntimeError('The internal register state should never be None in the ' 'context manager') + if verify: + raise RuntimeError('Using the verify option is not permitted inside the ' + '`single_write` or `single_read_modify_write` context manager') + self.__register_state = data else: await super().write(data) @@ -414,10 +580,18 @@ async def write(self, data: int, verify: bool = False) -> None: async def read(self) -> int: """ - Asynchronously read value from the register + Read value from the register + + Note: + The `read` method behaves differently when used within the one of the context managers: + - when used within `single_read_modify_write` or `single_read` it will return the + register value read at the entry into the context manager + - when used within `single_write` it will return the register value based on the + `initial_state` argument provided with the context manager """ - if self.__in_read_write_context_manager: - # pylint: disable=duplicate-code + # This function by its nature is very similar to the non-async version + # pylint: disable=duplicate-code + if self.__in_write_context_manager: if self.__register_state is None: raise RuntimeError('The internal register state should never be None in the ' 'context manager') @@ -428,10 +602,28 @@ async def read(self) -> int: return await super().read() + async def write_fields(self, **kwargs: Union[int, SystemRDLEnum]) -> None: + """ + Do a read-modify-write to the register, updating any field included in + the arguments + """ + if len(kwargs) == 0: + raise ValueError('no command args') + + async with self.single_read_modify_write() as reg: + for field_name, field_value in kwargs.items(): + if field_name not in reg.systemrdl_python_child_name_map.values(): + raise ValueError(f'{field_name} is not a member of the register') + + field = getattr(reg, field_name) + await field.write(field_value) + async def read_fields(self) -> dict['str', Union[bool, Enum, int]]: """ - asynchronously read the register and return a dictionary of the field values + read the register and return a dictionary of the field values """ + # This function by its nature is very similar to the non-async version + # pylint: disable=duplicate-code return_dict: dict['str', Union[bool, Enum, int]] = {} async with self.single_read() as reg: for field in reg.readable_fields: @@ -439,15 +631,21 @@ async def read_fields(self) -> dict['str', Union[bool, Enum, int]]: return return_dict - async def write_fields(self, **kwargs) -> None: # type: ignore[no-untyped-def] + async def write_all_fields_without_read(self, **kwargs: int) -> None: """ - asynchronously read-modify-write to the register, updating any field included in - the arguments + Do a write to all the fields in a register, updating any field included in + the arguments based on the starting state of `write_initial_state` """ - if len(kwargs) == 0: - raise ValueError('no command args') + # This function by its nature is very similar to the non-async version + # pylint: disable=duplicate-code - async with self.single_read_modify_write() as reg: + # This method should only be called with a complete set of fields specified + called_keys = set(kwargs.keys()) + expected_keys = set(self.systemrdl_python_child_name_map.values()) + if called_keys != expected_keys: + raise RuntimeError(f'{called_keys} mismatches the set of ' + f'expected keys: {expected_keys}') + async with self.single_write(initial_state=self.write_initial_state) as reg: for field_name, field_value in kwargs.items(): if field_name not in reg.systemrdl_python_child_name_map.values(): raise ValueError(f'{field_name} is not a member of the register') @@ -455,6 +653,20 @@ async def write_fields(self, **kwargs) -> None: # type: ignore[no-untyped-def] field = getattr(reg, field_name) await field.write(field_value) + @property + def write_initial_state(self) -> int: + """ + This property defines the initial state of the register when using the + `write_all_fields_without_read` method before any updates are applied. + """ + return self.__write_initial_state + + @write_initial_state.setter + def write_initial_state(self, initial_state: int) -> None: + # this method check the types and range checks the data + self._validate_data(data=initial_state) + self.__write_initial_state = initial_state + @property def _is_readable(self) -> bool: return True @@ -758,6 +970,8 @@ async def single_read(self) -> AsyncGenerator[Self]: skip_initial_read=False) as reg_array: yield reg_array + # These properties are by their nature very similar to the non-async version + # pylint: disable=duplicate-code @property def _is_readable(self) -> bool: return True @@ -765,6 +979,7 @@ def _is_readable(self) -> bool: @property def _is_writeable(self) -> bool: return False + # pylint: enable=duplicate-code class RegAsyncWriteOnlyArray(AsyncRegArray[RegAsyncWriteOnly], ABC): @@ -973,21 +1188,30 @@ def __init__(self, *, field_type=field_type) # pylint: enable=duplicate-code - async def write(self, value: FieldType) -> None: # pylint: disable=invalid-overridden-method + async def write(self, value: FieldType, verify: bool = False, + force_no_read: bool = False) -> None: """ The behaviour of this method depends on whether the field is located in a readable register or not: - If the register is readable, the method will perform an async read-modify-write - on the register updating the field with the value provided + If the register is readable, the method will perform a read-modify-write + on the register updating the field with the value provided, unless force_no_read` is set + to `True` in which case the register `write_initial_state` is used - If the register is not writable all other field values will be asynchronously written - with zero. + If the register is not writable, all other field values will be written + with based on the register `write_initial_state`. Args: value: field value to update to + verify: Read back the register value to confirm the write was successful + force_no_read: prevents a pre-write read in the case of a Read/Write Register + + Raises: + ValueError: if verify is turned on for a Write Only register """ + # This function by its nature is very similar to the non-async version + # pylint: disable=duplicate-code encoded_value = self._encode_write_value(value) if (self.high == (self.register_data_width - 1)) and (self.low == 0): @@ -996,16 +1220,25 @@ async def write(self, value: FieldType) -> None: # pylint: disable=invalid-over new_reg_value = encoded_value else: # do a read, modify write - if isinstance(self.parent_register, RegAsyncReadWrite): + if isinstance(self.parent_register, RegAsyncReadWrite) and not force_no_read: reg_value = await self.parent_register.read() masked_reg_value = reg_value & self.inverse_bitmask new_reg_value = masked_reg_value | encoded_value - elif isinstance(self.parent_register, RegAsyncWriteOnly): - new_reg_value = encoded_value + elif (isinstance(self.parent_register, RegAsyncWriteOnly) or + (isinstance(self.parent_register, RegAsyncReadWrite) and force_no_read)): + reg_value = self.parent_register.write_initial_state + masked_reg_value = reg_value & self.inverse_bitmask + new_reg_value = masked_reg_value | encoded_value else: raise TypeError('Unhandled parent type') - await self.parent_register.write(new_reg_value) + if isinstance(self.parent_register, RegAsyncReadWrite): + await self.parent_register.write(new_reg_value, verify=verify) + elif isinstance(self.parent_register, RegAsyncWriteOnly): + if verify: + raise ValueError('A post-write verify (read) can not be performed on a write-only ' + 'register') + await self.parent_register.write(new_reg_value) @property def parent_register(self) -> WritableAsyncRegister: diff --git a/src/peakrdl_python/lib/base_field.py b/src/peakrdl_python/lib/base_field.py index 3dc76e0f..68f2012c 100644 --- a/src/peakrdl_python/lib/base_field.py +++ b/src/peakrdl_python/lib/base_field.py @@ -162,10 +162,6 @@ def is_volatile(self) -> bool: class Field(Generic[FieldType], Base, ABC): """ base class of register field wrappers - - Note: - It is not expected that this class will be instantiated under normal - circumstances however, it is useful for type checking """ __slots__ = ['__size_props', '__misc_props', @@ -339,7 +335,7 @@ def default(self) -> Optional[FieldType]: - if the field is not reset. - if the register resets to a signal value that can not be determined """ - if issubclass(self._field_type, (SystemRDLEnum)): + if issubclass(self._field_type, SystemRDLEnum): int_default = self.__misc_props.default if int_default is not None: @@ -377,6 +373,32 @@ def __parent_register(self) -> BaseReg: def _field_type(self) -> type[FieldType]: return self.__field_type # type: ignore[return-value] + def _int_value(self, value: FieldType) -> int: + """ + Convert the value of the field to an in calculating register values + """ + if not isinstance(value, self._field_type): + raise TypeError(f'expected {self._field_type}, got {type(value)}') + + if issubclass(self._field_type, SystemRDLEnum): + if not isinstance(value, SystemRDLEnum): + raise TypeError(f'expected SystemRDLEnum (or sub-type), got {type(value)}') + return value.value + if issubclass(self._field_type, int): + if not isinstance(value, int): + raise TypeError(f'expected int, got {type(value)}') + return value + + raise RuntimeError('Unhandled type configuration') + + def _register_bits(self, int_value: int) -> int: + """ + Prepare the field value for being bitwise ORed ino the register + """ + if self.msb0 is True: + return swap_msb_lsb_ordering(value=int_value, width=self.width) << self.low + + return int_value << self.low class _FieldReadOnlyFramework(Field[FieldType], ABC): """ @@ -409,10 +431,11 @@ def _decode_read_value(self, value: int) -> FieldType: if self.msb0 is False: return_int_value = (value & self.bitmask) >> self.low else: - return_int_value = swap_msb_lsb_ordering(value=(value & self.bitmask) >> self.low, - width=self.width) + return_int_value = swap_msb_lsb_ordering( + value=(value & self.bitmask) >> self.low, + width=self.width) - if issubclass(self._field_type, (SystemRDLEnum)): + if issubclass(self._field_type, SystemRDLEnum): return self._field_type(return_int_value) # type: ignore[return-value] if issubclass(self._field_type, int): return return_int_value # type: ignore[return-value] @@ -465,25 +488,12 @@ def _encode_write_value(self, value: FieldType) -> int: value which can be applied to the register to update the field """ - if not isinstance(value, self._field_type): - raise TypeError(f'Field type is not as expected, got {type(value)},' - f' expected {self._field_type}') - - if isinstance(value, (SystemRDLEnum)): - int_value = value.value - elif isinstance(value, int): - int_value = value - else: - raise RuntimeError('Unhandled type configuration') + # no need to do a type check as that is part of `_int_value` + int_value = self._int_value(value=value) self._write_value_checks(value=int_value) - if self.msb0 is False: - return_value = int_value << self.low - else: - return_value = swap_msb_lsb_ordering(value=int_value, width=self.width) << self.low - - return return_value + return self._register_bits(int_value=int_value) class FieldEnum(Field[FieldType], ABC): diff --git a/src/peakrdl_python/lib/base_register.py b/src/peakrdl_python/lib/base_register.py index 23e2e720..59bd82de 100644 --- a/src/peakrdl_python/lib/base_register.py +++ b/src/peakrdl_python/lib/base_register.py @@ -22,12 +22,12 @@ from typing import Union, Optional, TypeVar from abc import ABC, abstractmethod - from .base import Node, NodeArray, IterationClassification from .sections import AddressMap, RegFile from .utility_functions import legal_register_width from .sections import AsyncAddressMap, AsyncRegFile from .memory import BaseMemory +from .field_encoding import SystemRDLEnum class RegisterWriteVerifyError(Exception): @@ -130,6 +130,51 @@ def _is_readable(self) -> bool: def _is_writeable(self) -> bool: ... + @property + @abstractmethod + def bitmask(self) -> int: + """ + The bit mask needed to for all the fields in the register + """ + + @property + def inverse_bitmask(self) -> int: + """ + The bitwise inverse of the bitmask needed to extract the field from its + register + + For example a register field occupying bits 7 to 4 in a 16-bit register + will have an inverse bit mask of 0xFF0F + """ + return self.max_value ^ self.bitmask + + def _check_kwargs_field_names(self, **kwargs: Union[int, SystemRDLEnum]) -> None: + """ + Check the kwargs to make sure: + 1. Every field is specified + 2. There are no extra entries + """ + # the expected names are in reg.systemrdl_python_child_name_map.values() + expected_keys = set(self.systemrdl_python_child_name_map.values()) + kwargs_keys = set(kwargs.keys()) + + if expected_keys != kwargs_keys: + raise KeyError('The provided arguments for the fields in the register ' + 'do not match the expected ones') + + @abstractmethod + def register_value(self, **kwargs: Union[int, SystemRDLEnum]) -> int: + """ + Make the register state from a dictionary of key value pairs for which must represent + every field the register + """ + + @abstractmethod + def inferred_default_register_value(self) -> int: + """ + Make the register state the inferred default value of each field + """ + # pylint: disable-next=invalid-name BaseRegArrayElementType= TypeVar('BaseRegArrayElementType', bound=BaseReg) diff --git a/src/peakrdl_python/lib/register_and_field.py b/src/peakrdl_python/lib/register_and_field.py index 8b2eb7c6..91f68bab 100644 --- a/src/peakrdl_python/lib/register_and_field.py +++ b/src/peakrdl_python/lib/register_and_field.py @@ -22,7 +22,7 @@ from enum import Enum from typing import Union, cast, Optional, TypeVar from collections.abc import Generator, Iterator, Iterable -from abc import ABC, abstractmethod +from abc import ABC from contextlib import contextmanager import sys @@ -33,6 +33,7 @@ from .base_register import BaseReg, BaseRegArray, RegisterWriteVerifyError from .base_field import FieldEnum, FieldSizeProps, FieldMiscProps, \ _FieldReadOnlyFramework, _FieldWriteOnlyFramework, FieldType +from .field_encoding import SystemRDLEnum # pylint: disable=duplicate-code if sys.version_info >= (3, 11): @@ -55,10 +56,6 @@ class Reg(BaseReg, Iterable[Union['FieldReadOnly', 'FieldWriteOnly', 'FieldReadWrite']], ABC): """ base class of non-async register wrappers - - Note: - It is not expected that this class will be instantiated under normal - circumstances however, it is useful for type checking """ __slots__: list[str] = [] @@ -98,6 +95,41 @@ def fields(self) -> Iterator[Union['FieldReadOnly', 'FieldWriteOnly', 'FieldRead """ yield from iter(self) + @property + def bitmask(self) -> int: + bitmask = 0 + for field in iter(self): + bitmask |= field.bitmask + + return bitmask + + def register_value(self, **kwargs: Union[int, SystemRDLEnum]) -> int: + self._check_kwargs_field_names(**kwargs) + register_value = 0 + for field_name, field_value in kwargs.items(): + field = getattr(self, field_name) + register_value &= field.inverse_bitmask + # pylint: disable=protected-access + int_value = field._int_value(field_value) + register_value |= field._register_bits(int_value) + # pylint: enable=protected-access + + return register_value + + def inferred_default_register_value(self) -> int: + + register_value = 0 + for field in self.fields: + default_value = field.default + if default_value is None: + continue + register_value &= field.inverse_bitmask + # pylint: disable=protected-access + int_default_value = field._int_value(default_value) + register_value |= field._register_bits(int_default_value) + # pylint: enable=protected-access + + return register_value # pylint: disable-next=invalid-name RegArrayElementType= TypeVar('RegArrayElementType', bound=BaseReg) @@ -106,10 +138,6 @@ def fields(self) -> Iterator[Union['FieldReadOnly', 'FieldWriteOnly', 'FieldRead class RegArray(BaseRegArray[RegArrayElementType], ABC): """ base class of register array wrappers - - Note: - It is not expected that this class will be instantiated under normal - circumstances however, it is useful for type checking """ # pylint: disable=too-many-arguments,duplicate-code @@ -442,10 +470,10 @@ def _is_writeable(self) -> bool: # pylint: disable=duplicate-code return False - -class RegWriteOnly(Reg, ABC): +#pylint:disable-next=invalid-name +class __RegWritable(Reg, ABC): """ - class for a write only register + base class for a writable register (either RegWriteOnly or RegReadWrite) """ __slots__: list[str] = [] @@ -464,7 +492,8 @@ def __init__(self, *, # pylint: enable=too-many-arguments, duplicate-code def write(self, data: int) -> None: - """Writes a value to the register + """ + Writes a value to the register Args: data: data to be written @@ -505,13 +534,97 @@ def is_writable(field: Union['FieldReadOnly', 'FieldWriteOnly', 'FieldReadWrite' return filter(is_writable, self.fields) - @abstractmethod - def write_fields(self, **kwargs) -> None: # type: ignore[no-untyped-def] + +class RegWriteOnly(__RegWritable, ABC): + """ + class for a write only register + """ + + __slots__: list[str] = ['__in_context_manager', '__register_state', '__write_initial_state'] + + # pylint: disable=too-many-arguments, duplicate-code, useless-parent-delegation + def __init__(self, *, + address: int, + logger_handle: str, + inst_name: str, + parent: Union[AddressMap, RegFile, WritableMemory]): + + super().__init__(address=address, + logger_handle=logger_handle, + inst_name=inst_name, + parent=parent) + + self.__in_context_manager: bool = False + self.__register_state: int = 0 + self.__write_initial_state: int = 0 + + # pylint: enable=too-many-arguments, duplicate-code + + def write(self, data: int) -> None: """ - Do a write to the register, updating any field included in - the arguments + Writes a value to the register + + Args: + data: data to be written + + Raises: + ValueError: if the value provided is outside the range of the + permissible values for the register + TypeError: if the type of data is wrong + RegisterWriteVerifyError: the read back data after the write does not match the + expected value + RuntimeError: if the read verify after write is set when inside a context manager """ + + if self.__in_context_manager: + self.__register_state = data + else: + super().write(data) + + @property + def write_initial_state(self) -> int: + """ + This property defines the initial state of the register when using: + - the `write_fields` method + - the `write` method on any field in the register + + By default this is initialised to `0`, however, advanced usages may include: + - A hidden field within the register which must not be set to `0` + + This property behaves accessed when inside the `single_write` when it returns the shadowed + state of the register which has not yet been writen back (the initial state plus any write + that has occurred). However, setting the property will still update the initial state not + the shadowed state. + """ + if self.__in_context_manager: + return self.__register_state + return self.__write_initial_state + + @write_initial_state.setter + def write_initial_state(self, initial_state: int) -> None: + # this method check the types and range checks the data + self._validate_data(data=initial_state) + self.__write_initial_state = initial_state + + def write_fields(self, **kwargs: Union[int,SystemRDLEnum]) -> None: + """ + Do a single write to the register based on the fields provided, all fields must be + specified to use this method + + Any parts of the register not set with this method will be based on the + `write_initial_state` property + """ + self._check_kwargs_field_names(**kwargs) + + with self.single_write(initial_state=self.write_initial_state) as reg: + for field_name, field_value in kwargs.items(): + if field_name not in reg.systemrdl_python_child_name_map.values(): + raise ValueError(f'{field_name} is not a member of the register') + + field = getattr(reg, field_name) + field.write(field_value) + @property def _is_readable(self) -> bool: # pylint: disable=duplicate-code @@ -522,14 +635,35 @@ def _is_writeable(self) -> bool: # pylint: disable=duplicate-code return True + @contextmanager + def single_write(self, initial_state: int) -> Generator[Self]: + """ + Context manager to allow multiple field write operations to be carried out with a + single write at the end, based on a specified initial state of the register. + """ + # this method check the types and range checks the data + self._validate_data(data=initial_state) + + self.__register_state = initial_state + self.__in_context_manager = True + # this try/finally is needed to make sure that in the event of an exception + # the state flags are not left incorrectly set + try: + yield self + finally: + self.__in_context_manager = False + self.write(self.__register_state) -class RegReadWrite(RegReadOnly, RegWriteOnly, ABC): + +class RegReadWrite(RegReadOnly, __RegWritable, ABC): """ class for a read and write only register """ - __slots__: list[str] = ['__in_read_write_context_manager', '__in_read_context_manager', - '__register_state'] + __slots__: list[str] = ['__in_read_context_manager', + '__in_write_context_manager', + '__register_state', + '__write_initial_state'] # pylint: disable=too-many-arguments, duplicate-code def __init__(self, *, @@ -543,47 +677,38 @@ def __init__(self, *, inst_name=inst_name, parent=parent) - self.__in_read_write_context_manager: bool = False self.__in_read_context_manager: bool = False + self.__in_write_context_manager: bool = False self.__register_state: Optional[int] = None + self.__write_initial_state = 0 # pylint: enable=too-many-arguments, duplicate-code @contextmanager def single_read_modify_write(self, verify: bool = False) -> Generator[Self]: """ - Context manager to allow multiple field reads/write to be done with a single set of - field operations + Context manager to allow multiple field reads/write to be done with a single register read + and write Args: verify (bool): verify the write with a read afterwards - """ - if self.__in_read_context_manager: - raise RuntimeError('using the `single_read_modify_write` context manager within the ' - 'single_read` is not permitted') - - self.__register_state = self.read() - self.__in_read_write_context_manager = True - try: - yield self - finally: - # need to make sure the state flag is cleared even if an exception occurs within - # the context - self.__in_read_write_context_manager = False - self.write(self.__register_state, verify) - - # clear the register states at the end of the context manager - self.__register_state = None + initial_state = self.read() + with self.single_write(initial_state=initial_state, verify=verify) as reg: + yield reg @contextmanager def single_read(self) -> Generator[Self]: """ Context manager to allow multiple field reads with a single register read """ - if self.__in_read_write_context_manager: + if self.__in_read_context_manager: + raise RuntimeError('using the `single_read` context manager double layered is ' + 'not permitted') + + if self.__in_write_context_manager: raise RuntimeError('using the `single_read` context manager within the ' - 'single_read_modify_write` is not permitted') + 'single_read_modify_write` or `single_write` is not permitted') self.__in_read_context_manager = True try: with super().single_read() as reg: @@ -591,6 +716,28 @@ def single_read(self) -> Generator[Self]: finally: self.__in_read_context_manager = False + @contextmanager + def single_write(self, initial_state: int, verify: bool = False) -> Generator[Self]: + """ + Context manager to allow multiple field updates with a single register write when the + context manager exits + """ + if self.__in_write_context_manager or self.__in_read_context_manager: + raise RuntimeError('using the `single_write` context manager within the ' + 'single_read_modify_write` or `single_read` is not permitted. ' + 'Similarly, using the `single_write` context manager within itself ' + 'is not permitted') + self.__in_write_context_manager = True + self.__register_state = initial_state + try: + yield self + finally: + self.__in_write_context_manager = False + self.write(self.__register_state, verify=verify) + + # clear the register states at the end of the context manager + self.__register_state = None + def write(self, data: int, verify: bool = False) -> None: # pylint: disable=arguments-differ """ Writes a value to the register @@ -605,14 +752,19 @@ def write(self, data: int, verify: bool = False) -> None: # pylint: disable=arg TypeError: if the type of data is wrong RegisterWriteVerifyError: the read back data after the write does not match the expected value + RuntimeError: if the read verify after write is set when inside a context manager """ if self.__in_read_context_manager: raise RuntimeError('writes within the single read context manager are not permitted') - if self.__in_read_write_context_manager: + if self.__in_write_context_manager: if self.__register_state is None: raise RuntimeError('The internal register state should never be None in the ' 'context manager') + if verify: + raise RuntimeError('Using the verify option is not permitted inside the ' + '`single_write` or `single_read_modify_write` context manager') + self.__register_state = data else: super().write(data) @@ -625,8 +777,15 @@ def write(self, data: int, verify: bool = False) -> None: # pylint: disable=arg def read(self) -> int: """ Read value from the register + + Note: + The `read` method behaves differently when used within the one of the context managers: + - when used within `single_read_modify_write` or `single_read` it will return the + register value read at the entry into the context manager + - when used within `single_write` it will return the register value based on the + `initial_state` argument provided with the context manager """ - if self.__in_read_write_context_manager: + if self.__in_write_context_manager: if self.__register_state is None: raise RuntimeError('The internal register state should never be None in the ' 'context manager') @@ -637,7 +796,7 @@ def read(self) -> int: return super().read() - def write_fields(self, **kwargs) -> None: # type: ignore[no-untyped-def] + def write_fields(self, **kwargs: Union[int, SystemRDLEnum]) -> None: """ Do a read-modify-write to the register, updating any field included in the arguments @@ -674,6 +833,39 @@ def _is_writeable(self) -> bool: # pylint: disable=duplicate-code return True + def write_all_fields_without_read(self, **kwargs: int) -> None: + """ + Do a write to all the fields in a register, updating any field included in + the arguments based on the starting state of `write_initial_state` + """ + # This method should only be called with a complete set of fields specified + called_keys = set(kwargs.keys()) + expected_keys = set(self.systemrdl_python_child_name_map.values()) + if called_keys != expected_keys: + raise RuntimeError(f'{called_keys} mismatches the set of ' + f'expected keys: {expected_keys}') + with self.single_write(initial_state=self.write_initial_state) as reg: + for field_name, field_value in kwargs.items(): + if field_name not in reg.systemrdl_python_child_name_map.values(): + raise ValueError(f'{field_name} is not a member of the register') + + field = getattr(reg, field_name) + field.write(field_value) + + @property + def write_initial_state(self) -> int: + """ + This property defines the initial state of the register when using the + `write_all_fields_without_read` method before any updates are applied. + """ + return self.__write_initial_state + + @write_initial_state.setter + def write_initial_state(self, initial_state: int) -> None: + # this method check the types and range checks the data + self._validate_data(data=initial_state) + self.__write_initial_state = initial_state + ReadableRegister = Union[RegReadOnly, RegReadWrite] WritableRegister = Union[RegWriteOnly, RegReadWrite] @@ -929,19 +1121,25 @@ def __init__(self, *, field_type=field_type) # pylint: enable=duplicate-code - def write(self, value: FieldType) -> None: + def write(self, value: FieldType, verify: bool = False, force_no_read: bool = False) -> None: """ The behaviour of this method depends on whether the field is located in a readable register or not: If the register is readable, the method will perform a read-modify-write - on the register updating the field with the value provided + on the register updating the field with the value provided, unless force_no_read` is set + to `True` in which case the register `write_initial_state` is used - If the register is not writable all other field values will be written - with zero. + If the register is not writable, all other field values will be written + with based on the register `write_initial_state`. Args: value: field value to update to + verify: Read back the register value to confirm the write was successful + force_no_read: prevents a pre-write read in the case of a Read/Write Register + + Raises: + ValueError: if verify is turned on for a Write Only register """ encoded_value = self._encode_write_value(value) @@ -952,16 +1150,25 @@ def write(self, value: FieldType) -> None: new_reg_value = encoded_value else: # do a read, modify write - if isinstance(self.parent_register, RegReadWrite): + if isinstance(self.parent_register, RegReadWrite) and not force_no_read: reg_value = self.parent_register.read() masked_reg_value = reg_value & self.inverse_bitmask new_reg_value = masked_reg_value | encoded_value - elif isinstance(self.parent_register, RegWriteOnly): - new_reg_value = encoded_value + elif (isinstance(self.parent_register, RegWriteOnly) or + (isinstance(self.parent_register, RegReadWrite) and force_no_read)): + reg_value = self.parent_register.write_initial_state + masked_reg_value = reg_value & self.inverse_bitmask + new_reg_value = masked_reg_value | encoded_value else: raise TypeError('Unhandled parent type') - self.parent_register.write(new_reg_value) + if isinstance(self.parent_register, RegReadWrite): + self.parent_register.write(new_reg_value, verify=verify) + elif isinstance(self.parent_register, RegWriteOnly): + if verify: + raise ValueError('A post-write verify (read) can not be performed on a write-only ' + 'register') + self.parent_register.write(new_reg_value) @property def parent_register(self) -> WritableRegister: diff --git a/src/peakrdl_python/lib_test/_common_base_test_class.py b/src/peakrdl_python/lib_test/_common_base_test_class.py index fe49e9af..fab24c75 100644 --- a/src/peakrdl_python/lib_test/_common_base_test_class.py +++ b/src/peakrdl_python/lib_test/_common_base_test_class.py @@ -197,6 +197,8 @@ def _single_register_property_test(self, *, parent_full_inst_name=parent_full_inst_name, inst_name=inst_name) + self.assertEqual(rut.inverse_bitmask, rut.bitmask ^ rut.max_value) + self.__bad_attribute_test(dut=rut) # pylint:disable-next=too-many-arguments diff --git a/src/peakrdl_python/lib_test/reg_test_class.py b/src/peakrdl_python/lib_test/reg_test_class.py index fecd3961..3c2aaf92 100644 --- a/src/peakrdl_python/lib_test/reg_test_class.py +++ b/src/peakrdl_python/lib_test/reg_test_class.py @@ -86,7 +86,9 @@ def _single_register_read_and_write_test(self, *, else: if not isinstance(rut, RegWriteOnly): raise TypeError('Test can not proceed as the rut is not a writable register') - self.__single_reg_full_write_fields_test(rut) + self.__single_write_only_reg_full_write_fields_test(rut) + self.__single_reg_write_all_fields_test(rut) + self.__single_reg_single_write_context_test(rut) else: # test that a non-writable register has no write method and @@ -238,7 +240,13 @@ def __single_reg_write_fields_and_context_test(self, rut: RegReadWrite) -> None: write_callback_mock.reset_mock() read_callback_mock.reset_mock() - def __single_reg_full_write_fields_test(self, rut: RegWriteOnly) -> None: + def __single_reg_write_all_fields_test(self, rut: RegReadWrite) -> None: + raise NotImplementedError('To be done later') + + def __single_reg_single_write_context_test(self, rut: RegReadWrite) -> None: + raise NotImplementedError('To be done later') + + def __single_write_only_reg_full_write_fields_test(self, rut: RegWriteOnly) -> None: """ Test the `write_fields` method of a Write Only Register """ diff --git a/src/peakrdl_python/lib_test/utilities.py b/src/peakrdl_python/lib_test/utilities.py index f23a83f4..5acb94f1 100644 --- a/src/peakrdl_python/lib_test/utilities.py +++ b/src/peakrdl_python/lib_test/utilities.py @@ -20,7 +20,7 @@ """ import random from typing import Union, TypeVar, TYPE_CHECKING -from enum import Enum, EnumMeta +from enum import EnumMeta from collections.abc import Iterable from dataclasses import dataclass from dataclasses import field as dataclass_field @@ -32,6 +32,7 @@ from ..lib.utility_functions import calculate_bitmask from ..lib import FieldEnum from ..lib.memory import BaseMemory +from ..lib import SystemRDLEnum def reverse_bits(value: int, number_bits: int) -> int: """ @@ -107,11 +108,11 @@ def random_int_field_value(fut: Field) -> int: return random.randint(0, fut.max_value) # The following line should be: -# FieldType = TypeVar('FieldType', bound=int|IntEnum|SystemRDLEnum) +# FieldType = TypeVar('FieldType', bound=int|SystemRDLEnum) # However, python 3.9 does not support the combination so the binding was removed # pylint: disable-next=invalid-name FieldType = TypeVar('FieldType') -def random_encoded_field_value(fut: FieldEnum[FieldType]) -> FieldType: +def random_encoded_field_value(fut: FieldEnum[SystemRDLEnum]) -> SystemRDLEnum: """ Return a random encoded values within the legal range for a field """ @@ -150,14 +151,14 @@ def __post_init__(self) -> None: self.value = end_value def _random_legal_values(self, initial_value:int ,field_iter: Iterable[Field]) -> tuple[ - int, dict[str, Union[int, Enum]]]: + int, dict[str, Union[int, SystemRDLEnum]]]: """ Returns a random register value, based on legal values for the fields within the register """ # build up a register value, starting with a random register value reg_value = initial_value - reg_field_content: dict[str, Union[int, Enum]] = {} + reg_field_content: dict[str, Union[int, SystemRDLEnum]] = {} for field in field_iter: if isinstance(field, FieldEnum): field_value_enum = random_encoded_field_value(field) @@ -186,7 +187,7 @@ class RegWriteTestSequence(RandomReg): value: int = dataclass_field(init=False) fields: Iterable[Field] start_value: int = dataclass_field(init=False) - write_sequence: dict[str, Union[int, Enum]] = dataclass_field(init=False) + write_sequence: dict[str, Union[int, SystemRDLEnum]] = dataclass_field(init=False) def __post_init__(self) -> None: super().__post_init__() @@ -201,7 +202,8 @@ class RegWriteZeroStartTestSequence(RandomReg): starting with a random register value. """ fields: Iterable[Field] - write_sequence: dict[str, Union[int, Enum]] = dataclass_field(init=False) + + write_sequence: dict[str, Union[int, SystemRDLEnum]] = dataclass_field(init=False) def __post_init__(self) -> None: self.value, self.write_sequence = self._random_legal_values(initial_value=0, diff --git a/src/peakrdl_python/templates/addrmap_register.py.jinja b/src/peakrdl_python/templates/addrmap_register.py.jinja index ba0c0eff..2b333693 100644 --- a/src/peakrdl_python/templates/addrmap_register.py.jinja +++ b/src/peakrdl_python/templates/addrmap_register.py.jinja @@ -82,7 +82,7 @@ class {{node.python_class_name}}({{node.base_class(asyncoutput)}}): {{get_table_block(node.instance) | indent}} """ - __slots__ : list[str] = [{%- for child_node in node.instance.children(unroll=False) -%}'__{{child_node.inst_name}}'{% if not loop.last %}, {% endif %}{%- endfor %}] + __slots__ : list[str] = [{%- for child_node in node.children(unroll=False) -%}'__{{child_node.inst_name}}'{% if not loop.last %}, {% endif %}{%- endfor %}] def __init__(self, address: int, @@ -127,20 +127,33 @@ class {{node.python_class_name}}({{node.base_class(asyncoutput)}}): def accesswidth(self) -> int: return {{node.accesswidth}} + {% if node.all_fields_hidden %} + {# Special case for the register_value field when all the fields are hidden #} + def register_value(self) -> int: # type: ignore[override] + return 0 + {% else %} + {# Special case for the register_value field when all the fields are hidden #} + def register_value(self, *,{%- for child_node in node.fields() -%} {{safe_node_name(child_node)}} : {{node.lookup_field_data_python_class(child_node)}},{%- endfor -%}) -> int: # type: ignore[override] + return super().register_value({%- for child_node in node.fields() -%} {{safe_node_name(child_node)}}={{safe_node_name(child_node)}},{%- endfor -%}) + {% endif %} + {% if node.write_only %} {# if the register has no readable components, all the fields must be writen as one #} - {% if asyncoutput %}async {% endif %}def write_fields(self, {%- for child_node in node.instance.fields() -%} {{safe_node_name(child_node)}} : {{node.lookup_field_data_python_class(child_node)}}{%- if not loop.last -%},{%- endif -%}{%- endfor -%}) -> None: # type: ignore[override] - """ - Do a write to the register, updating all fields - """ - reg_value = 0 - {%- for child_node in node.instance.fields() %} - reg_value &= self.{{safe_node_name(child_node)}}.inverse_bitmask - reg_value |= self.{{safe_node_name(child_node)}}._encode_write_value({{safe_node_name(child_node)}}) - {% endfor %} - - {% if asyncoutput %}await {% endif %}self.write(reg_value) + {% if asyncoutput %}async {% endif %}def write_fields(self, *,{%- for child_node in node.fields() -%} {{safe_node_name(child_node)}} : {{node.lookup_field_data_python_class(child_node)}},{%- endfor -%}) -> None: # type: ignore[override] + return super().write_fields({%- for child_node in node.fields() -%} {{safe_node_name(child_node)}}={{safe_node_name(child_node)}},{%- endfor -%}) + {% endif %} + {% if node.read_write %} + {% if node.all_writable_fields_hidden %} + {# Special case for the all_writable_fields_hidden field when all the fields are hidden #} + {% if asyncoutput %}async {% endif %}def write_all_fields_without_read(self, ) -> None: + raise NotImplementedError('There are no writeable fields') + return None + {% else %} {# if node.all_writable_fields_hidden #} + {# for read/write registers a method that forces all field to be written as one #} + {% if asyncoutput %}async {% endif %}def write_all_fields_without_read(self, *, {%- for child_node in node.writable_fields() -%} {{safe_node_name(child_node)}} : {{node.lookup_field_data_python_class(child_node)}},{%- endfor -%}) -> None: + return super().write_all_fields_without_read({%- for child_node in node.writable_fields() -%} {{safe_node_name(child_node)}}={{safe_node_name(child_node)}},{%- endfor -%}) + {% endif %} {# if node.all_writable_fields_hidden #} {% endif %} # build the properties for the fields diff --git a/src/peakrdl_python/unique_component_iterator.py b/src/peakrdl_python/unique_component_iterator.py index 690b25a0..a79969b5 100644 --- a/src/peakrdl_python/unique_component_iterator.py +++ b/src/peakrdl_python/unique_component_iterator.py @@ -41,6 +41,7 @@ from .systemrdl_node_utility_functions import get_properties_to_include from .systemrdl_node_utility_functions import get_reg_regwidth, get_reg_accesswidth from .systemrdl_node_utility_functions import get_memory_accesswidth +from .systemrdl_node_utility_functions import get_reg_writable_fields from .class_names import get_base_class_name @dataclass(frozen=True) @@ -153,6 +154,10 @@ def __post_init__(self) -> None: def read_write(self) -> bool: """ Determine if the register is read-write + + Note: + The fields is defined as read write even if the underlying fields that are readable + or writable are hidden. """ return self.instance.has_sw_readable and self.instance.has_sw_writable @@ -206,11 +211,38 @@ def lookup_field_data_python_class(self, field_node: FieldNode) -> str: def fields(self) -> Iterator[FieldNode]: """ - Iterator for all the systemRDL nodes which are not hidden + Iterator for all the systemRDL fields associated with the register which are not hidden """ yield from filterfalse(self.parent_walker.hide_node_callback, self.instance.fields()) + + def writable_fields(self) -> Iterator[FieldNode]: + """ + Iterator for all the systemRDL writtable fields associated with the register which are not + hidden i.e. read only fields are filtered out + """ + yield from get_reg_writable_fields( + node=self.instance, + hide_node_callback=self.parent_walker.hide_node_callback) + + @property + def all_fields_hidden(self) -> bool: + """ + It is mandatory to have at least one field in a systemRDL register, however, PeakRDL allows + that to be hidden creating a special case of a register with no accessible fields + """ + return len(tuple(self.fields())) == 0 + + @property + def all_writable_fields_hidden(self) -> bool: + """ + It is mandatory to have at least one field in a systemRDL register, however, PeakRDL allows + that to be hidden creating a special case of a register with no accessible fields + """ + return len(tuple(self.writable_fields())) == 0 + + @dataclass(frozen=True) class PeakRDLPythonUniqueMemoryComponents(PeakRDLPythonUniqueComponents): """ diff --git a/tests/testcases/basic.rdl b/tests/testcases/basic.rdl index d23016e5..fb3ed815 100644 --- a/tests/testcases/basic.rdl +++ b/tests/testcases/basic.rdl @@ -32,10 +32,10 @@ addrmap basic { reg { default sw = rw; default hw = r; - field { fieldwidth=8; } basicfield_h; - field { fieldwidth=8; } basicfield_i; - field { fieldwidth=8; } basicfield_j; - field { fieldwidth=8; } basicfield_k; + field { fieldwidth=8; } basicfield_h = 1; + field { fieldwidth=8; } basicfield_i = 2; + field { fieldwidth=8; } basicfield_j = 4; + field { fieldwidth=8; } basicfield_k = 8; } basicreg_e; reg { diff --git a/tests/testcases/name_clash.rdl b/tests/testcases/name_clash.rdl index 163154d5..2f6dc981 100644 --- a/tests/testcases/name_clash.rdl +++ b/tests/testcases/name_clash.rdl @@ -53,7 +53,8 @@ addrmap name_clash { reg { field { fieldwidth=1; } msb = 1; - field { fieldwidth=1; python_inst_name="pass_field"; } pass = 1; + field { fieldwidth=1; python_inst_name="pass_field_renamed"; } pass = 1; + field { fieldwidth=1; } raise = 1; } msb; addrmap { diff --git a/tests/unit_tests/simple_components.py b/tests/unit_tests/simple_components.py index ed58cd9e..24f99921 100644 --- a/tests/unit_tests/simple_components.py +++ b/tests/unit_tests/simple_components.py @@ -76,13 +76,6 @@ def width(self) -> int: def accesswidth(self) -> int: return 32 - @property - def readable_fields(self) -> Iterator[FieldReadOnly]: - """ - generator that produces has all the readable fields within the register - """ - yield self.field - def __iter__(self) -> Iterator[FieldReadOnly]: """ generator that produces has all the readable fields within the register @@ -158,13 +151,6 @@ def width(self) -> int: def accesswidth(self) -> int: return 32 - @property - def writable_fields(self) -> Iterator[FieldWriteOnly]: - """ - generator that produces has all the readable fields within the register - """ - yield self.field - def __iter__(self) -> Iterator[FieldWriteOnly]: """ generator that produces has all the readable fields within the register @@ -200,7 +186,7 @@ class ReadWriteRegisterToTest(RegReadWrite): """ Class to represent a register in the register model """ - __slots__: list[str] = ['__field'] + __slots__: list[str] = ['__field', '__another_field'] # pylint: disable=duplicate-code,too-many-arguments class FieldToTest(FieldReadWrite): @@ -235,6 +221,21 @@ def __init__(self, *, inst_name='field', field_type=int) + self.__another_field = self.FieldToTest( + parent_register=self, + size_props=FieldSizeProps( + width=2, + lsb=4, + msb=5, + low=4, + high=5), + misc_props=FieldMiscProps( + default=None, + is_volatile=False), + logger_handle=logger_handle + '.another_field', + inst_name='another_field', + field_type=int) + @property def width(self) -> int: return 32 @@ -243,25 +244,12 @@ def width(self) -> int: def accesswidth(self) -> int: return 32 - @property - def readable_fields(self) -> Iterator[FieldReadOnly]: - """ - generator that produces has all the readable fields within the register - """ - yield self.field - def __iter__(self) -> Iterator[FieldReadOnly]: """ generator that produces has all the readable fields within the register """ yield self.field - - @property - def writable_fields(self) -> Iterator[Union['FieldWriteOnly', 'FieldReadWrite']]: - """ - generator that produces has all the readable fields within the register - """ - yield self.field + yield self.another_field # build the properties for the fields @property @@ -271,8 +259,15 @@ def field(self) -> FieldToTest: """ return self.__field + @property + def another_field(self) -> FieldToTest: + """ + Property to access another_field of the register + """ + return self.__another_field + def write_fields(self, **kwargs: Any) -> None: - raise NotImplementedError('Not implemented for the purpose of tests') + return super().write_fields(**kwargs) @property def systemrdl_python_child_name_map(self) -> dict[str, str]: @@ -285,6 +280,7 @@ def systemrdl_python_child_name_map(self) -> dict[str, str]: """ return { 'field': 'field', + 'another_field': 'another_field' } diff --git a/tests/unit_tests/test_reg.py b/tests/unit_tests/test_reg.py index 7c5920be..5aae09e4 100644 --- a/tests/unit_tests/test_reg.py +++ b/tests/unit_tests/test_reg.py @@ -226,6 +226,10 @@ def test_register_write(self) -> None: accesswidth=self.dut.accesswidth, data=10) read_patch.assert_not_called() + def test_individual_write(self) -> None: + """ + Test write to a field + """ with patch.object(self.callbacks, 'read_callback', side_effect=self.read_addr_space) as read_patch, \ patch.object(self.callbacks, 'write_callback', @@ -236,6 +240,61 @@ def test_register_write(self) -> None: accesswidth=self.dut.accesswidth, data=1) read_patch.assert_not_called() + self.dut.write_initial_state = 0xAAAA_AAAA + self.assertEqual(self.dut.write_initial_state, 0xAAAA_AAAA) + + with patch.object(self.callbacks, 'read_callback', + side_effect=self.read_addr_space) as read_patch, \ + patch.object(self.callbacks, 'write_callback', + side_effect=self.write_addr_space) as write_patch: + self.dut.field.write(True) + write_patch.assert_called_once_with(addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth, data=0xAAAA_AAAA | 1) + read_patch.assert_not_called() + + def test_context_manager_write(self) -> None: + """ + Check write context manager + """ + self.dut.write_initial_state = 0xAAAA_AAAA + self.assertEqual(self.dut.write_initial_state, 0xAAAA_AAAA) + + with patch.object(self.callbacks, 'read_callback', + side_effect=self.read_addr_space) as read_patch, \ + patch.object(self.callbacks, 'write_callback', + side_effect=self.write_addr_space) as write_patch: + with self.dut.single_write(initial_state=0) as reg: + # the field write_initial_state takes on a different function inside the context manager + self.assertEqual(self.dut.write_initial_state, 0x0) + reg.field.write(True) + reg.field.write(True) + # the field write_initial_state takes on a different function inside the context manager + self.assertEqual(self.dut.write_initial_state, 0x1) + + read_patch.assert_not_called() + write_patch.assert_called_once_with(addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth, data=1) + + self.assertEqual(self.dut.write_initial_state, 0xAAAA_AAAA) + + with patch.object(self.callbacks, 'read_callback', + side_effect=self.read_addr_space) as read_patch, \ + patch.object(self.callbacks, 'write_callback', + side_effect=self.write_addr_space) as write_patch: + with self.dut.single_write(initial_state=self.dut.write_initial_state) as reg: + # the field write_initial_state takes on a different function inside the context manager + self.assertEqual(self.dut.write_initial_state, 0xAAAA_AAAA) + reg.field.write(True) + reg.field.write(True) + # the field write_initial_state takes on a different function inside the context manager + self.assertEqual(self.dut.write_initial_state, 0x1 | 0xAAAA_AAAA) + + read_patch.assert_not_called() + write_patch.assert_called_once_with(addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth, data=0x1 | 0xAAAA_AAAA) class TestReadWrite(RegTestBase): """ @@ -278,6 +337,11 @@ def test_register_write(self) -> None: accesswidth=self.dut.accesswidth, data=10) read_patch.assert_not_called() + def test_field_write(self) -> None: + """ + Test field write + """ + with patch.object(self.callbacks, 'read_callback', side_effect=self.read_addr_space) as read_patch, \ patch.object(self.callbacks, 'write_callback', @@ -304,6 +368,11 @@ def test_register_read(self) -> None: accesswidth=self.dut.accesswidth) write_patch.assert_not_called() + def test_field_read(self) -> None: + """ + Test field write + """ + with patch.object(self.callbacks, 'read_callback', side_effect=self.read_addr_space) as read_patch, \ patch.object(self.callbacks, 'write_callback', @@ -338,58 +407,6 @@ def test_context_manager_read_modify_write_check_writeback(self) -> None: width=self.dut.width, accesswidth=self.dut.accesswidth, data=1) - # check the `skip_write` works as expected, this will however raise an deprecation warning - # and the feature will be removed at some point in the future - with patch.object(self.callbacks, 'read_callback', - side_effect=self.read_addr_space) as read_patch, \ - patch.object(self.callbacks, 'write_callback', - side_effect=self.write_addr_space) as write_patch: - - with self.dut.single_read() as reg: - _ = reg.field.read() - _ = reg.field.read() - - read_patch.assert_called_once_with(addr=0, - width=self.dut.width, - accesswidth=self.dut.accesswidth) - write_patch.assert_not_called() - - # check that a write within the `skip_write` works still does not result in a write - with patch.object(self.callbacks, 'read_callback', - side_effect=self.read_addr_space) as read_patch, \ - patch.object(self.callbacks, 'write_callback', - side_effect=self.write_addr_space) as write_patch: - - with self.dut.single_read() as reg: - self.assertEqual(reg.field.read(), False) - with self.assertRaises(RuntimeError): - reg.field.write(True) - - read_patch.assert_called_once_with(addr=0, - width=self.dut.width, - accesswidth=self.dut.accesswidth) - write_patch.assert_not_called() - - # attempting to use the `single_read` inside the `single_read_modify_write` context - # should cause an exception - with patch.object(self.callbacks, 'read_callback', - side_effect=self.read_addr_space) as read_patch, \ - patch.object(self.callbacks, 'write_callback', - side_effect=self.write_addr_space) as write_patch: - - with self.dut.single_read_modify_write() as reg: - _ = reg.field.read() - with self.assertRaises(RuntimeError): - with reg.single_read() as alt_reg: - _ = alt_reg.field.read() - - read_patch.assert_called_once_with(addr=0, - width=self.dut.width, - accesswidth=self.dut.accesswidth) - write_patch.assert_called_once_with(addr=0, - width=self.dut.width, - accesswidth=self.dut.accesswidth, data=0) - # check the context manager cleans itself up properly even if an exception occurs within # the context with patch.object(self.callbacks, 'read_callback', @@ -424,13 +441,31 @@ def test_read_fields(self) -> None: result = self.dut.read_fields() - self.assertDictEqual(result, {'field': False}) + self.assertDictEqual(result, {'field': False, 'another_field': False}) read_patch.assert_called_once_with(addr=0, width=self.dut.width, accesswidth=self.dut.accesswidth) write_patch.assert_not_called() + def test_write_fields(self) -> None: + """ + Check the read fields methods reads the fields + """ + with patch.object(self.callbacks, 'read_callback', + side_effect=self.read_addr_space) as read_patch, \ + patch.object(self.callbacks, 'write_callback', + side_effect=self.write_addr_space) as write_patch: + + self.dut.write_fields(field=True) + + read_patch.assert_called_once_with(addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth) + write_patch.assert_called_once_with(addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth, data=1) + def test_context_manager_read(self) -> None: """ Check the write back has occurred, this happens by default even if nothing has changed in @@ -485,6 +520,24 @@ def test_context_manager_read(self) -> None: accesswidth=self.dut.accesswidth) write_patch.assert_not_called() + # attempting to use the `single_write` inside the `single_read` context + # should cause an exception + with patch.object(self.callbacks, 'read_callback', + side_effect=self.read_addr_space) as read_patch, \ + patch.object(self.callbacks, 'write_callback', + side_effect=self.write_addr_space) as write_patch: + + with self.dut.single_read() as reg: + _ = reg.field.read() + with self.assertRaises(RuntimeError): + with reg.single_write(initial_state=0) as alt_reg: + _ = alt_reg.field.read() + + read_patch.assert_called_once_with(addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth) + write_patch.assert_not_called() + # an exception within the `single_read` context must not leave the register in a bad # state with patch.object(self.callbacks, 'read_callback', @@ -516,6 +569,123 @@ def test_context_manager_read(self) -> None: width=self.dut.width, accesswidth=self.dut.accesswidth, data=1) + def test_context_manager_write(self) -> None: + """ + Check the write back has occurred, this happens by default even if nothing has changed in + the register + """ + with patch.object(self.callbacks, 'read_callback', + side_effect=self.read_addr_space) as read_patch, \ + patch.object(self.callbacks, 'write_callback', + side_effect=self.write_addr_space) as write_patch: + with self.dut.single_write(initial_state=0) as reg: + assert reg.field.read() == 0x0 + assert reg.read() == 0x0 + reg.field.write(True) + reg.field.write(True) + assert reg.field.read() == 0x01 + assert reg.read() == 0x1 + + read_patch.assert_not_called() + write_patch.assert_called_once_with(addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth, data=1) + + with patch.object(self.callbacks, 'read_callback', + side_effect=self.read_addr_space) as read_patch, \ + patch.object(self.callbacks, 'write_callback', + side_effect=self.write_addr_space) as write_patch: + with self.dut.single_write(initial_state=0xAAAA_AAAA) as reg: + # the field write_initial_state takes on a different function inside the context manager + assert reg.read() == 0xAAAA_AAAA + assert reg.field.read() == 0x0 + reg.field.write(True) + assert reg.read() == 0x1 | 0xAAAA_AAAA + assert reg.field.read() == 0x01 + reg.field.write(True) + + read_patch.assert_not_called() + write_patch.assert_called_once_with(addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth, + data=0x1 | 0xAAAA_AAAA) + + # attempting to use the `single_read` inside the `single_write` context + # should cause an exception. However the write back should + with patch.object(self.callbacks, 'read_callback', + side_effect=self.read_addr_space) as read_patch, \ + patch.object(self.callbacks, 'write_callback', + side_effect=self.write_addr_space) as write_patch: + + with self.dut.single_write(initial_state=0xCAFEF00D) as reg: + _ = reg.field.read() + with self.assertRaises(RuntimeError): + with reg.single_read() as alt_reg: + _ = alt_reg.field.read() + + read_patch.assert_not_called() + write_patch.assert_called_once_with(addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth, + data=0xCAFEF00D) + + # attempting to use the `single_read_modify_write` inside the `single_write` context + # should cause an exception. However, the writeback at the end of the context manager + # should still occur + with patch.object(self.callbacks, 'read_callback', + side_effect=self.read_addr_space) as read_patch, \ + patch.object(self.callbacks, 'write_callback', + side_effect=self.write_addr_space) as write_patch: + with self.dut.single_write(initial_state=0xF00DF00D) as reg: + _ = reg.field.read() + with self.assertRaises(RuntimeError): + with reg.single_read_modify_write() as alt_reg: + _ = alt_reg.field.read() + + read_patch.assert_not_called() + write_patch.assert_called_once_with(addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth, + data=0xF00DF00D) + + def test_write_all_fields_without_read(self) -> None: + """ + Check the read fields methods reads the fields + """ + self.dut.write_initial_state = 0x0 + self.assertEqual(self.dut.write_initial_state, 0x0) + + with patch.object(self.callbacks, 'read_callback', + side_effect=self.read_addr_space) as read_patch, \ + patch.object(self.callbacks, 'write_callback', + side_effect=self.write_addr_space) as write_patch: + # check that calling with an incomplete set of arguments causes an error + with self.assertRaises(RuntimeError): + self.dut.write_all_fields_without_read(field=True) + + self.dut.write_all_fields_without_read(field=True, another_field=False) + + read_patch.assert_not_called() + write_patch.assert_called_once_with(addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth, data=1) + + self.assertEqual(self.dut.write_initial_state, 0x0) + self.dut.write_initial_state = 0xAAAA_AAAA + self.assertEqual(self.dut.write_initial_state, 0xAAAA_AAAA) + + with patch.object(self.callbacks, 'read_callback', + side_effect=self.read_addr_space) as read_patch, \ + patch.object(self.callbacks, 'write_callback', + side_effect=self.write_addr_space) as write_patch: + self.dut.write_all_fields_without_read(field=True, another_field=False) + + read_patch.assert_not_called() + write_patch.assert_called_once_with( + addr=0, + width=self.dut.width, + accesswidth=self.dut.accesswidth, + data=0x1 | (0xAAAA_AAAA & self.dut.another_field.inverse_bitmask)) class TestRegWidthUtility(unittest.TestCase): """