diff --git a/docs/images/spi_device_hierarchy.dot b/docs/images/spi_device_hierarchy.dot index aa0769a..a205b03 100644 --- a/docs/images/spi_device_hierarchy.dot +++ b/docs/images/spi_device_hierarchy.dot @@ -11,6 +11,7 @@ digraph classes { MCP3xxx; MCP30xx; MCP32xx; + MCP3xx2; MCP33xx; /* Concrete classes */ @@ -21,6 +22,8 @@ digraph classes { MCP30xx->MCP3xxx; MCP32xx->MCP3xxx; MCP33xx->MCP3xxx; + MCP3xx2->MCP3xxx; + MCP3001->MCP30xx; MCP3002->MCP30xx; MCP3004->MCP30xx; @@ -29,6 +32,8 @@ digraph classes { MCP3202->MCP32xx; MCP3204->MCP32xx; MCP3208->MCP32xx; + MCP3002->MCP3xx2; + MCP3202->MCP3xx2; MCP3301->MCP33xx; MCP3302->MCP33xx; MCP3304->MCP33xx; diff --git a/docs/images/spi_device_hierarchy.pdf b/docs/images/spi_device_hierarchy.pdf index 9a75196..65ba0cd 100644 Binary files a/docs/images/spi_device_hierarchy.pdf and b/docs/images/spi_device_hierarchy.pdf differ diff --git a/docs/images/spi_device_hierarchy.png b/docs/images/spi_device_hierarchy.png index c52cbf3..7639917 100644 Binary files a/docs/images/spi_device_hierarchy.png and b/docs/images/spi_device_hierarchy.png differ diff --git a/docs/images/spi_device_hierarchy.svg b/docs/images/spi_device_hierarchy.svg index 0412455..b801f8e 100644 --- a/docs/images/spi_device_hierarchy.svg +++ b/docs/images/spi_device_hierarchy.svg @@ -1,7 +1,7 @@ - Device - -Device + +Device SPIDevice - -SPIDevice + +SPIDevice SPIDevice->Device - - + + AnalogInputDevice - -AnalogInputDevice + +AnalogInputDevice AnalogInputDevice->SPIDevice - - + + MCP3xxx - -MCP3xxx + +MCP3xxx MCP3xxx->AnalogInputDevice - - + + MCP30xx @@ -51,138 +51,158 @@ MCP30xx->MCP3xxx - - + + MCP32xx - -MCP32xx + +MCP32xx MCP32xx->MCP3xxx - - + + + + +MCP3xx2 + +MCP3xx2 + + +MCP3xx2->MCP3xxx + + -MCP33xx +MCP33xx MCP33xx MCP33xx->MCP3xxx - - + + -MCP3001 - -MCP3001 +MCP3001 + +MCP3001 -MCP3001->MCP30xx - - - - -MCP3002 - -MCP3002 - - -MCP3002->MCP30xx +MCP3001->MCP30xx + +MCP3002 + +MCP3002 + + +MCP3002->MCP30xx + + + + +MCP3002->MCP3xx2 + + + -MCP3004 +MCP3004 MCP3004 -MCP3004->MCP30xx +MCP3004->MCP30xx -MCP3008 - -MCP3008 +MCP3008 + +MCP3008 -MCP3008->MCP30xx - - +MCP3008->MCP30xx + + -MCP3201 - -MCP3201 +MCP3201 + +MCP3201 -MCP3201->MCP32xx - - +MCP3201->MCP32xx + + -MCP3202 - -MCP3202 +MCP3202 + +MCP3202 -MCP3202->MCP32xx - - +MCP3202->MCP32xx + + + + +MCP3202->MCP3xx2 + + -MCP3204 - -MCP3204 +MCP3204 + +MCP3204 -MCP3204->MCP32xx - - +MCP3204->MCP32xx + + -MCP3208 - -MCP3208 +MCP3208 + +MCP3208 -MCP3208->MCP32xx - - +MCP3208->MCP32xx + + -MCP3301 - -MCP3301 +MCP3301 + +MCP3301 -MCP3301->MCP33xx - - - - -MCP3302 - -MCP3302 - - -MCP3302->MCP33xx +MCP3301->MCP33xx - -MCP3304 + +MCP3302 -MCP3304 +MCP3302 - -MCP3304->MCP33xx + +MCP3302->MCP33xx + +MCP3304 + +MCP3304 + + +MCP3304->MCP33xx + + + diff --git a/gpiozero/exc.py b/gpiozero/exc.py index 1afd214..aedc96f 100644 --- a/gpiozero/exc.py +++ b/gpiozero/exc.py @@ -49,6 +49,9 @@ class SPIError(GPIOZeroError): class SPIBadArgs(SPIError, ValueError): "Error raised when invalid arguments are given while constructing :class:`SPIDevice`" +class SPIBadChannel(SPIError, ValueError): + "Error raised when an invalid channel is given to an :class:`AnalogInputDevice`" + class GPIODeviceError(GPIOZeroError): "Base class for errors specific to the GPIODevice hierarchy" diff --git a/gpiozero/pins/mock.py b/gpiozero/pins/mock.py index f865c30..f12c8cc 100644 --- a/gpiozero/pins/mock.py +++ b/gpiozero/pins/mock.py @@ -54,7 +54,8 @@ class MockPin(Pin): self._when_changed = None self.clear_states() return self - if old_pin.__class__ != cls: + # Ensure the pin class expected supports PWM (or not) + if issubclass(cls, MockPWMPin) != isinstance(old_pin, MockPWMPin): raise ValueError('pin %d is already in use as a %s' % (number, old_pin.__class__.__name__)) return old_pin @@ -249,7 +250,6 @@ class MockPWMPin(MockPin): """ This derivative of :class:`MockPin` adds PWM support. """ - def __init__(self, number): super(MockPWMPin, self).__init__() self._frequency = None @@ -275,3 +275,141 @@ class MockPWMPin(MockPin): if value is None: self._change_state(0.0) + +class MockSPIClockPin(MockPin): + """ + This derivative of :class:`MockPin` is intended to be used as the clock pin + of a mock SPI device. It is not intended for direct construction in tests; + rather, construct a :class:`MockSPIDevice` with various pin numbers, and + this class will be used for the clock pin. + """ + def __init__(self, number): + super(MockSPIClockPin, self).__init__() + if not hasattr(self, 'spi_devices'): + self.spi_devices = [] + + def _set_state(self, value): + super(MockSPIClockPin, self)._set_state(value) + for dev in self.spi_devices: + dev.on_clock() + + +class MockSPISelectPin(MockPin): + """ + This derivative of :class:`MockPin` is intended to be used as the select + pin of a mock SPI device. It is not intended for direct construction in + tests; rather, construct a :class:`MockSPIDevice` with various pin numbers, + and this class will be used for the select pin. + """ + def __init__(self, number): + super(MockSPISelectPin, self).__init__() + if not hasattr(self, 'spi_device'): + self.spi_device = None + + def _set_state(self, value): + super(MockSPISelectPin, self)._set_state(value) + if self.spi_device: + self.spi_device.on_select() + + +class MockSPIDevice(object): + def __init__( + self, clock_pin, mosi_pin, miso_pin, select_pin=None, + clock_polarity=False, clock_phase=False, lsb_first=False, + bits_per_word=8, select_high=False): + self.clock_pin = MockSPIClockPin(clock_pin) + self.mosi_pin = None if mosi_pin is None else MockPin(mosi_pin) + self.miso_pin = None if miso_pin is None else MockPin(miso_pin) + self.select_pin = None if select_pin is None else MockSPISelectPin(select_pin) + self.clock_polarity = clock_polarity + self.clock_phase = clock_phase + self.lsb_first = lsb_first + self.bits_per_word = bits_per_word + self.select_high = select_high + self.rx_bit = 0 + self.rx_buf = [] + self.tx_buf = [] + self.clock_pin.spi_devices.append(self) + self.select_pin.spi_device = self + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.close() + + def close(self): + if self in self.clock_pin.spi_devices: + self.clock_pin.spi_devices.remove(self) + if self.select_pin is not None: + self.select_pin.spi_device = None + + def on_select(self): + if self.select_pin.state == self.select_high: + self.on_start() + + def on_clock(self): + # Don't do anything if this SPI device isn't currently selected + if self.select_pin is None or self.select_pin.state == self.select_high: + # The XOR of the clock pin's values, polarity and phase indicates + # whether we're meant to be acting on this edge + if self.clock_pin.state ^ self.clock_polarity ^ self.clock_phase: + self.rx_bit += 1 + if self.mosi_pin is not None: + self.rx_buf.append(self.mosi_pin.state) + if self.miso_pin is not None: + try: + tx_value = self.tx_buf.pop(0) + except IndexError: + tx_value = 0 + if tx_value: + self.miso_pin.drive_high() + else: + self.miso_pin.drive_low() + self.on_bit() + + def on_start(self): + """ + Override this in descendents to detect when the mock SPI device's + select line is activated. + """ + self.rx_bit = 0 + self.rx_buf = [] + self.tx_buf = [] + + def on_bit(self): + """ + Override this in descendents to react to receiving a bit. + + The :attr:`rx_bit` attribute gives the index of the bit received (this + is reset to 0 by default by :meth:`on_select`). The :attr:`rx_buf` + sequence gives the sequence of 1s and 0s that have been recevied so + far. The :attr:`tx_buf` sequence gives the sequence of 1s and 0s to + transmit on the next clock pulses. All these attributes can be modified + within this method. + + The :meth:`rx_word` and :meth:`tx_word` methods can also be used to + read and append to the buffers using integers instead of bool bits. + """ + pass + + def rx_word(self): + result = 0 + bits = reversed(self.rx_buf) if self.lsb_first else self.rx_buf + for bit in bits: + result <<= 1 + result |= bit + return result + + def tx_word(self, value, bits_per_word=None): + if bits_per_word is None: + bits_per_word = self.bits_per_word + bits = [0] * bits_per_word + for bit in range(bits_per_word): + bits[bit] = value & 1 + value >>= 1 + assert not value + if not self.lsb_first: + bits = reversed(bits) + self.tx_buf.extend(bits) + diff --git a/gpiozero/spi.py b/gpiozero/spi.py index 431c73e..e89ab98 100644 --- a/gpiozero/spi.py +++ b/gpiozero/spi.py @@ -85,16 +85,16 @@ class SPIHardwareInterface(Device): self._device.mode = value def _get_clock_polarity(self): - return bool(self.mode & 2) + return bool(self.clock_mode & 2) def _set_clock_polarity(self, value): - self.mode = self.mode & (~2) | (bool(value) << 1) + self.clock_mode = self.clock_mode & (~2) | (bool(value) << 1) def _get_clock_phase(self): - return bool(self.mode & 1) + return bool(self.clock_mode & 1) def _set_clock_phase(self, value): - self.mode = self.mode & (~1) | bool(value) + self.clock_mode = self.clock_mode & (~1) | bool(value) def _get_lsb_first(self): return self._device.lsbfirst @@ -130,9 +130,6 @@ class SPISoftwareBus(SharedMixin, Device): self.miso = None super(SPISoftwareBus, self).__init__() self.lock = RLock() - self.clock_phase = False - self.lsb_first = False - self.bits_per_word = 8 try: self.clock = OutputDevice(clock_pin, active_high=True) if mosi_pin is not None: @@ -166,13 +163,7 @@ class SPISoftwareBus(SharedMixin, Device): def _shared_key(cls, clock_pin, mosi_pin, miso_pin): return (clock_pin, mosi_pin, miso_pin) - def read(self, n): - return self.transfer((0,) * n) - - def write(self, data): - return len(self.transfer(data)) - - def transfer(self, data): + def transfer(self, data, clock_phase=False, lsb_first=False, bits_per_word=8): """ Writes data (a list of integer words where each word is assumed to have :attr:`bits_per_word` bits or less) to the SPI interface, and reads an @@ -180,19 +171,19 @@ class SPISoftwareBus(SharedMixin, Device): """ result = [] with self.lock: - shift = operator.lshift if self.lsb_first else operator.rshift + shift = operator.lshift if lsb_first else operator.rshift for write_word in data: - mask = 1 if self.lsb_first else 1 << (self.bits_per_word - 1) + mask = 1 if lsb_first else 1 << (bits_per_word - 1) read_word = 0 - for _ in range(self.bits_per_word): + for _ in range(bits_per_word): if self.mosi is not None: self.mosi.value = bool(write_word & mask) self.clock.on() - if self.miso is not None and not self.clock_phase: + if self.miso is not None and not clock_phase: if self.miso.value: read_word |= mask self.clock.off() - if self.miso is not None and self.clock_phase: + if self.miso is not None and clock_phase: if self.miso.value: read_word |= mask mask = shift(mask, 1) @@ -205,6 +196,9 @@ class SPISoftwareInterface(OutputDevice): self._bus = None super(SPISoftwareInterface, self).__init__(select_pin, active_high=False) try: + self._clock_phase = False + self._lsb_first = False + self._bits_per_word = 8 self._bus = SPISoftwareBus(clock_pin, mosi_pin, miso_pin) except: self.close() @@ -230,16 +224,17 @@ class SPISoftwareInterface(OutputDevice): return "software SPI closed" def read(self, n): - return self._bus.read(n) + return self.transfer((0,) * n) def write(self, data): - return self._bus.write(data) + return len(self.transfer(data)) def transfer(self, data): with self._bus.lock: self.on() try: - return self._bus.transfer(data) + return self._bus.transfer( + data, self._clock_phase, self._lsb_first, self._bits_per_word) finally: self.off() @@ -250,40 +245,37 @@ class SPISoftwareInterface(OutputDevice): value = int(value) if not 0 <= value <= 3: raise ValueError('clock_mode must be a value between 0 and 3 inclusive') - with self._bus.lock: - self._bus.clock.active_high = not (value & 2) - self._bus.clock.off() - self._bus.clock_phase = bool(value & 1) + self.clock_polarity = bool(value & 2) + self.clock_phase = bool(value & 1) def _get_clock_polarity(self): - return not self._bus.clock.active_high + with self._bus.lock: + return not self._bus.clock.active_high def _set_clock_polarity(self, value): with self._bus.lock: self._bus.clock.active_high = not value + self._bus.clock.off() def _get_clock_phase(self): - return self._bus.clock_phase + return self._clock_phase def _set_clock_phase(self, value): - with self._bus.lock: - self._bus.clock_phase = bool(value) + self._clock_phase = bool(value) def _get_lsb_first(self): - return self._bus.lsb_first + return self._lsb_first def _set_lsb_first(self, value): - with self._bus.lock: - self._bus.lsb_first = bool(value) + self._lsb_first = bool(value) def _get_bits_per_word(self): - return self._bus.bits_per_word + return self._bits_per_word def _set_bits_per_word(self, value): if value < 1: raise ValueError('bits_per_word must be positive') - with self._bus.lock: - self._bus.bits_per_word = int(value) + self._bits_per_word = int(value) def _get_select_high(self): return self.active_high diff --git a/gpiozero/spi_devices.py b/gpiozero/spi_devices.py index d866b0c..7aff666 100644 --- a/gpiozero/spi_devices.py +++ b/gpiozero/spi_devices.py @@ -7,7 +7,14 @@ from __future__ import ( str = type('') -from .exc import DeviceClosed, InputDeviceError +from math import log, ceil +from operator import or_ +try: + from functools import reduce +except ImportError: + pass # py2's reduce is built-in + +from .exc import DeviceClosed, SPIBadChannel from .devices import Device from .spi import SPI @@ -34,6 +41,38 @@ class SPIDevice(Device): def closed(self): return self._spi is None + def _int_to_words(self, pattern): + """ + Given a bit-pattern expressed an integer number, return a sequence of + the individual words that make up the pattern. The number of bits per + word will be obtained from the internal SPI interface. + """ + try: + bits_required = int(ceil(log(pattern, 2))) + 1 + except ValueError: + # pattern == 0 (technically speaking, no bits are required to + # transmit the value zero ;) + bits_required = 1 + shifts = range(0, bits_required, self._spi.bits_per_word)[::-1] + mask = 2 ** self._spi.bits_per_word - 1 + return [(pattern >> shift) & mask for shift in shifts] + + def _words_to_int(self, words, expected_bits=None): + """ + Given a sequence of words which each fit in the internal SPI + interface's number of bits per word, returns the value obtained by + concatenating each word into a single bit-string. + + If *expected_bits* is specified, it limits the size of the output to + the specified number of bits (by masking off bits above the expected + number). If unspecified, no limit will be applied. + """ + if expected_bits is None: + expected_bits = len(words) * self._spi.bits_per_word + shifts = range(0, expected_bits, self._spi.bits_per_word)[::-1] + mask = 2 ** expected_bits - 1 + return reduce(or_, (word << shift for word, shift in zip(words, shifts))) & mask + def __repr__(self): try: self._check_open() @@ -72,10 +111,10 @@ class AnalogInputDevice(SPIDevice): .. _analog to digital converters: https://en.wikipedia.org/wiki/Analog-to-digital_converter """ - def __init__(self, bits=None, **spi_args): - if bits is None: - raise InputDeviceError('you must specify the bit resolution of the device') + def __init__(self, bits, **spi_args): self._bits = bits + self._min_value = -(2 ** bits) + self._range = 2 ** (bits + 1) - 1 super(AnalogInputDevice, self).__init__(shared=True, **spi_args) @property @@ -92,9 +131,9 @@ class AnalogInputDevice(SPIDevice): def value(self): """ The current value read from the device, scaled to a value between 0 and - 1. + 1 (or -1 to +1 for certain devices operating in differential mode). """ - return self._read() / (2**self.bits - 1) + return (2 * (self._read() - self._min_value) / self._range) - 1 @property def raw_value(self): @@ -128,10 +167,9 @@ class MCP3xxx(AnalogInputDevice): @property def differential(self): """ - If ``True``, the device is operated in pseudo-differential mode. In - this mode one channel (specified by the channel attribute) is read - relative to the value of a second channel (implied by the chip's - design). + If ``True``, the device is operated in differential mode. In this mode + one channel (specified by the channel attribute) is read relative to + the value of a second channel (implied by the chip's design). Please refer to the device data-sheet to determine which channel is used as the relative base value (for example, when using an @@ -141,28 +179,73 @@ class MCP3xxx(AnalogInputDevice): return self._differential def _read(self): - # MCP3008/04 or MCP3208/04 protocol looks like the following: + return self._words_to_int( + self._spi.transfer(self._send())[-2:], self.bits + ) + + def _send(self): + # MCP3004/08 protocol looks like the following: # # Byte 0 1 2 # ==== ======== ======== ======== - # Tx 0001MCCC xxxxxxxx xxxxxxxx - # Rx xxxxxxxx x0RRRRRR RRRRxxxx for the 3004/08 - # Rx xxxxxxxx x0RRRRRR RRRRRRxx for the 3204/08 + # Tx 00000001 MCCCxxxx xxxxxxxx + # Rx xxxxxxxx xxxxx0RR RRRRRRRR # - # The transmit bits start with 3 preamble bits "000" (to warm up), a - # start bit "1" followed by the single/differential bit (M) which is 1 - # for single-ended read, and 0 for differential read, followed by - # 3-bits for the channel (C). The remainder of the transmission are - # "don't care" bits (x). + # MCP3204/08 protocol looks like the following: # - # The first byte received and the top 1 bit of the second byte are - # don't care bits (x). These are followed by a null bit (0), and then - # the result bits (R). 10 bits for the MCP300x, 12 bits for the - # MCP320x. + # Byte 0 1 2 + # ==== ======== ======== ======== + # Tx 000001MC CCxxxxxx xxxxxxxx + # Rx xxxxxxxx xxx0RRRR RRRRRRRR # - # XXX Differential mode still requires testing - data = self._spi.transfer([16 + [8, 0][self.differential] + self.channel, 0, 0]) - return ((data[1] & 63) << (self.bits - 6)) | (data[2] >> (14 - self.bits)) + # The transmit bits start with several preamble "0" bits, the number + # of which is determined by the amount required to align the last byte + # of the result with the final byte of output. A start "1" bit is then + # transmitted, followed by the single/differential bit (M); 1 for + # single-ended read, 0 for differential read. Next comes three bits for + # channel (C). + # + # Read-out begins with a don't care bit (x), then a null bit (0) + # followed by the result bits (R). All other bits are don't care (x). + # + # The 3x01 variant of the chips always operates in differential mode + # and effectively only has one channel (composed of an IN+ and IN-). As + # such it requires no input, just output. + return self._int_to_words( + (0b10000 | (not self.differential) << 3 | self.channel) << (self.bits + 2) + ) + + +class MCP3xx2(MCP3xxx): + def _send(self): + # MCP3002 protocol looks like the following: + # + # Byte 0 1 + # ==== ======== ======== + # Tx 01MCLxxx xxxxxxxx + # Rx xxxxx0RR RRRRRRRR for the 3002 + # + # MCP3202 protocol looks like the following: + # + # Byte 0 1 2 + # ==== ======== ======== ======== + # Tx 00000001 MCLxxxxx xxxxxxxx + # Rx xxxxxxxx xxx0RRRR RRRRRRRR + # + # The transmit bits start with several preamble "0" bits, the number of + # which is determined by the amount required to align the last byte of + # the result with the final byte of output. A start "1" bit is then + # transmitted, followed by the single/differential bit (M); 1 for + # single-ended read, 0 for differential read. Next comes a single bit + # for channel (M) then the MSBF bit (L) which selects whether the data + # will be read out in MSB form only (1) or whether LSB read-out will + # occur after MSB read-out (0). + # + # Read-out begins with a null bit (0) followed by the result bits (R). + # All other bits are don't care (x). + return self._int_to_words( + (0b1001 | (not self.differential) << 2 | self.channel << 1) << (self.bits + 1) + ) class MCP30xx(MCP3xxx): @@ -196,20 +279,32 @@ class MCP33xx(MCP3xxx): super(MCP33xx, self).__init__(channel, 12, differential, **spi_args) def _read(self): - # MCP3304/02 protocol looks like the following: + if self.differential: + result = self._words_to_int( + self._spi.transfer(self._send())[-2:], self.bits + 1) + # Account for the sign bit + if result > 4095: + return -(8192 - result) + else: + return result + else: + return super(MCP33xx, self)._read() + + def _send(self): + # MCP3302/04 protocol looks like the following: # # Byte 0 1 2 # ==== ======== ======== ======== - # Tx 0001MCCC xxxxxxxx xxxxxxxx - # Rx xxxxxxxx x0SRRRRR RRRRRRRx + # Tx 00001MCC Cxxxxxxx xxxxxxxx + # Rx xxxxxxxx xx0SRRRR RRRRRRRR # - # The transmit bits start with 3 preamble bits "000" (to warm up), a - # start bit "1" followed by the single/differential bit (M) which is 1 - # for single-ended read, and 0 for differential read, followed by - # 3-bits for the channel (C). The remainder of the transmission are - # "don't care" bits (x). + # The transmit bits start with 4 preamble bits "0000", a start bit "1" + # followed by the single/differential bit (M) which is 1 for + # single-ended read, and 0 for differential read, followed by 3-bits + # for the channel (C). The remainder of the transmission are "don't + # care" bits (x). # - # The first byte received and the top 1 bit of the second byte are + # The first byte received and the top 2 bits of the second byte are # don't care bits (x). These are followed by a null bit (0), then the # sign bit (S), and then the 12 result bits (R). # @@ -217,22 +312,11 @@ class MCP33xx(MCP3xxx): # result is effectively 12-bits. In differential mode, the sign bit is # significant and the result is a two's-complement 13-bit value. # - # The MCP3301 variant of the chip always operates in differential - # mode and effectively only has one channel (composed of an IN+ and - # IN-). As such it requires no input, just output. This is the reason - # we split out _send() below; so that MCP3301 can override it. - data = self._spi.transfer(self._send()) - # Extract the last two bytes (again, for MCP3301) - data = data[-2:] - result = ((data[0] & 63) << 7) | (data[1] >> 1) - # Account for the sign bit - if self.differential and result > 4095: - result = -(8192 - result) - assert -4096 <= result < 4096 - return result - - def _send(self): - return [16 + [8, 0][self.differential] + self.channel, 0, 0] + # The MCP3301 variant operates similarly to the other MCP3x01 variants; + # no input, just output and always differential. + return self._int_to_words( + (0b10000 | (not self.differential) << 3 | self.channel) << (self.bits + 3) + ) @property def differential(self): @@ -259,15 +343,25 @@ class MCP33xx(MCP3xxx): class MCP3001(MCP30xx): """ - The `MCP3001`_ is a 10-bit analog to digital converter with 1 channel + The `MCP3001`_ is a 10-bit analog to digital converter with 1 channel. + Please note that the MCP3001 always operates in differential mode, + measuring the value of IN+ relative to IN-. .. _MCP3001: http://www.farnell.com/datasheets/630400.pdf """ def __init__(self, **spi_args): super(MCP3001, self).__init__(0, differential=True, **spi_args) + def _read(self): + # MCP3001 protocol looks like the following: + # + # Byte 0 1 + # ==== ======== ======== + # Rx xx0RRRRR RRRRRxxx + return self._words_to_int(self._spi.read(2), 13) >> 3 -class MCP3002(MCP30xx): + +class MCP3002(MCP30xx, MCP3xx2): """ The `MCP3002`_ is a 10-bit analog to digital converter with 2 channels (0-1). @@ -276,7 +370,7 @@ class MCP3002(MCP30xx): """ def __init__(self, channel=0, differential=False, **spi_args): if not 0 <= channel < 2: - raise InputDeviceError('channel must be 0 or 1') + raise SPIBadChannel('channel must be 0 or 1') super(MCP3002, self).__init__(channel, differential, **spi_args) @@ -289,7 +383,7 @@ class MCP3004(MCP30xx): """ def __init__(self, channel=0, differential=False, **spi_args): if not 0 <= channel < 4: - raise InputDeviceError('channel must be between 0 and 3') + raise SPIBadChannel('channel must be between 0 and 3') super(MCP3004, self).__init__(channel, differential, **spi_args) @@ -302,21 +396,31 @@ class MCP3008(MCP30xx): """ def __init__(self, channel=0, differential=False, **spi_args): if not 0 <= channel < 8: - raise InputDeviceError('channel must be between 0 and 7') + raise SPIBadChannel('channel must be between 0 and 7') super(MCP3008, self).__init__(channel, differential, **spi_args) class MCP3201(MCP32xx): """ - The `MCP3201`_ is a 12-bit analog to digital converter with 1 channel + The `MCP3201`_ is a 12-bit analog to digital converter with 1 channel. + Please note that the MCP3201 always operates in differential mode, + measuring the value of IN+ relative to IN-. .. _MCP3201: http://www.farnell.com/datasheets/1669366.pdf """ def __init__(self, **spi_args): super(MCP3201, self).__init__(0, differential=True, **spi_args) + def _read(self): + # MCP3201 protocol looks like the following: + # + # Byte 0 1 + # ==== ======== ======== + # Rx xx0RRRRR RRRRRRRx + return self._words_to_int(self._spi.read(2), 13) >> 1 -class MCP3202(MCP32xx): + +class MCP3202(MCP32xx, MCP3xx2): """ The `MCP3202`_ is a 12-bit analog to digital converter with 2 channels (0-1). @@ -325,7 +429,7 @@ class MCP3202(MCP32xx): """ def __init__(self, channel=0, differential=False, **spi_args): if not 0 <= channel < 2: - raise InputDeviceError('channel must be 0 or 1') + raise SPIBadChannel('channel must be 0 or 1') super(MCP3202, self).__init__(channel, differential, **spi_args) @@ -338,7 +442,7 @@ class MCP3204(MCP32xx): """ def __init__(self, channel=0, differential=False, **spi_args): if not 0 <= channel < 4: - raise InputDeviceError('channel must be between 0 and 3') + raise SPIBadChannel('channel must be between 0 and 3') super(MCP3204, self).__init__(channel, differential, **spi_args) @@ -351,23 +455,33 @@ class MCP3208(MCP32xx): """ def __init__(self, channel=0, differential=False, **spi_args): if not 0 <= channel < 8: - raise InputDeviceError('channel must be between 0 and 7') + raise SPIBadChannel('channel must be between 0 and 7') super(MCP3208, self).__init__(channel, differential, **spi_args) class MCP3301(MCP33xx): """ The `MCP3301`_ is a signed 13-bit analog to digital converter. Please note - that the MCP3301 always operates in differential mode between its two - channels and the output value is scaled from -1 to +1. + that the MCP3301 always operates in differential mode measuring the + difference between IN+ and IN-. Its output value is scaled from -1 to +1. .. _MCP3301: http://www.farnell.com/datasheets/1669397.pdf """ def __init__(self, **spi_args): super(MCP3301, self).__init__(0, differential=True, **spi_args) - def _send(self): - return [0, 0] + def _read(self): + # MCP3301 protocol looks like the following: + # + # Byte 0 1 + # ==== ======== ======== + # Rx xx0SRRRR RRRRRRRR + result = self._words_to_int(self._spi.read(2), 13) + # Account for the sign bit + if result > 4095: + return -(8192 - result) + else: + return result class MCP3302(MCP33xx): @@ -382,7 +496,7 @@ class MCP3302(MCP33xx): """ def __init__(self, channel=0, differential=False, **spi_args): if not 0 <= channel < 4: - raise InputDeviceError('channel must be between 0 and 4') + raise SPIBadChannel('channel must be between 0 and 4') super(MCP3302, self).__init__(channel, differential, **spi_args) @@ -398,6 +512,6 @@ class MCP3304(MCP33xx): """ def __init__(self, channel=0, differential=False, **spi_args): if not 0 <= channel < 8: - raise InputDeviceError('channel must be between 0 and 7') + raise SPIBadChannel('channel must be between 0 and 7') super(MCP3304, self).__init__(channel, differential, **spi_args) diff --git a/tests/test_spi.py b/tests/test_spi.py new file mode 100644 index 0000000..f035bde --- /dev/null +++ b/tests/test_spi.py @@ -0,0 +1,170 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + + +import sys +import mock +import pytest +from collections import namedtuple + +from gpiozero import * +from gpiozero.pins.mock import MockPin, MockSPIDevice +from gpiozero.spi import * + + +def setup_function(function): + import gpiozero.devices + gpiozero.devices.pin_factory = MockPin + +def teardown_function(function): + MockPin.clear_pins() + + +def test_spi_hardware_params(): + with mock.patch('gpiozero.spi.SpiDev') as spidev: + with SPI() as device: + assert isinstance(device, SPIHardwareInterface) + with SPI(port=0, device=0) as device: + assert isinstance(device, SPIHardwareInterface) + with SPI(port=0, device=1) as device: + assert isinstance(device, SPIHardwareInterface) + with SPI(clock_pin=11) as device: + assert isinstance(device, SPIHardwareInterface) + with SPI(clock_pin=11, mosi_pin=10, select_pin=8) as device: + assert isinstance(device, SPIHardwareInterface) + with SPI(clock_pin=11, mosi_pin=10, select_pin=7) as device: + assert isinstance(device, SPIHardwareInterface) + with SPI(shared=True) as device: + assert isinstance(device, SharedSPIHardwareInterface) + with pytest.raises(ValueError): + SPI(port=1) + with pytest.raises(ValueError): + SPI(device=2) + with pytest.raises(ValueError): + SPI(port=0, clock_pin=12) + with pytest.raises(ValueError): + SPI(foo='bar') + +def test_spi_software_params(): + with mock.patch('gpiozero.spi.SpiDev') as spidev: + with SPI(select_pin=6) as device: + assert isinstance(device, SPISoftwareInterface) + with SPI(clock_pin=11, mosi_pin=9, miso_pin=10) as device: + assert isinstance(device, SPISoftwareInterface) + with SPI(select_pin=6, shared=True) as device: + assert isinstance(device, SharedSPISoftwareInterface) + # Ensure software fallback works when SpiDev isn't present + with SPI() as device: + assert isinstance(device, SPISoftwareInterface) + +def test_spi_hardware_conflict(): + with mock.patch('gpiozero.spi.SpiDev') as spidev: + with LED(11) as led: + with pytest.raises(GPIOPinInUse): + SPI(port=0, device=0) + +def test_spi_hardware_read(): + with mock.patch('gpiozero.spi.SpiDev') as spidev: + spidev.return_value.xfer2.side_effect = lambda data: list(range(10))[:len(data)] + with SPI() as device: + assert device.read(3) == [0, 1, 2] + assert device.read(6) == list(range(6)) + +def test_spi_hardware_write(): + with mock.patch('gpiozero.spi.SpiDev') as spidev: + spidev.return_value.xfer2.side_effect = lambda data: list(range(10))[:len(data)] + with SPI() as device: + assert device.write([0, 1, 2]) == 3 + assert spidev.return_value.xfer2.called_with([0, 1, 2]) + assert device.write(list(range(6))) == 6 + assert spidev.return_value.xfer2.called_with(list(range(6))) + +def test_spi_hardware_modes(): + with mock.patch('gpiozero.spi.SpiDev') as spidev: + spidev.return_value.mode = 0 + spidev.return_value.lsbfirst = False + spidev.return_value.cshigh = True + spidev.return_value.bits_per_word = 8 + with SPI() as device: + assert device.clock_mode == 0 + assert not device.clock_polarity + assert not device.clock_phase + device.clock_polarity = False + assert device.clock_mode == 0 + device.clock_polarity = True + assert device.clock_mode == 2 + device.clock_phase = True + assert device.clock_mode == 3 + assert not device.lsb_first + assert device.select_high + assert device.bits_per_word == 8 + device.select_high = False + device.lsb_first = True + device.bits_per_word = 12 + assert not spidev.return_value.cshigh + assert spidev.return_value.lsbfirst + assert spidev.return_value.bits_per_word == 12 + +def test_spi_software_read(): + class SPISlave(MockSPIDevice): + def on_start(self): + super(SPISlave, self).on_start() + for i in range(10): + self.tx_word(i) + with SPISlave(11, 10, 9, 8) as slave, SPI() as master: + assert master.read(3) == [0, 1, 2] + assert master.read(6) == [0, 1, 2, 3, 4, 5] + slave.clock_phase = True + master.clock_phase = True + assert master.read(3) == [0, 1, 2] + assert master.read(6) == [0, 1, 2, 3, 4, 5] + +def test_spi_software_write(): + with MockSPIDevice(11, 10, 9, 8) as test_device, SPI() as master: + master.write([0]) + assert test_device.rx_word() == 0 + master.write([2, 0]) + assert test_device.rx_word() == 512 + master.write([0, 1, 1]) + assert test_device.rx_word() == 257 + +def test_spi_software_clock_mode(): + with SPI() as master: + assert master.clock_mode == 0 + assert not master.clock_polarity + assert not master.clock_phase + master.clock_polarity = False + assert master.clock_mode == 0 + master.clock_polarity = True + assert master.clock_mode == 2 + master.clock_phase = True + assert master.clock_mode == 3 + master.clock_mode = 0 + assert not master.clock_polarity + assert not master.clock_phase + with pytest.raises(ValueError): + master.clock_mode = 5 + +def test_spi_software_attr(): + with SPI() as master: + assert not master.lsb_first + assert not master.select_high + assert master.bits_per_word == 8 + master.bits_per_word = 12 + assert master.bits_per_word == 12 + master.lsb_first = True + assert master.lsb_first + master.select_high = True + assert master.select_high + with pytest.raises(ValueError): + master.bits_per_word = 0 + + +# XXX Test two simultaneous SPI devices sharing clock, MOSI, and MISO, with +# separate select pins (including threaded tests which attempt simultaneous +# reading/writing) diff --git a/tests/test_spi_devices.py b/tests/test_spi_devices.py new file mode 100644 index 0000000..ecf0ca8 --- /dev/null +++ b/tests/test_spi_devices.py @@ -0,0 +1,339 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + + +import sys +import pytest +from collections import namedtuple +try: + from math import isclose +except ImportError: + from gpiozero.compat import isclose + +from gpiozero import * +from gpiozero.pins.mock import MockSPIDevice, MockPin + + +def setup_function(function): + import gpiozero.devices + gpiozero.devices.pin_factory = MockPin + +def teardown_function(function): + MockPin.clear_pins() + +def clamp(v, min_value, max_value): + return min(max_value, max(min_value, v)) + +def scale(v, ref, bits): + v /= ref + vmin = -(2 ** bits) + vmax = -vmin - 1 + vrange = vmax - vmin + return int(((v + 1) / 2.0) * vrange + vmin) + + +class MockMCP3xxx(MockSPIDevice): + def __init__( + self, clock_pin, mosi_pin, miso_pin, select_pin=None, + channels=8, bits=10): + super(MockMCP3xxx, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin) + self.vref = 3.3 + self.channels = [0.0] * channels + self.channel_bits = 3 + self.bits = bits + self.state = 'idle' + + def on_start(self): + super(MockMCP3xxx, self).on_start() + self.state = 'idle' + + def on_bit(self): + if self.state == 'idle': + if self.rx_buf[-1]: + self.state = 'mode' + self.rx_buf = [] + elif self.state == 'mode': + if self.rx_buf[-1]: + self.state = 'single' + else: + self.state = 'diff' + self.rx_buf = [] + elif self.state in ('single', 'diff'): + if len(self.rx_buf) == self.channel_bits: + self.on_result(self.state == 'diff', self.rx_word()) + self.state = 'result' + elif self.state == 'result': + if not self.tx_buf: + self.state = 'idle' + self.rx_buf = [] + else: + assert False + + def on_result(self, differential, channel): + if differential: + pos_channel = channel + neg_channel = pos_channel ^ 1 + result = self.channels[pos_channel] - self.channels[neg_channel] + result = clamp(result, 0, self.vref) + else: + result = clamp(self.channels[channel], 0, self.vref) + result = scale(result, self.vref, self.bits) + self.tx_word(result, self.bits + 2) + + +class MockMCP3xx1(MockMCP3xxx): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, bits=10): + super(MockMCP3xx1, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, channels=2, bits=bits) + + def on_start(self): + super(MockMCP3xx1, self).on_start() + result = self.channels[0] - self.channels[1] + result = clamp(result, 0, self.vref) + result = scale(result, self.vref, self.bits) + self.tx_word(result, self.bits + 3) + + def on_bit(self): + pass + + +class MockMCP3xx2(MockMCP3xxx): + def __init__( + self, clock_pin, mosi_pin, miso_pin, select_pin=None, + bits=10): + super(MockMCP3xx2, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, channels=2, bits=bits) + self.channel_bits = 1 + + +class MockMCP33xx(MockMCP3xxx): + def __init__( + self, clock_pin, mosi_pin, miso_pin, select_pin=None, + channels=8): + super(MockMCP33xx, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, channels, 12) + + def on_result(self, differential, channel): + if differential: + pos_channel = channel + neg_channel = pos_channel ^ 1 + result = self.channels[pos_channel] - self.channels[neg_channel] + result = clamp(result, -self.vref, self.vref) + else: + result = clamp(self.channels[channel], 0, self.vref) + result = scale(result, self.vref, self.bits) + if result < 0: + result += 8192 + self.tx_word(result, self.bits + 3) + + +class MockMCP3001(MockMCP3xx1): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): + super(MockMCP3001, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, bits=10) + + +class MockMCP3002(MockMCP3xx2): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): + super(MockMCP3002, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, bits=10) + + +class MockMCP3004(MockMCP3xxx): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): + super(MockMCP3004, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, channels=4, bits=10) + + +class MockMCP3008(MockMCP3xxx): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): + super(MockMCP3008, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, channels=8, bits=10) + + +class MockMCP3201(MockMCP3xx1): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): + super(MockMCP3201, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, bits=12) + + +class MockMCP3202(MockMCP3xx2): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): + super(MockMCP3202, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, bits=12) + + +class MockMCP3204(MockMCP3xxx): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): + super(MockMCP3204, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, channels=4, bits=12) + + +class MockMCP3208(MockMCP3xxx): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): + super(MockMCP3208, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, channels=8, bits=12) + + +class MockMCP3301(MockMCP3xxx): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): + super(MockMCP3301, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, channels=2, bits=12) + + def on_start(self): + super(MockMCP3301, self).on_start() + result = self.channels[0] - self.channels[1] + result = clamp(result, -self.vref, self.vref) + result = scale(result, self.vref, self.bits) + if result < 0: + result += 8192 + self.tx_word(result, self.bits + 4) + + +class MockMCP3302(MockMCP33xx): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): + super(MockMCP3302, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, channels=4) + + +class MockMCP3304(MockMCP33xx): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): + super(MockMCP3304, self).__init__( + clock_pin, mosi_pin, miso_pin, select_pin, channels=8) + + +def single_mcp_test(mock, pot, channel, bits): + scale = 2**bits + tolerance = 1 / scale + mock.channels[channel] = 0.0 + assert pot.raw_value == 0 + assert isclose(pot.value, 0.0, abs_tol=tolerance) + mock.channels[channel] = mock.vref / 2 + assert pot.raw_value == (scale / 2) - 1 + assert isclose(pot.value, 0.5, abs_tol=tolerance) + mock.channels[channel] = mock.vref + assert pot.raw_value == scale - 1 + assert isclose(pot.value, 1.0, abs_tol=tolerance) + +def differential_mcp_test(mock, pot, pos_channel, neg_channel, bits, full=False): + scale = 2**bits + tolerance = 1 / scale + mock.channels[pos_channel] = 0.0 + mock.channels[neg_channel] = 0.0 + assert pot.raw_value == 0 + assert isclose(pot.value, 0.0, abs_tol=tolerance) + mock.channels[pos_channel] = mock.vref / 2 + assert pot.raw_value == (scale / 2) - 1 + assert isclose(pot.value, 0.5, abs_tol=tolerance) + mock.channels[pos_channel] = mock.vref + assert pot.raw_value == scale - 1 + assert isclose(pot.value, 1.0, abs_tol=tolerance) + mock.channels[neg_channel] = mock.vref / 2 + assert pot.raw_value == (scale / 2) - 1 + assert isclose(pot.value, 0.5, abs_tol=tolerance) + mock.channels[pos_channel] = mock.vref / 2 + assert pot.raw_value == 0 + assert isclose(pot.value, 0.0, abs_tol=tolerance) + mock.channels[pos_channel] = 0.0 + mock.channels[neg_channel] = mock.vref + if full: + assert pot.raw_value == -scale + assert isclose(pot.value, -1.0, abs_tol=tolerance) + else: + assert pot.raw_value == 0 + assert isclose(pot.value, 0.0, abs_tol=tolerance) + + +def test_MCP3001(): + mock = MockMCP3001(11, 10, 9, 8) + with MCP3001() as pot: + differential_mcp_test(mock, pot, 0, 1, 10) + +def test_MCP3002(): + mock = MockMCP3002(11, 10, 9, 8) + with pytest.raises(ValueError): + MCP3002(channel=5) + with MCP3002(channel=1) as pot: + single_mcp_test(mock, pot, 1, 10) + with MCP3002(channel=1, differential=True) as pot: + differential_mcp_test(mock, pot, 1, 0, 10) + +def test_MCP3004(): + mock = MockMCP3004(11, 10, 9, 8) + with pytest.raises(ValueError): + MCP3004(channel=5) + with MCP3004(channel=3) as pot: + single_mcp_test(mock, pot, 3, 10) + with MCP3004(channel=3, differential=True) as pot: + differential_mcp_test(mock, pot, 3, 2, 10) + +def test_MCP3008(): + mock = MockMCP3008(11, 10, 9, 8) + with pytest.raises(ValueError): + MCP3008(channel=9) + with MCP3008(channel=0) as pot: + single_mcp_test(mock, pot, 0, 10) + with MCP3008(channel=0, differential=True) as pot: + differential_mcp_test(mock, pot, 0, 1, 10) + +def test_MCP3201(): + mock = MockMCP3201(11, 10, 9, 8) + with MCP3201() as pot: + differential_mcp_test(mock, pot, 0, 1, 12) + +def test_MCP3202(): + mock = MockMCP3202(11, 10, 9, 8) + with pytest.raises(ValueError): + MCP3202(channel=5) + with MCP3202(channel=1) as pot: + single_mcp_test(mock, pot, 1, 12) + with MCP3202(channel=1, differential=True) as pot: + differential_mcp_test(mock, pot, 1, 0, 12) + +def test_MCP3204(): + mock = MockMCP3204(11, 10, 9, 8) + with pytest.raises(ValueError): + MCP3204(channel=5) + with MCP3204(channel=1) as pot: + single_mcp_test(mock, pot, 1, 12) + with MCP3204(channel=1, differential=True) as pot: + differential_mcp_test(mock, pot, 1, 0, 12) + +def test_MCP3208(): + mock = MockMCP3208(11, 10, 9, 8) + with pytest.raises(ValueError): + MCP3208(channel=9) + with MCP3208(channel=7) as pot: + single_mcp_test(mock, pot, 7, 12) + with MCP3208(channel=7, differential=True) as pot: + differential_mcp_test(mock, pot, 7, 6, 12) + +def test_MCP3301(): + mock = MockMCP3301(11, 10, 9, 8) + with MCP3301() as pot: + differential_mcp_test(mock, pot, 0, 1, 12, full=True) + +def test_MCP3302(): + mock = MockMCP3302(11, 10, 9, 8) + with pytest.raises(ValueError): + MCP3302(channel=4) + with MCP3302(channel=0) as pot: + single_mcp_test(mock, pot, 0, 12) + with MCP3302(channel=0, differential=True) as pot: + differential_mcp_test(mock, pot, 0, 1, 12, full=True) + +def test_MCP3304(): + mock = MockMCP3304(11, 10, 9, 8) + with pytest.raises(ValueError): + MCP3304(channel=9) + with MCP3304(channel=5) as pot: + single_mcp_test(mock, pot, 5, 12) + with MCP3304(channel=5, differential=True) as pot: + differential_mcp_test(mock, pot, 5, 4, 12, full=True) +