MikrofonSensor und TemperaturSenor die zwei Python programme funktionieren. mit den jeweiligen 2 json Datein. Beim TemperaturSensor wird im Terminal keine Wertre ausgegeben aber in der json Datei kann man die Temp und Hum sehen.

This commit is contained in:
Chiara 2025-05-28 14:53:44 +02:00
parent 4c654ec969
commit 1751076592
2614 changed files with 349009 additions and 0 deletions

View file

@ -0,0 +1 @@
Please read pyftdi/README.rst for installation instructions.

View file

@ -0,0 +1,42 @@
# Copyright (c) 2010-2024 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2016, Neotion
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
# pylint: disable=missing-docstring
__version__ = '0.56.0'
__title__ = 'PyFtdi'
__description__ = 'FTDI device driver (pure Python)'
__uri__ = 'http://github.com/eblot/pyftdi'
__doc__ = __description__ + ' <' + __uri__ + '>'
__author__ = 'Emmanuel Blot'
# For all support requests, please open a new issue on GitHub
__email__ = 'emmanuel.blot@free.fr'
__license__ = 'Modified BSD'
__copyright__ = 'Copyright (c) 2011-2024 Emmanuel Blot'
from logging import WARNING, NullHandler, getLogger
class FtdiLogger:
log = getLogger('pyftdi')
log.addHandler(NullHandler())
log.setLevel(level=WARNING)
@classmethod
def set_formatter(cls, formatter):
handlers = list(cls.log.handlers)
for handler in handlers:
handler.setFormatter(formatter)
@classmethod
def get_level(cls):
return cls.log.getEffectiveLevel()
@classmethod
def set_level(cls, level):
cls.log.setLevel(level=level)

View file

