From c93e1e7d196d497c94dcca0182f60c65a162e72b Mon Sep 17 00:00:00 2001 From: Ted Larsson Date: Wed, 7 Jul 2021 16:26:52 +0200 Subject: [PATCH 1/5] Support for CS active high CS lines can now be active high. Ideas taken from https://github.com/eblot/pyftdi/pull/86/files Main difference is that we can individually select which CS lines that shall be active high. This is done using a bitfield. --- pyftdi/spi.py | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/pyftdi/spi.py b/pyftdi/spi.py index a24d828e..f776471f 100644 --- a/pyftdi/spi.py +++ b/pyftdi/spi.py @@ -160,7 +160,12 @@ def set_mode(self, mode: int, cs_hold: Optional[int] = None) -> None: 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) | + + cs_bits = self._controller._cs_bits + + cs_bit_sel = ((SpiController.CS_BIT << self._cs) ^ + (cs_bits & ~self._controller._cs_idle)) + cs_select = 0xFF & ~(cs_bit_sel | (int(not self._cpol) and SpiController.SCK_BIT) | SpiController.DO_BIT) self._cs_prolog = bytes([cs_clock, cs_select]) @@ -354,10 +359,11 @@ def __init__(self, cs_count: int = 1, turbo: bool = True): self._immediate = bytes((Ftdi.SEND_IMMEDIATE,)) self._frequency = 0.0 self._clock_phase = False - self._cs_bits = 0 + self._cs_idle = 0 self._spi_ports = [] self._spi_dir = 0 self._spi_mask = self.SPI_BITS + self._cs_act_hi = 0 def configure(self, url: Union[str, UsbDevice], **kwargs: Mapping[str, Any]) -> None: @@ -384,6 +390,9 @@ def configure(self, url: Union[str, UsbDevice], frequency. * ``cs_count`` count of chip select signals dedicated to select SPI slave devices, starting from A*BUS3 pin + * ``cs_act_hi`` a bitfield specifying which SPI CS lines are active + high. Bit 4 is the first CS, bit 5 the second and so on. Bits + corresponding to pins not used/configured as CS are ignored. * ``turbo`` whether to enable or disable turbo mode * ``debug`` to increase log verbosity, using MPSSE tracer """ @@ -396,6 +405,9 @@ def configure(self, url: Union[str, UsbDevice], if not 1 <= self._cs_count <= 5: raise ValueError('Unsupported CS line count: %d' % self._cs_count) + if 'cs_act_hi' in kwargs: + self._cs_act_hi = int(kwargs['cs_act_hi']) & self._cs_bits + del kwargs['cs_act_hi'] if 'turbo' in kwargs: self._turbo = bool(kwargs['turbo']) del kwargs['turbo'] @@ -419,19 +431,21 @@ def configure(self, url: Union[str, UsbDevice], 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)) + + cs_bits = self._cs_bits + self._cs_idle = (~self._cs_act_hi) & cs_bits + self._spi_ports = [None] * self._cs_count - self._spi_dir = (self._cs_bits | + self._spi_dir = (cs_bits | SpiController.DO_BIT | SpiController.SCK_BIT) - self._spi_mask = self._cs_bits | self.SPI_BITS + self._spi_mask = 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) + kwargs['initial'] = self._cs_idle | (io_out & self._gpio_mask) if not isinstance(url, str): self._frequency = self._ftdi.open_mpsse_from_device( url, interface=interface, **kwargs) @@ -583,6 +597,17 @@ def gpio_all_pins(self): with self._lock: return mask & ~self._spi_mask + @property + def _cs_bits(self): + """Report the configured CS pins as a bitfield. + + A true bit represents a pin configured as a SPI CS. + + :return: the bitfield of configured CS pins. + """ + return (((SpiController.CS_BIT << self._cs_count) - 1) & + ~(SpiController.CS_BIT - 1)) + @property def width(self): """Report the FTDI count of addressable pins. @@ -800,7 +825,7 @@ def _exchange_half_duplex(self, frequency: float, 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, + cs_high = [Ftdi.SET_BITS_LOW, self._cs_idle | self._gpio_low, direction] if not self._turbo: cs_high.append(Ftdi.SEND_IMMEDIATE) @@ -907,7 +932,7 @@ def _exchange_full_duplex(self, frequency: float, 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, + cs_high = [Ftdi.SET_BITS_LOW, self._cs_idle | self._gpio_low, direction] if not self._turbo: cs_high.append(Ftdi.SEND_IMMEDIATE) From 2178afae9f1c58a55e68b0651648b20779a48a36 Mon Sep 17 00:00:00 2001 From: Ted Larsson Date: Sat, 10 Jul 2021 13:05:16 +0200 Subject: [PATCH 2/5] Improved handling of CS active high Property not needed - static bit mask better/faster. --- pyftdi/spi.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/pyftdi/spi.py b/pyftdi/spi.py index f776471f..75dfd11f 100644 --- a/pyftdi/spi.py +++ b/pyftdi/spi.py @@ -359,11 +359,12 @@ def __init__(self, cs_count: int = 1, turbo: bool = True): self._immediate = bytes((Ftdi.SEND_IMMEDIATE,)) self._frequency = 0.0 self._clock_phase = False + self._cs_bits = 0 self._cs_idle = 0 + self._cs_act_hi = 0 self._spi_ports = [] self._spi_dir = 0 self._spi_mask = self.SPI_BITS - self._cs_act_hi = 0 def configure(self, url: Union[str, UsbDevice], **kwargs: Mapping[str, Any]) -> None: @@ -406,7 +407,7 @@ def configure(self, url: Union[str, UsbDevice], raise ValueError('Unsupported CS line count: %d' % self._cs_count) if 'cs_act_hi' in kwargs: - self._cs_act_hi = int(kwargs['cs_act_hi']) & self._cs_bits + self._cs_act_hi = int(kwargs['cs_act_hi']) del kwargs['cs_act_hi'] if 'turbo' in kwargs: self._turbo = bool(kwargs['turbo']) @@ -431,15 +432,15 @@ def configure(self, url: Union[str, UsbDevice], with self._lock: if self._frequency > 0.0: raise SpiIOError('Already configured') - - cs_bits = self._cs_bits - self._cs_idle = (~self._cs_act_hi) & cs_bits - + self._cs_bits = (((SpiController.CS_BIT << self._cs_count) - 1) & + ~(SpiController.CS_BIT - 1)) + self._cs_act_hi &= self._cs_bits + self._cs_idle = (~self._cs_act_hi) & self._cs_bits self._spi_ports = [None] * self._cs_count - self._spi_dir = (cs_bits | + self._spi_dir = (self._cs_bits | SpiController.DO_BIT | SpiController.SCK_BIT) - self._spi_mask = cs_bits | self.SPI_BITS + 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 @@ -597,17 +598,6 @@ def gpio_all_pins(self): with self._lock: return mask & ~self._spi_mask - @property - def _cs_bits(self): - """Report the configured CS pins as a bitfield. - - A true bit represents a pin configured as a SPI CS. - - :return: the bitfield of configured CS pins. - """ - return (((SpiController.CS_BIT << self._cs_count) - 1) & - ~(SpiController.CS_BIT - 1)) - @property def width(self): """Report the FTDI count of addressable pins. From 747c313550b285b305e1aff5b054166379b37be1 Mon Sep 17 00:00:00 2001 From: Ted Larsson Date: Sat, 10 Jul 2021 14:39:45 +0200 Subject: [PATCH 3/5] BUgfix: handle several CS lines properly --- pyftdi/spi.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyftdi/spi.py b/pyftdi/spi.py index 75dfd11f..c0829b28 100644 --- a/pyftdi/spi.py +++ b/pyftdi/spi.py @@ -160,11 +160,9 @@ def set_mode(self, mode: int, cs_hold: Optional[int] = None) -> None: self._cpha = bool(mode & 0x1) cs_clock = 0xFF & ~((int(not self._cpol) and SpiController.SCK_BIT) | SpiController.DO_BIT) - - cs_bits = self._controller._cs_bits - - cs_bit_sel = ((SpiController.CS_BIT << self._cs) ^ - (cs_bits & ~self._controller._cs_idle)) + cs_bit_sel = ~((self._controller._cs_idle + ^ (SpiController.CS_BIT << self._cs)) + & self._controller._cs_bits) cs_select = 0xFF & ~(cs_bit_sel | (int(not self._cpol) and SpiController.SCK_BIT) | SpiController.DO_BIT) From 5e3a7e1c8f02b9d6a42bc28bf97ba0adadcad7b0 Mon Sep 17 00:00:00 2001 From: Ted Larsson Date: Tue, 24 Aug 2021 11:15:26 +0200 Subject: [PATCH 4/5] SPI: better API for CS active high More user friendly API for configuring active high CS lines. Can now check the configuration, both on the controller and on each port. Also add a minimal unit test that verifies we can set cs_act_hi and that the settings seem to take effect. --- pyftdi/spi.py | 43 ++++++++++++++++++++++++++++++++++++++----- pyftdi/tests/spi.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/pyftdi/spi.py b/pyftdi/spi.py index c0829b28..0e30f8d3 100644 --- a/pyftdi/spi.py +++ b/pyftdi/spi.py @@ -17,7 +17,7 @@ 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 typing import Any, Iterable, List, Mapping, Optional, Set, Union from usb.core import Device as UsbDevice from .ftdi import Ftdi, FtdiError @@ -220,6 +220,14 @@ def cs(self) -> int: """ return self._cs + @property + def cs_act_hi(self) -> bool: + """Is the CS line active high? + + :return: the CS polarity configuration + """ + return self._cs in self._controller.cs_act_hi + @property def mode(self) -> int: """Return the current SPI mode. @@ -389,9 +397,8 @@ def configure(self, url: Union[str, UsbDevice], frequency. * ``cs_count`` count of chip select signals dedicated to select SPI slave devices, starting from A*BUS3 pin - * ``cs_act_hi`` a bitfield specifying which SPI CS lines are active - high. Bit 4 is the first CS, bit 5 the second and so on. Bits - corresponding to pins not used/configured as CS are ignored. + * ``cs_act_hi`` an iterable specifying which SPI CS lines are active + high. Example: ``cs_act_hi=(0, 3)``. * ``turbo`` whether to enable or disable turbo mode * ``debug`` to increase log verbosity, using MPSSE tracer """ @@ -405,7 +412,7 @@ def configure(self, url: Union[str, UsbDevice], raise ValueError('Unsupported CS line count: %d' % self._cs_count) if 'cs_act_hi' in kwargs: - self._cs_act_hi = int(kwargs['cs_act_hi']) + self._parse_cs_active_high_arg(kwargs['cs_act_hi']) del kwargs['cs_act_hi'] if 'turbo' in kwargs: self._turbo = bool(kwargs['turbo']) @@ -571,6 +578,20 @@ def active_channels(self) -> Set[int]: """ return {port[0] for port in enumerate(self._spi_ports) if port[1]} + @property + def cs_act_hi(self) -> List[int]: + """Provide the active high CS lines. + + :return: List of channel numbers that are active high + """ + result = [] + mask = 0x08 + for i in range(self._cs_count): + if self._cs_act_hi & mask: + result.append(i) + mask <<= 1 + return result + @property def gpio_pins(self): """Report the configured GPIOs as a bitfield. @@ -724,6 +745,18 @@ def _set_gpio_direction(self, width: int, pins: int, self._gpio_dir |= (pins & direction) self._gpio_mask = gpio_mask & pins + def _parse_cs_active_high_arg(self, + cs_lines_hi: Iterable[int]) -> None: + bad_args = [] + for cs_line in cs_lines_hi: + if not 0 <= cs_line < self._cs_count: + bad_args.append(cs_line) + mask = 1 << (cs_line + 3) + self._cs_act_hi |= mask + if bad_args: + raise SpiIOError('Invalid active high CS lines: ' + + ', '.join([str(i) for i in bad_args])) + def _read_raw(self, read_high: bool) -> int: if not self._ftdi.is_connected: raise SpiIOError("FTDI controller not initialized") diff --git a/pyftdi/tests/spi.py b/pyftdi/tests/spi.py index 1753c872..66fa59a0 100755 --- a/pyftdi/tests/spi.py +++ b/pyftdi/tests/spi.py @@ -361,6 +361,34 @@ def test_cs_default_pulse_rev_clock(self): for _ in range(5): self._port.force_select() +class SpiCsActiveHighTestCase(unittest.TestCase): + """Basic test for checking CS active high function + + 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, cs_count=2, + cs_act_hi=[1]) + self._port0 = self._spi.get_port(0, freq=1E6, mode=0) + self._port1 = self._spi.get_port(1, freq=1E6, mode=0) + + def tearDown(self): + """Close the SPI connection""" + self._spi.terminate() + + def test_cs_config(self): + assert self._spi.cs_act_hi == [1] + assert not self._port0.cs_act_hi + assert self._port1.cs_act_hi + def suite(): suite_ = unittest.TestSuite() @@ -368,6 +396,7 @@ def suite(): # suite_.addTest(unittest.makeSuite(SpiGpioTestCase, 'test')) suite_.addTest(unittest.makeSuite(SpiUnalignedTestCase, 'test')) suite_.addTest(unittest.makeSuite(SpiCsForceTestCase, 'test')) + # suite_.addTest(unittest.makeSuite(SpiCsActiveHighTestCase, 'test')) return suite_ From 775dd18fbc7fd4784c92899dd80eb04c3be14fcd Mon Sep 17 00:00:00 2001 From: Ted Larsson Date: Wed, 25 Aug 2021 08:29:52 +0200 Subject: [PATCH 5/5] SPI: fix unit test for CS active high We are using unittest, not pytest... --- pyftdi/tests/spi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyftdi/tests/spi.py b/pyftdi/tests/spi.py index 66fa59a0..a5d12ab2 100755 --- a/pyftdi/tests/spi.py +++ b/pyftdi/tests/spi.py @@ -385,9 +385,9 @@ def tearDown(self): self._spi.terminate() def test_cs_config(self): - assert self._spi.cs_act_hi == [1] - assert not self._port0.cs_act_hi - assert self._port1.cs_act_hi + self.assertEqual(self._spi.cs_act_hi, [1]) + self.assertFalse(self._port0.cs_act_hi) + self.assertTrue(self._port1.cs_act_hi) def suite():