pax_global_header00006660000000000000000000000064142134624220014512gustar00rootroot0000000000000052 comment=fbe00b721b488c1251cf8509eb5f5271c4364ec1 pyftdi-0.54.0/000077500000000000000000000000001421346242200130775ustar00rootroot00000000000000pyftdi-0.54.0/.github/000077500000000000000000000000001421346242200144375ustar00rootroot00000000000000pyftdi-0.54.0/.github/workflows/000077500000000000000000000000001421346242200164745ustar00rootroot00000000000000pyftdi-0.54.0/.github/workflows/pythonchecksyntax.yml000066400000000000000000000014661421346242200230140ustar00rootroot00000000000000name: Python syntax tests # check that there is no import issues with tool suite on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install setuptools wheel ruamel.yaml - name: Check style run: | python setup.py check_style - name: Install package run: | python setup.py install - name: Run tests run: | python pyftdi/tests/toolsimport.py pyftdi-0.54.0/.github/workflows/pythonmocktests.yml000066400000000000000000000022331421346242200224750ustar00rootroot00000000000000name: Python USB mock tests on: push: pull_request: types: [assigned, opened, synchronize, reopened] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install setuptools wheel ruamel.yaml - name: Install package run: | python setup.py install - name: Run Mock tests env: FTDI_LOGLEVEL: WARNING FTDI_DEBUG: on run: | python pyftdi/tests/mockusb.py - name: Run GPIO tests env: FTDI_LOGLEVEL: WARNING FTDI_DEBUG: on FTDI_VIRTUAL: on run: | python pyftdi/tests/gpio.py - name: Run EEPROM tests env: FTDI_LOGLEVEL: WARNING FTDI_DEBUG: on FTDI_VIRTUAL: on run: | python pyftdi/tests/eeprom_mock.py pyftdi-0.54.0/.github/workflows/pythonpackage.yml000066400000000000000000000017741421346242200220650ustar00rootroot00000000000000name: Python package on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install setuptools wheel sphinx sphinx_autodoc_typehints # Shpinx Read the Doc theme seems to never get a release w/ fixed issues pip install -U -e git+https://github.com/readthedocs/sphinx_rtd_theme.git@2b8717a3647cc650625c566259e00305f7fb60aa#egg=sphinx_rtd_theme - name: Build package run: | python setup.py bdist python setup.py sdist bdist_wheel - name: Build documentation run: | mkdir doc cd doc sphinx-build -W -b html ../pyftdi/doc . pyftdi-0.54.0/.gitignore000066400000000000000000000001221421346242200150620ustar00rootroot00000000000000*.egg-info *.pyc *.pyo **/.DS_Store MANIFEST pyusb* dist/ build/ sphinx/ .vscode/ pyftdi-0.54.0/LICENSE000066400000000000000000000030221421346242200141010ustar00rootroot00000000000000Copyright (c) 2008-2021 Emmanuel Blot All Rights Reserved. SPDX-License-Identifier: BSD-3-Clause i.e. 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. pyftdi-0.54.0/MANIFEST.in000066400000000000000000000000611421346242200146320ustar00rootroot00000000000000exclude README.md include pyftdi/doc/images/*.pngpyftdi-0.54.0/README.md000066400000000000000000000051101421346242200143530ustar00rootroot00000000000000# PyFtdi ![Python package](https://github.com/eblot/pyftdi/workflows/Python%20package/badge.svg) ![Mock tests](https://github.com/eblot/pyftdi/workflows/Python%20mock%20tests/badge.svg) ![Syntax tests](https://github.com/eblot/pyftdi/workflows/Python%20syntax%20tests/badge.svg) [![PyPI](https://img.shields.io/pypi/v/pyftdi.svg?maxAge=2592000)](https://pypi.org/project/pyftdi/) [![Python Versions](https://img.shields.io/pypi/pyversions/pyftdi.svg)](https://pypi.org/project/pyftdi/) [![Downloads](https://img.shields.io/pypi/dm/pyftdi.svg)](https://pypi.org/project/pyftdi/) ## Documentation PyFtdi documentation is available from https://eblot.github.io/pyftdi/ ## Overview PyFtdi aims at providing a user-space driver for popular FTDI devices, implemented in pure Python language. Suported FTDI devices include: * UART and GPIO bridges * FT232R (single port, 3Mbps) * FT230X/FT231X/FT234X/ (single port, 3Mbps) * UART, GPIO 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) ## 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 ## License `SPDX-License-Identifier: BSD-3-Clause` ## Warnings ### Python support PyFtdi requires Python 3.7+. ### API break PyFtdi *v0.52* was the last PyFtdi version to support Python 3.5. Python 3.5 has reached end-of-life on September 5th, 2020. 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. pyftdi-0.54.0/_config.yml000066400000000000000000000000351421346242200152240ustar00rootroot00000000000000theme: jekyll-theme-architectpyftdi-0.54.0/pyftdi/000077500000000000000000000000001421346242200143765ustar00rootroot00000000000000pyftdi-0.54.0/pyftdi/INSTALL000066400000000000000000000000751421346242200154310ustar00rootroot00000000000000Please read pyftdi/README.rst for installation instructions. pyftdi-0.54.0/pyftdi/__init__.py000066400000000000000000000021451421346242200165110ustar00rootroot00000000000000# Copyright (c) 2010-2022 Emmanuel Blot # Copyright (c) 2010-2016, Neotion # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause #pylint: disable-msg=missing-docstring __version__ = '0.54.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-2021 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) pyftdi-0.54.0/pyftdi/bin/000077500000000000000000000000001421346242200151465ustar00rootroot00000000000000pyftdi-0.54.0/pyftdi/bin/ftconf.py000077500000000000000000000177561421346242200170220ustar00rootroot00000000000000#!/usr/bin/env python3 """Simple FTDI EEPROM configurator. """ # Copyright (c) 2019-2022, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from argparse import ArgumentParser, FileType from io import StringIO from logging import Formatter, StreamHandler, DEBUG, ERROR from sys import modules, stderr, stdout from textwrap import fill from traceback import format_exc from pyftdi import FtdiLogger from pyftdi.eeprom import FtdiEeprom from pyftdi.ftdi import Ftdi from pyftdi.misc import add_custom_devices, hexdump #pylint: disable-msg=too-many-locals #pylint: disable-msg=too-many-branches #pylint: disable-msg=too-many-statements def main(): """Main routine""" debug = False try: argparser = ArgumentParser(description=modules[__name__].__doc__) argparser.add_argument('device', nargs='?', default='ftdi:///?', help='serial port device name') files = argparser.add_argument_group(title='Files') files.add_argument('-i', '--input', type=FileType('rt'), help='input ini file to load EEPROM content') files.add_argument('-l', '--load', default='all', choices=('all', 'raw', 'values'), help='section(s) to load from input file') files.add_argument('-o', '--output', help='output ini file to save EEPROM content') files.add_argument('-V', '--virtual', type=FileType('r'), help='use a virtual device, specified as YaML') device = argparser.add_argument_group(title='Device') device.add_argument('-P', '--vidpid', action='append', help='specify a custom VID:PID device ID ' '(search for FTDI devices)') device.add_argument('-M', '--eeprom', help='force an EEPROM model') device.add_argument('-S', '--size', type=int, choices=FtdiEeprom.eeprom_sizes, help='force an EEPROM size') fmt = argparser.add_argument_group(title='Format') fmt.add_argument('-x', '--hexdump', action='store_true', help='dump EEPROM content as ASCII') fmt.add_argument('-X', '--hexblock', type=int, help='dump EEPROM as indented hexa blocks') config = argparser.add_argument_group(title='Configuration') config.add_argument('-s', '--serial-number', help='set serial number') config.add_argument('-m', '--manufacturer', help='set manufacturer name') config.add_argument('-p', '--product', help='set product name') config.add_argument('-c', '--config', action='append', help='change/configure a property as key=value ' 'pair') config.add_argument('--vid', type=lambda x: int(x, 16), help='shortcut to configure the USB vendor ID') config.add_argument('--pid', type=lambda x: int(x, 16), help='shortcut to configure the USB product ID') action = argparser.add_argument_group(title='Action') action.add_argument('-e', '--erase', action='store_true', help='erase the whole EEPROM content') action.add_argument('-E', '--full-erase', action='store_true', default=False, help='erase the whole EEPROM content, including ' 'the CRC') action.add_argument('-u', '--update', action='store_true', help='perform actual update, use w/ care') extra = argparser.add_argument_group(title='Extras') extra.add_argument('-v', '--verbose', action='count', default=0, help='increase verbosity') extra.add_argument('-d', '--debug', action='store_true', help='enable debug mode') args = argparser.parse_args() debug = args.debug if not args.device: argparser.error('Serial device not specified') loglevel = max(DEBUG, ERROR - (10 * args.verbose)) loglevel = min(ERROR, loglevel) if debug: formatter = Formatter('%(asctime)s.%(msecs)03d %(name)-20s ' '%(message)s', '%H:%M:%S') else: formatter = Formatter('%(message)s') FtdiLogger.set_formatter(formatter) FtdiLogger.set_level(loglevel) FtdiLogger.log.addHandler(StreamHandler(stderr)) if args.virtual: #pylint: disable-msg=import-outside-toplevel from pyftdi.usbtools import UsbTools # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('pyftdi.tests.backend.usbvirt', ) # Ensure the virtual backend can be found and is loaded backend = UsbTools.find_backend() loader = backend.create_loader()() loader.load(args.virtual) try: add_custom_devices(Ftdi, args.vidpid, force_hex=True) except ValueError as exc: argparser.error(str(exc)) eeprom = FtdiEeprom() eeprom.open(args.device, size=args.size, model=args.eeprom) if args.erase or args.full_erase: eeprom.erase() if args.input: eeprom.load_config(args.input, args.load) if args.serial_number: eeprom.set_serial_number(args.serial_number) if args.manufacturer: eeprom.set_manufacturer_name(args.manufacturer) if args.product: eeprom.set_product_name(args.product) for conf in args.config or []: if conf in ('?', 'help'): helpstr = ', '.join(sorted(eeprom.properties)) print(fill(helpstr, initial_indent=' ', subsequent_indent=' ')) exit(1) for sep in ':=': if sep in conf: name, value = conf.split(sep, 1) if not value: argparser.error('Configuration %s without value' % conf) if value == 'help': value = '?' helpio = StringIO() eeprom.set_property(name, value, helpio) helpstr = helpio.getvalue() if helpstr: print(fill(helpstr, initial_indent=' ', subsequent_indent=' ')) exit(1) break else: argparser.error('Missing name:value separator in %s' % conf) if args.vid: eeprom.set_property('vendor_id', args.vid) if args.pid: eeprom.set_property('product_id', args.pid) if args.hexdump: print(hexdump(eeprom.data)) if args.hexblock is not None: indent = ' ' * args.hexblock for pos in range(0, len(eeprom.data), 16): hexa = ' '.join(['%02x' % x for x in eeprom.data[pos:pos+16]]) print(indent, hexa, sep='') if args.update: if eeprom.commit(False, no_crc=args.full_erase): eeprom.reset_device() if args.verbose > 0: eeprom.dump_config() if args.output: if args.output == '-': eeprom.save_config(stdout) else: with open(args.output, 'wt') as ofp: eeprom.save_config(ofp) except (ImportError, IOError, NotImplementedError, ValueError) as exc: print('\nError: %s' % exc, file=stderr) if debug: print(format_exc(chain=False), file=stderr) exit(1) except KeyboardInterrupt: exit(2) if __name__ == '__main__': main() pyftdi-0.54.0/pyftdi/bin/ftdi_urls.py000077500000000000000000000052061421346242200175210ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) 2019-2022, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """List valid FTDI device URLs and descriptors.""" from argparse import ArgumentParser, FileType from logging import Formatter, StreamHandler, DEBUG, ERROR from sys import modules, stderr from traceback import format_exc from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi from pyftdi.misc import add_custom_devices def main(): """Entry point.""" debug = False try: argparser = ArgumentParser(description=modules[__name__].__doc__) argparser.add_argument('-P', '--vidpid', action='append', help='specify a custom VID:PID device ID, ' 'may be repeated') argparser.add_argument('-V', '--virtual', type=FileType('r'), help='use a virtual device, specified as YaML') argparser.add_argument('-v', '--verbose', action='count', default=0, help='increase verbosity') argparser.add_argument('-d', '--debug', action='store_true', help='enable debug mode') args = argparser.parse_args() debug = args.debug loglevel = max(DEBUG, ERROR - (10 * args.verbose)) loglevel = min(ERROR, loglevel) if debug: formatter = Formatter('%(asctime)s.%(msecs)03d %(name)-20s ' '%(message)s', '%H:%M:%S') else: formatter = Formatter('%(message)s') FtdiLogger.set_formatter(formatter) FtdiLogger.set_level(loglevel) FtdiLogger.log.addHandler(StreamHandler(stderr)) if args.virtual: #pylint: disable-msg=import-outside-toplevel from pyftdi.usbtools import UsbTools # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('pyftdi.tests.backend.usbvirt', ) # Ensure the virtual backend can be found and is loaded backend = UsbTools.find_backend() loader = backend.create_loader()() loader.load(args.virtual) try: add_custom_devices(Ftdi, args.vidpid, force_hex=True) except ValueError as exc: argparser.error(str(exc)) Ftdi.show_devices() except (ImportError, IOError, NotImplementedError, ValueError) as exc: print('\nError: %s' % exc, file=stderr) if debug: print(format_exc(chain=False), file=stderr) exit(1) except KeyboardInterrupt: exit(2) if __name__ == '__main__': main() pyftdi-0.54.0/pyftdi/bin/i2cscan.py000077500000000000000000000124051421346242200170470ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2018-2022, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """Tiny I2C bus scanner.""" #pylint: disable-msg=broad-except #pylint: disable-msg=too-few-public-methods from argparse import ArgumentParser, FileType from logging import Formatter, StreamHandler, getLogger, DEBUG, ERROR from sys import modules, stderr from traceback import format_exc from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi from pyftdi.i2c import I2cController, I2cNackError from pyftdi.misc import add_custom_devices class I2cBusScanner: """Scan I2C bus to find slave. Emit the I2C address message, but no data. Detect any ACK on each valid address. """ SMB_READ_RANGE = list(range(0x30, 0x38)) + list(range(0x50, 0x60)) HIGHEST_I2C_SLAVE_ADDRESS = 0x78 @classmethod def scan(cls, url: str, smb_mode: bool = True) -> None: """Scan an I2C bus to detect slave device. :param url: FTDI URL :param smb_mode: whether to use SMBbus restrictions or regular I2C mode. """ i2c = I2cController() slaves = [] getLogger('pyftdi.i2c').setLevel(ERROR) try: i2c.set_retry_count(1) i2c.configure(url) for addr in range(cls.HIGHEST_I2C_SLAVE_ADDRESS+1): port = i2c.get_port(addr) if smb_mode: try: if addr in cls.SMB_READ_RANGE: port.read(0) slaves.append('R') else: port.write([]) slaves.append('W') except I2cNackError: slaves.append('.') else: try: port.read(0) slaves.append('R') continue except I2cNackError: pass try: port.write([]) slaves.append('W') except I2cNackError: slaves.append('.') finally: i2c.terminate() columns = 16 row = 0 print(' %s' % ''.join(' %01X ' % col for col in range(columns))) while True: chunk = slaves[row:row+columns] if not chunk: break print(' %1X:' % (row//columns), ' '.join(chunk)) row += columns def main(): """Entry point.""" debug = False try: argparser = ArgumentParser(description=modules[__name__].__doc__) argparser.add_argument('device', nargs='?', default='ftdi:///?', help='serial port device name') argparser.add_argument('-S', '--no-smb', action='store_true', default=False, help='use regular I2C mode vs. SMBbus scan') argparser.add_argument('-P', '--vidpid', action='append', help='specify a custom VID:PID device ID, ' 'may be repeated') argparser.add_argument('-V', '--virtual', type=FileType('r'), help='use a virtual device, specified as YaML') argparser.add_argument('-v', '--verbose', action='count', default=0, help='increase verbosity') argparser.add_argument('-d', '--debug', action='store_true', help='enable debug mode') args = argparser.parse_args() debug = args.debug if not args.device: argparser.error('Serial device not specified') loglevel = max(DEBUG, ERROR - (10 * args.verbose)) loglevel = min(ERROR, loglevel) if debug: formatter = Formatter('%(asctime)s.%(msecs)03d %(name)-20s ' '%(message)s', '%H:%M:%S') else: formatter = Formatter('%(message)s') FtdiLogger.log.addHandler(StreamHandler(stderr)) FtdiLogger.set_formatter(formatter) FtdiLogger.set_level(loglevel) if args.virtual: #pylint: disable-msg=import-outside-toplevel from pyftdi.usbtools import UsbTools # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('pyftdi.tests.backend.usbvirt', ) # Ensure the virtual backend can be found and is loaded backend = UsbTools.find_backend() loader = backend.create_loader()() loader.load(args.virtual) try: add_custom_devices(Ftdi, args.vidpid, force_hex=True) except ValueError as exc: argparser.error(str(exc)) I2cBusScanner.scan(args.device, not args.no_smb) except (ImportError, IOError, NotImplementedError, ValueError) as exc: print('\nError: %s' % exc, file=stderr) if debug: print(format_exc(chain=False), file=stderr) exit(1) except KeyboardInterrupt: exit(2) if __name__ == '__main__': try: main() except Exception as exc: print(str(exc), file=stderr) pyftdi-0.54.0/pyftdi/bin/pyterm.py000077500000000000000000000342451421346242200170530ustar00rootroot00000000000000#!/usr/bin/env python3 """Simple Python serial terminal """ # Copyright (c) 2010-2022, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause #pylint: disable-msg=too-many-instance-attributes #pylint: disable-msg=too-many-arguments #pylint: disable-msg=too-many-nested-blocks #pylint: disable-msg=too-many-branches #pylint: disable-msg=too-many-statements #pylint: disable-msg=too-few-public-methods #pylint: disable-msg=broad-except #pylint: disable-msg=wrong-import-position from argparse import ArgumentParser, FileType from atexit import register from collections import deque from logging import Formatter, StreamHandler, DEBUG, ERROR from os import environ, linesep, stat from re import search from sys import exit as sysexit, modules, platform, stderr, stdout from time import sleep from threading import Event, Thread from traceback import format_exc from _thread import interrupt_main #pylint: disable-msg=import-error #pylint: disable-msg=import-outside-toplevel from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi from pyftdi.misc import to_bps, add_custom_devices from pyftdi.term import Terminal class MiniTerm: """A mini serial terminal to demonstrate pyserial extensions""" DEFAULT_BAUDRATE = 115200 def __init__(self, device, baudrate=None, parity=None, rtscts=False, debug=False): self._terminal = Terminal() self._device = device self._baudrate = baudrate or self.DEFAULT_BAUDRATE self._port = self._open_port(self._device, self._baudrate, parity, rtscts, debug) self._resume = False self._silent = False self._rxq = deque() self._rxe = Event() self._debug = debug register(self._cleanup) def run(self, fullmode=False, loopback=False, silent=False, localecho=False, autocr=False): """Switch to a pure serial terminal application""" self._terminal.init(fullmode) print('Entering minicom mode @ %d bps' % self._port.baudrate) stdout.flush() self._resume = True # start the reader (target to host direction) within a dedicated thread args = [loopback] if self._device.startswith('ftdi://'): # with pyftdi/pyusb/libusb stack, there is no kernel buffering # which means that a UART source with data burst may overflow the # FTDI HW buffer while the SW stack is dealing with formatting # and console output. Use an intermediate thread to pop out data # out from the HW as soon as it is made available, and use a deque # to serve the actual reader thread args.append(self._get_from_source) sourcer = Thread(target=self._sourcer, daemon=True) sourcer.start() else: # regular kernel buffered device args.append(self._get_from_port) reader = Thread(target=self._reader, args=tuple(args), daemon=True) reader.start() # start the writer (host to target direction) self._writer(fullmode, silent, localecho, autocr) def _sourcer(self): try: while self._resume: data = self._port.read(4096) if not data: continue self._rxq.append(data) self._rxe.set() except Exception as ex: self._resume = False print(str(ex), file=stderr) interrupt_main() def _get_from_source(self): while not self._rxq and self._resume: if self._rxe.wait(0.1): self._rxe.clear() break if not self._rxq: return bytearray() return self._rxq.popleft() def _get_from_port(self): try: return self._port.read(4096) except OSError as ex: self._resume = False print(str(ex), file=stderr) interrupt_main() except Exception as ex: print(str(ex), file=stderr) return bytearray() def _reader(self, loopback, getfunc): """Loop forever, processing received serial data in terminal mode""" try: # Try to read as many bytes as possible at once, and use a short # timeout to avoid blocking for more data self._port.timeout = 0.050 while self._resume: if self._silent: sleep(0.25) continue data = getfunc() if data: stdout.write(data.decode('utf8', errors='replace')) stdout.flush() if loopback: self._port.write(data) except KeyboardInterrupt: return except Exception as exc: print("Exception: %s" % exc) if self._debug: print(format_exc(chain=False), file=stderr) interrupt_main() def _writer(self, fullmode, silent, localecho, crlf=0): """Loop and copy console->serial until EOF character is found""" while self._resume: try: char = self._terminal.getkey() if fullmode and ord(char) == 0x2: # Ctrl+B self._cleanup(True) return if self._terminal.IS_MSWIN: if ord(char) in (0, 224): char = self._terminal.getkey() self._port.write(self._terminal.getch_to_escape(char)) continue if ord(char) == 0x3: # Ctrl+C raise KeyboardInterrupt('Ctrl-C break') if silent: if ord(char) == 0x6: # Ctrl+F self._silent = True print('Silent\n') continue if ord(char) == 0x7: # Ctrl+G self._silent = False print('Reg\n') continue if localecho: stdout.write(char.decode('utf8', errors='replace')) stdout.flush() if crlf: if char == b'\n': self._port.write(b'\r') if crlf > 1: continue self._port.write(char) except KeyError: continue except KeyboardInterrupt: if fullmode: if self._terminal.IS_MSWIN: self._port.write(b'\x03') continue self._cleanup(True) def _cleanup(self, *args): """Cleanup resource before exiting""" if args and args[0]: print('%sAborting...' % linesep) try: self._resume = False if self._port: # wait till the other thread completes sleep(0.5) try: rem = self._port.inWaiting() except IOError: # maybe a bug in underlying wrapper... rem = 0 # consumes all the received bytes for _ in range(rem): self._port.read() self._port.close() self._port = None print('Bye.') except Exception as ex: print(str(ex), file=stderr) finally: if self._terminal: self._terminal.reset() self._terminal = None @staticmethod def _open_port(device, baudrate, parity, rtscts, debug=False): """Open the serial communication port""" try: from serial.serialutil import SerialException from serial import PARITY_NONE except ImportError as exc: raise ImportError("Python serial module not installed") from exc try: from serial import serial_for_url, VERSION as serialver # use a simple regex rather than adding a new dependency on the # more complete 'packaging' module vmo = search(r'^(\d+)\.(\d+)', serialver) if not vmo: # unable to parse version raise ValueError() if tuple([int(x) for x in vmo.groups()]) < (3, 0): # pysrial version is too old raise ValueError() except (ValueError, IndexError, ImportError) as exc: raise ImportError("pyserial 3.0+ is required") from exc # the following import enables serial protocol extensions if device.startswith('ftdi:'): try: from pyftdi import serialext serialext.touch() except ImportError as exc: raise ImportError("PyFTDI module not installed") from exc try: port = serial_for_url(device, baudrate=baudrate, parity=parity or PARITY_NONE, rtscts=rtscts, timeout=0) if not port.is_open: port.open() if not port.is_open: raise IOError('Cannot open port "%s"' % device) if debug: backend = port.BACKEND if hasattr(port, 'BACKEND') else '?' print("Using serial backend '%s'" % backend) return port except SerialException as exc: raise IOError(str(exc)) from exc def get_default_device() -> str: """Return the default comm device, depending on the host/OS.""" envdev = environ.get('FTDI_DEVICE', '') if envdev: return envdev if platform == 'win32': device = 'COM1' elif platform == 'darwin': device = '/dev/cu.usbserial' elif platform == 'linux': device = '/dev/ttyS0' else: device = '' try: stat(device) except OSError: device = 'ftdi:///1' return device def main(): """Main routine""" debug = False try: default_device = get_default_device() argparser = ArgumentParser(description=modules[__name__].__doc__) argparser.add_argument('-f', '--fullmode', dest='fullmode', action='store_true', help='use full terminal mode, exit with ' '[Ctrl]+B') argparser.add_argument('device', nargs='?', default=default_device, help='serial port device name (default: %s)' % default_device) argparser.add_argument('-b', '--baudrate', help='serial port baudrate (default: %d)' % MiniTerm.DEFAULT_BAUDRATE, default='%s' % MiniTerm.DEFAULT_BAUDRATE) argparser.add_argument('-w', '--hwflow', action='store_true', help='hardware flow control') argparser.add_argument('-e', '--localecho', action='store_true', help='local echo mode (print all typed chars)') argparser.add_argument('-r', '--crlf', action='count', default=0, help='prefix LF with CR char, use twice to ' 'replace all LF with CR chars') argparser.add_argument('-l', '--loopback', action='store_true', help='loopback mode (send back all received ' 'chars)') argparser.add_argument('-s', '--silent', action='store_true', help='silent mode') argparser.add_argument('-P', '--vidpid', action='append', help='specify a custom VID:PID device ID, ' 'may be repeated') argparser.add_argument('-V', '--virtual', type=FileType('r'), help='use a virtual device, specified as YaML') argparser.add_argument('-v', '--verbose', action='count', help='increase verbosity') argparser.add_argument('-d', '--debug', action='store_true', help='enable debug mode') args = argparser.parse_args() debug = args.debug if not args.device: argparser.error('Serial device not specified') loglevel = max(DEBUG, ERROR - (10 * (args.verbose or 0))) loglevel = min(ERROR, loglevel) if debug: formatter = Formatter('%(asctime)s.%(msecs)03d %(name)-20s ' '%(message)s', '%H:%M:%S') else: formatter = Formatter('%(message)s') FtdiLogger.set_formatter(formatter) FtdiLogger.set_level(loglevel) FtdiLogger.log.addHandler(StreamHandler(stderr)) if args.virtual: from pyftdi.usbtools import UsbTools # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('pyftdi.tests.backend.usbvirt', ) # Ensure the virtual backend can be found and is loaded backend = UsbTools.find_backend() loader = backend.create_loader()() loader.load(args.virtual) try: add_custom_devices(Ftdi, args.vidpid, force_hex=True) except ValueError as exc: argparser.error(str(exc)) miniterm = MiniTerm(device=args.device, baudrate=to_bps(args.baudrate), parity='N', rtscts=args.hwflow, debug=args.debug) miniterm.run(args.fullmode, args.loopback, args.silent, args.localecho, args.crlf) except (IOError, ValueError) as exc: print('\nError: %s' % exc, file=stderr) if debug: print(format_exc(chain=False), file=stderr) sysexit(1) except KeyboardInterrupt: sysexit(2) if __name__ == '__main__': main() pyftdi-0.54.0/pyftdi/bin/uphy.sh000077500000000000000000000015161421346242200164750ustar00rootroot00000000000000#!/bin/sh # Load/unload kernel extension helper for macOS system=$(uname -s) if [ "${system}" != "Darwin" ]; then echo "This script is dedicated to macOS" >&2 exit 1 fi version=$(sw_vers -productVersion | cut -d. -f2) if [ ${version} -lt 9 ]; then echo "This version of OS X does not use an Apple FTDI driver" exit 0 fi if [ ${version} -gt 13 ]; then echo "Apple FTDI driver on this macOS version should not be unloaded" >&2 exit 1 fi kextstat 2>/dev/null | grep com.apple.driver.AppleUSBFTDI > /dev/null if [ $? -eq 0 ]; then echo "Admin priviledge required to unload Apple FTDI driver" sudo kextunload -v -bundle com.apple.driver.AppleUSBFTDI else if [ "$1" = "0" ]; then echo "Admin priviledge required to load Apple FTDI driver" sudo kextload -v -bundle com.apple.driver.AppleUSBFTDI fi fi pyftdi-0.54.0/pyftdi/bits.py000066400000000000000000000426561421346242200157260ustar00rootroot00000000000000# Copyright (c) 2010-2019 Emmanuel Blot # 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-msg=invalid-name #pylint: disable-msg=unneeded-not #pylint: disable-msg=too-many-branches #pylint: disable-msg=too-many-arguments #pylint: disable-msg=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("Cannot initialize from a %s" % 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 '%d: %s' % (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("Type %s cannot be converted to byte" % type(self)) def tobytes(self, msb=False, msby=False): raise BitSequenceError("Type %s cannot be converted to bytes" % type(self)) def matches(self, other): 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 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) pyftdi-0.54.0/pyftdi/doc/000077500000000000000000000000001421346242200151435ustar00rootroot00000000000000pyftdi-0.54.0/pyftdi/doc/api/000077500000000000000000000000001421346242200157145ustar00rootroot00000000000000pyftdi-0.54.0/pyftdi/doc/api/eeprom.rst000066400000000000000000000022731421346242200177410ustar00rootroot00000000000000 .. 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 pyftdi-0.54.0/pyftdi/doc/api/ftdi.rst000066400000000000000000000010201421346242200173650ustar00rootroot00000000000000.. -*- 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 `. Classes ~~~~~~~ .. autoclass :: Ftdi :members: Exceptions ~~~~~~~~~~ .. autoexception :: FtdiError .. autoexception :: FtdiMpsseError .. autoexception :: FtdiFeatureError pyftdi-0.54.0/pyftdi/doc/api/gpio.rst000066400000000000000000000017611421346242200174110ustar00rootroot00000000000000.. -*- 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 ` and the :doc:`I2C API `. .. 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 pyftdi-0.54.0/pyftdi/doc/api/i2c.rst000066400000000000000000000131721421346242200171270ustar00rootroot00000000000000 .. 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 and FT4232H 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) the current sourced from AD0 may prevent proper sampling of the 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 or FT4232H devices. It should not be fit with an FT232H. * ``AD7`` may be used as a regular GPIO with clock stretching is not required. pyftdi-0.54.0/pyftdi/doc/api/index.rst000066400000000000000000000003001421346242200175460ustar00rootroot00000000000000API documentation ================= .. include:: ../defs.rst |release| --------- .. toctree:: :maxdepth: 1 :glob: ftdi gpio i2c spi uart usbtools misc eeprom pyftdi-0.54.0/pyftdi/doc/api/misc.rst000066400000000000000000000002421421346242200173770ustar00rootroot00000000000000.. -*- coding: utf-8 -*- :mod:`misc` - Miscellaneous helpers ----------------------------------- Functions ~~~~~~~~~ .. automodule:: pyftdi.misc :members: pyftdi-0.54.0/pyftdi/doc/api/spi.rst000066400000000000000000000127041421346242200172450ustar00rootroot00000000000000.. 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). 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. pyftdi-0.54.0/pyftdi/doc/api/uart.rst000066400000000000000000000226331421346242200174270ustar00rootroot00000000000000.. 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` 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.* pyftdi-0.54.0/pyftdi/doc/api/usbtools.rst000066400000000000000000000003421421346242200203170ustar00rootroot00000000000000.. -*- coding: utf-8 -*- :mod:`usbtools` - USB tools --------------------------- .. module :: pyftdi.usbtools Classes ~~~~~~~ .. autoclass :: UsbTools :members: Exceptions ~~~~~~~~~~ .. autoexception :: UsbToolsError pyftdi-0.54.0/pyftdi/doc/authors.rst000066400000000000000000000012721421346242200173640ustar00rootroot00000000000000Authors ------- Main developers ~~~~~~~~~~~~~~~ * Emmanuel Blot * Emmanuel Bouaziz 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 pyftdi-0.54.0/pyftdi/doc/conf.py000066400000000000000000000053571421346242200164540ustar00rootroot00000000000000# Copyright (c) 2010-2021 Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import os import re import sys # pip3 install wheel # pip3 install sphinx-autodoc-typehints sphinx-pypi-upload sphinx_rtd_theme # python3 setup.py build_sphinx # sphinx-build -b html ../pyftdi/pyftdi/doc . topdir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) sys.path.append(topdir) def read(where, *parts): """ Build an absolute path from *parts* and and return the contents of the resulting file. Assume UTF-8 encoding. """ with open(os.path.join(where, *parts), 'rt') as f: return f.read() def find_meta(meta): """ Extract __*meta*__ from meta_file. """ meta_match = re.search( r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), meta_file, re.M ) if meta_match: return meta_match.group(1) raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) meta_file = read(topdir, 'pyftdi', '__init__.py') version = find_meta('version') needs_sphinx = '2.1' extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx_autodoc_typehints'] templates_path = ['templates'] source_suffix = '.rst' master_doc = 'index' project = find_meta('title') contact = '%s <%s>' % (find_meta('author'), find_meta('email')) copyright = '2010-2020, %s' % contact show_authors = True html_theme = 'sphinx_rtd_theme' htmlhelp_basename = 'doc' preamble = r''' \usepackage{wallpaper} \usepackage{titlesec} \titleformat{\chapter}[display]{}{\filleft\scshape\chaptername\enspace\thechapter}{-2pt}{\filright \Huge \bfseries}[\vskip4.5pt\titlerule] \titleformat{name=\chapter, numberless}[block]{}{}{0pt}{\filright \Huge \bfseries}[\vskip4.5pt\titlerule] \titlespacing{\chapter}{0pt}{0pt}{1cm} ''' latex_elements = { 'papersize': 'a4paper', 'fncychap': '', # No Title Page 'releasename': '', 'sphinxsetup': 'hmargin={2.0cm,2.0cm}, vmargin={2.5cm,2.5cm}, marginpar=5cm', 'classoptions': ',openany,oneside', # Avoid blank page aftre TOC, etc. 'preamble': preamble, 'releasename': '' } latex_documents = [ ('index', '%s.tex' % project.lower(), '%s Documentation' % project, contact, u'manual'), ] # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. latex_toplevel_sectioning = "chapter" man_pages = [ ('index', project, '%s Documentation' % project, [contact], 1) ] texinfo_documents = [ ('index', project, '%s Documentation' % project, contact, '', '%s Documentation' % project, 'Miscellaneous'), ] def setup(app): app.add_css_file('https://fonts.googleapis.com/css?family=Raleway') pyftdi-0.54.0/pyftdi/doc/defs.rst000066400000000000000000000036761421346242200166320ustar00rootroot00000000000000.. |I2C| replace:: I\ :sup:`2`\ C .. _FT232R: http://www.ftdichip.com/Products/ICs/FT232R.htm .. _FT230X: http://www.ftdichip.com/Products/ICs/FT230X.html .. _FT2232D: http://www.ftdichip.com/Products/ICs/FT2232D.htm .. _FT232H: http://www.ftdichip.com/Products/ICs/FT232H.htm .. _FT2232H: http://www.ftdichip.com/Products/ICs/FT2232H.html .. _FT4232H: http://www.ftdichip.com/Products/ICs/FT4232H.htm .. _FTDI_Recovery: http://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: http://www.ftdichip.com/ .. _PyUSB: http://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: http://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: http://zadig.akeo.ie/ .. _FTDI macOS guide: http://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 .. +++++++ pyftdi-0.54.0/pyftdi/doc/eeprom.rst000066400000000000000000000441601421346242200171710ustar00rootroot00000000000000.. 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 ` 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 ` 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 ` 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 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 `, 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 `, 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 switch. It is possible to select which section(s) from the INI file are loaded, using :ref:`-l ` option switch. The ``values`` section may be modified, as it takes precedence over the ``raw`` section. Note that without option :ref:`-u `, the EEPROM content is not actually modified, the script runs in dry-run mode. .. _option_l: ``-l `` Define which section(s) of the INI file are used to update the EEPROM content along with the :ref:`-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 switch. Unsupported feature are ignored, and a warning is emitted for each unsupported feature. .. _option_M_: ``-M `` 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 `` Assign a new manufacturer name to the device. Note that without option :ref:`-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 `. .. _option_o: ``-o `` 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 `. * ``[raw]`` that contains a compact representation of the EEPROM raw content, encoded as hexadecimal strings. .. _option_p: ``-p `` Assign a new product name to the device. Note that without option :ref:`-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 `. .. _option_pid: ``--pid`` Define the USB product identifier - as an hexadecimal number. This is a shortcut for `-c product_id` .. _option_S_: ``-S `` 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 `` Assign a new serial number to the device. Note that without option :ref:`-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 `. .. _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 pyftdi-0.54.0/pyftdi/doc/features.rst000066400000000000000000000054541421346242200175230ustar00rootroot00000000000000.. 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_) 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_ 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. pyftdi-0.54.0/pyftdi/doc/gpio.rst000066400000000000000000000371511421346242200166420ustar00rootroot00000000000000.. 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 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 `. 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) pyftdi-0.54.0/pyftdi/doc/images/000077500000000000000000000000001421346242200164105ustar00rootroot00000000000000pyftdi-0.54.0/pyftdi/doc/images/i2c_wiring.png000066400000000000000000000124511421346242200211550ustar00rootroot00000000000000‰PNG  IHDRÄ¥òPLTEÿÿÿþÿº†…‡ÂŠ„„„OÚÌIDATxÚìQŽÓ0†GõZ\YâÝb8@r É÷àþO8»NpR`7­=Ýÿ[–$5¿¿c'[ áb"2Þp“&Ì:?oó/—ÆÏ§ÿ55(Ö¦X²{9$˜ÔÁžX¢œ*ri¼ºLÍøq9gyQO—¨¦æÏÅf¹”á.S«}ލ¬ˆK~¾å¯EÌÕ~VÜcj5¢äȵâœß,Š³Æ¢xŒ©UÅRriXOÅëür¾V,ú³â޽Øpލ«SÔüEqíL›>Í¥£<™Úþ%G¾ÔÓ)¶%r+\_—K™Z¥œ¿Q¬iKÁ U±ä7«©™‘3iVlü¡b⢸o£ V¦x›¿ìš(ãûM­†©ŠW׋eS_óº—Ë2µŽŠ½DÌxu; #úŠK¿ÚQ”ËòtÛQHãªåÀêöÅ¥írK¾ÚL˜ûí‹™¹Ö„¦m·©­¡.€º§|JéæNþËW‚VìÆP|$Ç#ò;G=±(&œÓYÆ)¿MñCÍßLíâú–dU+>Îoc°c(1ýtNg§8Î?«—Bv—¾åcCŒ²èºQÖÝwåÿ)ùÛ¡@tI#Û9¢ N瞢䗯fH¿¨6rÅt¬xþE.ôWœ¸| n˜ú»òSÎßÍŠ»•OÍáD±Æ2.ù©äoí‹â^åS#Fr:oðÚüíPR|é­ø2GæFó=ùCÎ߉âþÿBSrã<.9-¿ •ánä-OúÖù(ȆMþvj¢¸kùØu+>È_‡º+žƒÍ3óËÐÏ‹cŒŠ-"ÝÁÔ†P,ï¶bÅ‹Hw8µK!(V,]×OmÅV¹âx¬Ø¡Ø¦„š{ñ²ª¹£© ЋcJ¨y¹‹%;µþË”À:b$U´ù›!QÜqj6éV|”_†FPL¤[ñÝüPüIÛPnæã‚2Åsþ¸Ÿß†rëñ- {qL¨Uegjþfjý‰B“Åœ?]åo§öv¡ßÔr P ŨRñ’Ÿ&ÊÛ©½]mìڋŬMŠI¥bÉ/Š«ÇÍç(lR,úû)¶ÊÛ¢˜ÅtSl»*¦¢X%’?+n‡lQÜ ¡\1+&Gq|]ÅqŤZ±½§˜ ø Åa`Å­Šÿ!?C±Wu+¶á8„b(V·@±6†WgÔÞ@—ü»ŠmLt¿†âCÐ(5 (ÞŠ¡ø>P¬ (nù„Šñ¤m(†âû@±: ¸ŠÕÅ P¬(n€bu@q«Š^]1çob(.@±Å¿þPlÞ¡øË9A>Η7AήbèâÊC³‡âÊc£oÀrÅÿ kŠ[ XPÜòêŠ=?à "žâÇÀÌŠÿ.*q™³t¿ ¶0(q‘swnE‡óëz#®wAÇ4±&^­°MÚJ|ØbÃÄÆ*ñ-uÖõ;ç5 %NE‰{‹Ë(qoQb™n‰¡½8³%VâoMŒ/ú«I⡹dÇOhòã]‡æ’AlŒßIÌW>ù¡ÄJÜH²ˆé¦ÄJ< £dðˆD‰Ïãñˆˆ>l¹µxMˆï3U‚˜L£1·Â!uw½èSÄDˆoo¿nÑ]ÝE/Ä %ˆW»´—­±*1vC<õ‚˜å±©Flº!F&Æ)=#¾?€¤yâ T‰o"–]±Ïá&1µÆzÄœæ;Š”°<Ü!Ý·r¸ë‰8¡$ÇÅ8µwDÒåÔã›§SB‰•˜3â%Vâ~‰7¸Ò÷ÜÓÀ²Äß•1‹+±ggÈ$¶ðOI‰ßEl¿3q…01@¤¥V2V¼?Ú&æ.¶Z$±9E ÏÃvòˆy÷žŸB~è®e ˜ ÞKñÖ‰9rá ý³+mL쎉Yرg%v$LÆ]”Ý'FDA\¬/Æ”7üÙÄ´;=J ñÎ:úßeb5mèSÄô ž¨Š/ØÍ8Ó iÃÄž¿ßš˜Ù°p‚øÅœâ€3‰íüã½7Lì’ÄqÄ,n«ïüì}L<~B _ùl¢p’ø"…_dœMì’Äö€xXÇÈ:¡ø¤g=n¥‰=ž%æañ^€„&"†øõš2‰ rà°)vƒŒ*w‚øD 3±Ð”œ¯¯\!^L×ÿêg&Ÿx oåçk‘? “ø[7å‹Ä°Al]-b²•E'wIü…œ ®°Ò&‰ß-úΗ: S¥£ N8ÑQbî^s—Ä¿ÍÄËÍW®2~êp'wY눓AÄNˆÉ¸Â ‰mi•"hîçé~=ab?`1??õ ÀD ‡SôŽì ã×ÍóÌ™¤q5ÆpŸIÌÆA–;¶Ž¨ÏM ·Ë~½ 4pËSËg[GÆ»ùCÒ¦-¯ íËÄXš˜3ù_·†|b2~ØFÛ¹Û`bwôFíÖß±‰ÉøAâä’ü.1ïÈÜÈXi+Il݇ÄdœA\ã¬"Нj”pÃIb2>C\íô(¯ö0hÆJ[)b0“qÄì˜VØž'&ãÄ•ê(«W\MÄð1ñjÙ¾yâ1)<âª%–/î)¸¢ï)bs‚¸RMÛ #Ÿ’«óå‰ g¤â^쮼ïΧZÈâ…w`¾q:RˆÿoÄÖ%äóˆ%¤ÇÿŒî –»îÊÿzÁ•ß|¯4J‰ƒq;ÄGÕ@]vëR>@âw<(æŠþŠÄ\põˆíšØ=b]½AÛqÁUš¸é©‡Úý¡VMbYpµÞ â¹²ÁW¥EëUªÕÛî€nU‰ÓW½sÁÝ·@üaú!†¤½Ê7 †÷ üF‰]âñ~>:¥ubÛ=±ˆÛí(’'ù»!%¹£àʶLÜûÇXÁ{lpäI~%¾—ؘVGß…Ø…›ßH¢Ä"ùWt¯Äûɾˆ{%¾’ƒ‚+z@‰“1ÊèuDQ‚xLË‹ˆ5¼ Ô"±ÇÓÄÆêì®`Á÷sb©QâTÁÕârDgª”øäEÄh3(ñ•®8UÓÆ¸g¯ª¢ÄWJ\„Ä#J|%8_DL‰ï-¸¢{¯ÄGÉ¿ˆ˜˜6?RP‰•ø JÜ-1Ìõ J|+±Ub‘Â5mÖiG!Rö W }qˆwJ<Ÿ»Ó¾xV?Z‰÷ˆÃM‰o%vJ,’}±¸ ¤Ä÷\)ñ‰ä^D,µ£¸¹àjôz¸»¹à Å®tÐV¶àjD¿&Öe âCÔõâ?µ‘¢çîþ²w‡)nÃP€0ýr£]`ˆ=Jï„ڪ̳WqžVòHšG» i ˷‰ÙQÖ1‰ ^Qpõ%"§«‹‰½ˆTK¬®„øšÅn“8ñè™Z‰¥D ‘x·Œ+%ÖJÄÂ=W‚X_ÆCgÚ6˸vâñøoʼnÓe\)1~H–1‰3Ë2î…¸ô¾²ŒIœ‰X–q/Ä&Ê»$/˸b[šX~µb9>Ø2ÄòÄnˆ‹o$˸â¡8±¼âõFl¦BÄòŠGâLIJŒ&N¶”›üIJŒ+&~]"†B<úˆ]ši;&6ňeWL¬îÚx{B<•&}µÄ›À4ñ¼Œ[&vÉ®v(N<új‰JàêþÀ ž—q¥ÄJ‰X¸‡‘iýßj‰_®"1DàÊ×J¬”ˆ…?«±Nb= ä–\ý©ó@N<úˆ•ÀUü ¸ª”ø°D „xôM¿\]NKlÛ ¾ ¸Æ%>ñ°±FÃÕèÏ»®åp4[¿ÑùÞõÄ%b(Äó2bõ(pM,Ù‰ÚÛ瀕ˆ9Œ¶ÁÑëÄò†g0SøñúÚ!Ä`%báå.,cïO'$¶—^Ç‚®æ JÄF¥d%²&ÄQ90¯Ë¬D̹¤D¬äÅL!VÞ²™#bk×Á \¡ìNVñ÷_eD0!„x¡#^pc”¡áê}bkíŽXÇ+p…Òpõ±‰oÚ¦±!Þ—ˆá4\bíX!Ž‹v,^ \Á4\~¹‹Gà”ØXKâƒe|šØ<'¼›AáL›¸ c4\<õH‰ãò#À*[i¸úàzØž@›åkx,p5­(p%—¶Äf׺l&°±ùæQOàJ.f ñz6ºž@[ðKòa€Wˆ—äщã2îìƒ%äÀU3Z ¸ªþCþ+W£ïŠø’À•ï0pU˜xô=E‰e÷C<1j¦­b»!Ë´5BL…›dKÁ©|‰XµÄjàê†A<úZ‰•]/ÿ`IÆ·@œD¸2m5—ˆ)»6:˜]›)û¸r ™¶–KÄ@2mÕžz€÷Q"æP2mµŸ\=Pvm¬˜øU‰ØåoÚ*C \±DlË´‘˜%b:ñ𵍙¶ê‰í2Й¶ú‰§øñ³ Ĉ™¶Ú‰ãç,[¦%â®JÄñÌ \"Fb®tb³3p&±þ3p•‡x â–ˆ!+«û}=ì]"Vñþh!Þ÷Öˆa+«ýoòǯ°%bˆÄJ‰˜eàêW2mqܳË@ \}žiÓ‰c®~¯Dl¾‘i«8på/KÄÂý)öµC¬wIË“U¬ßºŽo‘DDDDDôµºÈž¡yejë7×RÊYïÖãÐr” ™Cz¾õÍyU´ÑŠ¡OçqhC'ÃèÖÏŠ6êg+çgÅ4˜mÿ'ƒóæ°zŽnýu2l=Õå¼*í¬WEÕë²>W´„ádÈÁ s–†u¬€aA $®a`Qd&[dÌ© =2¤k)*`x­åÌýe 0ÀG+~‚៾ÞË7ü ¦á7LÃ_`xeïnr‡a(L/ɈwœdáûŸi"ÉŠéÜŸE@|D-[VågY–Ó¢"È@2 bÿ§Òâd Cg¸¡lN†çv!CeXÈÀ!’ d È0ˆ¸$ÄËüÛê™Ä]ÈÀ]È@2 d ¾^Ôm)…·C‘Z^üµ[Û ÆÜkßò1,û²v¥žð%¬‘›¡îûÉÞ’žÁ… ÏÍSõ÷žd@i Ë‹Ág_íîÈàK;+9öëòM阡3„çE>†Þb“'"ƒ/­ M¾¤c¸ô.ÿÜ#Ësgð­\.µ,•¿ë#/C݉…u}ÌÏðU|Ië3R|ð2ìãÈÕ3ÈЧ7ÅGR†:D’ Ýá!dY… d ²1(ìPIÊ`¢‡JN­ÉÇJRD cµ“UÕ'\Ò ÇJN†ÿ˜IÇ È@2|›)”rÌ ,CM40 „akGŃ¢8%aØð¦@m”4 Š-WEfuX`Àd í’£3À°À 5ãv„Ñs®±AÑìW9Œƒº_øAs=)´æ¢Á“È/¹î 2C»ÆqèSƒì “iµCºw 5%CMäÈ€³›&‡&µ¹J‡Èƒ6;ƒâ5P2`€:ŠÀDó†Î§OEiœ–áóí=×R(¦½)~ °–‡AP“ ŠÎäaPD†–¾mB²0Hgا A43CœRäah¡çœLó×5d È@†“À»…|'ÈPƒ ¿×÷Š?d È@†wfðÁÏu»_?‹)üœÁkô“½²7Ýça(?`¸ÇŠOÄpóûˆ¡í¢Iûæ­roå< ±·¼3€ÎÐÎÍÄp=e8vÉÁÍ[$døÇÞÝ&» Baf»€N6Àœé2l Ó±Kè>ºÿ_½B^IHÐÈ©•¶õöç¨È{ѯÚ~2à ™•à š2…¹O aÓŽ®þûÙ|*Tö¸ãp˜˜±ÂŽÓßÏ{ .þVN†“á¶×çAñ†!kK]Pk²M0¼ƒ¨‡I3`_O†P69(È!)-y†Í ˆÜ̈øïz¿Z,Y·¤2È_0sž´Åc65dp‘ÁÒ KïÊ:†ÿÕ wgp0ä§ÇB_¤Ü6x¾‚ÁU6ÎgðXÈ»]ŽÃÀu¾!o¿„ú•f>ýŠÚþÚš—¢lÀ9/Ìí ¿\%ì?ö øÛ2†øO_Å0+†8§·}‘—gì†÷8x› Pc¸TÒÝOÛ0˜)V¾d(ÞS`*Ø€M•SCÆp™Á€fÃÇ T` ´y!–¿£¤õØTc¸\Ãã)òš34(8E>1›ÉаÔÒå …òcü ü ôŠÁÐ >ìá ˆ5Kˆ! õ{ Y>ƒD ž>28ÜÃÏb¸ø6·V8Ñ€!ÌÀp‘càYÎ`k yácv †*ÌÒ-æêÆ4#ZQ|(€ák«¹ð)kEÎfÀÿáz4¥Ñ˜³»‹Gcº)Cl: 1Ö©šhãÈh¾"¿µŠµ§m¶ ·f(n°bͨ0.­®©’á>ÄãäÒ–—Gž¡~£ T†Âfßæñææ´Æ**& ¨ü×à«àÕ†{²Mïö»¶Á·yX·vs”ê­USÃ6=Ó¨ ‹v­-ƒÅµ} †ÏŽqÔ ²¥Þ™— ¬|“TxGF*µzËð²*¨|òlÌ`RKo#†²výµŠB7P>«‡a¬e2R †>b¨7¼_Þ=3pYÅ@ø£ÐGeùÊP(}3VÎfh׊¬‡w—g³ú  þÓ»83±d=¼Ãv1—uÆ•þ`™ÓÂòð.1 µjÃ`‰÷¿C~œ6­ ïD(cÈ#ÝœA¼ÔÃ;h¶ïÈ&ÖÝšÁWÂ;04 ï¬CžåÑrùðNŒ üÖ ,ñ2¼“:(ržŠ+R+1†¯`àBJü Ö=1Ù‚!îy!¼f@NE¸e¥V·<‹ëdR`¦÷•:Â;y§A[jE‡yÄÍîý N_'\¡œ y‘ïȈ¥V}<þÊž a2ìóah–lzSô3LN‘'®‹ŽÂ|E¡]0˜Õ FëÔ*.ÅicK¨V¸Ï´D¤!»ÑnËà2«cG*ƒŒÚ1Pª„˜"™È0¬ïx¡ì¯`°‘ÁÝŒ"†K…©ÕÊ &ºä –aXÞÕÌ ¼s‰î NC%¼C›‚AEöÆ`ÉmÎP ïøCëÔ*2»_0Ýæ µðŽ¥D,9‰ *R«§ðŽeD Ch•[RÆ ŸZrO fk 9+2ðúÆ S•1¤RÞñ§Ö ôÄ`' *R«ÇðÎ7K­Pßh´0<„w" Öic˜WZ3(ìoø”agÝ.õ>™³Î‘9t—,E†ƒç·›Áƒ3„ð®ÖõtœðŽ56PâÒ¢,¼Ûƒ3´ùƒ—máÝ{:¸Q–Z]p‡)Â?ÔW!e ^œÁ†<»³Nex „Èå$c6O­^‡w^êJAXF7$9á¯}z^äê!g.gÀ#I­SÞá±9ÍÂ;0¤d" GŠu†w2ßAo' 4e0¤!¼óOCÕ%ž †tÃët†wÒ ì1¨L­xAæ À9e კ2àJA*ûÿÃ0¨1¼ó’ hBNZ‘NCj%Þ¡tO¡6¼ ë„Ì!š¸°µ‡þùçEž \úé}²Äåðp‘áÐ]²éеۿÐI¬Ôèd@jUÉïöÞ¹Äp¦V'3¤Ù¡¯`8Z»Áhw­ÈJx'Ê€ð.®×ÞPuÛ„w`PÞñ6™gÐ#¼KKŠÃ;A†§®*︋î)µ2¦ex5áÎ ÷B€!ï°^oxÁð.­×Þ1ƒ|xwcPÞñ:I†<ÌUÞ…/¸J‡w‰Amx'È`ž4‡w<ý? Û‡wå—½ 3ð,ü¼1è ïÀ ™ZQœºÍû/{“e0tÃ07†3¼;Ã; Ý.Ÿ 9;^'\qXÉÁÇS‹:rj•ú Îðn.Ã_³ºüUËà"ƒ;v†‰SäÉp2LFל »d˜ñØL ÿÈàÅP}læ1Š‡ÔªÂ°ÛvÆúÈ»^jáÝÅ/y×/ûðnñ»kze¨„w¼zá³>ûëÚ‘w?;dh?älO.ãÏÚ¼Æ>Þ†w¼ 3«Cw ï¼Æ~Já]Z¹4®öÆ ’Z ãÉ«ÃÎæ…wÃx†z—ìøsß 3ûaÜ5C5¼CuØ1Ãüðn÷Ìઠ¨ûeXÐ%;Œ'C¬»g˜“acW ·¸ækšfè„CTÆ`C½:tÈPï@´ü-gÃØCØUô7€ý0\0 :ôÄà_…w`úlŒö0vÈð*µòËS+T‡n.×Â;7¯ß¡#†Ëufx·¼Œ?»dÈÃ;^XÞ ãŸNüwÉðnüÝ#Cáeo`ø$¼:a¨9»® ï¾urP4dÀy±C„w­†±?_ ïüá] "á]¿ mûÞ„»“!V‡}3XºÍjÕa× “‡×ªÃ¾ÒKá«áÝž¸X7/¼;Bu8P^1P­¨g ¹áÝ®,- ïÊ õßmrr3†ñ5UHYxçó'ýX2`¨U‡× <±ù˳ùãä+”…w df3 c–Àâ!sü NYx‡ï™Æƒç†we Ø ÓÙ†ø'¾d³çEÃ;0à9äl_0à »‹“. é ï¾ zŸhix7Ž/ —3Øì¨Ðò²7|Ƀ™É0”ÇYWdà%ƒÖ© ï2†¥¥Ì@³HIxwÁP«å ¨ ¿ÿ<3a ¢ŒîXªÂ;0, ïê xO‰Á:Má]ØðaxWgH;›éÐÑ”Z…ɇáÝ•iÁû/Ë ·UªÂ»P%†wÕSd‘Á¥-‘‰¶ ïJ/{‹"Ÿ†wï.˜4Ÿ¦µEUxWa¨VS(„¦è„ OÃ]/{ "Ã;réÖ x=Ám«¶ðŽ'mÃ;¼Ö ¼Œ©±Nw·K“ðŽL­òÞ§Õ¡«Þ§Ö ¨{î‹DYÞYª•}0puØuý½<¼KåàáÝìêp2„êp2ükïR†a(€ß øF70¹ØÜ5ØŠk7±Q1iÁÒÏ¢e˜UJBGóxÀÇ <`Èã0äqò8€£V`X*y÷<Ã’É»/0,˜¼ûÂÉ;0€ `Àð`_. Beware the online version may be more recent than the PyPI hosted version, as intermediate development versions are not published to `PyPi `_. 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 `_. PyFtdi development code is available from `GitHub `_. 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) 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 pyftdi-0.54.0/pyftdi/doc/installation.rst000066400000000000000000000170031421346242200203770ustar00rootroot00000000000000.. 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" .. 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 ``ftdu_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 ` ``udev`` rule file. pyftdi-0.54.0/pyftdi/doc/license.rst000066400000000000000000000041121421346242200173150ustar00rootroot00000000000000License ------- .. 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 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. pyftdi-0.54.0/pyftdi/doc/pinout.rst000066400000000000000000000046251421346242200172220ustar00rootroot00000000000000.. 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_ and FT4232H_ do. Note that FT4232H_ 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.pyftdi-0.54.0/pyftdi/doc/requirements.rst000066400000000000000000000017421421346242200204240ustar00rootroot00000000000000.. include:: defs.rst Requirements ------------ Python_ 3.7 or above is required. Python 3.6 has reached end-of-life on December 23rd, 2021. PyFtdi *v0.53* is the last PyFtdi version to support Python 3.6. PyFtdi *v0.52* is the last PyFtdi version to support Python 3.5. 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. pyftdi-0.54.0/pyftdi/doc/testing.rst000066400000000000000000000102321421346242200173500ustar00rootroot00000000000000Testing ------- .. 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. pyftdi-0.54.0/pyftdi/doc/tools.rst000066400000000000000000000071261421346242200170430ustar00rootroot00000000000000.. 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 `` 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=]:[product_name=]`` * ``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 ` would be ``ftdi://ftdi:0x9999/1`` * ``mycompany=0x666:myproduct=0xcafe``, *vid:pid* complete syntax with aliases; matching FTDI :ref:`URLs ` 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 `` 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`. pyftdi-0.54.0/pyftdi/doc/troubleshooting.rst000066400000000000000000000074071421346242200211340ustar00rootroot00000000000000.. 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 `_. 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=`` where ```` is the directory containing the ``libusb-1.*.so`` library file * On macOS: ``export DYLD_LIBRARY_PATH=.../lib`` where ```` 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 `` where ```` 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+. pyftdi-0.54.0/pyftdi/doc/urlscheme.rst000066400000000000000000000075051421346242200176730ustar00rootroot00000000000000.. 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``, ``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. pyftdi-0.54.0/pyftdi/eeprom.py000066400000000000000000001265061421346242200162510ustar00rootroot00000000000000# Copyright (c) 2019-2022, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """EEPROM management for PyFdti""" #pylint: disable-msg=too-many-arguments #pylint: disable-msg=too-many-branches #pylint: disable-msg=too-many-instance-attributes #pylint: disable-msg=too-many-locals #pylint: disable-msg=too-many-public-methods #pylint: disable-msg=wrong-import-position #pylint: disable-msg=import-error import sys from binascii import hexlify, unhexlify from collections import OrderedDict, namedtuple from configparser import ConfigParser from enum import IntEnum if sys.version_info[:2] > (3, 5): from enum import IntFlag from logging import getLogger from random import randint from re import match from struct import calcsize as scalc, pack as spack, unpack as sunpack from typing import BinaryIO, List, Optional, Set, TextIO, Union, Tuple if sys.version_info[:2] == (3, 5): from aenum import IntFlag from usb.core import Device as UsbDevice from .ftdi import Ftdi, FtdiError from .misc import classproperty, to_bool, to_int class FtdiEepromError(FtdiError): """FTDI EEPROM error.""" class Hex2Int(int): """Hexa representation of a byte.""" def __str__(self): return '0x%02x' % int(self) class Hex4Int(int): """Hexa representation of a half-word.""" def __str__(self): return '0x%04x' % int(self) class FtdiEeprom: """FTDI EEPROM management """ _PROPS = namedtuple('PROPS', 'size user dynoff chipoff') """Properties for each FTDI device release. * size is the size in bytes of the EEPROM storage area * user is the size in bytes of the user storage area, if any/supported * dynoff is the offset in EEPROM of the first bytes to store strings * chipoff is the offset in EEPROM of the EEPROM chip type """ _PROPERTIES = { 0x0200: _PROPS(0, None, 0, None), # FT232AM 0x0400: _PROPS(256, 0x14, 0x94, None), # FT232BM 0x0500: _PROPS(256, 0x16, 0x96, 0x14), # FT2232D 0x0600: _PROPS(128, None, 0x18, None), # FT232R 0x0700: _PROPS(256, 0x1A, 0x9A, 0x18), # FT2232H 0x0800: _PROPS(256, 0x1A, 0x9A, 0x18), # FT4232H 0x0900: _PROPS(256, 0x1A, 0xA0, 0x1e), # FT232H 0x1000: _PROPS(1024, 0x1A, 0xA0, None), # FT230X/FT231X/FT234X } """EEPROM properties.""" CBUS = IntEnum('CBus', 'TXDEN PWREN TXLED RXLED TXRXLED SLEEP CLK48 CLK24 CLK12 ' 'CLK6 GPIO BB_WR BB_RD', start=0) """Alternate features for legacy FT232R devices.""" CBUSH = IntEnum('CBusH', 'TRISTATE TXLED RXLED TXRXLED PWREN SLEEP DRIVE0 DRIVE1 ' 'GPIO TXDEN CLK30 CLK15 CLK7_5', start=0) """Alternate features for FT232H/FT2232H/FT4232H devices.""" CBUSX = IntEnum('CBusX', 'TRISTATE TXLED RXLED TXRXLED PWREN SLEEP DRIVE0 DRIVE1 ' 'GPIO TXDEN CLK24 CLK12 CLK6 BAT_DETECT BAT_NDETECT ' 'I2C_TXE I2C_RXF VBUS_SENSE BB_WR BB_RD TIMESTAMP AWAKE', start=0) """Alternate features for FT230X devices.""" UART_BITS = IntFlag('UartBits', 'TXD RXD RTS CTS DTR DSR DCD RI') """Inversion flags for FT232R and FT-X devices.""" CHANNEL = IntFlag('Channel', 'FIFO OPTO CPU FT128 RS485') """Alternate port mode.""" DRIVE = IntFlag('Drive', 'LOW HIGH SLOW_SLEW SCHMITT _10 _20 _40 PWRSAVE_DIS') """Driver options for I/O pins.""" CFG1 = IntFlag('Cfg1', 'CLK_IDLE_STATE DATA_LSB FLOW_CONTROL _08 ' 'HIGH_CURRENTDRIVE _20 _40 SUSPEND_DBUS7') """Configuration bits stored @ 0x01.""" VAR_STRINGS = ('manufacturer', 'product', 'serial') """EEPROM strings with variable length.""" def __init__(self): self.log = getLogger('pyftdi.eeprom') self._ftdi = Ftdi() self._eeprom = bytearray() self._size = 0 self._dev_ver = 0 self._valid = False self._config = OrderedDict() self._dirty = set() self._modified = False self._chip: Optional[int] = None self._mirror = False def __getattr__(self, name): if name in self._config: return self._config[name] raise AttributeError('No such attribute: %s' % name) @classproperty def eeprom_sizes(cls) -> List[int]: """Return a list of supported EEPROM sizes. :return: the supported EEPROM sizes """ return sorted({p.size for p in cls._PROPERTIES.values() if p.size}) def open(self, device: Union[str, UsbDevice], ignore: bool = False, size: Optional[int] = None, model: Optional[str] = None) -> None: """Open a new connection to the FTDI USB device. :param device: the device URL or a USB device instance. :param ignore: whether to ignore existing content :param size: a custom EEPROM size :param model: the EEPROM model used to specify a custom size """ if self._ftdi.is_connected: raise FtdiError('Already open') if isinstance(device, str): self._ftdi.open_from_url(device) else: self._ftdi.open_from_device(device) if model and not size: # 93xxx46/56/66 mmo = match(r'(?i)^93[a-z]*([456])6.*$', model) if not mmo: raise ValueError(f'Unknown EEPROM device: {model}') mmul = int(mmo.group(1)) size = 128 << (mmul - 4) if size: if size not in self.eeprom_sizes: raise ValueError(f'Unsupported EEPROM size: {size}') self._size = min(size, 256) if not ignore: self._eeprom = self._read_eeprom() if self._valid: self._decode_eeprom() def close(self) -> None: """Close the current connection to the FTDI USB device, """ if self._ftdi.is_connected: self._ftdi.close() self._eeprom = bytearray() self._dev_ver = 0 self._config.clear() def connect(self, ftdi: Ftdi, ignore: bool = False) -> None: """Connect a FTDI EEPROM to an existing Ftdi instance. :param ftdi: the Ftdi instance to use :param ignore: whether to ignore existing content """ self._ftdi = ftdi self._eeprom = bytearray() self._dev_ver = 0 self._valid = False self._config = OrderedDict() self._dirty = set() if not ignore: self._eeprom = self._read_eeprom() if self._valid: self._decode_eeprom() self._decode_eeprom() @property def device_version(self) -> int: """Report the version of the FTDI device. :return: the release """ if not self._dev_ver: if not self._ftdi.is_connected: raise FtdiError('Not connected') self._dev_ver = self._ftdi.device_version return self._dev_ver @property def size(self) -> int: """Report the EEPROM size. Use the most common (default) EEPROM size of the size is not yet known. :return: the size in bytes """ if not self._size: self._size = self.default_size return self._size @property def default_size(self) -> int: """Report the default EEPROM size based on the FTDI type. The physical EEPROM size may be greater or lower, depending on the actual connected EEPROM device. :return: the size in bytes """ if self._chip == 0x46: return 0x80 # 93C46 if self._chip == 0x56: return 0x100 # 93C56 if self._chip == 0x66: return 0x100 # 93C66 (512 bytes, only 256 are used) try: eeprom_size = self._PROPERTIES[self.device_version].size except (AttributeError, KeyError) as exc: raise FtdiError('No EEPROM') from exc return eeprom_size @property def storage_size(self) -> int: """Report the number of EEPROM bytes that can be used for configuration storage. The physical EEPROM size may be greater :return: the number of bytes in the eeprom that will be used for configuration storage """ try: eeprom_storage_size = self.size if self.is_mirroring_enabled: eeprom_storage_size = self.mirror_sector except FtdiError as fe: raise fe return eeprom_storage_size @property def data(self) -> bytes: """Returns the content of the EEPROM. :return: the content as bytes. """ self._sync_eeprom() return bytes(self._eeprom) @property def properties(self) -> Set[str]: """Returns the supported properties for the current device. :return: the supported properies. """ props = set(self._config.keys()) props -= set(self.VAR_STRINGS) return props @property def is_empty(self) -> bool: """Reports whether the EEPROM has been erased, or no EEPROM is connected to the FTDI EEPROM port. :return: True if no content is detected """ if len(self._eeprom) != self.size: return False for byte in self._eeprom: if byte != 0xFF: return False return True @property def cbus_pins(self) -> List[int]: """Return the list of CBUS pins configured as GPIO, if any :return: list of CBUS pins """ pins = [pin for pin in range(0, 10) if self._config.get('cbus_func_%d' % pin, '') == 'GPIO'] return pins @property def cbus_mask(self) -> int: """Return the bitmask of CBUS pins configured as GPIO. The bitmap contains four bits, ordered in natural order. :return: CBUS mask """ if self.device_version == 0x900: # FT232H cbus = [5, 6, 8, 9] else: cbus = list(range(4)) mask = 0 for bix, pin in enumerate(cbus): if self._config.get('cbus_func_%d' % pin, '') == 'GPIO': mask |= 1 << bix return mask @property def has_mirroring(self) -> bool: """Report whether the device supports EEPROM content duplication across its two sectors. :return: True if the device support mirorring """ return (self._PROPERTIES[self.device_version].user and self._ftdi.device_version != 0x1000) @property def mirror_sector(self) -> int: """Report start address of the mirror sector in the EEPROM. This is only valid if the FTDI is capable of mirroring EEPROM data. :return: the start address """ if self.has_mirroring: return self.size // 2 raise FtdiError('EEPROM does not support mirroring') @property def is_mirroring_enabled(self) -> bool: """Check if EEPROM mirroring is currently enabled for this EEPROM. See enable_mirroring for more details on EEPROM mirroring functionality """ return self.has_mirroring and self._mirror def enable_mirroring(self, enable : bool) -> None: """Enable EEPROM write mirroring. When enabled, this divides the EEPROM into 2 sectors and mirrors configuration data between them. For example on a 256 byte EEPROM, two 128 byte 'sectors' will be used to store identical data. Configuration properties/strings will be writen to both of these sectors. For some devices (like the 4232H), this makes the PyFtdi EEPROM functionally similar to FT_PROG. Note: Data will only be mirrored if the has_mirroring property returns true (after establishing a connection to the ftdi) :param enable: enable or disable EEPROM mirroring """ self._mirror = enable def save_config(self, file: TextIO) -> None: """Save the EEPROM content as an INI stream. :param file: output stream """ self._sync_eeprom() cfg = ConfigParser() cfg.add_section('values') for name, value in self._config.items(): val = str(value) if isinstance(value, bool): val = val.lower() cfg.set('values', name, val) cfg.add_section('raw') length = 16 for i in range(0, len(self._eeprom), length): chunk = self._eeprom[i:i+length] hexa = hexlify(chunk).decode() cfg.set('raw', '@%02x' % i, hexa) cfg.write(file) def load_config(self, file: TextIO, section: Optional[str] = None) -> None: """Load the EEPROM content from an INI stream. The ``section`` argument selects which section(s) to load: * ``raw`` only loads the raw data (hexabytes) from a previous dump * ``values`` only loads the values section, that is the human readable configuration. * ``all``, which is the default section selection, load the raw section, then overwrite part of it with any configuration value from the ``values`` section. This provides a handy way to use an existing dump from a valid EEPROM content, while customizing some parameters, such as the serial number. :param file: input stream :paran section: which section to load from the ini file """ self._sync_eeprom() cfg = ConfigParser() cfg.read_file(file) loaded = False sections = cfg.sections() if section not in ('all', None) and section not in sections: raise FtdiEepromError(f'No such configuration section {section}') sect = 'raw' if sect in sections and section in (None, 'all', sect): if not cfg.has_section(sect): raise FtdiEepromError("No '%s' section in INI file" % sect) options = cfg.options(sect) try: for opt in options: if not opt.startswith('@'): raise ValueError() address = int(opt[1:], 16) hexval = cfg.get(sect, opt).strip() buf = unhexlify(hexval) self._eeprom[address:address+len(buf)] = buf except IndexError as exc: raise ValueError("Invalid address in '%s'' section" % sect) from exc except ValueError as exc: raise ValueError("Invalid line in '%s'' section" % sect) from exc self._compute_crc(self._eeprom, True) if not self._valid: raise ValueError('Loaded RAW section is invalid (CRC mismatch') loaded = True sect = 'values' vmap = { 'manufacturer': 'manufacturer_name', 'product': 'product_name', 'serial': 'serial_number' } if sect in sections and section in (None, 'all', sect): if not cfg.has_section(sect): raise FtdiEepromError("No '%s' section in INI file" % sect) options = cfg.options(sect) for opt in options: value = cfg.get(sect, opt).strip() if opt in vmap: func = getattr(self, 'set_%s' % vmap[opt]) func(value) else: self.log.debug('Assigning opt %s = %s', opt, value) try: self.set_property(opt, value) except (TypeError, ValueError, NotImplementedError) as exc: self.log.warning("Ignoring setting '%s': %s", opt, exc) loaded = True if not loaded: raise ValueError('Invalid section: %s' % section) self._sync_eeprom() def set_serial_number(self, serial: str) -> None: """Define a new serial number.""" self._validate_string(serial) self._update_var_string('serial', serial) self.set_property('has_serial', True) def set_manufacturer_name(self, manufacturer: str) -> None: """Define a new manufacturer string.""" self._validate_string(manufacturer) self._update_var_string('manufacturer', manufacturer) def set_product_name(self, product: str) -> None: """Define a new product name.""" self._validate_string(product) self._update_var_string('product', product) def set_property(self, name: str, value: Union[str, int, bool], out: Optional[TextIO] = None) -> None: """Change the value of a stored property. :see: :py:meth:`properties` for a list of valid property names. Note that for now, only a small subset of properties can be changed. :param name: the property to change :param value: the new value (supported values depend on property) :param out: optional output stream to report hints """ mobj = match(r'cbus_func_(\d)', name) if mobj: if not isinstance(value, str): raise ValueError("'%s' should be specified as a string" % name) self._set_cbus_func(int(mobj.group(1)), value, out) self._dirty.add(name) return mobj = match(r'([abcd])bus_(drive|slow_slew|schmitt)', name) if mobj: self._set_bus_control(mobj.group(1), mobj.group(2), value, out) self._dirty.add(name) return mobj = match(r'group_(\d)_(drive|schmitt|slow_slew)', name) if mobj: self._set_group(int(mobj.group(1)), mobj.group(2), value, out) self._dirty.add(name) return confs = { 'remote_wakeup': (0, 5), 'self_powered': (0, 6), 'in_isochronous': (2, 0), 'out_isochronous': (2, 1), 'suspend_pull_down': (2, 2), 'has_serial': (2, 3), } hwords = { 'vendor_id': 0x02, 'product_id': 0x04, 'type': 0x06, } if self.device_version in (0x0400, 0x0500): # Type BM and 2232C/D use 0xc to encode the USB version to expose # H device use this location to encode bus/group properties hwords['usb_version'] = 0x0c confs['use_usb_version'] = (2, 4) if name in hwords: val = to_int(value) if not 0 <= val <= 0xFFFF: raise ValueError('Invalid value for %s' % name) offset = hwords[name] self._eeprom[offset:offset+2] = spack('> 1 idx = 0x09 self._eeprom[idx] = val if self.is_mirroring_enabled: # duplicate in 'sector 2' idx2 = self.mirror_sector + idx self._eeprom[idx2] = val self._dirty.add(name) return if name.startswith('invert_'): if not self.device_version in (0x600, 0x1000): raise ValueError('UART control line inversion not available ' 'with this device') self._set_invert(name[len('invert_'):], value, out) self._dirty.add(name) return if name in self.properties: if name not in self._config: raise NotImplementedError("change is not supported") curval = self._config[name] try: curtype = type(curval) value = curtype(value) except (ValueError, TypeError) as exc: raise ValueError("cannot be converted to " "the proper type '%s'" % curtype) from exc if value != curval: raise NotImplementedError("not yet supported") # no-op change is silently ignored return raise ValueError(f"unknown property: {name}") def erase(self, erase_byte: Optional[int] = 0xFF) -> None: """Erase the whole EEPROM. :param erase_byte: Optional erase byte to use. Default to 0xFF """ self._eeprom = bytearray([erase_byte] * self.size) self._config.clear() self._dirty.add('eeprom') def initialize(self) -> None: """Initialize the EEPROM with some default sensible values. """ dev_ver = self.device_version dev_name = Ftdi.DEVICE_NAMES[dev_ver] vid = Ftdi.FTDI_VENDOR pid = Ftdi.PRODUCT_IDS[vid][dev_name] self.set_manufacturer_name('FTDI') self.set_product_name(dev_name.upper()) sernum = ''.join([chr(randint(ord('A'), ord('Z'))) for _ in range(5)]) self.set_serial_number('FT%d%s' % (randint(0, 9), sernum)) self.set_property('vendor_id', vid) self.set_property('product_id', pid) self.set_property('type', dev_ver) self.set_property('power_max', 150) self._sync_eeprom() def sync(self) -> None: """Force re-evaluation of configuration after some changes. This API is not useful for regular usage, but might help for testing when the EEPROM does not go through a full save/load cycle """ self._sync_eeprom() def dump_config(self, file: Optional[BinaryIO] = None) -> None: """Dump the configuration to a file. :param file: the output file, default to stdout """ if self._dirty: self._decode_eeprom() for name, value in self._config.items(): print('%s: %s' % (name, value), file=file or sys.stdout) def commit(self, dry_run: bool = True, no_crc: bool = False) -> bool: """Commit any changes to the EEPROM. :param dry_run: log what should be written, do not actually change the EEPROM content :param no_crc: do not compute EEPROM CRC. This should only be used to perform a full erasure of the EEPROM, as an attempt to recover from a corrupted config. :return: True if some changes have been committed to the EEPROM """ self._sync_eeprom(no_crc) if not self._modified: self.log.warning('No change to commit, EEPROM not modified') return False self._ftdi.overwrite_eeprom(self._eeprom, dry_run=dry_run) if not dry_run: eeprom = self._read_eeprom() if eeprom != self._eeprom: pos = 0 for pos, (old, new) in enumerate(zip(self._eeprom, eeprom)): if old != new: break pos &= ~0x1 raise FtdiEepromError('Write to EEPROM failed @ 0x%02x' % pos) self._modified = False return dry_run def reset_device(self): """Execute a USB device reset.""" self._ftdi.reset(usb_reset=True) @classmethod def _validate_string(cls, string): for invchr in ':/': # do not accept characters which are interpreted as URL seperators if invchr in string: raise ValueError("Invalid character '%s' in string" % invchr) def _update_var_string(self, name: str, value: str) -> None: if name not in self.VAR_STRINGS: raise ValueError('%s is not a variable string' % name) try: if value == self._config[name]: return except KeyError: # not yet defined pass self._config[name] = value self._dirty.add(name) def _generate_var_strings(self, fill=True) -> None: """ :param fill: fill the remainder of the space after the var strings with 0s """ stream = bytearray() dynpos = self._PROPERTIES[self.device_version].dynoff if dynpos > self._size: # if a custom,small EEPROM device is used dynpos = 0x40 data_pos = dynpos # start of var-strings in sector 1 (used for mirrored config) s1_vstr_start = data_pos - self.mirror_sector tbl_pos = 0x0e tbl_sector2_pos = self.mirror_sector + tbl_pos for name in self.VAR_STRINGS: try: ustr = self._config[name].encode('utf-16le') except KeyError: ustr = '' length = len(ustr)+2 stream.append(length) stream.append(0x03) # string descriptor stream.extend(ustr) self._eeprom[tbl_pos] = data_pos if self.is_mirroring_enabled: self._eeprom[tbl_sector2_pos] = data_pos tbl_pos += 1 tbl_sector2_pos += 1 self._eeprom[tbl_pos] = length if self.is_mirroring_enabled: self._eeprom[tbl_sector2_pos] = length tbl_pos += 1 tbl_sector2_pos += 1 data_pos += length if self.is_mirroring_enabled: self._eeprom[s1_vstr_start:s1_vstr_start+len(stream)] = stream self._eeprom[dynpos:dynpos+len(stream)] = stream mtp = self._ftdi.device_version == 0x1000 crc_pos = 0x100 if mtp else self._size rem = crc_pos - (dynpos + len(stream)) if rem < 0: oversize = (-rem + 2) // 2 raise FtdiEepromError(f'Cannot fit strings into EEPROM, ' f'{oversize} oversize characters') if fill: self._eeprom[dynpos+len(stream):crc_pos] = bytes(rem) if self.is_mirroring_enabled: crc_s1_pos = self.mirror_sector self._eeprom[s1_vstr_start+len(stream):crc_s1_pos] = bytes(rem) def _sync_eeprom(self, no_crc: bool = False): if not self._dirty: self.log.debug('No change detected for EEPROM content') return if not no_crc: if any([x in self._dirty for x in self.VAR_STRINGS]): self._generate_var_strings() for varstr in self.VAR_STRINGS: self._dirty.discard(varstr) self._update_crc() self._decode_eeprom() self._dirty.clear() self._modified = True self.log.debug('EEPROM content regenerated (not yet committed)') def _compute_crc(self, eeprom: Union[bytes, bytearray], check=False): mtp = self._ftdi.device_version == 0x1000 crc_pos = 0x100 if mtp else len(eeprom) crc_size = scalc(' Tuple[int, bool]: """ :return: Tuple of: - int of usable size of the eeprom - bool of whether eeprom mirroring was detected or not """ if self._ftdi.is_eeprom_internal: return self._ftdi.max_eeprom_size, False if all([x == 0xFF for x in eeprom]): # erased EEPROM, size is unknown return self._ftdi.max_eeprom_size, False if eeprom[0:0x80] == eeprom[0x80:0x100]: return 0x80, True if eeprom[0:0x40] == eeprom[0x40:0x80]: return 0x40, True return 0x100, False def _read_eeprom(self) -> bytes: buf = self._ftdi.read_eeprom(0, eeprom_size=self.size) eeprom = bytearray(buf) size, mirror_detected = self._compute_size(eeprom) if size < len(eeprom): eeprom = eeprom[:size] crc = self._compute_crc(eeprom, True)[0] if crc: if self.is_empty: self.log.info('No EEPROM or EEPROM erased') else: self.log.error('Invalid CRC or EEPROM content') if not self.is_empty and mirror_detected: self.log.info("Detected a mirrored eeprom. " + "Enabling mirrored writing") self._mirror = True return eeprom def _decode_eeprom(self): cfg = self._config cfg.clear() chipoff = self._PROPERTIES[self.device_version].chipoff if chipoff is not None: self._chip = Hex2Int(self._eeprom[chipoff]) cfg['chip'] = self._chip cfg['vendor_id'] = Hex4Int(sunpack(' None: cmap = {0x600: (self.CBUS, 5, 0x14, 4), # FT232R 0x900: (self.CBUSH, 10, 0x18, 4), # FT232H 0x1000: (self.CBUSX, 4, 0x1A, 8)} # FT230X/FT231X/FT234X try: cbus, count, offset, width = cmap[self.device_version] except KeyError as exc: raise ValueError('This property is not supported on this ' 'device') from exc pin_filter = getattr(self, '_filter_cbus_func_x%x' % self.device_version, None) if value == '?' and out: items = {item.name for item in cbus} if pin_filter: items = {val for val in items if pin_filter(cpin, val)} print(', '.join(sorted(items)) if items else '(none)', file=out) return if not 0 <= cpin < count: raise ValueError("Unsupported CBUS pin '%d'" % cpin) try: code = cbus[value.upper()].value except KeyError as exc: raise ValueError("CBUS pin %d does not have function '%s'" % (cpin, value)) from exc if pin_filter and not pin_filter(cpin, value.upper()): raise ValueError("Unsupported CBUS function '%s' for pin '%d'" % (value, cpin)) addr = offset + (cpin*width)//8 if width == 4: bitoff = 4 if cpin & 0x1 else 0 mask = 0x0F << bitoff else: bitoff = 0 mask = 0xFF old = self._eeprom[addr] self._eeprom[addr] &= ~mask self._eeprom[addr] |= code << bitoff self.log.debug('Cpin %d, addr 0x%02x, value 0x%02x->0x%02x', cpin, addr, old, self._eeprom[addr]) @classmethod def _filter_cbus_func_x900(cls, cpin: int, value: str): if cpin == 7: # nothing can be assigned to ACBUS7 return False if value in 'TRISTATE TXLED RXLED TXRXLED PWREN SLEEP DRIVE0'.split(): # any pin can be assigned these functions return True if cpin in (5, 6, 8, 9): # any function can be assigned to ACBUS5, ACBUS6, ACBUS8, ACBUS9 return True if cpin == 0: return value != 'GPIO' return False @classmethod def _filter_cbus_func_x600(cls, cpin: int, value: str): if value == 'BB_WR': # this signal is only available on CBUS0, CBUS1 return cpin < 2 return True def _set_bus_control(self, bus: str, control: str, value: Union[str, int, bool], out: Optional[TextIO]) -> None: if self.device_version == 0x1000: self._set_bus_control_230x(bus, control, value, out) return # for now, only support FT-X devices raise ValueError('Bus control not implemented for this device') def _set_group(self, group: int, control: str, value: Union[str, int, bool], out: Optional[TextIO]) \ -> None: if self.device_version in (0x0700, 0x0800, 0x0900): self._set_group_x232h(group, control, value, out) return raise ValueError('Group not implemented for this device') def _set_bus_control_230x(self, bus: str, control: str, value: Union[str, int, bool], out: Optional[TextIO]) -> None: if bus not in 'cd': raise ValueError('Invalid bus: %s' % bus) self._set_bus_xprop(0x0c, bus == 'c', control, value, out) def _set_group_x232h(self, group: int, control: str, value: str, out: Optional[TextIO]) -> None: if self.device_version in (0x0700, 0x800): # 2232H/4232H offset = 0x0c + group//2 nibble = group & 1 else: # 232H offset = 0x0c + group nibble = 0 self._set_bus_xprop(offset, nibble, control, value, out) def _set_bus_xprop(self, offset: int, high_nibble: bool, control: str, value: Union[str, int, bool], out: Optional[TextIO]) \ -> None: try: if control == 'drive': candidates = (4, 8, 12, 16) if value == '?' and out: print(', '.join([str(v) for v in candidates]), file=out) return value = int(value) if value not in candidates: raise ValueError('Invalid drive current: %d mA' % value) value //= 4 value -= 1 elif control in ('slow_slew', 'schmitt'): if value == '?' and out: print('off, on', file=out) return value = int(to_bool(value)) else: raise ValueError('Unsupported control: %s' % control) except (ValueError, TypeError) as exc: raise ValueError('Invalid %s value: %s' % (control, value)) from exc config = self._eeprom[offset] if not high_nibble: conf = config & 0x0F config &= 0xF0 cshift = 0 else: conf = config >> 4 config &= 0x0F cshift = 4 if control == 'drive': conf &= 0b1100 conf |= value elif control == 'slow_slew': conf &= 0b1011 conf |= value << 2 elif control == 'schmitt': conf &= 0b0111 conf |= value << 3 else: raise RuntimeError('Internal error') config |= conf << cshift self._eeprom[offset] = config def _set_invert(self, name, value, out): if value == '?' and out: print('off, on', file=out) return if name.upper() not in self.UART_BITS.__members__: raise ValueError('Unknown property: %s' % name) value = to_bool(value, permissive=False) code = getattr(self.UART_BITS, name.upper()) invert = self._eeprom[0x0B] if value: invert |= code else: invert &= ~code self._eeprom[0x0B] = invert def _decode_x(self): # FT-X series cfg = self._config misc, = sunpack('>= 4 for bix in range(4): value = self._eeprom[0x1A + bix] try: cfg['cbus_func_%d' % bix] = self.CBUSX(value).name except ValueError: pass def _decode_232h(self): cfg = self._config cfg0, cfg1 = self._eeprom[0x00], self._eeprom[0x01] cfg['channel_a_type'] = cfg0 & 0x0F cfg['channel_a_driver'] = 'VCP' if (cfg0 & (1 << 4)) else 'D2XX' cfg['clock_polarity'] = 'high' if (cfg1 & self.CFG1.CLK_IDLE_STATE) \ else 'low' cfg['lsb_data'] = bool(cfg1 & self.CFG1.DATA_LSB) cfg['flow_control'] = 'on' if (cfg1 & self.CFG1.FLOW_CONTROL) \ else 'off' cfg['powersave'] = bool(cfg1 & self.DRIVE.PWRSAVE_DIS) max_drive = self.DRIVE.LOW.value | self.DRIVE.HIGH.value for grp in range(2): conf = self._eeprom[0x0c+grp] cfg['group_%d_drive' % grp] = 4 * (1+(conf & max_drive)) cfg['group_%d_schmitt' % grp] = \ bool(conf & self.DRIVE.SCHMITT.value) cfg['group_%d_slow_slew' % grp] = \ bool(conf & self.DRIVE.SLOW_SLEW.value) for bix in range(5): value = self._eeprom[0x18 + bix] low, high = value & 0x0F, value >> 4 try: cfg['cbus_func_%d' % ((2*bix)+0)] = self.CBUSH(low).name except ValueError: pass try: cfg['cbus_func_%d' % ((2*bix)+1)] = self.CBUSH(high).name except ValueError: pass def _decode_232r(self): cfg = self._config cfg0 = self._eeprom[0x00] cfg['channel_a_driver'] = 'VCP' if (~cfg0 & (1 << 3)) else '' cfg['high_current'] = bool(~cfg0 & (1 << 2)) cfg['external_oscillator'] = cfg0 & 0x02 for bit in self.UART_BITS: value = self._eeprom[0x0B] cfg['invert_%s' % self.UART_BITS(bit).name] = bool(value & bit) bix = 0 while True: value = self._eeprom[0x14 + bix] low, high = value & 0x0F, value >> 4 try: cfg['cbus_func_%d' % ((2*bix)+0)] = self.CBUS(low).name except ValueError: pass if bix == 2: break try: cfg['cbus_func_%d' % ((2*bix)+1)] = self.CBUS(high).name except ValueError: pass bix += 1 def _decode_2232h(self): cfg = self._config self._decode_x232h(cfg) cfg0, cfg1 = self._eeprom[0x00], self._eeprom[0x01] cfg['channel_a_type'] = self.CHANNEL(cfg0 & 0x7).name or 'UART' cfg['channel_b_type'] = self.CHANNEL(cfg1 & 0x7).name or 'UART' cfg['suspend_dbus7'] = bool(cfg1 & self.CFG1.SUSPEND_DBUS7.value) def _decode_4232h(self): cfg = self._config self._decode_x232h(cfg) cfg0, cfg1 = self._eeprom[0x00], self._eeprom[0x01] cfg['channel_c_driver'] = 'VCP' if ((cfg0 >> 4) & (1 << 3)) else 'D2XX' cfg['channel_d_driver'] = 'VCP' if ((cfg1 >> 4) & (1 << 3)) else 'D2XX' conf = self._eeprom[0x0B] rs485 = self.CHANNEL.RS485 for chix in range(4): cfg['channel_%x_rs485' % (0xa+chix)] = bool(conf & (rs485 << chix)) def _decode_x232h(self, cfg): # common code for 2232h and 4232h cfg0, cfg1 = self._eeprom[0x00], self._eeprom[0x01] cfg['channel_a_driver'] = 'VCP' if (cfg0 & (1 << 3)) else 'D2XX' cfg['channel_b_driver'] = 'VCP' if (cfg1 & (1 << 3)) else 'D2XX' max_drive = self.DRIVE.LOW.value | self.DRIVE.HIGH.value for bix in range(4): if not bix & 1: val = self._eeprom[0x0c + bix//2] else: val >>= 4 cfg['group_%d_drive' % bix] = 4 * (1+(val & max_drive)) cfg['group_%d_schmitt' % bix] = \ bool(val & self.DRIVE.SCHMITT.value) cfg['group_%d_slow_slew' % bix] = \ bool(val & self.DRIVE.SLOW_SLEW.value) pyftdi-0.54.0/pyftdi/ftdi.py000066400000000000000000002750201421346242200157040ustar00rootroot00000000000000# Copyright (C) 2010-2020 Emmanuel Blot # Copyright (c) 2016 Emmanuel Bouaziz # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """FTDI core driver.""" from binascii import hexlify from collections import OrderedDict from enum import IntEnum, unique from errno import ENODEV from logging import getLogger, DEBUG from struct import unpack as sunpack from sys import platform from typing import Callable, Optional, List, Sequence, TextIO, Tuple, Union from usb.core import (Configuration as UsbConfiguration, Device as UsbDevice, USBError) from usb.util import (build_request_type, release_interface, CTRL_IN, CTRL_OUT, CTRL_TYPE_VENDOR, CTRL_RECIPIENT_DEVICE) from .misc import to_bool from .usbtools import UsbDeviceDescriptor, UsbTools #pylint: disable-msg=invalid-name #pylint: disable-msg=too-many-arguments #pylint: disable=too-many-arguments #pylint: disable=too-many-branches #pylint: disable=too-many-statements #pylint: disable=too-many-nested-blocks #pylint: disable=too-many-instance-attributes #pylint: disable=too-many-nested-blocks #pylint: disable=too-many-public-methods #pylint: disable=too-many-locals #pylint: disable=too-many-lines class FtdiError(IOError): """Base class error for all FTDI device""" class FtdiFeatureError(FtdiError): """Requested feature is not available on FTDI device""" class FtdiMpsseError(FtdiFeatureError): """MPSSE mode not supported on FTDI device""" class FtdiEepromError(FtdiError): """FTDI EEPROM access errors""" class Ftdi: """FTDI device driver""" SCHEME = 'ftdi' """URL scheme for :py:class:`UsbTools`.""" FTDI_VENDOR = 0x403 """USB VID for FTDI chips.""" VENDOR_IDS = {'ftdi': FTDI_VENDOR} """Supported vendors, only FTDI. To add third parties vendors see :py:meth:`add_custom_vendor`. """ PRODUCT_IDS = { FTDI_VENDOR: OrderedDict(( # use an ordered dict so that the first occurence of a PID takes # precedence when generating URLs - order does matter. ('232', 0x6001), ('232r', 0x6001), ('232h', 0x6014), ('2232', 0x6010), ('2232c', 0x6010), ('2232d', 0x6010), ('2232h', 0x6010), ('4232', 0x6011), ('4232h', 0x6011), ('ft-x', 0x6015), ('230x', 0x6015), ('231x', 0x6015), ('234x', 0x6015), ('ft232', 0x6001), ('ft232r', 0x6001), ('ft232h', 0x6014), ('ft2232', 0x6010), ('ft2232c', 0x6010), ('ft2232d', 0x6010), ('ft2232h', 0x6010), ('ft4232', 0x6011), ('ft4232h', 0x6011), ('ft230x', 0x6015), ('ft231x', 0x6015), ('ft234x', 0x6015))) } """Supported products, only FTDI officials ones. To add third parties and customized products, see :py:meth:`add_custom_product`. """ DEFAULT_VENDOR = FTDI_VENDOR """Default vendor: FTDI.""" DEVICE_NAMES = { 0x0200: 'ft232am', 0x0400: 'ft232bm', 0x0500: 'ft2232c', 0x0600: 'ft232r', 0x0700: 'ft2232h', 0x0800: 'ft4232h', 0x0900: 'ft232h', 0x1000: 'ft-x'} """Common names of FTDI supported devices.""" # Note that the FTDI datasheets contradict themselves, so # the following values may not be the right ones... FIFO_SIZES = { 0x0200: (128, 128), # FT232AM: TX: 128, RX: 128 0x0400: (128, 384), # FT232BM: TX: 128, RX: 384 0x0500: (128, 384), # FT2232C: TX: 128, RX: 384 0x0600: (256, 128), # FT232R: TX: 256, RX: 128 0x0700: (4096, 4096), # FT2232H: TX: 4KiB, RX: 4KiB 0x0800: (2048, 2048), # FT4232H: TX: 2KiB, RX: 2KiB 0x0900: (1024, 1024), # FT232H: TX: 1KiB, RX: 1KiB 0x1000: (512, 512), # FT-X: TX: 512, RX: 512 } """FTDI chip internal FIFO sizes Note that 'TX' and 'RX' are inverted with the datasheet terminology: Values here are seen from the host perspective, whereas datasheet values are defined from the device perspective """ @unique class BitMode(IntEnum): """Function selection.""" RESET = 0x00 # switch off altnerative mode (default to UART) BITBANG = 0x01 # classical asynchronous bitbang mode MPSSE = 0x02 # MPSSE mode, available on 2232x chips SYNCBB = 0x04 # synchronous bitbang mode MCU = 0x08 # MCU Host Bus Emulation mode, OPTO = 0x10 # Fast Opto-Isolated Serial Interface Mode CBUS = 0x20 # Bitbang on CBUS pins of R-type chips SYNCFF = 0x40 # Single Channel Synchronous FIFO mode # MPSSE Commands WRITE_BYTES_PVE_MSB = 0x10 WRITE_BYTES_NVE_MSB = 0x11 WRITE_BITS_PVE_MSB = 0x12 WRITE_BITS_NVE_MSB = 0x13 WRITE_BYTES_PVE_LSB = 0x18 WRITE_BYTES_NVE_LSB = 0x19 WRITE_BITS_PVE_LSB = 0x1a WRITE_BITS_NVE_LSB = 0x1b READ_BYTES_PVE_MSB = 0x20 READ_BYTES_NVE_MSB = 0x24 READ_BITS_PVE_MSB = 0x22 READ_BITS_NVE_MSB = 0x26 READ_BYTES_PVE_LSB = 0x28 READ_BYTES_NVE_LSB = 0x2c READ_BITS_PVE_LSB = 0x2a READ_BITS_NVE_LSB = 0x2e RW_BYTES_PVE_NVE_MSB = 0x31 RW_BYTES_NVE_PVE_MSB = 0x34 RW_BITS_PVE_NVE_MSB = 0x33 RW_BITS_NVE_PVE_MSB = 0x36 RW_BYTES_PVE_NVE_LSB = 0x39 RW_BYTES_NVE_PVE_LSB = 0x3c RW_BITS_PVE_NVE_LSB = 0x3b RW_BITS_NVE_PVE_LSB = 0x3e WRITE_BITS_TMS_PVE = 0x4a WRITE_BITS_TMS_NVE = 0x4b RW_BITS_TMS_PVE_PVE = 0x6a RW_BITS_TMS_PVE_NVE = 0x6b RW_BITS_TMS_NVE_PVE = 0x6e RW_BITS_TMS_NVE_NVE = 0x6f SEND_IMMEDIATE = 0x87 WAIT_ON_HIGH = 0x88 WAIT_ON_LOW = 0x89 READ_SHORT = 0x90 READ_EXTENDED = 0x91 WRITE_SHORT = 0x92 WRITE_EXTENDED = 0x93 # -H series only DISABLE_CLK_DIV5 = 0x8a ENABLE_CLK_DIV5 = 0x8b # Modem status MODEM_CTS = (1 << 4) # Clear to send MODEM_DSR = (1 << 5) # Data set ready MODEM_RI = (1 << 6) # Ring indicator MODEM_RLSD = (1 << 7) # Carrier detect MODEM_DR = (1 << 8) # Data ready MODEM_OE = (1 << 9) # Overrun error MODEM_PE = (1 << 10) # Parity error MODEM_FE = (1 << 11) # Framing error MODEM_BI = (1 << 12) # Break interrupt MODEM_THRE = (1 << 13) # Transmitter holding register MODEM_TEMT = (1 << 14) # Transmitter empty MODEM_RCVE = (1 << 15) # Error in RCVR FIFO # FTDI MPSSE commands SET_BITS_LOW = 0x80 # Change LSB GPIO output SET_BITS_HIGH = 0x82 # Change MSB GPIO output GET_BITS_LOW = 0x81 # Get LSB GPIO output GET_BITS_HIGH = 0x83 # Get MSB GPIO output LOOPBACK_START = 0x84 # Enable loopback LOOPBACK_END = 0x85 # Disable loopback SET_TCK_DIVISOR = 0x86 # Set clock # -H series only ENABLE_CLK_3PHASE = 0x8c # Enable 3-phase data clocking (I2C) DISABLE_CLK_3PHASE = 0x8d # Disable 3-phase data clocking CLK_BITS_NO_DATA = 0x8e # Allows JTAG clock to be output w/o data CLK_BYTES_NO_DATA = 0x8f # Allows JTAG clock to be output w/o data CLK_WAIT_ON_HIGH = 0x94 # Clock until GPIOL1 is high CLK_WAIT_ON_LOW = 0x95 # Clock until GPIOL1 is low ENABLE_CLK_ADAPTIVE = 0x96 # Enable JTAG adaptive clock for ARM DISABLE_CLK_ADAPTIVE = 0x97 # Disable JTAG adaptive clock CLK_COUNT_WAIT_ON_HIGH = 0x9c # Clock byte cycles until GPIOL1 is high CLK_COUNT_WAIT_ON_LOW = 0x9d # Clock byte cycles until GPIOL1 is low # FT232H only DRIVE_ZERO = 0x9e # Drive-zero mode # USB control requests REQ_OUT = build_request_type(CTRL_OUT, CTRL_TYPE_VENDOR, CTRL_RECIPIENT_DEVICE) REQ_IN = build_request_type(CTRL_IN, CTRL_TYPE_VENDOR, CTRL_RECIPIENT_DEVICE) # Requests SIO_REQ_RESET = 0x0 # Reset the port SIO_REQ_SET_MODEM_CTRL = 0x1 # Set the modem control register SIO_REQ_SET_FLOW_CTRL = 0x2 # Set flow control register SIO_REQ_SET_BAUDRATE = 0x3 # Set baud rate SIO_REQ_SET_DATA = 0x4 # Set the data characteristics of the port SIO_REQ_POLL_MODEM_STATUS = 0x5 # Get line status SIO_REQ_SET_EVENT_CHAR = 0x6 # Change event character SIO_REQ_SET_ERROR_CHAR = 0x7 # Change error character SIO_REQ_SET_LATENCY_TIMER = 0x9 # Change latency timer SIO_REQ_GET_LATENCY_TIMER = 0xa # Get latency timer SIO_REQ_SET_BITMODE = 0xb # Change bit mode SIO_REQ_READ_PINS = 0xc # Read GPIO pin value (or "get bitmode") # Eeprom requests SIO_REQ_EEPROM = 0x90 SIO_REQ_READ_EEPROM = SIO_REQ_EEPROM + 0 # Read EEPROM content SIO_REQ_WRITE_EEPROM = SIO_REQ_EEPROM + 1 # Write EEPROM content SIO_REQ_ERASE_EEPROM = SIO_REQ_EEPROM + 2 # Erase EEPROM content # Reset arguments SIO_RESET_SIO = 0 # Reset device SIO_RESET_PURGE_RX = 1 # Drain USB RX buffer (host-to-ftdi) SIO_RESET_PURGE_TX = 2 # Drain USB TX buffer (ftdi-to-host) # Flow control arguments SIO_DISABLE_FLOW_CTRL = 0x0 SIO_RTS_CTS_HS = (0x1 << 8) SIO_DTR_DSR_HS = (0x2 << 8) SIO_XON_XOFF_HS = (0x4 << 8) SIO_SET_DTR_MASK = 0x1 SIO_SET_DTR_HIGH = (SIO_SET_DTR_MASK | (SIO_SET_DTR_MASK << 8)) SIO_SET_DTR_LOW = (0x0 | (SIO_SET_DTR_MASK << 8)) SIO_SET_RTS_MASK = 0x2 SIO_SET_RTS_HIGH = (SIO_SET_RTS_MASK | (SIO_SET_RTS_MASK << 8)) SIO_SET_RTS_LOW = (0x0 | (SIO_SET_RTS_MASK << 8)) # Parity bits PARITY_NONE, PARITY_ODD, PARITY_EVEN, PARITY_MARK, PARITY_SPACE = range(5) # Number of stop bits STOP_BIT_1, STOP_BIT_15, STOP_BIT_2 = range(3) # Number of bits BITS_7, BITS_8 = [7+i for i in range(2)] # Break type BREAK_OFF, BREAK_ON = range(2) # cts: Clear to send # dsr: Data set ready # ri: Ring indicator # dcd: Data carrier detect # dr: Data ready # oe: Overrun error # pe: Parity error # fe: Framing error # bi: Break interrupt # thre: Transmitter holding register empty # temt: Transmitter empty # err: Error in RCVR FIFO MODEM_STATUS = [('', '', '', '', 'cts', 'dsr', 'ri', 'dcd'), ('dr', 'overrun', 'parity', 'framing', 'break', 'thre', 'txe', 'rcve')] ERROR_BITS = (0x00, 0x8E) TX_EMPTY_BITS = 0x60 # Clocks and baudrates BUS_CLOCK_BASE = 6.0E6 # 6 MHz BUS_CLOCK_HIGH = 30.0E6 # 30 MHz BAUDRATE_REF_BASE = int(3.0E6) # 3 MHz BAUDRATE_REF_HIGH = int(12.0E6) # 12 MHz BITBANG_BAUDRATE_RATIO_BASE = 16 BITBANG_BAUDRATE_RATIO_HIGH = 5 BAUDRATE_TOLERANCE = 3.0 # acceptable clock drift for UART, in % FRAC_DIV_CODE = (0, 3, 2, 4, 1, 5, 6, 7) # Latency LATENCY_MIN = 1 LATENCY_MAX = 255 LATENCY_EEPROM_FT232R = 77 # EEPROM Properties EXT_EEPROM_SIZES = (128, 256) # in bytes (93C66 seen as 93C56) INT_EEPROMS = { 0x0600: 0x80, # FT232R: 128 bytes, 1024 bits 0x1000: 0x400 # FT230*X: 1KiB } def __init__(self): self.log = getLogger('pyftdi.ftdi') self._debug_log = False self._usb_dev = None self._usb_read_timeout = 5000 self._usb_write_timeout = 5000 self._baudrate = -1 self._readbuffer = bytearray() self._readoffset = 0 self._readbuffer_chunksize = 4 << 10 # 4KiB self._writebuffer_chunksize = 4 << 10 # 4KiB self._max_packet_size = 0 self._interface = None self._index = None self._in_ep = None self._out_ep = None self._bitmode = Ftdi.BitMode.RESET self._latency = 0 self._latency_count = 0 self._latency_min = self.LATENCY_MIN self._latency_max = self.LATENCY_MAX self._latency_threshold = None # disable dynamic latency self._lineprop = 0 self._cbus_pins = (0, 0) self._cbus_out = 0 self._tracer = None # --- Public API ------------------------------------------------------- @classmethod def create_from_url(cls, url: str) -> 'Ftdi': """Create an Ftdi instance from an URL URL scheme: ftdi://[vendor[:product[:index|:serial]]]/interface :param url: FTDI device selector :return: a fresh, open Ftdi instance """ device = Ftdi() device.open_from_url(url) return device @classmethod def list_devices(cls, url: Optional[str] = None) -> \ List[Tuple[UsbDeviceDescriptor, int]]: """List of URLs of connected FTDI devices. :param url: a pattern URL to restrict the search :return: list of (UsbDeviceDescriptor, interface) """ return UsbTools.list_devices(url or 'ftdi:///?', cls.VENDOR_IDS, cls.PRODUCT_IDS, cls.DEFAULT_VENDOR) @classmethod def show_devices(cls, url: Optional[str] = None, out: Optional[TextIO] = None) -> None: """Print the URLs and descriptors of connected FTDI devices. :param url: a pattern URL to restrict the search :param out: output stream, default to stdout """ devdescs = UsbTools.list_devices(url or 'ftdi:///?', cls.VENDOR_IDS, cls.PRODUCT_IDS, cls.DEFAULT_VENDOR) UsbTools.show_devices('ftdi', cls.VENDOR_IDS, cls.PRODUCT_IDS, devdescs, out) @classmethod def get_identifiers(cls, url: str) -> Tuple[UsbDeviceDescriptor, int]: """Extract the identifiers of an FTDI device from URL, if any :param url: input URL to parse """ return UsbTools.parse_url(url, cls.SCHEME, cls.VENDOR_IDS, cls.PRODUCT_IDS, cls.DEFAULT_VENDOR) @classmethod def get_device(cls, url: str) -> UsbDevice: """Get a USB device from its URL, without opening an instance. :param url: input URL to parse :return: the USB device that match the specified URL """ devdesc, _ = cls.get_identifiers(url) return UsbTools.get_device(devdesc) @classmethod def add_custom_vendor(cls, vid: int, vidname: str = '') -> None: """Add a custom USB vendor identifier. It can be useful to use a pretty URL for opening FTDI device :param vid: Vendor ID (USB 16-bit identifier) :param vidname: Vendor name (arbitrary string) :raise ValueError: if the vendor id is already referenced """ if vid in cls.VENDOR_IDS.values(): raise ValueError('Vendor ID 0x%04x already registered' % vid) if not vidname: vidname = '0x%04x' % vid cls.VENDOR_IDS[vidname] = vid @classmethod def add_custom_product(cls, vid: int, pid: int, pidname: str = '') -> None: """Add a custom USB product identifier. It is required for opening FTDI device with non-standard VID/PID USB identifiers. :param vid: Vendor ID (USB 16-bit identifier) :param pid: Product ID (USB 16-bit identifier) :param pidname: Product name (arbitrary string) :raise ValueError: if the product id is already referenced """ if vid not in cls.PRODUCT_IDS: cls.PRODUCT_IDS[vid] = OrderedDict() elif pid in cls.PRODUCT_IDS[vid].values(): raise ValueError('Product ID 0x%04x:0x%04x already registered' % (vid, pid)) if not pidname: pidname = '0x%04x' % pid cls.PRODUCT_IDS[vid][pidname] = pid @classmethod def decode_modem_status(cls, value: bytes, error_only: bool = False) -> \ Tuple[str, ...]: """Decode the FTDI modem status bitfield into short strings. :param value: 2-byte mode status :param error_only: only decode error flags :return: a tuple of status identifiers """ status = [] for pos, (byte_, ebits) in enumerate(zip(value, cls.ERROR_BITS)): for bit, _ in enumerate(cls.MODEM_STATUS[pos]): if error_only: byte_ &= ebits if byte_ & (1 << bit): status.append(cls.MODEM_STATUS[pos][bit]) return tuple(status) @staticmethod def find_all(vps: Sequence[Tuple[int, int]], nocache: bool = False) -> \ List[Tuple[UsbDeviceDescriptor, int]]: """Find all devices that match the vendor/product pairs of the vps list. :param vps: a sequence of 2-tuple (vid, pid) pairs :type vps: tuple(int, int) :param bool nocache: bypass cache to re-enumerate USB devices on the host :return: a list of 5-tuple (vid, pid, sernum, iface, description) device descriptors :rtype: list(tuple(int,int,str,int,str)) """ return UsbTools.find_all(vps, nocache) @property def is_connected(self) -> bool: """Tells whether this instance is connected to an actual USB slave. :return: the slave connection status """ return bool(self._usb_dev) def open_from_url(self, url: str) -> None: """Open a new interface to the specified FTDI device. :param str url: a FTDI URL selector """ devdesc, interface = self.get_identifiers(url) device = UsbTools.get_device(devdesc) self.open_from_device(device, interface) def open(self, vendor: int, product: int, bus: Optional[int] = None, address: Optional[int] = None, index: int = 0, serial: Optional[str] = None, interface: int = 1) -> None: """Open a new interface to the specified FTDI device. 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 and FT4232H). The interface argument selects the FTDI port to use, starting from 1 (not 0). :param int vendor: USB vendor id :param int product: USB product id :param int bus: optional selector, USB bus :param int address: optional selector, USB address on bus :param int index: optional selector, specified the n-th matching FTDI enumerated USB device on the host :param str serial: optional selector, specified the FTDI device by its serial number :param str interface: FTDI interface/port """ devdesc = UsbDeviceDescriptor(vendor, product, bus, address, serial, index, None) device = UsbTools.get_device(devdesc) self.open_from_device(device, interface) def open_from_device(self, device: UsbDevice, interface: int = 1) -> None: """Open a new interface from an existing USB device. :param device: FTDI USB device (PyUSB instance) :param interface: FTDI interface to use (integer starting from 1) """ if not isinstance(device, UsbDevice): raise FtdiError("Device '%s' is not a PyUSB device" % device) self._usb_dev = device try: self._usb_dev.set_configuration() except USBError: pass # detect invalid interface as early as possible config = self._usb_dev.get_active_configuration() if interface > config.bNumInterfaces: raise FtdiError('No such FTDI port: %d' % interface) self._set_interface(config, interface) self._max_packet_size = self._get_max_packet_size() # Invalidate data in the readbuffer self._readoffset = 0 self._readbuffer = bytearray() # Drain input buffer self.purge_buffers() # Shallow reset self._reset_device() # Reset feature mode self.set_bitmode(0, Ftdi.BitMode.RESET) # Init latency self._latency_threshold = None self.set_latency_timer(self.LATENCY_MIN) self._debug_log = self.log.getEffectiveLevel() == DEBUG def close(self, freeze: bool = False) -> None: """Close the FTDI 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._usb_dev: dev = self._usb_dev if self._is_pyusb_handle_active(): # Do not attempt to execute the following calls if the # device has been closed: the ResourceManager may attempt # to re-open the device that has been already closed, and # this may lead to a (native) crash in libusb. try: if not freeze: self.set_bitmode(0, Ftdi.BitMode.RESET) self.set_latency_timer(self.LATENCY_MAX) release_interface(dev, self._index - 1) except FtdiError as exc: self.log.warning('FTDI device may be gone: %s', exc) try: self._usb_dev.attach_kernel_driver(self._index - 1) except (NotImplementedError, USBError): pass self._usb_dev = None UsbTools.release_device(dev) def reset(self, usb_reset: bool = False) -> None: """Reset FTDI device. :param usb_reset: wether to perform a full USB reset of the device. Beware that selecting usb_reset performs a full USB device reset, which means all other interfaces of the same device are also affected. """ if not self.is_connected: raise FtdiError('Not connected') self._reset_device() if usb_reset: self._reset_usb_device() def open_mpsse_from_url(self, url: str, direction: int = 0x0, initial: int = 0x0, frequency: float = 6.0E6, latency: int = 16, debug: bool = False) -> float: """Open a new interface to the specified FTDI device in MPSSE mode. MPSSE enables I2C, SPI, JTAG or other synchronous serial interface modes (vs. UART mode). :param url: a FTDI URL selector :param direction: a bitfield specifying the FTDI GPIO direction, where high level defines an output, and low level defines an input :param initial: a bitfield specifying the initial output value :param float frequency: serial interface clock in Hz :param latency: low-level latency in milliseconds. The shorter the delay, the higher the host CPU load. Do not use shorter values than the default, as it triggers data loss in FTDI. :param debug: use a tracer to decode MPSSE protocol :return: actual bus frequency in Hz """ devdesc, interface = self.get_identifiers(url) device = UsbTools.get_device(devdesc) return self.open_mpsse_from_device(device, interface, direction=direction, initial=initial, frequency=frequency, latency=latency, debug=debug) def open_mpsse(self, vendor: int, product: int, bus: Optional[int] = None, address: Optional[int] = None, index: int = 0, serial: Optional[str] = None, interface: int = 1, direction: int = 0x0, initial: int = 0x0, frequency: float = 6.0E6, latency: int = 16, debug: bool = False) -> float: """Open a new interface to the specified FTDI device in MPSSE mode. MPSSE enables I2C, SPI, JTAG or other synchronous serial interface modes (vs. UART mode). 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 and FT4232H). The interface argument selects the FTDI port to use, starting from 1 (not 0). Note that not all FTDI ports are MPSSE capable. :param vendor: USB vendor id :param product: USB product id :param bus: optional selector, USB bus :param address: optional selector, USB address on bus :param index: optional selector, specified the n-th matching FTDI enumerated USB device on the host :param serial: optional selector, specified the FTDI device by its serial number :param interface: FTDI interface/port :param direction: a bitfield specifying the FTDI GPIO direction, where high level defines an output, and low level defines an input :param initial: a bitfield specifying the initial output value :param frequency: serial interface clock in Hz :param latency: low-level latency in milliseconds. The shorter the delay, the higher the host CPU load. Do not use shorter values than the default, as it triggers data loss in FTDI. :param bool debug: use a tracer to decode MPSSE protocol :return: actual bus frequency in Hz """ devdesc = UsbDeviceDescriptor(vendor, product, bus, address, serial, index, None) device = UsbTools.get_device(devdesc) return self.open_mpsse_from_device(device, interface, direction=direction, initial=initial, frequency=frequency, latency=latency, debug=debug) def open_mpsse_from_device(self, device: UsbDevice, interface: int = 1, direction: int = 0x0, initial: int = 0x0, frequency: float = 6.0E6, latency: int = 16, tracer: bool = False, debug: bool = False) -> float: """Open a new interface to the specified FTDI device in MPSSE mode. MPSSE enables I2C, SPI, JTAG or other synchronous serial interface modes (vs. UART mode). 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 and FT4232H). The interface argument selects the FTDI port to use, starting from 1 (not 0). Note that not all FTDI ports are MPSSE capable. :param device: FTDI USB device :param interface: FTDI interface/port :param direction: a bitfield specifying the FTDI GPIO direction, where high level defines an output, and low level defines an input :param initial: a bitfield specifying the initial output value :param frequency: serial interface clock in Hz :param latency: low-level latency in milliseconds. The shorter the delay, the higher the host CPU load. Do not use shorter values than the default, as it triggers data loss in FTDI. :param bool tracer: use a tracer to decode MPSSE protocol :param bool debug: add more debug traces :return: actual bus frequency in Hz """ # pylint: disable-msg=unused-argument self.open_from_device(device, interface) if not self.is_mpsse_interface(interface): self.close() raise FtdiMpsseError('This interface does not support MPSSE') if to_bool(tracer): # accept strings as boolean #pylint: disable-msg=import-outside-toplevel from .tracer import FtdiMpsseTracer self._tracer = FtdiMpsseTracer(self.device_version) self.log.debug('Using MPSSE tracer') # Set latency timer self.set_latency_timer(latency) # Set chunk size self.write_data_set_chunksize() self.read_data_set_chunksize() # Reset feature mode self.set_bitmode(0, Ftdi.BitMode.RESET) # Drain buffers self.purge_buffers() # Disable event and error characters self.set_event_char(0, False) self.set_error_char(0, False) # Enable MPSSE mode self.set_bitmode(direction, Ftdi.BitMode.MPSSE) # Configure clock frequency = self._set_frequency(frequency) # Configure I/O cmd = bytearray((Ftdi.SET_BITS_LOW, initial & 0xFF, direction & 0xFF)) if self.has_wide_port: initial >>= 8 direction >>= 8 cmd.extend((Ftdi.SET_BITS_HIGH, initial & 0xFF, direction & 0xFF)) self.write_data(cmd) # Disable loopback self.write_data(bytearray((Ftdi.LOOPBACK_END,))) self.validate_mpsse() # Return the actual frequency return frequency def open_bitbang_from_url(self, url: str, direction: int = 0x0, latency: int = 16, baudrate: int = 1000000, sync: bool = False) -> float: """Open a new interface to the specified FTDI device in bitbang mode. Bitbang enables direct read or write to FTDI GPIOs. :param url: a FTDI URL selector :param direction: a bitfield specifying the FTDI GPIO direction, where high level defines an output, and low level defines an input :param latency: low-level latency to select the USB FTDI poll delay. The shorter the delay, the higher the host CPU load. :param baudrate: pace to sequence GPIO exchanges :param sync: whether to use synchronous or asynchronous bitbang :return: actual bitbang baudrate in bps """ devdesc, interface = self.get_identifiers(url) device = UsbTools.get_device(devdesc) return self.open_bitbang_from_device(device, interface, direction=direction, latency=latency, baudrate=baudrate, sync=sync) def open_bitbang(self, vendor: int, product: int, bus: Optional[int] = None, address: Optional[int] = None, index: int = 0, serial: Optional[str] = None, interface: int = 1, direction: int = 0x0, latency: int = 16, baudrate: int = 1000000, sync: bool = False) -> float: """Open a new interface to the specified FTDI device in bitbang mode. Bitbang enables direct read or write to FTDI GPIOs. :param vendor: USB vendor id :param product: USB product id :param index: optional selector, specified the n-th matching FTDI enumerated USB device on the host :param serial: optional selector, specified the FTDI device by its serial number :param interface: FTDI interface/port :param direction: a bitfield specifying the FTDI GPIO direction, where high level defines an output, and low level defines an input :param latency: low-level latency to select the USB FTDI poll delay. The shorter the delay, the higher the host CPU load. :param baudrate: pace to sequence GPIO exchanges :param sync: whether to use synchronous or asynchronous bitbang :return: actual bitbang baudrate in bps """ devdesc = UsbDeviceDescriptor(vendor, product, bus, address, serial, index, None) device = UsbTools.get_device(devdesc) return self.open_bitbang_from_device(device, interface, direction=direction, latency=latency, baudrate=baudrate, sync=sync) def open_bitbang_from_device(self, device: UsbDevice, interface: int = 1, direction: int = 0x0, latency: int = 16, baudrate: int = 1000000, sync: bool = False) -> int: """Open a new interface to the specified FTDI device in bitbang mode. Bitbang enables direct read or write to FTDI GPIOs. :param device: FTDI USB device :param interface: FTDI interface/port :param direction: a bitfield specifying the FTDI GPIO direction, where high level defines an output, and low level defines an input :param latency: low-level latency to select the USB FTDI poll delay. The shorter the delay, the higher the host CPU load. :param baudrate: pace to sequence GPIO exchanges :param sync: whether to use synchronous or asynchronous bitbang :return: actual bitbang baudrate in bps """ self.open_from_device(device, interface) # Set latency timer self.set_latency_timer(latency) # Set chunk size # Beware that RX buffer, over 512 bytes, contains 2-byte modem marker # on every 512 byte chunk, so data and out-of-band marker get # interleaved. This is not yet supported with read_data_bytes for now self.write_data_set_chunksize() self.read_data_set_chunksize() # disable flow control self.set_flowctrl('') # Enable BITBANG mode self.set_bitmode(direction, Ftdi.BitMode.BITBANG if not sync else Ftdi.BitMode.SYNCBB) # Configure clock if baudrate: self._baudrate = self._set_baudrate(baudrate, False) # Drain input buffer self.purge_buffers() return self._baudrate @property def usb_path(self) -> Tuple[int, int, int]: """Provide the physical location on the USB topology. :return: a tuple of bus, address, interface; if connected """ if not self.is_connected: raise FtdiError('Not connected') return (self._usb_dev.bus, self._usb_dev.address, self._interface.bInterfaceNumber) @property def device_version(self) -> int: """Report the device version, i.e. the kind of device. :see: :py:meth:`ic_name` for a product version of this information. :return: the device version (16-bit integer) """ if not self.is_connected: raise FtdiError('Device characteristics not yet known') return self._usb_dev.bcdDevice @property def ic_name(self) -> str: """Return the current type of the FTDI device as a string see also http://www.ftdichip.com/Support/ Documents/TechnicalNotes/TN_100_USB_VID-PID_Guidelines.pdf :return: the identified FTDI device as a string """ if not self.is_connected: return 'unknown' return self.DEVICE_NAMES.get(self.device_version, 'undefined') @property def device_port_count(self) -> int: """Report the count of port/interface of the Ftdi device. :return: the count of ports """ if not self.is_connected: raise FtdiError('Device characteristics not yet known') return self._usb_dev.get_active_configuration().bNumInterfaces @property def port_index(self) -> int: """Report the port/interface index, starting from 1 :return: the port position/index """ if not self.is_connected: raise FtdiError('Device characteristics not yet known') return self._index @property def port_width(self) -> int: """Report the width of a single port / interface :return: the width of the port, in bits :raise FtdiError: if no FTDI port is open """ if not self.is_connected: raise FtdiError('Device characteristics not yet known') if self.device_version in (0x0700, 0x0900): return 16 if self.device_version in (0x0500, ): return 12 return 8 @property def has_mpsse(self) -> bool: """Tell whether the device supports MPSSE (I2C, SPI, JTAG, ...) :return: True if the FTDI device supports MPSSE :raise FtdiError: if no FTDI port is open """ if not self.is_connected: raise FtdiError('Device characteristics not yet known') return self.device_version in (0x0500, 0x0700, 0x0800, 0x0900) @property def has_wide_port(self) -> bool: """Tell whether the device supports 16-bit GPIO ports (vs. 8 bits) :return: True if the FTDI device supports wide GPIO port :raise FtdiError: if no FTDI port is open """ return self.port_width > 8 @property def has_cbus(self) -> bool: """Tell whether the device supports CBUS bitbang. CBUS bitbanging feature requires a special configuration in EEPROM. This function only reports if the current device supports this mode, not if this mode has been enabled in EEPROM. EEPROM configuration must be queried to check which CBUS pins have been configured for GPIO/bitbang mode. :return: True if the FTDI device supports CBUS bitbang :raise FtdiError: if no FTDI port is open """ if not self.is_connected: raise FtdiError('Device characteristics not yet known') return self.device_version in (0x0600, 0x0900, 0x1000) @property def has_drivezero(self) -> bool: """Tell whether the device supports drive-zero mode, i.e. if the device supports the open-collector drive mode, useful for I2C communication for example. :return: True if the FTDI device features drive-zero mode :raise FtdiError: if no FTDI port is open """ if not self.is_connected: raise FtdiError('Device characteristics not yet known') return self.device_version in (0x0900, ) @property def is_legacy(self) -> bool: """Tell whether the device is a low-end FTDI :return: True if the FTDI device can only be used as a slow USB-UART bridge :raise FtdiError: if no FTDI port is open """ if not self.is_connected: raise FtdiError('Device characteristics not yet known') return self.device_version <= 0x0200 @property def is_H_series(self) -> bool: """Tell whether the device is a high-end FTDI :return: True if the FTDI device is a high-end USB-UART bridge :raise FtdiError: if no FTDI port is open """ if not self.is_connected: raise FtdiError('Device characteristics not yet known') return self.device_version in (0x0700, 0x0800, 0x0900) @property def is_mpsse(self) -> bool: """Tell whether the device is configured in MPSSE mode :return: True if the FTDI interface is configured in MPSSE mode """ return self._bitmode == Ftdi.BitMode.MPSSE def is_mpsse_interface(self, interface: int) -> bool: """Tell whether the interface supports MPSSE (I2C, SPI, JTAG, ...) :return: True if the FTDI interface supports MPSSE :raise FtdiError: if no FTDI port is open """ if not self.has_mpsse: return False if self.device_version == 0x0800 and interface > 2: return False return True @property def is_bitbang_enabled(self) -> bool: """Tell whether some bitbang mode is activated :return: True if the FTDI interface is configured to support bitbanging """ return self._bitmode not in ( Ftdi.BitMode.RESET, Ftdi.BitMode.MPSSE, Ftdi.BitMode.CBUS # CBUS mode does not change base frequency ) # legacy API bitbang_enabled = is_bitbang_enabled @property def is_eeprom_internal(self) -> bool: """Tell whether the device has an internal EEPROM. :return: True if the device has an internal EEPROM. """ return self.device_version in self.INT_EEPROMS @property def max_eeprom_size(self) -> int: """Report the maximum size of the EEPROM. The actual size may be lower, of even 0 if no EEPROM is connected or supported. :return: the maximum size in bytes. """ if self.device_version in self.INT_EEPROMS: return self.INT_EEPROMS[self.device_version] if self.device_version == 0x0600: return 0x80 return 0x100 @property def frequency_max(self) -> float: """Tells the maximum frequency for MPSSE clock. :return: the maximum supported frequency in Hz """ return Ftdi.BUS_CLOCK_HIGH if self.is_H_series else Ftdi.BUS_CLOCK_BASE @property def fifo_sizes(self) -> Tuple[int, int]: """Return the (TX, RX) tupple of hardware FIFO sizes :return: 2-tuple of TX, RX FIFO size in bytes """ try: return Ftdi.FIFO_SIZES[self.device_version] except KeyError as exc: raise FtdiFeatureError('Unsupported device: 0x%04x' % self.device_version) from exc @property def mpsse_bit_delay(self) -> float: """Delay between execution of two MPSSE SET_BITS commands. :return: minimum delay (actual value might be larger) in seconds """ # measured on FTDI2232H, not documented in datasheet, hence may vary # from on FTDI model to another... # left as a variable so it could be tweaked base on the FTDI bcd type, # the frequency, or ... whatever else return 0.5E-6 # seems to vary between 5 and 6.5 us @property def baudrate(self) -> int: """Return current baudrate. """ return self._baudrate @property def usb_dev(self) -> UsbDevice: """Return the underlying USB Device. """ return self._usb_dev def set_baudrate(self, baudrate: int, constrain: bool = True) -> int: """Change the current UART or BitBang baudrate. The FTDI device is not able to use an arbitrary baudrate. Its internal dividors are only able to achieve some baudrates. PyFtdi attemps to find the closest configurable baudrate and if the deviation from the requested baudrate is too high, it rejects the configuration if constrain is set. :py:attr:`baudrate` attribute can be used to retrieve the exact selected baudrate. :py:const:`BAUDRATE_TOLERANCE` defines the maximum deviation between the requested baudrate and the closest FTDI achieveable baudrate, which matches standard UART clock drift (3%). If the achievable baudrate is not within limits, baudrate setting is rejected. :param baudrate: the new baudrate for the UART. :param constrain: whether to validate baudrate is in RS232 tolerance limits or allow larger drift :raise ValueError: if deviation from selected baudrate is too large :raise FtdiError: on IO Error :return: the effective baudrate """ self._baudrate = self._set_baudrate(baudrate, constrain) return self._baudrate def set_frequency(self, frequency: float) -> float: """Change the current MPSSE bus frequency The FTDI device is not able to use an arbitrary frequency. Its internal dividors are only able to achieve some frequencies. PyFtdi finds and selects the closest configurable frequency. :param frequency: the new frequency for the serial interface, in Hz. :return: the selected frequency, which may differ from the requested one, in Hz """ return self._set_frequency(frequency) def purge_rx_buffer(self) -> None: """Clear the USB receive buffer on the chip (host-to-ftdi) and the internal read buffer.""" if self._ctrl_transfer_out(Ftdi.SIO_REQ_RESET, Ftdi.SIO_RESET_PURGE_RX): raise FtdiError('Unable to flush RX buffer') # Invalidate data in the readbuffer self._readoffset = 0 self._readbuffer = bytearray() self.log.debug('rx buf purged') def purge_tx_buffer(self) -> None: """Clear the USB transmit buffer on the chip (ftdi-to-host).""" if self._ctrl_transfer_out(Ftdi.SIO_REQ_RESET, Ftdi.SIO_RESET_PURGE_TX): raise FtdiError('Unable to flush TX buffer') def purge_buffers(self) -> None: """Clear the buffers on the chip and the internal read buffer.""" self.purge_rx_buffer() self.purge_tx_buffer() def write_data_set_chunksize(self, chunksize: int = 0) -> None: """Configure write buffer chunk size. This is a low-level configuration option, which is not intended to be use for a regular usage. :param chunksize: the optional size of the write buffer in bytes, it is recommended to use 0 to force automatic evaluation of the best value. """ if chunksize == 0: chunksize = self.fifo_sizes[0] self._writebuffer_chunksize = chunksize self.log.debug('TX chunksize: %d', self._writebuffer_chunksize) def write_data_get_chunksize(self) -> int: """Get write buffer chunk size. :return: the size of the write buffer in bytes """ return self._writebuffer_chunksize def read_data_set_chunksize(self, chunksize: int = 0) -> None: """Configure read buffer chunk size. This is a low-level configuration option, which is not intended to be use for a regular usage. :param chunksize: the optional size of the read buffer in bytes, it is recommended to use 0 to force automatic evaluation of the best value. """ # Invalidate all remaining data self._readoffset = 0 self._readbuffer = bytearray() if chunksize == 0: # status byte prolog is emitted every maxpacketsize, but for "some" # reasons, FT232R emits it every RX FIFO size bytes... Other # devices use a maxpacketsize which is smaller or equal to their # FIFO size, so this weird behavior is for now only experienced # with FT232R. Any, the following compution should address all # devices. chunksize = min(self.fifo_sizes[0], self.fifo_sizes[1], self._max_packet_size) if platform == 'linux': if chunksize > 16384: chunksize = 16384 self._readbuffer_chunksize = chunksize self.log.debug('RX chunksize: %d', self._readbuffer_chunksize) def read_data_get_chunksize(self) -> int: """Get read buffer chunk size. :return: the size of the write buffer in bytes """ return self._readbuffer_chunksize def set_bitmode(self, bitmask: int, mode: 'Ftdi.BitMode') -> None: """Enable/disable bitbang modes. Switch the FTDI interface to bitbang mode. """ self.log.debug('bitmode: %s', mode.name) mask = sum(Ftdi.BitMode) value = (bitmask & 0xff) | ((mode.value & mask) << 8) if self._ctrl_transfer_out(Ftdi.SIO_REQ_SET_BITMODE, value): raise FtdiError('Unable to set bitmode') self._bitmode = mode def read_pins(self) -> int: """Directly read pin state, circumventing the read buffer. Useful for bitbang mode. :return: bitfield of FTDI interface input GPIO """ pins = self._ctrl_transfer_in(Ftdi.SIO_REQ_READ_PINS, 1) if not pins: raise FtdiError('Unable to read pins') return pins[0] def set_cbus_direction(self, mask: int, direction: int) -> None: """Configure the CBUS pins used as GPIOs :param mask: which pins to configure as GPIOs :param direction: which pins are output (vs. input) """ # sanity check: there cannot be more than 4 CBUS pins in bitbang mode if not 0 <= mask <= 0x0F: raise ValueError('Invalid CBUS gpio mask: 0x%02x' % mask) if not 0 <= direction <= 0x0F: raise ValueError('Invalid CBUS gpio direction: 0x%02x' % direction) self._cbus_pins = (mask, direction) def get_cbus_gpio(self) -> int: """Get the CBUS pins configured as GPIO inputs :return: bitfield of CBUS read pins """ if self._bitmode not in (Ftdi.BitMode.RESET, Ftdi.BitMode.CBUS): raise FtdiError('CBUS gpio not available from current mode') if not self._cbus_pins[0] & ~self._cbus_pins[1]: raise FtdiError('No CBUS IO configured as input') outv = (self._cbus_pins[1] << 4) | self._cbus_out oldmode = self._bitmode try: self.set_bitmode(outv, Ftdi.BitMode.CBUS) inv = self.read_pins() finally: if oldmode != self._bitmode: self.set_bitmode(0, oldmode) return inv & ~self._cbus_pins[1] & self._cbus_pins[0] def set_cbus_gpio(self, pins: int) -> None: """Set the CBUS pins configured as GPIO outputs :param pins: bitfield to apply to CBUS output pins """ if self._bitmode not in (Ftdi.BitMode.RESET, Ftdi.BitMode.CBUS): raise FtdiError('CBUS gpio not available from current mode') # sanity check: there cannot be more than 4 CBUS pins in bitbang mode if not 0 <= pins <= 0x0F: raise ValueError('Invalid CBUS gpio pins: 0x%02x' % pins) if not self._cbus_pins[0] & self._cbus_pins[1]: raise FtdiError('No CBUS IO configured as output') pins &= self._cbus_pins[0] & self._cbus_pins[1] value = (self._cbus_pins[1] << 4) | pins oldmode = self._bitmode try: self.set_bitmode(value, Ftdi.BitMode.CBUS) self._cbus_out = pins finally: if oldmode != self._bitmode: self.set_bitmode(0, oldmode) def set_latency_timer(self, latency: int): """Set latency timer. The FTDI chip keeps data in the internal buffer for a specific amount of time if the buffer is not full yet to decrease load on the usb bus. The shorted the latency, the shorted the delay to obtain data and the higher the host CPU load. Be careful with this option. :param latency: latency (unspecified unit) """ if not Ftdi.LATENCY_MIN <= latency <= Ftdi.LATENCY_MAX: raise ValueError("Latency out of range") if self._ctrl_transfer_out(Ftdi.SIO_REQ_SET_LATENCY_TIMER, latency): raise FtdiError('Unable to latency timer') def get_latency_timer(self) -> int: """Get latency timer. :return: the current latency (unspecified unit) """ latency = self._ctrl_transfer_in(Ftdi.SIO_REQ_GET_LATENCY_TIMER, 1) if not latency: raise FtdiError('Unable to get latency') return latency[0] def poll_modem_status(self) -> int: """Poll modem status information. This function allows the retrieve the two status bytes of the device, useful in UART mode. FTDI device does not have a so-called USB "interrupt" end-point, event polling on the UART interface is done through the regular control endpoint. see :py:func:`modem_status` to obtain decoded status strings :return: modem status, as a proprietary bitfield """ value = self._ctrl_transfer_in(Ftdi.SIO_REQ_POLL_MODEM_STATUS, 2) if not value or len(value) != 2: raise FtdiError('Unable to get modem status') status, = sunpack(' Tuple[str, ...]: """Provide the current modem status as a tuple of set signals :return: decodede modem status as short strings """ value = self._ctrl_transfer_in(Ftdi.SIO_REQ_POLL_MODEM_STATUS, 2) if not value or len(value) != 2: raise FtdiError('Unable to get modem status') return self.decode_modem_status(value) def set_flowctrl(self, flowctrl: str) -> None: """Select flowcontrol in UART mode. Either hardware flow control through RTS/CTS UART lines, software or no flow control. :param str flowctrl: either 'hw' for HW flow control or '' (empty string) for no flow control. :raise ValueError: if the flow control argument is invalid .. note:: How does RTS/CTS flow control work (from FTDI FAQ): FTxxx RTS# pin is an output. It should be connected to the CTS# input pin of the device at the other end of the UART link. * If RTS# is logic 0 it is indicating the FTxxx device can accept more data on the RXD pin. * If RTS# is logic 1 it is indicating the FTxxx device cannot accept more data. RTS# changes state when the chip buffer reaches its last 32 bytes of space to allow time for the external device to stop sending data to the FTxxx device. FTxxx CTS# pin is an input. It should be connected to the RTS# output pin of the device at the other end of the UART link. * If CTS# is logic 0 it is indicating the external device can accept more data, and the FTxxx will transmit on the TXD pin. * 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. Word to the wise. Not only do RS232 level shifting devices level shift, but they also invert the signal. """ ctrl = {'hw': Ftdi.SIO_RTS_CTS_HS, '': Ftdi.SIO_DISABLE_FLOW_CTRL} try: value = ctrl[flowctrl] | self._index except KeyError as exc: raise ValueError('Unknown flow control: %s' % flowctrl) from exc try: if self._usb_dev.ctrl_transfer( Ftdi.REQ_OUT, Ftdi.SIO_REQ_SET_FLOW_CTRL, 0, value, bytearray(), self._usb_write_timeout): raise FtdiError('Unable to set flow control') except USBError as exc: raise FtdiError('UsbError: %s' % str(exc)) from exc def set_dtr(self, state: bool) -> None: """Set dtr line :param state: new DTR logical level """ value = Ftdi.SIO_SET_DTR_HIGH if state else Ftdi.SIO_SET_DTR_LOW if self._ctrl_transfer_out(Ftdi.SIO_REQ_SET_MODEM_CTRL, value): raise FtdiError('Unable to set DTR line') def set_rts(self, state: bool) -> None: """Set rts line :param state: new RTS logical level """ value = Ftdi.SIO_SET_RTS_HIGH if state else Ftdi.SIO_SET_RTS_LOW if self._ctrl_transfer_out(Ftdi.SIO_REQ_SET_MODEM_CTRL, value): raise FtdiError('Unable to set RTS line') def set_dtr_rts(self, dtr: bool, rts: bool) -> None: """Set dtr and rts lines at once :param dtr: new DTR logical level :param rts: new RTS logical level """ value = 0 value |= Ftdi.SIO_SET_DTR_HIGH if dtr else Ftdi.SIO_SET_DTR_LOW value |= Ftdi.SIO_SET_RTS_HIGH if rts else Ftdi.SIO_SET_RTS_LOW if self._ctrl_transfer_out(Ftdi.SIO_REQ_SET_MODEM_CTRL, value): raise FtdiError('Unable to set DTR/RTS lines') def set_break(self, break_: bool) -> None: """Start or stop a break exception event on the serial line :param break_: either start or stop break event """ if break_: value = self._lineprop | (0x01 << 14) if self._ctrl_transfer_out(Ftdi.SIO_REQ_SET_DATA, value): raise FtdiError('Unable to start break sequence') else: value = self._lineprop & ~(0x01 << 14) if self._ctrl_transfer_out(Ftdi.SIO_REQ_SET_DATA, value): raise FtdiError('Unable to stop break sequence') self._lineprop = value def set_event_char(self, eventch: int, enable: bool) -> None: """Set the special event character""" value = eventch if enable: value |= 1 << 8 if self._ctrl_transfer_out(Ftdi.SIO_REQ_SET_EVENT_CHAR, value): raise FtdiError('Unable to set event char') def set_error_char(self, errorch: int, enable: bool) -> None: """Set error character""" value = errorch if enable: value |= 1 << 8 if self._ctrl_transfer_out(Ftdi.SIO_REQ_SET_ERROR_CHAR, value): raise FtdiError('Unable to set error char') def set_line_property(self, bits: int, stopbit: Union[int, float], parity: str, break_: bool = False) -> None: """Configure the (RS232) UART characteristics. Arguments match the valid subset for FTDI HW of pyserial definitions. Bits accepts one of the following values: * ``7`` for 7-bit characters * ``8`` for 8-bit characters Stopbit accepts one of the following values: * ``1`` for a single bit * ``1.5`` for a bit and a half * ``2`` for two bits Parity accepts one of the following strings: * ``N`` for no parity bit * ``O`` for odd parity bit * ``E`` for even parity bit * ``M`` for parity bit always set * ``S`` for parity bit always reset :param bits: data bit count :param stopbit: stop bit count :param parity: parity mode as a single uppercase character :param break_: force break event """ bytelength = {7: Ftdi.BITS_7, 8: Ftdi.BITS_8} parities = {'N': Ftdi.PARITY_NONE, 'O': Ftdi.PARITY_ODD, 'E': Ftdi.PARITY_EVEN, 'M': Ftdi.PARITY_MARK, 'S': Ftdi.PARITY_SPACE} stopbits = {1: Ftdi.STOP_BIT_1, 1.5: Ftdi.STOP_BIT_15, 2: Ftdi.STOP_BIT_2} if parity not in parities: raise FtdiFeatureError("Unsupported parity") if bits not in bytelength: raise FtdiFeatureError("Unsupported byte length") if stopbit not in stopbits: raise FtdiFeatureError("Unsupported stop bits") value = bits & 0x0F try: value |= {Ftdi.PARITY_NONE: 0x00 << 8, Ftdi.PARITY_ODD: 0x01 << 8, Ftdi.PARITY_EVEN: 0x02 << 8, Ftdi.PARITY_MARK: 0x03 << 8, Ftdi.PARITY_SPACE: 0x04 << 8}[parities[parity]] value |= {Ftdi.STOP_BIT_1: 0x00 << 11, Ftdi.STOP_BIT_15: 0x01 << 11, Ftdi.STOP_BIT_2: 0x02 << 11}[stopbits[stopbit]] if break_ == Ftdi.BREAK_ON: value |= 0x01 << 14 except KeyError as exc: raise ValueError('Invalid line property') from exc if self._ctrl_transfer_out(Ftdi.SIO_REQ_SET_DATA, value): raise FtdiError('Unable to set line property') self._lineprop = value def enable_adaptive_clock(self, enable: bool = True) -> None: """Enable adaptative clock mode, useful in MPSEE mode. Adaptive clock is a unique feature designed for a feedback clock for JTAG with ARM core. :param enable: whether to enable or disable this mode. :raise FtdiMpsseError: if MPSSE mode is not enabled """ if not self.is_mpsse: raise FtdiMpsseError('Setting adaptive clock mode is only ' 'available from MPSSE mode') self.write_data(bytearray([enable and Ftdi.ENABLE_CLK_ADAPTIVE or Ftdi.DISABLE_CLK_ADAPTIVE])) def enable_3phase_clock(self, enable: bool = True) -> None: """Enable 3-phase clocking mode, useful in MPSSE mode. 3-phase clock is mostly useful with I2C mode. It is also be used as a workaround to support SPI mode 3. :param enable: whether to enable or disable this mode. :raise FtdiMpsseError: if MPSSE mode is not enabled or device is not capable of 3-phase clocking """ if not self.is_mpsse: raise FtdiMpsseError('Setting 3-phase clock mode is only ' 'available from MPSSE mode') if not self.is_H_series: raise FtdiFeatureError('This device does not support 3-phase ' 'clock') self.write_data(bytearray([enable and Ftdi.ENABLE_CLK_3PHASE or Ftdi.DISABLE_CLK_3PHASE])) def enable_drivezero_mode(self, lines: int) -> None: """Enable drive-zero mode, useful in MPSSE mode. drive-zero mode is mostly useful with I2C mode, to support the open collector driving mode. :param lines: bitfield of GPIO to drive in collector driven mode :raise FtdiMpsseError: if MPSSE mode is not enabled or device is not capable of drive-zero mode """ if not self.is_mpsse: raise FtdiMpsseError('Setting drive-zero mode is only ' 'available from MPSSE mode') if not self.has_drivezero: raise FtdiFeatureError('This device does not support drive-zero ' 'mode') self.write_data(bytearray([Ftdi.DRIVE_ZERO, lines & 0xff, (lines >> 8) & 0xff])) def enable_loopback_mode(self, loopback: bool = False) -> None: """Enable loopback, i.e. connect DO to DI in FTDI MPSSE port for test purposes only. It does not support UART (TX to RX) mode. :param loopback: whether to enable or disable this mode """ self.write_data(bytearray((Ftdi.LOOPBACK_START if loopback else Ftdi.LOOPBACK_END,))) def calc_eeprom_checksum(self, data: Union[bytes, bytearray]) -> int: """Calculate EEPROM checksum over the data :param data: data to compute checksum over. Must be an even number of bytes :return: checksum """ length = len(data) if not length: raise ValueError('No data to checksum') if length & 0x1: raise ValueError('Length not even') # NOTE: checksum is computed using 16-bit values in little endian # ordering checksum = 0XAAAA mtp = self.device_version == 0x1000 # FT230X for idx in range(0, length, 2): if mtp and 0x24 <= idx < 0x80: # special MTP user section which is not considered for the CRC continue val = ((data[idx+1] << 8) + data[idx]) & 0xffff checksum = val ^ checksum checksum = ((checksum << 1) & 0xffff) | ((checksum >> 15) & 0xffff) return checksum def read_eeprom(self, addr: int = 0, length: Optional[int] = None, eeprom_size: Optional[int] = None) -> bytes: """Read the EEPROM starting at byte address, addr, and returning length bytes. Here, addr and length are in bytes but we access a 16-bit word at a time, so automatically update addr and length to work with word accesses. :param addr: byte address that desire to read. :param length: byte length to read or None :param eeprom_size: total size in bytes of the eeprom or None :return: eeprom bytes, as an array of bytes """ eeprom_size = self._check_eeprom_size(eeprom_size) if length is None: length = eeprom_size if addr < 0 or (addr+length) > eeprom_size: raise ValueError('Invalid address/length') word_addr = addr >> 1 word_count = length >> 1 if (addr & 0x1) | (length & 0x1): word_count += 1 try: data = bytearray() while word_count: buf = self._usb_dev.ctrl_transfer( Ftdi.REQ_IN, Ftdi.SIO_REQ_READ_EEPROM, 0, word_addr, 2, self._usb_read_timeout) if not buf: raise FtdiEepromError('EEPROM read error @ %d' % (word_addr << 1)) data.extend(buf) word_count -= 1 word_addr += 1 start = addr & 0x1 return bytes(data[start:start+length]) except USBError as exc: raise FtdiError('UsbError: %s' % exc) from exc def write_eeprom(self, addr: int, data: Union[bytes, bytearray], eeprom_size: Optional[int] = None, dry_run: bool = True) -> None: """Write multiple bytes to the EEPROM starting at byte address, addr. This function also updates the checksum automatically. .. warning:: You can brick your device with invalid size or content. Use this function at your own risk, and RTFM. :param addr: starting byte address to start writing :param data: data to be written :param eeprom_size: total size in bytes of the eeprom or None :param dry_run: log what should be written, do not actually change the EEPROM content """ eeprom_size = self._check_eeprom_size(eeprom_size) if not data: return length = len(data) if addr < 0 or (addr+length) > eeprom_size: # accept up to eeprom_size, even if the last two bytes are # overwritten with a locally computed checksum raise ValueError('Invalid address/length') # First, read out the entire EEPROM, based on eeprom_size. eeprom = bytearray(self.read_eeprom(0, eeprom_size)) # patch in the new data eeprom[addr:addr+len(data)] = data # compute new checksum chksum = self.calc_eeprom_checksum(eeprom[:-2]) self.log.info('New EEPROM checksum: 0x%04x', chksum) # insert updated checksum - it is last 16-bits in EEPROM if self.device_version == 0x1000: # FT230x EEPROM structure is different eeprom[0x7e] = chksum & 0x0ff eeprom[0x7f] = chksum >> 8 else: eeprom[-2] = chksum & 0x0ff eeprom[-1] = chksum >> 8 # Write back the new data and checksum back to # EEPROM. Only write data that is changing instead of writing # everything in EEPROM, even if the data does not change. # # Compute start and end sections of eeprom baring in mind that # they must be even since it is a 16-bit EEPROM. # If start addr is odd, back it up one. start = addr size = length if start & 0x1: start -= 1 size += 1 if size & 0x1: size += 1 if size > eeprom_size-2: size = eeprom_size-2 # finally, write new section of data and ... self._write_eeprom_raw(start, eeprom[start:start+size], dry_run=dry_run) # ... updated checksum self._write_eeprom_raw((eeprom_size-2), eeprom[-2:], dry_run=dry_run) def overwrite_eeprom(self, data: Union[bytes, bytearray], dry_run: bool = True) -> None: """Write the whole EEPROM content, from first to last byte. .. warning:: You can brick your device with invalid size or content. Use this function at your own risk, and RTFM. :param data: data to be written (should include the checksum) :param dry_run: log what should be written, do not actually change the EEPROM content """ if self.is_eeprom_internal: eeprom_size = self.INT_EEPROMS[self.device_version] if len(data) != eeprom_size: raise ValueError('Invalid EEPROM size') elif len(data) not in self.EXT_EEPROM_SIZES: raise ValueError('Invalid EEPROM size') self._write_eeprom_raw(0, data, dry_run=dry_run) def write_data(self, data: Union[bytes, bytearray]) -> int: """Write data to the FTDI port. In UART mode, data contains the serial stream to write to the UART interface. In MPSSE mode, data contains the sequence of MPSSE commands and data. Data buffer is split into chunk-sized blocks before being sent over the USB bus. :param data: the byte stream to send to the FTDI interface :return: count of written bytes """ offset = 0 size = len(data) try: while offset < size: write_size = self._writebuffer_chunksize if offset + write_size > size: write_size = size - offset length = self._write(data[offset:offset+write_size]) if length <= 0: raise FtdiError("Usb bulk write error") offset += length return offset except USBError as exc: raise FtdiError('UsbError: %s' % str(exc)) from exc def read_data_bytes(self, size: int, attempt: int = 1, request_gen: Optional[Callable[[int], bytes]] = None) \ -> bytes: """Read data from the FTDI interface In UART mode, data contains the serial stream read from the UART interface. In MPSSE mode, data contains the sequence of data received and processed with the MPSEE engine. Data buffer is rebuilt from chunk-sized blocks received over the USB bus. FTDI device always sends internal status bytes, which are stripped out as not part of the data payload. Because of the multiple buses, buffers, FIFOs, and MPSSE command processing, data might not be immediately available on the host side. The attempt argument can be used to increase the attempt count to retrieve the expected amount of data, before giving up and returning all the received data, which may be shorted than the requested amount. :param size: the number of bytes to received from the device :param attempt: attempt cycle count :param request_gen: a callable that takes the number of bytes read and expect a bytes byffer to send back to the remote device. This is only useful to perform optimized/continuous transfer from a slave device. :return: payload bytes, as bytes """ # Packet size sanity check if not self._max_packet_size: raise FtdiError("max_packet_size is bogus") packet_size = self._max_packet_size length = 1 # initial condition to enter the usb_read loop data = bytearray() # everything we want is still in the cache? if size <= len(self._readbuffer)-self._readoffset: data = self._readbuffer[self._readoffset:self._readoffset+size] self._readoffset += size return data # something still in the cache, but not enough to satisfy 'size'? if len(self._readbuffer)-self._readoffset != 0: data = self._readbuffer[self._readoffset:] # end of readbuffer reached self._readoffset = len(self._readbuffer) # read from USB, filling in the local cache as it is empty retry = attempt req_size = size try: while (len(data) < size) and (length > 0): while True: tempbuf = self._read() retry -= 1 length = len(tempbuf) # the received buffer contains at least one useful databyte # (first 2 bytes in each packet represent the current modem # status) if length >= 2: if tempbuf[1] & self.TX_EMPTY_BITS: if request_gen: req_size -= length-2 if req_size > 0: cmd = request_gen(req_size) if cmd: self.write_data(cmd) if length > 2: retry = attempt if self._latency_threshold: self._adapt_latency(True) # skip the status bytes chunks = (length+packet_size-1) // packet_size count = packet_size - 2 # if you want to show status, use the following code: status = tempbuf[:2] if status[1] & self.ERROR_BITS[1]: self.log.error( 'FTDI error: %02x:%02x %s', status[0], status[1], (' '.join( self.decode_modem_status(status, True)).title())) self._readbuffer = bytearray() self._readoffset = 0 srcoff = 2 for _ in range(chunks): self._readbuffer += tempbuf[srcoff:srcoff+count] srcoff += packet_size length = len(self._readbuffer) break # received buffer only contains the modem status bytes # no data received, may be late, try again if retry > 0: continue # no actual data self._readbuffer = bytearray() self._readoffset = 0 if self._latency_threshold: self._adapt_latency(False) # no more data to read? return data if length > 0: # data still fits in buf? if (len(data) + length) <= size: data += self._readbuffer[self._readoffset: self._readoffset+length] self._readoffset += length # did we read exactly the right amount of bytes? if len(data) == size: return data else: # partial copy, not enough bytes in the local cache to # fulfill the request part_size = min(size-len(data), len(self._readbuffer)-self._readoffset) if part_size < 0: raise FtdiError("Internal Error") data += self._readbuffer[self._readoffset: self._readoffset+part_size] self._readoffset += part_size return data except USBError as exc: raise FtdiError('UsbError: %s' % str(exc)) from exc # never reached raise FtdiError("Internal error") def read_data(self, size: int) -> bytes: """Shortcut to received a bytes buffer instead of the array of bytes. Note that output byte buffer may be shorted than the requested size. :param size: the number of bytes to received from the device :return: payload bytes """ return bytes(self.read_data_bytes(size)) def get_cts(self) -> bool: """Read terminal status line: Clear To Send :return: CTS line logical level """ status = self.poll_modem_status() return bool(status & self.MODEM_CTS) def get_dsr(self) -> bool: """Read terminal status line: Data Set Ready :return: DSR line logical level """ status = self.poll_modem_status() return bool(status & self.MODEM_DSR) def get_ri(self) -> bool: """Read terminal status line: Ring Indicator :return: RI line logical level """ status = self.poll_modem_status() return bool(status & self.MODEM_RI) def get_cd(self) -> bool: """Read terminal status line: Carrier Detect :return: CD line logical level """ status = self.poll_modem_status() return bool(status & self.MODEM_RLSD) def set_dynamic_latency(self, lmin: int, lmax: int, threshold: int) -> None: """Set up or disable latency values. Dynamic latency management is a load balancer to adapt the responsiveness of FTDI read request vs. the host CPU load. It is mostly useful in UART mode, so that read bandwidth can be increased to the maximum achievable throughput, while maintaining very low host CPU load when no data is received from the UART. There should be no need to tweak the default values. Use with care. Minimum latency is limited to 12 or above, at FTDI device starts losing bytes when latency is too short... Maximum latency value is 255 ms. Polling latency is reset to `lmin` each time at least one payload byte is received from the FTDI device. It doubles, up to `lmax`, every `threshold` times no payload has been received from the FTDI device. :param lmin: minimum latency level (ms) :param lmax: maximum latenty level (ms) :param threshold: count to reset latency to maximum level """ if not threshold: self._latency_count = 0 self._latency_threshold = None else: for lat in (lmin, lmax): if not self.LATENCY_MIN <= lat <= self.LATENCY_MAX: raise ValueError("Latency out of range: %d" % lat) self._latency_min = lmin self._latency_max = lmax self._latency_threshold = threshold self._latency = lmin self.set_latency_timer(self._latency) def validate_mpsse(self) -> None: """Check that the previous MPSSE request has been accepted by the FTDI device. :raise FtdiError: if the FTDI device rejected the command. """ # only useful in MPSSE mode bytes_ = self.read_data(2) if (len(bytes_) >= 2) and (bytes_[0] == '\xfa'): raise FtdiError("Invalid command @ %d" % bytes_[1]) @classmethod def get_error_string(cls) -> str: """Wrapper for legacy compatibility. :return: a constant, meaningless string """ return "Unknown error" # --- Private implementation ------------------------------------------- def _set_interface(self, config: UsbConfiguration, ifnum: int): """Select the interface to use on the FTDI device""" if ifnum == 0: ifnum = 1 if ifnum-1 not in range(config.bNumInterfaces): raise ValueError("No such interface for this device") self._interface = config[(ifnum-1, 0)] self._index = self._interface.bInterfaceNumber+1 endpoints = sorted([ep.bEndpointAddress for ep in self._interface]) self._in_ep, self._out_ep = endpoints[:2] # detach kernel driver from the interface try: if self._usb_dev.is_kernel_driver_active(self._index - 1): self._usb_dev.detach_kernel_driver(self._index - 1) except (NotImplementedError, USBError): pass #pylint: disable-msg=protected-access # need to access private member _ctx of PyUSB device (resource manager) # until PyUSB #302 is addressed def _reset_usb_device(self) -> None: """Reset USB device (USB command, not FTDI specific).""" self._usb_dev._ctx.backend.reset_device(self._usb_dev._ctx.handle) def _is_pyusb_handle_active(self) -> bool: # Unfortunately, we need to access pyusb ResourceManager # and there is no public API for this. return bool(self._usb_dev._ctx.handle) #pylint: enable-msg=protected-access def _reset_device(self): """Reset the FTDI device (FTDI vendor command)""" if self._ctrl_transfer_out(Ftdi.SIO_REQ_RESET, Ftdi.SIO_RESET_SIO): raise FtdiError('Unable to reset FTDI device') def _ctrl_transfer_out(self, reqtype: int, value: int, data: bytes = b''): """Send a control message to the device""" try: return self._usb_dev.ctrl_transfer( Ftdi.REQ_OUT, reqtype, value, self._index, bytearray(data), self._usb_write_timeout) except USBError as ex: raise FtdiError('UsbError: %s' % str(ex)) from None def _ctrl_transfer_in(self, reqtype: int, length: int): """Request for a control message from the device""" try: return self._usb_dev.ctrl_transfer( Ftdi.REQ_IN, reqtype, 0, self._index, length, self._usb_read_timeout) except USBError as ex: raise FtdiError('UsbError: %s' % str(ex)) from None def _write(self, data: Union[bytes, bytearray]) -> int: if self._debug_log: try: self.log.debug('> %s', hexlify(data).decode()) except TypeError as exc: self.log.warning('> (invalid output byte sequence: %s)', exc) if self._tracer: self._tracer.send(self._index, data) try: return self._usb_dev.write(self._in_ep, data, self._usb_write_timeout) except USBError as ex: raise FtdiError('UsbError: %s' % str(ex)) from None def _read(self) -> bytes: try: data = self._usb_dev.read(self._out_ep, self._readbuffer_chunksize, self._usb_read_timeout) except USBError as ex: raise FtdiError('UsbError: %s' % str(ex)) from None if data: if self._debug_log: self.log.debug('< %s', hexlify(data).decode()) if self._tracer and len(data) > 2: self._tracer.receive(self._index, data[2:]) return data def _adapt_latency(self, payload_detected: bool) -> None: """Dynamic latency adaptation depending on the presence of a payload in a RX buffer. :param payload_detected: whether a payload has been received within last RX buffer """ if payload_detected: self._latency_count = 0 if self._latency != self._latency_min: self.set_latency_timer(self._latency_min) self._latency = self._latency_min return # no payload received self._latency_count += 1 if self._latency != self._latency_max: if self._latency_count > \ self._latency_threshold: self._latency *= 2 if self._latency > self._latency_max: self._latency = self._latency_max else: self._latency_count = 0 self.set_latency_timer(self._latency) def _check_eeprom_size(self, eeprom_size: Optional[int]) -> int: if self.device_version in self.INT_EEPROMS: if (eeprom_size and eeprom_size != self.INT_EEPROMS[self.device_version]): raise ValueError('Invalid EEPROM size: %d' % eeprom_size) eeprom_size = self.INT_EEPROMS[self.device_version] else: if eeprom_size is None: eeprom_size = self.max_eeprom_size if eeprom_size not in self.EXT_EEPROM_SIZES: raise ValueError('Invalid EEPROM size: %d' % eeprom_size) return eeprom_size def _write_eeprom_raw(self, addr: int, data: Union[bytes, bytearray], dry_run: bool = True) -> None: """Write multiple bytes to the EEPROM starting at byte address, addr. Length of data must be a multiple of 2 since the EEPROM is 16-bits. So automatically extend data by 1 byte if this is not the case. :param int addr: starting byte address to start writing :param bytes data: data to be written :param dry_run: log what should be written, do not actually change the EEPROM content """ if self.device_version == 0x0600: # FT232R internal EEPROM is unstable and latency timer seems # to have a direct impact on EEPROM programming... latency = self.get_latency_timer() else: latency = 0 try: if latency: self.set_latency_timer(self.LATENCY_EEPROM_FT232R) length = len(data) if addr & 0x1 or length & 0x1: raise ValueError('Address/length not even') for word in sunpack('<%dH' % (length//2), data): if not dry_run: out = self._usb_dev.ctrl_transfer( Ftdi.REQ_OUT, Ftdi.SIO_REQ_WRITE_EEPROM, word, addr >> 1, b'', self._usb_write_timeout) if out: raise FtdiEepromError('EEPROM Write Error @ %d' % addr) self.log.debug('Write EEPROM [0x%02x]: 0x%04x', addr, word) else: self.log.info('Fake write EEPROM [0x%02x]: 0x%04x', addr, word) addr += 2 finally: if latency: self.set_latency_timer(latency) def _get_max_packet_size(self) -> int: """Retrieve the maximum length of a data packet""" if not self.is_connected: raise IOError("Device is not yet known", ENODEV) if not self._interface: raise IOError("Interface is not yet known", ENODEV) endpoint = self._interface[0] packet_size = endpoint.wMaxPacketSize return packet_size def _convert_baudrate_legacy(self, baudrate: int) -> Tuple[int, int, int]: if baudrate > self.BAUDRATE_REF_BASE: raise ValueError('Invalid baudrate (too high)') div8 = int(round((8 * self.BAUDRATE_REF_BASE) / baudrate)) if (div8 & 0x7) == 7: div8 += 1 div = div8 >> 3 div8 &= 0x7 if div8 == 1: div |= 0xc000 elif div8 >= 4: div |= 0x4000 elif div8 != 0: div |= 0x8000 elif div == 1: div = 0 value = div & 0xFFFF index = (div >> 16) & 0xFFFF estimate = int(((8 * self.BAUDRATE_REF_BASE) + (div8//2))//div8) return estimate, value, index def _convert_baudrate(self, baudrate: int) -> Tuple[int, int, int]: """Convert a requested baudrate into the closest possible baudrate that can be assigned to the FTDI device :param baudrate: the baudrate in bps :return: a 3-uple of the apprimated baudrate, the value and index to use as the USB configuration parameter """ if self.device_version == 0x200: return self._convert_baudrate_legacy(baudrate) if self.is_H_series and baudrate >= 1200: hispeed = True clock = self.BAUDRATE_REF_HIGH bb_ratio = self.BITBANG_BAUDRATE_RATIO_HIGH else: hispeed = False clock = self.BAUDRATE_REF_BASE bb_ratio = self.BITBANG_BAUDRATE_RATIO_BASE if baudrate > clock: raise ValueError('Invalid baudrate (too high)') if baudrate < ((clock >> 14) + 1): raise ValueError('Invalid baudrate (too low)') if self.is_bitbang_enabled: baudrate //= bb_ratio div8 = int(round((8 * clock) / baudrate)) div = div8 >> 3 div |= self.FRAC_DIV_CODE[div8 & 0x7] << 14 if div == 1: div = 0 elif div == 0x4001: div = 1 if hispeed: div |= 0x00020000 value = div & 0xFFFF index = (div >> 16) & 0xFFFF if self.device_version >= 0x0700 or self.device_version == 0x0500: index <<= 8 index |= self._index estimate = int(((8 * clock) + (div8//2))//div8) if self.is_bitbang_enabled: estimate *= bb_ratio return estimate, value, index def _set_baudrate(self, baudrate: int, constrain: bool) -> int: if self.is_mpsse: raise FtdiFeatureError('Cannot change frequency w/ current mode') actual, value, index = self._convert_baudrate(baudrate) delta = 100*abs(float(actual-baudrate))/baudrate self.log.debug('Actual baudrate: %d %.1f%% div [%04x:%04x]', actual, delta, index, value) # return actual if constrain and delta > Ftdi.BAUDRATE_TOLERANCE: raise ValueError('Baudrate tolerance exceeded: %.02f%% ' '(wanted %d, achievable %d)' % (delta, baudrate, actual)) try: if self._usb_dev.ctrl_transfer( Ftdi.REQ_OUT, Ftdi.SIO_REQ_SET_BAUDRATE, value, index, bytearray(), self._usb_write_timeout): raise FtdiError('Unable to set baudrate') return actual except USBError as exc: raise FtdiError('UsbError: %s' % str(exc)) from exc def _set_frequency(self, frequency: float) -> float: """Convert a frequency value into a TCK divisor setting""" if not self.is_mpsse: raise FtdiFeatureError('Cannot change frequency w/ current mode') if frequency > self.frequency_max: raise FtdiFeatureError('Unsupported frequency: %f' % frequency) # Calculate base speed clock divider divcode = Ftdi.ENABLE_CLK_DIV5 divisor = int((Ftdi.BUS_CLOCK_BASE+frequency/2)/frequency)-1 divisor = max(0, min(0xFFFF, divisor)) actual_freq = Ftdi.BUS_CLOCK_BASE/(divisor+1) error = (actual_freq/frequency)-1 # Should we use high speed clock available in H series? if self.is_H_series: # Calculate high speed clock divider divisor_hs = int((Ftdi.BUS_CLOCK_HIGH+frequency/2)/frequency)-1 divisor_hs = max(0, min(0xFFFF, divisor_hs)) actual_freq_hs = Ftdi.BUS_CLOCK_HIGH/(divisor_hs+1) error_hs = (actual_freq_hs/frequency)-1 # Enable if closer to desired frequency (percentually) if abs(error_hs) < abs(error): divcode = Ftdi.DISABLE_CLK_DIV5 divisor = divisor_hs actual_freq = actual_freq_hs error = error_hs # FTDI expects little endian if self.is_H_series: cmd = bytearray((divcode,)) else: cmd = bytearray() cmd.extend((Ftdi.SET_TCK_DIVISOR, divisor & 0xff, (divisor >> 8) & 0xff)) self.write_data(cmd) self.validate_mpsse() # Drain input buffer self.purge_rx_buffer() # Note that bus frequency may differ from clock frequency, when # 3-phase clock is enable, in which case bus frequency = 2/3 clock # frequency if actual_freq > 1E6: self.log.debug('Clock frequency: %.6f MHz (error: %+.1f %%)', (actual_freq/1E6), error*100) else: self.log.debug('Clock frequency: %.3f KHz (error: %+.1f %%)', (actual_freq/1E3), error*100) return actual_freq def __get_timeouts(self) -> Tuple[int, int]: return self._usb_read_timeout, self._usb_write_timeout def __set_timeouts(self, timeouts: Tuple[int, int]): (read_timeout, write_timeout) = timeouts self._usb_read_timeout = read_timeout self._usb_write_timeout = write_timeout timeouts = property(__get_timeouts, __set_timeouts) pyftdi-0.54.0/pyftdi/gpio.py000066400000000000000000000516111421346242200157120ustar00rootroot00000000000000# Copyright (c) 2014-2020, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """GPIO/BitBang support for PyFdti""" #pylint: disable-msg=too-few-public-methods 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 udpated 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 = '<%dH' % count 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('Cannot read GPIO, recv %d out of %d bytes' % (len(data), size)) 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)) pyftdi-0.54.0/pyftdi/i2c.py000066400000000000000000001257011421346242200154330ustar00rootroot00000000000000# Copyright (c) 2017-2021, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """I2C support for PyFdti""" #pylint: disable-msg=too-many-lines #pylint: disable-msg=too-many-locals #pylint: disable-msg=too-many-instance-attributes #pylint: disable-msg=too-many-public-methods #pylint: disable-msg=too-many-arguments #pylint: disable-msg=too-many-branches #pylint: disable-msg=too-many-statements from binascii import hexlify from collections import namedtuple 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, Tuple, Union from usb.core import Device as UsbDevice from .ftdi import Ftdi, FtdiFeatureError from .misc import to_bool class I2cIOError(IOError): """I2c I/O error""" class I2cNackError(I2cIOError): """I2c NACK receive from slave""" class I2cTimeoutError(TimeoutError): """I2c timeout on polling""" class I2cPort: """I2C port. An I2C port is never instanciated directly: use :py:meth:`I2cController.get_port()` method to obtain an I2C port. ``relax`` parameter in I2cPort methods may be used to prevent the master from releasing the I2C bus, if some further data should be exchanged with the slave device. Note that in case of any error, the I2C bus is released and the ``relax`` parameter is ignored in such an event. Example: >>> ctrl = I2cController() >>> ctrl.configure('ftdi://ftdi:232h/1') >>> i2c = ctrl.get_port(0x21) >>> # send 2 bytes >>> i2c.write([0x12, 0x34]) >>> # send 2 bytes, then receive 2 bytes >>> out = i2c.exchange([0x12, 0x34], 2) """ FORMATS = {scalc(fmt): fmt for fmt in 'BHI'} def __init__(self, controller: 'I2cController', address: int): self._controller = controller self._address = address self._shift = 0 self._endian = '<' self._format = 'B' def configure_register(self, bigendian: bool = False, width: int = 1) -> None: """Reconfigure the format of the slave address register (if any) :param bigendian: True for a big endian encoding, False otherwise :param width: width, in bytes, of the register """ try: self._format = self.FORMATS[width] except KeyError as exc: raise I2cIOError('Unsupported integer width') from exc self._endian = '>' if bigendian else '<' def shift_address(self, offset: int): """Tweak the I2C slave address, as required with some devices """ I2cController.validate_address(self._address+offset) self._shift = offset def read(self, readlen: int = 0, relax: bool = True, start: bool = True) -> bytes: """Read one or more bytes from a remote slave :param readlen: count of bytes to read out. :param relax: whether to relax the bus (emit STOP) or not :param start: whether to emit a start sequence (w/ address) :return: byte sequence of read out bytes :raise I2cIOError: if device is not configured or input parameters are invalid """ return self._controller.read( self._address+self._shift if start else None, readlen=readlen, relax=relax) def write(self, out: Union[bytes, bytearray, Iterable[int]], relax: bool = True, start: bool = True) -> None: """Write one or more bytes to a remote slave :param out: the byte buffer to send :param relax: whether to relax the bus (emit STOP) or not :param start: whether to emit a start sequence (w/ address) :raise I2cIOError: if device is not configured or input parameters are invalid """ return self._controller.write( self._address+self._shift if start else None, out, relax=relax) def read_from(self, regaddr: int, readlen: int = 0, relax: bool = True, start: bool = True) -> bytes: """Read one or more bytes from a remote slave :param regaddr: slave register address to read from :param readlen: count of bytes to read out. :param relax: whether to relax the bus (emit STOP) or not :param start: whether to emit a start sequence (w/ address) :return: data read out from the slave :raise I2cIOError: if device is not configured or input parameters are invalid """ return self._controller.exchange( self._address+self._shift if start else None, out=self._make_buffer(regaddr), readlen=readlen, relax=relax) def write_to(self, regaddr: int, out: Union[bytes, bytearray, Iterable[int]], relax: bool = True, start: bool = True): """Read one or more bytes from a remote slave :param regaddr: slave register address to write to :param out: the byte buffer to send :param relax: whether to relax the bus (emit STOP) or not :param start: whether to emit a start sequence (w/ address) :raise I2cIOError: if device is not configured or input parameters are invalid """ return self._controller.write( self._address+self._shift if start else None, out=self._make_buffer(regaddr, out), relax=relax) def exchange(self, out: Union[bytes, bytearray, Iterable[int]] = b'', readlen: int = 0, relax: bool = True, start: bool = True) -> bytes: """Perform an exchange or a transaction with the I2c slave :param out: an array of bytes to send to the I2c slave, may be empty to only read out data from the slave :param readlen: count of bytes to read out from the slave, may be zero to only write to the slave :param relax: whether to relax the bus (emit STOP) or not :param start: whether to emit a start sequence (w/ address) :return: data read out from the slave """ return self._controller.exchange( self._address+self._shift if start else None, out, readlen, relax=relax) def poll(self, write: bool = False, relax: bool = True, start: bool = True) -> bool: """Poll a remote slave, expect ACK or NACK. :param write: poll in write mode (vs. read) :param relax: whether to relax the bus (emit STOP) or not :param start: whether to emit a start sequence (w/ address) :return: True if the slave acknowledged, False otherwise """ return self._controller.poll( self._address+self._shift if start else None, write, relax=relax) def poll_cond(self, width: int, mask: int, value: int, count: int, relax: bool = True, start: bool = True) -> Optional[bytes]: """Poll a remove slave, watching for condition to satisfy. On each poll cycle, a repeated start condition is emitted, without releasing the I2C bus, and an ACK is returned to the slave. If relax is set, this method releases the I2C bus however it leaves. :param width: count of bytes to poll for the condition check, that is the size of the condition register :param mask: binary mask to apply on the condition register before testing for the value :param value: value to test the masked condition register against. Condition is satisfied when register & mask == value :param count: maximum poll count before raising a timeout :param relax: whether to relax the bus (emit STOP) or not :param start: whether to emit a start sequence (w/ address) :return: the polled register value :raise I2cTimeoutError: if poll condition is not satisified """ try: fmt = ''.join((self._endian, self.FORMATS[width])) except KeyError as exc: raise I2cIOError('Unsupported integer width') from exc return self._controller.poll_cond( self._address+self._shift if start else None, fmt, mask, value, count, relax=relax) def flush(self) -> None: """Force the flush of the HW FIFOs. """ self._controller.flush() @property def frequency(self) -> float: """Provide the current I2c bus frequency. """ return self._controller.frequency @property def address(self) -> int: """Return the slave address.""" return self._address def _make_buffer(self, regaddr: int, out: Union[bytes, bytearray, Iterable[int], None] = None)\ -> bytes: data = bytearray() data.extend(spack('%s%s' % (self._endian, self._format), regaddr)) if out: data.extend(out) return bytes(data) class I2cGpioPort: """GPIO port A I2cGpioPort instance enables to drive GPIOs wich are not reserved for I2c feature as regular GPIOs. GPIO are managed as a bitfield. The LSBs are reserved for the I2c feature, which means that the lowest pin that can be used as a GPIO is *b3*: * *b0*: I2C SCL * *b1*: I2C SDA_O * *b2*: I2C SDA_I * *b3*: first GPIO * *b7*: reserved for I2C clock stretching, if this mode is enabled There is no offset bias in GPIO bit position, *i.e.* the first available GPIO can be reached from as ``0x08``. 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 I2cGpio port is never instanciated directly: use :py:meth:`I2cController.get_gpio()` method to obtain the GPIO port. """ def __init__(self, controller: 'I2cController'): self.log = getLogger('pyftdi.i2c.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 I2C 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 I2C ones, are reported. :return: the count of IO pins (including I2C 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) I2CTimings = namedtuple('I2CTimings', 't_hd_sta t_su_sta t_su_sto t_buf') """I2C standard timings. """ class I2cController: """I2c master. An I2c master should be instanciated only once for each FTDI port that supports MPSSE (one or two ports, depending on the FTDI device). Once configured, :py:func:`get_port` should be invoked to obtain an I2c port for each I2c slave to drive. I2c port should handle all I/O requests for its associated HW slave. It is not recommended to use I2cController :py:func:`read`, :py:func:`write` or :py:func:`exchange` directly. * ``SCK`` should be connected to ``A*BUS0``, and ``A*BUS7`` if clock stretching mode is enabled * ``SDA`` should be connected to ``A*BUS1`` **and** ``A*BUS2`` """ LOW = 0x00 HIGH = 0xff BIT0 = 0x01 IDLE = HIGH SCL_BIT = 0x01 #AD0 SDA_O_BIT = 0x02 #AD1 SDA_I_BIT = 0x04 #AD2 SCL_FB_BIT = 0x80 #AD7 PAYLOAD_MAX_LENGTH = 0xFF00 # 16 bits max (- spare for control) HIGHEST_I2C_ADDRESS = 0x7F DEFAULT_BUS_FREQUENCY = 100000.0 HIGH_BUS_FREQUENCY = 400000.0 RETRY_COUNT = 3 I2C_MASK = SCL_BIT | SDA_O_BIT | SDA_I_BIT I2C_MASK_CS = SCL_BIT | SDA_O_BIT | SDA_I_BIT | SCL_FB_BIT I2C_DIR = SCL_BIT | SDA_O_BIT I2C_100K = I2CTimings(4.0E-6, 4.7E-6, 4.0E-6, 4.7E-6) I2C_400K = I2CTimings(0.6E-6, 0.6E-6, 0.6E-6, 1.3E-6) I2C_1M = I2CTimings(0.26E-6, 0.26E-6, 0.26E-6, 0.5E-6) def __init__(self): self._ftdi = Ftdi() self._lock = Lock() self.log = getLogger('pyftdi.i2c') self._gpio_port = None self._gpio_dir = 0 self._gpio_low = 0 self._gpio_mask = 0 self._i2c_mask = 0 self._wide_port = False self._slaves = {} self._retry_count = self.RETRY_COUNT self._frequency = 0.0 self._immediate = (Ftdi.SEND_IMMEDIATE,) self._read_bit = (Ftdi.READ_BITS_PVE_MSB, 0) self._read_byte = (Ftdi.READ_BYTES_PVE_MSB, 0, 0) self._write_byte = (Ftdi.WRITE_BYTES_NVE_MSB, 0, 0) self._nack = (Ftdi.WRITE_BITS_NVE_MSB, 0, self.HIGH) self._ack = (Ftdi.WRITE_BITS_NVE_MSB, 0, self.LOW) self._ck_delay = 1 self._fake_tristate = False self._tx_size = 1 self._rx_size = 1 self._ck_hd_sta = 0 self._ck_su_sto = 0 self._ck_idle = 0 self._read_optim = True self._disable_3phase_clock = False def set_retry_count(self, count: int) -> None: """Change the default retry count when a communication error occurs, before bailing out. :param count: count of retries """ if not isinstance(count, int) or not 0 < count <= 16: raise ValueError('Invalid retry count') self._retry_count = count def configure(self, url: Union[str, UsbDevice], **kwargs: Mapping[str, Any]) -> None: """Configure the FTDI interface as a I2c master. :param url: FTDI URL string, such as ``ftdi://ftdi:232h/1`` :param kwargs: options to configure the I2C 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:`I2cGpioPort` to drive GPIOs. Note that pins reserved for I2C 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:`I2cGpioPort` to drive GPIOs. * ``frequency`` float value the I2C bus frequency in Hz * ``clockstretching`` boolean value to enable clockstreching. xD7 (GPIO7) pin should be connected back to xD0 (SCK) * ``debug`` to increase log verbosity, using MPSSE tracer """ if 'frequency' in kwargs: frequency = kwargs['frequency'] del kwargs['frequency'] else: frequency = self.DEFAULT_BUS_FREQUENCY # Fix frequency for 3-phase clock if frequency <= 100E3: timings = self.I2C_100K elif frequency <= 400E3: timings = self.I2C_400K else: timings = self.I2C_1M if 'clockstretching' in kwargs: clkstrch = bool(kwargs['clockstretching']) del kwargs['clockstretching'] else: clkstrch = False 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 I2cIOError('url and interface are mutually exclusive') interface = int(kwargs['interface']) del kwargs['interface'] else: interface = 1 if 'rdoptim' in kwargs: self._read_optim = to_bool(kwargs['rdoptim']) del kwargs['rdoptim'] with self._lock: self._ck_hd_sta = self._compute_delay_cycles(timings.t_hd_sta) self._ck_su_sto = self._compute_delay_cycles(timings.t_su_sto) ck_su_sta = self._compute_delay_cycles(timings.t_su_sta) ck_buf = self._compute_delay_cycles(timings.t_buf) self._ck_idle = max(ck_su_sta, ck_buf) self._ck_delay = ck_buf if clkstrch: self._i2c_mask = self.I2C_MASK_CS else: self._i2c_mask = self.I2C_MASK # 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, io_out, io_dir) # as 3-phase clock frequency mode is required for I2C mode, the # FTDI clock should be adapted to match the required frequency. kwargs['direction'] = self.I2C_DIR | self._gpio_dir kwargs['initial'] = self.IDLE | (io_out & self._gpio_mask) kwargs['frequency'] = (3.0*frequency)/2.0 if not isinstance(url, str): frequency = self._ftdi.open_mpsse_from_device( url, interface=interface, **kwargs) else: frequency = self._ftdi.open_mpsse_from_url(url, **kwargs) self._frequency = (2.0*frequency)/3.0 self._tx_size, self._rx_size = self._ftdi.fifo_sizes self._ftdi.enable_adaptive_clock(clkstrch) if not self._disable_3phase_clock: self._ftdi.enable_3phase_clock(True) try: self._ftdi.enable_drivezero_mode(self.SCL_BIT | self.SDA_O_BIT | self.SDA_I_BIT) except FtdiFeatureError: # when open collector feature is not available (FT2232, FT4232) # SDA line is temporary move to high-z to enable ACK/NACK # read back from slave self._fake_tristate = True 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 force_clock_mode(self, enable: bool) -> None: """Force unsupported I2C clock signalling on devices that have no I2C capabilities (i.e. FT2232D). I2cController cowardly refuses to use unsupported devices. When this mode is enabled, I2cController can drive such devices, but I2C signalling is not compliant with I2C specifications and may not work with most I2C slaves. :py:meth:`force_clock_mode` should always be called before :py:meth:`configure` to be effective. This is a fully unsupported feature (bug reports will be ignored). :param enable: whether to drive non-I2C capable devices. """ if enable: self.log.info('I2C signalling forced to non-I2C compliant mode.') self._disable_3phase_clock = enable 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) def terminate(self) -> None: """Close the FTDI interface. :note: deprecated API, use close() """ self.close() def get_port(self, address: int) -> I2cPort: """Obtain an I2cPort to drive an I2c slave. :param address: the address on the I2C bus :return: an I2cPort instance """ if not self._ftdi.is_connected: raise I2cIOError("FTDI controller not initialized") self.validate_address(address) if address not in self._slaves: self._slaves[address] = I2cPort(self, address) return self._slaves[address] def get_gpio(self) -> I2cGpioPort: """Retrieve the GPIO port. :return: GPIO port """ with self._lock: if not self._ftdi.is_connected: raise I2cIOError("FTDI controller not initialized") if not self._gpio_port: self._gpio_port = I2cGpioPort(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 and bool(self._start) @classmethod def validate_address(cls, address: Optional[int]) -> None: """Assert an I2C slave address is in the supported range. None is a special bypass address. :param address: the address on the I2C bus :raise I2cIOError: if the I2C slave address is not supported """ if address is None: return if address > cls.HIGHEST_I2C_ADDRESS: raise I2cIOError("No such I2c slave: 0x%02x" % address) @property def frequency_max(self) -> float: """Provides the maximum I2C clock frequency in Hz. :return: I2C bus clock frequency """ return self._ftdi.frequency_max @property def frequency(self) -> float: """Provides the current I2C clock frequency in Hz. :return: the I2C bus clock frequency """ return self._frequency @property def direction(self) -> int: """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.I2C_DIR | self._gpio_dir @property def gpio_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. """ with self._lock: return self._gpio_mask @property def gpio_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 I2C support) :return: the bitfield of configurable GPIO pins. """ mask = (1 << self.width) - 1 with self._lock: return mask & ~self._i2c_mask @property def width(self) -> int: """Report the FTDI count of addressable pins. :return: the count of IO pins (including I2C ones). """ return 16 if self._wide_port else 8 def read(self, address: int, readlen: int = 1, relax: bool = True) -> bytes: """Read one or more bytes from a remote slave :param address: the address on the I2C bus, or None to discard start :param readlen: count of bytes to read out. :param relax: not used :return: read bytes :raise I2cIOError: if device is not configured or input parameters are invalid Address is a logical slave address (0x7f max) Most I2C devices require a register address to read out check out the exchange() method. """ if not self.configured: raise I2cIOError("FTDI controller not initialized") self.validate_address(address) if address is None: i2caddress = None else: i2caddress = (address << 1) & self.HIGH i2caddress |= self.BIT0 retries = self._retry_count do_epilog = True with self._lock: while True: try: self._do_prolog(i2caddress) data = self._do_read(readlen) do_epilog = relax return data except I2cNackError: retries -= 1 if not retries: raise self.log.warning('Retry read') finally: if do_epilog: self._do_epilog() def write(self, address: int, out: Union[bytes, bytearray, Iterable[int]], relax: bool = True) -> None: """Write one or more bytes to a remote slave :param address: the address on the I2C bus, or None to discard start :param out: the byte buffer to send :param relax: whether to relax the bus (emit STOP) or not :raise I2cIOError: if device is not configured or input parameters are invalid Address is a logical slave address (0x7f max) Most I2C devices require a register address to write into. It should be added as the first (byte)s of the output buffer. """ if not self.configured: raise I2cIOError("FTDI controller not initialized") self.validate_address(address) if address is None: i2caddress = None else: i2caddress = (address << 1) & self.HIGH retries = self._retry_count do_epilog = True with self._lock: while True: try: self._do_prolog(i2caddress) self._do_write(out) do_epilog = relax return except I2cNackError: retries -= 1 if not retries: raise self.log.warning('Retry write') finally: if do_epilog: self._do_epilog() def exchange(self, address: int, out: Union[bytes, bytearray, Iterable[int]], readlen: int = 0, relax: bool = True) -> bytes: """Send a byte sequence to a remote slave followed with a read request of one or more bytes. This command is useful to tell the slave what data should be read out. :param address: the address on the I2C bus, or None to discard start :param out: the byte buffer to send :param readlen: count of bytes to read out. :param relax: whether to relax the bus (emit STOP) or not :return: read bytes :raise I2cIOError: if device is not configured or input parameters are invalid Address is a logical slave address (0x7f max) """ if not self.configured: raise I2cIOError("FTDI controller not initialized") self.validate_address(address) if readlen < 1: raise I2cIOError('Nothing to read') if readlen > (self.PAYLOAD_MAX_LENGTH/3-1): raise I2cIOError("Input payload is too large") if address is None: i2caddress = None else: i2caddress = (address << 1) & self.HIGH retries = self._retry_count do_epilog = True with self._lock: while True: try: self._do_prolog(i2caddress) self._do_write(out) self._do_prolog(i2caddress | self.BIT0) if readlen: data = self._do_read(readlen) do_epilog = relax return data except I2cNackError: retries -= 1 if not retries: raise self.log.warning('Retry exchange') finally: if do_epilog: self._do_epilog() def poll(self, address: int, write: bool = False, relax: bool = True) -> bool: """Poll a remote slave, expect ACK or NACK. :param address: the address on the I2C bus, or None to discard start :param write: poll in write mode (vs. read) :param relax: whether to relax the bus (emit STOP) or not :return: True if the slave acknowledged, False otherwise """ if not self.configured: raise I2cIOError("FTDI controller not initialized") self.validate_address(address) if address is None: i2caddress = None else: i2caddress = (address << 1) & self.HIGH if not write: i2caddress |= self.BIT0 do_epilog = True with self._lock: try: self._do_prolog(i2caddress) do_epilog = relax return True except I2cNackError: self.log.info('Not ready') return False finally: if do_epilog: self._do_epilog() def poll_cond(self, address: int, fmt: str, mask: int, value: int, count: int, relax: bool = True) -> Optional[bytes]: """Poll a remove slave, watching for condition to satisfy. On each poll cycle, a repeated start condition is emitted, without releasing the I2C bus, and an ACK is returned to the slave. If relax is set, this method releases the I2C bus however it leaves. :param address: the address on the I2C bus, or None to discard start :param fmt: struct format for poll register :param mask: binary mask to apply on the condition register before testing for the value :param value: value to test the masked condition register against. Condition is satisfied when register & mask == value :param count: maximum poll count before raising a timeout :param relax: whether to relax the bus (emit STOP) or not :return: the polled register value, or None if poll failed """ if not self.configured: raise I2cIOError("FTDI controller not initialized") self.validate_address(address) if address is None: i2caddress = None else: i2caddress = (address << 1) & self.HIGH i2caddress |= self.BIT0 do_epilog = True with self._lock: try: retry = 0 while retry < count: retry += 1 size = scalc(fmt) self._do_prolog(i2caddress) data = self._do_read(size) self.log.debug("Poll data: %s", hexlify(data).decode()) cond, = sunpack(fmt, data) if (cond & mask) == value: self.log.debug('Poll condition matched') break data = None self.log.debug('Poll condition not fulfilled: %x/%x', cond & mask, value) do_epilog = relax if not data: self.log.warning('Poll condition failed') return data except I2cNackError: self.log.info('Not ready') return None finally: if do_epilog: self._do_epilog() def flush(self) -> None: """Flush the HW FIFOs. """ if not self.configured: raise I2cIOError("FTDI controller not initialized") with self._lock: self._ftdi.write_data(self._immediate) self._ftdi.purge_buffers() 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 I2cIOError('No such GPO pins: %04x/%04x' % (self._gpio_dir, value)) # 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._i2c_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._i2c_mask: raise I2cIOError('Cannot access I2C pins as GPIO') gpio_mask = (1 << width) - 1 gpio_mask &= ~self._i2c_mask if (pins & gpio_mask) != pins: raise I2cIOError('No such GPIO pin(s)') self._gpio_dir &= ~pins self._gpio_dir |= (pins & direction) self._gpio_mask = gpio_mask & pins @property def _data_lo(self) -> Tuple[int]: return (Ftdi.SET_BITS_LOW, self.SCL_BIT | self._gpio_low, self.I2C_DIR | (self._gpio_dir & 0xFF)) @property def _clk_lo_data_hi(self) -> Tuple[int]: return (Ftdi.SET_BITS_LOW, self.SDA_O_BIT | self._gpio_low, self.I2C_DIR | (self._gpio_dir & 0xFF)) @property def _clk_lo_data_input(self) -> Tuple[int]: return (Ftdi.SET_BITS_LOW, self.LOW | self._gpio_low, self.SCL_BIT | (self._gpio_dir & 0xFF)) @property def _clk_lo_data_lo(self) -> Tuple[int]: return (Ftdi.SET_BITS_LOW, self._gpio_low, self.I2C_DIR | (self._gpio_dir & 0xFF)) @property def _idle(self) -> Tuple[int]: return (Ftdi.SET_BITS_LOW, self.I2C_DIR | self._gpio_low, self.I2C_DIR | (self._gpio_dir & 0xFF)) @property def _start(self) -> Tuple[int]: return self._data_lo * self._ck_hd_sta + \ self._clk_lo_data_lo * self._ck_hd_sta @property def _stop(self) -> Tuple[int]: return self._clk_lo_data_hi * self._ck_hd_sta + \ self._clk_lo_data_lo * self._ck_hd_sta + \ self._data_lo * self._ck_su_sto + \ self._idle * self._ck_idle def _compute_delay_cycles(self, value: Union[int, float]) -> int: # approx ceiling without relying on math module # the bit delay is far from being precisely known anyway bit_delay = self._ftdi.mpsse_bit_delay return max(1, int((value + bit_delay) / bit_delay)) def _read_raw(self, read_high: bool) -> int: if read_high: cmd = bytes([Ftdi.GET_BITS_LOW, Ftdi.GET_BITS_HIGH, Ftdi.SEND_IMMEDIATE]) fmt = '> 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 _do_prolog(self, i2caddress: int) -> None: if i2caddress is None: return self.log.debug(' prolog 0x%x', i2caddress >> 1) cmd = bytearray(self._idle * self._ck_delay) cmd.extend(self._start) cmd.extend(self._write_byte) cmd.append(i2caddress) try: self._send_check_ack(cmd) except I2cNackError: self.log.warning('NACK @ 0x%02x', (i2caddress>>1)) raise def _do_epilog(self) -> None: self.log.debug(' epilog') cmd = bytearray(self._stop) self._ftdi.write_data(cmd) # be sure to purge the MPSSE reply self._ftdi.read_data_bytes(1, 1) def _send_check_ack(self, cmd: bytearray): # note: cmd is modified if self._fake_tristate: # SCL low, SDA high-Z (input) cmd.extend(self._clk_lo_data_input) # read SDA (ack from slave) cmd.extend(self._read_bit) # leave SCL low, restore SDA as output cmd.extend(self._clk_lo_data_hi) else: # SCL low, SDA high-Z cmd.extend(self._clk_lo_data_hi) # read SDA (ack from slave) cmd.extend(self._read_bit) cmd.extend(self._immediate) self._ftdi.write_data(cmd) ack = self._ftdi.read_data_bytes(1, 4) if not ack: raise I2cIOError('No answer from FTDI') if ack[0] & self.BIT0: raise I2cNackError('NACK from slave') def _do_read(self, readlen: int) -> bytes: self.log.debug('- read %d byte(s)', readlen) if not readlen: # force a real read request on device, but discard any result cmd = bytearray() cmd.extend(self._immediate) self._ftdi.write_data(cmd) self._ftdi.read_data_bytes(0, 4) return bytearray() if self._fake_tristate: read_byte = (self._clk_lo_data_input + self._read_byte + self._clk_lo_data_hi) read_not_last = (read_byte + self._ack + self._clk_lo_data_lo * self._ck_delay) read_last = (read_byte + self._nack + self._clk_lo_data_hi * self._ck_delay) else: read_not_last = (self._read_byte + self._ack + self._clk_lo_data_hi * self._ck_delay) read_last = (self._read_byte + self._nack + self._clk_lo_data_hi * self._ck_delay) # maximum RX size to fit in FTDI FIFO, minus 2 status bytes chunk_size = self._rx_size-2 cmd_size = len(read_last) # limit RX chunk size to the count of I2C packable commands in the FTDI # TX FIFO (minus one byte for the last 'send immediate' command) tx_count = (self._tx_size-1) // cmd_size chunk_size = min(tx_count, chunk_size) chunks = [] cmd = None rem = readlen if self._read_optim and rem > chunk_size: chunk_size //= 2 self.log.debug('Use optimized transfer, %d byte at a time', chunk_size) cmd_chunk = bytearray() cmd_chunk.extend(read_not_last * chunk_size) cmd_chunk.extend(self._immediate) def write_command_gen(length: int): if length <= 0: # no more data return b'' if length <= chunk_size: cmd = bytearray() cmd.extend(read_not_last * (length-1)) cmd.extend(read_last) cmd.extend(self._immediate) return cmd return cmd_chunk while rem: buf = self._ftdi.read_data_bytes(rem, 4, write_command_gen) self.log.debug('- read %d bytes, rem: %d', len(buf), rem) chunks.append(buf) rem -= len(buf) else: while rem: if rem > chunk_size: if not cmd: # build the command sequence only once, as it may be # repeated till the end of the transfer cmd = bytearray() cmd.extend(read_not_last * chunk_size) size = chunk_size else: cmd = bytearray() cmd.extend(read_not_last * (rem-1)) cmd.extend(read_last) cmd.extend(self._immediate) size = rem self._ftdi.write_data(cmd) buf = self._ftdi.read_data_bytes(size, 4) self.log.debug('- read %d byte(s): %s', len(buf), hexlify(buf).decode()) chunks.append(buf) rem -= size return bytearray(b''.join(chunks)) def _do_write(self, out: Union[bytes, bytearray, Iterable[int]]): if not isinstance(out, bytearray): out = bytearray(out) if not out: return self.log.debug('- write %d byte(s): %s', len(out), hexlify(out).decode()) for byte in out: cmd = bytearray(self._write_byte) cmd.append(byte) self._send_check_ack(cmd) pyftdi-0.54.0/pyftdi/jtag.py000066400000000000000000000570361421346242200157100ustar00rootroot00000000000000# Copyright (c) 2010-2020, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """JTAG support for PyFdti""" #pylint: disable-msg=invalid-name #pylint: disable-msg=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(l), l) for l 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) 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("Invalid state: %s" % 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("Invalid state: %s" % 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("Invalid state: %s" % 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("Testing for length %d" % length) if length > 5: raise ValueError('Abort detection over reg length %d' % 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("Register detected length: %d" % length) return length if stuck is not None: raise JtagError('TDO seems to be stuck') raise JtagError('Unable to detect register length') pyftdi-0.54.0/pyftdi/misc.py000066400000000000000000000275651421346242200157220ustar00rootroot00000000000000# Copyright (c) 2010-2021 Emmanuel Blot # Copyright (c) 2008-2016, Neotion # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """Miscellaneous helpers""" #pylint: disable-msg=invalid-name #pylint: disable-msg=import-outside-toplevel #pylint: disable-msg=too-many-locals #pylint: disable-msg=too-many-arguments 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("Unsupported data type '%s'" % 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(["%02x" % x 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("%08x %-*s %-*s |%s|\n" % (i, hl*3, hx1, hl*3, hx2, printable)) else: result.append("%06x %-*s %s\n" % (i, length*3, hexa, printable)) 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("Unsupported data type '%s'" % type(data)) from exc hexa = sep.join(["%02x" % x for x in src]) printable = src.translate(ASCIIFILTER).decode('ascii') return "(%d) %s : %s" % (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("Invalid boolean value: '%d'" % value) if value.lower() in TRUE_BOOLEANS: return True if permissive or (value.lower() in FALSE_BOOLEANS): return False raise ValueError('"Invalid boolean value: "%s"' % 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[-+]?[0-9]*\.?[0-9]+(?:[Ee][-+]?[0-9]+)?)' r'(?P[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-msg=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 '%d%sMiB' % (ssize, sep) if size > lim_k: ssize = size >> 10 if floor or (ssize << 10) == size: return '%d%sKiB' % (ssize, sep) return '%d%sbyte%s' % (size, sep, (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=]:[product_name=] * 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("'%s' object has no attribute '%s'" % (self.__class__.__name__, 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()}) pyftdi-0.54.0/pyftdi/serialext/000077500000000000000000000000001421346242200163765ustar00rootroot00000000000000pyftdi-0.54.0/pyftdi/serialext/__init__.py000066400000000000000000000020011421346242200205000ustar00rootroot00000000000000# Copyright (c) 2010-2016 Emmanuel Blot # 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 """ pyftdi-0.54.0/pyftdi/serialext/logger.py000066400000000000000000000121361421346242200202320ustar00rootroot00000000000000# Copyright (C) 2010-2016 Emmanuel Blot # Copyright (c) 2008-2016, Neotion # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause #pylint: disable-msg=no-member #pylint: disable-msg=broad-except #pylint: disable-msg=invalid-name #pylint: disable-msg=super-with-arguments #pylint: disable-msg=missing-function-docstring #pylint: disable-msg=missing-module-docstring 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: self._logger = open(logpath, "wt") except IOError as e: print("Cannot log data to %s: %s" % (logpath, str(e)), 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', 'for %.3f' % duration) 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("%s (%3.3f ms):\n%s" % (header, delta, string or ''), file=self._logger) self._logger.flush() def _log_init(self, *args, **kwargs): try: self._print( 'NEW', ' args: %s %s' % (', '.join(args), ', '.join({'%s=%s' % it for it in kwargs.items()}))) except Exception as e: print('Cannot log init (%s)' % e, file=stderr) def _log_open(self): try: self._print('OPEN') except Exception as e: print('Cannot log open (%s)' % e, file=stderr) def _log_close(self): try: self._print('CLOSE') except Exception as e: print('Cannot log close (%s)' % e, file=stderr) def _log_read(self, data): try: self._print('READ', hexdump(data)) except Exception as e: print('Cannot log input data (%s)' % e, file=stderr) def _log_write(self, data): try: self._print('WRITE', hexdump(data)) except Exception as e: print('Cannot log output data (%s)' % e, data, file=stderr) def _log_flush(self): try: self._print('FLUSH') except Exception as e: print('Cannot log flush action (%s)' % e, file=stderr) def _log_reset(self, type_): try: self._print('RESET BUFFER', type_) except Exception as e: print('Cannot log reset buffer (%s)' % e, file=stderr) def _log_waiting(self, count): try: self._print('INWAITING', '%d' % count) except Exception as e: print('Cannot log inwaiting (%s)' % e, file=stderr) def _log_signal(self, name, value): try: self._print(name.upper(), '%s' % value) except Exception as e: print('Cannot log %s (%s)' % (name, e), file=stderr) pyftdi-0.54.0/pyftdi/serialext/protocol_ftdi.py000066400000000000000000000141251421346242200216220ustar00rootroot00000000000000# Copyright (c) 2008-2020, Emmanuel Blot # 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-msg=attribute-defined-outside-init #pylint: disable-msg=invalid-name #pylint: disable-msg=missing-class-docstring #pylint: disable-msg=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 ex: raise SerialException('Unable to open USB port %s: %s' % (self.portstr, str(ex))) from ex 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("%s (%s)" % (str(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) pyftdi-0.54.0/pyftdi/serialext/protocol_unix.py000066400000000000000000000154761421346242200216710ustar00rootroot00000000000000# Copyright (c) 2008-2016, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # 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-msg=broad-except #pylint: disable-msg=attribute-defined-outside-init #pylint: disable-msg=redefined-outer-name #pylint: disable-msg=invalid-name #pylint: disable-msg=too-few-public-methods #pylint: disable-msg=missing-function-docstring #pylint: disable-msg=missing-class-docstring #pylint: disable-msg=missing-module-docstring import errno import os import select import socket from io import RawIOBase from serial import (SerialBase, SerialException, portNotOpenError, writeTimeoutError, 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 e: self.close() msg = "Could not open port: %s" % (str(e),) if isinstance(e, socket.error): # pylint: disable-msg=no-member raise SerialExceptionWithErrno(msg, e.errno) from e raise SerialException(msg) from e 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.""" #pylint: disable-msg=no-self-use 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 writeTimeoutError 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 writeTimeoutError 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, on=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 pyftdi-0.54.0/pyftdi/serialext/tests/000077500000000000000000000000001421346242200175405ustar00rootroot00000000000000pyftdi-0.54.0/pyftdi/serialext/tests/rl.py000066400000000000000000000010031421346242200205210ustar00rootroot00000000000000#!/usr/bin/env python3 """Check readline feature.""" from os.path import dirname from sys import path from serial import serial_for_url path.append(dirname(dirname(dirname(dirname(__file__))))) #pylint: disable-msg=wrong-import-position from pyftdi import serialext def main(): """Verify readline() works with PyFTDI extension.""" serialext.touch() with serial_for_url('ftdi:///1', baudrate=115200) as ser: line = ser.readline() print(line) if __name__ == '__main__': main() pyftdi-0.54.0/pyftdi/spi.py000066400000000000000000001127661421346242200155600ustar00rootroot00000000000000# Copyright (c) 2010-2020, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """SPI support for PyFdti""" #pylint: disable-msg=too-many-arguments #pylint: disable-msg=too-many-locals #pylint: disable-msg=too-many-branches #pylint: disable-msg=too-many-statements #pylint: disable-msg=too-many-instance-attributes #pylint: disable-msg=too-many-public-methods #pylint: disable-msg=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('Invalid SPI mode: %d' % 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('Unsupported CS line count: %d' % 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 %d not reserved for SPI" % cs) raise SpiIOError("No such SPI port: %d" % 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('No such GPO pins: %04x/%04x' % (self._gpio_dir, value)) # 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 = ' 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(' 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(' None: self._ftdi.write_data(self._immediate) self._ftdi.purge_buffers() pyftdi-0.54.0/pyftdi/term.py000077500000000000000000000146021421346242200157250ustar00rootroot00000000000000"""Terminal management helpers""" # Copyright (c) 2020-2021, Emmanuel Blot # Copyright (c) 2020, Michael Pratt # 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-msg=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) 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', 134: 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 pyftdi-0.54.0/pyftdi/tests/000077500000000000000000000000001421346242200155405ustar00rootroot00000000000000pyftdi-0.54.0/pyftdi/tests/backend/000077500000000000000000000000001421346242200171275ustar00rootroot00000000000000pyftdi-0.54.0/pyftdi/tests/backend/__init__.py000066400000000000000000000001751421346242200212430ustar00rootroot00000000000000# Copyright (c) 2020, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause pyftdi-0.54.0/pyftdi/tests/backend/consts.py000066400000000000000000000136241421346242200210200ustar00rootroot00000000000000"""Constant importer from existing modules.""" # Copyright (c) 2020-2021, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause #pylint: disable-msg=missing-docstring #pylint: disable-msg=invalid-name #pylint: disable-msg=too-many-instance-attributes from enum import Enum from importlib import import_module from sys import version_info from pyftdi.ftdi import Ftdi from pyftdi.misc import EasyDict class UsbConstants: """Expose useful constants defined in PyUSB and allow reverse search, i.e. retrieve constant literals from integral values. """ DEVICE_REQUESTS = { (True, 0x0): 'get_status', (False, 0x1): 'clear_feature', (False, 0x3): 'set_feature', (False, 0x5): 'set_address', (True, 0x6): 'get_descriptor', (False, 0x7): 'set_descriptor', (True, 0x8): 'get_configuration', (False, 0x9): 'set_configuration', } def __init__(self): self._desc_type = self._load_constants('desc_type') self._desc_type_mask = self._mask(self._desc_type) self._ctrl_dir = self._load_constants('ctrl') self._ctrl_dir_mask = self._mask(self._ctrl_dir) self._ctrl_type = self._load_constants('ctrl_type') self._ctrl_type_mask = self._mask(self._ctrl_type) self._ctrl_recipient = self._load_constants('ctrl_recipient') self._ctrl_recipient_mask = self._mask(self._ctrl_recipient) self._endpoint_type = self._load_constants('endpoint_type') self._endpoint_type_mask = self._mask(self._endpoint_type) self._descriptors = EasyDict({v.upper(): k for k, v in self._desc_type.items()}) self.endpoints = self._load_constants('endpoint', True) self.endpoint_types = self._load_constants('endpoint_type', True) self.speeds = self._load_constants('speed', True) @property def descriptors(self): return self._descriptors @classmethod def _load_constants(cls, prefix: str, reverse=False): prefix = prefix.upper() if not prefix.endswith('_'): prefix = f'{prefix}_' mod = import_module('usb.util') mapping = EasyDict() plen = len(prefix) for entry in dir(mod): if not entry.startswith(prefix): continue if '_' in entry[plen:]: continue if not reverse: mapping[getattr(mod, entry)] = entry[plen:].lower() else: mapping[entry[plen:].lower()] = getattr(mod, entry) if not mapping: raise ValueError(f"No USB constant found for {prefix.rstrip('_')}") return mapping @classmethod def _mask(cls, mapping: dict) -> int: mask = 0 for val in mapping: mask |= val return mask def is_req_out(self, reqtype: int) -> str: return not reqtype & self._ctrl_dir_mask def dec_req_ctrl(self, reqtype: int) -> str: return self._ctrl_dir[reqtype & self._ctrl_dir_mask] def dec_req_type(self, reqtype: int) -> str: return self._ctrl_type[reqtype & self._ctrl_type_mask] def dec_req_rcpt(self, reqtype: int) -> str: return self._ctrl_recipient[reqtype & self._ctrl_recipient_mask] def dec_req_name(self, reqtype: int, request: int) -> str: direction = bool(reqtype & self._ctrl_dir_mask) try: return self.DEVICE_REQUESTS[(direction, request)] except KeyError: return f'req x{request:02x}' def dec_desc_type(self, desctype: int) -> str: return self._desc_type[desctype & self._desc_type_mask] class FtdiConstants: """Expose useful constants defined in Ftdi and allow reverse search, i.e. retrieve constant literals from integral values. """ def __init__(self): self._dcache = {} self._rcache = {} @classmethod def _load_constants(cls, prefix: str, reverse=False): prefix = prefix.upper() if not prefix.endswith('_'): prefix = f'{prefix}_' mapping = EasyDict() plen = len(prefix) for name in dir(Ftdi): if not name.startswith(prefix): continue if not reverse: mapping[getattr(Ftdi, name)] = name[plen:].lower() else: mapping[name[plen:].lower()] = getattr(Ftdi, name) if not mapping: # maybe an enum prefix = prefix.rstrip('_').lower() for name in dir(Ftdi): if not name.lower().startswith(prefix): continue item = getattr(Ftdi, name) if issubclass(item, Enum): if not reverse: mapping = {en.value: en.name.lower() for en in item} else: mapping = {en.name.lower(): en.value for en in item} if not mapping: raise ValueError(f"No FTDI constant found for " f"{prefix.rstrip('_')}") return mapping def get_name(self, prefix: str, value: int) -> str: if prefix not in self._dcache: self._dcache[prefix] = self._load_constants(prefix) try: return self._dcache[prefix][value] except KeyError: return f'x?{value:04x}' def get_value(self, prefix: str, name: str) -> str: if prefix not in self._rcache: self._rcache[prefix] = self._load_constants(prefix, True) try: return self._rcache[prefix][name.lower()] except KeyError as exc: raise ValueError(f'Unknown name {prefix}.{name}') from exc def dec_req_name(self, request: int) -> str: return self.get_name('sio_req', request) USBCONST = UsbConstants() """Unique instance of USB constant container.""" FTDICONST = FtdiConstants() """Unique instances of FTDI constant container.""" pyftdi-0.54.0/pyftdi/tests/backend/ftdivirt.py000066400000000000000000001345021421346242200213410ustar00rootroot00000000000000"""PyUSB virtual FTDI device.""" # Copyright (c) 2020-2021, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause #pylint: disable-msg=missing-docstring #pylint: disable-msg=unused-argument #pylint: disable-msg=invalid-name #pylint: disable-msg=too-many-arguments #pylint: disable-msg=too-many-locals #pylint: disable-msg=too-many-branches #pylint: disable-msg=too-many-statements #pylint: disable-msg=too-many-instance-attributes #pylint: disable-msg=too-many-public-methods #pylint: disable-msg=too-few-public-methods #pylint: disable-msg=no-self-use import os from array import array from binascii import hexlify from collections import deque from enum import IntEnum, unique from logging import getLogger from struct import calcsize as scalc, pack as spack, unpack as sunpack from sys import version_info from threading import Event, Lock, Thread from time import sleep, time as now from typing import List, Mapping, NamedTuple, Optional, Sequence, Tuple from pyftdi.eeprom import FtdiEeprom # only for consts, do not use code from .consts import FTDICONST, USBCONST from .mpsse import VirtMpsseEngine, VirtMpsseTracer class Pipe: """Wrapper around os.pipe """ PIPE = {'r': 0, 'w': 1} def __init__(self): self._pipe = None self._zombie = False def close(self): if self._pipe is None: return os.close(self._pipe[0]) os.close(self._pipe[1]) self._pipe = None self._zombie = True def read(self, count: int) -> bytes: return os.read(self.r, count) def write(self, buf: bytes) -> None: os.write(self.w, buf) @property def ep_out(self) -> 'Pipe': # if pin is output, return RX endpoint (so client can read from) return self.r @property def ep_in(self) -> 'Pipe': # if pin in input, return TX endpoint (so client can write to) return self.w def __getattr__(self, name): if self._zombie: raise IOError('Closed pipe') try: pos = self.PIPE[name[0]] except KeyError as exc: raise AttributeError(f'No such pipe attribute: {name}') from exc if not self._pipe: # lazy instanciation self._pipe = os.pipe() return self._pipe[pos] class Fifo: """Communication queue.""" def __init__(self, size: int = 0): self.q = deque() self.lock = Lock() self.event = Event() self.size: int = size class VirtualFtdiPin: """Virtual FTDI pin to enable interconnexion/communication """ class Function(IntEnum): TRISTATE = 0 GPIO = 1 CLOCK = 2 STREAM = 3 def __init__(self, port, position): self.log = getLogger(f'pyftdi.vftdi[{port.iface}].{position}') self._port = port self._position = position self._next_pin = None self._function = self.Function.TRISTATE self._pipe = None def connect_to(self, pin: 'VirtualFtdiPin') -> None: if self == pin and self._position == pin.position: raise ValueError(f'Cannot connect pin {self._port.iface}:' f'{self._position} to itself') self._next_pin = pin def disconnect(self, pin: 'VirtualFtdiPin') -> None: self._next_pin = None def set_function(self, function: Optional['VirtualFtdiPin.Function'] = None) \ -> None: self._function = function or self.Function.TRISTATE if self._function != self.Function.STREAM: if self._pipe: self._pipe.close() self._pipe = None def set(self, source: bool, signal: bool) -> None: self.log.debug('set %d <- %s', int(signal), 'ext' if source else 'int') if self._function == self.Function.TRISTATE: return if self._function == self.Function.GPIO: if self.direction: if self._next_pin: # when propagate to another pin, always an external source self._next_pin.set(True, signal) else: self._port.set_io(self, signal) return self.log.error('GPIO update on a featured pin: %s', self._function) def read(self, count: int) -> bytes: # from external if self._function != self.Function.STREAM: raise IOError('Pin is not of stream kind') if self._pipe is None: self._pipe = Pipe() return self._pipe.read(count) def push_to_pin(self, buf: bytes) -> None: # from internal if self._function != self.Function.STREAM: raise IOError('Pin is not of stream kind') if self.is_connected: self._next_pin.write(buf) if self._pipe is None: self._pipe = Pipe() self._pipe.write(buf) def write(self, buf: bytes) -> None: # from external if self._function != self.Function.STREAM: raise IOError('Pin is not of stream kind') if self.is_connected: raise IOError(f'Cannot write to connected pin ({self._position})') self._port.write_from_pin(self._position, buf) @property def port(self) -> 'VirtFtdiPort': return self._port @property def connection(self) -> Optional['VirtualFtdiPin']: return self._next_pin @property def is_connected(self) -> bool: return bool(self._next_pin) @property def position(self) -> int: return self._position @property def direction(self) -> bool: return bool(self._port.direction & (1 << self._position)) @property def signal(self) -> Optional[bool]: if self._function == self.Function.TRISTATE: return None if self._function == self.Function.GPIO: return bool(self._port.gpio & (1 << self._position)) raise IOError('No signal available from this pin') class VirtFtdiPort: """Virtual FTDI port/interface :param iface: the interface number (start from 1) """ POLL_DELAY = 1e-3 """Delay between processing (generating) a chunk of data.""" SLEEP_DELAY = 200e-3 """Timeout when the worker is idle.""" class Fifos(NamedTuple): rx: Fifo # Host-to-FTDI tx: Fifo # FTDI-to-host @unique class BitMode(IntEnum): """Function mode selection. It is used so many times hereafter than relying on FTDICONST is not very handy, so redefine it """ RESET = 0x00 # switch off altnerative mode (default to UART) BITBANG = 0x01 # classical asynchronous bitbang mode MPSSE = 0x02 # MPSSE mode, available on 2232x chips SYNCBB = 0x04 # synchronous bitbang mode MCU = 0x08 # MCU Host Bus Emulation mode, OPTO = 0x10 # Fast Opto-Isolated Serial Interface Mode CBUS = 0x20 # Bitbang on CBUS pins of R-type chips SYNCFF = 0x40 # Single Channel Synchronous FIFO mode FIFO_SIZES = { 0x0200: (128, 128), # FT232AM: TX: 128, RX: 128 0x0400: (128, 384), # FT232BM: TX: 128, RX: 384 0x0500: (128, 384), # FT2232C: TX: 128, RX: 384 0x0600: (256, 128), # FT232R: TX: 256, RX: 128 0x0700: (4096, 4096), # FT2232H: TX: 4KiB, RX: 4KiB 0x0800: (2048, 2048), # FT4232H: TX: 2KiB, RX: 2KiB 0x0900: (1024, 1024), # FT232H: TX: 1KiB, RX: 1KiB 0x1000: (512, 512), # FT-X: TX: 512, RX: 512 } """FTDI chip internal FIFO sizes. TX: to-host, RX: from-host """ WIDTHS = { 0x0200: 8, 0x0400: 8, 0x0500: 12, 0x0600: 8, 0x0700: 16, 0x0800: 8, 0x0900: 16, 0x1000: 8} """Interterface pin count.""" UART_PINS = IntEnum('UartPins', 'TXD RXD RTS CTS DTR DSR DCD RI', start=0) """Function of pins in UART mode.""" @unique class Command(IntEnum): """Command type for command queue (TX thread).""" TERMINATE = 0x1 # Terminate thread SET_BITMODE = 0x2 # Change the bitmode def __init__(self, parent: 'VirtFtdi', iface: int): self.log = getLogger(f'pyftdi.vftdi[{iface}]') self._parent = parent self._iface: int = iface self._bitmode = self.BitMode.RESET self._hispeed: bool = False self._baudrate: int = 0 self._bb_clock: int = 0 self._mpsse: Optional[VirtMpsseTracer] = None self._direction: int = 0 self._gpio: int = 0 self._width = self.WIDTHS[parent.version] self._pins: List[VirtualFtdiPin] = [] self._fifos: VirtFtdiPort.Fifos = VirtFtdiPort.Fifos( Fifo(self.FIFO_SIZES[self._parent.version][1]), Fifo(self.FIFO_SIZES[self._parent.version][0])) self._cbus_dir: int = 0 # logical (commands) self._cbus: int = 0 # logical (commands) self._cbus_map: Optional[Mapping[int, int]] = None # logical to phys. self._cbusp_gpio: int = 0 # physical (pins) self._cbusp_force: int = 0 # physical (pins) self._cbusp_active: int = 0 # physical (pins) self._resume: bool = True self._cmd_q = Fifo() self._last_txw_ts = 0 for pin in range(self._width): self._pins.append(VirtualFtdiPin(self, pin)) self._rx_thread = Thread( target=self._rx_worker, name=f'Ftdi-{parent.bus}:{parent.address}/{iface}-Rx', daemon=True) self._tx_thread = Thread( target=self._tx_worker, name=f'Ftdi-{parent.bus}:{parent.address}/{iface}-Tx', daemon=True) self._rx_thread.start() self._tx_thread.start() def close(self, freeze: bool = False) -> None: self.log.debug('> close %d', freeze) if self._tx_thread: with self._cmd_q.lock: self._cmd_q.q.append((self.Command.TERMINATE,)) self._cmd_q.event.set() timeout = now()+.5 while self._cmd_q.q: sleep(50e-3) if now() >= timeout: self.log.warning('aborting before full completion') break self._resume = False if self._rx_thread: self._rx_thread.join() self._rx_thread = None if self._tx_thread: self._tx_thread.join() self._tx_thread = None def terminate(self): self.close() def __getitem__(self, index: int) -> VirtualFtdiPin: if not isinstance(index, int): raise TypeError(f"sequence index must be integer, " f"not '{index.__class__.__name__}'") try: return self._pins[index] except IndexError: raise IndexError(f'Invalid pin: {index}') from None @property def iface(self) -> int: return self._iface @property def baudrate(self) -> int: return self._baudrate @property def width(self) -> int: return self._width @property def direction(self) -> int: return self._direction @property def gpio(self) -> int: return self._gpio @property def cbus(self) -> Tuple[int, int]: """Emulate CBUS output (from FTDI to peripheral). :return: a tuple of logical value on pins, active pins. non-active pins should be considered as High-Z """ return self._cbus_read() @cbus.setter def cbus(self, cbus: int) -> None: """Emulate CBUS input (from peripheral to FTDI).""" self._cbus_write(cbus) def write(self, data: array, timeout: int) -> int: rx_fifo = self._fifos.rx with rx_fifo.lock: count = len(rx_fifo.q) rx_fifo.q.append(data) rx_fifo.event.set() if not count: # the FIFO was empty, so wait for the first request to complete # this is a hackish way to ensure a request when the device is # not busy handling his FIFOs responds "immediately" to the # first request timeout = now()+2 # note that verbose traces may trigger a timeout while self._resume and rx_fifo.q: sleep(self.POLL_DELAY) if now() >= timeout: raise RuntimeError('RX queue seems stalled') return len(data) def read(self, buff: array, timeout: int) -> int: tx_fifo = self._fifos.tx count = len(buff) if count < 2: self.log.warning('Never handle buffers that cannot fit status') return 0 if self._bitmode in (self.BitMode.RESET, self.BitMode.BITBANG, self.BitMode.SYNCBB, self.BitMode.MPSSE): status = self.modem_status buff[0], buff[1] = status[0], status[1] pos = 2 with tx_fifo.lock: while tx_fifo.q and pos < count: buff[pos] = tx_fifo.q.popleft() pos += 1 return pos self.log.warning('Read buffer discarded, mode %s', self.BitMode(self._bitmode).name) self.log.warning('. bbr (%d)', len(buff)) return 0 def set_io(self, pin: VirtualFtdiPin, signal: bool) -> None: position = pin.position if not 0 <= position < self._width: raise ValueError(f'Invalid pin: {position}') if signal: self._update_gpio(True, self._gpio | (1 << position)) else: self._update_gpio(True, self._gpio & ~(1 << position)) def update_gpio(self, mpsse: VirtMpsseEngine, source: bool, direction: int, gpio: int) -> None: self._direction = direction self._update_gpio(source, gpio) @property def modem_status(self) -> Tuple[int, int]: # For some reason, B0 high nibble matches the LPC214x UART:UxMSR # B0.0 ? # B0.1 ? # B0.2 ? # B0.3 ? # B0.4 Clear to send (CTS) # B0.5 Data set ready (DTS) # B0.6 Ring indicator (RI) # B0.7 Receive line signal / Data carrier detect (RLSD/DCD) # For some reason, B1 exactly matches the LPC214x UART:UxLSR # B1.0 Data ready (DR) # B1.1 Overrun error (OE) # B1.2 Parity error (PE) # B1.3 Framing error (FE) # B1.4 Break interrupt (BI) # B1.5 Transmitter holding register (THRE) # B1.6 Transmitter empty (TEMT) # B1.7 Error in RCVR FIFO buf0 = 0x02 # magic constant, no idea for now if self._bitmode == self.BitMode.RESET: cts = 0x01 if self._gpio & 0x08 else 0 dsr = 0x02 if self._gpio & 0x20 else 0 ri = 0x04 if self._gpio & 0x80 else 0 dcd = 0x08 if self._gpio & 0x40 else 0 buf0 |= cts | dsr | ri | dcd else: # another magic constant buf0 |= 0x30 buf1 = 0 rx_fifo = self._fifos.rx with rx_fifo.lock: if not rx_fifo.q: # TX empty -> flag THRE & TEMT ("TX empty") buf1 |= 0x40 | 0x20 return buf0, buf1 def control_reset(self, wValue: int, wIndex: int, data: array) -> None: reset = FTDICONST.get_name('sio_reset', wValue) if reset == 'sio': # the thruth is that FTDI does not document what this command # exactly does... for fifo in self._fifos: with fifo.lock: fifo.q.clear() self.log.info('> ftdi reset is not fully implemented') self._gpio = 0 self._direction = 0 self._bitmode = self.BitMode.RESET return if reset == 'purge_tx': fifo = self._fifos.tx self.log.info('> ftdi %s: %d requests', reset, len(fifo.q)) with fifo.lock: fifo.q.clear() return if reset == 'purge_rx': fifo = self._fifos.rx self.log.info('> ftdi %s: %d requests', reset, len(fifo.q)) with fifo.lock: fifo.q.clear() return self.log.error('Unknown reset kind: %s', reset) def control_set_bitmode(self, wValue: int, wIndex: int, data: array) -> None: direction = wValue & 0xff bitmode = (wValue >> 8) & 0x7F mode = self.BitMode(bitmode).name self.log.info('> ftdi bitmode %s: %s', mode, f'{direction:08b}') self._bitmode = bitmode with self._cmd_q.lock: self._cmd_q.q.append((self.Command.SET_BITMODE, self._bitmode)) self._cmd_q.event.set() # be sure to wait for the command to be popped out before resuming loop = 10 # is something goes wrong, do not loop forever while loop: if not self._cmd_q.q: # command queue has been fully processed break if not self._resume: # if the worker threads are ending or have ended, abort self.log.warning('Premature end of worker') return loop -= 1 # kepp some time for the commands to be processed sleep(0.05) else: raise RuntimeError(f'Command {self._cmd_q.q[-1][0].name} ' f'not handled') if bitmode == self.BitMode.CBUS: self._cbus_dir = direction >> 4 mask = (1 << self._parent.properties.cbuswidth) - 1 self._cbus_dir &= mask # clear output pins self._cbus &= ~self._cbus_dir & 0xF # update output pins output = direction & 0xF & self._cbus_dir self._cbus |= output self.log.info('> ftdi cbus dir %s, io %s, mask %s', f'{self._cbus_dir:04b}', f'{self._cbus:04b}', f'{mask:04b}') elif bitmode == self.BitMode.RESET: self._direction = ((1 << VirtFtdiPort.UART_PINS.TXD) | (1 << VirtFtdiPort.UART_PINS.RTS) | (1 << VirtFtdiPort.UART_PINS.DTR)) self._pins[0].set_function(VirtualFtdiPin.Function.STREAM) self._pins[1].set_function(VirtualFtdiPin.Function.STREAM) for pin in self._pins[2:]: pin.set_function(VirtualFtdiPin.Function.GPIO) else: self._direction = direction for pin in self._pins: pin.set_function(VirtualFtdiPin.Function.GPIO) if bitmode == self.BitMode.MPSSE: for pin in self._pins: pin.set_function(VirtualFtdiPin.Function.GPIO) if not self._mpsse: self._mpsse = VirtMpsseTracer(self, self._parent.version) def control_set_latency_timer(self, wValue: int, wIndex: int, data: array) -> None: self.log.debug('> ftdi latency timer: %d', wValue) def control_set_event_char(self, wValue: int, wIndex: int, data: array) -> None: char = wValue & 0xFF enable = bool(wValue >> 8) self.log.info('> ftdi %sable event char: 0x%02x', 'en' if enable else 'dis', char) def control_set_error_char(self, wValue: int, wIndex: int, data: array) -> None: char = wValue & 0xFF enable = bool(wValue >> 8) self.log.info('> ftdi %sable error char: 0x%02x', 'en' if enable else 'dis', char) def control_read_pins(self, wValue: int, wIndex: int, data: array) -> bytes: mode = FTDICONST.get_name('bitmode', self._bitmode) self.log.info('> ftdi read_pins %s', mode) if mode == 'cbus': cbus = self._cbus & ~self._cbus_dir & 0xF self.log.info('< cbus 0x%01x: %s', cbus, f'{cbus:04b}') return bytes([cbus]) low_gpio = self._gpio & 0xFF self.log.info('< gpio 0x%02x: %s', low_gpio, f'{low_gpio:08b}') return bytes([low_gpio]) def control_set_baudrate(self, wValue: int, wIndex: int, data: array) -> None: FRAC_INV_DIV = (0, 4, 2, 1, 3, 5, 6, 7) BAUDRATE_REF_BASE = 3.0E6 # 3 MHz BAUDRATE_REF_HIGH = 12.0E6 # 12 MHz if self._parent.is_hispeed_device or self._parent.is_x_series: wIndex >>= 8 divisor = wValue | (wIndex << 16) div = divisor & 0x3FFF hispeed = bool(divisor & 0x20000) if not self._parent.is_hispeed_device and hispeed: raise ValueError('Invalid hispeed mode with non-H series') subdiv_code = (divisor >> 14) & 0x7 refclock = BAUDRATE_REF_HIGH if hispeed else BAUDRATE_REF_BASE if div < 2 and subdiv_code: raise ValueError('Invalid sub-divisor with special div') if div == 0: baudrate = 3.0e6 elif div == 1: baudrate = 2.0e6 else: subdiv = FRAC_INV_DIV[subdiv_code] baudrate = round((refclock * 8) / (div * 8 + subdiv)) self._hispeed = hispeed self._baudrate = int(baudrate) multiplier = FTDICONST.get_value('BITBANG_BAUDRATE_RATIO', 'HIGH' if hispeed else 'BASE') self._bb_clock = self._baudrate * multiplier self.log.info('> ftdi set_baudrate %d bps, HS: %s, bb %d Hz', baudrate, hispeed, self._bb_clock) def control_set_data(self, wValue: int, wIndex: int, data: array) -> None: self.log.debug('> ftdi set_data (NOT IMPLEMENTED)') def control_set_flow_ctrl(self, wValue: int, wIndex: int, data: array) -> None: self.log.debug('> ftdi set_flow_ctrl (NOT IMPLEMENTED)') def control_poll_modem_status(self, wValue: int, wIndex: int, data: array) -> None: status = self.modem_status self.log.info('> ftdi poll_modem_status %02x%02x', status[0], status[1]) return status def write_from_pin(self, pin: int, buf: bytes) -> None: tx_fifo = self._fifos.tx with tx_fifo.lock: free_count = tx_fifo.size - len(tx_fifo.q) if free_count > 0: tx_fifo.q.extend(buf[:free_count]) if free_count < len(buf): self.log.warning('FIFO full, truncated buffer from %d', pin) def write_from_mpsse(self, mpsse: VirtMpsseEngine, buf: bytes) -> None: tx_fifo = self._fifos.tx with tx_fifo.lock: free_count = tx_fifo.size - len(tx_fifo.q) if free_count > 0: tx_fifo.q.extend(buf[:free_count]) if free_count < len(buf): self.log.warning('FIFO full (%d bytes), truncated buffer', len(tx_fifo.q)) self._mpsse.receive(self._iface, buf) def _update_gpio(self, source: bool, gpio: int) -> None: changed = self._gpio ^ gpio self._gpio = gpio if source: # call comes from an external source, no need to propagate more return for pos, pin in enumerate(self._pins): bit = 1 << pos if not bit & self._direction: # do not update input pins continue if bit & changed: pin.set(False, bool(gpio & (1 << pos))) def _decode_cbus_x1000(self) -> None: cbus_gpio = 0 cbus_force = 0 cbus_active = 0 for bix in range(4): value = self._parent.eeprom[0x1A + bix] bit = 1 << bix if FtdiEeprom.CBUSX(value).name == 'GPIO': cbus_gpio |= bit cbus_active |= bit elif FtdiEeprom.CBUSX(value).name == 'DRIVE0': cbus_force &= ~bit # useless, for code symmetry cbus_active |= bit elif FtdiEeprom.CBUSX(value).name == 'DRIVE1': cbus_force |= bit cbus_active |= bit mask = (1 << self._parent.properties.cbuswidth) - 1 self._cbusp_gpio = cbus_gpio & mask self._cbusp_force = cbus_force & mask self._cbusp_active = cbus_active & mask self.log.debug('x1000 config gpio %s, force %s, active %s', f'{self._cbusp_gpio:04b}', f'{self._cbusp_force:04b}', f'{self._cbusp_active:04b}') def _decode_cbus_x0900(self) -> None: cbus_gpio = 0 cbus_force = 0 cbus_active = 0 for bix in range(5): value = self._parent.eeprom[0x18 + bix] low, high = value & 0x0F, value >> 4 bit = 1 << 2*bix if FtdiEeprom.CBUSH(low).name == 'GPIO': cbus_gpio |= bit cbus_active |= bit elif FtdiEeprom.CBUSH(low).name == 'DRIVE0': cbus_force &= ~bit # useless, for code symmetry cbus_active |= bit elif FtdiEeprom.CBUSH(low).name == 'DRIVE1': cbus_force |= bit cbus_active |= bit bit <<= 1 if FtdiEeprom.CBUSH(high).name == 'GPIO': cbus_gpio |= bit cbus_active |= bit elif FtdiEeprom.CBUSH(high).name == 'DRIVE0': cbus_force &= ~bit # useless, for code symmetry cbus_active |= bit elif FtdiEeprom.CBUSH(high).name == 'DRIVE1': cbus_force |= bit cbus_active |= bit mask = (1 << self._parent.properties.cbuswidth) - 1 self._cbusp_gpio = cbus_gpio & mask self._cbusp_force = cbus_force & mask self._cbusp_active = cbus_active & mask self._cbus_map = {0: 5, 1: 6, 2: 8, 3: 9} self.log.debug('x0900 config gpio %s, force %s, active %s', f'{self._cbusp_gpio:04b}', f'{self._cbusp_force:04b}', f'{self._cbusp_active:04b}') def _decode_cbus_x0600(self) -> None: cbus_gpio = 0 cbus_active = 0 bix = 0 while True: value = self._parent.eeprom[0x14 + bix] low, high = value & 0x0F, value >> 4 bit = 1 << (2*bix) if FtdiEeprom.CBUS(low).name == 'GPIO': cbus_gpio |= bit cbus_active |= bit if bix == 2: break bit <<= 1 if FtdiEeprom.CBUS(high).name == 'GPIO': cbus_gpio |= bit cbus_active |= bit bix += 1 mask = (1 << self._parent.properties.cbuswidth) - 1 self._cbusp_gpio = cbus_gpio & mask self._cbusp_force = 0 self._cbusp_active = cbus_active & mask self.log.debug('x0600 config gpio %s, force %s, active %s', f'{self._cbusp_gpio:04b}', f'{self._cbusp_force:04b}', f'{self._cbusp_active:04b}') def _cbus_write(self, cbus: int) -> None: # from peripheral to FTDI # mask out CBUS pins which are not configured as GPIOs cbus &= self._cbusp_active cbus &= self._cbusp_gpio self.log.debug('> cbus_write active: %s gpio: %s, force: %s, cbus: %s', f'{self._cbusp_active:04b}', f'{self._cbusp_gpio:04b}', f'{self._cbusp_force:04b}', f'{cbus:04b}') if self._cbus_map: self.log.info('cbus_write map') # convert physical gpio into logical gpio lgpio = 0 for log, phy in self._cbus_map: if cbus & (1 << phy): lgpio |= 1 << log cbus = lgpio # only consider logical input cbus &= ~self._cbus_dir # combine existing output with new input self._cbus &= ~self._cbus_dir self._cbus |= cbus & ~self._cbus_dir def _cbus_read(self) -> Tuple[int, int]: # from FTDI to peripheral cbus = self._cbus self.log.debug('> cbus_read active %s, gpio %s, force %s, cbus %s', f'{self._cbusp_active:04b}', f'{self._cbusp_gpio:04b}', f'{self._cbusp_force:04b}', f'{cbus:04b}') if self._cbus_map: self.log.info('cbus_read map') # convert logical gpio into physical gpio pgpio = 0 for log, phy in self._cbus_map: if cbus & (1 << log): pgpio |= 1 << phy cbus = pgpio # mask out CBUS pins which are not configured as GPIOs cbus &= self._cbusp_gpio # apply DRIVE1 to gpio cbus |= self._cbusp_force self.log.info('< cbus_read cbus %s, active %s', f'{cbus:04b}', f'{self._cbusp_active:04b}') return cbus, self._cbusp_active def _rx_worker(self): """Background handling of data received from host.""" try: while self._resume: rx_fifo = self._fifos.rx rx_fifo.lock.acquire() if not rx_fifo.q: rx_fifo.lock.release() if not rx_fifo.event.wait(self.POLL_DELAY): continue rx_fifo.lock.acquire() rx_fifo.event.clear() if not rx_fifo.q: self.log.error('wake up w/o RX data') rx_fifo.lock.release() continue rx_fifo.event.clear() data = rx_fifo.q[0] rx_fifo.lock.release() if self._bitmode == self.BitMode.MPSSE: self._mpsse.send(self._iface, data) elif self._bitmode == self.BitMode.RESET: self[self.UART_PINS.TXD].push_to_pin(data) elif self._bitmode == self.BitMode.BITBANG: for byte in data: # only 8 LSBs are addressable through this command gpi = self._gpio & ~self._direction & 0xFF gpo = byte & self._direction & 0xFF msb = self._gpio & ~0xFF gpio = gpi | gpo | msb self._update_gpio(False, gpio) self.log.debug('. bbw %02x: %s', self._gpio, f'{self._gpio:08b}') elif self._bitmode == self.BitMode.SYNCBB: tx_fifo = self._fifos.tx lost = 0 for byte in data: with tx_fifo.lock: free_count = tx_fifo.size - len(tx_fifo.q) if free_count > 0: tx_fifo.q.append(self._gpio & 0xFF) else: lost += 1 # only 8 LSBs are addressable through this command gpi = self._gpio & ~self._direction & 0xFF gpo = byte & self._direction & 0xFF msb = self._gpio & ~0xFF gpio = gpi | gpo | msb self._update_gpio(False, gpio) self.log.debug('. bbw %02x: %s', self._gpio, f'{self._gpio:08b}') if lost: self.log.debug('%d samples lost, TX full', lost) else: try: mode = self.BitMode(self._bitmode).name except ValueError: mode = 'unknown' self.log.warning('Write buffer discarded, mode %s', mode) self.log.warning('. (%d) %s', len(data), hexlify(data).decode()) with rx_fifo.lock: rx_fifo.q.popleft() self.log.debug('End of worker %s', self._rx_thread.name) except Exception as exc: self.log.error('Dead of worker %s: %s', self._rx_thread.name, exc) raise finally: # ensure other threads to not run forever if this one is on error if self._resume: self.log.info('RX worker is triggering death') self._resume = False def _tx_worker(self): """Background handling of data sent to host.""" bitmode = self.BitMode.RESET _wait_delay = self.SLEEP_DELAY terminated = False try: while self._resume: # in order not to overload CPU with real-time computation, # time is sliced into POLL_DELAY period; every POLL_DELAY # period, this thead generates as many data as the real HW # would have generated during this period (~ epsilon due to # rounding) if not self._cmd_q.q: if not self._cmd_q.event.wait(_wait_delay): self._tx_worker_generate(bitmode) continue with self._cmd_q.lock: if not self._cmd_q.q: self.log.error('wake up w/o CMD') continue self._cmd_q.event.clear() command = self._cmd_q.q.popleft() self.log.info('Command %s', self.Command(command[0]).name) if command[0] == self.Command.TERMINATE: terminated = True break if command[0] == self.Command.SET_BITMODE: bitmode = command[1] self._last_txw_ts = now() if bitmode in (self.BitMode.BITBANG, self.BitMode.SYNCBB): _wait_delay = self.POLL_DELAY else: _wait_delay = self.SLEEP_DELAY continue else: self.log.error('Unimplemented support for command %d', command) continue self.log.debug('End of worker %s', self._tx_thread.name) except Exception as exc: self.log.error('Dead of worker %s: %s', self._tx_thread.name, exc) raise finally: # ensure other threads to not run forever if this one is on error if not terminated and self._resume: self.log.info('TX worker is triggering death') self._resume = False def _tx_worker_generate(self, bitmode) -> None: tx_fifo = self._fifos.tx if bitmode == self.BitMode.BITBANG: ts = now() # how much time has elapsed since last background action elapsed = ts-self._last_txw_ts self._last_txw_ts = ts # how many bytes should have been captured since last # action byte_count = round(self._baudrate*elapsed) # fill the TX FIFO with as many bytes, stop if full if byte_count: with tx_fifo.lock: free_count = tx_fifo.size - len(tx_fifo.q) push_count = min(free_count, byte_count) tx_fifo.q.extend([self._gpio] * push_count) self.log.debug('in %.3fms -> %d', elapsed*1000, push_count) class VirtFtdi: """Virtual FTDI device. :param version: FTDI version (device kind) :param eeprom_size: size of external EEPROM size, if any """ class Properties(NamedTuple): """Device properties.""" ifcount: int # count of interface ifwidth: int # pin width of an interface cbuswidth: int # pin width of the control bus EXT_EEPROMS: Mapping[str, int] = { '93c46': 128, # 1024 bits '93c56': 256, # 2048 bits '93c66': 256, # 2048 bits (93C66 seen as 93C56) } """External EEPROMs.""" INT_EEPROMS: Mapping[int, int] = { 0x0600: 0x80, # FT232R: 128 bytes, 1024 bits 0x1000: 0x400 # FT23*X: 1KiB } """Internal EEPROMs.""" PROPERTIES: Mapping[int, Properties] = { 0x0200: Properties(1, 8, 0), # FT232AM 0x0400: Properties(1, 8, 0), # FT232BM 0x0500: Properties(1, 8, 0), # FT2232D 0x0600: Properties(1, 8, 5), # FT232R 0x0700: Properties(2, 16, 0), # FT2232H 0x0800: Properties(4, 8, 0), # FT4232H 0x0900: Properties(1, 8, 10), # FT232H 0x1000: Properties(1, 8, 4), # FT231X } """Width of port/bus (regular, cbus).""" EEPROM_REQ_BASE = FTDICONST.get_value('SIO_REQ', 'EEPROM') """Base value for EEPROM request.""" def __init__(self, version: int, bus: int, address: int, eeprom: Optional[dict] = None): self.log = getLogger('pyftdi.vftdi') self._version = version self._bus: int = bus self._address: int = address self._eeprom: bytearray = self._build_eeprom(version, eeprom) self._ports: List[VirtFtdiPort] = [] for iface in range(self.PROPERTIES[self._version].ifcount): self._ports.append(VirtFtdiPort(self, iface+1)) def close(self, freeze: bool = False) -> None: for port in self._ports: port.close(freeze) def terminate(self): self.close() @property def version(self) -> int: return self._version @property def bus(self) -> int: return self._bus @property def address(self) -> int: return self._address @property def properties(self) -> 'VirtFtdi.Properties': return self.PROPERTIES[self._version] @property def is_mpsse_device(self) -> bool: """Tell whether the device has an MPSSE engine :return: True if the FTDI device supports MPSSE """ return self._version in (0x0500, 0x0700, 0x0800, 0x0900) @property def is_hispeed_device(self) -> bool: """Tell whether the device is a high speed device :return: True if the FTDI device is HS """ return self._version in (0x0700, 0x0800, 0x0900) @property def is_x_series(self) -> bool: """Tell whether the device is a FT-X device :return: True for FT-X device """ return self._version == 0x1000 def apply_eeprom_config(self, devdesc: dict, cfgdescs: Sequence[dict]) -> None: self._load_eeprom(devdesc, cfgdescs) def control(self, dev_handle: 'VirtDeviceHandle', bmRequestType: int, bRequest: int, wValue: int, wIndex: int, data: array, timeout: int) -> int: req_ctrl = USBCONST.dec_req_ctrl(bmRequestType) req_type = USBCONST.dec_req_type(bmRequestType) req_rcpt = USBCONST.dec_req_rcpt(bmRequestType) req_desc = ':'.join([req_ctrl, req_type, req_rcpt]) req_name = FTDICONST.dec_req_name(bRequest) dstr = (hexlify(data).decode() if USBCONST.is_req_out(bmRequestType) else f'({len(data)})') self.log.debug('> control ftdi hdl %d, %s, %s, ' 'val 0x%04x, idx 0x%04x, data %s, to %d', dev_handle.handle, req_desc, req_name, wValue, wIndex, dstr, timeout) size = 0 try: if bRequest >= self.EEPROM_REQ_BASE: obj = self else: obj = self._ports[0xFF & (wIndex-1)] if wIndex else self except IndexError: raise ValueError(f'Invalid iface: 0x{wIndex:04x} ' f'for {req_name}') from None try: pre = '_' if obj == self else '' handler = getattr(obj, f'{pre}control_{req_name}') except AttributeError: self.log.warning('Unknown request %02x:%04x: %s for %s)', bRequest, wIndex, req_name, obj.__class__.__name__) return size buf = handler(wValue, wIndex, data) or b'' size = len(buf) data[:size] = array('B', buf) self.log.debug('< (%d) %s', size, hexlify(data[:size]).decode()) return size def write(self, dev_handle: 'VirtDeviceHandle', ep: int, intf: int, data: array, timeout: int) -> int: return self._ports[intf].write(data, timeout) def read(self, dev_handle: 'VirtDeviceHandle', ep: int, intf: int, buff: array, timeout: int) -> int: return self._ports[intf].read(buff, timeout) def get_port(self, iface: int): # iface: 1..n return self._ports[iface-1] @property def eeprom(self) -> bytes: return bytes(self._eeprom) @eeprom.setter def eeprom(self, value: bytes): if len(value) != len(self._eeprom): raise ValueError('EEPROM size mismatch') self._eeprom = bytearray(value) @classmethod def _build_eeprom(cls, version, eeprom: Optional[dict]) -> bytearray: size = 0 data = b'' if eeprom: model = eeprom.get('model', None) if model: if version in cls.INT_EEPROMS: raise ValueError('No external EEPROM supported on this ' 'device') try: size = cls.EXT_EEPROMS[model.lower()] except KeyError as exc: raise ValueError('Unsupported EEPROM model: {model}') \ from exc data = eeprom.get('data', b'') if version in cls.INT_EEPROMS: int_size = cls.INT_EEPROMS[version] # FT232R, FT230x, FT231x, FT234x if size: if size != int_size: raise ValueError('Internal EEPROM size cannot be changed') else: size = int_size else: if size and size not in cls.EXT_EEPROMS.values(): raise ValueError(f'Invalid EEPROM size: {size}') if data and len(data) > size: raise ValueError('Data cannot fit into EEPROM') buf = bytearray(size) buf[:len(data)] = data return buf def _checksum_eeprom(self, data: bytearray) -> int: length = len(data) if length & 0x1: raise ValueError('Length not even') # NOTE: checksum is computed using 16-bit values in little endian # ordering checksum = 0XAAAA mtp = self._version == 0x1000 # FT230X for idx in range(0, length, 2): if mtp and 0x24 <= idx < 0x80: # special MTP user section which is not considered for the CRC continue val = ((data[idx+1] << 8) + data[idx]) & 0xffff checksum = val ^ checksum checksum = ((checksum << 1) & 0xffff) | ((checksum >> 15) & 0xffff) return checksum def _load_eeprom(self, devdesc: dict, cfgdescs: Sequence[dict]) -> None: whole = self._version != 0x1000 # FT230X buf = self._eeprom if whole else self._eeprom[:0x100] chksum = self._checksum_eeprom(buf) if chksum: self.log.warning('Invalid EEPROM checksum, ignoring content') return # only apply a subset of what the EEPROM can configure for now devdesc['idVendor'] = sunpack(' Optional[bytes]: self.log.debug('> ftdi read_eeprom @ 0x%04x', wIndex*2) if not self._eeprom: self.log.warning('Missing EEPROM') return None if len(data) != 2: self.log.warning('Unexpected read size: %d', len(data)) # resume anyway address = abs(wIndex * 2) if address + 1 > len(self._eeprom): # out of bound self.log.warning('Invalid EEPROM address: 0x%04x', wValue) return None word = bytes(self._eeprom[address: address+2]) return word def _control_write_eeprom(self, wValue: int, wIndex: int, data: array) -> None: self.log.info('> ftdi write_eeprom @ 0x%04x', wIndex*2) if not self._eeprom: self.log.warning('Missing EEPROM') return address = abs(wIndex * 2) if address + 1 > len(self._eeprom): # out of bound self.log.warning('Invalid EEPROM address: 0x%04x', wValue) return if self._version == 0x1000: if 0x80 <= address < 0xA0: # those address are R/O on FT230x self.log.warning('Protected EEPROM address: 0x%04x', wValue) return self._eeprom[address: address+2] = spack(' None: # set_baudrate is very specific and hack-ish, as the HW is. # the way the interface is encoded is simply ugly (likely a legacy # from single-port device, then FTDI needed to support multi-port # ones and found a hack-ish way to implement it w/o breaking the # existing implementation) if self.is_mpsse_device: iface = wIndex & 0xFF wIndex >>= 16 else: iface = 1 return self._ports[iface-1].control_set_baudrate(wValue, wIndex, data) pyftdi-0.54.0/pyftdi/tests/backend/loader.py000066400000000000000000000367651421346242200207700ustar00rootroot00000000000000"""Virtual USB backend loader. """ # Copyright (c) 2020-2021, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause #pylint: disable-msg=missing-docstring #pylint: disable-msg=too-few-public-methods #pylint: disable-msg=too-many-branches #pylint: disable-msg=too-many-statements #pylint: disable-msg=too-many-nested-blocks #pylint: disable-msg=no-self-use from binascii import unhexlify from logging import getLogger from sys import version_info from typing import BinaryIO from ruamel.yaml import YAML from pyftdi.misc import to_bool from pyftdi.usbtools import UsbTools from .usbvirt import (VirtConfiguration, VirtDevice, VirtInterface, VirtEndpoint, get_backend) from .consts import USBCONST class VirtLoader: """Load a virtual USB bus environment from a YaML description stream. """ def __init__(self): self.log = getLogger('pyftdi.virt.backend') self._last_ep_idx = 0 self._epprom_backup = b'' def load(self, yamlfp: BinaryIO) -> None: """Load a YaML configuration stream. :param yamlfp: YaML stream to be parsed """ backend = get_backend() with yamlfp: try: for ydef in YAML().load_all(yamlfp): self._build_root(backend, ydef) except Exception as exc: raise ValueError(f'Invalid configuration: {exc}') from exc self._validate() UsbTools.release_all_devices(VirtDevice) UsbTools.flush_cache() def unload(self) -> None: """Unload current USB topology, release all allocated devices, and flush UsbTools cache. Note that the application should also flush UsbTools cache, or reference to 'disconnected' devices may persist. """ backend = get_backend() backend.flush_devices() count = UsbTools.release_all_devices(VirtDevice) UsbTools.flush_cache() return count def get_virtual_ftdi(self, bus, address): return get_backend().get_virtual_ftdi(bus, address) @property def eeprom_backup(self) -> bytes: """Return the prefined content of the EEPROM, if any. """ return self._epprom_backup def _validate(self): locations = set() for device in get_backend().devices: # check location on buses location = (device.bus, device.address) if location in locations: raise ValueError('Two devices on same USB location ' f'{location}') locations.add(location) configs = set() ifaces = set() epaddrs = set() for config in device.configurations: cfgval = config.bConfigurationValue if cfgval in configs: raise ValueError(f'Config {cfgval} assigned twice') configs.add(cfgval) for iface in config.interfaces: ifval = iface.bInterfaceNumber if ifval in ifaces: raise ValueError(f'Interface {ifval} assigned twice') ifaces.add(iface) # check endpoint addresses for endpoint in iface.endpoints: epaddr = endpoint.bEndpointAddress if epaddr in epaddrs: raise ValueError(f'EP 0x{epaddr:02x} ' 'assigned twice') epaddrs.add(epaddr) def _build_root(self, backend, container): backend.flush_devices() if not isinstance(container, dict): raise ValueError('Top-level not a dict') for ykey, yval in container.items(): if ykey != 'devices': continue if not isinstance(yval, list): raise ValueError('Devices not a list') for yitem in yval: if not isinstance(container, dict): raise ValueError('Device not a dict') self._last_ep_idx = 0 device = self._build_device(yitem) device.build() backend.add_device(device) def _build_device(self, container): devdesc = None configs = [] properties = {} delayed_load = False for ykey, yval in container.items(): if ykey == 'descriptor': if not isinstance(yval, dict): raise ValueError('Device descriptor not a dict') devdesc = self._build_device_descriptor(yval) continue if ykey == 'configurations': if not isinstance(yval, list): raise ValueError('Configurations not a list') configs = [self._build_configuration(conf) for conf in yval] continue if ykey == 'noaccess': yval = to_bool(yval) if ykey == 'speed' and isinstance(yval, str): try: yval = USBCONST.speeds[yval] except KeyError as exc: raise ValueError(f'Invalid device speed {yval}') from exc if ykey == 'eeprom': if not isinstance(yval, dict): raise ValueError('Invalid EEPROM section') for pkey, pval in yval.items(): if pkey == 'model': if not isinstance(pval, str): raise ValueError('Invalid EEPROM model') continue if pkey == 'load': try: pval = to_bool(pval, permissive=False, allow_int=True) yval[pkey] = pval delayed_load = pval except ValueError as exc: raise ValueError('Invalid EEPROM load option') \ from exc continue if pkey == 'data': if isinstance(pval, str): hexstr = pval.replace(' ', '').replace('\n', '') try: pval = unhexlify(hexstr) yval[pkey] = pval except ValueError as exc: raise ValueError('Invalid EEPROM hex format') \ from exc if not isinstance(pval, bytes): raise ValueError(f'Invalid EEPROM data ' f'{type(pval)}') self._epprom_backup = pval continue raise ValueError(f'Unknown EEPROM option {pkey}') properties[ykey] = yval if not devdesc: raise ValueError('Missing device descriptor') if not configs: configs = [self._build_configuration({})] device = VirtDevice(devdesc, **properties) for config in configs: device.add_configuration(config) if delayed_load: device.ftdi.apply_eeprom_config(device.desc, [cfg.desc for cfg in configs]) return device def _build_device_descriptor(self, container) -> dict: kmap = { 'usb': 'bcdUSB', 'class': 'bDeviceClass', 'subclass': 'bDeviceSubClass', 'protocol': 'bDeviceProtocol', 'maxpacketsize': 'bMaxPacketSize0', 'vid': 'idVendor', 'pid': 'idProduct', 'version': 'bcdDevice', 'manufacturer': 'iManufacturer', 'product': 'iProduct', 'serialnumber': 'iSerialNumber', } kwargs = {} for ckey, cval in container.items(): try: dkey = kmap[ckey] except KeyError as exc: raise ValueError(f'Unknown descriptor field {dkey}') from exc kwargs[dkey] = cval return kwargs def _build_configuration(self, container): if not isinstance(container, dict): raise ValueError('Invalid configuration entry') cfgdesc = {} interfaces = [] for ykey, yval in container.items(): if ykey == 'descriptor': if not isinstance(yval, dict): raise ValueError('Configuration descriptor not a dict') cfgdesc = self._build_config_descriptor(yval) continue if ykey == 'interfaces': if not isinstance(yval, list): raise ValueError('Interfaces not a list') for conf in yval: interfaces.extend(self._build_interfaces(conf)) continue raise ValueError(f'Unknown config entry {ykey}') if not interfaces: interfaces.extend(self._build_interfaces({})) config = VirtConfiguration(cfgdesc) for iface in interfaces: config.add_interface(iface) return config def _build_config_descriptor(self, container) -> dict: kmap = { 'attributes': 'bmAttributes', 'maxpower': 'bMaxPower', 'configuration': 'iConfiguration' } kwargs = {} for ckey, cval in container.items(): try: dkey = kmap[ckey] except KeyError as exc: raise ValueError(f'Unknown descriptor field {ckey}') from exc if ckey == 'maxpower': cval //= 2 elif ckey == 'attributes': if not isinstance(cval, list): raise ValueError('Invalid config attributes') aval = 0x80 for feature in cval: if feature == 'selfpowered': aval |= 1 << 6 if feature == 'wakeup': aval |= 1 << 5 cval = aval elif ckey == 'configuration': pass else: raise ValueError(f'Unknown config descriptor {ckey}') kwargs[dkey] = cval return kwargs def _build_interfaces(self, container): if not isinstance(container, dict): raise ValueError('Invalid interface entry') repeat = 1 altdef = [{}] for ikey, ival in container.items(): if ikey == 'alternatives': if not isinstance(ival, list): raise ValueError(f'Invalid interface entry {ikey}') if len(ival) > 1: raise ValueError('Unsupported alternative count') if ival: altdef = ival elif ikey == 'repeat': if not isinstance(ival, int): raise ValueError(f'Invalid repeat count {ival}') repeat = ival else: raise ValueError(f'Invalid interface entry {ikey}') ifaces = [] while repeat: repeat -= 1 ifdesc, endpoints = self._build_alternative(altdef[0]) self._last_ep_idx = max([ep.bEndpointAddress & 0x7F for ep in endpoints]) iface = VirtInterface(ifdesc) for endpoint in endpoints: iface.add_endpoint(endpoint) ifaces.append(iface) return ifaces def _build_alternative(self, container): if not isinstance(container, dict): raise ValueError('Invalid alternative entry') ifdesc = {} endpoints = [] for ikey, ival in container.items(): if ikey == 'descriptor': if not isinstance(ival, dict): raise ValueError('Interface descriptor not a dict') ifdesc = self._build_interface_descriptor(ival) continue if ikey == 'endpoints': if not isinstance(ival, list): raise ValueError('Interface encpoints not a list') endpoints = [self._build_endpoint(ep) for ep in ival] if not endpoints: epidx = self._last_ep_idx epidx += 1 desc = {'descriptor': {'direction': 'in', 'number': epidx}} ep0 = self._build_endpoint(desc) epidx += 1 desc = {'descriptor': {'direction': 'out', 'number': epidx}} ep1 = self._build_endpoint(desc) endpoints = [ep0, ep1] return ifdesc, endpoints def _build_interface_descriptor(self, container) -> dict: kmap = { 'class': 'bDeviceClass', 'subclass': 'bDeviceSubClass', 'protocol': 'bDeviceProtocol', 'interface': 'iInterface', } kwargs = {} for ckey, cval in container.items(): try: dkey = kmap[ckey] except KeyError as exc: raise ValueError(f'Unknown descriptor field {ckey}') from exc kwargs[dkey] = cval return kwargs def _build_endpoint(self, container): if not isinstance(container, dict): raise ValueError('Invalid endpoint entry') epdesc = None for ikey, ival in container.items(): if ikey == 'descriptor': if not isinstance(ival, dict): raise ValueError('Interface descriptor not a dict') epdesc = self._build_endpoint_descriptor(ival) continue raise ValueError(f'Unknown config entry {ikey}') if not epdesc: raise ValueError('Missing endpoint descriptor') endpoint = VirtEndpoint(epdesc) return endpoint def _build_endpoint_descriptor(self, container) -> dict: kwargs = {} if 'number' not in container: raise ValueError('Missing endpoint number') if 'direction' not in container: raise ValueError('Missing endpoint direction') if 'type' not in container: container = dict(container) container['type'] = 'bulk' for ekey, val in container.items(): if ekey == 'maxpacketsize': kwargs['wMaxPacketSize'] = val continue if ekey == 'interval': kwargs['bInterval'] = val continue if ekey == 'direction': try: value = USBCONST.endpoints[val.lower()] except KeyError as exc: raise ValueError('Unknown endpoint direction') from exc kwargs.setdefault('bEndpointAddress', 0) kwargs['bEndpointAddress'] |= value continue if ekey == 'number': if not isinstance(val, int) or not 0 < val < 16: raise ValueError(f'Invalid endpoint number {val}') kwargs.setdefault('bEndpointAddress', 0) kwargs['bEndpointAddress'] |= val continue if ekey == 'type': try: kwargs['bmAttributes'] = \ USBCONST.endpoint_types[val.lower()] except KeyError as exc: raise ValueError('Unknown endpoint type') from exc continue if ekey == 'endpoint': kwargs['iEndpoint'] = val continue raise ValueError(f'Unknown endpoint entry {ekey}') return kwargs pyftdi-0.54.0/pyftdi/tests/backend/mpsse.py000066400000000000000000000066651421346242200206450ustar00rootroot00000000000000"""PyUSB virtual FTDI device.""" # Copyright (c) 2020, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from collections import deque from logging import getLogger from struct import unpack as sunpack from typing import Union from pyftdi.tracer import FtdiMpsseEngine, FtdiMpsseTracer class VirtMpsseTracer(FtdiMpsseTracer): """Reuse MPSSE tracer as a MPSSE command decoder engine. """ def __init__(self, port: 'VirtFtdiPort', version: int): super().__init__(version) self.log = getLogger('pyftdi.virt.mpsse.{port.iface}') self._port = port def _get_engine(self, iface: int): iface -= 1 try: self._engines[iface] except IndexError as exc: raise ValueError('No MPSSE engine available on interface %d' % iface) from exc if not self._engines[iface]: self._engines[iface] = VirtMpsseEngine(self, self._port) return self._engines[iface] class VirtMpsseEngine(FtdiMpsseEngine): """Virtual implementation of a MPSSE. Far from being complete for now :-) """ def __init__(self, tracer: VirtMpsseTracer, port: 'VirtFtdiPort'): super().__init__(port.iface) self.log = getLogger(f'pyftdi.virt.mpsse.{port.iface}') self._tracer = tracer self._port = port self._width = port.width self._mask = (1 << self._width) - 1 self._reply_q = deque() def send(self, buf: Union[bytes, bytearray]) -> None: super().send(buf) # cannot post the response before the send() method has completed # see FtdiMpsseEngine.send() for execution steps: expected reply size # is only known (stored) once the command execution has completed self.reply() def reply(self) -> None: """Post the reply to a command back into the virtual FTDI FIFO.""" while self._reply_q: self._port.write_from_mpsse(self, self._reply_q.popleft()) def _cmd_get_bits_low(self): super()._cmd_get_bits_low() byte = self._port.gpio & 0xff buf = bytes([byte]) self._reply_q.append(buf) return True def _cmd_get_bits_high(self): super()._cmd_get_bits_high() byte = (self._port.gpio >> 8) & 0xff buf = bytes([byte]) self._reply_q.append(buf) return True def _cmd_set_bits_low(self): buf = self._trace_tx[1:3] if not super()._cmd_set_bits_low(): return False port = self._port byte, direction = sunpack('BB', buf) gpi = port.gpio & ~direction & self._mask gpo = byte & direction & self._mask msb = port.gpio & ~0xFF gpio = gpi | gpo | msb port.update_gpio(self, False, direction, gpio) self.log.debug('. bbwl %04x: %s', port.gpio, f'{port.gpio:016b}') return True def _cmd_set_bits_high(self): buf = self._trace_tx[1:3] if not super()._cmd_set_bits_high(): return False port = self._port byte, direction = sunpack('BB', buf) byte <<= 8 direction <<= 8 gpi = port.gpio & ~direction & self._mask gpo = byte & direction & self._mask lsb = port.gpio & 0xFF gpio = gpi | gpo | lsb port.update_gpio(self, False, direction, gpio) self.log.debug('. bbwh %04x: %s', port.gpio, f'{port.gpio:016b}') return True pyftdi-0.54.0/pyftdi/tests/backend/usbvirt.py000066400000000000000000000404371421346242200212070ustar00rootroot00000000000000"""PyUSB virtual USB backend to intercept all USB requests. The role of this module is to enable PyFtdi API testing w/o any FTDI hardware. """ # Copyright (c) 2020-2021, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause #pylint: disable-msg=missing-docstring #pylint: disable-msg=invalid-name #pylint: disable-msg=attribute-defined-outside-init #pylint: disable-msg=too-many-locals #pylint: disable-msg=too-many-arguments #pylint: disable-msg=too-many-instance-attributes from array import array from binascii import hexlify from functools import partial from importlib import import_module from logging import getLogger from struct import calcsize as scalc, pack as spack from sys import version_info from typing import List, Mapping, Optional, Tuple from usb.backend import IBackend from pyftdi.misc import EasyDict from .consts import USBCONST from .ftdivirt import VirtFtdi class VirtEndpoint: """Fake USB interface endpoint. """ DESCRIPTOR_FORMAT = '<4BHB' def __init__(self, defs: dict, extra: Optional[bytes] = None): class EndpointDescriptor(EasyDict): pass if extra and not isinstance(extra, (bytes, bytearray)): raise ValueError('Invalid extra payload') self.desc = EndpointDescriptor( bLength=scalc(self.DESCRIPTOR_FORMAT), bDescriptorType=USBCONST.descriptors.ENDPOINT, bEndpointAddress=0, bmAttributes=0, wMaxPacketSize=64, bInterval=0, bRefresh=0, bSynchAddress=0, extra_descriptors=extra or b'') self.desc.update(defs) def build_strings(self, func): func(self.desc) def get_length(self) -> int: return self.desc.bLength def __getattr__(self, name): return getattr(self.desc, name) class VirtInterface: """Fake USB configuration interface. """ DESCRIPTOR_FORMAT = '<9B' def __init__(self, defs: dict, extra: Optional[bytes] = None): class InterfaceDescriptor(EasyDict): pass if extra and not isinstance(extra, (bytes, bytearray)): raise ValueError('Invalid extra payload') desc = InterfaceDescriptor( bLength=scalc(self.DESCRIPTOR_FORMAT), bDescriptorType=USBCONST.descriptors.INTERFACE, bInterfaceNumber=0, bAlternateSetting=0, bNumEndpoints=0, bInterfaceClass=0xFF, bInterfaceSubClass=0xFF, bInterfaceProtocol=0xFF, iInterface=0, # String desc index extra_descriptors=extra or b'') desc.update(defs) self.alt = 0 self.altsettings: List[Tuple[VirtInterface, List[VirtEndpoint]]] = [(desc, [])] def add_endpoint(self, endpoint: VirtEndpoint): altsetting = self.altsettings[self.alt] altsetting[1].append(endpoint) altsetting[0].bNumEndpoints = len(altsetting[1]) def update_number(self, number: int) -> None: self.altsettings[self.alt][0].bInterfaceNumber = number @property def endpoints(self): return self.altsettings[self.alt][1] def build_strings(self, func): for desc, _ in self.altsettings: func(desc) for _, endpoints in self.altsettings: for endpoint in endpoints: endpoint.build_strings(func) def add_bulk_pair(self): endpoints = self.altsettings[self.alt][1] desc = { 'bEndpointAddress': len(endpoints)+1 | USBCONST.endpoints['in'], 'bmAttributes': USBCONST.endpoint_types['bulk'] } ep = VirtEndpoint(desc) self.add_endpoint(ep) desc = { 'bEndpointAddress': len(endpoints)+1 | USBCONST.endpoints['out'], 'bmAttributes': USBCONST.endpoint_types['bulk'] } ep = VirtEndpoint(desc) self.add_endpoint(ep) def get_length(self) -> int: length = 0 for desc, endpoints in self.altsettings: length += desc.bLength for endpoint in endpoints: length += endpoint.get_length() return length @property def num_altsetting(self): return len(self.altsetting) def __getitem__(self, item): if isinstance(item, int): return self.altsettings[item] raise IndexError('Invalid alternate setting') def __getattr__(self, name): return getattr(self.altsettings[self.alt][0], name) class VirtConfiguration: """Fake USB device configuration. """ DESCRIPTOR_FORMAT = '<2BH5B' def __init__(self, defs: dict, extra: Optional[bytes] = None): class ConfigDescriptor(EasyDict): pass if extra and not isinstance(extra, (bytes, bytearray)): raise ValueError('Invalid extra payload') self.desc = ConfigDescriptor( bLength=scalc(self.DESCRIPTOR_FORMAT), bDescriptorType=USBCONST.descriptors.CONFIG, wTotalLength=0, bNumInterfaces=0, bConfigurationValue=0, iConfiguration=0, # string index bmAttributes=0x80, # bus-powered bMaxPower=150//2, # 150 mA extra_descriptors=extra or b'') self.desc.update(defs) self.interfaces: List[VirtInterface] = [] def add_interface(self, interface: VirtInterface): interface.update_number(len(self.interfaces)) self.interfaces.append(interface) self.desc.bNumInterfaces = len(self.interfaces) def build_strings(self, func): func(self.desc) for iface in self.interfaces: iface.build_strings(func) def update(self): # wTotalLength needs to be updated to the actual length of the # sub-objects self.desc.wTotalLength = self.get_length() def get_length(self) -> int: length = self.desc.bLength for iface in self.interfaces: length += iface.get_length() return length def __getattr__(self, name): return getattr(self.desc, name) class VirtDevice: """Fake USB device. """ DESCRIPTOR_FORMAT = '<2BH4B3H4B' DEFAULT_LANGUAGE = 0x0409 # en_US def __init__(self, defs: dict, **kwargs): class DeviceDescriptor(EasyDict): pass self.desc = DeviceDescriptor( bLength=scalc(self.DESCRIPTOR_FORMAT), bDescriptorType=USBCONST.descriptors.DEVICE, bcdUSB=0x200, # USB 2.0 bDeviceClass=0, bDeviceSubClass=0, bDeviceProtocol=0, bMaxPacketSize0=8, idVendor=0, idProduct=0, bcdDevice=0, iManufacturer=0, iProduct=0, iSerialNumber=0, bNumConfigurations=0, # updated later port_number=None, # unsupported port_numbers=None, # unsupported bus=0, address=0, speed=3, eeprom=None) self.desc.update(defs) self._props = set() for key in kwargs: # be sure not to allow descriptor override by arbitrary properties if key not in defs: self.desc[key] = kwargs[key] self._props.add(key) self.configurations = [] self.strings = [''] # slot 0 is reserved self._ftdi = VirtFtdi(self.desc.bcdDevice, self.desc.bus, self.desc.address, kwargs.get('eeprom', {})) def close(self, freeze: bool = False): self._ftdi.close(freeze) def terminate(self): self._ftdi.terminate() def add_configuration(self, config: VirtConfiguration): config.update() self.configurations.append(config) self.desc.bNumConfigurations = len(self.configurations) def build(self): func = partial(VirtDevice._store_strings, self) self.build_strings(func) def build_strings(self, func): func(self.desc, self._props) for config in self.configurations: config.build_strings(func) @property def ftdi(self) -> VirtFtdi: return self._ftdi @staticmethod def _store_strings(obj, desc, ignore=None): for dkey in sorted(desc): if ignore and dkey in ignore: continue if isinstance(desc[dkey], str): stridx = len(obj.strings) obj.strings.append(desc[dkey]) desc[dkey] = stridx def get_string(self, type_: int, index: int) -> str: if index == 0: if self.desc.get('noaccess', False): # simulate unauthorized access to the USB device return b'' # request for list of supported languages # only support one fmt = ' 'VirtLooader': """Provide the loader class to configure this virtual backend instance. Using this method to retrieve a loader ensure both the virtual backend and the loader have been loaded from the same package. :return: the VirtLoader class """ # this is a bit circumvoluted, but modules cannot cross-reference loader_modname = '.'.join(__name__.split('.')[:-1] + ['loader']) loader_mod = import_module(loader_modname) VirtLoader = getattr(loader_mod, 'VirtLoader') return VirtLoader @property def devices(self) -> List[VirtDevice]: return self._devices def get_virtual_ftdi(self, bus: int, address: int) -> VirtFtdi: for dev in self._devices: if dev.bus == bus and dev.address == address: return dev.ftdi raise ValueError('No FTDI @ {bus:address}') def enumerate_devices(self) -> VirtDevice: for dev in self._devices: yield dev def open_device(self, dev: VirtDevice) -> VirtDeviceHandle: self._device_handle_count += 1 devhdl = VirtDeviceHandle(dev, self._device_handle_count) self._device_handles[devhdl.handle] = devhdl return devhdl def close_device(self, dev_handle: VirtDeviceHandle) -> None: del self._device_handles[dev_handle.handle] def claim_interface(self, dev_handle: VirtDeviceHandle, intf: int) \ -> None: self.log.info('> claim interface h:%d: if:%d', dev_handle.handle, intf) def release_interface(self, dev_handle: VirtDeviceHandle, intf: int) \ -> None: self.log.info('> release interface h:%d: if:%d', dev_handle.handle, intf) def get_configuration(self, dev_handle: VirtDeviceHandle) -> VirtConfiguration: dev = dev_handle.device return dev.configurations[0] def set_configuration(self, dev_handle: VirtDeviceHandle, config_value: int) -> None: # config_value = ConfigDesc.bConfigurationValue pass def get_device_descriptor(self, dev: VirtDevice) -> EasyDict: return dev.desc def get_configuration_descriptor(self, dev: VirtDevice, config: int) \ -> EasyDict: return dev.configurations[config].desc def get_interface_descriptor(self, dev: VirtDevice, intf: int, alt: int, config: int) -> EasyDict: cfg = dev.configurations[config] iface = cfg.interfaces[intf] intf_desc = iface[alt][0] return intf_desc def get_endpoint_descriptor(self, dev: VirtDevice, ep: int, intf: int, alt: int, config: int) \ -> EasyDict: cfg = dev.configurations[config] iface = cfg.interfaces[intf] endpoints = iface[alt][1] ep_desc = endpoints[ep].desc return ep_desc def ctrl_transfer(self, dev_handle: VirtDeviceHandle, bmRequestType: int, bRequest: int, wValue: int, wIndex: int, data: array, timeout: int) -> int: req_type = USBCONST.dec_req_type(bmRequestType) if req_type == 'standard': return self._ctrl_standard(dev_handle, bmRequestType, bRequest, wValue, wIndex, data, timeout) if req_type == 'vendor': ftdi = dev_handle.device.ftdi return ftdi.control(dev_handle, bmRequestType, bRequest, wValue, wIndex, data, timeout) self.log.error('Unknown request') return 0 def bulk_write(self, dev_handle: VirtDeviceHandle, ep: int, intf: int, data: array, timeout: int) -> int: self.log.debug('> write h:%d ep:%0x02x if:%d, d:%s, to:%d', dev_handle.handle, ep, intf, hexlify(data).decode(), timeout) ftdi = dev_handle.device.ftdi return ftdi.write(dev_handle, ep, intf, data, timeout) def bulk_read(self, dev_handle: VirtDeviceHandle, ep: int, intf: int, buff: array, timeout: int) -> int: self.log.debug('> read h:%d ep:0x%02x if:%d, l:%d, to:%d', dev_handle.handle, ep, intf, len(buff), timeout) ftdi = dev_handle.device.ftdi return ftdi.read(dev_handle, ep, intf, buff, timeout) def _ctrl_standard(self, dev_handle: VirtDeviceHandle, bmRequestType: int, bRequest: int, wValue: int, wIndex: int, data: array, timeout: int) -> int: req_ctrl = USBCONST.dec_req_ctrl(bmRequestType) req_type = USBCONST.dec_req_type(bmRequestType) req_rcpt = USBCONST.dec_req_rcpt(bmRequestType) req_desc = ':'.join([req_ctrl, req_type, req_rcpt]) req_name = USBCONST.dec_req_name(bmRequestType, bRequest) dstr = (hexlify(data).decode() if USBCONST.is_req_out(bmRequestType) else f'({len(data)})') self.log.debug('> ctrl_transfer hdl %d, %s, %s, ' 'val 0x%04x, idx 0x%04x, data %s, to %d', dev_handle.handle, req_desc, req_name, wValue, wIndex, dstr, timeout) size = 0 if req_name == 'get_descriptor': desc_idx = wValue & 0xFF desc_type = wValue >> 8 self.log.debug(' %s: 0x%02x', USBCONST.dec_desc_type(desc_type), desc_idx) dev = dev_handle.device buf = dev.get_string(desc_type, desc_idx) size = len(buf) data[:size] = array('B', buf) else: self.log.warning('Unknown request') self.log.debug('< (%d) %s', size, hexlify(data[:size]).decode()) return size _VirtBackend = VirtBackend() """Unique instance of PyUSB virtual backend.""" def get_backend(*_): """PyUSB API implementation.""" return _VirtBackend pyftdi-0.54.0/pyftdi/tests/bits.py000077500000000000000000000225111421346242200170570ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2010-2016 Emmanuel Blot # Copyright (c) 2010-2016, Neotion # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause #pylint: disable-msg=broad-except import unittest from pyftdi.bits import BitSequence, BitZSequence, BitSequenceError class BitSequenceTestCase(unittest.TestCase): def setUp(self): self.bs1 = BitSequence(0x01, msb=True, length=8) self.bs2 = BitSequence(0x02, msb=True, length=8) self.bs3 = BitSequence(0x04, msb=True, length=7) self.bs4 = BitSequence(0x04, msb=True, length=11) self.bs5 = BitSequence(299999999999998) self.bs6 = BitSequence(299999999999999) self.bs7 = BitSequence(value='10010101011111') self.bzs1 = BitZSequence(0x01, msb=True, length=8) self.bzs2 = BitZSequence('0Z1') self.bzs3 = BitZSequence('0Z1', length=5) self.bzs4 = BitZSequence('0010ZZ010Z1Z11') self.bzs5 = BitZSequence(value=[True, False, None, False, False, True]) self.bzs6 = BitZSequence(value=[True, False, None, False, False, True], length=len(self.bzs4)) def test_bitwise_ops(self): self.assertEqual(int(BitSequence(0x01, length=8) | BitSequence(0x02, length=8)), 3) self.assertEqual(int(BitSequence(0x07, length=8) & BitSequence(0x02, length=8)), 2) self.assertEqual(int(BitZSequence(0x01, length=8) | BitSequence(0x02, length=8)), 3) self.assertEqual(int(BitSequence(0x07, length=8) & BitZSequence(0x02, length=8)), 2) self.assertRaises(BitSequenceError, BitZSequence.__or__, self.bzs4, self.bzs5) self.assertRaises(BitSequenceError, BitZSequence.__and__, self.bzs4, self.bzs5) self.assertEqual(repr(self.bzs6), '00000000100Z01') self.assertEqual(repr(self.bzs6 | self.bzs4), '11Z1Z010ZZ0Z01') self.assertEqual(repr(self.bzs6 & self.bzs4), '00Z0Z000ZZ0Z00') self.assertEqual(repr(self.bzs4 & self.bs7), '11Z1Z010ZZ0000') self.assertEqual(repr(self.bs7 & self.bzs4), '11Z1Z010ZZ0000') self.assertEqual(repr(self.bzs4 | self.bs7), '11Z1Z010ZZ1101') self.assertEqual(repr(self.bs7 | self.bzs4), '11Z1Z010ZZ1101') self.assertEqual(repr(self.bs7.invert()), '00000101010110') self.assertEqual(repr(self.bzs4.invert()), '00Z0Z101ZZ1011') self.assertLess(self.bs5, self.bs6) self.assertLessEqual(self.bs5, self.bs6) self.assertLess(self.bs6, self.bs5) self.assertLessEqual(self.bs6, self.bs5) def test_cmp(self): self.assertTrue(self.bs1 == self.bs1) self.assertTrue(self.bs1 != self.bs2) self.assertTrue(self.bs2 != BitSequence(0x02, msb=True, length=4)) self.assertTrue(self.bzs2 == self.bzs2) self.assertTrue(self.bzs1 != self.bzs2) self.assertTrue(self.bs1 == self.bzs1) self.assertTrue(self.bzs1 == self.bs1) self.assertTrue(self.bzs3 != self.bzs2) self.assertNotEqual(self.bzs4, self.bzs5) bzs = BitZSequence(self.bs7) self.assertTrue(bzs == self.bs7) bzs |= BitZSequence('00Z0Z000ZZ0Z00') self.assertFalse(bzs == self.bs7) self.assertTrue(bzs.matches(self.bs7)) def test_representation(self): self.assertEqual("%s / %r" % (self.bs1, self.bs1), "8: 10000000 / 10000000") self.assertEqual("%s / %r" % (self.bs2, self.bs2), "8: 01000000 / 01000000") self.assertEqual("%s / %r" % (self.bs3, self.bs3), "7: 0010000 / 0010000") self.assertEqual("%s / %r" % (self.bs4, self.bs4), "11: 001 00000000 / 00100000000") self.assertEqual("%s / %r" % (self.bs5, self.bs5), "49: 1 00010000 11011001 00110001 01101110 10111111 " "11111110 / 100010000110110010011000101101110101111" "1111111110") self.assertEqual("%s / %r" % (self.bs6, self.bs6), "49: 1 00010000 11011001 00110001 01101110 10111111 " "11111111 / 100010000110110010011000101101110101111" "1111111111") self.assertEqual(repr(self.bzs4), '11Z1Z010ZZ0100') self.assertEqual(repr(self.bzs5), '100Z01') def test_init(self): self.assertEqual(int(BitSequence([0, 0, 1, 0])), 4) self.assertEqual(int(BitSequence((0, 1, 0, 0), msb=True)), 4) self.assertEqual(int(BitSequence(4, length=8)), 4) self.assertEqual(int(BitSequence(int(4), msb=True, length=8)), 32) self.assertEqual(int(BitSequence("0010")), 4) self.assertEqual(int(BitSequence("0100", msb=True)), 4) bs = BitSequence("0100", msb=True) self.assertEqual(bs, BitSequence(bs)) bssub = BitSequence(bs[1:3]) self.assertEqual(str(bssub), '2: 10') bs[0:3] = '11' self.assertEqual(str(bs), '4: 0011') bzs = BitZSequence(self.bzs4) self.assertEqual(bzs, self.bzs4) bs = BitSequence('11111010101001', msb=True) bs[8:12] = BitSequence(value='0000') self.assertEqual(repr(bs), '11000010101001') try: bs[8:12] = BitZSequence(value='ZZZZ') except BitSequenceError: pass except Exception as e: self.fail("Unexpected exception %s" % e) else: self.fail("Error was expected") bs = BitZSequence('1111101010100111Z1Z010ZZ0100', msb=True) bs[8:12] = BitZSequence(value='ZZZZ') self.assertEqual(repr(bs), '1111101010100111ZZZZ10ZZ0100') bs[8:12] = BitSequence(value='0000') self.assertEqual(repr(bs), '1111101010100111000010ZZ0100') n = 548521358 bs = BitSequence(bin(n), msb=True) self.assertEqual(int(bs), n) bzs = BitZSequence(bin(n), msb=True) self.assertEqual(str(bzs), '30: 100000 10110001 11000101 10001110') bs = BitSequence(bytes_=[0x44, 0x66, 0xcc], msby=False) self.assertEqual(int(bs), 0x4466cc) bs = BitSequence(bytes_=(0x44, 0x66, 0xcc), msby=True) self.assertEqual(int(bs), 0xcc6644) try: bs = BitSequence(bytes_=[0x44, 0x666, 0xcc], msby=False) except BitSequenceError: pass except Exception as e: self.fail("Unexpected exception %s" % e) else: self.fail("Error was expected") def test_conversion(self): bs = BitSequence(0xCA, msb=True, length=8) self.assertEqual('%02x' % bs.tobyte(False), '53') self.assertEqual('%02x' % bs.tobyte(True), 'ca') self.assertEqual(bs, BitSequence(bs.tobyte(True), msb=True, length=8)) self.assertRaises(BitSequenceError, BitZSequence.__int__, self.bzs5) self.assertRaises(BitSequenceError, BitZSequence.tobyte, self.bzs5) self.assertRaises(BitSequenceError, BitZSequence.tobytes, self.bzs5) bzs = BitZSequence(0xaa) self.assertEqual(int(bzs), 0xaa) def test_misc(self): ba = BitSequence(12, msb=True, length=16) bb = BitSequence(12, msb=True, length=14) bl = [ba, bb] bl.sort(key=int) self.assertEqual(str(bl), "[00110000000000, 0011000000000000]") self.assertEqual(str(ba.tobytes()), "[48, 0]") self.assertEqual(str(ba.tobytes(True)), "[0, 12]") self.assertEqual(str(bb.tobytes(True)), "[0, 12]") b = BitSequence(length=254) b[0:4] = '1111' self.assertEqual( str(b), '254: 000000 00000000 00000000 00000000 ' '00000000 00000000 00000000 00000000 00000000 00000000 00000000 ' '00000000 00000000 00000000 00000000 00000000 00000000 00000000 ' '00000000 00000000 00000000 00000000 00000000 00000000 00000000 ' '00000000 00000000 00000000 00000000 00000000 00000000 00001111') self.assertEqual( str(b.tobytes()), '[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ' '0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15]') b = BitSequence(bytes_=[0xa0, '\x0f', 0x77], msb=False, msby=False) self.assertEqual(str(['%02x' % x for x in b.tobytes(False)]), "['a0', '0f', '77']") b = BitSequence(bytes_=[0xa0, '\x0f', 0x77], msb=True, msby=True) self.assertEqual(str(['%02x' % x for x in b.tobytes(True)]), "['a0', '0f', '77']") b = BitSequence(length=7) b[6] = '1' self.assertEqual(str(b), '7: 1000000') def test_rotations(self): b = BitSequence('10101110') b.lsr(2) self.assertEqual(str(b), '8: 01011101') b.lsr(10) self.assertEqual(str(b), '8: 01010111') b.rsr(3) self.assertEqual(str(b), '8: 10111010') def test_concat(self): self.assertEqual(repr(self.bzs4+self.bzs5), '100Z0111Z1Z010ZZ0100') self.assertEqual(repr(self.bzs4+self.bs7), '1111101010100111Z1Z010ZZ0100') self.assertEqual(repr(self.bs7+self.bzs4), '11Z1Z010ZZ010011111010101001') def suite(): return unittest.makeSuite(BitSequenceTestCase, 'test_') if __name__ == '__main__': unittest.main(defaultTest='suite') pyftdi-0.54.0/pyftdi/tests/cbus.py000077500000000000000000000075221421346242200170570ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2020, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import sys from doctest import testmod from os import environ from unittest import TestCase, TestSuite, makeSuite, main as ut_main from pyftdi.ftdi import Ftdi, FtdiError from pyftdi.eeprom import FtdiEeprom #pylint: disable-msg=empty-docstring #pylint: disable-msg=missing-docstring class CbusGpioTestCase(TestCase): """FTDI CBUS GPIO feature test case""" @classmethod def setUpClass(cls): """Default values""" cls.url = environ.get('FTDI_DEVICE', 'ftdi:///1') def test_output_gpio(self): """Simple test to demonstrate ouput bit-banging on CBUS. You need a CBUS-capable FTDI (FT232R/FT232H/FT230X/FT231X), whose EEPROM has been configured to support GPIOs on CBUS0 and CBUS3. Hard-wiring is required to run this test: * CBUS0 (output) should be connected to CTS (input) * CBUS3 (output) should be connected to DSR (input) """ ftdi = Ftdi() ftdi.open_from_url(self.url) # sanity check: device should support CBUS feature self.assertEqual(ftdi.has_cbus, True) eeprom = FtdiEeprom() eeprom.connect(ftdi) # sanity check: device should have been configured for CBUS GPIOs self.assertEqual(eeprom.cbus_mask & 0b1001, 0b1001) # configure CBUS0 and CBUS3 as output ftdi.set_cbus_direction(0b1001, 0b1001) # no input pin available self.assertRaises(FtdiError, ftdi.get_cbus_gpio) for cycle in range(40): value = cycle & 0x3 # CBUS0 and CBUS3 cbus = ((value & 0x2) << 2) | value & 0x1 # for now, need a digital/logic analyzer to validate output ftdi.set_cbus_gpio(cbus) # CBUS0 is connected to CTS, CBUS3 to DSR # need to inverse logical level as RS232 uses negative logic sig = int(not ftdi.get_cts()) | (int(not ftdi.get_dsr()) << 1) self.assertEqual(value, sig) def test_input_gpio(self): """Simple test to demonstrate input bit-banging on CBUS. You need a CBUS-capable FTDI (FT232R/FT232H/FT230X/FT231X), whose EEPROM has been configured to support GPIOs on CBUS0 and CBUS3. Hard-wiring is required to run this test: * CBUS0 (input) should be connected to RTS (output) * CBUS3 (input) should be connected to DTR (output) """ ftdi = Ftdi() ftdi.open_from_url(self.url) # sanity check: device should support CBUS feature self.assertEqual(ftdi.has_cbus, True) eeprom = FtdiEeprom() eeprom.connect(ftdi) # sanity check: device should have been configured for CBUS GPIOs self.assertEqual(eeprom.cbus_mask & 0b1001, 0b1001) # configure CBUS0 and CBUS3 as input ftdi.set_cbus_direction(0b1001, 0b0000) # no output pin available self.assertRaises(FtdiError, ftdi.set_cbus_gpio, 0) for cycle in range(40): rts = bool(cycle & 0x1) dtr = bool(cycle & 0x2) ftdi.set_rts(rts) ftdi.set_dtr(dtr) # need to inverse logical level as RS232 uses negative logic cbus = ~ftdi.get_cbus_gpio() sig = (cbus & 0x1) | ((cbus & 0x8) >> 2) value = cycle & 0x3 self.assertEqual(value, sig) def suite(): suite_ = TestSuite() # peak the test that matches your HW setup, see test doc for details # suite_.addTest(makeSuite(CbusGpioTestCase, 'test_output')) suite_.addTest(makeSuite(CbusGpioTestCase, 'test_input')) return suite_ def main(): testmod(sys.modules[__name__]) ut_main(defaultTest='suite') if __name__ == '__main__': main() pyftdi-0.54.0/pyftdi/tests/eeprom.py000077500000000000000000000103161421346242200174050ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2018, Stephen Goadhouse # Copyright (c) 2019, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import logging import unittest from doctest import testmod from os import environ from sys import modules, stdout from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi from pyftdi.misc import hexdump, to_bool #pylint: disable-msg=missing-docstring class EepromTestCase(unittest.TestCase): """FTDI EEPROM access method test case""" @classmethod def setUpClass(cls): """Default values""" cls.eeprom_size = int(environ.get('FTDI_EEPROM_SIZE', '256')) cls.url = environ.get('FTDI_DEVICE', 'ftdi://ftdi:2232h/1') def setUp(self): """Open a connection to the FTDI, defining which pins are configured as output and input""" # out_pins value of 0x00 means all inputs out_pins = 0x00 try: ftdi = Ftdi() # If you REALLY muck things up, need to use this open_bitbang() # function directly and enter vendor and product ID: # ftdi.open_bitbang(vendor=0x0403, product=0x6011, # direction=out_pins) ftdi.open_bitbang_from_url(self.url, direction=out_pins) self.ftdi = ftdi except IOError as exc: raise IOError('Unable to open USB port: %s' % str(exc)) from exc def tearDown(self): """Close the FTDI connection""" self.ftdi.close() def test_eeprom_read(self): """Simple test to demonstrate EEPROM read out. """ ref_data = self.ftdi.read_eeprom(eeprom_size=self.eeprom_size) print(hexdump(ref_data)) # check that the right number of bytes were read self.assertEqual(len(ref_data), self.eeprom_size) # Pull out actual checksum from EEPROM data ck_act = (ref_data[-1] << 8) | ref_data[-2] # compute expected checksum value over the EEPROM contents, except # the EEPROM word ck_expo = self.ftdi.calc_eeprom_checksum(ref_data[:-2]) self.assertEqual(ck_act, ck_expo) maxsize = self.eeprom_size # verify access to various data segments segments = ((1, 2), (1, 3), (2, 4), (2, 5), (maxsize-8, 8), (maxsize-3, 3), (0, maxsize)) for start, size in segments: chunk = self.ftdi.read_eeprom(start, size, self.eeprom_size) self.assertEqual(len(chunk), size) self.assertEqual(chunk, ref_data[start:start+size]) # verify reject access to various invalid data segments segments = (-1, 2), (0, maxsize+1), (maxsize-6, maxsize+1) for start, size in segments: self.assertRaises(ValueError, self.ftdi.read_eeprom, start, size) def test_eeprom_write_reject(self): """Simple test to demonstrate rejection of invalid EEPROM write requests. """ ref_data = self.ftdi.read_eeprom(eeprom_size=self.eeprom_size) # check that the right number of bytes were read self.assertEqual(len(ref_data), self.eeprom_size) # verify reject access to various invalid data segments segments = (-1, 2), (0, 257), (250, 7) for start, size in segments: self.assertRaises(ValueError, self.ftdi.write_eeprom, start, [0] * size, self.eeprom_size) def test_eeprom_write(self): """Simple test to demonstrate EEPROM write requests. """ self.ftdi.write_eeprom(0x80, b'test', eeprom_size=self.eeprom_size) def suite(): suite_ = unittest.TestSuite() suite_.addTest(unittest.makeSuite(EepromTestCase, 'test')) return suite_ def main(): if to_bool(environ.get('FTDI_DEBUG', 'off')): FtdiLogger.log.addHandler(logging.StreamHandler(stdout)) level = environ.get('FTDI_LOGLEVEL', 'info').upper() try: loglevel = getattr(logging, level) except AttributeError as exc: raise ValueError('Invalid log level: %s' % level) from exc FtdiLogger.set_level(loglevel) testmod(modules[__name__]) unittest.main(defaultTest='suite') if __name__ == '__main__': main() pyftdi-0.54.0/pyftdi/tests/eeprom_mock.py000066400000000000000000000244271421346242200204230ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright (c) 2019-2021, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause # # mock eeprom tests that can be run in CI without a device connected import logging from os import environ from sys import modules, stdout from unittest import TestCase, TestSuite, SkipTest, makeSuite, main as ut_main from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi from pyftdi.eeprom import FtdiEeprom from pyftdi.misc import to_bool, hexdump from pyftdi.ftdi import FtdiError VirtLoader = None class FtdiTestCase(TestCase): """Common features for all tests. """ @classmethod def setUpClass(cls): cls.debug = to_bool(environ.get('FTDI_DEBUG', 'off'), permissive=False) cls.url = environ.get('FTDI_DEVICE', 'ftdi:///1') cls.loader = None @classmethod def tearDownClass(cls): if cls.loader: cls.loader.unload() def setUp(self): pass class EepromMirrorTestCase(FtdiTestCase): """Test FTDI EEPROM mirror feature (duplicate eeprom data over 2 eeprom sectors). Generally this is tested with a virtual eeprom (by setting environment variable FTDI_VIRTUAL=on), however you may also test with an actual device at your own risk. Note that none of the tests should commit any of their eeprom changes """ # manufacturer name string to use in tests TEST_MANU_NAME = "MNAME" TEST_PROD_NAME = "PNAME" TEST_SN = "SN123" @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() if VirtLoader: cls.loader = VirtLoader() with open(cls.TEST_CONFIG_FILENAME, 'rb') as yfp: cls.loader.load(yfp) if cls.url == 'ftdi:///1': ftdi = Ftdi() ftdi.open_from_url(cls.url) count = ftdi.device_port_count ftdi.close() def test_mirror_properties(self): """Check FtdiEeprom properties are accurate for a device that can mirror """ # properties should work regardless of if the mirror option is set # or not eeprom = FtdiEeprom() eeprom.open(self.url, ignore=True) self.assertTrue(eeprom.has_mirroring) self.assertEqual(eeprom.size // 2, eeprom.mirror_sector) eeprom.close() mirrored_eeprom = FtdiEeprom() mirrored_eeprom.enable_mirroring(True) mirrored_eeprom.open(self.url, ignore=True) self.assertTrue(mirrored_eeprom.has_mirroring) self.assertEqual(mirrored_eeprom.size // 2, mirrored_eeprom.mirror_sector) mirrored_eeprom.close() def test_mirror_manufacturer(self): """Verify manufacturer string is properly duplicated across the 2 eeprom sectors """ eeprom = FtdiEeprom() eeprom.enable_mirroring(True) eeprom.open(self.url, ignore=True) eeprom.erase() eeprom.set_manufacturer_name(self.TEST_MANU_NAME) self._check_for_mirrored_eeprom_contents(eeprom) def test_mirror_product(self): """Verify product string is properly duplicated across the 2 eeprom sectors """ eeprom = FtdiEeprom() eeprom.enable_mirroring(True) eeprom.open(self.url, ignore=True) eeprom.erase() eeprom.set_product_name(self.TEST_PROD_NAME) self._check_for_mirrored_eeprom_contents(eeprom) def test_mirror_serial(self): """Verify serial string is properly duplicated across the 2 eeprom sectors """ eeprom = FtdiEeprom() eeprom.enable_mirroring(True) eeprom.open(self.url, ignore=True) eeprom.erase() eeprom.set_serial_number(self.TEST_SN) self._check_for_mirrored_eeprom_contents(eeprom) def test_varstr_combinations(self): """Verify various combinations of var strings are properly duplicated across the 2 eeprom sectors """ eeprom = FtdiEeprom() eeprom.enable_mirroring(True) eeprom.open(self.url, ignore=True) # manu + prod str eeprom.erase() eeprom.set_manufacturer_name(self.TEST_MANU_NAME) eeprom.set_product_name(self.TEST_PROD_NAME) self._check_for_mirrored_eeprom_contents(eeprom) # manu + sn str eeprom.erase() eeprom.set_manufacturer_name(self.TEST_MANU_NAME) eeprom.set_serial_number(self.TEST_SN) self._check_for_mirrored_eeprom_contents(eeprom) # prod + sn str eeprom.erase() eeprom.set_manufacturer_name(self.TEST_PROD_NAME) eeprom.set_serial_number(self.TEST_SN) self._check_for_mirrored_eeprom_contents(eeprom) # manu + prod + sn str eeprom.erase() eeprom.set_manufacturer_name(self.TEST_MANU_NAME) eeprom.set_manufacturer_name(self.TEST_PROD_NAME) eeprom.set_serial_number(self.TEST_SN) self._check_for_mirrored_eeprom_contents(eeprom) def test_compute_size_detects_mirror(self): """Verify the eeproms internal _compute_size method returns the correct bool value when it detects an eeprom mirror """ eeprom = FtdiEeprom() eeprom.open(self.url, ignore=True) _, mirrored = eeprom._compute_size([]) self.assertFalse(mirrored) test_buf = bytearray(eeprom.size) sector_mid = eeprom.size // 2 for ii in range(sector_mid): test_buf[ii] = ii % 255 test_buf[sector_mid+ii] = test_buf[ii] _, mirrored = eeprom._compute_size(bytes(test_buf)) self.assertTrue(mirrored) # change one byte and confirm failure test_buf[eeprom.size - 2] = test_buf[eeprom.size - 2] - 1 _, mirrored = eeprom._compute_size(bytes(test_buf)) self.assertFalse(mirrored) def _check_for_mirrored_eeprom_contents(self, eeprom: FtdiEeprom): """Check that contents of the eeprom is identical over the two sectors """ sector_size = eeprom.size // 2 for ii in range(0, sector_size): self.assertEqual(eeprom.data[ii], eeprom.data[ii + eeprom.mirror_sector]) class EepromMirrorFt232hTestCase(EepromMirrorTestCase): TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft232h.yaml' class EepromMirrorFt2232hTestCase(EepromMirrorTestCase): TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft2232h.yaml' class EepromMirrorFt230xTestCase(FtdiTestCase): """Test FTDI eeprom with non-mirroring capabilities to ensure it works as expected. """ TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft230x.yaml' @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() if VirtLoader: cls.loader = VirtLoader() with open(cls.TEST_CONFIG_FILENAME, 'rb') as yfp: cls.loader.load(yfp) if cls.url == 'ftdi:///1': ftdi = Ftdi() ftdi.open_from_url(cls.url) count = ftdi.device_port_count ftdi.close() def test_mirror_properties(self): """Check FtdiEeprom properties are accurate for a device that can not mirror. """ # properties should work regardless of if the mirror option is set # or not eeprom = FtdiEeprom() eeprom.open(self.url, ignore=True) self.assertFalse(eeprom.has_mirroring) with self.assertRaises(FtdiError): eeprom.mirror_sector eeprom.close() # even if mirroring is enabled, should still stay false mirrored_eeprom = FtdiEeprom() mirrored_eeprom.enable_mirroring(True) mirrored_eeprom.open(self.url, ignore=True) self.assertFalse(mirrored_eeprom.has_mirroring) with self.assertRaises(FtdiError): eeprom.mirror_sector mirrored_eeprom.close() def test_compute_size_does_not_mirror(self): """Verify the eeproms internal _compute_size method returns the correct bool value when it detects no mirroring. """ eeprom = FtdiEeprom() eeprom.open(self.url, ignore=True) _, mirrored = eeprom._compute_size([]) self.assertFalse(mirrored) eeprom.close() eeprom = FtdiEeprom() eeprom.open(self.url, ignore=False) _, mirrored = eeprom._compute_size([]) self.assertFalse(mirrored) eeprom.close() def suite(): suite_ = TestSuite() suite_.addTest(makeSuite(EepromMirrorFt232hTestCase, 'test')) suite_.addTest(makeSuite(EepromMirrorFt2232hTestCase, 'test')) suite_.addTest(makeSuite(EepromMirrorFt230xTestCase, 'test')) return suite_ def virtualize(): if not to_bool(environ.get('FTDI_VIRTUAL', 'off')): return from pyftdi.usbtools import UsbTools # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('backend.usbvirt', ) # Ensure the virtual backend can be found and is loaded backend = UsbTools.find_backend() try: # obtain the loader class associated with the virtual backend global VirtLoader VirtLoader = backend.create_loader() except AttributeError: raise AssertionError('Cannot load virtual USB backend') def main(): import doctest doctest.testmod(modules[__name__]) debug = to_bool(environ.get('FTDI_DEBUG', 'off')) if debug: formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(levelname)-7s' ' %(name)-20s [%(lineno)4d] %(message)s', '%H:%M:%S') else: formatter = logging.Formatter('%(message)s') level = environ.get('FTDI_LOGLEVEL', 'warning').upper() try: loglevel = getattr(logging, level) except AttributeError: raise ValueError(f'Invalid log level: {level}') FtdiLogger.log.addHandler(logging.StreamHandler(stdout)) FtdiLogger.set_level(loglevel) FtdiLogger.set_formatter(formatter) virtualize() try: ut_main(defaultTest='suite') except KeyboardInterrupt: pass if __name__ == '__main__': # Useful environment variables: # FTDI_DEVICE: a specific FTDI URL, default to ftdi:///1 # FTDI_LOGLEVEL: a Logger debug level, to define log verbosity # FTDI_DEBUG: to enable/disable debug mode # FTDI_VIRTUAL: to use a virtual device rather than a physical device main() pyftdi-0.54.0/pyftdi/tests/ftdi.py000077500000000000000000000140071421346242200170450ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2010-2020, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import logging from doctest import testmod from os import environ from sys import modules, stdout from time import sleep, time as now from unittest import TestCase, TestSuite, SkipTest, makeSuite, main as ut_main from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi, FtdiError from pyftdi.usbtools import UsbTools, UsbToolsError class FtdiTestCase(TestCase): """FTDI driver test case""" def test_multiple_interface(self): # the following calls used to create issues (several interfaces from # the same device). The test expects an FTDI 2232H here ftdi1 = Ftdi() ftdi1.open(vendor=0x403, product=0x6010, interface=1) ftdi2 = Ftdi() ftdi2.open(vendor=0x403, product=0x6010, interface=2) for _ in range(5): print("If#1: ", hex(ftdi1.poll_modem_status())) print("If#2: ", ftdi2.modem_status()) sleep(0.500) ftdi1.close() ftdi2.close() class HotplugTestCase(TestCase): def test_hotplug_discovery(self): """Demonstrate how to connect to an hotplugged FTDI device, i.e. an FTDI device that is connected after the initial attempt to enumerate it on the USB bus.""" url = environ.get('FTDI_DEVICE', 'ftdi:///1') ftdi = Ftdi() timeout = now() + 5.0 # sanity check: bail out after 10 seconds while now() < timeout: try: ftdi.open_from_url(url) break except UsbToolsError: UsbTools.flush_cache() sleep(0.05) continue self.assertTrue(ftdi.is_connected, 'Unable to connect to FTDI') print('Connected to FTDI', url) class ResetTestCase(TestCase): def test_simple_reset(self): """Demonstrate how to connect to an hotplugged FTDI device, i.e. an FTDI device that is connected after the initial attempt to enumerate it on the USB bus.""" url = environ.get('FTDI_DEVICE', 'ftdi:///1') ftdi = Ftdi() ftdi.open_from_url(url) self.assertTrue(ftdi.is_connected, 'Unable to connect to FTDI') ftdi.close() self.assertFalse(ftdi.is_connected, 'Unable to close connection') ftdi.open_from_url(url) self.assertTrue(ftdi.is_connected, 'Unable to connect to FTDI') ftdi.reset(False) def test_dual_if_reset(self): """Demonstrate how to connect to an hotplugged FTDI device, i.e. an FTDI device that is connected after the initial attempt to enumerate it on the USB bus.""" url1 = environ.get('FTDI_DEVICE', 'ftdi:///1') ftdi1 = Ftdi() ftdi1.open_from_url(url1) count = ftdi1.device_port_count if count < 2: ftdi1.close() raise SkipTest('FTDI device is not a multi-port device') next_port = (int(url1[-1]) % count) + 1 url2 = 'ftdi:///%d' % next_port ftdi2 = Ftdi() self.assertTrue(ftdi1.is_connected, 'Unable to connect to FTDI') ftdi2.open_from_url(url2) # use latenty setting to set/test configuration is preserved ftdi2.set_latency_timer(128) # should be the same value self.assertEqual(ftdi2.get_latency_timer(), 128) self.assertTrue(ftdi2.is_connected, 'Unable to connect to FTDI') ftdi1.close() self.assertFalse(ftdi1.is_connected, 'Unable to close connection') # closing first connection should not alter second interface self.assertEqual(ftdi2.get_latency_timer(), 128) ftdi1.open_from_url(url1) self.assertTrue(ftdi1.is_connected, 'Unable to connect to FTDI') # a FTDI reset should not alter settings... ftdi1.reset(False) self.assertEqual(ftdi2.get_latency_timer(), 128) # ... however performing a USB reset through any interface should alter # any previous settings made to all interfaces ftdi1.reset(True) self.assertNotEqual(ftdi2.get_latency_timer(), 128) class DisconnectTestCase(TestCase): """This test requires user interaction to unplug/plug back the device. """ def test_close_on_disconnect(self): """Validate close after disconnect.""" log = logging.getLogger('pyftdi.tests.ftdi') url = environ.get('FTDI_DEVICE', 'ftdi:///1') ftdi = Ftdi() ftdi.open_from_url(url) self.assertTrue(ftdi.is_connected, 'Unable to connect to FTDI') print('Please disconnect FTDI device') while ftdi.is_connected: try: ftdi.poll_modem_status() except FtdiError: break sleep(0.1) ftdi.close() print('Please reconnect FTDI device') while True: UsbTools.flush_cache() try: ftdi.open_from_url(url) except (FtdiError, UsbToolsError): log.debug('FTDI device not detected') sleep(0.1) except ValueError: log.warning('FTDI device not initialized') ftdi.close() sleep(0.1) else: log.info('FTDI device detected') break ftdi.poll_modem_status() ftdi.close() def suite(): suite_ = TestSuite() #suite_.addTest(makeSuite(FtdiTestCase, 'test')) #suite_.addTest(makeSuite(HotplugTestCase, 'test')) suite_.addTest(makeSuite(ResetTestCase, 'test')) suite_.addTest(makeSuite(DisconnectTestCase, 'test')) return suite_ if __name__ == '__main__': testmod(modules[__name__]) FtdiLogger.log.addHandler(logging.StreamHandler(stdout)) level = environ.get('FTDI_LOGLEVEL', 'info').upper() try: loglevel = getattr(logging, level) except AttributeError as exc: raise ValueError(f'Invalid log level: {level}') from exc FtdiLogger.set_level(loglevel) ut_main(defaultTest='suite') pyftdi-0.54.0/pyftdi/tests/gpio.py000077500000000000000000000640731421346242200170650ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2016-2020, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause #pylint: disable-msg=empty-docstring #pylint: disable-msg=missing-docstring #pylint: disable-msg=invalid-name #pylint: disable-msg=global-statement import logging from collections import deque from os import environ from sys import modules, stdout from time import sleep from unittest import TestCase, TestSuite, SkipTest, makeSuite, main as ut_main from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi from pyftdi.gpio import (GpioAsyncController, GpioSyncController, GpioMpsseController) from pyftdi.misc import to_bool # When USB virtualization is enabled, this loader is instanciated VirtLoader = None class FtdiTestCase(TestCase): """Common features for all tests. """ @classmethod def setUpClass(cls): cls.debug = to_bool(environ.get('FTDI_DEBUG', 'off'), permissive=False) cls.url = environ.get('FTDI_DEVICE', 'ftdi:///1') cls.loader = None @classmethod def tearDownClass(cls): if cls.loader: cls.loader.unload() def setUp(self): if self.debug: print('.'.join(self.id().split('.')[-2:])) class GpioAsyncTestCase(FtdiTestCase): """FTDI Asynchronous GPIO driver test case. Please ensure that the HW you connect to the FTDI port A does match the encoded configuration. Check your HW setup before running this test as it might damage your HW. You've been warned. Low nibble is used as input, high nibble is used as output. They should be interconnected as follow: * b0 should be connected to b4 * b1 should be connected to b5 * b2 should be connected to b6 * b3 should be connected to b7 Note that this test cannot work with LC231X board, as FTDI is stupid enough to add ubidirectionnal output buffer on DTR and RTS, so both nibbles have at lest on output pin... """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() if VirtLoader: cls.loader = VirtLoader() with open('pyftdi/tests/resources/ft232r.yaml', 'rb') as yfp: cls.loader.load(yfp) vftdi = cls.loader.get_virtual_ftdi(1, 1) vport = vftdi.get_port(1) # create virtual connections as real HW in_pins = [vport[pos] for pos in range(4)] out_pins = [vport[pos] for pos in range(4, 8)] for in_pin, out_pin in zip(in_pins, out_pins): out_pin.connect_to(in_pin) if cls.url == 'ftdi:///1': # assumes that if not specific device is used, and a multiport # device is connected, there is no loopback wires between pins of # the same port. This hack allows to run the same test with a # FT232H, then a FT2232H for ex, to that with two test sessions # the whole test set is run. If a specific device is selected # assume the HW always match the expected configuration. ftdi = Ftdi() ftdi.open_from_url(cls.url) count = ftdi.device_port_count ftdi.close() cls.skip_loopback = count > 1 else: cls.skip_loopback = False def test_gpio_freeze(self): """Simple test to demonstrate freeze on close. For now, it requires a logic analyzer to verify the output, this is not automatically validated by SW """ direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio = GpioAsyncController() gpio.configure(self.url, direction=direction, frequency=1e3, initial=0x0) port = gpio.get_gpio() # emit a sequence as a visual marker on b3,b2,b1,b0 port.write([x<<4 for x in range(16)]) sleep(0.01) # write 0b0110 to the port port.write(0x6<<4) sleep(0.001) # close w/o freeze: all the outputs should be reset (usually 0b1111) # it might need pull up (or pull down) to observe the change as # output are not high-Z. gpio.close() sleep(0.01) gpio.configure(self.url, direction=direction, frequency=1e3, initial=0x0) port = gpio.get_gpio() # emit a sequence as a visual marker with on b3 and b1 port.write([(x<<4)&0x90 for x in range(16)]) sleep(0.01) # write 0b0110 to the port port.write(0x6<<4) sleep(0.01) # close w/ freeze: outputs should not be reset (usually 0b0110) gpio.close(True) def test_gpio_values(self): """Simple test to demonstrate bit-banging. """ if self.skip_loopback: raise SkipTest('Skip loopback test on multiport device') direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio = GpioAsyncController() gpio.configure(self.url, direction=direction, frequency=1e6, initial=0x0) port = gpio.get_gpio() # useless, for API duck typing # legacy API: peek mode, 1 byte ingress = port.read() self.assertIsInstance(ingress, int) # peek mode always gives a single byte output ingress = port.read(peek=True) self.assertIsInstance(ingress, int) # stream mode always gives a bytes buffer port.write([0xaa for _ in range(256)]) ingress = port.read(100, peek=False, noflush=False) self.assertIsInstance(ingress, bytes) if not VirtLoader: # the virtual task may sometimes not be triggered soon enough self.assertGreater(len(ingress), 2) # direct mode is not available with multi-byte mode self.assertRaises(ValueError, port.read, 3, True) ingress = port.read(3) self.assertIsInstance(ingress, bytes) if not VirtLoader: # the virtual task may sometimes not be triggered soon enough self.assertGreater(len(ingress), 0) self.assertLessEqual(len(ingress), 3) port.write(0x00) port.write(0xFF) # only 8 bit values are accepted self.assertRaises(ValueError, port.write, 0x100) port.write([0x00, 0xFF, 0x00]) port.write(bytes([0x00, 0xFF, 0x00])) # only 8 bit values are accepted self.assertRaises(ValueError, port.write, [0x00, 0x100, 0x00]) # check direction API port.set_direction(0xFF, 0xFF & ~direction) gpio.close() def test_gpio_initial(self): """Check initial values. """ if self.skip_loopback: raise SkipTest('Skip initial test on multiport device') if not self.loader: raise SkipTest('Skip initial test on physical device') direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In vftdi = self.loader.get_virtual_ftdi(1, 1) vport = vftdi.get_port(1) gpio = GpioAsyncController() for initial in (0xaf, 0xf0, 0x13, 0x00): gpio.configure(self.url, direction=direction, frequency=1e6, initial=initial) expect = (initial & 0xF0) | (initial >> 4) self.assertEqual(vport.gpio, expect) gpio.close() def test_gpio_loopback(self): """Check I/O. """ if self.skip_loopback: raise SkipTest('Skip loopback test on multiport device') gpio = GpioAsyncController() direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio.configure(self.url, direction=direction, frequency=800000) for out in range(16): # print(f'Write {out:04b} -> {out << 4:08b}') gpio.write(out << 4) fback = gpio.read() lsbs = fback & ~direction msbs = fback >> 4 # check inputs match outputs self.assertEqual(lsbs, out) # check level of outputs match the ones written self.assertEqual(msbs, out) outs = list([(out & 0xf) << 4 for out in range(1000)]) gpio.write(outs) gpio.ftdi.read_data(512) for _ in range(len(outs)): _ = gpio.read(14) last = outs[-1] >> 4 for _ in range(10): fbacks = gpio.read(1000) for fback in fbacks: lsbs = fback & ~direction msbs = fback >> 4 # check inputs match last output self.assertEqual(lsbs, last) # check level of output match the last written self.assertEqual(msbs, last) gpio.close() def test_gpio_baudate(self): # this test requires an external device (logic analyser or scope) to # check the bitbang read and bitbang write signal (BB_RD, BB_WR) and # mesure their frequency. The EEPROM should be configured to enable # those signal on some of the CBUS pins, for example. gpio = GpioAsyncController() direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio.configure(self.url, direction=direction) buf = bytes([0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00]) freqs = [50e3, 200e3, 1e6, 3e6] if gpio.ftdi.is_H_series: freqs.extend([6e6, 10e6, 12e6]) gpio.read(128) for freq in freqs: # set the bitbang refresh rate gpio.set_frequency(freq) self.assertEqual(gpio.frequency, freq) # be sure to leave enough time to purge buffers (HW FIFO) or # the frequency changes occur on the current buffer... gpio.write(buf) gpio.read(128) sleep(0.01) gpio.close() class GpioSyncTestCase(FtdiTestCase): """FTDI Synchrnous GPIO driver test case. Please ensure that the HW you connect to the FTDI port A does match the encoded configuration. Check your HW setup before running this test as it might damage your HW. You've been warned. Low nibble is used as input, high nibble is used as output. They should be interconnected as follow: * b0 should be connected to b4 * b1 should be connected to b5 * b2 should be connected to b6 * b3 should be connected to b7 Note that this test cannot work with LC231X board, as FTDI is stupid enough to add ubidirectionnal output buffer on DTR and RTS, so both nibbles have at lest on output pin... """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() if VirtLoader: cls.loader = VirtLoader() with open('pyftdi/tests/resources/ft232r.yaml', 'rb') as yfp: cls.loader.load(yfp) vftdi = cls.loader.get_virtual_ftdi(1, 1) vport = vftdi.get_port(1) # create virtual connections as real HW in_pins = [vport[pos] for pos in range(4)] out_pins = [vport[pos] for pos in range(4, 8)] for in_pin, out_pin in zip(in_pins, out_pins): out_pin.connect_to(in_pin) if cls.url == 'ftdi:///1': # assumes that if not specific device is used, and a multiport # device is connected, there is no loopback wires between pins of # the same port. This hack allows to run the same test with a # FT232H, then a FT2232H for ex, to that with two test sessions # the whole test set is run. If a specific device is selected # assume the HW always match the expected configuration. ftdi = Ftdi() ftdi.open_from_url(cls.url) count = ftdi.device_port_count ftdi.close() cls.skip_loopback = count > 1 else: cls.skip_loopback = False def test_gpio_values(self): """Simple test to demonstrate bit-banging. """ if self.skip_loopback: raise SkipTest('Skip loopback test on multiport device') direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio = GpioSyncController() gpio.configure(self.url, direction=direction, initial=0xee) outs = bytes([(out & 0xf)<<4 for out in range(1000)]) ins = gpio.exchange(outs) exp_in_count = min(len(outs), gpio.ftdi.fifo_sizes[0]) self.assertEqual(len(ins), exp_in_count) last = None for sout, sin in zip(outs, ins): if last is not None: # output nibble sin_out = sin >> 4 # input nibble sin_in = sin & 0xF # check inputs match last output self.assertEqual(sin_out, last) # check level of output match the last written self.assertEqual(sin_in, last) # an IN sample if captured on the next clock of the OUT sample # keep the MSB nibble, i.e. the nibble configured as output last = sout >> 4 gpio.close() def test_gpio_baudate(self): # this test requires an external device (logic analyser or scope) to # check the bitbang read and bitbang write signal (BB_RD, BB_WR) and # mesure their frequency. The EEPROM should be configured to enable # those signal on some of the CBUS pins, for example. gpio = GpioSyncController() direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio.configure(self.url, direction=direction) buf = bytes([0xf0, 0x00] * 64) freqs = [50e3, 200e3, 1e6, 3e6] if gpio.ftdi.is_H_series: freqs.extend([6e6, 10e6, 12e6]) for freq in freqs: # set the bitbang refresh rate gpio.set_frequency(freq) self.assertEqual(gpio.frequency, freq) # be sure to leave enough time to purge buffers (HW FIFO) or # the frequency changes occur on the current buffer... gpio.exchange(buf) sleep(0.01) gpio.close() class GpioMultiportTestCase(FtdiTestCase): """FTDI GPIO test for multi-port FTDI devices, i.e. FT2232H/FT4232H. Please ensure that the HW you connect to the FTDI port A does match the encoded configuration. Check your HW setup before running this test as it might damage your HW. You've been warned. First port is used as input, second port is used as output. They should be interconnected as follow: * AD0 should be connected to BD0 * AD1 should be connected to BD1 * AD2 should be connected to BD2 * AD3 should be connected to BD3 * AD0 should be connected to BD0 * AD1 should be connected to BD1 * AD2 should be connected to BD2 * AD3 should be connected to BD3 """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() if VirtLoader: cls.loader = VirtLoader() with open('pyftdi/tests/resources/ft2232h.yaml', 'rb') as yfp: cls.loader.load(yfp) vftdi = cls.loader.get_virtual_ftdi(1, 1) vport1 = vftdi.get_port(1) vport2 = vftdi.get_port(2) # create virtual connections as real HW in_pins = [vport1[pos] for pos in range(8)] out_pins = [vport2[pos] for pos in range(8)] for in_pin, out_pin in zip(in_pins, out_pins): out_pin.connect_to(in_pin) ftdi = Ftdi() ftdi.open_from_url(cls.url) count = ftdi.device_port_count pos = ftdi.port_index ftdi.close() if pos != 1: raise ValueError("FTDI interface should be the device's first") if count < 2: raise SkipTest('FTDI device is not a multi-port device') url = cls.url[:-1] cls.urls = [f'{url}1', f'{url}2'] def test_gpio_peek(self): """Check I/O. """ gpio_in, gpio_out = GpioAsyncController(), GpioAsyncController() gpio_in.configure(self.urls[0], direction=0x00, frequency=1e6) gpio_out.configure(self.urls[1], direction=0xFF, frequency=1e6) for out in range(256): gpio_out.write(out) outv = gpio_out.read() inv = gpio_in.read() # check inputs match outputs self.assertEqual(inv, out) # check level of outputs match the ones written self.assertEqual(outv, out) gpio_in.close() gpio_out.close() def test_gpio_stream(self): """Check I/O streaming """ if VirtLoader: # this would require to synchronize virtual clock between all ports # which is not supported by the virtual framework raise SkipTest('Skip gpio stream with virtual device') gpio_in, gpio_out = GpioAsyncController(), GpioAsyncController() gpio_in.configure(self.urls[0], direction=0x00, frequency=1e4) gpio_out.configure(self.urls[1], direction=0xFF, frequency=1e4) outs = bytes(range(256)) gpio_out.write(outs) # read @ same speed (and same clock source, so no jitter), flushing # the byffer which has been filled since the port has been opened ins = gpio_in.read(len(outs)) qout = deque(outs) ifirst = ins[0] # the inout stream should be a copy of the output stream, minus a # couple of missing samples that did not get captured while output # was streaming but read command has not been yet received. while qout: if qout[0] == ifirst: break qout.popleft() # offset is the count of missed bytes offset = len(ins)-len(qout) self.assertGreater(offset, 0) # no more output than input self.assertLess(offset, 16) # seems to be in the 6..12 range # print('Offset', offset) # check that the remaining sequence match for sout, sin in zip(qout, ins): #print(f'{sout:08b} --> {sin:08b}') # check inputs match outputs self.assertEqual(sout, sin) gpio_in.close() gpio_out.close() def test_direction(self): b5 = 1 << 5 for controller in (GpioAsyncController, GpioSyncController, GpioMpsseController): gpio_in, gpio_out = controller(), controller() gpio_in.configure(self.urls[0], direction=0x00, frequency=1e6, debug=self.debug) gpio_out.configure(self.urls[1], direction=0xFF, frequency=1e6, debug=self.debug) for direction in None, 0xFF, b5, 0xF0: if direction is not None: gpio_out.set_direction(0xFF, direction) for out in 0 << 5, 1 << 5: if controller != GpioSyncController: gpio_out.write(out) outp = gpio_out.read(1) inp = gpio_in.read(1) if controller == GpioMpsseController: outp = outp[0] inp = inp[0] else: # write twice the output value, only the second value # matters (see Sync bitbang for details) outp = gpio_out.exchange(bytes([out, out]))[1] # write anything as we just need the input value which # is sampled on each write inp = gpio_in.exchange(b'\x00')[0] self.assertEqual(outp & b5, out) self.assertEqual(inp & b5, out) class GpioMpsseTestCase(FtdiTestCase): """FTDI GPIO test for 16-bit port FTDI devices, i.e. FT2232H. Please ensure that the HW you connect to the FTDI port A does match the encoded configuration. Check your HW setup before running this test as it might damage your HW. You've been warned. First port is used as input, second port is used as output. They should be interconnected as follow: * AD0 should be connected to BD0 * AD1 should be connected to BD1 * AD2 should be connected to BD2 * AD3 should be connected to BD3 * AD0 should be connected to BD0 * AD1 should be connected to BD1 * AD2 should be connected to BD2 * AD3 should be connected to BD3 * AC0 should be connected to BC0 * AC1 should be connected to BC1 * AC2 should be connected to BC2 * AC3 should be connected to BC3 * AC0 should be connected to BC0 * AC1 should be connected to BC1 * AC2 should be connected to BC2 * AC3 should be connected to BC3 """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() if VirtLoader: cls.loader = VirtLoader() with open('pyftdi/tests/resources/ft2232h.yaml', 'rb') as yfp: cls.loader.load(yfp) vftdi = cls.loader.get_virtual_ftdi(1, 1) vport1 = vftdi.get_port(1) vport2 = vftdi.get_port(2) # create virtual connections as real HW in_pins = [vport1[pos] for pos in range(16)] out_pins = [vport2[pos] for pos in range(16)] for in_pin, out_pin in zip(in_pins, out_pins): out_pin.connect_to(in_pin) # prevent from using the tracer twice (Ftdi & VirtualFtdi) cls.debug_mpsse = False else: cls.debug_mpsse = cls.debug url = environ.get('FTDI_DEVICE', 'ftdi:///1') ftdi = Ftdi() ftdi.open_from_url(url) count = ftdi.device_port_count width = ftdi.port_width ftdi.close() if count < 2: raise SkipTest('FTDI device is not a multi-port device') if width < 2: raise SkipTest('FTDI device does not support wide ports') url = url[:-1] cls.urls = [f'{url}1', f'{url}2'] def test_default_gpio(self): """Check I/O. """ gpio_in, gpio_out = GpioMpsseController(), GpioMpsseController() gpio_in.configure(self.urls[0], direction=0x0000, frequency=10e6, debug=self.debug_mpsse) gpio_out.configure(self.urls[1], direction=0xFFFF, frequency=10e6, debug=self.debug_mpsse) for out in range(3, 0x10000, 29): gpio_out.write(out) outv = gpio_out.read()[0] inv = gpio_in.read()[0] # check inputs match outputs self.assertEqual(inv, out) # check level of outputs match the ones written self.assertEqual(outv, out) gpio_in.close() gpio_out.close() def test_peek_gpio(self): """Check I/O peeking """ gpio_in, gpio_out = GpioMpsseController(), GpioMpsseController() gpio_in.configure(self.urls[0], direction=0xFF00, frequency=10e6, debug=self.debug) gpio_out.configure(self.urls[1], direction=0x00FF, frequency=10e6, debug=self.debug) for out in range(256): gpio_out.write(out) outv = gpio_out.read()[0] inv = gpio_in.read(peek=True) # check inputs match outputs self.assertEqual(inv, out) #print(f'{out} {inv}') # check level of outputs match the ones written self.assertEqual(outv, out) gpio_in.close() gpio_out.close() def test_stream_gpio(self): """Check I/O streaming. Beware this test is CPU intensive w/ virtual framework """ gpio_in, gpio_out = GpioMpsseController(), GpioMpsseController() gpio_in.configure(self.urls[0], direction=0x0000, frequency=10e6, debug=self.debug) gpio_out.configure(self.urls[1], direction=0xFFFF, frequency=10e6, debug=self.debug) outv = list(range(0, 0x10000, 29)) max_count = min(gpio_out.ftdi.fifo_sizes[0], gpio_in.ftdi.fifo_sizes[1])//2 # 2 bytes/value outv = outv[:max_count] gpio_out.write(outv) inv = gpio_in.read(len(outv)) # for now, it is hard to test value exactness self.assertEqual(len(outv), len(inv)) gpio_in.close() gpio_out.close() def suite(): suite_ = TestSuite() suite_.addTest(makeSuite(GpioAsyncTestCase, 'test')) suite_.addTest(makeSuite(GpioSyncTestCase, 'test')) suite_.addTest(makeSuite(GpioMpsseTestCase, 'test')) suite_.addTest(makeSuite(GpioMultiportTestCase, 'test')) return suite_ def virtualize(): if not to_bool(environ.get('FTDI_VIRTUAL', 'off')): return from pyftdi.usbtools import UsbTools # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('backend.usbvirt', ) # Ensure the virtual backend can be found and is loaded backend = UsbTools.find_backend() try: # obtain the loader class associated with the virtual backend global VirtLoader VirtLoader = backend.create_loader() except AttributeError as exc: raise AssertionError('Cannot load virtual USB backend') from exc def main(): import doctest doctest.testmod(modules[__name__]) debug = to_bool(environ.get('FTDI_DEBUG', 'off')) if debug: formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(levelname)-7s' ' %(name)-20s [%(lineno)4d] %(message)s', '%H:%M:%S') else: formatter = logging.Formatter('%(message)s') level = environ.get('FTDI_LOGLEVEL', 'warning').upper() try: loglevel = getattr(logging, level) except AttributeError as exc: raise ValueError(f'Invalid log level: {level}') from exc FtdiLogger.log.addHandler(logging.StreamHandler(stdout)) FtdiLogger.set_level(loglevel) FtdiLogger.set_formatter(formatter) virtualize() try: ut_main(defaultTest='suite') except KeyboardInterrupt: pass if __name__ == '__main__': # Useful environment variables: # FTDI_DEVICE: a specific FTDI URL, default to ftdi:///1 # FTDI_LOGLEVEL: a Logger debug level, to define log verbosity # FTDI_DEBUG: to enable/disable debug mode # FTDI_VIRTUAL: to use a virtual device rather than a physical device main() pyftdi-0.54.0/pyftdi/tests/i2c.py000077500000000000000000000235461421346242200166040ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2017-2020, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import logging from unittest import TestCase, TestSuite, main as ut_main, makeSuite from binascii import hexlify from doctest import testmod from os import environ from sys import modules, stdout from pyftdi import FtdiLogger from pyftdi.i2c import I2cController, I2cIOError from pyftdi.misc import pretty_size #pylint: disable-msg=attribute-defined-outside-init #pylint: disable-msg=missing-docstring #pylint: disable-msg=no-self-use class I2cTca9555TestCase(TestCase): """Simple test for a TCA9555 device on I2C bus @ address 0x21 """ def test(self): self._i2c = I2cController() self._open() self._read_it() self._write_it() self._close() def _open(self): """Open an I2c connection to a slave""" url = environ.get('FTDI_DEVICE', 'ftdi:///1') self._i2c.configure(url) def _read_it(self): port = self._i2c.get_port(0x21) port.exchange([0x04], 1) def _write_it(self): port = self._i2c.get_port(0x21) port.write_to(0x06, b'\x00') port.write_to(0x02, b'\x55') port.read_from(0x00, 1) def _close(self): """Close the I2C connection""" self._i2c.terminate() class I2cAccelTest(TestCase): """Basic test for an ADXL345 device on I2C bus @ address 0x53 """ def test(self): self._i2c = I2cController() self._open() self._read_device_id() self._close() def _open(self): """Open an I2c connection to a slave""" url = environ.get('FTDI_DEVICE', 'ftdi:///1') self._i2c.configure(url) def _read_device_id(self): port = self._i2c.get_port(0x53) device_id = port.exchange([0x00], 1) hex_device_id = hexlify(device_id).decode() print('DEVICE ID:', hex_device_id) return hex_device_id def _close(self): """Close the I2C connection""" self._i2c.terminate() class I2cReadTest(TestCase): """Simple test to read a sequence of bytes I2C bus @ address 0x36 """ def test(self): self._i2c = I2cController() self._open() self._read() self._close() def _open(self): """Open an I2c connection to a slave""" url = environ.get('FTDI_DEVICE', 'ftdi:///1') self._i2c.configure(url) def _read(self): address = environ.get('I2C_ADDRESS', '0x36').lower() addr = int(address, 16 if address.startswith('0x') else 10) port = self._i2c.get_port(addr) data = port.read(32) print(hexlify(data).decode(), data.decode('utf8', errors='replace')) def _close(self): """Close the I2C connection""" self._i2c.terminate() class I2cEepromTest(TestCase): """Simple test to read a sequence of bytes I2C bus @ address 0x50, from an I2C data flash """ @classmethod def setUpClass(cls): cls.url = environ.get('FTDI_DEVICE', 'ftdi:///1') address = environ.get('I2C_ADDRESS', '0x50').lower() cls.address = int(address, 16 if address.startswith('0x') else 10) def setUp(self): self._i2c = I2cController() self._i2c.configure(self.url, frequency=400e3, clockstretching=False, debug=False, rdoptim=True) def tearDown(self): self._i2c.terminate() def test_short(self): port = self._i2c.get_port(self.address) # select start address port.write(b'\x00\x08') data = port.read(4) text = data.decode('utf8', errors='replace') # print(hexlify(data).decode(), text) self.assertEqual(text, 'Worl') def test_long(self): port = self._i2c.get_port(self.address) # select start address #print('RC', self._i2c.ftdi.read_data_get_chunksize()) #print('WC', self._i2c.ftdi.write_data_get_chunksize()) from time import time as now size = 4096 port.write(b'\x00\x00') start = now() data = port.read(size) stop = now() text = data.decode('utf8', errors='replace') delta = stop-start byterate = pretty_size(len(data)/delta) print(f'Exec time: {1000*delta:.3f} ms, {byterate}/s') self.assertEqual(text[8:12], 'Worl') class I2cReadGpioTest(TestCase): """Simple test to exercise I2C + GPIO mode. A slave device (such as EEPROM) should be connected to the I2C bus at either the default 0x36 address or defined with the I2C_ADDRESS ebvironment variable. AD0: SCL, AD1+AD2: SDA, AD3 connected to AC0, AD4 connected to AC1 I2C EEPROM is read, and values are written to AD3:AD4 and read back from AC0:AC1. """ GPIO_WIDTH = 2 # use 2 GPIOs for output, 2 GPIOs for input (loopback) GPIO_OUT_OFFSET = 3 # GPIO output are b3..b4 GPIO_IN_OFFSET = 8 # GPIO input are b8..b9 def test(self): self._i2c = I2cController() self._open() self._execute_sequence() self._execute_interleave() self._close() def _open(self): """Open an I2c connection to a slave""" url = environ.get('FTDI_DEVICE', 'ftdi:///1') self._i2c.configure(url) address = environ.get('I2C_ADDRESS', '0x36').lower() addr = int(address, 16 if address.startswith('0x') else 10) self._port = self._i2c.get_port(addr) self._gpio = self._i2c.get_gpio() mask = (1 << self.GPIO_WIDTH) - 1 self._gpio.set_direction(mask << self.GPIO_OUT_OFFSET | mask << self.GPIO_IN_OFFSET, mask << self.GPIO_OUT_OFFSET) def _execute_sequence(self): # reset EEPROM read pointer position self._port.write(b'\x00\x00') ref = self._port.read(32) for dout in range(1 << self.GPIO_WIDTH): self._gpio.write(dout << self.GPIO_OUT_OFFSET) din = self._gpio.read() >> self.GPIO_IN_OFFSET if dout != din: raise AssertionError(f'GPIO mismatch {din:04x} != {dout:04x}') self._gpio.write(0) # reset EEPROM read pointer position self._port.write(b'\x00\x00') data = self._port.read(32) if data != ref: raise AssertionError("I2C data mismatch") def _execute_interleave(self): # reset EEPROM read pointer position self._port.write(b'\x00\x00') ref = self._port.read(32) for dout in range(1 << self.GPIO_WIDTH): self._gpio.write(dout << self.GPIO_OUT_OFFSET) # reset EEPROM read pointer position self._port.write(b'\x00\x00') data = self._port.read(32) din = self._gpio.read() >> self.GPIO_IN_OFFSET if data != ref: raise AssertionError("I2C data mismatch") if dout != din: raise AssertionError(f'GPIO mismatch {din:04x} != {dout:04x}') self._gpio.write(0) def _close(self): """Close the I2C connection""" self._i2c.terminate() class I2cClockStrechingGpioCheck(TestCase): """Simple test to check clock stretching cannot be overwritten with GPIOs. """ def test(self): self._i2c = I2cController() url = environ.get('FTDI_DEVICE', 'ftdi:///1') self._i2c.configure(url, clockstretching=True) gpio = self._i2c.get_gpio() self.assertRaises(I2cIOError, gpio.set_direction, 1 << 7, 0) class I2cDualMaster(TestCase): """Check the behaviour of 2 I2C masters. Requires a multi port FTDI device, i.e. FT2232H or FT4232H. See issue #159. """ def test(self): url1 = environ.get('FTDI_DEVICE', 'ftdi:///1') i2c1 = I2cController() i2c1.configure(url1, frequency=100000) url2 = '%s%d' % (url1[:-1], int(url1[-1])+1) i2c2 = I2cController() i2c2.configure(url2, frequency=100000) port = i2c2.get_port(0x76) print(port.read_from(0x00, 2)) print(port.read_from(0x00, 2)) class I2cIssue143(TestCase): """#143. """ def test(self): url = environ.get('FTDI_DEVICE', 'ftdi:///1') address = environ.get('I2C_ADDRESS', '0x50').lower() addr = int(address, 16 if address.startswith('0x') else 10) i2c = I2cController() i2c.configure(url) slave = i2c.get_port(addr) gpio = i2c.get_gpio() gpio.set_direction(0x0010, 0x0010) gpio.write(0) gpio.write(1<<4) gpio.write(0) slave.write([0x12, 0x34]) gpio.write(0) gpio.write(1<<4) gpio.write(0) def suite(): """FTDI I2C driver test suite Simple test to demonstrate I2C. Please ensure that the HW you connect to the FTDI port A does match the encoded configuration. GPIOs can be driven high or low, so check your HW setup before running this test as it might damage your HW. Do NOT run this test if you use FTDI port A as an UART or SPI bridge -or any unsupported setup!! You've been warned. """ ste = TestSuite() #ste.addTest(I2cTca9555TestCase('test')) #ste.addTest(I2cAccelTest('test')) #ste.addTest(I2cReadTest('test')) ste.addTest(makeSuite(I2cEepromTest, 'test')) #ste.addTest(I2cReadGpioTest('test')) ste.addTest(I2cClockStrechingGpioCheck('test')) #ste.addTest(I2cDualMaster('test')) ste.addTest(I2cIssue143('test')) return ste def main(): testmod(modules[__name__]) FtdiLogger.log.addHandler(logging.StreamHandler(stdout)) level = environ.get('FTDI_LOGLEVEL', 'info').upper() try: loglevel = getattr(logging, level) except AttributeError as exc: raise ValueError(f'Invalid log level: {level}') from exc FtdiLogger.set_level(loglevel) ut_main(defaultTest='suite') if __name__ == '__main__': main() pyftdi-0.54.0/pyftdi/tests/jtag.py000077500000000000000000000057501421346242200170510ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2011-2020, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from os import environ from unittest import TestCase, main as ut_main, makeSuite from pyftdi.jtag import JtagEngine, JtagTool from pyftdi.bits import BitSequence #pylint: disable-msg=missing-docstring # Should match the tested device JTAG_INSTR = {'SAMPLE': BitSequence('0001', msb=True, length=4), 'PRELOAD': BitSequence('0001', msb=True, length=4), 'IDCODE': BitSequence('0100', msb=True, length=4), 'BYPASS': BitSequence('1111', msb=True, length=4)} class JtagTestCase(TestCase): def setUp(self): url = environ.get('FTDI_DEVICE', 'ftdi://ftdi:2232h/1') self.jtag = JtagEngine(trst=True, frequency=3E6) self.jtag.configure(url) self.jtag.reset() self.tool = JtagTool(self.jtag) def tearDown(self): del self.jtag def test_idcode_reset(self): """Read the IDCODE right after a JTAG reset""" self.jtag.reset() idcode = self.jtag.read_dr(32) self.jtag.go_idle() print("IDCODE (reset): 0x%x" % int(idcode)) def test_idcode_sequence(self): """Read the IDCODE using the dedicated instruction""" instruction = JTAG_INSTR['IDCODE'] self.jtag.write_ir(instruction) idcode = self.jtag.read_dr(32) self.jtag.go_idle() print("IDCODE (idcode): 0x%08x" % int(idcode)) def test_idcode_shift_register(self): """Read the IDCODE using the dedicated instruction with shift_and_update_register""" instruction = JTAG_INSTR['IDCODE'] self.jtag.change_state('shift_ir') retval = self.jtag.shift_and_update_register(instruction) print("retval: 0x%x" % int(retval)) self.jtag.go_idle() self.jtag.change_state('shift_dr') idcode = self.jtag.shift_and_update_register(BitSequence('0'*32)) self.jtag.go_idle() print("IDCODE (idcode): 0x%08x" % int(idcode)) def test_bypass_shift_register(self): """Test the BYPASS instruction using shift_and_update_register""" instruction = JTAG_INSTR['BYPASS'] self.jtag.change_state('shift_ir') retval = self.jtag.shift_and_update_register(instruction) print("retval: 0x%x" % int(retval)) self.jtag.go_idle() self.jtag.change_state('shift_dr') _in = BitSequence('011011110000'*2, length=24) out = self.jtag.shift_and_update_register(_in) self.jtag.go_idle() print("BYPASS sent: %s, received: %s (should be left shifted by one)" % (_in, out)) def _test_detect_ir_length(self): """Detect the instruction register length""" self.jtag.go_idle() self.jtag.capture_ir() self.tool.detect_register_size() def suite(): return makeSuite(JtagTestCase, 'test') if __name__ == '__main__': ut_main(defaultTest='suite') pyftdi-0.54.0/pyftdi/tests/mockusb.py000077500000000000000000000770461421346242200175760ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2020-2021, Emmanuel Blot # All rights reserved. #pylint: disable-msg=empty-docstring #pylint: disable-msg=missing-docstring #pylint: disable-msg=no-self-use #pylint: disable-msg=invalid-name #pylint: disable-msg=global-statement #pylint: disable-msg=too-many-locals import logging from collections import defaultdict from contextlib import redirect_stdout from doctest import testmod from io import StringIO from os import environ from string import ascii_letters from sys import modules, stdout, version_info from unittest import TestCase, TestSuite, makeSuite, main as ut_main from urllib.parse import urlsplit from pyftdi import FtdiLogger from pyftdi.eeprom import FtdiEeprom from pyftdi.ftdi import Ftdi, FtdiMpsseError from pyftdi.gpio import GpioController from pyftdi.misc import to_bool from pyftdi.serialext import serial_for_url from pyftdi.usbtools import UsbTools # MockLoader is assigned in ut_main MockLoader = None class FtdiTestCase(TestCase): """Common features for all tests. """ @classmethod def setUpClass(cls): cls.loader = MockLoader() cls.debug = to_bool(environ.get('FTDI_DEBUG', 'off'), permissive=False) @classmethod def tearDownClass(cls): if cls.loader: cls.loader.unload() def setUp(self): if self.debug: print('.'.join(self.id().split('.')[-2:])) class MockUsbToolsTestCase(FtdiTestCase): """Test UsbTools APIs. """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() with open('pyftdi/tests/resources/ftmany.yaml', 'rb') as yfp: cls.loader.load(yfp) def test_enumerate(self): """Enumerate FTDI devices.""" ftdis = [(0x403, pid) for pid in (0x6001, 0x6010, 0x6011, 0x6014, 0x6015)] count = len(UsbTools.find_all(ftdis)) self.assertEqual(count, 6) def test_device(self): """Access and release FTDI device.""" ftdis = [(0x403, 0x6001)] ft232rs = UsbTools.find_all(ftdis) self.assertEqual(len(ft232rs), 1) devdesc, ifcount = ft232rs[0] self.assertEqual(ifcount, 1) dev = UsbTools.get_device(devdesc) self.assertIsNotNone(dev) UsbTools.release_device(dev) def test_string(self): """Retrieve a string from its identifier.""" ftdis = [(0x403, 0x6010)] ft2232h = UsbTools.find_all(ftdis)[0] devdesc, _ = ft2232h dev = UsbTools.get_device(devdesc) serialn = UsbTools.get_string(dev, dev.iSerialNumber) self.assertEqual(serialn, 'FT2DEF') def test_list_devices(self): """List FTDI devices.""" vid = 0x403 vids = {'ftdi': vid} pids = { vid: { '230x': 0x6015, '232r': 0x6001, '232h': 0x6014, '2232h': 0x6010, '4232h': 0x6011, } } devs = UsbTools.list_devices('ftdi:///?', vids, pids, vid) self.assertEqual(len(devs), 6) ifmap = { 0x6001: 1, 0x6010: 2, 0x6011: 4, 0x6014: 1, 0x6015: 1 } for dev, desc in devs: strings = UsbTools.build_dev_strings('ftdi', vids, pids, [(dev, desc)]) self.assertEqual(len(strings), ifmap[dev.pid]) for url, _ in strings: parts, _ = UsbTools.parse_url(url, 'ftdi', vids, pids, vid) self.assertEqual(parts.vid, dev.vid) self.assertEqual(parts.pid, dev.pid) self.assertEqual(parts.bus, dev.bus) self.assertEqual(parts.address, dev.address) self.assertEqual(parts.sn, dev.sn) devs = UsbTools.list_devices('ftdi://:232h/?', vids, pids, vid) self.assertEqual(len(devs), 2) devs = UsbTools.list_devices('ftdi://:2232h/?', vids, pids, vid) self.assertEqual(len(devs), 1) class MockFtdiDiscoveryTestCase(FtdiTestCase): """Test FTDI device discovery APIs. These APIs are FTDI wrappers for UsbTools APIs. """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() with open('pyftdi/tests/resources/ftmany.yaml', 'rb') as yfp: cls.loader.load(yfp) def test_list_devices(self): """List FTDI devices.""" devs = Ftdi.list_devices('ftdi:///?') self.assertEqual(len(devs), 6) devs = Ftdi.list_devices('ftdi://:232h/?') self.assertEqual(len(devs), 2) devs = Ftdi.list_devices('ftdi://:2232h/?') self.assertEqual(len(devs), 1) devs = Ftdi.list_devices('ftdi://:4232h/?') self.assertEqual(len(devs), 1) out = StringIO() Ftdi.show_devices('ftdi:///?', out) lines = [l.strip() for l in out.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() self.assertEqual(len(lines), 10) portmap = defaultdict(int) reference = {'232': 1, '2232': 2, '4232': 4, '232h': 2, 'ft-x': 1} for line in lines: url = line.split(' ')[0].strip() parts = urlsplit(url) self.assertEqual(parts.scheme, 'ftdi') self.assertRegex(parts.path, r'^/[1-4]$') product = parts.netloc.split(':')[1] portmap[product] += 1 self.assertEqual(portmap, reference) class MockSimpleDeviceTestCase(FtdiTestCase): """Test FTDI APIs with a single-port FTDI device (FT232H) """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() with open('pyftdi/tests/resources/ft232h.yaml', 'rb') as yfp: cls.loader.load(yfp) def test_enumerate(self): """Check simple enumeration of a single FTDI device.""" ftdi = Ftdi() temp_stdout = StringIO() with redirect_stdout(temp_stdout): self.assertRaises(SystemExit, ftdi.open_from_url, 'ftdi:///?') lines = [l.strip() for l in temp_stdout.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() self.assertEqual(len(lines), 1) self.assertTrue(lines[0].startswith('ftdi://')) # skip description, i.e. consider URL only self.assertTrue(lines[0].split(' ')[0].endswith('/1')) class MockDualDeviceTestCase(FtdiTestCase): """Test FTDI APIs with two similar single-port FTDI devices (FT232H) """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() with open('pyftdi/tests/resources/ft232h_x2.yaml', 'rb') as yfp: cls.loader.load(yfp) def test_enumerate(self): """Check simple enumeration of a 2-port FTDI device.""" ftdi = Ftdi() temp_stdout = StringIO() with redirect_stdout(temp_stdout): self.assertRaises(SystemExit, ftdi.open_from_url, 'ftdi:///?') lines = [l.strip() for l in temp_stdout.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() self.assertEqual(len(lines), 2) for line in lines: self.assertTrue(line.startswith('ftdi://')) # skip description, i.e. consider URL only self.assertTrue(line.split(' ')[0].endswith('/1')) class MockTwoPortDeviceTestCase(FtdiTestCase): """Test FTDI APIs with a dual-port FTDI device (FT2232H) """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() with open('pyftdi/tests/resources/ft2232h.yaml', 'rb') as yfp: cls.loader.load(yfp) def test_enumerate(self): """Check simple enumeration of a 4-port FTDI device.""" ftdi = Ftdi() temp_stdout = StringIO() with redirect_stdout(temp_stdout): self.assertRaises(SystemExit, ftdi.open_from_url, 'ftdi:///?') lines = [l.strip() for l in temp_stdout.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() self.assertEqual(len(lines), 2) for pos, line in enumerate(lines, start=1): self.assertTrue(line.startswith('ftdi://')) # skip description, i.e. consider URL only self.assertTrue(line.split(' ')[0].endswith(f'/{pos}')) class MockFourPortDeviceTestCase(FtdiTestCase): """Test FTDI APIs with a quad-port FTDI device (FT4232H) """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() with open('pyftdi/tests/resources/ft4232h.yaml', 'rb') as yfp: cls.loader.load(yfp) def test_enumerate(self): """Check simple enumeration of two similar FTDI device.""" ftdi = Ftdi() temp_stdout = StringIO() with redirect_stdout(temp_stdout): self.assertRaises(SystemExit, ftdi.open_from_url, 'ftdi:///?') lines = [l.strip() for l in temp_stdout.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() self.assertEqual(len(lines), 4) for pos, line in enumerate(lines, start=1): self.assertTrue(line.startswith('ftdi://')) # skip description, i.e. consider URL only self.assertTrue(line.split(' ')[0].endswith(f'/{pos}')) class MockManyDevicesTestCase(FtdiTestCase): """Test FTDI APIs with several, mixed type FTDI devices """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() with open('pyftdi/tests/resources/ftmany.yaml', 'rb') as yfp: cls.loader.load(yfp) def test_enumerate(self): """Check simple enumeration of two similar FTDI device.""" ftdi = Ftdi() temp_stdout = StringIO() with redirect_stdout(temp_stdout): self.assertRaises(SystemExit, ftdi.open_from_url, 'ftdi:///?') lines = [l.strip() for l in temp_stdout.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() self.assertEqual(len(lines), 10) for line in lines: self.assertTrue(line.startswith('ftdi://')) # skip description, i.e. consider URL only url = line.split(' ')[0] urlparts = urlsplit(url) self.assertEqual(urlparts.scheme, 'ftdi') parts = urlparts.netloc.split(':') if parts[1] == '4232': # def file contains no serial number, so expect bus:addr syntax self.assertEqual(len(parts), 4) self.assertRegex(parts[2], r'^\d$') self.assertRegex(parts[3], r'^\d$') else: # other devices are assigned a serial number self.assertEqual(len(parts), 3) self.assertTrue(parts[2].startswith('FT')) self.assertRegex(urlparts.path, r'^/\d$') class MockSimpleDirectTestCase(FtdiTestCase): """Test FTDI open/close APIs with a basic featured FTDI device (FT230H) """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() with open('pyftdi/tests/resources/ft230x.yaml', 'rb') as yfp: cls.loader.load(yfp) def test_open_close(self): """Check simple open/close sequence.""" ftdi = Ftdi() ftdi.open_from_url('ftdi:///1') self.assertEqual(ftdi.usb_path, (1, 1, 0)) ftdi.close() def test_open_bitbang(self): """Check simple open/close BitBang sequence.""" ftdi = Ftdi() ftdi.open_bitbang_from_url('ftdi:///1') ftdi.close() def test_open_mpsse(self): """Check simple MPSSE access.""" ftdi = Ftdi() # FT230X is a pure UART bridge, MPSSE should not be available self.assertRaises(FtdiMpsseError, ftdi.open_mpsse_from_url, 'ftdi:///1') class MockSimpleMpsseTestCase(FtdiTestCase): """Test FTDI open/close APIs with a MPSSE featured FTDI device (FT232H) """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() with open('pyftdi/tests/resources/ft232h.yaml', 'rb') as yfp: cls.loader.load(yfp) def test_open_close(self): """Check simple open/close sequence.""" ftdi = Ftdi() ftdi.open_from_url('ftdi:///1') self.assertEqual(ftdi.usb_path, (4, 5, 0)) ftdi.close() def test_open_bitbang(self): """Check simple open/close BitBang sequence.""" ftdi = Ftdi() ftdi.open_bitbang_from_url('ftdi:///1') ftdi.close() def test_open_mpsse(self): """Check simple MPSSE access.""" ftdi = Ftdi() ftdi.open_mpsse_from_url('ftdi:///1') ftdi.close() class MockSimpleGpioTestCase(FtdiTestCase): """Test FTDI GPIO APIs """ def tearDown(self): self.loader.unload() def _test_gpio(self): """Check simple GPIO write and read sequence.""" with open('pyftdi/tests/resources/ft232h.yaml', 'rb') as yfp: self.loader.load(yfp) gpio = GpioController() # access to the virtual GPIO port out_pins = 0xAA gpio.configure('ftdi://:232h/1', direction=out_pins) bus, address, iface = gpio.ftdi.usb_path self.assertEqual((bus, address, iface), (4, 5, 0)) vftdi = self.loader.get_virtual_ftdi(bus, address) vport = vftdi.get_port(1) gpio.write_port(0xF3) self.assertEqual(vport.gpio, 0xAA & 0xF3) vport.gpio = 0x0c vio = gpio.read_port() self.assertEqual(vio, (0xAA & 0xF3) | (~0xAA & 0x0c)) gpio.close() def test_baudrate(self): """Check simple GPIO write and read sequence.""" # load custom CBUS config, with: # CBUS0: GPIO (gpio) # CBUS1: GPIO (gpio) # CBUS0: DRIVE1 (forced to high level) # CBUS0: TXLED (eq. to highz for tests) with open('pyftdi/tests/resources/ft230x_io.yaml', 'rb') as yfp: self.loader.load(yfp) gpio = GpioController() # access to the virtual GPIO port out_pins = 0xAA gpio.configure('ftdi://:230x/1', direction=out_pins) vftdi = self.loader.get_virtual_ftdi(1, 1) vftdi.get_port(1) baudrate = 1000000 gpio.set_frequency(baudrate) gpio.close() class MockSimpleUartTestCase(FtdiTestCase): """Test FTDI UART APIs """ def tearDown(self): self.loader.unload() def test_uart(self): """Check simple TX/RX sequence.""" with open('pyftdi/tests/resources/ft232h.yaml', 'rb') as yfp: self.loader.load(yfp) port = serial_for_url('ftdi:///1') bus, address, _ = port.usb_path vftdi = self.loader.get_virtual_ftdi(bus, address) vport = vftdi.get_port(1) msg = ascii_letters port.write(msg.encode()) txd = vport[vport.UART_PINS.TXD] buf = txd.read(len(ascii_letters)+10).decode() self.assertEqual(msg, buf) msg = ''.join(reversed(msg)) rxd = vport[vport.UART_PINS.TXD] rxd.write(msg.encode()) buf = port.read(len(ascii_letters)).decode() self.assertEqual(msg, buf) port.close() def test_uart_loopback(self): """Check TXD/RXD loopback.""" with open('pyftdi/tests/resources/ft232h.yaml', 'rb') as yfp: self.loader.load(yfp) port = serial_for_url('ftdi:///1') bus, address, _ = port.usb_path vftdi = self.loader.get_virtual_ftdi(bus, address) vport = vftdi.get_port(1) txd = vport[vport.UART_PINS.TXD] rxd = vport[vport.UART_PINS.RXD] txd.connect_to(rxd) msg = ascii_letters port.write(msg.encode()) buf = port.read(len(ascii_letters)).decode() self.assertEqual(msg, buf) port.close() def test_baudrate_fs_dev(self): """Check baudrate settings for full speed devices.""" with open('pyftdi/tests/resources/ft230x.yaml', 'rb') as yfp: self.loader.load(yfp) port = serial_for_url('ftdi:///1', baudrate=1200) bus, address, _ = port.usb_path vftdi = self.loader.get_virtual_ftdi(bus, address) vport = vftdi.get_port(1) self.assertRaises(ValueError, setattr, port, 'baudrate', 100) self.assertRaises(ValueError, setattr, port, 'baudrate', 3100000) for baudrate in (200, 600, 1200, 2400, 4800, 9600, 115200, 230400, 460800, 490000, 921600, 1000000, 1200000, 1500000, 2000000, 3000000): port.baudrate = baudrate #print(f'{baudrate} -> {port.ftdi.baudrate} -> {vport.baudrate}') self.assertEqual(port.ftdi.baudrate, vport.baudrate) port.close() def test_baudrate_hs_dev(self): """Check baudrate settings for high speed devices.""" with open('pyftdi/tests/resources/ft232h.yaml', 'rb') as yfp: self.loader.load(yfp) port = serial_for_url('ftdi:///1', baudrate=1200) bus, address, _ = port.usb_path vftdi = self.loader.get_virtual_ftdi(bus, address) vport = vftdi.get_port(1) self.assertRaises(ValueError, setattr, port, 'baudrate', 100) self.assertRaises(ValueError, setattr, port, 'baudrate', 12100000) for baudrate in (200, 600, 1200, 2400, 4800, 9600, 115200, 230400, 460800, 490000, 921600, 1000000, 1200000, 1500000, 2000000, 3000000, 4000000, 6000000): port.baudrate = baudrate #print(f'{baudrate} -> {port.ftdi.baudrate} -> {vport.baudrate}') self.assertEqual(port.ftdi.baudrate, vport.baudrate) port.close() class MockRawExtEepromTestCase(FtdiTestCase): """Test FTDI EEPROM low-level APIs with external EEPROM device """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() with open('pyftdi/tests/resources/ft232h.yaml', 'rb') as yfp: cls.loader.load(yfp) def _restore_eeprom(self, ftdi): bus, address, _ = ftdi.usb_path vftdi = self.loader.get_virtual_ftdi(bus, address) data = self.loader.eeprom_backup size = len(vftdi.eeprom) if len(data) < size: data = bytearray(data) + bytes(size-len(data)) vftdi.eeprom = bytes(data) def test_dump(self): """Check EEPROM full content.""" ftdi = Ftdi() ftdi.open_from_url('ftdi:///1') self._restore_eeprom(ftdi) ref_data = bytes(list(range(256))) size = len(ref_data) data = ftdi.read_eeprom() self.assertEqual(len(data), size) self.assertEqual(ref_data, data) ftdi.close() def test_random_access_read(self): """Check EEPROM random read access.""" ftdi = Ftdi() ftdi.open_from_url('ftdi:///1') self._restore_eeprom(ftdi) ref_data = bytes(list(range(256))) size = len(ref_data) # out of bound self.assertRaises(ValueError, ftdi.read_eeprom, size, 2) # last bytes buf = ftdi.read_eeprom(size-2, 2) self.assertEqual(buf[0:2], ref_data[-2:]) self.assertEqual(buf[0:2], b'\xfe\xff') # out of bound self.assertRaises(ValueError, ftdi.read_eeprom, size-2, 4) # unaligned access buf = ftdi.read_eeprom(1, 2) self.assertEqual(buf[0:2], ref_data[1:3]) self.assertEqual(buf[0:2], b'\x01\x02') # long read, unaligned access, unaligned size buf = ftdi.read_eeprom(43, 43) self.assertEqual(len(buf), 43) self.assertEqual(buf, ref_data[43:86]) ftdi.close() def test_randow_access_write(self): """Check EEPROM random write access.""" ftdi = Ftdi() ftdi.open_from_url('ftdi:///1') bus, address, _ = ftdi.usb_path vftdi = self.loader.get_virtual_ftdi(bus, address) self._restore_eeprom(ftdi) checksum1 = vftdi.eeprom[-2:] orig_data = vftdi.eeprom[:8] ref_data = b'ABCD' ftdi.write_eeprom(0, ref_data, dry_run=False) checksum2 = vftdi.eeprom[-2:] # verify the data have been written self.assertEqual(vftdi.eeprom[:4], ref_data) # verify the data have not been overwritten self.assertEqual(vftdi.eeprom[4:8], orig_data[4:]) # verify the checksum has been updated # TODO compute the expected checksum self.assertNotEqual(checksum1, checksum2) checksum1 = vftdi.eeprom[-2:] orig_data = vftdi.eeprom[:24] ftdi.write_eeprom(9, ref_data, dry_run=False) checksum2 = vftdi.eeprom[-2:] # verify the unaligned data have been written self.assertEqual(vftdi.eeprom[9:13], ref_data) # verify the data have not been overwritten self.assertEqual(vftdi.eeprom[:9], orig_data[:9]) self.assertEqual(vftdi.eeprom[13:24], orig_data[13:]) # verify the checksum has been updated self.assertNotEqual(checksum1, checksum2) checksum1 = vftdi.eeprom[-2:] orig_data = vftdi.eeprom[:48] ftdi.write_eeprom(33, ref_data[:3], dry_run=False) checksum2 = vftdi.eeprom[-2:] # verify the unaligned data have been written self.assertEqual(vftdi.eeprom[33:36], ref_data[:3]) # verify the data have not been overwritten self.assertEqual(vftdi.eeprom[:33], orig_data[:33]) self.assertEqual(vftdi.eeprom[36:48], orig_data[36:]) # verify the checksum has been updated self.assertNotEqual(checksum1, checksum2) class MockRawIntEepromTestCase(FtdiTestCase): """Test FTDI EEPROM low-level APIs with internal EEPROM device """ @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() with open('pyftdi/tests/resources/ft230x.yaml', 'rb') as yfp: cls.loader.load(yfp) def test_descriptor_update(self): """Check EEPROM content overrides YaML configuration.""" # this test is more about testing the virtual FTDI infrastructure # than a pure PyFtdi test devs = Ftdi.list_devices('ftdi:///?') self.assertEqual(len(devs), 1) desc = devs[0][0] # these values are not the ones defined in YaML, but stored in EEPROM self.assertEqual(desc.sn, 'FT3KMGTL') self.assertEqual(desc.description, 'LC231X') def test_eeprom_read(self): """Check full read sequence.""" ftdi = Ftdi() ftdi.open_from_url('ftdi:///1') data = ftdi.read_eeprom() self.assertEqual(len(data), 0x400) ftdi.close() class MockCBusEepromTestCase(FtdiTestCase): """Test FTDI EEPROM APIs that manage CBUS feature """ def tearDown(self): self.loader.unload() def test_ft230x(self): with open('pyftdi/tests/resources/ft230x.yaml', 'rb') as yfp: self.loader.load(yfp) eeprom = FtdiEeprom() eeprom.open('ftdi:///1') # default EEPROM config does not have any CBUS configured as GPIO self.assertEqual(eeprom.cbus_pins, []) self.assertEqual(eeprom.cbus_mask, 0) # enable CBUS1 and CBUS3 as GPIO eeprom.set_property('cbus_func_1', 'gpio') eeprom.set_property('cbus_func_3', 'gpio') eeprom.sync() self.assertEqual(eeprom.cbus_pins, [1, 3]) self.assertEqual(eeprom.cbus_mask, 0xA) # enable CBUS0 and CBUS2 as GPIO eeprom.set_property('cbus_func_0', 'gpio') eeprom.set_property('cbus_func_2', 'gpio') # not yet committed self.assertEqual(eeprom.cbus_pins, [1, 3]) self.assertEqual(eeprom.cbus_mask, 0xA) eeprom.sync() # committed self.assertEqual(eeprom.cbus_pins, [0, 1, 2, 3]) self.assertEqual(eeprom.cbus_mask, 0xF) # invalid CBUS pin self.assertRaises(ValueError, eeprom.set_property, 'cbus_func_4', 'gpio') # invalid pin function self.assertRaises(ValueError, eeprom.set_property, 'cbus_func_0', 'gpio_') # invalid pin self.assertRaises(ValueError, eeprom.set_property, 'cbus_func', 'gpio') # valid alternative mode eeprom.set_property('cbus_func_0', 'txled') eeprom.set_property('cbus_func_1', 'rxled') eeprom.sync() self.assertEqual(eeprom.cbus_pins, [2, 3]) self.assertEqual(eeprom.cbus_mask, 0xC) eeprom.close() def test_ft232h(self): with open('pyftdi/tests/resources/ft232h_x2.yaml', 'rb') as yfp: self.loader.load(yfp) eeprom = FtdiEeprom() eeprom.open('ftdi://::FT1ABC1/1', ignore=True) eeprom.erase() eeprom.initialize() # default EEPROM config does not have any CBUS configured as GPIO self.assertEqual(eeprom.cbus_pins, []) self.assertEqual(eeprom.cbus_mask, 0) eeprom.set_property('cbus_func_6', 'gpio') eeprom.set_property('cbus_func_9', 'gpio') # not yet committed self.assertEqual(eeprom.cbus_pins, []) self.assertEqual(eeprom.cbus_mask, 0) eeprom.sync() # committed self.assertEqual(eeprom.cbus_pins, [6, 9]) self.assertEqual(eeprom.cbus_mask, 0xA) eeprom.set_property('cbus_func_5', 'gpio') eeprom.set_property('cbus_func_8', 'gpio') eeprom.sync() self.assertEqual(eeprom.cbus_pins, [5, 6, 8, 9]) self.assertEqual(eeprom.cbus_mask, 0xF) # pin1 and pin3 is not configurable as GPIO self.assertRaises(ValueError, eeprom.set_property, 'cbus_func_1', 'gpio') self.assertRaises(ValueError, eeprom.set_property, 'cbus_func_3', 'gpio') eeprom.close() class MockCbusGpioTestCase(FtdiTestCase): """Test FTDI CBUS GPIO APIs """ def tearDown(self): self.loader.unload() def test_230x(self): """Check simple GPIO write and read sequence.""" # load custom CBUS config, with: # CBUS0: GPIO (gpio) # CBUS1: GPIO (gpio) # CBUS0: DRIVE1 (forced to high level) # CBUS0: TXLED (eq. to highz for tests) with open('pyftdi/tests/resources/ft230x_io.yaml', 'rb') as yfp: self.loader.load(yfp) ftdi = Ftdi() ftdi.open_from_url('ftdi:///1') self.assertEqual(ftdi.has_cbus, True) vftdi = self.loader.get_virtual_ftdi(1, 1) vport = vftdi.get_port(1) # CBUS0: in, CBUS1: out, CBUS2: in, CBUS3: out # however, only CBUS0 and CBUS1 are mapped as GPIO, # CBUS2 forced to 1 and CBUS3 not usable as IO # even if use mask is 1111 eeprom_mask = 0b0011 eeprom_force = 0b0100 cbus_mask = 0b1111 cbus_dir = 0b1010 ftdi.set_cbus_direction(cbus_mask, cbus_dir) cbus_out = 0b0011 # CBUS0: 1, CBUS1: 1 # however, only CBUS1 is out, so CBUS0 output value should be ignored ftdi.set_cbus_gpio(cbus_out) exp_out = cbus_dir & cbus_out exp_out &= eeprom_mask exp_out |= eeprom_force vcbus, vactive = vport.cbus self.assertEqual(vcbus, exp_out) self.assertEqual(vactive, eeprom_mask | eeprom_force) cbus_out = 0b0000 ftdi.set_cbus_gpio(cbus_out) exp_out = cbus_dir & cbus_out exp_out &= eeprom_mask exp_out |= eeprom_force vcbus, vactive = vport.cbus self.assertEqual(vcbus, exp_out) cbus_in = 0b0101 vport.cbus = cbus_in cbus = ftdi.get_cbus_gpio() exp_in = cbus_in & eeprom_mask self.assertEqual(cbus, exp_in) ftdi.close() def test_lc231x(self): """Check simple GPIO write and read sequence.""" # load custom CBUS config, with: # CBUS0: GPIO (gpio) # CBUS1: TXLED # CBUS2: DRIVE0 (to light up RX green led) # CBUS3: GPIO (gpio) # only CBUS0 and CBUS3 are available on LC231X # CBUS1 is connected to TX led, CBUS2 to RX led with open('pyftdi/tests/resources/ft231x_cbus.yaml', 'rb') as yfp: self.loader.load(yfp) ftdi = Ftdi() ftdi.open_from_url('ftdi:///1') self.assertEqual(ftdi.has_cbus, True) vftdi = self.loader.get_virtual_ftdi(1, 1) vport = vftdi.get_port(1) # CBUS0: in, CBUS1: out, CBUS2: in, CBUS3: out # however, only CBUS0 and CBUS3 are mapped as GPIO, # CBUS1 not usable as IO, CBUS2 is fixed to low # even if use mask is 1111 eeprom_mask = 0b1001 eeprom_force_low = 0b0100 cbus_mask = 0b1111 cbus_dir = 0b1010 ftdi.set_cbus_direction(cbus_mask, cbus_dir) cbus_out = 0b1111 # however, only CBUS0 & 3 are out, so CBUS1/CBUS2 should be ignored ftdi.set_cbus_gpio(cbus_out) exp_out = cbus_dir & cbus_out exp_out &= eeprom_mask vcbus, vactive = vport.cbus self.assertEqual(vcbus, exp_out) self.assertEqual(vactive, eeprom_mask | eeprom_force_low) cbus_out = 0b0000 ftdi.set_cbus_gpio(cbus_out) exp_out = cbus_dir & cbus_out exp_out &= eeprom_mask vcbus, vactive = vport.cbus self.assertEqual(vcbus, exp_out) cbus_in = 0b0101 vport.cbus = cbus_in cbus = ftdi.get_cbus_gpio() exp_in = cbus_in & eeprom_mask self.assertEqual(cbus, exp_in) ftdi.close() def suite(): suite_ = TestSuite() suite_.addTest(makeSuite(MockUsbToolsTestCase, 'test')) suite_.addTest(makeSuite(MockFtdiDiscoveryTestCase, 'test')) suite_.addTest(makeSuite(MockSimpleDeviceTestCase, 'test')) suite_.addTest(makeSuite(MockDualDeviceTestCase, 'test')) suite_.addTest(makeSuite(MockTwoPortDeviceTestCase, 'test')) suite_.addTest(makeSuite(MockFourPortDeviceTestCase, 'test')) suite_.addTest(makeSuite(MockManyDevicesTestCase, 'test')) suite_.addTest(makeSuite(MockSimpleDirectTestCase, 'test')) suite_.addTest(makeSuite(MockSimpleMpsseTestCase, 'test')) suite_.addTest(makeSuite(MockSimpleGpioTestCase, 'test')) suite_.addTest(makeSuite(MockSimpleUartTestCase, 'test')) suite_.addTest(makeSuite(MockRawExtEepromTestCase, 'test')) suite_.addTest(makeSuite(MockRawIntEepromTestCase, 'test')) suite_.addTest(makeSuite(MockCBusEepromTestCase, 'test')) suite_.addTest(makeSuite(MockCbusGpioTestCase, 'test')) return suite_ def main(): testmod(modules[__name__]) debug = to_bool(environ.get('FTDI_DEBUG', 'off')) if debug: formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(levelname)-7s' ' %(name)-18s [%(lineno)4d] %(message)s', '%H:%M:%S') else: formatter = logging.Formatter('%(message)s') level = environ.get('FTDI_LOGLEVEL', 'warning').upper() try: loglevel = getattr(logging, level) except AttributeError as exc: raise ValueError(f'Invalid log level: {level}') from exc FtdiLogger.log.addHandler(logging.StreamHandler(stdout)) FtdiLogger.set_level(loglevel) FtdiLogger.set_formatter(formatter) # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('backend.usbvirt', ) # Ensure the virtual backend can be found and is loaded backend = UsbTools.find_backend() try: # obtain the loader class associated with the virtual backend global MockLoader MockLoader = backend.create_loader() except AttributeError as exc: raise AssertionError('Cannot load virtual USB backend') from exc ut_main(defaultTest='suite') if __name__ == '__main__': main() pyftdi-0.54.0/pyftdi/tests/resources/000077500000000000000000000000001421346242200175525ustar00rootroot00000000000000pyftdi-0.54.0/pyftdi/tests/resources/custom_vidpid.yaml000066400000000000000000000006331421346242200233110ustar00rootroot00000000000000# SPDX-License-Identifier: BSD-3-Clause devices: - bus: 1 address: 1 descriptor: vid: 0x403 pid: 0x8a99 version: 0x700 manufacturer: FTDI product: TUMPA serialnumber: TIAO_USB_1 - bus: 1 address: 2 descriptor: vid: 0xcafe pid: 0xbeef version: 0x1000 manufacturer: Acme product: Whatever serialnumber: SomeRandomNumber pyftdi-0.54.0/pyftdi/tests/resources/ft2232h.yaml000066400000000000000000000004341421346242200215310ustar00rootroot00000000000000# SPDX-License-Identifier: BSD-3-Clause devices: - bus: 1 address: 1 descriptor: vid: 0x403 pid: 0x6010 version: 0x700 manufacturer: FTDI product: FT2232H serialnumber: FT2DEF configurations: - interfaces: - repeat: 2 pyftdi-0.54.0/pyftdi/tests/resources/ft230x.yaml000066400000000000000000000022431421346242200214650ustar00rootroot00000000000000# SPDX-License-Identifier: BSD-3-Clause devices: - bus: 1 address: 1 descriptor: vid: 0x403 pid: 0x6015 version: 0x1000 manufacturer: FTDI product: FT230X serialnumber: FT1XYZ eeprom: load: yes data: | # hex byte encode format 80 00 03 04 15 60 00 10 80 2d 08 00 00 00 a0 0a aa 0e b8 12 00 00 00 00 00 00 00 02 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 36 d6 c9 01 00 77 f2 b8 68 40 00 00 00 00 00 00 00 00 00 44 4d 51 38 4c 42 4a 45 00 00 00 00 0a 03 46 00 54 00 44 00 49 00 0e 03 4c 00 43 00 32 00 33 00 31 00 58 00 12 03 46 00 54 00 33 00 4b 00 4d 00 47 00 54 00 4c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 cc 57 pyftdi-0.54.0/pyftdi/tests/resources/ft230x_io.yaml000066400000000000000000000025141421346242200221550ustar00rootroot00000000000000# SPDX-License-Identifier: BSD-3-Clause devices: - bus: 1 address: 1 descriptor: vid: 0x403 pid: 0x6015 version: 0x1000 manufacturer: FTDI product: FT230X serialnumber: FT1XYZ eeprom: load: yes data: | # hex byte encode format 80 00 03 04 15 60 00 10 80 2d 08 00 00 00 a0 0a aa 0e b8 0c 00 00 00 00 00 00 08 08 07 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 36 d6 c9 01 00 77 f2 b8 68 40 00 00 00 00 00 00 00 00 00 44 4d 51 38 4c 42 4a 45 00 00 00 00 0a 03 46 00 54 00 44 00 49 00 0e 03 4c 00 43 00 32 00 33 00 31 00 58 00 0c 03 41 00 42 00 43 00 44 00 45 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 d8 bf # CBUS0: GPIO (gpio) # CBUS1: GPIO (gpio) # CBUS2: DRIVE1 (forced to high level) # CBUS3: TXLED (eq. to highz for tests)pyftdi-0.54.0/pyftdi/tests/resources/ft231x_cbus.yaml000066400000000000000000000026501421346242200225040ustar00rootroot00000000000000# SPDX-License-Identifier: BSD-3-Clause devices: - bus: 1 address: 1 descriptor: vid: 0x403 pid: 0x6015 version: 0x1000 manufacturer: FTDI product: LC231X serialnumber: FT1RANDOM eeprom: load: yes data: | # hex byte encode format 80 00 03 04 15 60 00 10 80 2d 08 00 00 00 a0 0a aa 0e b8 0c 00 00 00 00 00 00 08 01 06 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 29 36 d6 c9 01 00 77 f2 b8 68 40 00 00 00 00 00 00 00 00 00 44 4d 51 38 4c 42 4a 45 00 00 00 00 0a 03 46 00 54 00 44 00 49 00 0e 03 4c 00 43 00 32 00 33 00 31 00 58 00 0c 03 41 00 42 00 43 00 44 00 45 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 d0 67 # CBUS0: GPIO (gpio) # CBUS1: TXLED # CBUS2: DRIVE0 (to light up RX green led) # CBUS3: GPIO (gpio) # only CBUS0 and CBUS3 are available on LC231X # CBUS1 is connected to TX led, CBUS2 to RX ledpyftdi-0.54.0/pyftdi/tests/resources/ft232h.yaml000066400000000000000000000050201421346242200214430ustar00rootroot00000000000000# SPDX-License-Identifier: BSD-3-Clause # This file defines a regular FT232H virtual device # It also shows supported options and device definition structure # In most use cases, far simpler device definitions may be used, as the # MockLoader automatically generates sub device structures that are not # expliclty defined. devices: - bus: 4 address: 5 speed: high noaccess: no descriptor: usb: 0x200 class: 0 subclass: 0 protocol: 0 maxpacketsize: 8 vid: 0x403 pid: 0x6014 version: 0x900 manufacturer: FTDI product: FT232H serialnumber: FT1ABC configurations: - descriptor: attributes: - selfpowered maxpower: 150 interfaces: - alternatives: - descriptor: class: 0xff subclass: 0xff protocol: 0xff endpoints: - descriptor: number: 1 maxpacketsize: 64 interval: 0 direction: in type: bulk - descriptor: number: 2 maxpacketsize: 64 interval: 0 direction: out type: bulk eeprom: load: no model: 93c66 # invalid EEPROM content to perform content access tests data: | # hex byte encode format 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f 90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff # data: !!binary | # base64 encode format # R0lGODlhDAAMAIQAAP//9/X # 17unp5WZmZgAAAOfn515eXv # Pz7Y6OjuDg4J+fn5OTk6enp # 56enmleECcgggoBADs= pyftdi-0.54.0/pyftdi/tests/resources/ft232h_x2.yaml000066400000000000000000000006141421346242200220600ustar00rootroot00000000000000# SPDX-License-Identifier: BSD-3-Clause devices: - bus: 1 address: 1 descriptor: vid: 0x403 pid: 0x6014 version: 0x900 manufacturer: FTDI product: FT232H serialnumber: FT1ABC1 - bus: 1 address: 2 descriptor: vid: 0x403 pid: 0x6014 version: 0x900 manufacturer: FTDI product: FT232H serialnumber: FT1ABC2 pyftdi-0.54.0/pyftdi/tests/resources/ft232r.yaml000066400000000000000000000013471421346242200214650ustar00rootroot00000000000000# SPDX-License-Identifier: BSD-3-Clause devices: - bus: 1 address: 1 descriptor: vid: 0x403 pid: 0x6015 version: 0x0600 manufacturer: FTDI product: Basic UART serialnumber: FT2XYZ eeprom: load: yes data: | # hex byte encode format 00 40 03 04 01 60 00 06 a0 32 08 00 00 02 98 0a a2 2c ce 12 32 11 05 00 0a 03 46 00 54 00 44 00 49 00 2c 03 55 00 4d 00 32 00 33 00 32 00 52 00 20 00 55 00 53 00 42 00 20 00 3c 00 2d 00 3e 00 20 00 53 00 65 00 72 00 69 00 61 00 6c 00 12 03 46 00 54 00 47 00 58 00 53 00 59 00 57 00 4a 00 02 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 09 13 pyftdi-0.54.0/pyftdi/tests/resources/ft4232h.yaml000066400000000000000000000004341421346242200215330ustar00rootroot00000000000000# SPDX-License-Identifier: BSD-3-Clause devices: - bus: 1 address: 1 descriptor: vid: 0x403 pid: 0x6011 version: 0x800 manufacturer: FTDI product: FT4232H serialnumber: FT3GHI configurations: - interfaces: - repeat: 4 pyftdi-0.54.0/pyftdi/tests/resources/ftmany.yaml000066400000000000000000000022411421346242200217330ustar00rootroot00000000000000# SPDX-License-Identifier: BSD-3-Clause devices: - bus: 1 address: 2 descriptor: vid: 0x403 pid: 0x6015 version: 0x1000 manufacturer: FTDI product: FT230X serialnumber: FT1XYZ - bus: 2 address: 1 descriptor: vid: 0x403 pid: 0x6014 version: 0x900 manufacturer: FTDI product: FT232H serialnumber: FT1ABC1 - bus: 2 address: 2 descriptor: vid: 0x403 pid: 0x6014 version: 0x900 manufacturer: FTDI product: FT232H serialnumber: FT1ABC2 - bus: 3 address: 1 descriptor: vid: 0x403 pid: 0x6010 version: 0x700 manufacturer: FTDI product: FT2232H serialnumber: FT2DEF configurations: - interfaces: - repeat: 2 - bus: 3 address: 3 descriptor: vid: 0x403 pid: 0x6011 version: 0x800 manufacturer: FTDI product: FT4232H configurations: - interfaces: - repeat: 4 - bus: 3 address: 4 descriptor: vid: 0x403 pid: 0x6001 version: 0x600 manufacturer: FTDI product: FT2232R serialnumber: FT1OPQ pyftdi-0.54.0/pyftdi/tests/spi.py000077500000000000000000000302111421346242200167050ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2017-2020, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause #pylint: disable-msg=empty-docstring #pylint: disable-msg=missing-docstring #pylint: disable-msg=no-self-use import logging import unittest from binascii import hexlify from doctest import testmod from os import environ from sys import modules, stderr, stdout from time import sleep from pyftdi import FtdiLogger from pyftdi.misc import to_bool from pyftdi.spi import SpiController, SpiIOError class SpiDataFlashTest: """Basic test for a MX25L1606E data flash device selected as CS0, SPI mode 0 """ def __init__(self): self._spi = SpiController(cs_count=3) def open(self): """Open an SPI connection to a slave""" url = environ.get('FTDI_DEVICE', 'ftdi:///1') debug = to_bool(environ.get('FTDI_DEBUG', 'off')) self._spi.configure(url, debug=debug) def read_jedec_id(self): port = self._spi.get_port(0, freq=3E6, mode=0) jedec_id = port.exchange([0x9f], 3) hex_jedec_id = hexlify(jedec_id).decode() print('JEDEC ID:', hex_jedec_id) return hex_jedec_id def close(self): """Close the SPI connection""" self._spi.terminate() class SpiAccelTest: """Basic test for an ADXL345 device selected as CS1, SPI mode 3 """ def __init__(self): self._spi = SpiController(cs_count=3) def open(self): """Open an SPI connection to a slave""" url = environ.get('FTDI_DEVICE', 'ftdi:///1') debug = to_bool(environ.get('FTDI_DEBUG', 'off')) self._spi.configure(url, debug=debug) def read_device_id(self): port = self._spi.get_port(1, freq=6E6, mode=3) device_id = port.exchange([0x00], 1) hex_device_id = hexlify(device_id).decode() print('DEVICE ID:', hex_device_id) return hex_device_id def close(self): """Close the SPI connection""" self._spi.terminate() class SpiRfda2125Test: """Basic test for a RFDA2125 Digital Controlled Variable Gain Amplifier selected as CS2, SPI mode 0 """ def __init__(self): self._spi = SpiController(cs_count=3) self._port = None def open(self): """Open an SPI connection to a slave""" url = environ.get('FTDI_DEVICE', 'ftdi:///1') debug = to_bool(environ.get('FTDI_DEBUG', 'off')) self._spi.configure(url, debug=debug) self._port = self._spi.get_port(2, freq=1E6, mode=0) def change_attenuation(self, value): if not 0.0 <= value <= 31.5: print('Out-of-bound attenuation', file=stderr) intval = 63-int(value*2) self._port.write(bytes([intval]), 1) def close(self): """Close the SPI connection""" self._spi.terminate() class SpiTestCase(unittest.TestCase): """FTDI SPI driver test case Simple test to demonstrate SPI feature. Please ensure that the HW you connect to the FTDI port A does match the encoded configuration. GPIOs can be driven high or low, so check your HW setup before running this test as it might damage your HW. Do NOT run this test if you use FTDI port A as an UART or I2C bridge -or any unsupported setup!! You've been warned. """ def test_spi1(self): spi = SpiDataFlashTest() spi.open() jedec_id = spi.read_jedec_id() self.assertIn(jedec_id, ('c22016', 'bf254a')) spi.close() def _test_spi2(self): spi = SpiAccelTest() spi.open() device_id = spi.read_device_id() self.assertEqual(device_id, 'e5') spi.close() def _test_spi3(self): spi = SpiRfda2125Test() spi.open() slope = 1 attenuation = 0.0 for _ in range(10): for _ in range(63): attenuation += float(slope) print(attenuation/2.0) spi.change_attenuation(attenuation/2.0) sleep(0.05) # 50 ms slope = -slope spi.close() class SpiGpioTestCase(unittest.TestCase): """Basic test for GPIO access w/ SPI mode It expects the following I/O setup: AD4 connected t0 AC0 AD5 connected t0 AC1 AD6 connected t0 AC2 AD7 connected t0 AC3 """ # AD0: SCLK, AD1: MOSI, AD2: MISO, AD3: /CS AD_OFFSET = 4 AC_OFFSET = 8 PIN_COUNT = 4 @classmethod def setUpClass(cls): cls.url = environ.get('FTDI_DEVICE', 'ftdi:///1') cls.debug = to_bool(environ.get('FTDI_DEBUG', 'off')) def setUp(self): self._spi = SpiController(cs_count=1) self._spi.configure(self.url, debug=self.debug) self._port = self._spi.get_port(0, freq=1E6, mode=0) self._io = self._spi.get_gpio() def tearDown(self): """Close the SPI connection""" self._spi.terminate() def test_ac_to_ad(self): ad_pins = ((1 << self.PIN_COUNT) - 1) << self.AD_OFFSET # input ac_pins = ((1 << self.PIN_COUNT) - 1) << self.AC_OFFSET # output io_pins = ad_pins | ac_pins def ac_to_ad(ac_output): ac_output &= ac_pins ac_output >>= self.AC_OFFSET - self.AD_OFFSET return ac_output & ad_pins self._io.set_direction(io_pins, ac_pins) for ac_pin in range(1 << self.PIN_COUNT): ac_out = ac_pin << self.AC_OFFSET ad_in = ac_to_ad(ac_out) self._io.write(ac_out) # random SPI exchange to ensure SPI does not change GPIO self._port.exchange([0x00, 0xff], 2) buf = self._io.read() self.assertEqual(buf, ad_in) self.assertRaises(SpiIOError, self._io.write, ad_pins) def test_ad_to_ac(self): ad_pins = ((1 << self.PIN_COUNT) - 1) << self.AD_OFFSET # output ac_pins = ((1 << self.PIN_COUNT) - 1) << self.AC_OFFSET # input io_pins = ad_pins | ac_pins def ad_to_ac(ad_output): ad_output &= ad_pins ad_output <<= self.AC_OFFSET - self.AD_OFFSET return ad_output & ac_pins self._io.set_direction(io_pins, ad_pins) for ad_pin in range(1 << self.PIN_COUNT): ad_out = ad_pin << self.AD_OFFSET ac_in = ad_to_ac(ad_out) self._io.write(ad_out) # random SPI exchange to ensure SPI does not change GPIO self._port.exchange([0x00, 0xff], 2) buf = self._io.read() self.assertEqual(buf, ac_in) self.assertRaises(SpiIOError, self._io.write, ac_pins) class SpiUnalignedTestCase(unittest.TestCase): """Basic test for SPI with non 8-bit multiple transfer It expects the following I/O setup: MOSI (AD1) connected to MISO (AD2) """ @classmethod def setUpClass(cls): cls.url = environ.get('FTDI_DEVICE', 'ftdi:///1') cls.debug = to_bool(environ.get('FTDI_DEBUG', 'off')) def setUp(self): self._spi = SpiController(cs_count=1) self._spi.configure(self.url, debug=self.debug) self._port = self._spi.get_port(0, freq=1E6, mode=0) def tearDown(self): """Close the SPI connection""" self._spi.terminate() def test_invalid_write(self): buf = b'\xff\xff' self.assertRaises(ValueError, self._port.write, buf, droptail=8) def test_bit_write(self): buf = b'\x0f' for loop in range(7): self._port.write(buf, droptail=loop+1) def test_bytebit_write(self): buf = b'\xff\xff\x0f' for loop in range(7): self._port.write(buf, droptail=loop+1) def test_invalid_read(self): self.assertRaises(ValueError, self._port.read, 1, droptail=8) self.assertRaises(ValueError, self._port.read, 2, droptail=8) def test_bit_read(self): # make MOSI stay to low level, so MISO samples 0 self._port.write([0x00]) for loop in range(7): data = self._port.read(1, droptail=loop+1) self.assertEqual(len(data), 1) # make MOSI stay to high level, so MISO samples 1 self._port.write([0x01]) for loop in range(7): data = self._port.read(1, droptail=loop+1) self.assertEqual(len(data), 1) def test_bytebit_read(self): self._port.write([0x00]) for loop in range(7): data = self._port.read(3, droptail=loop+1) self.assertEqual(len(data), 3) self.assertEqual(data[-1], 0) self._port.write([0x01]) for loop in range(7): data = self._port.read(3, droptail=loop+1) self.assertEqual(len(data), 3) def test_invalid_duplex(self): buf = b'\xff\xff' self.assertRaises(ValueError, self._port.exchange, buf, duplex=False, droptail=8) self.assertRaises(ValueError, self._port.exchange, buf, duplex=False, droptail=8) self.assertRaises(ValueError, self._port.exchange, buf, duplex=True, droptail=8) self.assertRaises(ValueError, self._port.exchange, buf, duplex=True, droptail=8) def test_bit_duplex(self): buf = b'\xcf' for loop in range(7): data = self._port.exchange(buf, duplex=True, droptail=loop+1) self.assertEqual(len(data), 1) exp = buf[0] & ~((1<<(loop+1))-1) # print(f'{data[0]:08b} {exp:08b}') self.assertEqual(data[0], exp) def test_bytebit_duplex(self): buf = b'\xff\xcf' for loop in range(7): data = self._port.exchange(buf, duplex=True, droptail=loop+1) self.assertEqual(len(data), 2) exp = buf[-1] & ~((1<<(loop+1))-1) # print(f'{data[-1]:08b} {exp:08b}') self.assertEqual(data[0], 0xFF) self.assertEqual(data[-1], exp) class SpiCsForceTestCase(unittest.TestCase): """Basic test for exercing direct /CS control. It requires a scope or a digital analyzer to validate the signal waveforms. """ @classmethod def setUpClass(cls): cls.url = environ.get('FTDI_DEVICE', 'ftdi:///1') cls.debug = to_bool(environ.get('FTDI_DEBUG', 'off')) def setUp(self): self._spi = SpiController(cs_count=1) self._spi.configure(self.url, debug=self.debug) self._port = self._spi.get_port(0, freq=1E6, mode=0) def tearDown(self): """Close the SPI connection""" self._spi.terminate() def test_cs_default_pulse(self): for _ in range(5): self._port.force_select() def test_cs_long_pulse(self): for _ in range(5): self._port.force_select(cs_hold=200) def test_cs_manual_pulse(self): for _ in range(5): self._port.force_select(level=False) self._port.force_select(level=True) # beware that random USB bus access does not allow to create # precise delays. This is only the shorter bound, longer one is # not defined sleep(100e-6) def test_cs_pulse_write(self): self._port.force_select() self._port.write([0x00, 0x01, 0x02]) def test_cs_default_pulse_rev_clock(self): if not self._spi.is_inverted_cpha_supported: self.skipTest('FTDI does not support mode 3') self._port.set_mode(3) for _ in range(5): self._port.force_select() def suite(): suite_ = unittest.TestSuite() # suite_.addTest(unittest.makeSuite(SpiTestCase, 'test')) # suite_.addTest(unittest.makeSuite(SpiGpioTestCase, 'test')) suite_.addTest(unittest.makeSuite(SpiUnalignedTestCase, 'test')) suite_.addTest(unittest.makeSuite(SpiCsForceTestCase, 'test')) return suite_ def main(): testmod(modules[__name__]) FtdiLogger.log.addHandler(logging.StreamHandler(stdout)) level = environ.get('FTDI_LOGLEVEL', 'info').upper() try: loglevel = getattr(logging, level) except AttributeError as exc: raise ValueError(f'Invalid log level: {level}') from exc FtdiLogger.set_level(loglevel) unittest.main(defaultTest='suite') if __name__ == '__main__': main() pyftdi-0.54.0/pyftdi/tests/timearray.py000066400000000000000000000056411421346242200201150ustar00rootroot00000000000000#!/usr/bin/env python3 """Quick and dirty bytearray vs. array('B') performance test.""" from array import array from struct import pack from timeit import timeit from time import perf_counter def timing(f, n, a): start = perf_counter() for _ in range(n): f(a); f(a); f(a); f(a); f(a); f(a); f(a); f(a); f(a); f(a) finish = perf_counter() return '%s\t%f' % (f.__name__, finish - start) def time_array(addr): return array('B', addr) def time_bytearray(addr): return bytearray(addr) def extend_array(addr): b = bytearray() b.extend(addr) b.extend(b) b.extend(b) b.extend(b) b.extend(b) b.extend(b) return b def extend_bytearray(addr): b = bytearray() b.extend(addr) b.extend(b) b.extend(b) b.extend(b) b.extend(b) b.extend(b) return b def array_tostring(addr): return array('B', addr).tobytes() def str_bytearray(addr): return str(bytearray(addr)) def struct_pack(addr): return pack('4B', *addr) def main(): count = 100000 addr = '192.168.4.2' addr = tuple([int(i) for i in addr.split('.')]) print('\t\ttiming\t\tfunc\t\tno func') print('%s\t%s\t%s' % (timing(time_array, count, addr), timeit('time_array((192,168,4,2))', number=count, setup='from __main__ import time_array'), timeit("array('B', (192,168,4,2))", number=count, setup='from array import array'))) print('%s\t%s\t%s' % (timing(time_bytearray, count, addr), timeit('time_bytearray((192,168,4,2))', number=count, setup='from __main__ import time_bytearray'), timeit('bytearray((192,168,4,2))', number=count))) print('%s\t%s' % (timing(extend_array, count, addr), timeit('extend_array((192,168,4,2))', number=count, setup='from __main__ import extend_array'))) print('%s\t%s' % (timing(extend_bytearray, count, addr), timeit('extend_bytearray((192,168,4,2))', number=count, setup='from __main__ import extend_bytearray'))) print('%s\t%s\t%s' % (timing(array_tostring, count, addr), timeit('array_tostring((192,168,4,2))', number=count, setup='from __main__ import array_tostring'), timeit("array('B', (192,168,4,2)).tostring()", number=count, setup='from array import array'))) print('%s\t%s\t%s' % (timing(str_bytearray, count, addr), timeit('str_bytearray((192,168,4,2))', number=count, setup='from __main__ import str_bytearray'), timeit('str(bytearray((192,168,4,2)))', number=count))) print('%s\t%s\t%s' % (timing(struct_pack, count, addr), timeit('struct_pack((192,168,4,2))', number=count, setup='from __main__ import struct_pack'), timeit("pack('4B', *(192,168,4,2))", number=count, setup='from struct import pack'))) if __name__ == '__main__': main() pyftdi-0.54.0/pyftdi/tests/toolsimport.py000077500000000000000000000037031421346242200205130ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2020, Emmanuel Blot # All rights reserved. #pylint: disable-msg=empty-docstring #pylint: disable-msg=missing-docstring #pylint: disable-msg=no-self-use #pylint: disable-msg=invalid-name #pylint: disable-msg=global-statement from doctest import testmod from importlib import import_module from os.path import dirname, join as joinpath from sys import modules, path as syspath from unittest import TestCase, TestSuite, makeSuite, main as ut_main class ToolsTestCase(TestCase): """Test tool suite can be loaded. This is especially useful to find Python syntax version mismatch and other not-yet-supported modules/features. PyFtdi and tools should support Python 3.7 onwards. """ @classmethod def setUpClass(cls): tools_path = joinpath(dirname(dirname(__file__)), 'bin') syspath.append(tools_path) def test_ftconf(self): """Test ftconf.py tool""" mod = import_module('ftconf') self.assertIsNot(getattr(mod, 'main', None), mod) self.assertIsNot(mod.__doc__, None) def test_i2cscan(self): """Test ftconf.py tool""" mod = import_module('i2cscan') self.assertIsNot(getattr(mod, 'main', None), mod) self.assertIsNot(mod.__doc__, None) def test_pyterm(self): """Test ftconf.py tool""" mod = import_module('pyterm') self.assertIsNot(getattr(mod, 'main', None), mod) self.assertIsNot(mod.__doc__, None) def test_ftdi_urls(self): """Test ftconf.py tool""" mod = import_module('ftdi_urls') self.assertIsNot(getattr(mod, 'main', None), mod) self.assertIsNot(mod.__doc__, None) def suite(): suite_ = TestSuite() suite_.addTest(makeSuite(ToolsTestCase, 'test')) return suite_ def main(): testmod(modules[__name__]) ut_main(defaultTest='suite') if __name__ == '__main__': main() pyftdi-0.54.0/pyftdi/tests/uart.py000077500000000000000000000316651421346242200171030ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2017-2020, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import logging from doctest import testmod from os import environ from multiprocessing import Process, set_start_method from random import choice, seed from string import printable from struct import calcsize as scalc, pack as spack, unpack as sunpack from sys import modules, platform, stdout from time import sleep, time as now from threading import Thread from unittest import TestCase, TestSuite, skipIf, makeSuite, main as ut_main from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi from pyftdi.misc import to_bool from pyftdi.serialext import serial_for_url #pylint: disable-msg=missing-docstring #pylint: disable-msg=protected-access # Specify the second port for multi port device # Unfortunately, auto detection triggers some issue in multiprocess test URL_BASE = environ.get('FTDI_DEVICE', 'ftdi:///2') URL = ''.join((URL_BASE[:-1], '1')) IFCOUNT = int(URL_BASE[-1]) class FtdiTestCase(TestCase): """Common features for all tests. """ @classmethod def setUpClass(cls): cls.debug = to_bool(environ.get('FTDI_DEBUG', 'off'), permissive=False) def setUp(self): if self.debug: print('.'.join(self.id().split('.')[-2:])) class UartTestCase(FtdiTestCase): """FTDI UART driver test case Simple test to demonstrate UART feature. Depending on your FTDI device, you need to either: * connect RXD and TXD on the unique port for 1-port FTDI device (AD0 and AD1) * connect RXD1 and TXD2, and RXD2 and TXD1 on the two first port of 2- or 4- port FTDI devices (AD0 - BD1 and AD1 - BD0) Do NOT run this test if you use FTDI port(s) as an SPI or I2C bridge -or any unsupported setup!! You've been warned. """ COUNT = 512 @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() seed() @skipIf(IFCOUNT < 2, 'Device has not enough UART interfaces') def test_uart_cross_talk_sp(self): """Exchange a random byte stream between the two first UART interfaces of the same FTDI device, from the same process This also validates PyFtdi support to use several interfaces on the same FTDI device from the same Python process """ something_out = self.generate_bytes() urla = URL urlb = self.build_next_url(urla) porta = serial_for_url(urla, baudrate=1000000) portb = serial_for_url(urlb, baudrate=1000000) try: if not porta.is_open: porta.open() if not portb.is_open: portb.open() # print("porta: %d:%d:%d" % porta.usb_path) # print("portb: %d:%d:%d" % portb.usb_path) porta.timeout = 1.0 portb.timeout = 1.0 something_out = self.generate_bytes() porta.write(something_out) something_in = portb.read(len(something_out)) self.assertEqual(len(something_in), len(something_out)) self.assertEqual(something_in, something_out) something_out = self.generate_bytes() portb.write(something_out) something_in = porta.read(len(something_out)) self.assertEqual(len(something_in), len(something_out)) self.assertEqual(something_in, something_out) finally: porta.close() portb.close() @skipIf(IFCOUNT < 2, 'Device has not enough UART interfaces') @skipIf(platform == 'win32', 'Not tested on Windows') def test_uart_cross_talk_mp(self): if IFCOUNT > 4: raise IOError('No FTDI device') urla = URL urlb = self.build_next_url(urla) something_out = self.generate_bytes() proca = Process(target=self._cross_talk_write_then_read, args=(urla, something_out)) procb = Process(target=self._cross_talk_read_then_write, args=(urlb, something_out)) # start B first to ensure RX port is open before TX port starts # emitting procb.start() sleep(0.25) proca.start() proca.join(2) procb.join(2) # although the failure is reported (and traceback shown) in the # subprocess, we still need to fail the main process test in this case exita = proca.exitcode exitb = procb.exitcode self.assertEqual(exita, 0) self.assertEqual(exitb, 0) @skipIf(IFCOUNT != 1, 'Test reserved for single-port FTDI device') def test_uart_loopback(self): """Exchange a random byte stream between the two first UART interfaces of the same FTDI device, from the same process This also validates PyFtdi support to use several interfaces on the same FTDI device from the same Python process """ something_out = self.generate_bytes() port = serial_for_url(URL, baudrate=1000000) for _ in range(10): try: if not port.is_open: port.open() port.timeout = 1.0 something_out = self.generate_bytes() port.write(something_out) something_in = port.read(len(something_out)) self.assertEqual(len(something_in), len(something_out)) self.assertEqual(something_in, something_out) finally: port.close() @skipIf(IFCOUNT < 2, 'Device has not enough UART interfaces') def test2_uart_cross_talk_speed(self): urla = URL urlb = self.build_next_url(urla) porta = serial_for_url(urla, baudrate=6000000) portb = serial_for_url(urlb, baudrate=6000000) size = int(1e6) results = [None, None] chunk = 537 source = Thread(target=self._stream_source, args=(porta, chunk, size, results), daemon=True) sink = Thread(target=self._stream_sink, args=(portb, size, results), daemon=True) sink.start() source.start() source.join() sleep(0.5) sink.join() if isinstance(results[1], Exception): #pylint: disable-msg=raising-bad-type raise results[1] #pylint: disable-msg=unpacking-non-sequence tsize, tdelta = results[0] rsize, rdelta = results[1] self.assertGreater(rsize, 0, 'Not data received') if self.debug: print(f'TX: {tsize} bytes, {tdelta*1000:.3f} ms, ' f'{int(8*tsize/tdelta):d} bps') print(f'RX: {rsize} bytes, {rdelta*1000:.3f} ms, ' f'{int(8*rsize/rdelta):d} bps') self.assertTrue(rsize >= tsize-4*chunk, 'Data loss') @skipIf(IFCOUNT != 1, 'Test reserved for single-port FTDI device') def test_loopback_talk_speed(self): port = serial_for_url(URL, baudrate=6000000) size = int(1e6) results = [None, None] chunk = 537 source = Thread(target=self._stream_source, args=(port, chunk, size, results), daemon=True) sink = Thread(target=self._stream_sink, args=(port, size, results), daemon=True) sink.start() source.start() source.join() sleep(0.5) sink.join() if isinstance(results[1], Exception): #pylint: disable-msg=raising-bad-type raise results[1] #pylint: disable-msg=unpacking-non-sequence tsize, tdelta = results[0] rsize, rdelta = results[1] self.assertGreater(rsize, 0, 'Not data received') if self.debug: print(f'TX: {tsize} bytes, {tdelta*1000:.3f} ms, ' f'{int(8*tsize/tdelta):d} bps') print(f'RX: {rsize} bytes, {rdelta*1000:.3f} ms, ' f'{int(8*rsize/rdelta):d} bps') self.assertTrue(rsize >= tsize-4*chunk, 'Data loss') @classmethod def _stream_source(cls, port, chunk, size, results): pos = 0 tx_size = 0 start = now() while tx_size < size: samples = spack('>%dI' % chunk, *range(pos, pos+chunk)) pos += chunk port.write(samples) tx_size += len(samples) if results[1] is not None: break delta = now()-start results[0] = tx_size, delta @classmethod def _stream_sink(cls, port, size, results): pos = 0 first = None data = bytearray() sample_size = scalc('>I') rx_size = 0 port.timeout = 1.0 start = now() while rx_size < size: buf = port.read(1024) if not buf: print('T') break rx_size += len(buf) data.extend(buf) sample_count = len(data)//sample_size length = sample_count*sample_size samples = sunpack('>%dI' % sample_count, data[:length]) data = data[length:] for sample in samples: if first is None: first = sample pos = sample continue pos += 1 if sample != pos: results[1] = AssertionError('Lost byte @ %d', pos-first) return delta = now()-start results[1] = (rx_size, delta) @classmethod def _cross_talk_write_then_read(cls, url, refstream): # use classmethod & direct AssertionError to avoid pickler issues # with multiprocessing: # "TypeError: cannot serialize '_io.TextIOWrapper' object" port = serial_for_url(url, baudrate=1000000) try: if not port.is_open: port.open() port.timeout = 5.0 port.write(refstream) instream = port.read(len(refstream)) if len(instream) != len(refstream): raise AssertionError('Stream length differ') # we expect the peer to return the same stream, inverted localstream = bytes(reversed(instream)) if localstream != refstream: raise AssertionError('Stream content differ') finally: port.close() @classmethod def _cross_talk_read_then_write(cls, url, refstream): # use classmethod & direct AssertionError to avoid pickler issues # with multiprocessing: # "TypeError: cannot serialize '_io.TextIOWrapper' object" port = serial_for_url(url, baudrate=1000000) try: if not port.is_open: port.open() port.timeout = 5.0 instream = port.read(len(refstream)) if len(instream) != len(refstream): raise AssertionError('Stream length differ') if instream != refstream: raise AssertionError('Stream content differ') # the peer expect us to return the same stream, inverted outstream = bytes(reversed(instream)) port.write(outstream) finally: port.close() @classmethod def generate_bytes(cls, count=0): return ''.join([choice(printable) for x in range(count or cls.COUNT)]).encode() @classmethod def build_next_url(cls, url): iface = int(url[-1]) iface = (iface + 1) % 3 return '%s%d' % (url[:-1], iface) class BaudrateTestCase(FtdiTestCase): """Simple test to check clock stretching cannot be overwritten with GPIOs. """ BAUDRATES = (300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 576000, 921600, 1000000, 1500000, 1843200, 2000000, 2500000, 3000000, 4000000, 6000000, 8000000, 12000000) def test(self): ftdi = Ftdi() ftdi.open_from_url(URL) for baudrate in self.BAUDRATES: actual, _, _ = ftdi._convert_baudrate(baudrate) ratio = baudrate/actual self.assertTrue(0.97 <= ratio <= 1.03, "Invalid baudrate") def suite(): suite_ = TestSuite() suite_.addTest(makeSuite(BaudrateTestCase, 'test')) suite_.addTest(makeSuite(UartTestCase, 'test')) return suite_ def main(): testmod(modules[__name__]) FtdiLogger.log.addHandler(logging.StreamHandler(stdout)) level = environ.get('FTDI_LOGLEVEL', 'info').upper() try: loglevel = getattr(logging, level) except AttributeError as exc: raise ValueError('Invalid log level: %s' % level) from exc FtdiLogger.set_level(loglevel) ut_main(defaultTest='suite') if __name__ == '__main__': if platform == 'darwin': # avoid the infamous "The process has forked and you cannot use this # CoreFoundation functionality safely. You MUST exec()." error on macOS set_start_method('spawn') main() pyftdi-0.54.0/pyftdi/tracer.py000066400000000000000000000416621421346242200162410ustar00rootroot00000000000000# Copyright (c) 2017-2020, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """MPSSE command debug tracer.""" #pylint: disable-msg=missing-docstring #pylint: disable-msg=too-many-instance-attributes from binascii import hexlify from collections import deque from inspect import currentframe from logging import getLogger from string import ascii_uppercase from struct import unpack as sunpack from typing import Union from .ftdi import Ftdi class FtdiMpsseTracer: """FTDI MPSSE protocol decoder.""" MPSSE_ENGINES = { 0x0200: 0, 0x0400: 0, 0x0500: 0, 0x0600: 0, 0x0700: 2, 0x0800: 2, 0x0900: 1, 0x1000: 0} """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('No MPSSE engine available on interface %d' % 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' def build_commands(prefix: str): commands = {} for cmd in dir(Ftdi): if cmd[0] not in ascii_uppercase: continue value = getattr(Ftdi, cmd) if not isinstance(value, int): continue family = cmd.split('_')[0] if family not in prefix.split(): continue commands[value] = cmd return commands COMMANDS = build_commands(COMMAND_PREFIX) 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 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, '_cmd_%s' % 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 as exc: 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, '_resp_%s' % 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() 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(' (%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(' 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, '0%db' % width) # 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 pyftdi-0.54.0/pyftdi/usbtools.py000066400000000000000000000662751421346242200166420ustar00rootroot00000000000000# Copyright (c) 2014-2020, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # 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-msg=broad-except #pylint: disable-msg=too-many-locals,too-many-branches,too-many-statements #pylint: disable-msg=too-many-arguments, too-many-nested-blocks 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) """ cls.Lock.acquire() try: 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) finally: cls.Lock.release() @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``. """ cls.Lock.acquire() cls.UsbDevices.clear() cls.Lock.release() @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 and FT4232H). 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 """ cls.Lock.acquire() try: 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] finally: cls.Lock.release() @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 cls.Lock.acquire() try: for devkey in cls.Devices: dev, refcount = cls.Devices[devkey] 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 finally: cls.Lock.release() @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. """ cls.Lock.acquire() try: remove_devs = set() 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) finally: cls.Lock.release() @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("Invalid URL: %s" % 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 else: interface = to_int(path) report_devices = False except (IndexError, ValueError) as exc: raise UsbToolsError('Invalid device URL: %s' % 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("%d USB devices match URL '%s'" % (len(candidates), urlstr)) idx = 0 try: desc, _ = candidates[idx] vendor, product = desc[:2] except IndexError: raise UsbToolsError('No USB device matches URL %s' % 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: raise UsbToolsError('Vendor ID %s not supported' % (vendor and '0x%04x' % vendor)) 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(): raise UsbToolsError('Product ID %s not supported' % (product and '0x%04x' % product)) 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('Product %s is not referenced' % plcomps[1]) from exc else: product = None except (IndexError, ValueError) as exc: raise UsbToolsError('Invalid device URL: %s' % 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('Invalid bus/address: %s' % ':'.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("No USB device with S/N %s" % 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 desc in devstrs: print((' %%-%ds %%s' % max_url_len) % 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 = '%04x' % desc.vid 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 = '%04x' % desc.pid 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 = '%d' % indices[ikey] else: serial = sernum if serial: parts.append(serial) elif desc.bus is not None and desc.address is not None: parts.append('%x' % desc.bus) parts.append('%x' % desc.address) # 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 = '(%s)' % 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-msg=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 """ cls.Lock.acquire() try: return cls._load_backend() finally: cls.Lock.release() @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(): 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 = dict() 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-msg=protected-access # need to access private member _ctx of PyUSB device # (resource manager) until PyUSB #302 is addressed return device._ctx.dev #pylint: disable-msg=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') pyftdi-0.54.0/requirements.txt000066400000000000000000000000451421346242200163620ustar00rootroot00000000000000pyusb>=1.0, != 1.2.0 pyserial >= 3.0 pyftdi-0.54.0/setup.cfg000066400000000000000000000002131421346242200147140ustar00rootroot00000000000000[bdist_wheel] [metadata] license_file = pyftdi/doc/license.rst [build_sphinx] source-dir = pyftdi/doc build-dir = sphinx all_files = 1 pyftdi-0.54.0/setup.py000077500000000000000000000145541421346242200146250ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (c) 2010-2021 Emmanuel Blot # Copyright (c) 2010-2016 Neotion # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause #pylint: disable-msg=unused-variable #pylint: disable-msg=missing-docstring #pylint: disable-msg=broad-except #pylint: disable-msg=no-self-use from codecs import open as codec_open from setuptools import find_packages, setup from setuptools.command.build_py import build_py from distutils.cmd import Command from distutils.log import DEBUG, INFO from os import close, getcwd, unlink, walk from os.path import abspath, dirname, join as joinpath, relpath from py_compile import compile as pycompile, PyCompileError from re import split as resplit, search as research from sys import stderr, exit as sysexit from tempfile import mkstemp NAME = 'pyftdi' PACKAGES = find_packages(where='.') META_PATH = joinpath('pyftdi', '__init__.py') KEYWORDS = ['driver', 'ftdi', 'usb', 'serial', 'spi', 'i2c', 'twi', 'rs232', 'gpio', 'bit-bang'] CLASSIFIERS = [ 'Development Status :: 4 - Beta', 'Environment :: Other Environment', 'Natural Language :: English', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Hardware :: Hardware Drivers', ] INSTALL_REQUIRES = [ 'pyusb >= 1.0.0, != 1.2.0', 'pyserial >= 3.0', ] TEST_REQUIRES = [ 'ruamel.yaml >= 0.16', ] HERE = abspath(dirname(__file__)) def read(*parts): """ Build an absolute path from *parts* and and return the contents of the resulting file. Assume UTF-8 encoding. """ with codec_open(joinpath(HERE, *parts), 'rb', 'utf-8') as dfp: return dfp.read() def read_desc(*parts): """Read and filter long description """ text = read(*parts) text = resplit(r'\.\.\sEOT', text)[0] return text META_FILE = read(META_PATH) def find_meta(meta): """ Extract __*meta*__ from META_FILE. """ meta_match = research( r"(?m)^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), META_FILE ) if meta_match: return meta_match.group(1) raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) class BuildPy(build_py): """Override byte-compile sequence to catch any syntax error issue. For some reason, distutils' byte-compile when it forks a sub-process to byte-compile a .py file into a .pyc does NOT check the success of the compilation. Therefore, any syntax error is explictly ignored, and no output file is generated. This ends up generating an incomplete package w/ a nevertheless successfull setup.py execution. Here, each Python file is build before invoking distutils, so that any syntax error is catched, raised and setup.py actually fails should this event arise. This step is critical to check that an unsupported syntax does not end up as a 'valid' package from setuptools perspective... """ def byte_compile(self, files): for file in files: if not file.endswith('.py'): continue pfd, pyc = mkstemp('.pyc') close(pfd) try: pycompile(file, pyc, doraise=True) continue except PyCompileError as exc: # avoid chaining exceptions print(str(exc), file=stderr) raise SyntaxError("Cannot byte-compile '%s'" % file) finally: unlink(pyc) super().byte_compile(files) class CheckStyle(Command): """A custom command to check Python coding style.""" description = 'check coding style' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): self.announce('checking coding style', level=INFO) filecount = 0 topdir = dirname(__file__) or getcwd() for dpath, dnames, fnames in walk(topdir): dnames[:] = [d for d in dnames if not d.startswith('.') and d != 'doc'] for filename in (joinpath(dpath, f) for f in fnames if f.endswith('.py')): self.announce('checking %s' % relpath(filename, topdir), level=INFO) with open(filename, 'rt') as pfp: for lpos, line in enumerate(pfp, start=1): if len(line) > 80: print('\n %d: %s' % (lpos, line.rstrip())) raise RuntimeError("Invalid line width '%s'" % relpath(filename, topdir)) filecount += 1 if not filecount: raise RuntimeError('No Python file found from "%s"' % topdir) def main(): setup( cmdclass={ 'build_py': BuildPy, 'check_style': CheckStyle }, name=NAME, description=find_meta('description'), license=find_meta('license'), url=find_meta('uri'), version=find_meta('version'), author=find_meta('author'), author_email=find_meta('email'), maintainer=find_meta('author'), maintainer_email=find_meta('email'), keywords=KEYWORDS, long_description=read_desc('pyftdi/doc/index.rst'), packages=PACKAGES, scripts=['pyftdi/bin/i2cscan.py', 'pyftdi/bin/ftdi_urls.py', 'pyftdi/bin/ftconf.py', 'pyftdi/bin/pyterm.py'], package_dir={'': '.'}, package_data={'pyftdi': ['*.rst', 'doc/*.rst', 'doc/api/*.rst', 'INSTALL'], 'pyftdi.serialext': ['*.rst', 'doc/api/uart.rst']}, classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, test_requires=TEST_REQUIRES, python_requires='>=3.7', ) if __name__ == '__main__': try: main() except Exception as exc: print(exc, file=stderr) sysexit(1)