@ -0,0 +1,534 @@
# Copyright (c) 2010-2024 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2008-2016, Neotion
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Bit field and sequence management."""
from typing import Iterable, List, Optional, Tuple, Union
from .misc import is_iterable, xor
# pylint: disable=invalid-name
# pylint: disable=unneeded-not
# pylint: disable=duplicate-key
class BitSequenceError(Exception):
"""Bit sequence error"""
class BitSequence:
"""Bit sequence.
Support most of the common bit operations: or, and, shift, comparison,
and conversion from and to integral values.
Bit sequence objects are iterable.
Can be initialized with another bit sequence, a integral value,
a sequence of bytes or an iterable of common boolean values.
:param value: initial value
:param msb: most significant bit first or not
:param length: count of signficant bits in the bit sequence
:param bytes_: initial value specified as a sequence of bytes
:param msby: most significant byte first or not
"""
def __init__(self, value: Union['BitSequence', str, int] = None,
msb: bool = False, length: int = 0,
bytes_: Optional[bytes] = None, msby: bool = True):
"""Instantiate a new bit sequence.
"""
self._seq = bytearray()
seq = self._seq
if value and bytes_:
raise BitSequenceError("Cannot inialize with both a value and "
"bytes")
if bytes_:
provider = list(bytes_).__iter__() if msby else reversed(bytes_)
for byte in provider:
if isinstance(byte, str):
byte = ord(byte)
elif byte > 0xff:
raise BitSequenceError("Invalid byte value")
b = []
for _ in range(8):
b.append(bool(byte & 0x1))
byte >>= 1
if msb:
b.reverse()
seq.extend(b)
else:
value = self._tomutable(value)
if isinstance(value, int):
self._init_from_integer(value, msb, length)
elif isinstance(value, BitSequence):
self._init_from_sibling(value, msb)
elif is_iterable(value):
self._init_from_iterable(value, msb)
elif value is None:
pass
else:
raise BitSequenceError(f"Cannot initialize from '{type(value)}'")
self._update_length(length, msb)
def sequence(self) -> bytearray:
"""Return the internal representation as a new mutable sequence"""
return bytearray(self._seq)
def reverse(self) -> 'BitSequence':
"""In-place reverse"""
self._seq.reverse()
return self
def invert(self) -> 'BitSequence':
"""In-place invert sequence values"""
self._seq = bytearray([x ^ 1 for x in self._seq])
return self
def append(self, seq) -> 'BitSequence':
"""Concatenate a new BitSequence"""
if not isinstance(seq, BitSequence):
seq = BitSequence(seq)
self._seq.extend(seq.sequence())
return self
def lsr(self, count: int) -> None:
"""Left shift rotate"""
count %= len(self)
self._seq[:] = self._seq[count:] + self._seq[:count]
def rsr(self, count: int) -> None:
"""Right shift rotate"""
count %= len(self)
self._seq[:] = self._seq[-count:] + self._seq[:-count]
def tobit(self) -> bool:
"""Degenerate the sequence into a single bit, if possible"""
if len(self) != 1:
raise BitSequenceError("BitSequence should be a scalar")
return bool(self._seq[0])
def tobyte(self, msb: bool = False) -> int:
"""Convert the sequence into a single byte value, if possible"""
if len(self) > 8:
raise BitSequenceError("Cannot fit into a single byte")
byte = 0
pos = -1 if not msb else 0
# copy the sequence
seq = self._seq[:]
while seq:
byte <<= 1
byte |= seq.pop(pos)
return byte
def tobytes(self, msb: bool = False, msby: bool = False) -> bytearray:
"""Convert the sequence into a sequence of byte values"""
blength = (len(self)+7) & (~0x7)
sequence = list(self._seq)
if not msb:
sequence.reverse()
bytes_ = bytearray()
for pos in range(0, blength, 8):
seq = sequence[pos:pos+8]
byte = 0
while seq:
byte <<= 1
byte |= seq.pop(0)
bytes_.append(byte)
if msby:
bytes_.reverse()
return bytes_
@staticmethod
def _tomutable(value: Union[str, Tuple]) -> List:
"""Convert a immutable sequence into a mutable one"""
if isinstance(value, tuple):
# convert immutable sequence into a list so it can be popped out
value = list(value)
elif isinstance(value, str):
# convert immutable sequence into a list so it can be popped out
if value.startswith('0b'):
value = list(value[2:])
else:
value = list(value)
return value
def _init_from_integer(self, value: int, msb: bool, length: int) -> None:
"""Initialize from any integer value"""
bl = length or -1
seq = self._seq
while bl:
seq.append(bool(value & 1))
value >>= 1
if not value:
break
bl -= 1
if msb:
seq.reverse()
def _init_from_iterable(self, iterable: Iterable, msb: bool) -> None:
"""Initialize from an iterable"""
smap = {'0': 0, '1': 1, False: 0, True: 1, 0: 0, 1: 1}
seq = self._seq
try:
if msb:
seq.extend([smap[bit] for bit in reversed(iterable)])
else:
seq.extend([smap[bit] for bit in iterable])
except KeyError as exc:
raise BitSequenceError('Invalid binary character in initializer') \
from exc
def _init_from_sibling(self, value: 'BitSequence', msb: bool) -> None:
"""Initialize from a fellow object"""
self._seq = value.sequence()
if msb:
self._seq.reverse()
def _update_length(self, length, msb):
"""If a specific length is specified, extend the sequence as
expected"""
if length and (len(self) < length):
extra = bytearray([False] * (length-len(self)))
if msb:
extra.extend(self._seq)
self._seq = extra
else:
self._seq.extend(extra)
def __iter__(self):
return self._seq.__iter__()
def __reversed__(self):
return self._seq.__reversed__()
def __getitem__(self, index):
if isinstance(index, slice):
return self.__class__(value=self._seq[index])
return self._seq[index]
def __setitem__(self, index, value):
if isinstance(value, BitSequence):
if issubclass(value.__class__, self.__class__) and \
value.__class__ != self.__class__:
raise BitSequenceError("Cannot set item with instance of a "
"subclass")
if isinstance(index, slice):
value = self.__class__(value, length=len(self._seq[index]))
self._seq[index] = value.sequence()
else:
if not isinstance(value, BitSequence):
value = self.__class__(value)
val = value.tobit()
if index > len(self._seq):
raise BitSequenceError("Cannot change the sequence size")
self._seq[index] = val
def __len__(self):
return len(self._seq)
def __eq__(self, other):
return self._cmp(other) == 0
def __ne__(self, other):
return not self == other
def __le__(self, other):
return not self._cmp(other) <= 0
def __lt__(self, other):
return not self._cmp(other) < 0
def __ge__(self, other):
return not self._cmp(other) >= 0
def __gt__(self, other):
return not self._cmp(other) > 0
def _cmp(self, other):
# the bit sequence should be of the same length
ld = len(self) - len(other)
if ld:
return ld
for n, (x, y) in enumerate(zip(self._seq, other.sequence()), start=1):
if xor(x, y):
return n
return 0
def __repr__(self):
# cannot use bin() as it truncates the MSB zero bits
return ''.join([b and '1' or '0' for b in reversed(self._seq)])
def __str__(self):
chunks = []
srepr = repr(self)
length = len(self)
for i in range(0, length, 8):
if i:
j = -i
else:
j = None
chunks.append(srepr[-i-8:j])
return f'{len(self)}: {" ".join(reversed(chunks))}'
def __int__(self):
value = 0
for b in reversed(self._seq):
value <<= 1
value |= b and 1
return value
def __and__(self, other):
if not isinstance(other, self.__class__):
raise BitSequenceError('Need a BitSequence to combine')
if len(self) != len(other):
raise BitSequenceError('Sequences must be the same size')
return self.__class__(value=list(map(lambda x, y: x and y,
self._seq, other.sequence())))
def __or__(self, other):
if not isinstance(other, self.__class__):
raise BitSequenceError('Need a BitSequence to combine')
if len(self) != len(other):
raise BitSequenceError('Sequences must be the same size')
return self.__class__(value=list(map(lambda x, y: x or y,
self._seq, other.sequence())))
def __add__(self, other):
return self.__class__(value=self._seq + other.sequence())
def __ilshift__(self, count):
count %= len(self)
seq = bytearray([0]*count)
seq.extend(self._seq[:-count])
self._seq = seq
return self
def __irshift__(self, count):
count %= len(self)
seq = self._seq[count:]
seq.extend([0]*count)
self._seq = seq
return self
def inc(self) -> None:
"""Increment the sequence"""
for p, b in enumerate(self._seq):
b ^= True
self._seq[p] = b
if b:
break
def dec(self) -> None:
"""Decrement the sequence"""
for p, b in enumerate(self._seq):
b ^= True
self._seq[p] = b
if not b:
break
def invariant(self) -> bool:
"""Tells whether all bits of the sequence are of the same value.
Return the value, or ValueError if the bits are not of the same
value
"""
try:
ref = self._seq[0]
except IndexError as exc:
raise ValueError('Empty sequence') from exc
if len(self._seq) == 1:
return ref
for b in self._seq[1:]:
if b != ref:
raise ValueError('Bits do no match')
return ref
class BitZSequence(BitSequence):
"""Tri-state bit sequence manipulation.
Support most of the BitSequence operations, with an extra high-Z state
:param value: initial value
:param msb: most significant bit first or not
:param length: count of signficant bits in the bit sequence
"""
__slots__ = ['_seq']
Z = 0xff # maximum byte value
def __init__(self, value=None, msb=False, length=0):
BitSequence.__init__(self, value=value, msb=msb, length=length)
def invert(self):
self._seq = [x in (None, BitZSequence.Z) and BitZSequence.Z or x ^ 1
for x in self._seq]
return self
def tobyte(self, msb=False):
raise BitSequenceError(f'Type {type(self)} cannot be converted to '
f'byte')
def tobytes(self, msb=False, msby=False):
raise BitSequenceError(f'Type {type(self)} cannot be converted to '
f'bytes')
def matches(self, other):
# pylint: disable=missing-function-docstring
if not isinstance(self, BitSequence):
raise BitSequenceError('Not a BitSequence instance')
# the bit sequence should be of the same length
ld = len(self) - len(other)
if ld:
return ld
for (x, y) in zip(self._seq, other.sequence()):
if BitZSequence.Z in (x, y):
continue
if x is not y:
return False
return True
def _init_from_iterable(self, iterable, msb):
"""Initialize from an iterable"""
smap = {'0': 0, '1': 1, 'Z': BitZSequence.Z,
False: 0, True: 1, None: BitZSequence.Z,
0: 0, 1: 1, BitZSequence.Z: BitZSequence.Z}
seq = self._seq
try:
if msb:
seq.extend([smap[bit] for bit in reversed(iterable)])
else:
seq.extend([smap[bit] for bit in iterable])
except KeyError as exc:
raise BitSequenceError("Invalid binary character in initializer") \
from exc
def __repr__(self):
smap = {False: '0', True: '1', BitZSequence.Z: 'Z'}
return ''.join([smap[b] for b in reversed(self._seq)])
def __int__(self):
if BitZSequence.Z in self._seq:
raise BitSequenceError("High-Z BitSequence cannot be converted to "
"an integral type")
return BitSequence.__int__(self)
def __cmp__(self, other):
# the bit sequence should be of the same length
ld = len(self) - len(other)
if ld:
return ld
for n, (x, y) in enumerate(zip(self._seq, other.sequence()), start=1):
if x is not y:
return n
return 0
def __and__(self, other):
if not isinstance(self, BitSequence):
raise BitSequenceError('Need a BitSequence-compliant object to '
'combine')
if len(self) != len(other):
raise BitSequenceError('Sequences must be the same size')
def andz(x, y):
"""Compute the boolean AND operation for a tri-state boolean"""
if BitZSequence.Z in (x, y):
return BitZSequence.Z
return x and y
return self.__class__(
value=list(map(andz, self._seq, other.sequence())))
def __or__(self, other):
if not isinstance(self, BitSequence):
raise BitSequenceError('Need a BitSequence-compliant object to '
'combine')
if len(self) != len(other):
raise BitSequenceError('Sequences must be the same size')
def orz(x, y):
"""Compute the boolean OR operation for a tri-state boolean"""
if BitZSequence.Z in (x, y):
return BitZSequence.Z
return x or y
return self.__class__(value=list(map(orz, self._seq,
other.sequence())))
def __rand__(self, other):
return self.__and__(other)
def __ror__(self, other):
return self.__or__(other)
def __radd__(self, other):
return self.__class__(value=other) + self
class BitField:
"""Bit field class to access and modify an integral value
Beware the slices does not behave as regular Python slices:
bitfield[3:5] means b3..b5, NOT b3..b4 as with regular slices
"""
__slots__ = ['_val']
def __init__(self, value=0):
self._val = value
def to_seq(self, msb=0, lsb=0):
"""Return the BitFiled as a sequence of boolean value"""
seq = bytearray()
count = 0
value = self._val
while value:
count += 1
value >>= 1
for x in range(lsb, max(msb, count)):
seq.append(bool((self._val >> x) & 1))
return tuple(reversed(seq))
def __getitem__(self, index):
if isinstance(index, slice):
if index.stop == index.start:
return None
if index.stop < index.start:
offset = index.stop
count = index.start-index.stop+1
else:
offset = index.start
count = index.stop-index.start+1
mask = (1 << count)-1
return (self._val >> offset) & mask
return (self._val >> index) & 1
def __setitem__(self, index, value):
if isinstance(index, slice):
if index.stop == index.start:
return
if index.stop < index.start:
offset = index.stop
count = index.start-index.stop+1
else:
offset = index.start
count = index.stop-index.start+1
mask = (1 << count)-1
value = (value & mask) << offset
mask <<= offset
self._val = (self._val & ~mask) | value
else:
if isinstance(value, bool):
value = int(value)
value = (value & int(1)) << index
mask = int(1) << index
self._val = (self._val & ~mask) | value
def __int__(self):
return self._val
def __str__(self):
return bin(self._val)

View file

@ -0,0 +1,69 @@
.. include:: ../defs.rst
:mod:`eeprom` - EEPROM API
--------------------------
.. module :: pyftdi.eeprom
Quickstart
~~~~~~~~~~
Example: dump the EEPROM content
.. code-block:: python
# Instantiate an EEPROM manager
eeprom = FtdiEeprom()
# Select the FTDI device to access (the interface is mandatory but any
# valid interface for the device fits)
eeprom.open('ftdi://ftdi:2232h/1')
# Show the EEPROM content
eeprom.dump_config()
# Show the raw EEPROM content
from pyftdi.misc import hexdump
print(hexdump(eeprom.data))
Example: update the serial number
.. code-block:: python
# Instantiate an EEPROM manager
eeprom = FtdiEeprom()
# Select the FTDI device to access
eeprom.open('ftdi://ftdi:2232h/1')
# Change the serial number
eeprom.set_serial_number('123456')
# Commit the change to the EEPROM
eeprom.commit(dry_run=False)
Classes
~~~~~~~
.. autoclass :: FtdiEeprom
:members:
Exceptions
~~~~~~~~~~
.. autoexception :: FtdiEepromError
Tests
~~~~~
.. code-block:: shell
# optional: specify an alternative FTDI device
export FTDI_DEVICE=ftdi://ftdi:2232h/1
PYTHONPATH=. python3 pyftdi/tests/eeprom.py

View file

@ -0,0 +1,26 @@
.. -*- coding: utf-8 -*-
.. include:: ../defs.rst
:mod:`ftdi` - FTDI low-level driver
-----------------------------------
.. module :: pyftdi.ftdi
This module implements access to the low level FTDI hardware. There are very
few reasons to use this module directly. Most of PyFtdi_ features are available
through the dedicated :doc:`APIs <index>`.
Classes
~~~~~~~
.. autoclass :: Ftdi
:members:
Exceptions
~~~~~~~~~~
.. autoexception :: FtdiError
.. autoexception :: FtdiMpsseError
.. autoexception :: FtdiFeatureError

View file

@ -0,0 +1,56 @@
.. -*- coding: utf-8 -*-
.. include:: ../defs.rst
:mod:`gpio` - GPIO API
----------------------
.. module :: pyftdi.gpio
Direct drive GPIO pins of FTDI device.
.. note::
This mode is mutually exclusive with advanced serial MPSSE features, such as
|I2C|, SPI, JTAG, ...
If you need to use GPIO pins and MPSSE interface on the same port, you need
to use the dedicated API. This shared mode is supported with the
:doc:`SPI API <spi>` and the :doc:`I2C API <i2c>`.
.. warning::
This API does not provide access to the special CBUS port of FT232R, FT232H,
FT230X and FT231X devices. See :ref:`cbus_gpio` for details.
Quickstart
~~~~~~~~~~
See ``tests/gpio.py`` example
Classes
~~~~~~~
.. autoclass :: GpioPort
.. autoclass :: GpioAsyncController
:members:
.. autoclass :: GpioSyncController
:members:
.. autoclass :: GpioMpsseController
:members:
Exceptions
~~~~~~~~~~
.. autoexception :: GpioException
Info about GPIO API
~~~~~~~~~~~~~~~~~~~
See :doc:`../gpio` for details

View file

@ -0,0 +1,191 @@
.. include:: ../defs.rst
:mod:`i2c` - |I2C| API
----------------------
.. module :: pyftdi.i2c
Quickstart
~~~~~~~~~~
Example: communication with an |I2C| GPIO expander
.. code-block:: python
# Instantiate an I2C controller
i2c = I2cController()
# Configure the first interface (IF/1) of the FTDI device as an I2C master
i2c.configure('ftdi://ftdi:2232h/1')
# Get a port to an I2C slave device
slave = i2c.get_port(0x21)
# Send one byte, then receive one byte
slave.exchange([0x04], 1)
# Write a register to the I2C slave
slave.write_to(0x06, b'\x00')
# Read a register from the I2C slave
slave.read_from(0x00, 1)
Example: mastering the |I2C| bus with a complex transaction
.. code-block:: python
from time import sleep
port = I2cController().get_port(0x56)
# emit a START sequence is read address, but read no data and keep the bus
# busy
port.read(0, relax=False)
# wait for ~1ms
sleep(0.001)
# write 4 bytes, without neither emitting the start or stop sequence
port.write(b'\x00\x01', relax=False, start=False)
# read 4 bytes, without emitting the start sequence, and release the bus
port.read(4, start=False)
See also pyi2cflash_ module and ``tests/i2c.py``, which provide more detailed
examples on how to use the |I2C| API.
Classes
~~~~~~~
.. autoclass :: I2cPort
:members:
.. autoclass :: I2cGpioPort
:members:
.. autoclass :: I2cController
:members:
Exceptions
~~~~~~~~~~
.. autoexception :: I2cIOError
.. autoexception :: I2cNackError
.. autoexception:: I2cTimeoutError
GPIOs
~~~~~
See :doc:`../gpio` for details
Tests
~~~~~
|I2C| sample tests expect:
* TCA9555 device on slave address 0x21
* ADXL345 device on slave address 0x53
Checkout a fresh copy from PyFtdi_ github repository.
See :doc:`../pinout` for FTDI wiring.
.. code-block:: shell
# optional: specify an alternative FTDI device
export FTDI_DEVICE=ftdi://ftdi:2232h/1
# optional: increase log level
export FTDI_LOGLEVEL=DEBUG
# be sure to connect the appropriate I2C slaves to the FTDI I2C bus and run
PYTHONPATH=. python3 pyftdi/tests/i2c.py
.. _i2c_limitations:
Caveats
~~~~~~~
Open-collector bus
``````````````````
|I2C| uses only two bidirectional open collector (or open drain) lines, pulled
up with resistors. These resistors are also required on an |I2C| bus when an
FTDI master is used.
However, most FTDI devices do not use open collector outputs. Some software
tricks are used to fake open collector mode when possible, for example to
sample for slave ACK/NACK, but most communication (R/W, addressing, data)
cannot use open collector mode. This means that most FTDI devices source
current to the SCL and SDA lines. FTDI HW is able to cope with conflicting
signalling, where FTDI HW forces a line the high logical level while a slave
forces it to the low logical level, and limits the sourced current. You may
want to check your schematics if the slave is not able to handle 4 .. 16 mA
input current in SCL and SDA, for example. The maximal source current depends
on the FTDI device and the attached EEPROM configuration which may be used to
limit further down the sourced current.
Fortunately, FT232H device is fitted with real open collector outputs, and
PyFtdi always enable this mode on SCL and SDA lines when a FT232H device is
used.
Other FTDI devices such as FT2232H, FT4232H and FT4232HA do not support open
collector mode, and source current to SCL and SDA lines.
Clock streching
```````````````
Clock stretching is supported through a hack that re-uses the JTAG adaptative
clock mode designed for ARM devices. FTDI HW drives SCL on ``AD0`` (`BD0`), and
samples the SCL line on : the 8\ :sup:`th` pin of a port ``AD7`` (``BD7``).
When a FTDI device without an open collector capability is used
(FT2232H, FT4232H, FT4232HA) the current sourced from AD0 may prevent proper
sampling ofthe SCL line when the slave attempts to strech the clock. It is
therefore recommended to add a low forward voltage drop diode to `AD0` to
prevent AD0 to source current to the SCL bus. See the wiring section.
Speed
`````
Due to the FTDI MPSSE engine limitations, the actual bitrate for write
operations over I2C is very slow. As the I2C protocol enforces that each I2C
exchanged byte needs to be acknowledged by the peer, a I2C byte cannot be
written to the slave before the previous byte has been acknowledged by the
slave and read back by the I2C master, that is the host. This requires several
USB transfer for each byte, on top of each latency of the USB stack may add up.
With the introduction of PyFtdi_ v0.51, read operations have been optimized so
that long read operations are now much faster thanwith previous PyFtdi_
versions, and exhibits far shorter latencies.
Use of PyFtdi_ should nevetherless carefully studied and is not recommended if
you need to achieve medium to high speed write operations with a slave
(relative to the I2C clock...). Dedicated I2C master such as FT4222H device is
likely a better option, but is not currently supported with PyFtdi_ as it uses
a different communication protocol.
.. _i2c_wiring:
Wiring
~~~~~~
.. figure:: ../images/i2c_wiring.png
:scale: 50 %
:alt: I2C wiring
:align: right
Fig.1: FT2232H with clock stretching
* ``AD0`` should be connected to the SCL bus
* ``AD1`` and ``AD2`` should be both connected to the SDA bus
* ``AD7`` should be connected to the SCL bus, if clock streching is required
* remaining pins can be freely used as regular GPIOs.
*Fig.1*:
* ``D1`` is only required when clock streching is used along with
FT2232H, FT4232H or FT4232HA devices. It should not be fit with an FT232H.
* ``AD7`` may be used as a regular GPIO with clock stretching is not required.

View file

@ -0,0 +1,20 @@
API documentation
=================
.. include:: ../defs.rst
|release|
---------
.. toctree::
:maxdepth: 1
:glob:
ftdi
gpio
i2c
spi
uart
usbtools
misc
eeprom

View file

@ -0,0 +1,11 @@
.. -*- coding: utf-8 -*-
:mod:`misc` - Miscellaneous helpers
-----------------------------------
Functions
~~~~~~~~~
.. automodule:: pyftdi.misc
:members:

View file

@ -0,0 +1,203 @@
.. include:: ../defs.rst
:mod:`spi` - SPI API
--------------------
.. module :: pyftdi.spi
Quickstart
~~~~~~~~~~
Example: communication with a SPI data flash (half-duplex example)
.. code-block:: python
# Instantiate a SPI controller
spi = SpiController()
# Configure the first interface (IF/1) of the FTDI device as a SPI master
spi.configure('ftdi://ftdi:2232h/1')
# Get a port to a SPI slave w/ /CS on A*BUS3 and SPI mode 0 @ 12MHz
slave = spi.get_port(cs=0, freq=12E6, mode=0)
# Request the JEDEC ID from the SPI slave
jedec_id = slave.exchange([0x9f], 3)
Example: communication with a remote SPI device using full-duplex mode
.. code-block:: python
# Instantiate a SPI controller
# We need want to use A*BUS4 for /CS, so at least 2 /CS lines should be
# reserved for SPI, the remaining IO are available as GPIOs.
spi = SpiController(cs_count=2)
# Configure the first interface (IF/1) of the FTDI device as a SPI master
spi.configure('ftdi://ftdi:2232h/1')
# Get a port to a SPI slave w/ /CS on A*BUS4 and SPI mode 2 @ 10MHz
slave = spi.get_port(cs=1, freq=10E6, mode=2)
# Synchronous exchange with the remote SPI slave
write_buf = b'\x01\x02\x03'
read_buf = slave.exchange(write_buf, duplex=True)
Example: communication with a SPI device and an extra GPIO
.. code-block:: python
# Instantiate a SPI controller
spi = SpiController()
# Configure the first interface (IF/1) of the first FTDI device as a
# SPI master
spi.configure('ftdi://::/1')
# Get a SPI port to a SPI slave w/ /CS on A*BUS3 and SPI mode 0 @ 12MHz
slave = spi.get_port(cs=0, freq=12E6, mode=0)
# Get GPIO port to manage extra pins, use A*BUS4 as GPO, A*BUS4 as GPI
gpio = spi.get_gpio()
gpio.set_direction(0x30, 0x10)
# Assert GPO pin
gpio.write(0x10)
# Write to SPI slace
slave.write(b'hello world!')
# Release GPO pin
gpio.write(0x00)
# Test GPI pin
pin = bool(gpio.read() & 0x20)
Example: managing non-byte aligned transfers
.. code-block:: python
# Instantiate a SPI controller
spi = SpiController()
# Configure the first interface (IF/1) of the first FTDI device as a
# SPI master
spi.configure('ftdi://::/1')
# Get a SPI port to a SPI slave w/ /CS on A*BUS3
slave = spi.get_port(cs=0)
# write 6 first bits of a byte buffer
slave.write(b'\xff', droptail=2)
# read only 13 bits from a slave (13 clock cycles)
# only the 5 MSBs of the last byte are valid, 3 LSBs are force to zero
slave.read(2, droptail=3)
See also pyspiflash_ module and ``tests/spi.py``, which provide more detailed
examples on how to use the SPI API.
Classes
~~~~~~~
.. autoclass :: SpiPort
:members:
.. autoclass :: SpiGpioPort
:members:
.. autoclass :: SpiController
:members:
Exceptions
~~~~~~~~~~
.. autoexception :: SpiIOError
GPIOs
~~~~~
See :doc:`../gpio` for details
Tests
~~~~~
SPI sample tests expect:
* MX25L1606E device on /CS 0, SPI mode 0
* ADXL345 device on /CS 1, SPI mode 2
* RFDA2125 device on /CS 2, SPI mode 0
Checkout a fresh copy from PyFtdi_ github repository.
See :doc:`../pinout` for FTDI wiring.
.. code-block:: shell
# optional: specify an alternative FTDI device
export FTDI_DEVICE=ftdi://ftdi:2232h/1
# optional: increase log level
export FTDI_LOGLEVEL=DEBUG
# be sure to connect the appropriate SPI slaves to the FTDI SPI bus and run
PYTHONPATH=. python3 pyftdi/tests/spi.py
.. _spi_limitations:
Limitations
~~~~~~~~~~~
SPI Modes 1 & 3
```````````````
FTDI hardware does not support cpha=1 (mode 1 and mode 3). As stated in
Application Node 114:
"*It is recommended that designers review the SPI Slave
data sheet to determine the SPI mode implementation. FTDI device can only
support mode 0 and mode 2 due to the limitation of MPSSE engine.*".
Support for mode 1 and mode 3 is implemented with some workarounds, but
generated signals may not be reliable: YMMV. It is only available with -H
series (232H, 2232H, 4232H, 4232HA).
The 3-clock phase mode which has initially be designed to cope with |I2C|
signalling is used to delay the data lines from the clock signals. A direct
consequence of this workaround is that SCLK duty cycle is not longer 50% but
25% (mode 1) or 75% (mode 3). Again, support for mode 1 and mode 3 should be
considered as a kludge, you've been warned.
Time-sensitive usage
````````````````````
Due to the MPSSE engine limitation, it is not possible to achieve
time-controlled request sequence. In other words, if the SPI slave needs to
receive command sequences at precise instants - for example ADC or DAC
devices - PyFtdi_ use is not recommended. This limitation is likely to apply
to any library that relies on FTDI device. The USB bus latency and the lack
of timestamped commands always add jitter and delays, with no easy known
workaround.
.. _spi_wiring:
Wiring
~~~~~~
.. figure:: ../images/spi_wiring.png
:scale: 50 %
:alt: SPI wiring
:align: right
Fig.1: FT2232H with two SPI slaves
* ``AD0`` should be connected to SCLK
* ``AD1`` should be connected to MOSI
* ``AD2`` should be connected to MISO
* ``AD3`` should be connected to the first slave /CS.
* ``AD4`` should be connected to the second slave /CS, if any
* remaining pins can be freely used as regular GPIOs.
*Fig.1*:
* ``AD4`` may be used as a regular GPIO if a single SPI slave is used
* ``AD5`` may be used as another /CS signal for a third slave, in this case
the first available GPIO is ``AD6``, etc.

View file

@ -0,0 +1,228 @@
.. include:: ../defs.rst
:mod:`serialext` - UART API
---------------------------
There is no dedicated module for the UART API, as PyFtdi_ acts as a backend of
the well-known pyserial_ module.
The pyserial_ backend module is implemented as the `serialext.protocol_ftdi`
module. It is not documented here as no direct call to this module is required,
as the UART client should use the regular pyserial_ API.
Usage
~~~~~
To enable PyFtdi_ as a pyserial_ backend, use the following import:
.. code-block:: python
import pyftdi.serialext
Then use
.. code-block:: python
pyftdi.serialext.serial_for_url(url, **options)
to open a pyserial_ serial port instance.
Quickstart
~~~~~~~~~~
.. code-block:: python
# Enable pyserial extensions
import pyftdi.serialext
# Open a serial port on the second FTDI device interface (IF/2) @ 3Mbaud
port = pyftdi.serialext.serial_for_url('ftdi://ftdi:2232h/2', baudrate=3000000)
# Send bytes
port.write(b'Hello World')
# Receive bytes
data = port.read(1024)
.. _uart_gpio:
GPIO access
~~~~~~~~~~~
UART mode, the primary function of FTDI \*232\* devices, is somewhat limited
when it comes to GPIO management, as opposed to alternative mode such as |I2C|,
SPI and JTAG. It is not possible to assign the unused pins of an UART mode to
arbitrary GPIO functions.
All the 8 lower pins of an UART port are dedicated to the UART function,
although most of them are seldomely used, as dedicated to manage a modem or a
legacy DCE_ device. Upper pins (b\ :sub:`7`\ ..b\ :sub:`15`\ ), on devices that
have ones, cannot be driven while UART port is enabled.
It is nevertheless possible to have limited access to the lower pins as GPIO,
with many limitations:
- the GPIO direction of each pin is hardcoded and cannot be changed
- GPIO pins cannot be addressed atomically: it is possible to read the state
of an input GPIO, or to change the state of an output GPIO, one after
another. This means than obtaining the state of several input GPIOs or
changing the state of several output GPIO at once is not possible.
- some pins cannot be used as GPIO is hardware flow control is enabled.
Keep in mind However that HW flow control with FTDI is not reliable, see the
:ref:`hardware_flow_control` section.
Accessing those GPIO pins is done through the UART extended pins, using their
UART assigned name, as PySerial port attributes. See the table below:
+---------------+------+-----------+-------------------------------+
| Bit | UART | Direction | API |
+===============+======+===========+===============================+
| b\ :sub:`0`\ | TX | Out | ``port.write(buffer)`` |
+---------------+------+-----------+-------------------------------+
| b\ :sub:`1`\ | RX | In | ``buffer = port.read(count)`` |
+---------------+------+-----------+-------------------------------+
| b\ :sub:`2`\ | RTS | Out | ``port.rts = state`` |
+---------------+------+-----------+-------------------------------+
| b\ :sub:`3`\ | CTS | In | ``state = port.cts`` |
+---------------+------+-----------+-------------------------------+
| b\ :sub:`4`\ | DTR | Out | ``port.dtr = state`` |
+---------------+------+-----------+-------------------------------+
| b\ :sub:`5`\ | DSR | In | ``state = port.dsr`` |
+---------------+------+-----------+-------------------------------+
| b\ :sub:`6`\ | DCD | In | ``state = port.dcd`` |
+---------------+------+-----------+-------------------------------+
| b\ :sub:`7`\ | RI | In | ``state = port.ri`` |
+---------------+------+-----------+-------------------------------+
CBUS support
````````````
Some FTDI devices (FT232R, FT232H, FT230X, FT231X) support additional CBUS
pins, which can be used as regular GPIOs pins. See :ref:`CBUS GPIO<cbus_gpio>`
for details.
.. _pyterm:
Mini serial terminal
~~~~~~~~~~~~~~~~~~~~
``pyterm.py`` is a simple serial terminal that can be used to test the serial
port feature. See the :ref:`tools` chapter to locate this tool.
::
Usage: pyterm.py [-h] [-f] [-b BAUDRATE] [-w] [-e] [-r] [-l] [-s] [-P VIDPID]
[-V VIRTUAL] [-v] [-d]
[device]
Simple Python serial terminal
positional arguments:
device serial port device name (default: ftdi:///1)
optional arguments:
-h, --help show this help message and exit
-f, --fullmode use full terminal mode, exit with [Ctrl]+B
-b BAUDRATE, --baudrate BAUDRATE
serial port baudrate (default: 115200)
-w, --hwflow hardware flow control
-e, --localecho local echo mode (print all typed chars)
-r, --crlf prefix LF with CR char, use twice to replace all LF
with CR chars
-l, --loopback loopback mode (send back all received chars)
-s, --silent silent mode
-P VIDPID, --vidpid VIDPID
specify a custom VID:PID device ID, may be repeated
-V VIRTUAL, --virtual VIRTUAL
use a virtual device, specified as YaML
-v, --verbose increase verbosity
-d, --debug enable debug mode
If the PyFtdi module is not yet installed and ``pyterm.py`` is run from the
archive directory, ``PYTHONPATH`` should be defined to the current directory::
PYTHONPATH=$PWD pyftdi/bin/pyterm.py ftdi:///?
The above command lists all the available FTDI device ports. To avoid conflicts
with some shells such as `zsh`, escape the `?` char as ``ftdi:///\?``.
To start up a serial terminal session, specify the FTDI port to use, for
example:
.. code-block:: shell
# detect all FTDI connected devices
PYTHONPATH=. python3 pyftdi/bin/ftdi_urls.py
# use the first interface of the first FT2232H as a serial port
PYTHONPATH=$PWD pyftdi/bin/pyterm.py ftdi://ftdi:2232/1
.. _uart-limitations:
Limitations
~~~~~~~~~~~
Although the FTDI H series are in theory capable of 12 MBps baudrate, baudrates
above 6 Mbps are barely usable.
See the following table for details.
+------------+-------------+------------+-------------+------------+--------+
| Requ. bps |HW capability| 9-bit time | Real bps | Duty cycle | Stable |
+============+=============+============+=============+============+========+
| 115.2 Kbps | 115.2 Kbps | 78.08 µs | 115.26 Kbps | 49.9% | Yes |
+------------+-------------+------------+-------------+------------+--------+
| 460.8 Kbps | 461.54 Kbps | 19.49 µs | 461.77 Kbps | 49.9% | Yes |
+------------+-------------+------------+-------------+------------+--------+
| 1 Mbps | 1 Mbps | 8.98 µs | 1.002 Mbps | 49.5% | Yes |
+------------+-------------+------------+-------------+------------+--------+
| 4 Mbps | 4 Mbps | 2.24 µs | 4.018 Mbps | 48% | Yes |
+------------+-------------+------------+-------------+------------+--------+
| 5 Mbps | 5.052 Mbps | 1.78 µs | 5.056 Mbps | 50% | Yes |
+------------+-------------+------------+-------------+------------+--------+
| 6 Mbps | 6 Mbps | 1.49 µs | 6.040 Mbps | 48.5% | Yes |
+------------+-------------+------------+-------------+------------+--------+
| 7 Mbps | 6.857 Mbps | 1.11 µs | 8.108 Mbps | 44% | No |
+------------+-------------+------------+-------------+------------+--------+
| 8 Mbps | 8 Mbps | 1.11 µs | 8.108 Mbps | 44%-48% | No |
+------------+-------------+------------+-------------+------------+--------+
| 8.8 Mbps | 8.727 Mbps | 1.13 µs | 7.964 Mbps | 44% | No |
+------------+-------------+------------+-------------+------------+--------+
| 9.6 Mbps | 9.6 Mbps | 1.12 µs | 8.036 Mbps | 48% | No |
+------------+-------------+------------+-------------+------------+--------+
| 10.5 Mbps | 10.667 Mbps | 1.11 µs | 8.108 Mbps | 44% | No |
+------------+-------------+------------+-------------+------------+--------+
| 12 Mbps | 12 Mbps | 0.75 µs | 12 Mbps | 43% | Yes |
+------------+-------------+------------+-------------+------------+--------+
* 9-bit time is the measured time @ FTDI output pins for a 8-bit character
(start bit + 8 bit data)
* Duty cycle is the ratio between a low-bit duration and a high-bit duration,
a good UART should exhibit the same duration for low bits and high bits,
*i.e.* a duty cycle close to 50%.
* Stability reports whether subsequent runs, with the very same HW settings,
produce the same timings.
Achieving a reliable connection over 6 Mbps has proven difficult, if not
impossible: Any baudrate greater than 6 Mbps (except the upper 12 Mbps limit)
results into an actual baudrate of about 8 Mbps, and suffer from clock
fluterring [7.95 .. 8.1Mbps].
.. _hardware_flow_control:
Hardware flow control
`````````````````````
Moreover, as the hardware flow control of the FTDI device is not a true HW
flow control. Quoting FTDI application note:
*If CTS# is logic 1 it is indicating the external device cannot accept more
data. the FTxxx will stop transmitting within 0~3 characters, depending on
what is in the buffer.*
**This potential 3 character overrun does occasionally present problems.**
*Customers shoud be made aware the FTxxx is a USB device and not a "normal"
RS232 device as seen on a PC. As such the device operates on a packet
basis as opposed to a byte basis.*

View file

@ -0,0 +1,19 @@
.. -*- coding: utf-8 -*-
:mod:`usbtools` - USB tools
---------------------------
.. module :: pyftdi.usbtools
Classes
~~~~~~~
.. autoclass :: UsbTools
:members:
Exceptions
~~~~~~~~~~
.. autoexception :: UsbToolsError

View file

@ -0,0 +1,55 @@
Authors
-------
Main developers
~~~~~~~~~~~~~~~
* Emmanuel Blot <emmanuel.blot@free.fr>
* Emmanuel Bouaziz <ebouaziz@free.fr>
Contributors
~~~~~~~~~~~~
* Nikus-V
* Dave McCoy
* Adam Feuer
* endlesscoil
* humm (Fabien Benureau)
* dlharmon
* DavidWC
* Sebastian
* Anders (anders-code)
* Andrea Concil
* Darren Garnier
* Michael Leonard
* nopeppermint (Stefan)
* hannesweisbach
* Vianney le Clément de Saint-Marcq
* Pete Schwamb
* Will Richey
* sgoadhouse
* tavip (Octavian Purdila)
* Tim Legrand
* vestom
* meierphil
* etherfi
* sgoadhouse
* jnmacd
* naushir
* markmelvin (Mark Melvin)
* stiebrs
* mpratt14
* alexforencich
* TedKus
* Amanita-muscaria
* len0rd
* Rod Whitby
* Kornel Swierzy
* Taisuke Yamada
* Michael Niewöhner
* Kalofin
* Henry Au-Yeung
* Roman Dobrodii
* Mark Mentovai
* Alessandro Zini
* Sjoerd Simons

View file

@ -0,0 +1,49 @@
.. |I2C| replace:: I\ :sup:`2`\ C
.. _FT232R: https://www.ftdichip.com/Products/ICs/FT232R.htm
.. _FT230X: https://www.ftdichip.com/Products/ICs/FT230X.html
.. _FT2232D: https://www.ftdichip.com/Products/ICs/FT2232D.htm
.. _FT232H: https://www.ftdichip.com/Products/ICs/FT232H.htm
.. _FT2232H: https://www.ftdichip.com/Products/ICs/FT2232H.html
.. _FT4232H: https://www.ftdichip.com/Products/ICs/FT4232H.htm
.. _FT4232HA: http://ftdichip.com/products/ft4232haq/
.. _FTDI_Recovery: https://www.ftdichip.com/Support/Documents/AppNotes/AN_136%20Hi%20Speed%20Mini%20Module%20EEPROM%20Disaster%20Recovery.pdf
.. _PyFtdi: https://www.github.com/eblot/pyftdi
.. _PyFtdiTools: https://github.com/eblot/pyftdi/tree/master/pyftdi/bin
.. _FTDI: https://www.ftdichip.com/
.. _PyUSB: https://pyusb.github.io/pyusb/
.. _Python: https://www.python.org/
.. _pyserial: https://pythonhosted.org/pyserial/
.. _libftdi: https://www.intra2net.com/en/developer/libftdi/
.. _pyspiflash: https://github.com/eblot/pyspiflash/
.. _pyi2cflash: https://github.com/eblot/pyi2cflash/
.. _libusb: https://www.libusb.info/
.. _Libusb on Windows: https://github.com/libusb/libusb/wiki/Windows
.. _Libusb win32: https://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/
.. _Zadig: https://zadig.akeo.ie/
.. _FTDI macOS guide: https://www.ftdichip.com/Support/Documents/AppNotes/AN_134_FTDI_Drivers_Installation_Guide_for_MAC_OSX.pdf
.. _Libusb issue on macOs: https://github.com/libusb/libusb/commit/5e45e0741daee4fa295c6cc977edfb986c872152
.. _FT_PROG: https://www.ftdichip.com/Support/Utilities.htm#FT_PROG
.. _fstring: https://www.python.org/dev/peps/pep-0498
.. _DCE: https://en.wikipedia.org/wiki/Data_circuit-terminating_equipment
.. _PEP_498: https://www.python.org/dev/peps/pep-0498
.. _PEP_526: https://www.python.org/dev/peps/pep-0526
.. _ruamel.yaml: https://pypi.org/project/ruamel.yaml
.. Restructured Text levels
.. Level 1
.. -------
.. Level 2
.. ~~~~~~~
.. Level 3
.. ```````
.. Level 4
.. .......
.. Level 5
.. +++++++

View file

@ -0,0 +1,401 @@
.. include:: defs.rst
EEPROM management
-----------------
.. warning::
Writing to the EEPROM can cause very **undesired** effects if the wrong
value is written in the wrong place. You can even essentially **brick** your
FTDI device. Use this function only with **extreme** caution.
It is not recommended to use this application with devices that use an
internal EEPROM such as FT232R or FT-X series, as if something goes wrong,
recovery options are indeed limited. FT232R internal EEPROM seems to be
unstable, even the official FT_PROG_ tool from FTDI may fail to fix it on
some conditions.
If using a Hi-Speed Mini Module and you brick for FTDI device, see
FTDI_Recovery_
Supported features
~~~~~~~~~~~~~~~~~~
EEPROM support is under active development.
Some features may be wrongly decoded, as each FTDI model implements a different
feature map, and more test/validation are required.
The :doc:`EEPROM API <api/eeprom>` implements the upper API to access the
EEPROM content.
.. _ftconf:
EEPROM configuration tool
~~~~~~~~~~~~~~~~~~~~~~~~~
``ftconf.py`` is a companion script to help managing the content of the FTDI
EEPROM from the command line. See the :ref:`tools` chapter to locate this tool.
::
usage: ftconf.py [-h] [-i INPUT] [-l {all,raw,values}] [-o OUTPUT] [-V VIRTUAL]
[-P VIDPID] [-M EEPROM] [-S {128,256,1024}] [-x] [-X HEXBLOCK]
[-s SERIAL_NUMBER] [-m MANUFACTURER] [-p PRODUCT] [-c CONFIG]
[--vid VID] [--pid PID] [-e] [-E] [-u] [-v] [-d]
[device]
Simple FTDI EEPROM configurator.
positional arguments:
device serial port device name
optional arguments:
-h, --help show this help message and exit
Files:
-i INPUT, --input INPUT
input ini file to load EEPROM content
-l {all,raw,values}, --load {all,raw,values}
section(s) to load from input file
-o OUTPUT, --output OUTPUT
output ini file to save EEPROM content
-V VIRTUAL, --virtual VIRTUAL
use a virtual device, specified as YaML
Device:
-P VIDPID, --vidpid VIDPID
specify a custom VID:PID device ID (search for FTDI devices)
-M EEPROM, --eeprom EEPROM
force an EEPROM model
-S {128,256,1024}, --size {128,256,1024}
force an EEPROM size
Format:
-x, --hexdump dump EEPROM content as ASCII
-X HEXBLOCK, --hexblock HEXBLOCK
dump EEPROM as indented hexa blocks
Configuration:
-s SERIAL_NUMBER, --serial-number SERIAL_NUMBER
set serial number
-m MANUFACTURER, --manufacturer MANUFACTURER
set manufacturer name
-p PRODUCT, --product PRODUCT
set product name
-c CONFIG, --config CONFIG
change/configure a property as key=value pair
--vid VID shortcut to configure the USB vendor ID
--pid PID shortcut to configure the USB product ID
Action:
-e, --erase erase the whole EEPROM content
-E, --full-erase erase the whole EEPROM content, including the CRC
-u, --update perform actual update, use w/ care
Extras:
-v, --verbose increase verbosity
-d, --debug enable debug mode
**Again, please read the** :doc:`license` **terms before using the EEPROM API
or this script. You may brick your device if something goes wrong, and there
may be no way to recover your device.**
Note that to protect the EEPROM content of unexpected modification, it is
mandatory to specify the :ref:`-u <option_u>` flag along any alteration/change
of the EEPROM content. Without this flag, the script performs a dry-run
execution of the changes, *i.e.* all actions but the write request to the
EEPROM are executed.
Once updated, you need to unplug/plug back the device to use the new EEPROM
configuration.
It is recommended to first save the current content of the EEPROM, using the
:ref:`-o <option_o>` flag, to have a working copy of the EEPROM data before any
attempt to modify it. It can help restoring the EEPROM if something gets wrong
during a subsequence update, thanks to the :ref:`-i <option_i>` option switch.
Most FTDI device can run without an EEPROM. If something goes wrong, try to
erase the EEPROM content, then restore the original content.
Option switches
```````````````
In addition to the :ref:`common_option_switches` for PyFtdi_ tools,
``ftconf.py`` support the following arguments:
.. _option_c:
``-c name=value``
Change a configuration in the EEPROM. This flag can be repeated as many times
as required to change several configuration parameter at once. Note that
without option ``-u``, the EEPROM content is not actually modified, the
script runs in dry-run mode.
The name should be separated from the value with an equal ``=`` sign or
alternatively a full column ``:`` character.
* To obtain the list of supported name, use the `?` wildcard: ``-c ?``, or
`-c help` to avoid conflicts with some shells
* To obtain the list of supported values for a name, use the `?` or the `help`
wildcard:
``-c name=help``, where *name* is a supported name.
See :ref:`cbus_func` table for the alternate function associated with each
name.
.. _option_E_:
``-E``
Erase the full EEPROM content including the CRC. As the CRC no longer
validates the EEPROM content, the EEPROM configuration is ignored on the next
power cycle of the device, so the default FTDI configuration is used.
This may be useful to recover from a corrupted EEPROM, as when no EEPROM or a
blank EEPROM is detected, the FTDI falls back to a default configuration.
Note that without option :ref:`-u <option_u>`, the EEPROM content is not
actually modified, the script runs in dry-run mode.
.. _option_e:
``-e``
Erase the whole EEPROM and regenerates a valid CRC.
Beware that as `-e` option generates a valid CRC for the erased EEPROM
content, the FTDI device may identified itself as VID:PID FFFF:FFFF on next
reboot. You should likely use the `--vid` and `--pid` option to define a
valid FDTI device USB identifier with this option to ensure the device
identifies itself as a FTDI device on next power cycle.
Note that without option :ref:`-u <option_u>`, the EEPROM content is not
actually modified, the script runs in dry-run mode.
Alternatively, use `-E` option that erase the full EEPROM content including
the CRC.
.. _option_i:
``-i``
Load a INI file (as generated with the :ref:`-o <option_o>` option switch. It
is possible to select which section(s) from the INI file are loaded, using
:ref:`-l <option_l>` option switch. The ``values`` section may be modified,
as it takes precedence over the ``raw`` section. Note that without option
:ref:`-u <option_u>`, the EEPROM content is not actually modified, the script
runs in dry-run mode.
.. _option_l:
``-l <all|raw|values>``
Define which section(s) of the INI file are used to update the EEPROM content
along with the :ref:`-i <option_i>` option switch. Defaults to ``all``.
The supported feature set of the ``values`` is the same as the one exposed
through the :ref:`-c <option_c>` option switch. Unsupported feature are
ignored, and a warning is emitted for each unsupported feature.
.. _option_M_:
``-M <model>``
Specify the EEPROM model (93c46, 93c56, 93c66) that is connected to the FTDI
device. There is no reason to use this option except for recovery purposes,
see option `-E`. It is mutually exclusive with the `-S` option.
.. _option_m:
``-m <manufacturer>``
Assign a new manufacturer name to the device. Note that without option
:ref:`-u <option_u>`, the EEPROM content is not actually modified, the script
runs in dry-run mode. Manufacturer names with ``/`` or ``:`` characters are
rejected, to avoid parsing issues with FTDI :ref:`URLs <url_scheme>`.
.. _option_o:
``-o <output>``
Generate and write to the specified file the EEPROM content as decoded
values and a hexa dump. The special ``-`` file can be used as the output file
to print to the standard output. The output file contains two sections:
* ``[values]`` that contain the decoded EEPROM configuration as key, value
pair. Note that the keys and values can be used as configuration input, see
option :ref:`-c <option_c>`.
* ``[raw]`` that contains a compact representation of the EEPROM raw content,
encoded as hexadecimal strings.
.. _option_p:
``-p <product>``
Assign a new product name to the device. Note that without option :ref:`-u
<option_u>`, the EEPROM content is not actually modified, the script runs in
dry-run mode. Product names with ``/`` or ``:`` characters are rejected, to
avoid parsing issues with FTDI :ref:`URLs <url_scheme>`.
.. _option_pid:
``--pid``
Define the USB product identifier - as an hexadecimal number. This is a
shortcut for `-c product_id`
.. _option_S_:
``-S <size>``
Specify the EEPROM size -in bytes- that is connected to the FTDI device.
There is no reason to use this option except for recovery purposes,
see option `-E`. It is mutually exclusive with the `-M` option.
.. _option_s:
``-s <serial>``
Assign a new serial number to the device. Note that without option :ref:`-u
<option_u>`, the EEPROM content is not actually modified, the script runs in
dry-run mode. Serial number with ``/`` or ``:`` characters are rejected, to
avoid parsing issues with FTDI :ref:`URLs <url_scheme>`.
.. _option_u:
``-u``
Update the EEPROM with the new settings. Without this flag, the script runs
in dry-run mode, so no change is made to the EEPROM. Whenever this flag is
used, the EEPROM is actually updated and its checksum regenerated. If
something goes wrong at this point, you may brick you board, you've been
warned. PyFtdi_ offers neither guarantee whatsoever than altering the EEPROM
content is safe, nor that it is possible to recover from a bricked device.
.. _option_vid:
``--vid``
Define the USB vendor identifier - as an hexadecimal number. This is a
shortcut for `-c vendor_id`.
.. _option_x:
``-x``
Generate and print a hexadecimal raw dump of the EEPROM content, similar to
the output of the `hexdump -Cv` tool.
.. _cbus_func:
CBUS function
`````````````
The following table describes the CBUS pin alternate functions. Note that
depending on the actual device, some alternate function may not be available.
+-----------------+--------+--------------------------------------------------------------------------------+
| Name | Active | Description |
+=================+========+================================================================================+
| ``TRISTATE`` | Hi-Z | IO Pad is tri-stated |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``TXLED`` | Low | TX activity, can be used as status for LED |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``RXLED`` | Low | RX activity, can be used as status for LED |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``TXRXLED`` | Low | TX & RX activity, can be used as status for LED |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``PWREN`` | Low | USB configured, USB suspend: high |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``SLEEP`` | Low | USB suspend, typically used to power down external devices. |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``DRIVE0`` | Low | Drive a constant (FT232H and FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``DRIVE1`` | High | Drive a constant (FT232H and FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``GPIO`` | | IO port for CBUS bit bang mode |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``TXDEN`` | High | Enable transmit for RS485 mode |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``CLK48`` | | Output 48 MHz clock (FT232R only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``CLK30`` | | Output 30 MHz clock (FT232H only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``CLK24`` | | Output 24 MHz clock (FT232R and FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``CLK15`` | | Output 12 MHz clock (FT232H only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``CLK12`` | | Output 12 MHz clock (FT232R and FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``CLK7_5`` | | Output 7.5 MHz clock (FT232H only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``CLK6`` | | Output 6 MHz clock (FT232R and FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``BAT_DETECT`` | High | Battery Charger Detect (FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``BAT_NDETECT`` | Low | Inverse signal of BAT_DETECT (FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``I2C_TXE`` | Low | Transmit buffer empty (FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``I2C_RXF`` | Low | Receive buffer full (FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``VBUS_SENSE`` | High | Detect when VBUS is present via the appropriate AC IO pad (FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``BB_WR`` | Low | Synchronous Bit Bang Write strobe (FT232R and FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``BB_RD`` | Low | Synchronous Bit Bang Read strobe (FT232R and FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``TIMESTAMP`` | | Toggle signal each time a USB SOF is received (FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
| ``AWAKE`` | Low | Do not suspend when unplugged/disconnect/suspsend (FT-X only) |
+-----------------+--------+--------------------------------------------------------------------------------+
Examples
````````
* Change product name and serial number
::
pyftdi/bin/ftconf.py ftdi:///1 -p UartBridge -s abcd1234 -u
* List supported configuration parameters
::
pyftdi/bin/ftconf.py ftdi:///1 -c ?
cbus_func_0, cbus_func_1, cbus_func_2, cbus_func_3, cbus_func_4,
cbus_func_5, cbus_func_6, cbus_func_7, cbus_func_8, cbus_func_9,
channel_a_driver, channel_a_type, chip, clock_polarity,
flow_control, group_0_drive, group_0_schmitt, group_0_slew,
group_1_drive, group_1_schmitt, group_1_slew, has_serial,
has_usb_version, in_isochronous, lsb_data, out_isochronous,
power_max, powersave, product_id, remote_wakeup, self_powered,
suspend_pull_down, type, usb_version, vendor_id
* List supported configuration values for CBUS0
::
pyftdi/bin/ftconf.py ftdi:///1 -c cbus_func_0:?
AWAKE, BAT_DETECT, BAT_NDETECT, BB_RD, BB_WR, CLK12, CLK24, CLK6,
DRIVE0, DRIVE1, I2C_RXF, I2C_TXE, GPIO, PWREN, RXLED, SLEEP,
TIME_STAMP, TRISTATE, TXDEN, TXLED, TXRXLED, VBUS_SENSE
* Erase the whole EEPROM including its CRC.
Once power cycle, the device should run as if no EEPROM was connected.
Do not use this with internal, embedded EEPROMs such as FT230X.
::
pyftdi/bin/ftconf.py -P ffff:ffff ftdi://ffff:ffff/1 -E -u
* Recover from an erased EEPROM with a valid CRC
::
# for a FT4232 device
# note that ffff matches an erased EEPROM, other corrupted values may
# exist, such device can be identified with system tools such as lsusb
pyftdi/bin/ftconf.py -P ffff:ffff ftdi://ffff:ffff/1 -e -u \
--vid 0403 --pid 6011
.. _eeprom_cbus:
* Configure CBUS: 0 and 3 as GPIOs, then show the device configuration
::
pyftdi/bin/ftconf.py ftdi:///1 -v
-c cbus_func_0:GPIO -c cbus_func_3:GPIO

View file

@ -0,0 +1,98 @@
.. include:: defs.rst
Features
--------
Devices
~~~~~~~
* All FTDI device ports (UART, MPSSE) can be used simultaneously.
* SPI and |I2C| SPI support simultaneous GPIO R/W access for all pins that
are not used for SPI/|I2C| feature.
* Several FTDI adapters can be accessed simultaneously from the same Python
runtime instance.
Supported features
~~~~~~~~~~~~~~~~~~
UART
````
Serial port, up to 6 Mbps. PyFtdi_ includes a pyserial_ emulation layer that
offers transparent access to the FTDI serial ports through a pyserial_-
compliant API. The ``serialext`` directory contains a minimal serial terminal
demonstrating the use of this extension, and a dispatcher automatically
selecting the serial backend (pyserial_, PyFtdi_), based on the serial port
name.
See also :ref:`uart-limitations`.
SPI master
``````````
Supported devices:
===== ===== ====== ============================================================
Mode CPol CPha Status
===== ===== ====== ============================================================
0 0 0 Supported on all MPSSE devices
1 0 1 Workaround available for on -H series
2 1 0 Supported on -H series (FT232H_/FT2232H_/FT4232H_/FT4232HA_)
3 1 1 Workaround available for on -H series
===== ===== ====== ============================================================
PyFtdi_ can be used with pyspiflash_ module that demonstrates how to
use the FTDI SPI master with a pure-Python serial flash device driver for
several common devices.
Both Half-duplex (write or read) and full-duplex (synchronous write and read)
communication modes are supported.
Experimental support for non-byte aligned access, where up to 7 trailing bits
can be discarded: no clock pulse is generated for those bits, so that SPI
transfer of non byte-sized can be performed.
See :ref:`spi_wiring` and :ref:`spi_limitations`.
Note: FTDI*232* devices cannot be used as an SPI slave.
|I2C| master
````````````
Supported devices: FT232H_, FT2232H_, FT4232H_, FT4232HA_
For now, only 7-bit addresses are supported.
GPIOs can be used while |I2C| mode is enabled.
The ``i2cscan.py`` script helps to discover which I2C devices are connected to
the FTDI I2C bus. See the :ref:`tools` chapter to locate this tool.
The pyi2cflash_ module demonstrates how to use the FTDI |I2C| master to access
serial EEPROMS.
See :ref:`i2c_wiring` and :ref:`i2c_limitations`.
Note: FTDI*232* devices cannot be used as an |I2C| slave.
JTAG
````
JTAG API is limited to low-level access. It is not intented to be used for
any flashing or debugging purpose, but may be used as a base to perform SoC
tests and boundary scans.
EEPROM
``````
The ``pyftdi/bin/ftconf.py`` script helps to manage the content of the FTDI
companion EEPROM.
Status
~~~~~~
This project is still in beta development stage. PyFtdi_ is developed as an
open-source solution.

View file

@ -0,0 +1,451 @@
.. include:: defs.rst
GPIOs
-----
Overview
~~~~~~~~
Many PyFtdi APIs give direct access to the IO pins of the FTDI devices:
* *GpioController*, implemented as ``GpioAsyncController``,
``GpioSyncController`` and ``GpioMpsseController`` (see :doc:`api/gpio`)
gives full access to the FTDI pins as raw I/O pins,
* ``SpiGpioPort`` (see :doc:`api/spi`) gives access to all free pins of an
FTDI interface, which are not reserved for the SPI feature,
* ``I2cGpioPort`` (see :doc:`api/i2c`) gives access to all free pins of an
FTDI interface, which are not reserved for the I2C feature
Other modes
```````````
* Gpio raw access is not yet supported with JTAG feature.
* It is not possible to use GPIO along with UART mode on the same interface.
However, UART mode still provides (very) limited access to GPIO pins, see
UART :ref:`uart_gpio` for details.
This document presents the common definitions for these APIs and explain how to
drive those pins.
Definitions
~~~~~~~~~~~
Interfaces
``````````
An FTDI *interface* follows the definition of a *USB interface*: it is an
independent hardware communication port with an FTDI device. Each interface can
be configured independently from the other interfaces on the same device, e.g.
one interface may be configured as an UART, the other one as |I2C| + GPIO.
It is possible to access two distinct interfaces of the same FTDI device
from a multithreaded application, and even from different applications, or
Python interpreters. However two applications cannot access the same interface
at the same time.
.. warning::
Performing a USB device reset affects all the interfaces of an FTDI device,
this is the rationale for not automatically performing a device reset when
an interface is initialiazed and configured from PyFtdi_.
.. _ftdi_ports:
Ports
`````
An FTDI port is ofter used in PyFtdi as a synonym for an interface. This may
differ from the FTDI datasheets that sometimes show an interface with several
ports (A\*BUS, B\*BUS). From a software standpoint, ports and interfaces are
equivalent: APIs access all the HW port from the same interface at once. From a
pure hardware standpoint, a single interface may be depicted as one or two
*ports*.
With PyFtdi_, *ports* and *interfaces* should be considered as synomyms.
Each port can be accessed as raw input/output pins. At a given time, a pin is
either configured as an input or an output function.
The width of a port, that is the number of pins of the interface, depending on
the actual hardware, *i.e.* the FTDI model:
* FT232R features a single port, which is 8-bit wide: `DBUS`,
* FT232H features a single port, which is 16-bit wide: `ADBUS/ACBUS`,
* FT2232D features two ports, which are 12-bit wide each: `ADBUS/ACBUS` and
`BDBUS/BCBUS`,
* FT2232H features two ports, which are 16-bit wide each: `ADBUS/ACBUS` and
`BDBUS/BCBUS`,
* FT4232H/FT4232HA features four ports, which are 8-bit wide each: `ADBUS`,
`BDBUS`, `CDBUS` and `DDBUS`,
* FT230X features a single port, which is 4-bit wide,
* FT231X feature a single port, which is 8-bit wide
For historical reasons, 16-bit ports used to be named *wide* ports and 8-bit
ports used to be called *narrow* with PyFtdi_. This terminology and APIs are
no longer used, but are kept to prevent API break. Please only use the port
``width`` rather than these legacy port types.
GPIO value
``````````
* A logical ``0`` bit represents a low level value on a pin, that is *GND*
* A logical ``1`` bit represents a high level value on a pin, that is *Vdd*
which is typically 3.3 volts on most FTDIs
Please refers to the FTDI datasheet of your device for the tolerance and
supported analog levels for more details
.. hint::
FT232H supports a specific feature, which is dedicated to better supporting
the |I2C| feature. This specific devices enables an open-collector mode:
* Setting a pin to a low level drains it to *GND*
* Setting a pin to a high level sets the pin as High-Z
This feature is automatically activated when |I2C| feature is enabled on a
port, for the two first pins, i.e. `SCL` and `SDA out`.
However, PyFTDI does not yet provide an API to enable this mode to the
other pins of a port, *i.e.* for the pins used as GPIOs.
Direction
`````````
An FTDI pin should either be configured as an input or an ouput. It is
mandatory to (re)configure the direction of a pin before changing the way it is
used.
* A logical ``0`` bit represents an input pin, *i.e.* a pin whose value can be
sampled and read via the PyFTDI APIs
* A logical ``1`` bit represents an output pin, *i.e.* a pin whose value can be
set/written with the PyFTDI APIs
.. _cbus_gpio:
CBUS GPIOs
~~~~~~~~~~
FT232R, FT232H and FT230X/FT231X support an additional port denoted CBUS:
* FT232R provides an additional 5-bit wide port, where only 4 LSBs can be
used as programmable GPIOs: ``CBUS0`` to ``CBUS3``,
* FT232H provices an additional 10-bit wide port, where only 4 pins can be
used as programmable GPIOs: ``CBUS5``, ``CBUS6``, ``CBUS8``, ``CBUS9``
* FT230X/FT231X provides an additional 4-bit wide port: ``CBUS0`` to ``CBUS3``
Note that CBUS access is slower than regular asynchronous bitbang mode.
CBUS EEPROM configuration
`````````````````````````
Accessing this extra port requires a specific EEPROM configuration.
The EEPROM needs to be configured so that the CBUS pins that need to be used
as GPIOs are defined as ``GPIO``. Without this special configuration, CBUS
pins are used for other functions, such as driving leds when data is exchanged
over the UART port. Remember to power-cycle the FTDI device after changing its
EEPROM configuration to force load the new configuration.
The :ref:`ftconf` tool can be used to query and change the EEPROM
configuration. See the EEPROM configuration :ref:`example <eeprom_cbus>`.
CBUS GPIO API
`````````````
PyFtdi_ starting from v0.47 supports CBUS pins as special GPIO port. This port
is *not* mapped as regular GPIO, a dedicated API is reserved to drive those
pins:
* :py:meth:`pyftdi.ftdi.Ftdi.has_cbus` to report whether the device supports
CBUS gpios,
* :py:meth:`pyftdi.ftdi.Ftdi.set_cbus_direction` to configure the port,
* :py:meth:`pyftdi.ftdi.Ftdi.get_cbus_gpio` to get the logical values from the
port,
* :py:meth:`pyftdi.ftdi.Ftdi.set_cbus_gpio` to set new logical values to the
port
Additionally, the EEPROM configuration can be queried to retrieve which CBUS
pins have been assigned to GPIO functions:
* :py:meth:`pyftdi.eeprom.FtdiEeprom.cbus_pins` to report CBUS GPIO pins
The CBUS port is **not** available through the
:py:class:`pyftdi.gpio.GpioController` API, as it cannot be considered as a
regular GPIO port.
.. warning::
CBUS GPIO feature has only be tested with the virtual test framework and a
real FT231X HW device. It should be considered as an experimental feature
for now.
Configuration
~~~~~~~~~~~~~
GPIO bitmap
```````````
The GPIO pins of a port are always accessed as an integer, whose supported
width depends on the width of the port. These integers should be considered as
a bitmap of pins, and are always assigned the same mapping, whatever feature is
enabled:
* b\ :sub:`0`\ (``0x01``) represents the first pin of a port, *i.e.* AD0/BD0
* b\ :sub:`1`\ (``0x02``) represents the second pin of a port, *i.e.* AD1/BD1
* ...
* b\ :sub:`7`\ (``0x80``) represents the eighth pin of a port, *i.e.* AD7/BD7
* b\ :sub:`N`\ represents the highest pin of a port, *i.e.* AD7/BD7 for an
8-bit port, AD15/BD15 for a 16-bit port, etc.
Pins reserved for a specific feature (|I2C|, SPI, ...) cannot be accessed as
a regular GPIO. They cannot be arbitrarily written and should be masked out
when the GPIO output value is set. See :ref:`reserved_pins` for details.
FT232H CBUS exception
.....................
Note that there is an exception to this rule for FT232H CBUS port: FTDI has
decided to map non-contiguous CBUS pins as GPIO-capable CBUS pins, that is
``CBUS5``, ``CBUS6``, ``CBUS8``, ``CBUS9``, where other CBUS-enabled devices
use ``CBUS0``, ``CBUS1``, ``CBUS2``, ``CBUS3``.
If the CBUS GPIO feature is used with an FT232H device, the pin positions for
the GPIO port are not b\ :sub:`5`\ .. b\ :sub:`9`\ but b\ :sub:`0`\ to
b\ :sub:`3`\ . This may sounds weird, but CBUS feature is somewhat hack-ish
even with FTDI commands, so it did not deserve a special treatment for the sake
of handling the weird implementation of FT232H.
Direction bitmap
````````````````
Before using a port as GPIO, the port must be configured as GPIO. This is
achieved by either instanciating one of the *GpioController* or by requesting
the GPIO port from a specific serial bus controller:
``I2cController.get_gpio()`` and ``SpiController.get_gpio()``. All instances
provide a similar API (duck typing API) to configure, read and write to GPIO
pins.
Once a GPIO port is instanciated, the direction of each pin should be defined.
The direction can be changed at any time. It is not possible to write to /
read from a pin before the proper direction has been defined.
To configure the direction, use the `set_direction` API with a bitmap integer
value that defines the direction to use of each pin.
Direction example
.................
A 8-bit port, dedicated to GPIO, is configured as follows:
* BD0, BD3, BD7: input, `I` for short
* BD1-BD2, BD4-BD6: output, `O` for short
That is, MSB to LSB: *I O O O I O O I*.
This translates to 0b ``0111 0110`` as output is ``1`` and input is ``0``,
that is ``0x76`` as an hexa value. This is the direction value to use to
``configure()`` the port.
See also the ``set_direction()`` API to reconfigure the direction of GPIO pins
at any time. This method accepts two arguments. This first arguments,
``pins``, defines which pins - the ones with the maching bit set - to consider
in the second ``direction`` argument, so there is no need to
preserve/read-modify-copy the configuration of other pins. Pins with their
matching bit reset are not reconfigured, whatever their direction bit.
.. code-block:: python
gpio = GpioAsyncController()
gpio.configure('ftdi:///1', direction=0x76)
# later, reconfigure BD2 as input and BD7 as output
gpio.set_direction(0x84, 0x80)
Using GPIO APIs
~~~~~~~~~~~~~~~
There are 3 variant of *GpioController*, depending on which features are needed
and how the GPIO port usage is intended. :doc:`api/gpio` gives in depth details
about those controllers. Those controllers are mapped onto FTDI HW features.
* ``GpioAsyncController`` is likely the most useful API to drive GPIOs.
It enables reading current GPIO input pin levels and to change GPIO output
pin levels. When vector values (byte buffers) are used instead of scalar
value (single byte), GPIO pins are samples/updated at a regular pace, whose
frequency can be configured. It is however impossible to control the exact
time when input pins start to be sampled, which can be tricky to use with
most applications. See :doc:`api/gpio` for details.
* ``GpioSyncController`` is a variant of the previous API.
It is aimed at precise time control of sampling/updating the GPIO: a new
GPIO input sample is captured once every time GPIO output pins are updated.
With byte buffers, GPIO pins are samples/updated at a regular pace, whose
frequency can be configured as well. The API of ``GpioSyncController``
slightly differ from the other GPIO APIs, as the usual ``read``/``write``
method are replaced with a single ``exchange`` method.
Both ``GpioAsyncController`` and ``GpioSyncController`` are restricted to only
access the 8 LSB pins of a port, which means that FTDI device with wider port
(12- and 16- pins) cannot be fully addressed, as only b\ :sub:`0`\ to b\
:sub:`7`\ can be addressed.
* ``GpioMpsseController`` enables access to the MSB pins of wide ports.
However LSB and MSB pins cannot be addressed in a true atomic manner, which
means that there is a short delay between sampling/updating the LSB and MSB
part of the same wide port. Byte buffer can also be sampled/updated at a
regular pace, but the achievable frequency range may differ from the other
controllers.
It is recommened to read the ``tests/gpio.py`` files - available from GitHub -
to get some examples on how to use these API variants.
Setting GPIO pin state
``````````````````````
To write to a GPIO, use the `write()` method. The caller needs to mask out
the bits configured as input, or an exception is triggered:
* writing ``0`` to an input pin is ignored
* writing ``1`` to an input pin raises an exception
.. code-block:: python
gpio = GpioAsyncController()
gpio.configure('ftdi:///1', direction=0x76)
# all output set low
gpio.write(0x00)
# all output set high
gpio.write(0x76)
# all output set high, apply direction mask
gpio.write(0xFF & gpio.direction)
# all output forced to high, writing to input pins is illegal
gpio.write(0xFF) # raises an IOError
gpio.close()
Retrieving GPIO pin state
`````````````````````````
To read a GPIO, use the `read()` method.
.. code-block:: python
gpio = GpioAsyncController()
gpio.configure('ftdi:///1', direction=0x76)
# read whole port
pins = gpio.read()
# ignore output values (optional)
pins &= ~gpio.direction
gpio.close()
Modifying GPIO pin state
````````````````````````
A read-modify-write sequence is required.
.. code-block:: python
gpio = GpioAsyncController()
gpio.configure('ftdi:///1', direction=0x76)
# read whole port
pins = gpio.read()
# clearing out AD1 and AD2
pins &= ~((1 << 1) | (1 << 2)) # or 0x06
# want AD2=0, AD1=1
pins |= 1 << 1
# update GPIO output
gpio.write(pins)
gpio.close()
Synchronous GPIO access
```````````````````````
.. code-block:: python
gpio = GpioSyncController()
gpio.configure('ftdi:///1', direction=0x0F, frequency=1e6)
outs = bytes(range(16))
ins = gpio.exchange(outs)
# ins contains as many bytes as outs
gpio.close()
CBUS GPIO access
````````````````
.. code-block:: python
ftdi = Ftdi()
ftdi.open_from_url('ftdi:///1')
# validate CBUS feature with the current device
assert ftdi.has_cbus
# validate CBUS EEPROM configuration with the current device
eeprom = FtdiEeprom()
eeprom.connect(ftdi)
# here we use CBUS0 and CBUS3 (or CBUS5 and CBUS9 on FT232H)
assert eeprom.cbus_mask & 0b1001 == 0b1001
# configure CBUS0 as output and CBUS3 as input
ftdi.set_cbus_direction(0b1001, 0b0001)
# set CBUS0
ftdi.set_cbus_gpio(0x1)
# get CBUS3
cbus3 = ftdi.get_cbus_gpio() >> 3
.. code-block:: python
# it is possible to open the ftdi object from an existing serial connection:
port = serial_for_url('ftdi:///1')
ftdi = port.ftdi
ftdi.has_cbus
# etc...
.. _reserved_pins:
Reserved pins
~~~~~~~~~~~~~
GPIO pins vs. feature pins
``````````````````````````
It is important to note that the reserved pins do not change the pin
assignment, *i.e.* the lowest pins of a port may become unavailable as regular
GPIO when the feature is enabled:
Example
.......
|I2C| feature reserves
the three first pins, as *SCL*, *SDA output*, *SDA input* (w/o clock stretching
feature which also reserves another pin). This means that AD0, AD1 and AD2,
that is b\ :sub:`0`\ , b\ :sub:`1`\ , b\ :sub:`2`\ cannot be directly
accessed.
The first accessible GPIO pin in this case is no longer AD0 but AD3, which
means that b\ :sub:`3`\ becomes the lowest bit which can be read/written.
.. code-block:: python
# use I2C feature
i2c = I2cController()
# configure the I2C feature, and predefines the direction of the GPIO pins
i2c.configure('ftdi:///1', direction=0x78)
gpio = i2c.get_gpio()
# read whole port
pins = gpio.read()
# clearing out I2C bits (SCL, SDAo, SDAi)
pins &= 0x07
# set AD4
pins |= 1 << 4
# update GPIO output
gpio.write(pins)

View file

@ -0,0 +1,125 @@
PyFtdi
======
.. cannot use defs.rst here, as PyPi wants a standalone file.
.. |I2C| replace:: I\ :sup:`2`\ C
Documentation
-------------
The latest PyFtdi online documentation is always available from
`here <https://eblot.github.io/pyftdi>`_.
Beware the online version may be more recent than the PyPI hosted version, as
intermediate development versions are not published to
`PyPi <https://pypi.org/project/pyftdi>`_.
PyFtdi documentation can be locally build with Sphinx, see the installation
instructions.
Source code
-----------
PyFtdi releases are available from the Python Package Index from
`PyPi <https://pypi.org/project/pyftdi>`_.
PyFtdi development code is available from
`GitHub <https://github.com/eblot/pyftdi>`_.
Overview
--------
PyFtdi aims at providing a user-space driver for popular FTDI devices,
implemented in pure Python language.
Supported FTDI devices include:
* UART and GPIO bridges
* FT232R (single port, 3Mbps)
* FT230X/FT231X/FT234X (single port, 3Mbps)
* UART and multi-serial protocols (SPI, |I2C|, JTAG) bridges
* FT2232C/D (dual port, clock up to 6 MHz)
* FT232H (single port, clock up to 30 MHz)
* FT2232H (dual port, clock up to 30 MHz)
* FT4232H (quad port, clock up to 30 MHz)
* FT4232HA (quad port, clock up to 30 MHz)
Features
--------
PyFtdi currently supports the following features:
* UART/Serial USB converter, up to 12Mbps (depending on the FTDI device
capability)
* GPIO/Bitbang support, with 8-bit asynchronous, 8-bit synchronous and
8-/16-bit MPSSE variants
* SPI master, with simultanous GPIO support, up to 12 pins per port,
with support for non-byte sized transfer
* |I2C| master, with simultanous GPIO support, up to 14 pins per port
* Basic JTAG master capabilities
* EEPROM support (some parameters cannot yet be modified, only retrieved)
* Experimental CBUS support on selected devices, 4 pins per port
Supported host OSes
-------------------
* macOS
* Linux
* FreeBSD
* Windows, although not officially supported
.. EOT
Warning
-------
Starting with version *v0.40.0*, several API changes are being introduced.
While PyFtdi tries to maintain backward compatibility with previous versions,
some of these changes may require existing clients to update calls to PyFtdi.
Do not upgrade to *v0.40.0* or above without testing your client against the
new PyFtdi releases. PyFtdi versions up to *v0.39.9* keep a stable API
with *v0.22+* series.
See the *Major Changes* section on the online documentation for details about
potential API breaks.
Major changes
~~~~~~~~~~~~~
* *read* methods now return ``bytearray`` instead of `Array('B')` so that
pyserial ``readline()`` may be used. It also brings some performance
improvements.
* PyFtdi URLs now supports ``bus:address`` alternative specifiers, which
required to augment the ``open_*()`` methods with new, optional parameters.
* ``SpiController`` reserves only one slave line (*/CS*) where it used to
reserve 4 slave lines in previous releases. This frees more GPIOs when
default value is used - it is nevertheless still possible to reserve up to 5
slave lines.
* type hinting is used for most, if not all, public methods.
* simplified baudrate divider calculation.
PyFTDI in details
-----------------
.. toctree::
:maxdepth: 1
:glob:
features
requirements
installation
urlscheme
tools
api/index
pinout
gpio
eeprom
testing
troubleshooting
authors
license

View file

@ -0,0 +1,274 @@
.. include:: defs.rst
Installation
------------
Prerequisites
~~~~~~~~~~~~~
PyFTDI_ relies on PyUSB_, which requires a native dependency: libusb 1.x.
The actual command to install depends on your OS and/or your distribution,
see below
.. _install_linux:
Debian/Ubuntu Linux
```````````````````
.. code-block:: shell
apt-get install libusb-1.0
On Linux, you also need to create a `udev` configuration file to allow
user-space processes to access to the FTDI devices. There are many ways to
configure `udev`, here is a typical setup:
::
# /etc/udev/rules.d/11-ftdi.rules
# FT232AM/FT232BM/FT232R
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6001", GROUP="plugdev", MODE="0664"
# FT2232C/FT2232D/FT2232H
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6010", GROUP="plugdev", MODE="0664"
# FT4232/FT4232H
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6011", GROUP="plugdev", MODE="0664"
# FT232H
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6014", GROUP="plugdev", MODE="0664"
# FT230X/FT231X/FT234X
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6015", GROUP="plugdev", MODE="0664"
# FT4232HA
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6048", GROUP="plugdev", MODE="0664"
.. note:: **Accessing FTDI devices with custom VID/PID**
You need to add a line for each device with a custom VID / PID pair you
declare, see :ref:`custom_vid_pid` for details.
You need to unplug / plug back the FTDI device once this file has been
created so that `udev` loads the rules for the matching device, or
alternatively, inform the ``udev`` daemon about the changes:
.. code-block:: shell
sudo udevadm control --reload-rules
sudo udevadm trigger
With this setup, be sure to add users that want to run PyFtdi_ to the
`plugdev` group, *e.g.*
.. code-block:: shell
sudo adduser $USER plugdev
Remember that you need to log out / log in to get the above command
effective, or start a subshell to try testing PyFtdi_:
.. code-block:: shell
newgrp plugdev
.. _install_macos:
Homebrew macOS
``````````````
.. code-block:: shell
brew install libusb
.. _install_windows:
Windows
```````
Windows is not officially supported (*i.e.* not tested) but some users have
reported successful installations. Windows requires a specific libusb backend
installation.
Zadig
.....
The probably easiest way to deal with libusb on Windows is to use Zadig_
1. Start up the Zadig utility
2. Select ``Options/List All Devices``, then select the FTDI devices you want
to communicate with. Its names depends on your hardware, *i.e.* the name
stored in the FTDI EEPROM.
* With FTDI devices with multiple channels, such as FT2232 (2 channels) and
FT4232 (4 channels), you **must** install the driver for the composite
parent, **not** for the individual interfaces. If you install the driver
for each interface, each interface will be presented as a unique FTDI
device and you may have difficulties to select a specific FTDI device port
once the installation is completed. To make the composite parents to appear
in the device list, uncheck the ``Options/Ignore Hubs or Composite Parents``
menu item.
* Be sure to select the parent device, *i.e.* the device name should not end
with *(Interface N)*, where *N* is the channel number.
* for example *Dual RS232-HS* represents the composite parent, while
*Dual RS232-HS (Interface 0)* represents a single channel of the FTDI
device. Always select the former.
3. Select ``libusb-win32`` (not ``WinUSB``) in the driver list.
4. Click on ``Replace Driver``
See also `Libusb on Windows`_
.. _install_python:
Python
~~~~~~
Python dependencies
```````````````````
Dependencies should be automatically installed with PIP.
* pyusb >= 1.0.0, != 1.2.0
* pyserial >= 3.0
Do *not* install PyUSB_ from GitHub development branch (``master``, ...).
Always prefer a stable, tagged release.
PyUSB 1.2.0 also broke the backward compatibility of the Device API, so it will
not work with PyFtdi.
Installing with PIP
```````````````````
PIP should automatically install the missing dependencies.
.. code-block:: shell
pip3 install pyftdi
.. _install_from_source:
Installing from source
``````````````````````
If you prefer to install from source, check out a fresh copy from PyFtdi_
github repository.
.. code-block:: shell
git clone https://github.com/eblot/pyftdi.git
cd pyftdi
# note: 'pip3' may simply be 'pip' on some hosts
pip3 install -r requirements.txt
python3 setup.py install
.. _generate_doc:
Generating the documentation
````````````````````````````
Follow :ref:`install_from_source` then:
.. code-block:: shell
pip3 install setuptools wheel sphinx sphinx_autodoc_typehints
# Shpinx Read the Doc theme seems to never get a release w/ fixed issues
pip3 install -U -e git+https://github.com/readthedocs/sphinx_rtd_theme.git@2b8717a3647cc650625c566259e00305f7fb60aa#egg=sphinx_rtd_theme
sphinx-build -b html pyftdi/doc .
The documentation may be accessed from the generated ``index.html`` entry file.
Post-installation sanity check
``````````````````````````````
Open a *shell*, or a *CMD* on Windows
.. code-block:: shell
python3 # or 'python' on Windows
from pyftdi.ftdi import Ftdi
Ftdi.show_devices()
should list all the FTDI devices available on your host.
Alternatively, you can invoke ``ftdi_urls.py`` script that lists all detected
FTDI devices. See the :doc:`tools` chapter for details.
* Example with 1 FT232H device with a serial number and 1 FT2232 device
with no serial number, connected to the host:
.. code-block::
Available interfaces:
ftdi://ftdi:232h:FT1PWZ0Q/1 (C232HD-DDHSP-0)
ftdi://ftdi:2232/1 (Dual RS232-HS)
ftdi://ftdi:2232/2 (Dual RS232-HS)
Note that FTDI devices with custom VID/PID are not listed with this simple
command, please refer to the PyFtdi_ API to add custom identifiers, *i.e.* see
:py:meth:`pyftdi.ftdi.Ftdi.add_custom_vendor` and
:py:meth:`pyftdi.ftdi.Ftdi.add_custom_product` APIs.
.. _custom_vid_pid:
Custom USB vendor and product IDs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PyFtdi only recognizes FTDI official vendor and product IDs.
If you have an FTDI device with an EEPROM with customized IDs, you need to tell
PyFtdi to support those custom USB identifiers.
Custom PID
``````````
To support a custom product ID (16-bit integer) with the official FTDI ID, add
the following code **before** any call to an FTDI ``open()`` method.
.. code-block:: python
from pyftdi.ftdi import Ftdi
Ftdi.add_custom_product(Ftdi.DEFAULT_VENDOR, product_id)
Custom VID
``````````
To support a custom vendor ID and product ID (16-bit integers), add the
following code **before** any call to an FTDI ``open()`` method.
.. code-block:: python
from pyftdi.ftdi import Ftdi
Ftdi.add_custom_vendor(vendor_id)
Ftdi.add_custom_product(vendor_id, product_id)
You may also specify an arbitrary string to each method if you want to specify
a URL by custom vendor and product names instead of their numerical values:
.. code-block:: python
from pyftdi.ftdi import Ftdi
Ftdi.add_custom_vendor(0x1234, 'myvendor')
Ftdi.add_custom_product(0x1234, 0x5678, 'myproduct')
f1 = Ftdi.create_from_url('ftdi://0x1234:0x5678/1')
f2 = Ftdi.create_from_url('ftdi://myvendor:myproduct/2')
.. note::
Remember that on OSes that require per-device access permissions such as
Linux, you also need to add the custom VID/PID entry to the configuration
file, see :ref:`Linux installation <install_linux>` ``udev`` rule file.

View file

@ -0,0 +1,44 @@
License
-------
.. include:: defs.rst
For historical reasons (PyFtdi has been initially developed as a compatibility
layer with libftdi_), the main ``ftdi.py`` file had originally been licensed
under the same license as the libftdi_ project, the GNU Lesser General Public
License LGPL v2 license. It does not share code from this project anymore, but
implements a similar API.
From my perspective, you may use it freely in open source or close source, free
or commercial projects as long as you comply with the BSD 3-clause license.
BSD 3-clause
~~~~~~~~~~~~
::
Copyright (c) 2008-2021 Emmanuel Blot <emmanuel.blot@free.fr>
All Rights Reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL NEOTION BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,39 @@
.. include:: defs.rst
FTDI device pinout
------------------
============ ============= ======= ====== ============== ========== ====== =============
IF/1 [#ih]_ IF/2 [#if2]_ BitBang UART |I2C| SPI JTAG C232HD cable
============ ============= ======= ====== ============== ========== ====== =============
``ADBUS0`` ``BDBUS0`` GPIO0 TxD SCK SCLK TCK Orange
``ADBUS1`` ``BDBUS1`` GPIO1 RxD SDA/O [#i2c]_ MOSI TDI Yellow
``ADBUS2`` ``BDBUS2`` GPIO2 RTS SDA/I [#i2c]_ MISO TDO Green
``ADBUS3`` ``BDBUS3`` GPIO3 CTS GPIO3 CS0 TMS Brown
``ADBUS4`` ``BDBUS4`` GPIO4 DTR GPIO4 CS1/GPIO4 Grey
``ADBUS5`` ``BDBUS5`` GPIO5 DSR GPIO5 CS2/GPIO5 Purple
``ADBUS6`` ``BDBUS6`` GPIO6 DCD GPIO6 CS3/GPIO6 White
``ADBUS7`` ``BDBUS7`` GPIO7 RI RSCK [#rck]_ CS4/GPIO7 RCLK Blue
``ACBUS0`` ``BCBUS0`` GPIO8 GPIO8
``ACBUS1`` ``BCBUS1`` GPIO9 GPIO9
``ACBUS2`` ``BCBUS2`` GPIO10 GPIO10
``ACBUS3`` ``BCBUS3`` GPIO11 GPIO11
``ACBUS4`` ``BCBUS4`` GPIO12 GPIO12
``ACBUS5`` ``BCBUS5`` GPIO13 GPIO13
``ACBUS6`` ``BCBUS6`` GPIO14 GPIO14
``ACBUS7`` ``BCBUS7`` GPIO15 GPIO15
============ ============= ======= ====== ============== ========== ====== =============
.. [#ih] 16-bit port (ACBUS, BCBUS) is not available with FT4232H_ series, and
FTDI2232C/D only support 12-bit ports.
.. [#i2c] FTDI pins are either configured as input or output. As |I2C| SDA line
is bi-directional, two FTDI pins are required to provide the SDA
feature, and they should be connected together and to the SDA |I2C|
bus line. Pull-up resistors on SCK and SDA lines should be used.
.. [#if2] FT232H_ does not support a secondary MPSSE port, only FT2232H_,
FT4232H_ and FT4232HA_ do. Note that FT4232H_/FT4232HA_ has 4 serial
ports, but only the first two interfaces are MPSSE-capable. C232HD
cable only exposes IF/1 (ADBUS).
.. [#rck] In order to support I2C clock stretch mode, ADBUS7 should be
connected to SCK. When clock stretching mode is not selected, ADBUS7
may be used as GPIO7.

View file

@ -0,0 +1,54 @@
.. include:: defs.rst
Requirements
------------
Python_ 3.9 or above is required.
* PyFtdi *v0.55* is the last PyFtdi version to support Python 3.8.
* Python 3.7 has reached end-of-life on June 27rd, 2023.
* PyFtdi *v0.53* is the last PyFtdi version to support Python 3.6.
* Python 3.6 has reached end-of-life on December 23rd, 2021.
* PyFtdi *v0.52* is the last PyFtdi version to support Python 3.5.
* Python 3.5 has reached end-of-life on September 5th, 2020.
PyFtdi_ relies on PyUSB_, which itself depends on one of the following native
libraries:
* libusb_, currently tested with 1.0.23
PyFtdi_ does not depend on any other native library, and only uses standard
Python modules along with PyUSB_ and pyserial_.
PyFtdi_ is beeing tested with PyUSB_ 1.1.0.
Development
~~~~~~~~~~~
PyFtdi_ is developed on macOS platforms (64-bit kernel), and is validated on a
regular basis on Linux hosts.
As it contains no native code, it should work on any PyUSB_ and libusb_
supported platforms. However, M$ Windows is a seamless source of issues and is
not officially supported, although users have reported successful installation
with Windows 7 for example. Your mileage may vary.
API breaks
~~~~~~~~~~
Starting with version *v0.40.0*, several API changes are being introduced.
While PyFtdi tries to maintain backward compatibility with previous versions,
some of these changes may require existing clients to update calls to PyFtdi.
Do not upgrade to *v0.40.0* or above without testing your client against the
new PyFtdi releases. PyFtdi versions up to *v0.39.9* keep a stable API
with *v0.22+* series.
See the *Major Changes* section for details about potential API breaks.

View file

@ -0,0 +1,126 @@
Testing
-------
.. include:: defs.rst
Overview
~~~~~~~~
Testing PyFTDI is challenging because it relies on several pieces of hardware:
* one or more FTDI device
* |I2C|, SPI, JTAG bus slaves or communication equipment for UART
The ``tests`` directory contain several tests files, which are primarily aimed
at demonstrating usage of PyFTDI in common use cases.
Most unit tests are disabled, as they require specific slaves, with a dedicated
HW wiring. Reproducing such test environments can be challenging, as it
requires dedicated test benchs.
This is a growing concern as PyFTDI keeps evolving, and up to now, regression
tests were hard to run.
Hardware tests
~~~~~~~~~~~~~~
Please refer to the ``pyftdi/tests`` directory. There is one file dedicated to
each feature to test. Note that you need to read and edit these tests files to
fit your actual test environment, and enable the proper unit test cases, as
most are actually disabled by default.
You need specific bus slaves to perform most of these tests.
.. _virtual_framework:
Virtual test framework
~~~~~~~~~~~~~~~~~~~~~~
With PyFTDI v0.45, a new test module enables PyFTDI API partial testing using a
pure software environment with no hardware. This also eases automatic testing
within a continuous integration environment.
This new module implements a virtual USB backend for PyUSB, which creates some
kind of virtual, limited USB stack. The PyUSB can be told to substitute the
native platform's libusb with this module.
This module, ``usbvirt`` can be dynamically confifured with the help of YaML
definition files to create one or more virtual FTDI devices on a virtual USB
bus topology. This enables to test ``usbtools`` module to enumerate, detect,
report and access FTDI devices using the regular :doc:`urlscheme` syntax.
``usbvirt`` also routes all vendor-specific USB API calls to a secondary
``ftdivirt`` module, which is in charge of handling all FTDI USB requests.
This module enables testing PyFtdi_ APIs. It also re-uses the MPSSE tracker
engine to decode and verify MPSSE requests used to support |I2C|, SPI and UART
features.
For now, it is able to emulate most of GPIO requests (async, sync and MPSSE)
and UART input/output. It also manages the frequency and baudrate settings.
It is not able to emulate the MPSSE commands (with the exception of set and get
GPIO values), as it represents a massive workload...
Beware: WIP
```````````
This is an experimental work in progress, which is its early inception stage.
It has nevertheless already revealed a couple of bugs that had been hiding
within PyFtdi_ for years.
There is a large work effort ahead to be able to support more use cases and
tests more APIs, and many unit tests to write.
It cannot replace hardware tests with actual boards and slaves, but should
simplify test setup and help avoiding regression issues.
Usage
`````
No hardware is required to run these tests, to even a single FTDI device.
The test configuration files are described in YaML file format, therefore the
ruamel.yaml_ package is required.
.. code-block:: python
pip3 install ruamel.yaml
PYTHONPATH=. FTDI_LOGLEVEL=info pyftdi/tests/mockusb.py
Configuration
`````````````
The ``pyftdi/tests/resources`` directory contains definition files which are
loaded by the mock unit tests.
Although it is possible to create fine grained USB device definitions, the
configuration loader tries to automatically define missing parts to match the
USB device topology of FTDI devices.
This enables to create simple definition files without having to mess with low
level USB definitions whenever possible.
EEPROM content
..............
The :ref:`ftconf` tool can be used to load, modify and generate the content of
a virtual EEPROM, see :doc:`eeprom`.
Examples
........
* An example of a nearly comprehensive syntax can be found in ``ft232h.yaml``.
* Another, much more simple example with only mandatory settings can be found
in ``ft230x.yaml``.
* An example of multiple FTDI device definitions can be found in
``ftmany.yaml``
Availability
~~~~~~~~~~~~
Note that unit tests and the virtual infrastructure are not included in the
distributed Python packages, they are only available from the git repository.

View file

@ -0,0 +1,137 @@
.. include:: defs.rst
.. _tools:
Tools
-----
Overview
~~~~~~~~
PyFtdi_ comes with a couple of scripts designed to help using PyFtdi_ APIs,
and can be useful to quick start working with PyFtdi_.
Scripts
~~~~~~~
.. _ftdi_urls:
``ftdi_urls``
`````````````
This tiny script ``ftdi_urls.py`` to list the available, *i.e.* detected,
FTDI devices connected to the host, and the URLs than can be used to open a
:py:class:`pyftdi.ftdi.Ftdi` instance with the
:py:class:`pyftdi.ftdi.Ftdi.open_from_url` family and ``configure`` methods.
``ftconf``
``````````
``ftconf.py`` is a companion script to help managing the content of
the FTDI EEPROM from the command line. See the :ref:`ftconf` documentation.
.. _i2cscan:
``i2cscan``
```````````
The ``i2cscan.py`` script helps to discover which I2C devices
are connected to the FTDI I2C bus.
.. _pyterm.py:
``pyterm``
``````````
``pyterm.py`` is a simple serial terminal that can be used to test the serial
port feature, see the :ref:`pyterm` documentation.
Where to find these tools?
~~~~~~~~~~~~~~~~~~~~~~~~~~
These scripts can be downloaded from PyFtdiTools_, and are also installed along
with the PyFtdi_ module on the local host.
The location of the scripts depends on how PyFtdi_ has been installed and the
type of hosts:
* on linux and macOS, there are located in the ``bin/`` directory, that is the
directory where the Python interpreter is installed.
* on Windows, there are located in the ``Scripts/`` directory, which is a
subdirectory of the directory where the Python interpreter is installed.
.. _common_option_switches:
Common options switches
~~~~~~~~~~~~~~~~~~~~~~~
PyFtdi_ tools share many common option switches:
.. _option_d:
``-d``
Enable debug mode, which emits Python traceback on exceptions
.. _option_h:
``-h``
Show quick help and exit
.. _option_P_:
``-P <vidpid>``
Add custom vendor and product identifiers.
PyFtdi_ only recognizes FTDI official USB vendor identifier (*0x403*) and
the USB identifiers of their products.
In order to use alternative VID/PID values, the PyFtdi_ tools accept the
``-P`` option to describe those products
The ``vidpid`` argument should match the following format:
``[vendor_name=]<vendor_id>:[product_name=]<product_id>``
* ``vendor_name`` and ``product_name`` are optional strings, they may be
omitted as they only serve as human-readable aliases for the vendor and
product names. See example below.
* ``vendor_id`` and ``product_id`` are mandatory strings that should resolve
into 16-bit integers (USB VID and PID values). Integer values are always
interpreted as hexadecimal values, *e.g.* `-P 1234:6789` is parsed as
`-P 0x1234:0x6789`.
This option may be repeated as many times as required to add support for
several custom devices.
examples:
* ``0x403:0x9999``, *vid:pid* short syntax, with no alias names;
a matching FTDI :ref:`URL <url_scheme>` would be ``ftdi://ftdi:0x9999/1``
* ``mycompany=0x666:myproduct=0xcafe``, *vid:pid* complete syntax with
aliases; matching FTDI :ref:`URLs <url_scheme>` could be:
* ``ftdi://0x666:0x9999/1``
* ``ftdi://mycompany:myproduct/1``
* ``ftdi://mycompany:0x9999/1``
* ...
.. _option_v:
``-v``
Increase verbosity, useful for debugging the tool. It can be repeated to
increase more the verbosity.
.. _option_V_:
``-V <virtual>``
Load a virtual USB device configuration, to use a virtualized FTDI/EEPROM
environment. This is useful for PyFtdi_ development, and to test EEPROM
configuration with a virtual setup. This option is not useful for regular
usage. See :ref:`virtual_framework`.

View file

@ -0,0 +1,122 @@
.. include:: defs.rst
Troubleshooting
---------------
Reporting a bug
~~~~~~~~~~~~~~~
Please do not contact the author by email. The preferered method to report bugs
and/or enhancement requests is through
`GitHub <https://github.com/eblot/pyftdi/issues>`_.
Please be sure to read the next sections before reporting a new issue.
Logging
~~~~~~~
FTDI uses the `pyftdi` logger.
It emits log messages with raw payload bytes at DEBUG level, and data loss
at ERROR level.
Common error messages
~~~~~~~~~~~~~~~~~~~~~
"Error: No backend available"
`````````````````````````````
libusb native library cannot be loaded. Try helping the dynamic loader:
* On Linux: ``export LD_LIBRARY_PATH=<path>``
where ``<path>`` is the directory containing the ``libusb-1.*.so``
library file
* On macOS: ``export DYLD_LIBRARY_PATH=.../lib``
where ``<path>`` is the directory containing the ``libusb-1.*.dylib``
library file
* On Windows:
Try to copy the USB dll where the Python executable is installed, along
with the other Python DLLs.
If this happens while using an exe created by pyinstaller:
``copy C:\Windows\System32\libusb0.dll <path>``
where ``<path>`` is the directory containing the executable created
by pyinstaller. This assumes you have installed libusb (using a tool
like Zadig) as referenced in the installation guide for Windows.
"Error: Access denied (insufficient permissions)"
`````````````````````````````````````````````````
The system may already be using the device.
* On macOS: starting with 10.9 "*Mavericks*", macOS ships with a native FTDI
kernel extension that preempts access to the FTDI device.
Up to 10.13 "*High Sierra*", this driver can be unloaded this way:
.. code-block:: shell
sudo kextunload [-v] -bundle com.apple.driver.AppleUSBFTDI
You may want to use an alias or a tiny script such as
``pyftdi/bin/uphy.sh``
Please note that the system automatically reloads the driver, so it may be
useful to move the kernel extension so that the system never loads it.
.. warning::
From macOS 10.14 "*Mojave*", the Apple kernel extension peacefully
co-exists with libusb_ and PyFtdi_, so you no longer need - and **should
not attempt** - to unload the kernel extension. If you still experience
this error, please verify you have not installed another driver from FTDI,
such as FTDI's D2XX.
* On Linux: it may indicate a missing or invalid udev configuration. See
the :doc:`installation` section.
* This error message may also be triggered whenever the communication port is
already in use.
"Error: The device has no langid"
`````````````````````````````````
* On Linux, it usually comes from the same installation issue as the
``Access denied`` error: the current user is not granted the permissions to
access the FTDI device, therefore pyusb cannot read the FTDI registers. Check
out the :doc:`installation` section.
"Bus error / Access violation"
``````````````````````````````
PyFtdi does not use any native library, but relies on PyUSB_ and libusb_. The
latter uses native code that may trigger OS error. Some early development
versions of libusb_, for example 1.0.22-b…, have been reported to trigger
such issues. Please ensure you use a stable/final versions of libusb_ if you
experience this kind of fatal error.
"serial.serialutil.SerialException: Unable to open USB port"
````````````````````````````````````````````````````````````
May be caused by a conflict with the FTDI virtual COM port (VCOM). Try
uninstalling the driver. On macOS, refer to this `FTDI macOS guide`_.
Slow initialisation on OS X El Capitan
``````````````````````````````````````
It may take several seconds to open or enumerate FTDI devices.
If you run libusb <= v1.0.20, be sure to read the `Libusb issue on macOS`_
with OS X 10.11+.

View file

@ -0,0 +1,125 @@
.. include:: defs.rst
.. _url_scheme:
URL Scheme
----------
There are two ways to open a connection to an `Ftdi` object.
The recommended way to open a connection is to specify connection details
using a URL. The URL scheme is defined as:
::
ftdi://[vendor][:[product][:serial|:bus:address|:index]]/interface
where:
* vendor: the USB vendor ID of the manufacturer
* ex: ``ftdi`` or ``0x403``
* product: the USB product ID of the device
* ex: ``232h`` or ``0x6014``
* Supported product IDs: ``0x6001``, ``0x6010``, ``0x6011``, ``0x6014``,
``0x6015``
* Supported product aliases:
* ``232``, ``232r``, ``232h``, ``2232d``, ``2232h``, ``4232h``, ``4232ha``,
``230x``
* ``ft`` prefix for all aliases is also accepted, as for example ``ft232h``
* ``serial``: the serial number as a string. This is the preferred method to
uniquely identify a specific FTDI device. However, some FTDI device are not
fitted with an EEPROM, or the EEPROM is either corrupted or erased. In this
case, FTDI devices report no serial number
Examples:
* ``ftdi://ftdi:232h:FT0FMF6V/1``
* ``ftdi://:232h:FT0FMF6V/1``
* ``ftdi://::FT0FMF6V/1``
* ``bus:addess``: it is possible to select a FTDI device through a bus:address
pair, specified as *hexadecimal* integer values.
Examples:
* ``ftdi://ftdi:232h:10:22/1``
* ``ftdi://ftdi:232h:10:22/1``
* ``ftdi://::10:22/1``
Here, bus ``(0x)10`` = 16 (decimal) and address ``(0x)22`` = 34 (decimal)
* ``index``: an integer - not particularly useful, as it depends on the
enumeration order on the USB buses, and may vary from on session to another.
* ``interface``: the interface of FTDI device, starting from 1
* ``1`` for 230x and 232\* devices,
* ``1`` or ``2`` for 2232\* devices,
* ``1``, ``2``, ``3`` or ``4`` for 4232\* devices
All parameters but the interface are optional, PyFtdi tries to find the best
match. Therefore, if you have a single FTDI device connected to your system,
``ftdi:///1`` should be enough.
You can also ask PyFtdi to enumerate all the compatible devices with the
special ``ftdi:///?`` syntax. This syntax is useful to retrieve the available
FTDI URLs with serial number and/or bus:address selectors. To avoid conflicts
with some shells such as `zsh`, escape the `?` char as ``ftdi:///\?``.
There are several APIs available to enumerate/filter available FTDI device.
See :doc:`api/ftdi`.
Note that opening an FTDI connection with a URL ending with `?` is interpreted
as a query for matching FTDI devices and immediately stop. With this special
URL syntax, the avaialble devices are printed out to the standard output, and
the Python interpreter is forced to exit (`SystemExit` is raised).
When simple enumeration of the available FTDI devices is needed - so that
execution is not interrupted, two helper methods are available as
:py:meth:`pyftdi.ftdi.Ftdi.list_devices` and
:py:meth:`pyftdi.ftdi.Ftdi.show_devices` and accept the same URL syntax.
Opening a connection
~~~~~~~~~~~~~~~~~~~~
URL-based methods to open a connection
``````````````````````````````````````
.. code-block:: python
open_from_url()
open_mpsse_from_url()
open_bitbang_from_url()
Device-based methods to open a connection
`````````````````````````````````````````
You may also open an Ftdi device from an existing PyUSB_ device, with the help
of the ``open_from_device()`` helper method.
.. code-block:: python
open_from_device()
open_mpsse_from_device()
open_bitbang_from_device()
Legacy methods to open a connection
```````````````````````````````````
The old, deprecated method to open a connection is to use the ``open()``
methods without the ``_from_url`` suffix, which accept VID, PID, and serial
parameters (among others).
.. code-block:: python
open()
open_mpsse()
open_bitbang()
See the :ref:`ftdi_urls` tool to obtain the URLs for the connected FTDI
devices.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,532 @@
# Copyright (c) 2014-2024, Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2016, Emmanuel Bouaziz <ebouaziz@free.fr>
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""GPIO/BitBang support for PyFdti"""
from struct import calcsize as scalc, unpack as sunpack
from typing import Iterable, Optional, Tuple, Union
from .ftdi import Ftdi, FtdiError
from .misc import is_iterable
class GpioException(FtdiError):
"""Base class for GPIO errors.
"""
class GpioPort:
"""Duck-type GPIO port for GPIO all controllers.
"""
class GpioBaseController(GpioPort):
"""GPIO controller for an FTDI port, in bit-bang legacy mode.
GPIO bit-bang mode is limited to the 8 lower pins of each GPIO port.
"""
def __init__(self):
self._ftdi = Ftdi()
self._direction = 0
self._width = 0
self._mask = 0
self._frequency = 0
@property
def ftdi(self) -> Ftdi:
"""Return the Ftdi instance.
:return: the Ftdi instance
"""
return self._ftdi
@property
def is_connected(self) -> bool:
"""Reports whether a connection exists with the FTDI interface.
:return: the FTDI slave connection status
"""
return self._ftdi.is_connected
def configure(self, url: str, direction: int = 0,
**kwargs) -> int:
"""Open a new interface to the specified FTDI device in bitbang mode.
:param str url: a FTDI URL selector
:param int direction: a bitfield specifying the FTDI GPIO direction,
where high level defines an output, and low level defines an
input
:param initial: optional initial GPIO output value
:param pace: optional pace in GPIO sample per second
:return: actual bitbang pace in sample per second
"""
if self.is_connected:
raise FtdiError('Already connected')
kwargs = dict(kwargs)
frequency = kwargs.get('frequency', None)
if frequency is None:
frequency = kwargs.get('baudrate', None)
for k in ('direction', 'sync', 'frequency', 'baudrate'):
if k in kwargs:
del kwargs[k]
self._frequency = self._configure(url, direction, frequency, **kwargs)
def close(self, freeze: bool = False) -> None:
"""Close the GPIO port.
:param freeze: if set, FTDI port is not reset to its default
state on close. This means the port is left with
its current configuration and output signals.
This feature should not be used except for very
specific needs.
"""
if self._ftdi.is_connected:
self._ftdi.close(freeze)
def get_gpio(self) -> GpioPort:
"""Retrieve the GPIO port.
This method is mostly useless, it is a wrapper to duck type other
GPIO APIs (I2C, SPI, ...)
:return: GPIO port
"""
return self
@property
def direction(self) -> int:
"""Reports the GPIO direction.
:return: a bitfield specifying the FTDI GPIO direction, where high
level reports an output pin, and low level reports an input pin
"""
return self._direction
@property
def pins(self) -> int:
"""Report the configured GPIOs as a bitfield.
A true bit represents a GPIO, a false bit a reserved or not
configured pin.
:return: always 0xFF for GpioController instance.
"""
return self._mask
@property
def all_pins(self) -> int:
"""Report the addressable GPIOs as a bitfield.
A true bit represents a pin which may be used as a GPIO, a false bit
a reserved pin
:return: always 0xFF for GpioController instance.
"""
return self._mask
@property
def width(self) -> int:
"""Report the FTDI count of addressable pins.
:return: the width of the GPIO port.
"""
return self._width
@property
def frequency(self) -> float:
"""Return the pace at which sequence of GPIO samples are read
and written.
"""
return self._frequency
def set_frequency(self, frequency: Union[int, float]) -> None:
"""Set the frequency at which sequence of GPIO samples are read
and written.
:param frequency: the new frequency, in GPIO samples per second
"""
raise NotImplementedError('GpioBaseController cannot be instanciated')
def set_direction(self, pins: int, direction: int) -> None:
"""Update the GPIO pin direction.
:param pins: which GPIO pins should be reconfigured
:param direction: a bitfield of GPIO pins. Each bit represent a
GPIO pin, where a high level sets the pin as output and a low
level sets the pin as input/high-Z.
"""
if direction > self._mask:
raise GpioException("Invalid direction mask")
self._direction &= ~pins
self._direction |= (pins & direction)
self._update_direction()
def _configure(self, url: str, direction: int,
frequency: Union[int, float, None] = None, **kwargs) -> int:
raise NotImplementedError('GpioBaseController cannot be instanciated')
def _update_direction(self) -> None:
raise NotImplementedError('Missing implementation')
class GpioAsyncController(GpioBaseController):
"""GPIO controller for an FTDI port, in bit-bang asynchronous mode.
GPIO accessible pins are limited to the 8 lower pins of each GPIO port.
Asynchronous bitbang output are updated on write request using the
:py:meth:`write` method, clocked at the selected frequency.
Asynchronous bitbang input are sampled at the same rate, as soon as the
controller is initialized. The GPIO input samples fill in the FTDI HW
buffer until it is filled up, in which case sampling stops until the
GPIO samples are read out with the :py:meth:`read` method. It may be
therefore hard to use, except if peek mode is selected,
see :py:meth:`read` for details.
Note that FTDI internal clock divider cannot generate any arbitrary
frequency, so the closest frequency to the request one that can be
generated is selected. The actual :py:attr:`frequency` may be tested to
check if it matches the board requirements.
"""
def read(self, readlen: int = 1, peek: Optional[bool] = None,
noflush: bool = False) -> Union[int, bytes]:
"""Read the GPIO input pin electrical level.
:param readlen: how many GPIO samples to retrieve. Each sample is
8-bit wide.
:param peek: whether to peek/sample the instantaneous GPIO pin
values from port, or to use the HW FIFO. The HW FIFO is
continously filled up with GPIO sample at the current
frequency, until it is full - samples are no longer
collected until the FIFO is read. This means than
non-peek mode read "old" values, with no way to know at
which time they have been sampled. PyFtdi ensures that
old sampled values before the completion of a previous
GPIO write are discarded. When peek mode is selected,
readlen should be 1.
:param noflush: whether to disable the RX buffer flush before
reading out data
:return: a 8-bit wide integer if peek mode is used, or
a bytes buffer otherwise.
"""
if not self.is_connected:
raise GpioException('Not connected')
if peek is None and readlen == 1:
# compatibility with legacy API
peek = True
if peek:
if readlen != 1:
raise ValueError('Invalid read length with peek mode')
return self._ftdi.read_pins()
# in asynchronous bitbang mode, the FTDI-to-host FIFO is filled in
# continuously once this mode is activated. This means there is no
# way to trigger the exact moment where the buffer is filled in, nor
# to define the write pointer in the buffer. Reading out this buffer
# at any time is likely to contain a mix of old and new values.
# Anyway, flushing the FTDI-to-host buffer seems to be a proper
# to get in sync with the buffer.
if noflush:
return self._ftdi.read_data(readlen)
loop = 10000
while loop:
loop -= 1
# do not attempt to do anything till the FTDI HW buffer has been
# emptied, i.e. previous write calls have been handled.
status = self._ftdi.poll_modem_status()
if status & Ftdi.MODEM_TEMT:
# TX buffer is now empty, any "write" GPIO rquest has completed
# so start reading GPIO samples from this very moment.
break
else:
# sanity check to avoid endless loop on errors
raise FtdiError('FTDI TX buffer error')
# now flush the FTDI-to-host buffer as it keeps being filled with data
self._ftdi.purge_tx_buffer()
# finally perform the actual read out
return self._ftdi.read_data(readlen)
def write(self, out: Union[bytes, bytearray, int]) -> None:
"""Set the GPIO output pin electrical level, or output a sequence of
bytes @ constant frequency to GPIO output pins.
:param out: a bitfield of GPIO pins, or a sequence of them
"""
if not self.is_connected:
raise GpioException('Not connected')
if isinstance(out, (bytes, bytearray)):
pass
else:
if isinstance(out, int):
out = bytes([out])
else:
if not is_iterable(out):
raise TypeError('Invalid output value')
for val in out:
if val > self._mask:
raise ValueError('Invalid output value')
out = bytes(out)
self._ftdi.write_data(out)
def set_frequency(self, frequency: Union[int, float]) -> None:
"""Set the frequency at which sequence of GPIO samples are read
and written.
note: FTDI may update its clock register before it has emptied its
internal buffer. If the current frequency is "low", some
yet-to-output bytes may end up being clocked at the new frequency.
Unfortunately, it seems there is no way to wait for the internal
buffer to be emptied out. They can be flushed (i.e. discarded), but
not synchronized :-(
PyFtdi client should add "some" short delay to ensure a previous,
long write request has been fully output @ low freq before changing
the frequency.
Beware that only some exact frequencies can be generated. Contrary
to the UART mode, an approximate frequency is always accepted for
GPIO/bitbang mode. To get the actual frequency, and optionally abort
if it is out-of-spec, use :py:meth:`frequency` property.
:param frequency: the new frequency, in GPIO samples per second
"""
self._frequency = float(self._ftdi.set_baudrate(int(frequency), False))
def _configure(self, url: str, direction: int,
frequency: Union[int, float, None] = None, **kwargs) -> int:
if 'initial' in kwargs:
initial = kwargs['initial']
del kwargs['initial']
else:
initial = None
if 'debug' in kwargs:
# debug is not implemented
del kwargs['debug']
baudrate = int(frequency) if frequency is not None else None
baudrate = self._ftdi.open_bitbang_from_url(url,
direction=direction,
sync=False,
baudrate=baudrate,
**kwargs)
self._width = 8
self._mask = (1 << self._width) - 1
self._direction = direction & self._mask
if initial is not None:
initial &= self._mask
self.write(initial)
return float(baudrate)
def _update_direction(self) -> None:
self._ftdi.set_bitmode(self._direction, Ftdi.BitMode.BITBANG)
# old API names
open_from_url = GpioBaseController.configure
read_port = read
write_port = write
# old API compatibility
GpioController = GpioAsyncController
class GpioSyncController(GpioBaseController):
"""GPIO controller for an FTDI port, in bit-bang synchronous mode.
GPIO accessible pins are limited to the 8 lower pins of each GPIO port.
Synchronous bitbang input and output are synchronized. Eveery time GPIO
output is updated, the GPIO input is sampled and buffered.
Update and sampling are clocked at the selected frequency. The GPIO
samples are transfer in both direction with the :py:meth:`exchange`
method, which therefore always returns as many input samples as output
bytes.
Note that FTDI internal clock divider cannot generate any arbitrary
frequency, so the closest frequency to the request one that can be
generated is selected. The actual :py:attr:`frequency` may be tested to
check if it matches the board requirements.
"""
def exchange(self, out: Union[bytes, bytearray]) -> bytes:
"""Set the GPIO output pin electrical level, or output a sequence of
bytes @ constant frequency to GPIO output pins.
:param out: the byte buffer to output as GPIO
:return: a byte buffer of the same length as out buffer.
"""
if not self.is_connected:
raise GpioException('Not connected')
if isinstance(out, (bytes, bytearray)):
pass
else:
if isinstance(out, int):
out = bytes([out])
elif not is_iterable(out):
raise TypeError('Invalid output value')
for val in out:
if val > self._mask:
raise GpioException("Invalid value")
self._ftdi.write_data(out)
data = self._ftdi.read_data_bytes(len(out), 4)
return data
def set_frequency(self, frequency: Union[int, float]) -> None:
"""Set the frequency at which sequence of GPIO samples are read
and written.
:param frequency: the new frequency, in GPIO samples per second
"""
self._frequency = float(self._ftdi.set_baudrate(int(frequency), False))
def _configure(self, url: str, direction: int,
frequency: Union[int, float, None] = None, **kwargs):
if 'initial' in kwargs:
initial = kwargs['initial']
del kwargs['initial']
else:
initial = None
if 'debug' in kwargs:
# debug is not implemented
del kwargs['debug']
baudrate = int(frequency) if frequency is not None else None
baudrate = self._ftdi.open_bitbang_from_url(url,
direction=direction,
sync=True,
baudrate=baudrate,
**kwargs)
self._width = 8
self._mask = (1 << self._width) - 1
self._direction = direction & self._mask
if initial is not None:
initial &= self._mask
self.exchange(initial)
return float(baudrate)
def _update_direction(self) -> None:
self._ftdi.set_bitmode(self._direction, Ftdi.BitMode.SYNCBB)
class GpioMpsseController(GpioBaseController):
"""GPIO controller for an FTDI port, in MPSSE mode.
All GPIO pins are reachable, but MPSSE mode is slower than other modes.
Beware that LSBs (b0..b7) and MSBs (b8..b15) are accessed with two
subsequence commands, so a slight delay may occur when sampling or
changing both groups at once. In other word, it is not possible to
atomically read to / write from LSBs and MSBs. This might be worth
checking the board design if atomic access to several lines is required.
"""
MPSSE_PAYLOAD_MAX_LENGTH = 0xFF00 # 16 bits max (- spare for control)
def read(self, readlen: int = 1, peek: Optional[bool] = None) \
-> Union[int, bytes, Tuple[int]]:
"""Read the GPIO input pin electrical level.
:param readlen: how many GPIO samples to retrieve. Each sample if
:py:meth:`width` bit wide.
:param peek: whether to peak current value from port, or to use
MPSSE stream and HW FIFO. When peek mode is selected,
readlen should be 1. It is not available with wide
ports if some of the MSB pins are configured as input
:return: a :py:meth:`width` bit wide integer if direct mode is used,
a bytes buffer if :py:meth:`width` is a byte,
a list of integer otherwise (MPSSE mode only).
"""
if not self.is_connected:
raise GpioException('Not connected')
if peek:
if readlen != 1:
raise ValueError('Invalid read length with direct mode')
if self._width > 8:
if (0xFFFF & ~self._direction) >> 8:
raise ValueError('Peek mode not available with selected '
'input config')
if peek:
return self._ftdi.read_pins()
return self._read_mpsse(readlen)
def write(self, out: Union[bytes, bytearray, Iterable[int], int]) -> None:
"""Set the GPIO output pin electrical level, or output a sequence of
bytes @ constant frequency to GPIO output pins.
:param out: a bitfield of GPIO pins, or a sequence of them
"""
if not self.is_connected:
raise GpioException('Not connected')
if isinstance(out, (bytes, bytearray)):
pass
else:
if isinstance(out, int):
out = [out]
elif not is_iterable(out):
raise TypeError('Invalid output value')
for val in out:
if val > self._mask:
raise GpioException("Invalid value")
self._write_mpsse(out)
def set_frequency(self, frequency: Union[int, float]) -> None:
if not self.is_connected:
raise GpioException('Not connected')
self._frequency = self._ftdi.set_frequency(float(frequency))
def _update_direction(self) -> None:
# nothing to do in MPSSE mode, as direction is updated with each
# GPIO command
pass
def _configure(self, url: str, direction: int,
frequency: Union[int, float, None] = None, **kwargs):
frequency = self._ftdi.open_mpsse_from_url(url,
direction=direction,
frequency=frequency,
**kwargs)
self._width = self._ftdi.port_width
self._mask = (1 << self._width) - 1
self._direction = direction & self._mask
return frequency
def _read_mpsse(self, count: int) -> Tuple[int]:
if self._width > 8:
cmd = bytearray([Ftdi.GET_BITS_LOW, Ftdi.GET_BITS_HIGH] * count)
fmt = f'<{count}H'
else:
cmd = bytearray([Ftdi.GET_BITS_LOW] * count)
fmt = None
cmd.append(Ftdi.SEND_IMMEDIATE)
if len(cmd) > self.MPSSE_PAYLOAD_MAX_LENGTH:
raise ValueError('Too many samples')
self._ftdi.write_data(cmd)
size = scalc(fmt) if fmt else count
data = self._ftdi.read_data_bytes(size, 4)
if len(data) != size:
raise FtdiError(f'Cannot read GPIO, recv {len(data)} '
f'out of {size} bytes')
if fmt:
return sunpack(fmt, data)
return data
def _write_mpsse(self,
out: Union[bytes, bytearray, Iterable[int], int]) -> None:
cmd = []
low_dir = self._direction & 0xFF
if self._width > 8:
high_dir = (self._direction >> 8) & 0xFF
for data in out:
low_data = data & 0xFF
high_data = (data >> 8) & 0xFF
cmd.extend([Ftdi.SET_BITS_LOW, low_data, low_dir,
Ftdi.SET_BITS_HIGH, high_data, high_dir])
else:
for data in out:
cmd.extend([Ftdi.SET_BITS_LOW, data, low_dir])
self._ftdi.write_data(bytes(cmd))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,654 @@
# Copyright (c) 2010-2024, Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2016, Emmanuel Bouaziz <ebouaziz@free.fr>
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""JTAG support for PyFdti"""
# pylint: disable=invalid-name
# pylint: disable=missing-function-docstring
from time import sleep
from typing import List, Tuple, Union
from .ftdi import Ftdi
from .bits import BitSequence
class JtagError(Exception):
"""Generic JTAG error."""
class JtagState:
"""Test Access Port controller state"""
def __init__(self, name: str, modes: Tuple[str, str]):
self.name = name
self.modes = modes
self.exits = [self, self] # dummy value before initial configuration
def __str__(self):
return self.name
def __repr__(self):
return self.name
def setx(self, fstate: 'JtagState', tstate: 'JtagState'):
self.exits = [fstate, tstate]
def getx(self, event):
x = int(bool(event))
return self.exits[x]
def is_of(self, mode: str) -> bool:
return mode in self.modes
class JtagStateMachine:
"""Test Access Port controller state machine."""
def __init__(self):
self.states = {}
for s, modes in [('test_logic_reset', ('reset', ' idle')),
('run_test_idle', ('idle',)),
('select_dr_scan', ('dr',)),
('capture_dr', ('dr', 'shift', 'capture')),
('shift_dr', ('dr', 'shift')),
('exit_1_dr', ('dr', 'update', 'pause')),
('pause_dr', ('dr', 'pause')),
('exit_2_dr', ('dr', 'shift', 'udpate')),
('update_dr', ('dr', 'idle')),
('select_ir_scan', ('ir',)),
('capture_ir', ('ir', 'shift', 'capture')),
('shift_ir', ('ir', 'shift')),
('exit_1_ir', ('ir', 'udpate', 'pause')),
('pause_ir', ('ir', 'pause')),
('exit_2_ir', ('ir', 'shift', 'update')),
('update_ir', ('ir', 'idle'))]:
self.states[s] = JtagState(s, modes)
self['test_logic_reset'].setx(self['run_test_idle'],
self['test_logic_reset'])
self['run_test_idle'].setx(self['run_test_idle'],
self['select_dr_scan'])
self['select_dr_scan'].setx(self['capture_dr'],
self['select_ir_scan'])
self['capture_dr'].setx(self['shift_dr'], self['exit_1_dr'])
self['shift_dr'].setx(self['shift_dr'], self['exit_1_dr'])
self['exit_1_dr'].setx(self['pause_dr'], self['update_dr'])
self['pause_dr'].setx(self['pause_dr'], self['exit_2_dr'])
self['exit_2_dr'].setx(self['shift_dr'], self['update_dr'])
self['update_dr'].setx(self['run_test_idle'],
self['select_dr_scan'])
self['select_ir_scan'].setx(self['capture_ir'],
self['test_logic_reset'])
self['capture_ir'].setx(self['shift_ir'], self['exit_1_ir'])
self['shift_ir'].setx(self['shift_ir'], self['exit_1_ir'])
self['exit_1_ir'].setx(self['pause_ir'], self['update_ir'])
self['pause_ir'].setx(self['pause_ir'], self['exit_2_ir'])
self['exit_2_ir'].setx(self['shift_ir'], self['update_ir'])
self['update_ir'].setx(self['run_test_idle'], self['select_dr_scan'])
self._current = self['test_logic_reset']
def __getitem__(self, name: str) -> JtagState:
return self.states[name]
def state(self) -> JtagState:
return self._current
def state_of(self, mode: str) -> bool:
return self._current.is_of(mode)
def reset(self):
self._current = self['test_logic_reset']
def find_path(self, target: Union[JtagState, str],
source: Union[JtagState, str, None] = None) \
-> List[JtagState]:
"""Find the shortest event sequence to move from source state to
target state. If source state is not specified, used the current
state.
:return: the list of states, including source and target states.
"""
if source is None:
source = self.state()
if isinstance(source, str):
source = self[source]
if isinstance(target, str):
target = self[target]
def next_path(state, target, path):
# this test match the target, path is valid
if state == target:
return path+[state]
# candidate paths
paths = []
for x in state.exits:
# next state is self (loop around), kill the path
if x == state:
continue
# next state already in upstream (loop back), kill the path
if x in path:
continue
# try the current path
npath = next_path(x, target, path + [state])
# downstream is a valid path, store it
if npath:
paths.append(npath)
# keep the shortest path
return min(((len(p), p) for p in paths), key=lambda x: x[0])[1] \
if paths else []
return next_path(source, target, [])
@classmethod
def get_events(cls, path):
"""Build up an event sequence from a state sequence, so that the
resulting event sequence allows the JTAG state machine to advance
from the first state to the last one of the input sequence"""
events = []
for s, d in zip(path[:-1], path[1:]):
for e, x in enumerate(s.exits):
if x == d:
events.append(e)
if len(events) != len(path) - 1:
raise JtagError("Invalid path")
return BitSequence(events)
def handle_events(self, events):
for event in events:
self._current = self._current.getx(event)
class JtagController:
"""JTAG master of an FTDI device"""
TCK_BIT = 0x01 # FTDI output
TDI_BIT = 0x02 # FTDI output
TDO_BIT = 0x04 # FTDI input
TMS_BIT = 0x08 # FTDI output
TRST_BIT = 0x10 # FTDI output, not available on 2232 JTAG debugger
JTAG_MASK = 0x1f
FTDI_PIPE_LEN = 512
# Private API
def __init__(self, trst: bool = False, frequency: float = 3.0E6):
"""
trst uses the nTRST optional JTAG line to hard-reset the TAP
controller
"""
self._ftdi = Ftdi()
self._trst = trst
self._frequency = frequency
self.direction = (JtagController.TCK_BIT |
JtagController.TDI_BIT |
JtagController.TMS_BIT |
(self._trst and JtagController.TRST_BIT or 0))
self._last = None # Last deferred TDO bit
self._write_buff = bytearray()
# Public API
def configure(self, url: str) -> None:
"""Configure the FTDI interface as a JTAG controller"""
self._ftdi.open_mpsse_from_url(
url, direction=self.direction, frequency=self._frequency)
# FTDI requires to initialize all GPIOs before MPSSE kicks in
cmd = bytearray((Ftdi.SET_BITS_LOW, 0x0, self.direction))
self._ftdi.write_data(cmd)
def close(self, freeze: bool = False) -> None:
"""Close the JTAG interface/port.
:param freeze: if set, FTDI port is not reset to its default
state on close. This means the port is left with
its current configuration and output signals.
This feature should not be used except for very
specific needs.
"""
if self._ftdi.is_connected:
self._ftdi.close(freeze)
def purge(self) -> None:
self._ftdi.purge_buffers()
def reset(self, sync: bool = False) -> None:
"""Reset the attached TAP controller.
sync sends the command immediately (no caching)
"""
# we can either send a TRST HW signal or perform 5 cycles with TMS=1
# to move the remote TAP controller back to 'test_logic_reset' state
# do both for now
if not self._ftdi.is_connected:
raise JtagError("FTDI controller terminated")
if self._trst:
# nTRST
value = 0
cmd = bytearray((Ftdi.SET_BITS_LOW, value, self.direction))
self._ftdi.write_data(cmd)
sleep(0.1)
# nTRST should be left to the high state
value = JtagController.TRST_BIT
cmd = bytearray((Ftdi.SET_BITS_LOW, value, self.direction))
self._ftdi.write_data(cmd)
sleep(0.1)
# TAP reset (even with HW reset, could be removed though)
self.write_tms(BitSequence('11111'))
if sync:
self.sync()
def sync(self) -> None:
if not self._ftdi.is_connected:
raise JtagError("FTDI controller terminated")
if self._write_buff:
self._ftdi.write_data(self._write_buff)
self._write_buff = bytearray()
def write_tms(self, tms: BitSequence,
should_read: bool = False) -> None:
"""Change the TAP controller state"""
if not isinstance(tms, BitSequence):
raise JtagError('Expect a BitSequence')
length = len(tms)
if not 0 < length < 8:
raise JtagError('Invalid TMS length')
out = BitSequence(tms, length=8)
# apply the last TDO bit
if self._last is not None:
out[7] = self._last
# print("TMS", tms, (self._last is not None) and 'w/ Last' or '')
# reset last bit
self._last = None
if should_read:
cmd = bytearray((Ftdi.RW_BITS_TMS_PVE_NVE, length-1, out.tobyte()))
else:
cmd = bytearray((Ftdi.WRITE_BITS_TMS_NVE, length-1, out.tobyte()))
self._stack_cmd(cmd)
if should_read:
self.sync()
def read(self, length: int) -> BitSequence:
"""Read out a sequence of bits from TDO."""
byte_count = length//8
bit_count = length-8*byte_count
bs = BitSequence()
if byte_count:
bytes_ = self._read_bytes(byte_count)
bs.append(bytes_)
if bit_count:
bits = self._read_bits(bit_count)
bs.append(bits)
return bs
def write(self, out: Union[BitSequence, str], use_last: bool = True):
"""Write a sequence of bits to TDI"""
if isinstance(out, str):
if len(out) > 1:
self._write_bytes_raw(out[:-1])
out = out[-1]
out = BitSequence(bytes_=out)
elif not isinstance(out, BitSequence):
out = BitSequence(out)
if use_last:
(out, self._last) = (out[:-1], bool(out[-1]))
byte_count = len(out)//8
pos = 8*byte_count
bit_count = len(out)-pos
if byte_count:
self._write_bytes(out[:pos])
if bit_count:
self._write_bits(out[pos:])
def write_with_read(self, out: BitSequence,
use_last: bool = False) -> int:
"""Write the given BitSequence while reading the same
number of bits into the FTDI read buffer.
Returns the number of bits written."""
if not isinstance(out, BitSequence):
return JtagError('Expect a BitSequence')
if use_last:
(out, self._last) = (out[:-1], int(out[-1]))
byte_count = len(out)//8
pos = 8*byte_count
bit_count = len(out)-pos
if not byte_count and not bit_count:
raise JtagError("Nothing to shift")
if byte_count:
blen = byte_count-1
# print("RW OUT %s" % out[:pos])
cmd = bytearray((Ftdi.RW_BYTES_PVE_NVE_LSB,
blen, (blen >> 8) & 0xff))
cmd.extend(out[:pos].tobytes(msby=True))
self._stack_cmd(cmd)
# print("push %d bytes" % byte_count)
if bit_count:
# print("RW OUT %s" % out[pos:])
cmd = bytearray((Ftdi.RW_BITS_PVE_NVE_LSB, bit_count-1))
cmd.append(out[pos:].tobyte())
self._stack_cmd(cmd)
# print("push %d bits" % bit_count)
return len(out)
def read_from_buffer(self, length) -> BitSequence:
"""Read the specified number of bits from the FTDI read buffer."""
self.sync()
bs = BitSequence()
byte_count = length//8
pos = 8*byte_count
bit_count = length-pos
if byte_count:
data = self._ftdi.read_data_bytes(byte_count, 4)
if not data:
raise JtagError('Unable to read data from FTDI')
byteseq = BitSequence(bytes_=data, length=8*byte_count)
# print("RW IN %s" % byteseq)
bs.append(byteseq)
# print("pop %d bytes" % byte_count)
if bit_count:
data = self._ftdi.read_data_bytes(1, 4)
if not data:
raise JtagError('Unable to read data from FTDI')
byte = data[0]
# need to shift bits as they are shifted in from the MSB in FTDI
byte >>= 8-bit_count
bitseq = BitSequence(byte, length=bit_count)
bs.append(bitseq)
# print("pop %d bits" % bit_count)
if len(bs) != length:
raise ValueError("Internal error")
return bs
@property
def ftdi(self) -> Ftdi:
"""Return the Ftdi instance.
:return: the Ftdi instance
"""
return self._ftdi
def _stack_cmd(self, cmd: Union[bytes, bytearray]):
if not isinstance(cmd, (bytes, bytearray)):
raise TypeError('Expect bytes or bytearray')
if not self._ftdi:
raise JtagError("FTDI controller terminated")
# Currrent buffer + new command + send_immediate
if (len(self._write_buff)+len(cmd)+1) >= JtagController.FTDI_PIPE_LEN:
self.sync()
self._write_buff.extend(cmd)
def _read_bits(self, length: int):
"""Read out bits from TDO"""
if length > 8:
raise JtagError("Cannot fit into FTDI fifo")
cmd = bytearray((Ftdi.READ_BITS_NVE_LSB, length-1))
self._stack_cmd(cmd)
self.sync()
data = self._ftdi.read_data_bytes(1, 4)
# need to shift bits as they are shifted in from the MSB in FTDI
byte = data[0] >> 8-length
bs = BitSequence(byte, length=length)
# print("READ BITS %s" % bs)
return bs
def _write_bits(self, out: BitSequence) -> None:
"""Output bits on TDI"""
length = len(out)
byte = out.tobyte()
# print("WRITE BITS %s" % out)
cmd = bytearray((Ftdi.WRITE_BITS_NVE_LSB, length-1, byte))
self._stack_cmd(cmd)
def _read_bytes(self, length: int) -> BitSequence:
"""Read out bytes from TDO"""
if length > JtagController.FTDI_PIPE_LEN:
raise JtagError("Cannot fit into FTDI fifo")
alen = length-1
cmd = bytearray((Ftdi.READ_BYTES_NVE_LSB, alen & 0xff,
(alen >> 8) & 0xff))
self._stack_cmd(cmd)
self.sync()
data = self._ftdi.read_data_bytes(length, 4)
bs = BitSequence(bytes_=data, length=8*length)
# print("READ BYTES %s" % bs)
return bs
def _write_bytes(self, out: BitSequence):
"""Output bytes on TDI"""
bytes_ = out.tobytes(msby=True) # don't ask...
olen = len(bytes_)-1
# print("WRITE BYTES %s" % out)
cmd = bytearray((Ftdi.WRITE_BYTES_NVE_LSB, olen & 0xff,
(olen >> 8) & 0xff))
cmd.extend(bytes_)
self._stack_cmd(cmd)
def _write_bytes_raw(self, out: BitSequence):
"""Output bytes on TDI"""
olen = len(out)-1
cmd = bytearray((Ftdi.WRITE_BYTES_NVE_LSB, olen & 0xff,
(olen >> 8) & 0xff))
cmd.extend(out)
self._stack_cmd(cmd)
class JtagEngine:
"""High-level JTAG engine controller"""
def __init__(self, trst: bool = False, frequency: float = 3E06):
self._ctrl = JtagController(trst, frequency)
self._sm = JtagStateMachine()
self._seq = bytearray()
@property
def state_machine(self):
return self._sm
@property
def controller(self):
return self._ctrl
def configure(self, url: str) -> None:
"""Configure the FTDI interface as a JTAG controller"""
self._ctrl.configure(url)
def close(self, freeze: bool = False) -> None:
"""Terminate a JTAG session/connection.
:param freeze: if set, FTDI port is not reset to its default
state on close.
"""
self._ctrl.close(freeze)
def purge(self) -> None:
"""Purge low level HW buffers"""
self._ctrl.purge()
def reset(self) -> None:
"""Reset the attached TAP controller"""
self._ctrl.reset()
self._sm.reset()
def write_tms(self, out, should_read=False) -> None:
"""Change the TAP controller state"""
self._ctrl.write_tms(out, should_read=should_read)
def read(self, length):
"""Read out a sequence of bits from TDO"""
return self._ctrl.read(length)
def write(self, out, use_last=False) -> None:
"""Write a sequence of bits to TDI"""
self._ctrl.write(out, use_last)
def get_available_statenames(self):
"""Return a list of supported state name"""
return [str(s) for s in self._sm.states]
def change_state(self, statename) -> None:
"""Advance the TAP controller to the defined state"""
# find the state machine path to move to the new instruction
path = self._sm.find_path(statename)
# convert the path into an event sequence
events = self._sm.get_events(path)
# update the remote device tap controller
self._ctrl.write_tms(events)
# update the current state machine's state
self._sm.handle_events(events)
def go_idle(self) -> None:
"""Change the current TAP controller to the IDLE state"""
self.change_state('run_test_idle')
def write_ir(self, instruction) -> None:
"""Change the current instruction of the TAP controller"""
self.change_state('shift_ir')
self._ctrl.write(instruction)
self.change_state('update_ir')
def capture_ir(self) -> None:
"""Capture the current instruction from the TAP controller"""
self.change_state('capture_ir')
def write_dr(self, data) -> None:
"""Change the data register of the TAP controller"""
self.change_state('shift_dr')
self._ctrl.write(data)
self.change_state('update_dr')
def read_dr(self, length: int) -> BitSequence:
"""Read the data register from the TAP controller"""
self.change_state('shift_dr')
data = self._ctrl.read(length)
self.change_state('update_dr')
return data
def capture_dr(self) -> None:
"""Capture the current data register from the TAP controller"""
self.change_state('capture_dr')
def sync(self) -> None:
self._ctrl.sync()
def shift_register(self, out: BitSequence) -> BitSequence:
if not self._sm.state_of('shift'):
raise JtagError(f'Invalid state: {self._sm.state()}')
if self._sm.state_of('capture'):
bs = BitSequence(False)
self._ctrl.write_tms(bs)
self._sm.handle_events(bs)
bits_out = self._ctrl.write_with_read(out)
bs = self._ctrl.read_from_buffer(bits_out)
if len(bs) != len(out):
raise ValueError("Internal error")
return bs
def shift_and_update_register(self, out: BitSequence) -> BitSequence:
"""Shift a BitSequence into the current register and retrieve the
register output, advancing the state to update_*r"""
if not self._sm.state_of('shift'):
raise JtagError(f'Invalid state: {self._sm.state()}')
if self._sm.state_of('capture'):
bs = BitSequence(False)
self._ctrl.write_tms(bs)
self._sm.handle_events(bs)
# Write with read using the last bit for next TMS transition
bits_out = self._ctrl.write_with_read(out, use_last=True)
# Advance the state from shift to update
events = BitSequence('11')
self.write_tms(events, should_read=True)
# (write_tms calls sync())
# update the current state machine's state
self._sm.handle_events(events)
# Read the bits clocked out as part of the initial write
bs = self._ctrl.read_from_buffer(bits_out)
# Read the last two bits clocked out with TMS above
# (but only use the lowest bit in the return data)
last_bits = self._ctrl.read_from_buffer(2)
bs.append(BitSequence((last_bits.tobyte() & 0x1), length=1))
if len(bs) != len(out):
raise ValueError("Internal error")
return bs
class JtagTool:
"""A helper class with facility functions"""
def __init__(self, engine):
self._engine = engine
def idcode(self) -> None:
idcode = self._engine.read_dr(32)
self._engine.go_idle()
return int(idcode)
def preload(self, bsdl, data) -> None:
instruction = bsdl.get_jtag_ir('preload')
self._engine.write_ir(instruction)
self._engine.write_dr(data)
self._engine.go_idle()
def sample(self, bsdl):
instruction = bsdl.get_jtag_ir('sample')
self._engine.write_ir(instruction)
data = self._engine.read_dr(bsdl.get_boundary_length())
self._engine.go_idle()
return data
def extest(self, bsdl) -> None:
instruction = bsdl.get_jtag_ir('extest')
self._engine.write_ir(instruction)
def readback(self, bsdl):
data = self._engine.read_dr(bsdl.get_boundary_length())
self._engine.go_idle()
return data
def detect_register_size(self) -> int:
# Freely inpired from UrJTAG
stm = self._engine.state_machine
if not stm.state_of('shift'):
raise JtagError(f'Invalid state: {stm.state()}')
if stm.state_of('capture'):
bs = BitSequence(False)
self._engine.controller.write_tms(bs)
stm.handle_events(bs)
MAX_REG_LEN = 1024
PATTERN_LEN = 8
stuck = None
for length in range(1, MAX_REG_LEN):
print(f'Testing for length {length}')
if length > 5:
raise ValueError(f'Abort detection over reg length {length}')
zero = BitSequence(length=length)
inj = BitSequence(length=length+PATTERN_LEN)
inj.inc()
ok = False
for _ in range(1, 1 << PATTERN_LEN):
ok = False
self._engine.write(zero, False)
rcv = self._engine.shift_register(inj)
try:
tdo = rcv.invariant()
except ValueError:
tdo = None
if stuck is None:
stuck = tdo
if stuck != tdo:
stuck = None
rcv >>= length
if rcv == inj:
ok = True
else:
break
inj.inc()
if ok:
print(f'Register detected length: {length}')
return length
if stuck is not None:
raise JtagError('TDO seems to be stuck')
raise JtagError('Unable to detect register length')

View file

@ -0,0 +1,349 @@
# Copyright (c) 2010-2024 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2008-2016, Neotion
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Miscellaneous helpers"""
# pylint: disable=invalid-name
# pylint: disable=import-outside-toplevel
from array import array
from copy import deepcopy
from re import match
from typing import Any, Iterable, Optional, Sequence, Union
# String values evaluated as true boolean values
TRUE_BOOLEANS = ['on', 'true', 'enable', 'enabled', 'yes', 'high', '1']
# String values evaluated as false boolean values
FALSE_BOOLEANS = ['off', 'false', 'disable', 'disabled', 'no', 'low', '0']
# ASCII or '.' filter
ASCIIFILTER = ''.join([((len(repr(chr(_x))) == 3) or (_x == 0x5c)) and chr(_x)
or '.' for _x in range(128)]) + '.' * 128
ASCIIFILTER = bytearray(ASCIIFILTER.encode('ascii'))
def hexdump(data: Union[bytes, bytearray, Iterable[int]],
full: bool = False, abbreviate: bool = False) -> str:
"""Convert a binary buffer into a hexadecimal representation.
Return a multi-line strings with hexadecimal values and ASCII
representation of the buffer data.
:param data: binary buffer to dump
:param full: use `hexdump -Cv` format
:param abbreviate: replace identical lines with '*'
:return: the generated string
"""
try:
if isinstance(data, (bytes, array)):
src = bytearray(data)
elif not isinstance(data, bytearray):
# data may be a list/tuple
src = bytearray(b''.join(data))
else:
src = data
except Exception as exc:
raise TypeError(f"Unsupported data type '{type(data)}'") from exc
length = 16
result = []
last = b''
abv = False
for i in range(0, len(src), length):
s = src[i:i+length]
if abbreviate:
if s == last:
if not abv:
result.append('*\n')
abv = True
continue
abv = False
hexa = ' '.join((f'{x:02x}' for x in s))
printable = s.translate(ASCIIFILTER).decode('ascii')
if full:
hx1, hx2 = hexa[:3*8], hexa[3*8:]
hl = length//2
result.append(f'{i:08x} {hx1:<{hl*3}} {hx2:<{hl*3}} '
f'|{printable}|\n')
else:
result.append(f'{i:06x} {hexa:<{length*3}} {printable}\n')
last = s
return ''.join(result)
def hexline(data: Union[bytes, bytearray, Iterable[int]],
sep: str = ' ') -> str:
"""Convert a binary buffer into a hexadecimal representation.
Return a string with hexadecimal values and ASCII representation
of the buffer data.
:param data: binary buffer to dump
:param sep: the separator string/char
:return: the formatted string
"""
try:
if isinstance(data, (bytes, array)):
src = bytearray(data)
elif not isinstance(data, bytearray):
# data may be a list/tuple
src = bytearray(b''.join(data))
else:
src = data
except Exception as exc:
raise TypeError(f"Unsupported data type '{type(data)}'") from exc
hexa = sep.join((f'{x:02x}' for x in src))
printable = src.translate(ASCIIFILTER).decode('ascii')
return f'({len(data)}) {hexa} : {printable}'
def to_int(value: Union[int, str]) -> int:
"""Parse a value and convert it into an integer value if possible.
Input value may be:
- a string with an integer coded as a decimal value
- a string with an integer coded as a hexadecimal value
- a integral value
- a integral value with a unit specifier (kilo or mega)
:param value: input value to convert to an integer
:return: the value as an integer
:rtype: int
:raise ValueError: if the input value cannot be converted into an int
"""
if not value:
return 0
if isinstance(value, int):
return value
mo = match(r'^\s*(\d+)\s*(?:([KMkm]i?)?B?)?\s*$', value)
if mo:
mult = {'K': (1000),
'KI': (1 << 10),
'M': (1000 * 1000),
'MI': (1 << 20)}
value = int(mo.group(1))
if mo.group(2):
value *= mult[mo.group(2).upper()]
return value
return int(value.strip(), value.startswith('0x') and 16 or 10)
def to_bool(value: Union[int, bool, str], permissive: bool = True,
allow_int: bool = False) -> bool:
"""Parse a string and convert it into a boolean value if possible.
Input value may be:
- a string with an integer value, if `allow_int` is enabled
- a boolean value
- a string with a common boolean definition
:param value: the value to parse and convert
:param permissive: default to the False value if parsing fails
:param allow_int: allow an integral type as the input value
:raise ValueError: if the input value cannot be converted into an bool
"""
if value is None:
return False
if isinstance(value, bool):
return value
if isinstance(value, int):
if allow_int:
return bool(value)
if permissive:
return False
raise ValueError(f"Invalid boolean value: '{value}'")
if value.lower() in TRUE_BOOLEANS:
return True
if permissive or (value.lower() in FALSE_BOOLEANS):
return False
raise ValueError(f"Invalid boolean value: '{value}'")
def to_bps(value: str) -> int:
"""Parse a string and convert it into a baudrate value.
The function accepts common multipliers as K, M and G
:param value: the value to parse and convert
:type value: str or int or float
:rtype: float
:raise ValueError: if the input value cannot be converted into a float
"""
if isinstance(value, float):
return int(value)
if isinstance(value, int):
return value
mo = match(r'^(?P<value>[-+]?[0-9]*\.?[0-9]+(?:[Ee][-+]?[0-9]+)?)'
r'(?P<unit>[KkMmGg])?$', value)
if not mo:
raise ValueError('Invalid frequency')
frequency = float(mo.group(1))
if mo.group(2):
mult = {'K': 1E3, 'M': 1E6, 'G': 1E9}
frequency *= mult[mo.group(2).upper()]
return int(frequency)
def xor(_a_: bool, _b_: bool) -> bool:
"""XOR logical operation.
:param _a_: first argument
:param _b_: second argument
:return: xor-ed value
"""
# pylint: disable=superfluous-parens
return bool((not (_a_) and _b_) or (_a_ and not (_b_)))
def is_iterable(obj: Any) -> bool:
"""Tells whether an instance is iterable or not.
:param obj: the instance to test
:type obj: object
:return: True if the object is iterable
:rtype: bool
"""
try:
iter(obj)
return True
except TypeError:
return False
def pretty_size(size, sep: str = ' ',
lim_k: int = 1 << 10, lim_m: int = 10 << 20,
plural: bool = True, floor: bool = True) -> str:
"""Convert a size into a more readable unit-indexed size (KiB, MiB)
:param size: integral value to convert
:param sep: the separator character between the integral value and
the unit specifier
:param lim_k: any value above this limit is a candidate for KiB
conversion.
:param lim_m: any value above this limit is a candidate for MiB
conversion.
:param plural: whether to append a final 's' to byte(s)
:param floor: how to behave when exact conversion cannot be
achieved: take the closest, smaller value or fallback to the next
unit that allows the exact representation of the input value
:return: the prettyfied size
"""
size = int(size)
if size > lim_m:
ssize = size >> 20
if floor or (ssize << 20) == size:
return f'{ssize}{sep}MiB'
if size > lim_k:
ssize = size >> 10
if floor or (ssize << 10) == size:
return f'{ssize}{sep}KiB'
return f'{size}{sep}byte{plural and "s" or ""}'
def add_custom_devices(ftdicls=None,
vpstr: Optional[Sequence[str]] = None,
force_hex: bool = False) -> None:
"""Helper function to add custom VID/PID to FTDI device identifer map.
The string to parse should match the following format:
[vendor_name=]<vendor_id>:[product_name=]<product_id>
* vendor_name and product_name are optional strings, they may be omitted
as they only serve as human-readable aliases for vendor and product
names.
* vendor_id and product_id are mandatory strings that should resolve
as 16-bit integer (USB VID and PID values). They may be expressed as
decimal or hexadecimal syntax.
ex:
* ``0x403:0x9999``, vid:pid short syntax, with no alias names
* ``mycompany=0x666:myproduct=0xcafe``, vid:pid complete syntax with
aliases
:param vpstr: typically, a option switch string describing the device
to add
:param ftdicls: the Ftdi class that should support the new device.
:param force_hex: if set, consider that the pid/vid string are
hexadecimal encoded values.
"""
from inspect import isclass
if not isclass(ftdicls):
raise ValueError('Expect Ftdi class, not instance')
for vidpid in vpstr or []:
vidpids = {vid: set() for vid in ftdicls.PRODUCT_IDS}
vname = ''
pname = ''
try:
vid, pid = vidpid.split(':')
if '=' in vid:
vname, vid = vid.split('=', 1)
if '=' in pid:
pname, pid = pid.split('=', 1)
if force_hex:
vid, pid = [int(v, 16) for v in (vid, pid)]
else:
vid, pid = [to_int(v) for v in (vid, pid)]
except ValueError as exc:
raise ValueError('Invalid VID:PID value') from exc
if vid not in vidpids:
ftdicls.add_custom_vendor(vid, vname)
vidpids[vid] = set()
if pid not in vidpids[vid]:
ftdicls.add_custom_product(vid, pid, pname)
vidpids[vid].add(pid)
def show_call_stack():
"""Print the current call stack, only useful for debugging purpose."""
from sys import _current_frames
from threading import current_thread
from traceback import print_stack
print_stack(_current_frames()[current_thread().ident])
class classproperty(property):
"""Getter property decorator for a class"""
# pylint: disable=invalid-name
def __get__(self, obj: Any, objtype=None) -> Any:
return super().__get__(objtype)
class EasyDict(dict):
"""Dictionary whose members can be accessed as instance members
"""
def __init__(self, dictionary=None, **kwargs):
super().__init__(self)
if dictionary is not None:
self.update(dictionary)
self.update(kwargs)
def __getattr__(self, name):
try:
return self.__getitem__(name)
except KeyError as exc:
raise AttributeError(f"'{self.__class__.__name__}' object has no "
f"attribute '{name}'") from exc
def __setattr__(self, name, value):
self.__setitem__(name, value)
@classmethod
def copy(cls, dictionary):
def _deep_copy(obj):
if isinstance(obj, list):
return [_deep_copy(v) for v in obj]
if isinstance(obj, dict):
return EasyDict({k: _deep_copy(obj[k]) for k in obj})
return deepcopy(obj)
return cls(_deep_copy(dictionary))
def mirror(self) -> 'EasyDict':
"""Instanciate a mirror EasyDict."""
return EasyDict({v: k for k, v in self.items()})

