Source code for philander.simdev

"""Serial device simulation module to support debugging and cross-platform development.

This module provides a fake serial device implementation to virtualize serial
communication. 
"""
__author__ = "Oliver Maye"
__version__ = "0.1"
__all__ = ["SimDev", "SimDevNull", "SimDevMemory", "MemoryType", "Register"]

from dataclasses import dataclass
from enum import Enum, unique, auto

from .module import Module
from .systypes import ErrorCode


[docs]class SimDev(): """Abstract base class to define the functionality of a simulated serial device. A sub class must overwrite at least the methods for reading and writing a single byte. Implementation should use as least as possible dependencies to other modules. Use of hardware-dependent drivers must be completely avoided! Objects of this type (and its derivatives) shall be stored as the ``sim`` attribute of a matching :class:`serialbus.SerialDevice` object. Remember that each :class:`serialbus.SerialDevice` must be registered with a :class:`serialbus.SerialBus` by calling its :meth:`serialbus.SerialBus.attach` method. The simulative serial bus implementation uses the :attr:`serialbus.SerialDevice.address` attribute to identify the addressed device and then looks up its ``SerialBusDevice.sim`` attribute to retrieve the serial simulation for that device. For that reason, implementations do not have to care about the address of the device - it's always the right one. """
[docs] def readByteRegister( self, aReg ): """Read a single byte from a certain register.\ A sub-class must overwrite this method. The method is expected to deliver a register's content to the caller. :param int aReg: The address of the register to be read. :return: A one-byte integer representing the response of the device\ and an error code indicating success or the reason of failure. :rtype: int, ErrorCode """ pass
[docs] def writeByteRegister( self, aReg, data ): """Write a single byte value into a certain register.\ A sub-class must overwrite this method. The method is expected to store the given value to a register. :param int aReg: The address of the register to receive the new value. :param int data: The new value to store to that register. :return: An error code indicating success or the reason of failure. :rtype: ErrorCode """ pass
[docs] def readWordRegister( self, aReg ): """Read a word from a certain register. The word is formed in little-endian order from the content of the given register (low) and the content of the immediate successor ``aReg+1`` of that register (high). :param int aReg: The address of the low-byte register to be read. :return: A 16-bit integer representing the response of the device\ and an error code indicating success or the reason of failure. :rtype: int, ErrorCode """ lo, _ = self.readByteRegister(aReg) hi, err = self.readByteRegister(aReg+1) data = ((hi << 8) | lo) return data, err
[docs] def writeWordRegister( self, aReg, data16 ): """Write a double-byte (word) value into a certain register. The method is expected to store the given value to a register or pair of registers in little-endian order. The low-part of the data16 item is stored at the given register, while the high-part is put at ``aReg+1``. :param int aReg: The address of the register to receive the\ low-part of the new value. :param int data16: The new value to store to that (pair of) registers. :return: An error code indicating success or the reason of failure. :rtype: ErrorCode """ bVal = data16 & 0xFF self.writeByteRegister(aReg, bVal) bVal = (data16 >> 8) & 0xFF err = self.writeByteRegister(aReg+1, bVal) return err
[docs] def readDWordRegister( self, aReg ): """Read a double word from a certain register. The dword is formed in little-endian order from the content of the four registers, starting with the given address ``aReg`` (low-byte of the low-word) and its successors ``aReg+1`` (high-byte of the low-word), ``aReg+2`` (low-byte of the high-word) and ``aReg+3`` (high-byte of the high-word). :param int aReg: The address of the first (lowest-byte) register to be read. :return: A 32-bit integer representing the response of the device\ and an error code indicating success or the reason of failure. :rtype: int, ErrorCode """ L, _ = self.readWordRegister( aReg ) H, err = self.readWordRegister( aReg+2 ) data = (H << 16) + L return data, err
[docs] def writeDWordRegister( self, aReg, data32 ): """Write a double-word (four bytes) value into a certain register. The method is expected to store the given value to a quadruple of registers in little-endian order. The low-byte of the low word is stored at the given register ``aReg``. The high-byte of the low-word goes to ``aReg+1``. The low-part of the high-word is stored to ``aReg+2`` and the high-part of the high-word is put at ``aReg+3``. :param int aReg: The address of the first (lowest byte) register\ to receive part of the new value. :param int data32: The new value to store to that quadruple of registers. :return: An error code indicating success or the reason of failure. :rtype: ErrorCode """ L = data32 & 0xFFFF H = (data32 & 0xFFFF0000) >> 16 self.writeWordRegister( aReg, L ) err = self.writeWordRegister( aReg+2, H ) return err
[docs] def readBufferRegister( self, aReg, length ): """Read a block of data starting from the given register. Starting with the given Register address, ``length`` bytes are read and returned. As with :meth:`readWordRegister` and :meth:`readDWordRegister`, this implementation assumes an auto-increment behavior of the target register. So, the returned data buffer is read as follows: data[0] -> aReg data[1] -> aReg + 1 ... If this doesn't match the actual chip behavior, a sub-class should overwrite this method. :param int aReg: The address of the first register to be read. :param int length: The number of bytes to read. :return: A buffer of the indicated length holding the response\ and an error code indicating success or the reason of failure. :rtype: list(int), ErrorCode """ data = [0] * length err = ErrorCode.errOk for idx in range(length): data[idx], err = self.readByteRegister(aReg+idx) return data, err
[docs] def writeBufferRegister( self, aReg, data ): """Write a block of byte data into registers. As with :meth:`readBufferRegister` an auto-increment applies for the target register. The first byte - at index zero - is stored at the given register ``aReg``, the next byte - at index 1 - is stored at ``aReg+1`` and so on. More formally:: data[0] -> aReg data[1] -> aReg + 1 ... The number of bytes written is determined implicitly by the length of the ``data`` list. If the auto-increment feature doesn't match the actual chip, a sub-class should overwrite this method. :param int aReg: The address of the first register to receive\ the block of data. :param list data: List of bytes to be written. The length of the\ list determines the number of bytes to write. So, all values in\ the list will be transferred to the device. :return: An error code indicating success or the reason of failure. :rtype: ErrorCode """ err = ErrorCode.errOk for idx in range( len(data) ): err = self.writeByteRegister(aReg+idx, data[idx]) return err
[docs]class SimDevNull( SimDev, Module ): """Slim-line serial device simulation. Reading retrieves always the same\ constant value, while writing is simply ignored. """ DEFAULT_READING = 0x3A
[docs] @classmethod def Params_init(cls, paramDict): """Initialize configuration parameters. Any supported option missed in the dictionary handed in, will be added upon return. Also see :meth:`.module.Module.Params_init`. The following options are supported. ================== ======= ========================== Key Range Default ================== ======= ========================== SimDevNull.reading integer SimDevNull.DEFAULT_READING ================== ======= ========================== :param dict(str, object) paramDict: Dictionary mapping option names to their respective values. :returns: none :rtype: None """ paramDict["SimDevNull.reading"] = paramDict.get("SimDevNull.reading", SimDevNull.DEFAULT_READING) return None
[docs] def open( self, paramDict ): """Open the instance and prepare it for use. Also see :meth:`.module.Module.open`. :param dict(str, object) paramDict: Configuration parameters as\ obtained from :meth:`Params_init`, possibly. :return: An error code indicating either success or the reason of failure. :rtype: ErrorCode """ paramDict["SimDevNull.reading"] = paramDict.get("SimDevNull.reading", SimDevNull.DEFAULT_READING) self._reading = paramDict["SimDevNull.reading"] return ErrorCode.errOk
[docs] def readByteRegister( self, aReg ): """Read a single byte. Independent of the given register, the delivered value will always be the same. That delivered reading can be configured using the SimDevNull.reading option when calling :meth:`open`. :param int aReg: The address of the register to be read.\ Actually ignored. :return: A one-byte integer representing the response of the device\ and an error code indicating success or the reason of failure. :rtype: int, ErrorCode """ del aReg return self._reading, ErrorCode.errOk
[docs] def writeByteRegister( self, aReg, data ): """ Write a single byte. Actually, does nothing. Also see :meth:`SimDev.writeByteRegister`. :param int aReg: The address of the register. Ignored. :param int data: The new value to store to that register. Ignored. :return: An error code indicating success or the reason of failure. :rtype: ErrorCode """ del aReg, data return ErrorCode.errOk
[docs]@unique class MemoryType(Enum): """Enumeration to reflect the different types of memory. """ ROM = auto() RAM = auto() NVM = auto() VOLATILE = auto()
[docs]@dataclass class Register: """Simulate a memory-based register. Depending on the type of memory, the register content can or cannot be changed by simply writing to it. Volatile registers are not writable. They may change their content spontaneously or by mechanisms that cannot be controlled by the user. """ address: int """The address to identify this register during read/write operations.""" content: int = 0 """The register content. Can be initialized, independently of the\ memory type of that register.""" type: MemoryType = MemoryType.RAM """The type of memory for that register."""
[docs]class SimDevMemory( SimDev ): """Serial bus implementation to simulate a device that can be accessed\ through a set of memory-based registers. The list of registers\ must be provided during instantiation. """ def __init__(self, regs): self._regs = regs def _findReg(self, regAdr): reg = next( (r for r in self._regs if r.address==regAdr), None) return reg
[docs] def readByteRegister( self, aReg ): """Retrieves a register's content. To also simulate side effects\ of reading, the following steps are executed in sequence, no matter what the memory type of the given register is: #. calling :meth:`._onPreRead` #. reading the register content #. calling :meth:`._onPostRead` Note that the return value is solely determined by what is read from the register in step #2. It cannot be altered by :meth:`._onPostRead`, anymore. Also see :meth:`.simbus.SimDev.readByteRegister`. :param int aReg: The address of the register to be read. :return: A one-byte integer representing the response of the device\ and an error code indicating success or the reason of failure. :rtype: int, ErrorCode """ reg = self._findReg( aReg ) if (reg is None): data = 0 err = ErrorCode.errInvalidParameter else: err = self._onPreRead( reg ) data = reg.content self._onPostRead( reg ) return data, err
[docs] def writeByteRegister( self, aReg, data ): """Write a single byte value into a certain register. Write attempts to registers with non-writable memory are ignored. For registers with writable memory, the following sequence is executed in order to give sub-classes the opportunity to simulate side effects: #. calling :meth:`._onPreWrite`, may alter the intended data and\ returns the actual new content to write. #. writing the new register content #. calling :meth:`._onPostWrite` :param int aReg: The address of the register to receive the new value. :param int data: The new value to store to that register. :return: An error code indicating success or the reason of failure. :rtype: ErrorCode """ reg = self._findReg( aReg ) err = ErrorCode.errOk if not reg: err = ErrorCode.errInvalidParameter else: if (reg.type == MemoryType.RAM): newContent = self._onPreWrite(reg, data) reg.content = newContent err = self._onPostWrite(reg) else: err = ErrorCode.errFailure return err
def _onPreRead(self, reg): """Interface function that will be called right before a register\ is read. Can be used by sub-classes to simulate the exact hardware behavior while reading a register. Modifying the register content here, would highly affect the return value of the surrounding :meth:`.readByteRegister` function. The return value is to indicate if the read operation will succeed. This implementation is simply empty. :param Register reg: The register instance to be read. :return: An error code indicating success or the reason of failure. :rtype: ErrorCode """ del reg return ErrorCode.errOk def _onPostRead(self, reg): """Interface function that will be called right after a register\ was read. Can be used by sub-classes to simulate the exact hardware behavior while reading a register. Any action in this routine will not influence the return value of the (current call of the) surrounding :meth:`.readByteRegister` function. This implementation increments the register content if the register's memory type is :attr:`MemoryType.VOLATILE`. :param Register reg: The register instance to be read. :returns: None :rtype: none """ if (reg.type == MemoryType.VOLATILE): reg.content = reg.content + 1 return None def _onPreWrite(self, reg, newData): """Interface function that will be called right before a register\ is written. Can be used by sub-classes to simulate the exact hardware behavior while writing a register. The return value immediately defines the actual content to be written. The current implementation just returns the `newData` argument. :param Register reg: The register instance to write to. :param int newData: The new value that is intended to be stored\ to that register. :returns: The value that will actually be stored to the register.\ Possibly a modified variant of the `newData` parameter. :rtype: int """ del reg return newData def _onPostWrite(self, reg): """Interface function that will be called right after a register\ was written. Can be used by sub-classes to simulate the exact hardware behavior while writing a register. The return value is to indicate if the write operation succeeded. This implementation is simply empty. :param Register reg: The register instance that was written. :return: An error code indicating success or the reason of failure. :rtype: ErrorCode """ del reg return ErrorCode.errOk