# -*- coding: utf-8 -*-
#############################################################################
# zlib License
#
# (C) 2024 Cristóvão Beirão da Cruz e Silva <cbeiraod@cern.ch>
#
# This software is provided 'as-is', without any express or implied
# warranty. In no event will the authors be held liable for any damages
# arising from the use of this software.
#
# Permission is granted to anyone to use this software for any purpose,
# including commercial applications, and to alter it and redistribute it
# freely, subject to the following restrictions:
#
# 1. The origin of this software must not be misrepresented; you must not
# claim that you wrote the original software. If you use this software
# in a product, an acknowledgment in the product documentation would be
# appreciated but is not required.
# 2. Altered source versions must be plainly marked as such, and must not be
# misrepresented as being the original software.
# 3. This notice may not be removed or altered from any source distribution.
#############################################################################
from __future__ import annotations
from typing import Union
from usb_iss import UsbIss
from usb_iss import defs
from .i2c_connection_helper import I2C_Connection_Helper
from .i2c_messages import I2CMessages
valid_clocks = [20, 50, 100, 400, 1000]
software_clocks = [20, 50, 100, 400]
hardware_clocks = [100, 400, 1000]
[docs]
class USB_ISS_Helper(I2C_Connection_Helper):
"""Class to handle the USB-ISS connection
This class is a wrapper around the usb_iss library, providing a simplified interface for the
usage in the ETROC DAQ assuming only I2C is used, with optional GPIO for the extra pins
Parameters
----------
port
The port where the USB-ISS device can be found
clock
The clock frequency to use for I2C communication. It must be a valid clock from the
list: 20, 50, 100, 400, 1000. For clock frequencies where there is both software and
hardware support, the hardware version will be given precedence.
use_serial
Whether to use the extra pins on the USB-ISS as a serial interface, if not they will be
GPIO (by default set to analog in). The serial port is only enabled if the baud rate is
also set.
baud_rate
The baud rate to use for the serial communication
verbose
Whether to output detailed information to the terminal
max_seq_byte
The maximum number of sequential bytes supported in a single I2C message. The limit is not
necessarily from the USB-ISS and may be from the device it is communicating with. TODO: Add
an option to set this limit per operation so different limits can be set for different devices.
dummy_connect
If set, the connection to the USB-ISS will be emulated and a dummy device will be configured
Raises
------
SerialException
If the serial object can not be found
ValueError
If a wrong value is passed to the clock configuration
RuntimeError
For other issues communicating with the USB-ISS device
Examples
--------
>>> import i2c_gui2
>>> usbiss = i2c_gui2.USB_ISS_Helper("/dev/ttyACM0")
>>> usbiss.version()
"""
_port: str
_clock: int
_verbose: bool
_use_serial: bool
_baud_rate: Union[int, None]
_iss: UsbIss
_fw_version: int
_serial: str
def __init__(
self,
port: str,
clock: int,
use_serial: bool = False,
baud_rate: Union[int, None] = None,
verbose: bool = False,
max_seq_byte: int = 8,
dummy_connect: bool = False,
):
super().__init__(max_seq_byte=max_seq_byte, no_connect=dummy_connect)
if clock not in valid_clocks:
raise ValueError(f"Received a wrong clock value: {clock} kHz")
self._port = port
self._clock = clock
self._verbose = verbose
self._use_serial = use_serial
self._baud_rate = baud_rate
if self._use_serial and self._baud_rate is None:
self._use_serial = False
if not self._use_serial:
self._baud_rate = None
self._iss = UsbIss(dummy=dummy_connect, verbose=self._verbose)
self._iss.open(port)
module_id = self._iss.read_module_id()
if module_id != 7 and not dummy_connect:
raise RuntimeError(f"Got an unexpected value for the module ID of the USB-ISS device: {module_id}")
self._fw_version = self._iss.read_fw_version()
self._serial = self._iss.read_serial_number()
self._use_hardware = False
if self._clock in hardware_clocks:
self._use_hardware = True
if not self._use_serial:
self._iss.setup_i2c(self._clock, self._use_hardware, defs.IOType.ANALOGUE_INPUT, defs.IOType.ANALOGUE_INPUT)
else:
self._iss.setup_i2c_serial(self._clock, self._use_hardware, self._baud_rate)
self._is_connected = True
def __del__(self):
if hasattr(self, "_iss"):
if self._iss is not None:
self._iss.close()
@property
def fw_version(self) -> int:
"""The fw_version property getter method
This method returns the firmware version of the USB-ISS firmware
Returns
-------
int
The firmware version number.
"""
return self._fw_version
@property
def serial(self) -> str:
"""The serial_number property getter method
This method returns the serial number of the USB-ISS firmware
Returns
-------
str
The serial number.
"""
return self._serial
@property
def port(self) -> str:
"""The port property getter method
This method returns the port the USB-ISS is connected to
Returns
-------
str
The port.
"""
return self._port
@property
def clock(self) -> int:
"""The clock property getter method
This method returns the clock frequency the USB-ISS I2C is configured to
Returns
-------
int
The clock frequency.
"""
return self._clock
@property
def use_serial(self) -> bool:
"""The use_serial property getter method
This method returns if the extra pins on the USB-ISS are being used as a serial communication
Returns
-------
bool
If the extra pins on the USB-ISS are being used as a serial communication
"""
return self._use_serial
@property
def baud_rate(self) -> Union[int, None]:
"""The baud_rate property getter method
This method returns the baud rate the extra pins on the USB-ISS used as a serial communication are set to
Returns
-------
int
If the extra pins on the USB-ISS are being used as a serial communication
None
If the USB-ISS is not configured to use the serial port for communication
"""
return self._baud_rate
def _check_i2c_device(self, device_address: int) -> bool:
"""The internal method to check if an i2c device with the given address is connected
This method overrides the one from the base class.
Parameters
----------
device_address
The I2C address of the device to check. It must be a 7-bit address as per the I2C standard
Returns
-------
bool
The presence or absence of the device with the `device_address`
"""
return self._iss.i2c.test(device_address)
def _write_i2c_device_memory(
self,
device_address: int,
word_address: int,
byte_data: list[int],
write_type: str = 'Normal',
address_bitlength: int = 8,
):
"""The internal method to write byte data to an i2c device with the given address.
This method overrides the one from the base class.
Parameters
----------
device_address
The I2C address of the device to write to. It must be a 7-bit address as per the I2C standard.
word_address
The address of the first byte to be written to. The address should have its endianness correctly
set such that the MSB of `word_address` is the first byte sent on the I2C lines.
byte_data
The byte data to be written to the I2C device. The data is written to the I2C bus in the order
given, so the endianness of the data must be correctly set, assuming the device contains registers
larger than 8 bits.
write_type
The type of protocol used for the actual writing procedure to the device.
address_bitlength
The length in bits of the address
Raises
------
RuntimeError
If a not supported configuration is chosen
"""
if write_type == 'Normal':
if address_bitlength == 16:
self._iss.i2c.write_ad2(device_address, word_address, byte_data)
elif address_bitlength == 8:
self._iss.i2c.write_ad1(device_address, word_address, byte_data)
else:
raise RuntimeError("Unknown bit size trying to be sent")
else:
raise RuntimeError("Unknown write type chosen for the USB ISS")
def _read_i2c_device_memory(
self,
device_address: int,
word_address: int,
byte_count: int,
read_type: str = 'Normal',
address_bitlength: int = 8,
) -> list[int]:
"""The internal method to read byte data from an i2c device with the given address.
This method overrides the one from the base class.
Parameters
----------
device_address
The I2C address of the device to write to. It must be a 7-bit address as per the I2C standard.
word_address
The address of the first byte to be read from. The address should have its endianness correctly
set such that the MSB of `word_address` is the first byte sent on the I2C lines.
byte_count
The number of bytes to be read from the I2C device. If multybyte registers are to be read, the
number of bytes should be given, not the number of words.
read_type
The type of protocol used for the actual reading procedure from the device. Supported protocols
are "Normal" and "Repeated Start". The Repeated Start protocol is implemented by the AD5593R chip.
address_bitlength
The length in bits of the address
Raises
------
RuntimeError
If a not supported configuration is chosen
Returns
-------
list[int]
The list of bytes in the order presented on the I2C bus. If multibyte registers are being read, then
the words must be correctly put back together by taking into account the data endianness sent by the
I2C device.
"""
if read_type == 'Normal':
if address_bitlength == 16:
return self._iss.i2c.read_ad2(device_address, word_address, byte_count)
if address_bitlength == 8:
return self._iss.i2c.read_ad1(device_address, word_address, byte_count)
else:
raise RuntimeError("Unknown bit size trying to be sent")
elif read_type == "Repeated Start":
direct_msg = [defs.I2CDirect.START]
device_address_byte = device_address << 1
if address_bitlength == 8:
direct_msg += [
defs.I2CDirect.WRITE2,
device_address_byte,
word_address & 0xFF,
]
elif address_bitlength == 16:
direct_msg += [
defs.I2CDirect.WRITE3,
device_address_byte,
(word_address >> 8) & 0xFF,
word_address & 0xFF,
]
else:
raise RuntimeError("Unknown bit size trying to be sent")
direct_msg += [
defs.I2CDirect.RESTART,
defs.I2CDirect.WRITE1,
device_address_byte | 0x01,
]
if byte_count <= 16:
if byte_count > 1:
direct_msg += [
getattr(defs.I2CDirect, f"READ{byte_count-1}"),
]
else:
raise RuntimeError("USB ISS does not support a block read of more than 16 bytes")
direct_msg += [
defs.I2CDirect.NACK,
defs.I2CDirect.READ1,
defs.I2CDirect.STOP,
]
retVal = self._iss.i2c.direct(direct_msg)
if len(retVal) != byte_count:
raise RuntimeError("Did not receive the expected number of bytes")
else:
return retVal
else:
raise RuntimeError("Unknown read type chosen for the USB ISS")
def _direct_i2c(self, commands: list[I2CMessages]) -> list[int]: # noqa: C901
"""The internal method to send arbitrary I2C messages to the I2C bus.
This method overrides the one from the base class.
Parameters
----------
commands
The I2C commands to be sent to the I2C bus in the order they are to be sent.
Raises
------
RuntimeError
If an unknown command is trying to be sent
Returns
-------
list[int]
The list of bytes returned to the I2C Bus in the order presented on the I2C bus.
"""
direct_msg = []
idx = 0
while True:
if idx >= len(commands):
break
command = commands[idx]
if command not in I2CMessages:
raise RuntimeError("Unknown I2C command")
direct_msg += [command.value]
if command == I2CMessages.WRITE1:
direct_msg += [commands[idx + 1]]
idx += 2
elif command == I2CMessages.WRITE2:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
idx += 3
elif command == I2CMessages.WRITE3:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
idx += 4
elif command == I2CMessages.WRITE4:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
idx += 5
elif command == I2CMessages.WRITE5:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
idx += 6
elif command == I2CMessages.WRITE6:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
direct_msg += [commands[idx + 6]]
idx += 7
elif command == I2CMessages.WRITE7:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
direct_msg += [commands[idx + 6]]
direct_msg += [commands[idx + 7]]
idx += 8
elif command == I2CMessages.WRITE8:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
direct_msg += [commands[idx + 6]]
direct_msg += [commands[idx + 7]]
direct_msg += [commands[idx + 8]]
idx += 9
elif command == I2CMessages.WRITE9:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
direct_msg += [commands[idx + 6]]
direct_msg += [commands[idx + 7]]
direct_msg += [commands[idx + 8]]
direct_msg += [commands[idx + 9]]
idx += 10
elif command == I2CMessages.WRITE10:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
direct_msg += [commands[idx + 6]]
direct_msg += [commands[idx + 7]]
direct_msg += [commands[idx + 8]]
direct_msg += [commands[idx + 9]]
direct_msg += [commands[idx + 10]]
idx += 11
elif command == I2CMessages.WRITE11:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
direct_msg += [commands[idx + 6]]
direct_msg += [commands[idx + 7]]
direct_msg += [commands[idx + 8]]
direct_msg += [commands[idx + 9]]
direct_msg += [commands[idx + 10]]
direct_msg += [commands[idx + 11]]
idx += 12
elif command == I2CMessages.WRITE12:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
direct_msg += [commands[idx + 6]]
direct_msg += [commands[idx + 7]]
direct_msg += [commands[idx + 8]]
direct_msg += [commands[idx + 9]]
direct_msg += [commands[idx + 10]]
direct_msg += [commands[idx + 11]]
direct_msg += [commands[idx + 12]]
idx += 13
elif command == I2CMessages.WRITE13:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
direct_msg += [commands[idx + 6]]
direct_msg += [commands[idx + 7]]
direct_msg += [commands[idx + 8]]
direct_msg += [commands[idx + 9]]
direct_msg += [commands[idx + 10]]
direct_msg += [commands[idx + 11]]
direct_msg += [commands[idx + 12]]
direct_msg += [commands[idx + 13]]
idx += 14
elif command == I2CMessages.WRITE14:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
direct_msg += [commands[idx + 6]]
direct_msg += [commands[idx + 7]]
direct_msg += [commands[idx + 8]]
direct_msg += [commands[idx + 9]]
direct_msg += [commands[idx + 10]]
direct_msg += [commands[idx + 11]]
direct_msg += [commands[idx + 12]]
direct_msg += [commands[idx + 13]]
direct_msg += [commands[idx + 14]]
idx += 15
elif command == I2CMessages.WRITE15:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
direct_msg += [commands[idx + 6]]
direct_msg += [commands[idx + 7]]
direct_msg += [commands[idx + 8]]
direct_msg += [commands[idx + 9]]
direct_msg += [commands[idx + 10]]
direct_msg += [commands[idx + 11]]
direct_msg += [commands[idx + 12]]
direct_msg += [commands[idx + 13]]
direct_msg += [commands[idx + 14]]
direct_msg += [commands[idx + 15]]
idx += 16
elif command == I2CMessages.WRITE16:
direct_msg += [commands[idx + 1]]
direct_msg += [commands[idx + 2]]
direct_msg += [commands[idx + 3]]
direct_msg += [commands[idx + 4]]
direct_msg += [commands[idx + 5]]
direct_msg += [commands[idx + 6]]
direct_msg += [commands[idx + 7]]
direct_msg += [commands[idx + 8]]
direct_msg += [commands[idx + 9]]
direct_msg += [commands[idx + 10]]
direct_msg += [commands[idx + 11]]
direct_msg += [commands[idx + 12]]
direct_msg += [commands[idx + 13]]
direct_msg += [commands[idx + 14]]
direct_msg += [commands[idx + 15]]
direct_msg += [commands[idx + 16]]
idx += 17
else:
idx += 1
return self._iss.i2c.direct(direct_msg)