View file

@ -0,0 +1,33 @@
# Copyright (c) 2010-2024 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2008-2015, Neotion
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Serial modules compliant with pyserial APIs
"""
try:
from serial.serialutil import SerialException
except ImportError as exc:
raise ImportError("Python serial module not installed") from exc
try:
from serial import VERSION, serial_for_url as serial4url
version = tuple(int(x) for x in VERSION.split('.'))
if version < (3, 0):
raise ValueError
except (ValueError, IndexError, ImportError) as exc:
raise ImportError("pyserial 3.0+ is required") from exc
try:
from serial import protocol_handler_packages
protocol_handler_packages.append('pyftdi.serialext')
except ImportError as exc:
raise SerialException('Cannot register pyftdi extensions') from exc
serial_for_url = serial4url
def touch():
"""Do nothing, only for static checkers than do not like module import
with no module references
"""

View file

@ -0,0 +1,178 @@
# Copyright (c) 2010-2024 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2008-2016, Neotion
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
# pylint: disable=broad-except
# pylint: disable=invalid-name
# pylint: disable=missing-function-docstring
# pylint: disable=missing-module-docstring
# pylint: disable=no-member
# pylint: disable=super-with-arguments
from sys import stderr
from time import time
from ..misc import hexdump
__all__ = ['SerialLogger']
class SerialLogger:
"""Serial port wrapper to log input/output data to a log file.
"""
def __init__(self, *args, **kwargs):
logpath = kwargs.pop('logfile', None)
if not logpath:
raise ValueError('Missing logfile')
try:
# pylint: disable=consider-using-with
self._logger = open(logpath, "wt")
except IOError as exc:
print(f'Cannot log data to {logpath}: {exc}', file=stderr)
self._last = time()
self._log_init(*args, **kwargs)
super(SerialLogger, self).__init__(*args, **kwargs)
def open(self,):
self._log_open()
super(SerialLogger, self).open()
def close(self):
self._log_close()
self._logger.close()
super(SerialLogger, self).close()
def read(self, size=1):
data = super(SerialLogger, self).read(size)
self._log_read(data)
return data
def write(self, data):
if data:
self._log_write(data)
super(SerialLogger, self).write(data)
def flush(self):
self._log_flush()
super(SerialLogger, self).flush()
def reset_input_buffer(self):
self._log_reset('I')
super(SerialLogger, self).reset_input_buffer()
def reset_output_buffer(self):
self._log_reset('O')
super(SerialLogger, self).reset_output_buffer()
def send_break(self, duration=0.25):
self._log_signal('BREAK', f'for {duration:.3f}')
super(SerialLogger, self).send_break()
def _update_break_state(self):
self._log_signal('BREAK', self._break_state)
super(SerialLogger, self)._update_break_state()
def _update_rts_state(self):
self._log_signal('RTS', self._rts_state)
super(SerialLogger, self)._update_rts_state()
def _update_dtr_state(self):
self._log_signal('DTR', self._dtr_state)
super(SerialLogger, self)._update_dtr_state()
@property
def cts(self):
level = super(SerialLogger, self).cts
self._log_signal('CTS', level)
return level
@property
def dsr(self):
level = super(SerialLogger, self).dsr
self._log_signal('DSR', level)
return level
@property
def ri(self):
level = super(SerialLogger, self).ri
self._log_signal('RI', level)
return level
@property
def cd(self):
level = super(SerialLogger, self).cd
self._log_signal('CD', level)
return level
def in_waiting(self):
count = super(SerialLogger, self).in_waiting()
self._log_waiting(count)
return count
def _print(self, header, string=None):
if self._logger:
now = time()
delta = (now-self._last)*1000
self._last = now
print(f'{header} ({delta:3.3f} ms):\n{string or ""}',
file=self._logger)
self._logger.flush()
def _log_init(self, *args, **kwargs):
try:
sargs = ', '.join(args)
skwargs = ', '.join({f'{it[0]}={it[1]}' for it in kwargs.items()})
self._print('NEW', f' args: {sargs} {skwargs}')
except Exception as exc:
print(f'Cannot log init ({exc})', file=stderr)
def _log_open(self):
try:
self._print('OPEN')
except Exception as exc:
print(f'Cannot log open ({exc})', file=stderr)
def _log_close(self):
try:
self._print('CLOSE')
except Exception as exc:
print(f'Cannot log close ({exc})', file=stderr)
def _log_read(self, data):
try:
self._print('READ', hexdump(data))
except Exception as exc:
print(f'Cannot log input data ({exc})', file=stderr)
def _log_write(self, data):
try:
self._print('WRITE', hexdump(data))
except Exception as exc:
print(f'Cannot log output data ({exc})', data, file=stderr)
def _log_flush(self):
try:
self._print('FLUSH')
except Exception as exc:
print(f'Cannot log flush action ({exc})', file=stderr)
def _log_reset(self, type_):
try:
self._print('RESET BUFFER', type_)
except Exception as exc:
print(f'Cannot log reset buffer ({exc})', file=stderr)
def _log_waiting(self, count):
try:
self._print('INWAITING', f'{count}')
except Exception as exc:
print(f'Cannot log inwaiting ({exc})', file=stderr)
def _log_signal(self, name, value):
try:
self._print(name.upper(), str(value))
except Exception as exc:
print(f'Cannot log {name} ({exc})', file=stderr)

View file

@ -0,0 +1,196 @@
# Copyright (c) 2008-2024, Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2008-2016, Neotion
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
# this file has not been updated for a while, so coding style needs some love
# pylint: disable=attribute-defined-outside-init
# pylint: disable=invalid-name
# pylint: disable=missing-class-docstring
# pylint: disable=missing-module-docstring
from io import RawIOBase
from time import sleep, time as now
from serial import SerialBase, SerialException, VERSION as pyserialver
from pyftdi.ftdi import Ftdi
from pyftdi.usbtools import UsbToolsError
class FtdiSerial(SerialBase):
"""Base class for Serial port implementation compatible with pyserial API
using a USB device.
"""
BAUDRATES = sorted([9600 * (x+1) for x in range(6)] +
list(range(115200, 1000000, 115200)) +
list(range(1000000, 13000000, 100000)))
PYSERIAL_VERSION = tuple(int(x) for x in pyserialver.split('.'))
def open(self):
"""Open the initialized serial port"""
if self.port is None:
raise SerialException("Port must be configured before use.")
try:
device = Ftdi.create_from_url(self.port)
except (UsbToolsError, IOError) as exc:
raise SerialException(f'Unable to open USB port {self.portstr}: '
f'{exc}') from exc
self.udev = device
self._set_open_state(True)
self._reconfigure_port()
def close(self):
"""Close the open port"""
self._set_open_state(False)
if self.udev:
self.udev.close()
self.udev = None
def read(self, size=1):
"""Read size bytes from the serial port. If a timeout is set it may
return less characters as requested. With no timeout it will block
until the requested number of bytes is read."""
data = bytearray()
start = now()
while True:
buf = self.udev.read_data(size)
data.extend(buf)
size -= len(buf)
if size <= 0:
break
if self._timeout is not None:
if buf:
break
ms = now()-start
if ms > self._timeout:
break
sleep(0.01)
return bytes(data)
def write(self, data):
"""Output the given string over the serial port."""
return self.udev.write_data(data)
def flush(self):
"""Flush of file like objects. In this case, wait until all data
is written."""
def reset_input_buffer(self):
"""Clear input buffer, discarding all that is in the buffer."""
self.udev.purge_rx_buffer()
def reset_output_buffer(self):
"""Clear output buffer, aborting the current output and
discarding all that is in the buffer."""
self.udev.purge_tx_buffer()
def send_break(self, duration=0.25):
"""Send break condition."""
self.udev.set_break(True)
sleep(duration)
self.udev.set_break(False)
def _update_break_state(self):
"""Send break condition. Not supported"""
self.udev.set_break(self._break_state)
def _update_rts_state(self):
"""Set terminal status line: Request To Send"""
self.udev.set_rts(self._rts_state)
def _update_dtr_state(self):
"""Set terminal status line: Data Terminal Ready"""
self.udev.set_dtr(self._dtr_state)
@property
def ftdi(self) -> Ftdi:
"""Return the Ftdi instance.
:return: the Ftdi instance
"""
return self.udev
@property
def usb_path(self):
"""Return the physical location as a triplet.
* bus is the USB bus
* address is the address on the USB bus
* interface is the interface number on the FTDI debice
:return: (bus, address, interface)
:rtype: tuple(int)
"""
return self.udev.usb_path
@property
def cts(self):
"""Read terminal status line: Clear To Send"""
return self.udev.get_cts()
@property
def dsr(self):
"""Read terminal status line: Data Set Ready"""
return self.udev.get_dsr()
@property
def ri(self):
"""Read terminal status line: Ring Indicator"""
return self.udev.get_ri()
@property
def cd(self):
"""Read terminal status line: Carrier Detect"""
return self.udev.get_cd()
@property
def in_waiting(self):
"""Return the number of characters currently in the input buffer."""
# not implemented
return 0
@property
def out_waiting(self):
"""Return the number of bytes currently in the output buffer."""
return 0
@property
def fifoSizes(self):
"""Return the (TX, RX) tupple of hardware FIFO sizes"""
return self.udev.fifo_sizes
def _reconfigure_port(self):
try:
self._baudrate = self.udev.set_baudrate(self._baudrate, True)
self.udev.set_line_property(self._bytesize,
self._stopbits,
self._parity)
if self._rtscts:
self.udev.set_flowctrl('hw')
elif self._xonxoff:
self.udev.set_flowctrl('sw')
else:
self.udev.set_flowctrl('')
try:
self.udev.set_dynamic_latency(12, 200, 50)
except AttributeError:
# backend does not support this feature
pass
except IOError as exc:
err = self.udev.get_error_string()
raise SerialException(f'{exc} ({err})') from exc
def _set_open_state(self, open_):
self.is_open = bool(open_)
# assemble Serial class with the platform specific implementation and the base
# for file-like behavior.
class Serial(FtdiSerial, RawIOBase):
BACKEND = 'pyftdi'
def __init__(self, *args, **kwargs):
RawIOBase.__init__(self)
FtdiSerial.__init__(self, *args, **kwargs)

View file

@ -0,0 +1,213 @@
# Copyright (c) 2008-2024, Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2016, Emmanuel Bouaziz <ebouaziz@free.fr>
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
# this file has not been updated for a while, so coding style needs some love
# pylint: disable=broad-except
# pylint: disable=attribute-defined-outside-init
# pylint: disable=redefined-outer-name
# pylint: disable=invalid-name
# pylint: disable=missing-function-docstring
# pylint: disable=missing-class-docstring
# pylint: disable=missing-module-docstring
import errno
import os
import select
import socket
from io import RawIOBase
from serial import (SerialBase, SerialException, PortNotOpenError,
VERSION as pyserialver)
from ..misc import hexdump
__all__ = ['Serial']
class SerialExceptionWithErrno(SerialException):
"""Serial exception with errno extension"""
def __init__(self, message, errno=None):
SerialException.__init__(self, message)
self.errno = errno
class SocketSerial(SerialBase):
"""Fake serial port redirected to a Unix socket.
This is basically a copy of the serialposix serial port implementation
with redefined IO for a Unix socket"""
BACKEND = 'socket'
VIRTUAL_DEVICE = True
PYSERIAL_VERSION = tuple(int(x) for x in pyserialver.split('.'))
def _reconfigure_port(self):
pass
def open(self):
"""Open the initialized serial port"""
if self._port is None:
raise SerialException("Port must be configured before use.")
if self.isOpen():
raise SerialException("Port is already open.")
self._dump = False
self.sock = None
try:
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
filename = self.portstr[self.portstr.index('://')+3:]
if filename.startswith('~/'):
home = os.getenv('HOME')
if home:
filename = os.path.join(home, filename[2:])
self._filename = filename
self.sock.connect(self._filename)
except Exception as exc:
self.close()
msg = f'Could not open port: {exc}'
if isinstance(exc, socket.error):
# pylint: disable=no-member
raise SerialExceptionWithErrno(msg, exc.errno) from exc
raise SerialException(msg) from exc
self._set_open_state(True)
self._lastdtr = None
def close(self):
if self.sock:
try:
self.sock.shutdown(socket.SHUT_RDWR)
except Exception:
pass
try:
self.sock.close()
except Exception:
pass
self.sock = None
self._set_open_state(False)
def in_waiting(self):
"""Return the number of characters currently in the input buffer."""
return 0
def read(self, size=1):
"""Read size bytes from the serial port. If a timeout is set it may
return less characters as requested. With no timeout it will block
until the requested number of bytes is read."""
if self.sock is None:
raise PortNotOpenError
read = bytearray()
if size > 0:
while len(read) < size:
ready, _, _ = select.select([self.sock], [], [], self._timeout)
if not ready:
break # timeout
buf = self.sock.recv(size-len(read))
if not buf:
# Some character is ready, but none can be read
# it is a marker for a disconnected peer
raise PortNotOpenError
read += buf
if self._timeout >= 0 and not buf:
break # early abort on timeout
return read
def write(self, data):
"""Output the given string over the serial port."""
if self.sock is None:
raise PortNotOpenError
t = len(data)
d = data
while t > 0:
try:
if self.writeTimeout is not None and self.writeTimeout > 0:
_, ready, _ = select.select([], [self.sock], [],
self.writeTimeout)
if not ready:
raise TimeoutError()
n = self.sock.send(d)
if self._dump:
print(hexdump(d[:n]))
if self.writeTimeout is not None and self.writeTimeout > 0:
_, ready, _ = select.select([], [self.sock], [],
self.writeTimeout)
if not ready:
raise TimeoutError()
d = d[n:]
t = t - n
except OSError as e:
if e.errno != errno.EAGAIN:
raise
def flush(self):
"""Flush of file like objects. In this case, wait until all data
is written."""
def reset_input_buffer(self):
"""Clear input buffer, discarding all that is in the buffer."""
def reset_output_buffer(self):
"""Clear output buffer, aborting the current output and
discarding all that is in the buffer."""
def send_break(self, duration=0.25):
"""Send break condition. Not supported"""
def _update_break_state(self):
"""Send break condition. Not supported"""
def _update_rts_state(self):
"""Set terminal status line: Request To Send"""
def _update_dtr_state(self):
"""Set terminal status line: Data Terminal Ready"""
def setDTR(self, value=1):
"""Set terminal status line: Data Terminal Ready"""
@property
def cts(self):
"""Read terminal status line: Clear To Send"""
return True
@property
def dsr(self):
"""Read terminal status line: Data Set Ready"""
return True
@property
def ri(self):
"""Read terminal status line: Ring Indicator"""
return False
@property
def cd(self):
"""Read terminal status line: Carrier Detect"""
return False
# - - platform specific - - - -
def nonblocking(self):
"""internal - not portable!"""
if self.sock is None:
raise PortNotOpenError
self.sock.setblocking(0)
def dump(self, enable):
self._dump = enable
# - - Helpers - -
def _set_open_state(self, open_):
if self.PYSERIAL_VERSION < (3, 0):
self._isOpen = bool(open_)
else:
self.is_open = bool(open_)
# assemble Serial class with the platform specifc implementation and the base
# for file-like behavior.
class Serial(SocketSerial, RawIOBase):
pass

View file

@ -0,0 +1,949 @@
# Copyright (c) 2010-2024, Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2016, Emmanuel Bouaziz <ebouaziz@free.fr>
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""SPI support for PyFdti"""
# pylint: disable=invalid-name
from logging import getLogger
from struct import calcsize as scalc, pack as spack, unpack as sunpack
from threading import Lock
from typing import Any, Iterable, Mapping, Optional, Set, Union
from usb.core import Device as UsbDevice
from .ftdi import Ftdi, FtdiError
class SpiIOError(FtdiError):
"""SPI I/O error"""
class SpiPort:
"""SPI port
An SPI port is never instanciated directly: use
:py:meth:`SpiController.get_port()` method to obtain an SPI port.
Example:
>>> ctrl = SpiController()
>>> ctrl.configure('ftdi://ftdi:232h/1')
>>> spi = ctrl.get_port(1)
>>> spi.set_frequency(1000000)
>>> # send 2 bytes
>>> spi.exchange([0x12, 0x34])
>>> # send 2 bytes, then receive 2 bytes
>>> out = spi.exchange([0x12, 0x34], 2)
>>> # send 2 bytes, then receive 4 bytes, manage the transaction
>>> out = spi.exchange([0x12, 0x34], 2, True, False)
>>> out.extend(spi.exchange([], 2, False, True))
"""
def __init__(self, controller: 'SpiController', cs: int, cs_hold: int = 3,
spi_mode: int = 0):
self.log = getLogger('pyftdi.spi.port')
self._controller = controller
self._frequency = self._controller.frequency
self._cs = cs
self._cs_hold = cs_hold
self.set_mode(spi_mode)
def exchange(self, out: Union[bytes, bytearray, Iterable[int]] = b'',
readlen: int = 0, start: bool = True, stop: bool = True,
duplex: bool = False, droptail: int = 0) -> bytes:
"""Perform an exchange or a transaction with the SPI slave
:param out: data to send to the SPI slave, may be empty to read out
data from the slave with no write.
:param readlen: count of bytes to read out from the slave,
may be zero to only write to the slave
:param start: whether to start an SPI transaction, i.e.
activate the /CS line for the slave. Use False to
resume a previously started transaction
:param stop: whether to desactivete the /CS line for the slave.
Use False if the transaction should complete with a
further call to exchange()
:param duplex: perform a full-duplex exchange (vs. half-duplex),
i.e. bits are clocked in and out at once.
:param droptail: ignore up to 7 last bits (for non-byte sized SPI
accesses)
:return: an array of bytes containing the data read out from the
slave
"""
return self._controller.exchange(self._frequency, out, readlen,
start and self._cs_prolog,
stop and self._cs_epilog,
self._cpol, self._cpha,
duplex, droptail)
def read(self, readlen: int = 0, start: bool = True, stop: bool = True,
droptail: int = 0) -> bytes:
"""Read out bytes from the slave
:param readlen: count of bytes to read out from the slave,
may be zero to only write to the slave
:param start: whether to start an SPI transaction, i.e.
activate the /CS line for the slave. Use False to
resume a previously started transaction
:param stop: whether to desactivete the /CS line for the slave.
Use False if the transaction should complete with a
further call to exchange()
:param droptail: ignore up to 7 last bits (for non-byte sized SPI
accesses)
:return: an array of bytes containing the data read out from the
slave
"""
return self._controller.exchange(self._frequency, [], readlen,
start and self._cs_prolog,
stop and self._cs_epilog,
self._cpol, self._cpha, False,
droptail)
def write(self, out: Union[bytes, bytearray, Iterable[int]],
start: bool = True, stop: bool = True, droptail: int = 0) \
-> None:
"""Write bytes to the slave
:param out: data to send to the SPI slave, may be empty to read out
data from the slave with no write.
:param start: whether to start an SPI transaction, i.e.
activate the /CS line for the slave. Use False to
resume a previously started transaction
:param stop: whether to desactivete the /CS line for the slave.
Use False if the transaction should complete with a
further call to exchange()
:param droptail: ignore up to 7 last bits (for non-byte sized SPI
accesses)
"""
return self._controller.exchange(self._frequency, out, 0,
start and self._cs_prolog,
stop and self._cs_epilog,
self._cpol, self._cpha, False,
droptail)
def flush(self) -> None:
"""Force the flush of the HW FIFOs"""
self._controller.flush()
def set_frequency(self, frequency: float):
"""Change SPI bus frequency
:param float frequency: the new frequency in Hz
"""
self._frequency = min(frequency, self._controller.frequency_max)
def set_mode(self, mode: int, cs_hold: Optional[int] = None) -> None:
"""Set or change the SPI mode to communicate with the SPI slave.
:param mode: new SPI mode
:param cs_hold: change the /CS hold duration (or keep using previous
value)
"""
if not 0 <= mode <= 3:
raise SpiIOError(f'Invalid SPI mode: {mode}')
if (mode & 0x2) and not self._controller.is_inverted_cpha_supported:
raise SpiIOError('SPI with CPHA high is not supported by '
'this FTDI device')
if cs_hold is None:
cs_hold = self._cs_hold
else:
self._cs_hold = cs_hold
self._cpol = bool(mode & 0x2)
self._cpha = bool(mode & 0x1)
cs_clock = 0xFF & ~((int(not self._cpol) and SpiController.SCK_BIT) |
SpiController.DO_BIT)
cs_select = 0xFF & ~((SpiController.CS_BIT << self._cs) |
(int(not self._cpol) and SpiController.SCK_BIT) |
SpiController.DO_BIT)
self._cs_prolog = bytes([cs_clock, cs_select])
self._cs_epilog = bytes([cs_select] + [cs_clock] * int(cs_hold))
def force_select(self, level: Optional[bool] = None,
cs_hold: float = 0) -> None:
"""Force-drive /CS signal.
This API is not designed for a regular usage, but is reserved to
very specific slave devices that require non-standard SPI
signalling. There are very few use cases where this API is required.
:param level: level to force on /CS output. This is a tri-state
value. A boolean value forces the selected signal
level; note that SpiPort no longer enforces that
following API calls generates valid SPI signalling:
use with extreme care. `None` triggers a pulse on /CS
output, i.e. /CS is not asserted once the method
returns, whatever the actual /CS level when this API
is called.
:param cs_hold: /CS hold duration, as a unitless value. It is not
possible to control the exact duration of the pulse,
as it depends on the USB bus and the FTDI frequency.
"""
clk, sel = self._cs_prolog
if cs_hold:
hold = max(1, cs_hold)
if hold > SpiController.PAYLOAD_MAX_LENGTH:
raise ValueError('cs_hold is too long')
else:
hold = self._cs_hold
if level is None:
seq = bytearray([clk])
seq.extend([sel]*(1+hold))
seq.extend([clk]*self._cs_hold)
elif level:
seq = bytearray([clk] * hold)
else:
seq = bytearray([clk] * hold)
seq.extend([sel]*(1+hold))
self._controller.force_control(self._frequency, bytes(seq))
@property
def frequency(self) -> float:
"""Return the current SPI bus block"""
return self._frequency
@property
def cs(self) -> int:
"""Return the /CS index.
:return: the /CS index (starting from 0)
"""
return self._cs
@property
def mode(self) -> int:
"""Return the current SPI mode.
:return: the SPI mode
"""
return (int(self._cpol) << 2) | int(self._cpha)
class SpiGpioPort:
"""GPIO port
A SpiGpioPort instance enables to drive GPIOs wich are not reserved for
SPI feature as regular GPIOs.
GPIO are managed as a bitfield. The LSBs are reserved for the SPI
feature, which means that the lowest pin that can be used as a GPIO is
*b4*:
* *b0*: SPI SCLK
* *b1*: SPI MOSI
* *b2*: SPI MISO
* *b3*: SPI CS0
* *b4*: SPI CS1 or first GPIO
If more than one SPI device is used, less GPIO pins are available, see
the cs_count argument of the SpiController constructor.
There is no offset bias in GPIO bit position, *i.e.* the first available
GPIO can be reached from as ``0x10``.
Bitfield size depends on the FTDI device: 4432H series use 8-bit GPIO
ports, while 232H and 2232H series use wide 16-bit ports.
An SpiGpio port is never instanciated directly: use
:py:meth:`SpiController.get_gpio()` method to obtain the GPIO port.
"""
def __init__(self, controller: 'SpiController'):
self.log = getLogger('pyftdi.spi.gpio')
self._controller = controller
@property
def pins(self) -> int:
"""Report the configured GPIOs as a bitfield.
A true bit represents a GPIO, a false bit a reserved or not
configured pin.
:return: the bitfield of configured GPIO pins.
"""
return self._controller.gpio_pins
@property
def all_pins(self) -> int:
"""Report the addressable GPIOs as a bitfield.
A true bit represents a pin which may be used as a GPIO, a false bit
a reserved pin (for SPI support)
:return: the bitfield of configurable GPIO pins.
"""
return self._controller.gpio_all_pins
@property
def width(self) -> int:
"""Report the FTDI count of addressable pins.
Note that all pins, including reserved SPI ones, are reported.
:return: the count of IO pins (including SPI ones).
"""
return self._controller.width
@property
def direction(self) -> int:
"""Provide the FTDI GPIO direction.self
A true bit represents an output GPIO, a false bit an input GPIO.
:return: the bitfield of direction.
"""
return self._controller.direction
def read(self, with_output: bool = False) -> int:
"""Read GPIO port.
:param with_output: set to unmask output pins
:return: the GPIO port pins as a bitfield
"""
return self._controller.read_gpio(with_output)
def write(self, value: int) -> None:
"""Write GPIO port.
:param value: the GPIO port pins as a bitfield
"""
return self._controller.write_gpio(value)
def set_direction(self, pins: int, direction: int) -> None:
"""Change the direction of the GPIO pins.
:param pins: which GPIO pins should be reconfigured
:param direction: direction bitfield (high level for output)
"""
self._controller.set_gpio_direction(pins, direction)
class SpiController:
"""SPI master.
:param int cs_count: is the number of /CS lines (one per device to
drive on the SPI bus)
:param turbo: increase throughput over USB bus, but may not be
supported with some specific slaves
"""
SCK_BIT = 0x01
DO_BIT = 0x02
DI_BIT = 0x04
CS_BIT = 0x08
SPI_BITS = DI_BIT | DO_BIT | SCK_BIT
PAYLOAD_MAX_LENGTH = 0xFF00 # 16 bits max (- spare for control)
def __init__(self, cs_count: int = 1, turbo: bool = True):
self.log = getLogger('pyftdi.spi.ctrl')
self._ftdi = Ftdi()
self._lock = Lock()
self._gpio_port = None
self._gpio_dir = 0
self._gpio_mask = 0
self._gpio_low = 0
self._wide_port = False
self._cs_count = cs_count
self._turbo = turbo
self._immediate = bytes((Ftdi.SEND_IMMEDIATE,))
self._frequency = 0.0
self._clock_phase = False
self._cs_bits = 0
self._spi_ports = []
self._spi_dir = 0
self._spi_mask = self.SPI_BITS
def configure(self, url: Union[str, UsbDevice],
**kwargs: Mapping[str, Any]) -> None:
"""Configure the FTDI interface as a SPI master
:param url: FTDI URL string, such as ``ftdi://ftdi:232h/1``
:param kwargs: options to configure the SPI bus
Accepted options:
* ``interface``: when URL is specifed as a USB device, the interface
named argument can be used to select a specific port of the FTDI
device, as an integer starting from 1.
* ``direction`` a bitfield specifying the FTDI GPIO direction,
where high level defines an output, and low level defines an
input. Only useful to setup default IOs at start up, use
:py:class:`SpiGpioPort` to drive GPIOs. Note that pins reserved
for SPI feature take precedence over any this setting.
* ``initial`` a bitfield specifying the initial output value. Only
useful to setup default IOs at start up, use
:py:class:`SpiGpioPort` to drive GPIOs.
* ``frequency`` the SPI bus frequency in Hz. Note that each slave
may reconfigure the SPI bus with a specialized
frequency.
* ``cs_count`` count of chip select signals dedicated to select
SPI slave devices, starting from A*BUS3 pin
* ``turbo`` whether to enable or disable turbo mode
* ``debug`` to increase log verbosity, using MPSSE tracer
"""
# it is better to specify CS and turbo in configure, but the older
# API where these parameters are specified at instanciation has been
# preserved
if 'cs_count' in kwargs:
self._cs_count = int(kwargs['cs_count'])
del kwargs['cs_count']
if not 1 <= self._cs_count <= 5:
raise ValueError(f'Unsupported CS line count: {self._cs_count}')
if 'turbo' in kwargs:
self._turbo = bool(kwargs['turbo'])
del kwargs['turbo']
if 'direction' in kwargs:
io_dir = int(kwargs['direction'])
del kwargs['direction']
else:
io_dir = 0
if 'initial' in kwargs:
io_out = int(kwargs['initial'])
del kwargs['initial']
else:
io_out = 0
if 'interface' in kwargs:
if isinstance(url, str):
raise SpiIOError('url and interface are mutually exclusive')
interface = int(kwargs['interface'])
del kwargs['interface']
else:
interface = 1
with self._lock:
if self._frequency > 0.0:
raise SpiIOError('Already configured')
self._cs_bits = (((SpiController.CS_BIT << self._cs_count) - 1) &
~(SpiController.CS_BIT - 1))
self._spi_ports = [None] * self._cs_count
self._spi_dir = (self._cs_bits |
SpiController.DO_BIT |
SpiController.SCK_BIT)
self._spi_mask = self._cs_bits | self.SPI_BITS
# until the device is open, there is no way to tell if it has a
# wide (16) or narrow port (8). Lower API can deal with any, so
# delay any truncation till the device is actually open
self._set_gpio_direction(16, (~self._spi_mask) & 0xFFFF, io_dir)
kwargs['direction'] = self._spi_dir | self._gpio_dir
kwargs['initial'] = self._cs_bits | (io_out & self._gpio_mask)
if not isinstance(url, str):
self._frequency = self._ftdi.open_mpsse_from_device(
url, interface=interface, **kwargs)
else:
self._frequency = self._ftdi.open_mpsse_from_url(url, **kwargs)
self._ftdi.enable_adaptive_clock(False)
self._wide_port = self._ftdi.has_wide_port
if not self._wide_port:
self._set_gpio_direction(8, io_out & 0xFF, io_dir & 0xFF)
def close(self, freeze: bool = False) -> None:
"""Close the FTDI interface.
:param freeze: if set, FTDI port is not reset to its default
state on close.
"""
with self._lock:
if self._ftdi.is_connected:
self._ftdi.close(freeze)
self._frequency = 0.0
def terminate(self) -> None:
"""Close the FTDI interface.
:note: deprecated API, use close()
"""
self.close()
def get_port(self, cs: int, freq: Optional[float] = None,
mode: int = 0) -> SpiPort:
"""Obtain a SPI port to drive a SPI device selected by Chip Select.
:note: SPI mode 1 and 3 are not officially supported.
:param cs: chip select slot, starting from 0
:param freq: SPI bus frequency for this slave in Hz
:param mode: SPI mode [0, 1, 2, 3]
"""
with self._lock:
if not self._ftdi.is_connected:
raise SpiIOError("FTDI controller not initialized")
if cs >= len(self._spi_ports):
if cs < 5:
# increase cs_count (up to 4) to reserve more /CS channels
raise SpiIOError('/CS pin {cs} not reserved for SPI')
raise SpiIOError(f'No such SPI port: {cs}')
if not self._spi_ports[cs]:
freq = min(freq or self._frequency, self.frequency_max)
hold = freq and (1+int(1E6/freq))
self._spi_ports[cs] = SpiPort(self, cs, cs_hold=hold,
spi_mode=mode)
self._spi_ports[cs].set_frequency(freq)
self._flush()
return self._spi_ports[cs]
def get_gpio(self) -> SpiGpioPort:
"""Retrieve the GPIO port.
:return: GPIO port
"""
with self._lock:
if not self._ftdi.is_connected:
raise SpiIOError("FTDI controller not initialized")
if not self._gpio_port:
self._gpio_port = SpiGpioPort(self)
return self._gpio_port
@property
def ftdi(self) -> Ftdi:
"""Return the Ftdi instance.
:return: the Ftdi instance
"""
return self._ftdi
@property
def configured(self) -> bool:
"""Test whether the device has been properly configured.
:return: True if configured
"""
return self._ftdi.is_connected
@property
def frequency_max(self) -> float:
"""Provides the maximum SPI clock frequency in Hz.
:return: SPI bus clock frequency
"""
return self._ftdi.frequency_max
@property
def frequency(self) -> float:
"""Provides the current SPI clock frequency in Hz.
:return: the SPI bus clock frequency
"""
return self._frequency
@property
def direction(self):
"""Provide the FTDI pin direction
A true bit represents an output pin, a false bit an input pin.
:return: the bitfield of direction.
"""
return self._spi_dir | self._gpio_dir
@property
def channels(self) -> int:
"""Provide the maximum count of slaves.
:return: the count of pins reserved to drive the /CS signal
"""
return self._cs_count
@property
def active_channels(self) -> Set[int]:
"""Provide the set of configured slaves /CS.
:return: Set of /CS, one for each configured slaves
"""
return {port[0] for port in enumerate(self._spi_ports) if port[1]}
@property
def gpio_pins(self):
"""Report the configured GPIOs as a bitfield.
A true bit represents a GPIO, a false bit a reserved or not
configured pin.
:return: the bitfield of configured GPIO pins.
"""
with self._lock:
return self._gpio_mask
@property
def gpio_all_pins(self):
"""Report the addressable GPIOs as a bitfield.
A true bit represents a pin which may be used as a GPIO, a false bit
a reserved pin (for SPI support)
:return: the bitfield of configurable GPIO pins.
"""
mask = (1 << self.width) - 1
with self._lock:
return mask & ~self._spi_mask
@property
def width(self):
"""Report the FTDI count of addressable pins.
:return: the count of IO pins (including SPI ones).
"""
return 16 if self._wide_port else 8
@property
def is_inverted_cpha_supported(self) -> bool:
"""Report whether it is possible to supported CPHA=1.
:return: inverted CPHA supported (with a kludge)
"""
return self._ftdi.is_H_series
def exchange(self, frequency: float,
out: Union[bytes, bytearray, Iterable[int]], readlen: int,
cs_prolog: Optional[bytes] = None,
cs_epilog: Optional[bytes] = None,
cpol: bool = False, cpha: bool = False,
duplex: bool = False, droptail: int = 0) -> bytes:
"""Perform an exchange or a transaction with the SPI slave
:param out: data to send to the SPI slave, may be empty to read out
data from the slave with no write.
:param readlen: count of bytes to read out from the slave,
may be zero to only write to the slave,
:param cs_prolog: the prolog MPSSE command sequence to execute
before the actual exchange.
:param cs_epilog: the epilog MPSSE command sequence to execute
after the actual exchange.
:param cpol: SPI clock polarity, derived from the SPI mode
:param cpol: SPI clock phase, derived from the SPI mode
:param duplex: perform a full-duplex exchange (vs. half-duplex),
i.e. bits are clocked in and out at once or
in a write-then-read manner.
:param droptail: ignore up to 7 last bits (for non-byte sized SPI
accesses)
:return: bytes containing the data read out from the slave, if any
"""
if not 0 <= droptail <= 7:
raise ValueError('Invalid skip bit count')
if duplex:
if readlen > len(out):
tmp = bytearray(out)
tmp.extend([0] * (readlen - len(out)))
out = tmp
elif not readlen:
readlen = len(out)
with self._lock:
if duplex:
data = self._exchange_full_duplex(frequency, out,
cs_prolog, cs_epilog,
cpol, cpha, droptail)
return data[:readlen]
return self._exchange_half_duplex(frequency, out, readlen,
cs_prolog, cs_epilog,
cpol, cpha, droptail)
def force_control(self, frequency: float, sequence: bytes) -> None:
"""Execution an arbitrary SPI control bit sequence.
Use with extreme care, as it may lead to unexpected results. Regular
usage of SPI does not require to invoke this API.
:param sequence: the bit sequence to execute.
"""
with self._lock:
self._force(frequency, sequence)
def flush(self) -> None:
"""Flush the HW FIFOs.
"""
with self._lock:
self._flush()
def read_gpio(self, with_output: bool = False) -> int:
"""Read GPIO port
:param with_output: set to unmask output pins
:return: the GPIO port pins as a bitfield
"""
with self._lock:
data = self._read_raw(self._wide_port)
value = data & self._gpio_mask
if not with_output:
value &= ~self._gpio_dir
return value
def write_gpio(self, value: int) -> None:
"""Write GPIO port
:param value: the GPIO port pins as a bitfield
"""
with self._lock:
if (value & self._gpio_dir) != value:
raise SpiIOError(f'No such GPO pins: '
f'{self._gpio_dir:04x}/{value:04x}')
# perform read-modify-write
use_high = self._wide_port and (self.direction & 0xff00)
data = self._read_raw(use_high)
data &= ~self._gpio_mask
data |= value
self._write_raw(data, use_high)
self._gpio_low = data & 0xFF & ~self._spi_mask
def set_gpio_direction(self, pins: int, direction: int) -> None:
"""Change the direction of the GPIO pins
:param pins: which GPIO pins should be reconfigured
:param direction: direction bitfield (on for output)
"""
with self._lock:
self._set_gpio_direction(16 if self._wide_port else 8,
pins, direction)
def _set_gpio_direction(self, width: int, pins: int,
direction: int) -> None:
if pins & self._spi_mask:
raise SpiIOError('Cannot access SPI pins as GPIO')
gpio_mask = (1 << width) - 1
gpio_mask &= ~self._spi_mask
if (pins & gpio_mask) != pins:
raise SpiIOError('No such GPIO pin(s)')
self._gpio_dir &= ~pins
self._gpio_dir |= (pins & direction)
self._gpio_mask = gpio_mask & pins
def _read_raw(self, read_high: bool) -> int:
if not self._ftdi.is_connected:
raise SpiIOError("FTDI controller not initialized")
if read_high:
cmd = bytes([Ftdi.GET_BITS_LOW,
Ftdi.GET_BITS_HIGH,
Ftdi.SEND_IMMEDIATE])
fmt = '<H'
else:
cmd = bytes([Ftdi.GET_BITS_LOW,
Ftdi.SEND_IMMEDIATE])
fmt = 'B'
self._ftdi.write_data(cmd)
size = scalc(fmt)
data = self._ftdi.read_data_bytes(size, 4)
if len(data) != size:
raise SpiIOError('Cannot read GPIO')
value, = sunpack(fmt, data)
return value
def _write_raw(self, data: int, write_high: bool) -> None:
if not self._ftdi.is_connected:
raise SpiIOError("FTDI controller not initialized")
direction = self.direction
low_data = data & 0xFF
low_dir = direction & 0xFF
if write_high:
high_data = (data >> 8) & 0xFF
high_dir = (direction >> 8) & 0xFF
cmd = bytes([Ftdi.SET_BITS_LOW, low_data, low_dir,
Ftdi.SET_BITS_HIGH, high_data, high_dir])
else:
cmd = bytes([Ftdi.SET_BITS_LOW, low_data, low_dir])
self._ftdi.write_data(cmd)
def _force(self, frequency: float, sequence: bytes):
if not self._ftdi.is_connected:
raise SpiIOError("FTDI controller not initialized")
if len(sequence) > SpiController.PAYLOAD_MAX_LENGTH:
raise SpiIOError("Output payload is too large")
if self._frequency != frequency:
self._ftdi.set_frequency(frequency)
# store the requested value, not the actual one (best effort),
# to avoid setting unavailable values on each call.
self._frequency = frequency
cmd = bytearray()
direction = self.direction & 0xFF
for ctrl in sequence:
ctrl &= self._spi_mask
ctrl |= self._gpio_low
cmd.extend((Ftdi.SET_BITS_LOW, ctrl, direction))
self._ftdi.write_data(cmd)
def _exchange_half_duplex(self, frequency: float,
out: Union[bytes, bytearray, Iterable[int]],
readlen: int, cs_prolog: bytes, cs_epilog: bytes,
cpol: bool, cpha: bool,
droptail: int) -> bytes:
if not self._ftdi.is_connected:
raise SpiIOError("FTDI controller not initialized")
if len(out) > SpiController.PAYLOAD_MAX_LENGTH:
raise SpiIOError("Output payload is too large")
if readlen > SpiController.PAYLOAD_MAX_LENGTH:
raise SpiIOError("Input payload is too large")
if cpha:
# to enable CPHA, we need to use a workaround with FTDI device,
# that is enable 3-phase clocking (which is usually dedicated to
# I2C support). This mode use use 3 clock period instead of 2,
# which implies the FTDI frequency should be fixed to match the
# requested one.
frequency = (3*frequency)//2
if self._frequency != frequency:
self._ftdi.set_frequency(frequency)
# store the requested value, not the actual one (best effort),
# to avoid setting unavailable values on each call.
self._frequency = frequency
direction = self.direction & 0xFF # low bits only
cmd = bytearray()
for ctrl in cs_prolog or []:
ctrl &= self._spi_mask
ctrl |= self._gpio_low
cmd.extend((Ftdi.SET_BITS_LOW, ctrl, direction))
epilog = bytearray()
if cs_epilog:
for ctrl in cs_epilog:
ctrl &= self._spi_mask
ctrl |= self._gpio_low
epilog.extend((Ftdi.SET_BITS_LOW, ctrl, direction))
# Restore idle state
cs_high = [Ftdi.SET_BITS_LOW, self._cs_bits | self._gpio_low,
direction]
if not self._turbo:
cs_high.append(Ftdi.SEND_IMMEDIATE)
epilog.extend(cs_high)
writelen = len(out)
if self._clock_phase != cpha:
self._ftdi.enable_3phase_clock(cpha)
self._clock_phase = cpha
if writelen:
if not droptail:
wcmd = (Ftdi.WRITE_BYTES_NVE_MSB if not cpol else
Ftdi.WRITE_BYTES_PVE_MSB)
write_cmd = spack('<BH', wcmd, writelen-1)
cmd.extend(write_cmd)
cmd.extend(out)
else:
bytelen = writelen-1
if bytelen:
wcmd = (Ftdi.WRITE_BYTES_NVE_MSB if not cpol else
Ftdi.WRITE_BYTES_PVE_MSB)
write_cmd = spack('<BH', wcmd, bytelen-1)
cmd.extend(write_cmd)
cmd.extend(out[:-1])
wcmd = (Ftdi.WRITE_BITS_NVE_MSB if not cpol else
Ftdi.WRITE_BITS_PVE_MSB)
write_cmd = spack('<BBB', wcmd, 7-droptail, out[-1])
cmd.extend(write_cmd)
if readlen:
if not droptail:
rcmd = (Ftdi.READ_BYTES_NVE_MSB if not cpol else
Ftdi.READ_BYTES_PVE_MSB)
read_cmd = spack('<BH', rcmd, readlen-1)
cmd.extend(read_cmd)
else:
bytelen = readlen-1
if bytelen:
rcmd = (Ftdi.READ_BYTES_NVE_MSB if not cpol else
Ftdi.READ_BYTES_PVE_MSB)
read_cmd = spack('<BH', rcmd, bytelen-1)
cmd.extend(read_cmd)
rcmd = (Ftdi.READ_BITS_NVE_MSB if not cpol else
Ftdi.READ_BITS_PVE_MSB)
read_cmd = spack('<BB', rcmd, 7-droptail)
cmd.extend(read_cmd)
cmd.extend(self._immediate)
if self._turbo:
if epilog:
cmd.extend(epilog)
self._ftdi.write_data(cmd)
else:
self._ftdi.write_data(cmd)
if epilog:
self._ftdi.write_data(epilog)
# USB read cycle may occur before the FTDI device has actually
# sent the data, so try to read more than once if no data is
# actually received
data = self._ftdi.read_data_bytes(readlen, 4)
if droptail:
data[-1] = 0xff & (data[-1] << droptail)
else:
if writelen:
if self._turbo:
if epilog:
cmd.extend(epilog)
self._ftdi.write_data(cmd)
else:
self._ftdi.write_data(cmd)
if epilog:
self._ftdi.write_data(epilog)
data = bytearray()
return data
def _exchange_full_duplex(self, frequency: float,
out: Union[bytes, bytearray, Iterable[int]],
cs_prolog: bytes, cs_epilog: bytes,
cpol: bool, cpha: bool,
droptail: int) -> bytes:
if not self._ftdi.is_connected:
raise SpiIOError("FTDI controller not initialized")
if len(out) > SpiController.PAYLOAD_MAX_LENGTH:
raise SpiIOError("Output payload is too large")
if cpha:
# to enable CPHA, we need to use a workaround with FTDI device,
# that is enable 3-phase clocking (which is usually dedicated to
# I2C support). This mode use use 3 clock period instead of 2,
# which implies the FTDI frequency should be fixed to match the
# requested one.
frequency = (3*frequency)//2
if self._frequency != frequency:
self._ftdi.set_frequency(frequency)
# store the requested value, not the actual one (best effort),
# to avoid setting unavailable values on each call.
self._frequency = frequency
direction = self.direction & 0xFF # low bits only
cmd = bytearray()
for ctrl in cs_prolog or []:
ctrl &= self._spi_mask
ctrl |= self._gpio_low
cmd.extend((Ftdi.SET_BITS_LOW, ctrl, direction))
epilog = bytearray()
if cs_epilog:
for ctrl in cs_epilog:
ctrl &= self._spi_mask
ctrl |= self._gpio_low
epilog.extend((Ftdi.SET_BITS_LOW, ctrl, direction))
# Restore idle state
cs_high = [Ftdi.SET_BITS_LOW, self._cs_bits | self._gpio_low,
direction]
if not self._turbo:
cs_high.append(Ftdi.SEND_IMMEDIATE)
epilog.extend(cs_high)
exlen = len(out)
if self._clock_phase != cpha:
self._ftdi.enable_3phase_clock(cpha)
self._clock_phase = cpha
if not droptail:
wcmd = (Ftdi.RW_BYTES_PVE_NVE_MSB if not cpol else
Ftdi.RW_BYTES_NVE_PVE_MSB)
write_cmd = spack('<BH', wcmd, exlen-1)
cmd.extend(write_cmd)
cmd.extend(out)
else:
bytelen = exlen-1
if bytelen:
wcmd = (Ftdi.RW_BYTES_PVE_NVE_MSB if not cpol else
Ftdi.RW_BYTES_NVE_PVE_MSB)
write_cmd = spack('<BH', wcmd, bytelen-1)
cmd.extend(write_cmd)
cmd.extend(out[:-1])
wcmd = (Ftdi.RW_BITS_PVE_NVE_MSB if not cpol else
Ftdi.RW_BITS_NVE_PVE_MSB)
write_cmd = spack('<BBB', wcmd, 7-droptail, out[-1])
cmd.extend(write_cmd)
cmd.extend(self._immediate)
if self._turbo:
if epilog:
cmd.extend(epilog)
self._ftdi.write_data(cmd)
else:
self._ftdi.write_data(cmd)
if epilog:
self._ftdi.write_data(epilog)
# USB read cycle may occur before the FTDI device has actually
# sent the data, so try to read more than once if no data is
# actually received
data = self._ftdi.read_data_bytes(exlen, 4)
if droptail:
data[-1] = 0xff & (data[-1] << droptail)
return data
def _flush(self) -> None:
self._ftdi.write_data(self._immediate)
self._ftdi.purge_buffers()

View file

@ -0,0 +1,210 @@
"""Terminal management helpers"""
# Copyright (c) 2020-2024, Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2020, Michael Pratt <mpratt51@gmail.com>
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from os import environ, read as os_read
from sys import platform, stderr, stdin, stdout
# pylint: disable=import-error
if platform == 'win32':
import msvcrt
from subprocess import call # ugly workaround for an ugly OS
else:
from termios import (ECHO, ICANON, TCSAFLUSH, TCSANOW, VINTR, VMIN, VSUSP,
VTIME, tcgetattr, tcsetattr)
# pylint workaround (disable=used-before-assignment)
def call():
# pylint: disable=missing-function-docstring
pass
class Terminal:
"""Terminal management function
"""
FNKEYS = {
# Ctrl + Alt + Backspace
14: b'\x1b^H',
# Ctrl + Alt + Enter
28: b'\x1b\r',
# Pause/Break
29: b'\x1c',
# Arrows
72: b'\x1b[A',
80: b'\x1b[B',
77: b'\x1b[C',
75: b'\x1b[D',
# Arrows (Alt)
152: b'\x1b[1;3A',
160: b'\x1b[1;3B',
157: b'\x1b[1;3C',
155: b'\x1b[1;3D',
# Arrows (Ctrl)
141: b'\x1b[1;5A',
145: b'\x1b[1;5B',
116: b'\x1b[1;5C',
115: b'\x1b[1;5D',
# Ctrl + Tab
148: b'\x1b[2J',
# Cursor (Home, Ins, Del...)
71: b'\x1b[1~',
82: b'\x1b[2~',
83: b'\x1b[3~',
79: b'\x1b[4~',
73: b'\x1b[5~',
81: b'\x1b[6~',
# Cursor + Alt
151: b'\x1b[1;3~',
162: b'\x1b[2;3~',
163: b'\x1b[3;3~',
159: b'\x1b[4;3~',
153: b'\x1b[5;3~',
161: b'\x1b[6;3~',
# Cursor + Ctrl (xterm)
119: b'\x1b[1;5H',
146: b'\x1b[2;5~',
147: b'\x1b[3;5~',
117: b'\x1b[1;5F',
114: b'\x1b[5;5~',
118: b'\x1b[6;5~',
# Function Keys (F1 - F12)
59: b'\x1b[11~',
60: b'\x1b[12~',
61: b'\x1b[13~',
62: b'\x1b[14~',
63: b'\x1b[15~',
64: b'\x1b[17~',
65: b'\x1b[18~',
66: b'\x1b[19~',
67: b'\x1b[20~',
68: b'\x1b[21~',
133: b'\x1b[23~',
134: b'\x1b[24~',
# Function Keys + Shift (F11 - F22)
84: b'\x1b[23;2~',
85: b'\x1b[24;2~',
86: b'\x1b[25~',
87: b'\x1b[26~',
88: b'\x1b[28~',
89: b'\x1b[29~',
90: b'\x1b[31~',
91: b'\x1b[32~',
92: b'\x1b[33~',
93: b'\x1b[34~',
135: b'\x1b[20;2~',
136: b'\x1b[21;2~',
# Function Keys + Ctrl (xterm)
94: b'\x1bOP',
95: b'\x1bOQ',
96: b'\x1bOR',
97: b'\x1bOS',
98: b'\x1b[15;2~',
99: b'\x1b[17;2~',
100: b'\x1b[18;2~',
101: b'\x1b[19;2~',
102: b'\x1b[20;3~',
103: b'\x1b[21;3~',
137: b'\x1b[23;3~',
138: b'\x1b[24;3~',
# Function Keys + Alt (xterm)
104: b'\x1b[11;5~',
105: b'\x1b[12;5~',
106: b'\x1b[13;5~',
107: b'\x1b[14;5~',
108: b'\x1b[15;5~',
109: b'\x1b[17;5~',
110: b'\x1b[18;5~',
111: b'\x1b[19;5~',
112: b'\x1b[20;5~',
113: b'\x1b[21;5~',
139: b'\x1b[23;5~',
140: b'\x1b[24;5~',
}
"""
Pause/Break, Ctrl+Alt+Del, Ctrl+Alt+arrows not mapable
key: ordinal of char from msvcrt.getch()
value: bytes string of ANSI escape sequence for linux/xterm
numerical used over linux specifics for Home and End
VT or CSI escape sequences used when linux has no sequence
something unique for keys without an escape function
0x1b == Escape key
"""
IS_MSWIN = platform == 'win32'
"""Whether we run on crap OS."""
def __init__(self):
self._termstates = []
def init(self, fullterm: bool) -> None:
"""Internal terminal initialization function"""
if not self.IS_MSWIN:
self._termstates = [(t.fileno(),
tcgetattr(t.fileno()) if t.isatty() else None)
for t in (stdin, stdout, stderr)]
tfd, istty = self._termstates[0]
if istty:
new = tcgetattr(tfd)
new[3] = new[3] & ~ICANON & ~ECHO
new[6][VMIN] = 1
new[6][VTIME] = 0
if fullterm:
new[6][VINTR] = 0
new[6][VSUSP] = 0
tcsetattr(tfd, TCSANOW, new)
else:
# Windows black magic
# https://stackoverflow.com/questions/12492810
call('', shell=True)
def reset(self) -> None:
"""Reset the terminal to its original state."""
for tfd, att in self._termstates:
# terminal modes have to be restored on exit...
if att is not None:
tcsetattr(tfd, TCSANOW, att)
tcsetattr(tfd, TCSAFLUSH, att)
@staticmethod
def is_term() -> bool:
"""Tells whether the current stdout/stderr stream are connected to a
terminal (vs. a regular file or pipe)"""
return stdout.isatty()
@staticmethod
def is_colorterm() -> bool:
"""Tells whether the current terminal (if any) support colors escape
sequences"""
terms = ['xterm-color', 'ansi']
return stdout.isatty() and environ.get('TERM') in terms
@classmethod
def getkey(cls) -> bytes:
"""Return a key from the current console, in a platform independent
way.
"""
# there's probably a better way to initialize the module without
# relying onto a singleton pattern. To be fixed
if cls.IS_MSWIN:
# w/ py2exe, it seems the importation fails to define the global
# symbol 'msvcrt', to be fixed
while True:
char = msvcrt.getch()
if char == b'\r':
return b'\n'
return char
else:
char = os_read(stdin.fileno(), 1)
return char
@classmethod
def getch_to_escape(cls, char: bytes) -> bytes:
"""Get Windows escape sequence."""
if cls.IS_MSWIN:
return cls.FNKEYS.get(ord(char), char)
return char

View file

@ -0,0 +1,489 @@
# Copyright (c) 2017-2024, Emmanuel Blot <emmanuel.blot@free.fr>
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""MPSSE command debug tracer."""
# pylint: disable=missing-docstring
from binascii import hexlify
from collections import deque
from importlib import import_module
from inspect import currentframe
from logging import getLogger
from string import ascii_uppercase
from struct import unpack as sunpack
from sys import modules
from typing import Union
class FtdiMpsseTracer:
"""FTDI MPSSE protocol decoder."""
MPSSE_ENGINES = {
0x0200: 0,
0x0400: 0,
0x0500: 0,
0x0600: 0,
0x0700: 2,
0x0800: 2,
0x0900: 1,
0x1000: 0,
0x3600: 2}
"""Count of MPSSE engines."""
def __init__(self, version):
count = self.MPSSE_ENGINES[version]
self._engines = [None] * count
def send(self, iface: int, buf: Union[bytes, bytearray]) -> None:
self._get_engine(iface).send(buf)
def receive(self, iface: int, buf: Union[bytes, bytearray]) -> None:
self._get_engine(iface).receive(buf)
def _get_engine(self, iface: int):
iface -= 1
try:
self._engines[iface]
except IndexError as exc:
raise ValueError(f'No MPSSE engine available on interface '
f'{iface}') from exc
if not self._engines[iface]:
self._engines[iface] = FtdiMpsseEngine(iface)
return self._engines[iface]
class FtdiMpsseEngine:
"""FTDI MPSSE virtual engine
Far from being complete for now
"""
COMMAND_PREFIX = \
'GET SET READ WRITE RW ENABLE DISABLE CLK LOOPBACK SEND DRIVE'
ST_IDLE = range(1)
def __init__(self, iface: int):
self.log = getLogger('pyftdi.mpsse.tracer')
self._if = iface
self._trace_tx = bytearray()
self._trace_rx = bytearray()
self._state = self.ST_IDLE
self._clkdiv5 = False
self._cmd_decoded = True
self._resp_decoded = True
self._last_codes = deque()
self._expect_resp = deque() # positive: byte, negative: bit count
self._commands = self._build_commands()
def send(self, buf: Union[bytes, bytearray]) -> None:
self._trace_tx.extend(buf)
while self._trace_tx:
try:
code = self._trace_tx[0]
cmd = self._commands[code]
if self._cmd_decoded:
self.log.debug('[%d]:[Command: %02X: %s]',
self._if, code, cmd)
cmd_decoder = getattr(self, f'_cmd_{cmd.lower()}')
rdepth = len(self._expect_resp)
try:
self._cmd_decoded = cmd_decoder()
except AttributeError as exc:
raise ValueError(str(exc)) from exc
if len(self._expect_resp) > rdepth:
self._last_codes.append(code)
if self._cmd_decoded:
continue
# not enough data in buffer to decode a whole command
return
except IndexError:
self.log.warning('[%d]:Empty buffer on %02X: %s',
self._if, code, cmd)
except KeyError:
self.log.warning('[%d]:Unknown command code: %02X',
self._if, code)
except AttributeError:
self.log.warning('[%d]:Decoder for command %s [%02X] is not '
'implemented', self._if, cmd, code)
except ValueError as exc:
self.log.warning('[%d]:Decoder for command %s [%02X] failed: '
'%s', self._if, cmd, code, exc)
# on error, flush all buffers
self.log.warning('Flush TX/RX buffers')
self._trace_tx = bytearray()
self._trace_rx = bytearray()
self._last_codes.clear()
def receive(self, buf: Union[bytes, bytearray]) -> None:
self.log.info(' .. %s', hexlify(buf).decode())
self._trace_rx.extend(buf)
while self._trace_rx:
code = None
try:
code = self._last_codes.popleft()
cmd = self._commands[code]
resp_decoder = getattr(self, f'_resp_{cmd.lower()}')
self._resp_decoded = resp_decoder()
if self._resp_decoded:
continue
# not enough data in buffer to decode a whole response
return
except IndexError:
self.log.warning('[%d]:Empty buffer', self._if)
except KeyError:
self.log.warning('[%d]:Unknown command code: %02X',
self._if, code)
except AttributeError:
self.log.warning('[%d]:Decoder for response %s [%02X] is not '
'implemented', self._if, cmd, code)
# on error, flush RX buffer
self.log.warning('[%d]:Flush RX buffer', self._if)
self._trace_rx = bytearray()
self._last_codes.clear()
@classmethod
def _build_commands(cls):
# pylint: disable=no-self-argument
commands = {}
fdti_mod_name = 'pyftdi.ftdi'
ftdi_mod = modules.get(fdti_mod_name)
if not ftdi_mod:
ftdi_mod = import_module(fdti_mod_name)
ftdi_type = getattr(ftdi_mod, 'Ftdi')
for cmd in dir(ftdi_type):
if cmd[0] not in ascii_uppercase:
continue
value = getattr(ftdi_type, cmd)
if not isinstance(value, int):
continue
family = cmd.split('_')[0]
# pylint: disable=no-member
if family not in cls.COMMAND_PREFIX.split():
continue
commands[value] = cmd
return commands
def _cmd_enable_clk_div5(self):
self.log.info(' [%d]:Enable clock divisor /5', self._if)
self._clkdiv5 = True
self._trace_tx[:] = self._trace_tx[1:]
return True
def _cmd_disable_clk_div5(self):
self.log.info(' [%d]:Disable clock divisor /5', self._if)
self._clkdiv5 = False
self._trace_tx[:] = self._trace_tx[1:]
return True
def _cmd_set_tck_divisor(self):
if len(self._trace_tx) < 3:
return False
value, = sunpack('<H', self._trace_tx[1:3])
base = 12E6 if self._clkdiv5 else 60E6
freq = base / ((1 + value) * 2)
self.log.info(' [%d]:Set frequency %.3fMHZ', self._if, freq/1E6)
self._trace_tx[:] = self._trace_tx[3:]
return True
def _cmd_loopback_end(self):
self.log.info(' [%d]:Disable loopback', self._if)
self._trace_tx[:] = self._trace_tx[1:]
return True
def _cmd_enable_clk_adaptive(self):
self.log.info(' [%d]:Enable adaptive clock', self._if)
self._trace_tx[:] = self._trace_tx[1:]
return True
def _cmd_disable_clk_adaptive(self):
self.log.info(' [%d]:Disable adaptive clock', self._if)
self._trace_tx[:] = self._trace_tx[1:]
return True
def _cmd_enable_clk_3phase(self):
self.log.info(' [%d]:Enable 3-phase clock', self._if)
self._trace_tx[:] = self._trace_tx[1:]
return True
def _cmd_disable_clk_3phase(self):
self.log.info(' [%d]:Disable 3-phase clock', self._if)
self._trace_tx[:] = self._trace_tx[1:]
return True
def _cmd_drive_zero(self):
if len(self._trace_tx) < 3:
return False
value, = sunpack('H', self._trace_tx[1:3])
self.log.info(' [%d]:Open collector [15:0] %04x %s',
self._if, value, self.bitfmt(value, 16))
self._trace_tx[:] = self._trace_tx[3:]
return True
def _cmd_send_immediate(self):
self.log.debug(' [%d]:Send immediate', self._if)
self._trace_tx[:] = self._trace_tx[1:]
return True
def _cmd_get_bits_low(self):
self._trace_tx[:] = self._trace_tx[1:]
self._expect_resp.append(1)
return True
def _cmd_get_bits_high(self):
self._trace_tx[:] = self._trace_tx[1:]
self._expect_resp.append(1)
return True
def _cmd_set_bits_low(self):
if len(self._trace_tx) < 3:
return False
value, direction = sunpack('BB', self._trace_tx[1:3])
self.log.info(' [%d]:Set gpio[7:0] %02x %s',
self._if, value, self.bm2str(value, direction))
self._trace_tx[:] = self._trace_tx[3:]
return True
def _cmd_set_bits_high(self):
if len(self._trace_tx) < 3:
return False
value, direction = sunpack('BB', self._trace_tx[1:3])
self.log.info(' [%d]:Set gpio[15:8] %02x %s',
self._if, value, self.bm2str(value, direction))
self._trace_tx[:] = self._trace_tx[3:]
return True
def _cmd_write_bytes_pve_msb(self):
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name)
def _cmd_write_bytes_nve_msb(self):
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name)
def _cmd_write_bytes_pve_lsb(self):
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name)
def _cmd_write_bytes_nve_lsb(self):
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name)
def _cmd_read_bytes_pve_msb(self):
return self._decode_input_mpsse_byte_request()
def _resp_read_bytes_pve_msb(self):
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
def _cmd_read_bytes_nve_msb(self):
return self._decode_input_mpsse_byte_request()
def _resp_read_bytes_nve_msb(self):
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
def _cmd_read_bytes_pve_lsb(self):
return self._decode_input_mpsse_byte_request()
def _resp_read_bytes_pve_lsb(self):
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
def _cmd_read_bytes_nve_lsb(self):
return self._decode_input_mpsse_byte_request()
def _resp_read_bytes_nve_lsb(self):
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
def _cmd_rw_bytes_nve_pve_msb(self):
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name,
True)
def _resp_rw_bytes_nve_pve_msb(self):
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
def _cmd_rw_bytes_pve_nve_msb(self):
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name,
True)
def _resp_rw_bytes_pve_nve_msb(self):
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
def _cmd_write_bits_pve_msb(self):
return self._decode_output_mpsse_bits(currentframe().f_code.co_name)
def _cmd_write_bits_nve_msb(self):
return self._decode_output_mpsse_bits(currentframe().f_code.co_name)
def _cmd_write_bits_pve_lsb(self):
return self._decode_output_mpsse_bits(currentframe().f_code.co_name)
def _cmd_write_bits_nve_lsb(self):
return self._decode_output_mpsse_bits(currentframe().f_code.co_name)
def _cmd_read_bits_pve_msb(self):
return self._decode_input_mpsse_bit_request()
def _resp_read_bits_pve_msb(self):
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
def _cmd_read_bits_nve_msb(self):
return self._decode_input_mpsse_bit_request()
def _resp_read_bits_nve_msb(self):
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
def _cmd_read_bits_pve_lsb(self):
return self._decode_input_mpsse_bit_request()
def _resp_read_bits_pve_lsb(self):
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
def _cmd_read_bits_nve_lsb(self):
return self._decode_input_mpsse_bit_request()
def _resp_read_bits_nve_lsb(self):
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
def _cmd_rw_bits_nve_pve_msb(self):
return self._decode_output_mpsse_bits(currentframe().f_code.co_name,
True)
def _resp_rw_bits_nve_pve_msb(self):
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
def _cmd_rw_bits_pve_nve_msb(self):
return self._decode_output_mpsse_bits(currentframe().f_code.co_name,
True)
def _resp_rw_bits_pve_nve_msb(self):
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
def _resp_get_bits_low(self):
if self._trace_rx:
return False
value = self._trace_rx[0]
self.log.info(' [%d]:Get gpio[7:0] %02x %s',
self._if, value, self.bm2str(value, 0xFF))
self._trace_rx[:] = self._trace_rx[1:]
return True
def _resp_get_bits_high(self):
if self._trace_rx:
return False
value = self._trace_rx[0]
self.log.info(' [%d]:Get gpio[15:8] %02x %s',
self._if, value, self.bm2str(value, 0xFF))
self._trace_rx[:] = self._trace_rx[1:]
return True
def _decode_output_mpsse_bytes(self, caller, expect_rx=False):
if len(self._trace_tx) < 4:
return False
length = sunpack('<H', self._trace_tx[1:3])[0] + 1
if len(self._trace_tx) < 4 + length:
return False
if expect_rx:
self._expect_resp.append(length)
payload = self._trace_tx[3:3+length]
funcname = caller[5:].title().replace('_', '')
self.log.info(' [%d]:%s> (%d) %s',
self._if, funcname, length,
hexlify(payload).decode('utf8'))
self._trace_tx[:] = self._trace_tx[3+length:]
return True
def _decode_output_mpsse_bits(self, caller, expect_rx=False):
if len(self._trace_tx) < 3:
return False
bitlen = self._trace_tx[1] + 1
if expect_rx:
self._expect_resp.append(-bitlen)
payload = self._trace_tx[2]
funcname = caller[5:].title().replace('_', '')
msb = caller[5:][-3].lower() == 'm'
self.log.info(' %s> (%d) %s',
funcname, bitlen, self.bit2str(payload, bitlen, msb))
self._trace_tx[:] = self._trace_tx[3:]
return True
def _decode_input_mpsse_byte_request(self):
if len(self._trace_tx) < 3:
return False
length = sunpack('<H', self._trace_tx[1:3])[0] + 1
self._expect_resp.append(length)
self._trace_tx[:] = self._trace_tx[3:]
return True
def _decode_input_mpsse_bit_request(self):
if len(self._trace_tx) < 2:
return False
bitlen = self._trace_tx[1] + 1
self._expect_resp.append(-bitlen)
self._trace_tx[:] = self._trace_tx[2:]
return True
def _decode_input_mpsse_bytes(self, caller):
if not self._expect_resp:
self.log.warning('[%d]:Response w/o request?', self._if)
return False
if self._expect_resp[0] < 0:
self.log.warning('[%d]:Handling byte request w/ bit length',
self._if)
return False
if len(self._trace_rx) < self._expect_resp[0]: # peek
return False
length = self._expect_resp.popleft()
payload = self._trace_rx[:length]
self._trace_rx[:] = self._trace_rx[length:]
funcname = caller[5:].title().replace('_', '')
self.log.info(' %s< (%d) %s',
funcname, length, hexlify(payload).decode('utf8'))
return True
def _decode_input_mpsse_bits(self, caller):
if not self._expect_resp:
self.log.warning('[%d]:Response w/o request?', self._if)
return False
if not self._trace_rx: # peek
return False
if self._expect_resp[0] > 0:
self.log.warning('[%d]:Handling bit request w/ byte length',
self._if)
bitlen = -self._expect_resp.popleft()
payload = self._trace_rx[0]
self._trace_rx[:] = self._trace_rx[1:]
funcname = caller[5:].title().replace('_', '')
msb = caller[5:][-3].lower() == 'm'
self.log.info(' %s< (%d) %s',
funcname, bitlen, self.bit2str(payload, bitlen, msb))
return True
@classmethod
def bit2str(cls, value: int, count: int, msb: bool, hiz: str = '_') -> str:
mask = (1 << count) - 1
if msb:
mask <<= 8 - count
return cls.bm2str(value, mask, hiz)
@classmethod
def bm2str(cls, value: int, mask: int, hiz: str = '_') -> str:
vstr = cls.bitfmt(value, 8)
mstr = cls.bitfmt(mask, 8)
return ''.join([m == '1' and v or hiz for v, m in zip(vstr, mstr)])
@classmethod
def bitfmt(cls, value, width):
return format(value, f'0{width}b')
# rw_bytes_pve_pve_lsb
# rw_bytes_pve_nve_lsb
# rw_bytes_nve_pve_lsb
# rw_bytes_nve_nve_lsb
# rw_bits_pve_pve_lsb
# rw_bits_pve_nve_lsb
# rw_bits_nve_pve_lsb
# rw_bits_nve_nve_lsb
# write_bits_tms_pve
# write_bits_tms_nve
# rw_bits_tms_pve_pve
# rw_bits_tms_nve_pve
# rw_bits_tms_pve_nve
# rw_bits_tms_nve_nve

View file

@ -0,0 +1,649 @@
# Copyright (c) 2014-2024, Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2016, Emmanuel Bouaziz <ebouaziz@free.fr>
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""USB Helpers"""
import sys
from importlib import import_module
from string import printable as printablechars
from threading import RLock
from typing import (Any, Dict, List, NamedTuple, Optional, Sequence, Set,
TextIO, Type, Tuple, Union)
from urllib.parse import SplitResult, urlsplit, urlunsplit
from usb.backend import IBackend
from usb.core import Device as UsbDevice, USBError
from usb.util import dispose_resources, get_string as usb_get_string
from .misc import to_int
# pylint: disable=broad-except
UsbDeviceDescriptor = NamedTuple('UsbDeviceDescriptor',
(('vid', int),
('pid', int),
('bus', Optional[int]),
('address', Optional[int]),
('sn', Optional[str]),
('index', Optional[int]),
('description', Optional[str])))
"""USB Device descriptor are used to report known information about a FTDI
compatible device, and as a device selection filter
* vid: vendor identifier, 16-bit integer
* pid: product identifier, 16-bit integer
* bus: USB bus identifier, host dependent integer
* address: USB address identifier on a USB bus, host dependent integer
* sn: serial number, string
* index: integer, can be used to descriminate similar devices
* description: device description, as a string
To select a device, use None for unknown fields
.. note::
* Always prefer serial number to other identification methods if available
* Prefer bus/address selector over index
"""
UsbDeviceKey = Union[Tuple[int, int, int, int], Tuple[int, int]]
"""USB device indentifier on the system.
This is used as USB device identifiers on the host. On proper hosts,
this is a (bus, address, vid, pid) 4-uple. On stupid hosts (such as M$Win),
it may be degraded to (vid, pid) 2-uple.
"""
class UsbToolsError(Exception):
"""UsbTools error."""
class UsbTools:
"""Helpers to obtain information about connected USB devices."""
# Supported back ends, in preference order
BACKENDS = ('usb.backend.libusb1', 'usb.backend.libusb0')
# Need to maintain a list of reference USB devices, to circumvent a
# limitation in pyusb that prevents from opening several times the same
# USB device. The following dictionary used bus/address/vendor/product keys
# to track (device, refcount) pairs
Lock = RLock()
Devices = {} # (bus, address, vid, pid): (usb.core.Device, refcount)
UsbDevices = {} # (vid, pid): {usb.core.Device}
UsbApi = None
@classmethod
def find_all(cls, vps: Sequence[Tuple[int, int]],
nocache: bool = False) -> \
List[Tuple[UsbDeviceDescriptor, int]]:
"""Find all devices that match the specified vendor/product pairs.
:param vps: a sequence of 2-tuple (vid, pid) pairs
:param bool nocache: bypass cache to re-enumerate USB devices on
the host
:return: a list of 2-tuple (UsbDeviceDescriptor, interface count)
"""
with cls.Lock:
devs = set()
for vid, pid in vps:
# TODO optimize useless loops
devs.update(UsbTools._find_devices(vid, pid, nocache))
devices = set()
for dev in devs:
ifcount = max(cfg.bNumInterfaces for cfg in dev)
# TODO: handle / is serial number strings
sernum = UsbTools.get_string(dev, dev.iSerialNumber)
description = UsbTools.get_string(dev, dev.iProduct)
descriptor = UsbDeviceDescriptor(dev.idVendor, dev.idProduct,
dev.bus, dev.address,
sernum, None, description)
devices.add((descriptor, ifcount))
return list(devices)
@classmethod
def flush_cache(cls, ):
"""Flush the FTDI device cache.
It is highly recommanded to call this method a FTDI device is
unplugged/plugged back since the last enumeration, as the device
may appear on a different USB location each time it is plugged
in.
Failing to clear out the cache may lead to USB Error 19:
``Device may have been disconnected``.
"""
with cls.Lock:
cls.UsbDevices.clear()
@classmethod
def get_device(cls, devdesc: UsbDeviceDescriptor) -> UsbDevice:
"""Find a previously open device with the same vendor/product
or initialize a new one, and return it.
If several FTDI devices of the same kind (vid, pid) are connected
to the host, either index or serial argument should be used to
discriminate the FTDI device.
index argument is not a reliable solution as the host may enumerate
the USB device in random order. serial argument is more reliable
selector and should always be prefered.
Some FTDI devices support several interfaces/ports (such as FT2232H,
FT4232H and FT4232HA). The interface argument selects the FTDI port
to use, starting from 1 (not 0).
:param devdesc: Device descriptor that identifies the device by
constraints.
:return: PyUSB device instance
"""
with cls.Lock:
if devdesc.index or devdesc.sn or devdesc.description:
dev = None
if not devdesc.vid:
raise ValueError('Vendor identifier is required')
devs = cls._find_devices(devdesc.vid, devdesc.pid)
if devdesc.description:
devs = [dev for dev in devs if
UsbTools.get_string(dev, dev.iProduct) ==
devdesc.description]
if devdesc.sn:
devs = [dev for dev in devs if
UsbTools.get_string(dev, dev.iSerialNumber) ==
devdesc.sn]
if devdesc.bus is not None and devdesc.address is not None:
devs = [dev for dev in devs if
(devdesc.bus == dev.bus and
devdesc.address == dev.address)]
if isinstance(devs, set):
# there is no guarantee the same index with lead to the
# same device. Indexing should be reworked
devs = list(devs)
try:
dev = devs[devdesc.index or 0]
except IndexError as exc:
raise IOError("No such device") from exc
else:
devs = cls._find_devices(devdesc.vid, devdesc.pid)
dev = list(devs)[0] if devs else None
if not dev:
raise IOError('Device not found')
try:
devkey = (dev.bus, dev.address, devdesc.vid, devdesc.pid)
if None in devkey[0:2]:
raise AttributeError('USB backend does not support bus '
'enumeration')
except AttributeError:
devkey = (devdesc.vid, devdesc.pid)
if devkey not in cls.Devices:
# only change the active configuration if the active one is
# not the first. This allows other libusb sessions running
# with the same device to run seamlessly.
try:
config = dev.get_active_configuration()
setconf = config.bConfigurationValue != 1
except USBError:
setconf = True
if setconf:
try:
dev.set_configuration()
except USBError:
pass
cls.Devices[devkey] = [dev, 1]
else:
cls.Devices[devkey][1] += 1
return cls.Devices[devkey][0]
@classmethod
def release_device(cls, usb_dev: UsbDevice):
"""Release a previously open device, if it not used anymore.
:param usb_dev: a previously instanciated USB device instance
"""
# Lookup for ourselves in the class dictionary
with cls.Lock:
# pylint: disable=unnecessary-dict-index-lookup
for devkey, (dev, refcount) in cls.Devices.items():
if dev == usb_dev:
# found
if refcount > 1:
# another interface is open, decrement
cls.Devices[devkey][1] -= 1
else:
# last interface in use, release
dispose_resources(cls.Devices[devkey][0])
del cls.Devices[devkey]
break
@classmethod
def release_all_devices(cls, devclass: Optional[Type] = None) -> int:
"""Release all open devices.
:param devclass: optional class to only release devices of one type
:return: the count of device that have been released.
"""
with cls.Lock:
remove_devs = set()
# pylint: disable=consider-using-dict-items
for devkey in cls.Devices:
if devclass:
dev = cls._get_backend_device(cls.Devices[devkey][0])
if dev is None or not isinstance(dev, devclass):
continue
dispose_resources(cls.Devices[devkey][0])
remove_devs.add(devkey)
for devkey in remove_devs:
del cls.Devices[devkey]
return len(remove_devs)
@classmethod
def list_devices(cls, urlstr: str,
vdict: Dict[str, int],
pdict: Dict[int, Dict[str, int]],
default_vendor: int) -> \
List[Tuple[UsbDeviceDescriptor, int]]:
"""List candidates that match the device URL pattern.
:see: :py:meth:`show_devices` to generate the URLs from the
candidates list
:param url: the URL to parse
:param vdict: vendor name map of USB vendor ids
:param pdict: vendor id map of product name map of product ids
:param default_vendor: default vendor id
:return: list of (UsbDeviceDescriptor, interface)
"""
urlparts = urlsplit(urlstr)
if not urlparts.path:
raise UsbToolsError('URL string is missing device port')
candidates, _ = cls.enumerate_candidates(urlparts, vdict, pdict,
default_vendor)
return candidates
@classmethod
def parse_url(cls, urlstr: str, scheme: str,
vdict: Dict[str, int],
pdict: Dict[int, Dict[str, int]],
default_vendor: int) -> Tuple[UsbDeviceDescriptor, int]:
"""Parse a device specifier URL.
:param url: the URL to parse
:param scheme: scheme to match in the URL string (scheme://...)
:param vdict: vendor name map of USB vendor ids
:param pdict: vendor id map of product name map of product ids
:param default_vendor: default vendor id
:return: UsbDeviceDescriptor, interface
..note:
URL syntax:
protocol://vendor:product[:serial|:index|:bus:addr]/interface
"""
urlparts = urlsplit(urlstr)
if scheme != urlparts.scheme:
raise UsbToolsError(f'Invalid URL: {urlstr}')
try:
if not urlparts.path:
raise UsbToolsError('URL string is missing device port')
path = urlparts.path.strip('/')
if path == '?' or (not path and urlstr.endswith('?')):
report_devices = True
interface = -1
else:
interface = to_int(path)
report_devices = False
except (IndexError, ValueError) as exc:
raise UsbToolsError(f'Invalid device URL: {urlstr}') from exc
candidates, idx = cls.enumerate_candidates(urlparts, vdict, pdict,
default_vendor)
if report_devices:
UsbTools.show_devices(scheme, vdict, pdict, candidates)
raise SystemExit(candidates and
'Please specify the USB device' or
'No USB-Serial device has been detected')
if idx is None:
if len(candidates) > 1:
raise UsbToolsError(f"{len(candidates)} USB devices match URL "
f"'{urlstr}'")
idx = 0
try:
desc, _ = candidates[idx]
vendor, product = desc[:2]
except IndexError:
raise UsbToolsError(f'No USB device matches URL {urlstr}') \
from None
if not vendor:
cvendors = {candidate[0] for candidate in candidates}
if len(cvendors) == 1:
vendor = cvendors.pop()
if vendor not in pdict:
vstr = '0x{vendor:04x}' if vendor is not None else '?'
raise UsbToolsError(f'Vendor ID {vstr} not supported')
if not product:
cproducts = {candidate[1] for candidate in candidates
if candidate[0] == vendor}
if len(cproducts) == 1:
product = cproducts.pop()
if product not in pdict[vendor].values():
pstr = '0x{vendor:04x}' if product is not None else '?'
raise UsbToolsError(f'Product ID {pstr} not supported')
devdesc = UsbDeviceDescriptor(vendor, product, desc.bus, desc.address,
desc.sn, idx, desc.description)
return devdesc, interface
@classmethod
def enumerate_candidates(cls, urlparts: SplitResult,
vdict: Dict[str, int],
pdict: Dict[int, Dict[str, int]],
default_vendor: int) -> \
Tuple[List[Tuple[UsbDeviceDescriptor, int]], Optional[int]]:
"""Enumerate USB device URLs that match partial URL and VID/PID
criteria.
:param urlpart: splitted device specifier URL
:param vdict: vendor name map of USB vendor ids
:param pdict: vendor id map of product name map of product ids
:param default_vendor: default vendor id
:return: list of (usbdev, iface), parsed index if any
"""
specifiers = urlparts.netloc.split(':')
plcomps = specifiers + [''] * 2
try:
plcomps[0] = vdict.get(plcomps[0], plcomps[0])
if plcomps[0]:
vendor = to_int(plcomps[0])
else:
vendor = None
product_ids = pdict.get(vendor, None)
if not product_ids:
product_ids = pdict[default_vendor]
plcomps[1] = product_ids.get(plcomps[1], plcomps[1])
if plcomps[1]:
try:
product = to_int(plcomps[1])
except ValueError as exc:
raise UsbToolsError(f'Product {plcomps[1]} is not '
f'referenced') from exc
else:
product = None
except (IndexError, ValueError) as exc:
raise UsbToolsError(f'Invalid device URL: '
f'{urlunsplit(urlparts)}') from exc
sernum = None
idx = None
bus = None
address = None
locators = specifiers[2:]
if len(locators) > 1:
try:
bus = int(locators[0], 16)
address = int(locators[1], 16)
except ValueError as exc:
raise UsbToolsError(f'Invalid bus/address: '
f'{":".join(locators)}') from exc
else:
if locators and locators[0]:
try:
devidx = to_int(locators[0])
if devidx > 255:
raise ValueError()
idx = devidx
if idx:
idx = devidx-1
except ValueError:
sernum = locators[0]
candidates = []
vendors = [vendor] if vendor else set(vdict.values())
vps = set()
for vid in vendors:
products = pdict.get(vid, [])
for pid in products:
vps.add((vid, products[pid]))
devices = cls.find_all(vps)
if sernum:
if sernum not in [dev.sn for dev, _ in devices]:
raise UsbToolsError(f'No USB device with S/N {sernum}')
for desc, ifcount in devices:
if vendor and vendor != desc.vid:
continue
if product and product != desc.pid:
continue
if sernum and sernum != desc.sn:
continue
if bus is not None:
if bus != desc.bus or address != desc.address:
continue
candidates.append((desc, ifcount))
return candidates, idx
@classmethod
def show_devices(cls, scheme: str,
vdict: Dict[str, int],
pdict: Dict[int, Dict[str, int]],
devdescs: Sequence[Tuple[UsbDeviceDescriptor, int]],
out: Optional[TextIO] = None):
"""Show supported devices. When the joker url ``scheme://*/?`` is
specified as an URL, it generates a list of connected USB devices
that match the supported USB devices. It can be used to provide the
end-user with a list of valid URL schemes.
:param scheme: scheme to match in the URL string (scheme://...)
:param vdict: vendor name map of USB vendor ids
:param pdict: vendor id map of product name map of product ids
:param devdescs: candidate devices
:param out: output stream, none for stdout
"""
if not devdescs:
return
if not out:
out = sys.stdout
devstrs = cls.build_dev_strings(scheme, vdict, pdict, devdescs)
max_url_len = max(len(url) for url, _ in devstrs)
print('Available interfaces:', file=out)
for url, desc in devstrs:
print(f' {url:{max_url_len}s} {desc}', file=out)
print('', file=out)
@classmethod
def build_dev_strings(cls, scheme: str,
vdict: Dict[str, int],
pdict: Dict[int, Dict[str, int]],
devdescs: Sequence[Tuple[UsbDeviceDescriptor,
int]]) -> \
List[Tuple[str, str]]:
"""Build URL and device descriptors from UsbDeviceDescriptors.
:param scheme: protocol part of the URLs to generate
:param vdict: vendor name map of USB vendor ids
:param pdict: vendor id map of product name map of product ids
:param devdescs: USB devices and interfaces
:return: list of (url, descriptors)
"""
indices = {} # Dict[Tuple[int, int], int]
descs = []
for desc, ifcount in sorted(devdescs):
ikey = (desc.vid, desc.pid)
indices[ikey] = indices.get(ikey, 0) + 1
# try to find a matching string for the current vendor
vendors = []
# fallback if no matching string for the current vendor is found
vendor = f'{desc.vid:04x}'
for vidc in vdict:
if vdict[vidc] == desc.vid:
vendors.append(vidc)
if vendors:
vendors.sort(key=len)
vendor = vendors[0]
# try to find a matching string for the current vendor
# fallback if no matching string for the current product is found
product = f'{desc.pid:04x}'
try:
products = []
productids = pdict[desc.vid]
for prdc in productids:
if productids[prdc] == desc.pid:
products.append(prdc)
if products:
product = products[0]
except KeyError:
pass
for port in range(1, ifcount+1):
fmt = '%s://%s/%d'
parts = [vendor, product]
sernum = desc.sn
if not sernum:
sernum = ''
if [c for c in sernum if c not in printablechars or c == '?']:
serial = f'{indices[ikey]}'
else:
serial = sernum
if serial:
parts.append(serial)
elif desc.bus is not None and desc.address is not None:
parts.append(f'{desc.bus:x}')
parts.append(f'{desc.address:x}')
# the description may contain characters that cannot be
# emitted in the output stream encoding format
try:
url = fmt % (scheme, ':'.join(parts), port)
except Exception:
url = fmt % (scheme, ':'.join([vendor, product, '???']),
port)
try:
if desc.description:
description = f'({desc.description})'
else:
description = ''
except Exception:
description = ''
descs.append((url, description))
return descs
@classmethod
def get_string(cls, device: UsbDevice, stridx: int) -> str:
"""Retrieve a string from the USB device, dealing with PyUSB API breaks
:param device: USB device instance
:param stridx: the string identifier
:return: the string read from the USB device
"""
if cls.UsbApi is None:
# pylint: disable=import-outside-toplevel
import inspect
args, _, _, _ = \
inspect.signature(UsbDevice.read).parameters
if (len(args) >= 3) and args[1] == 'length':
cls.UsbApi = 1
else:
cls.UsbApi = 2
try:
if cls.UsbApi == 2:
return usb_get_string(device, stridx)
return usb_get_string(device, 64, stridx)
except UnicodeDecodeError:
# do not abort if EEPROM data is somewhat incoherent
return ''
@classmethod
def find_backend(cls) -> IBackend:
"""Try to find and load an PyUSB backend.
..note:: There is no need to call this method for regular usage.
:return: PyUSB backend
"""
with cls.Lock:
return cls._load_backend()
@classmethod
def _find_devices(cls, vendor: int, product: int,
nocache: bool = False) -> Set[UsbDevice]:
"""Find a USB device and return it.
This code re-implements the usb.core.find() method using a local
cache to avoid calling several times the underlying LibUSB and the
system USB calls to enumerate the available USB devices. As these
calls are time-hungry (about 1 second/call), the enumerated devices
are cached. It consumes a bit more memory but dramatically improves
start-up time.
Hopefully, this kludge is temporary and replaced with a better
implementation from PyUSB at some point.
:param vendor: USB vendor id
:param product: USB product id
:param bool nocache: bypass cache to re-enumerate USB devices on
the host
:return: a set of USB device matching the vendor/product identifier
pair
"""
backend = cls._load_backend()
vidpid = (vendor, product)
if nocache or (vidpid not in cls.UsbDevices):
# not freed until Python runtime completion
# enumerate_devices returns a generator, so back up the
# generated device into a list. To save memory, we only
# back up the supported devices
devs = set()
vpdict = {} # Dict[int, List[int]]
vpdict.setdefault(vendor, [])
vpdict[vendor].append(product)
for dev in backend.enumerate_devices():
# pylint: disable=no-member
device = UsbDevice(dev, backend)
if device.idVendor in vpdict:
products = vpdict[device.idVendor]
if products and (device.idProduct not in products):
continue
devs.add(device)
if sys.platform == 'win32':
# ugly kludge for a boring OS:
# on Windows, the USB stack may enumerate the very same
# devices several times: a real device with N interface
# appears also as N device with as single interface.
# We only keep the "device" that declares the most
# interface count and discard the "virtual" ones.
filtered_devs = {}
for dev in devs:
vid = dev.idVendor
pid = dev.idProduct
ifc = max(cfg.bNumInterfaces for cfg in dev)
k = (vid, pid, dev.bus, dev.address)
if k not in filtered_devs:
filtered_devs[k] = dev
else:
fdev = filtered_devs[k]
fifc = max(cfg.bNumInterfaces for cfg in fdev)
if fifc < ifc:
filtered_devs[k] = dev
devs = set(filtered_devs.values())
cls.UsbDevices[vidpid] = devs
return cls.UsbDevices[vidpid]
@classmethod
def _get_backend_device(cls, device: UsbDevice) -> Any:
"""Return the backend implementation of a device.
:param device: the UsbDevice (usb.core.Device)
:return: the implementation of any
"""
try:
# pylint: disable=protected-access
# need to access private member _ctx of PyUSB device
# (resource manager) until PyUSB #302 is addressed
return device._ctx.dev
# pylint: disable=protected-access
except AttributeError:
return None
@classmethod
def _load_backend(cls) -> IBackend:
backend = None # Optional[IBackend]
for candidate in cls.BACKENDS:
mod = import_module(candidate)
backend = mod.get_backend()
if backend is not None:
return backend
raise ValueError('No backend available')