Skip to content

Commit 737a739

Browse files
committed
Added SPI tests, simplified the shared SPI software bus implementation, and fixed several protocol errors in our MCP3xxx classes (the x2 and x1 protocols were wrong)
1 parent b6fb8bf commit 737a739

File tree

10 files changed

+972
-191
lines changed

10 files changed

+972
-191
lines changed

docs/images/spi_device_hierarchy.dot

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ digraph classes {
1111
MCP3xxx;
1212
MCP30xx;
1313
MCP32xx;
14+
MCP3xx2;
1415
MCP33xx;
1516

1617
/* Concrete classes */
@@ -21,6 +22,8 @@ digraph classes {
2122
MCP30xx->MCP3xxx;
2223
MCP32xx->MCP3xxx;
2324
MCP33xx->MCP3xxx;
25+
MCP3xx2->MCP3xxx;
26+
2427
MCP3001->MCP30xx;
2528
MCP3002->MCP30xx;
2629
MCP3004->MCP30xx;
@@ -29,6 +32,8 @@ digraph classes {
2932
MCP3202->MCP32xx;
3033
MCP3204->MCP32xx;
3134
MCP3208->MCP32xx;
35+
MCP3002->MCP3xx2;
36+
MCP3202->MCP3xx2;
3237
MCP3301->MCP33xx;
3338
MCP3302->MCP33xx;
3439
MCP3304->MCP33xx;
139 Bytes
Binary file not shown.
1.67 KB
Loading

docs/images/spi_device_hierarchy.svg

Lines changed: 106 additions & 86 deletions
Loading

gpiozero/exc.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ class SPIError(GPIOZeroError):
4949
class SPIBadArgs(SPIError, ValueError):
5050
"Error raised when invalid arguments are given while constructing :class:`SPIDevice`"
5151

52+
class SPIBadChannel(SPIError, ValueError):
53+
"Error raised when an invalid channel is given to an :class:`AnalogInputDevice`"
54+
5255
class GPIODeviceError(GPIOZeroError):
5356
"Base class for errors specific to the GPIODevice hierarchy"
5457

gpiozero/pins/mock.py

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ def __new__(cls, number):
5454
self._when_changed = None
5555
self.clear_states()
5656
return self
57-
if old_pin.__class__ != cls:
57+
# Ensure the pin class expected supports PWM (or not)
58+
if issubclass(cls, MockPWMPin) != isinstance(old_pin, MockPWMPin):
5859
raise ValueError('pin %d is already in use as a %s' % (number, old_pin.__class__.__name__))
5960
return old_pin
6061

@@ -249,7 +250,6 @@ class MockPWMPin(MockPin):
249250
"""
250251
This derivative of :class:`MockPin` adds PWM support.
251252
"""
252-
253253
def __init__(self, number):
254254
super(MockPWMPin, self).__init__()
255255
self._frequency = None
@@ -275,3 +275,141 @@ def _set_frequency(self, value):
275275
if value is None:
276276
self._change_state(0.0)
277277

278+
279+
class MockSPIClockPin(MockPin):
280+
"""
281+
This derivative of :class:`MockPin` is intended to be used as the clock pin
282+
of a mock SPI device. It is not intended for direct construction in tests;
283+
rather, construct a :class:`MockSPIDevice` with various pin numbers, and
284+
this class will be used for the clock pin.
285+
"""
286+
def __init__(self, number):
287+
super(MockSPIClockPin, self).__init__()
288+
if not hasattr(self, 'spi_devices'):
289+
self.spi_devices = []
290+
291+
def _set_state(self, value):
292+
super(MockSPIClockPin, self)._set_state(value)
293+
for dev in self.spi_devices:
294+
dev.on_clock()
295+
296+
297+
class MockSPISelectPin(MockPin):
298+
"""
299+
This derivative of :class:`MockPin` is intended to be used as the select
300+
pin of a mock SPI device. It is not intended for direct construction in
301+
tests; rather, construct a :class:`MockSPIDevice` with various pin numbers,
302+
and this class will be used for the select pin.
303+
"""
304+
def __init__(self, number):
305+
super(MockSPISelectPin, self).__init__()
306+
if not hasattr(self, 'spi_device'):
307+
self.spi_device = None
308+
309+
def _set_state(self, value):
310+
super(MockSPISelectPin, self)._set_state(value)
311+
if self.spi_device:
312+
self.spi_device.on_select()
313+
314+
315+
class MockSPIDevice(object):
316+
def __init__(
317+
self, clock_pin, mosi_pin, miso_pin, select_pin=None,
318+
clock_polarity=False, clock_phase=False, lsb_first=False,
319+
bits_per_word=8, select_high=False):
320+
self.clock_pin = MockSPIClockPin(clock_pin)
321+
self.mosi_pin = None if mosi_pin is None else MockPin(mosi_pin)
322+
self.miso_pin = None if miso_pin is None else MockPin(miso_pin)
323+
self.select_pin = None if select_pin is None else MockSPISelectPin(select_pin)
324+
self.clock_polarity = clock_polarity
325+
self.clock_phase = clock_phase
326+
self.lsb_first = lsb_first
327+
self.bits_per_word = bits_per_word
328+
self.select_high = select_high
329+
self.rx_bit = 0
330+
self.rx_buf = []
331+
self.tx_buf = []
332+
self.clock_pin.spi_devices.append(self)
333+
self.select_pin.spi_device = self
334+
335+
def __enter__(self):
336+
return self
337+
338+
def __exit__(self, exc_type, exc_value, exc_tb):
339+
self.close()
340+
341+
def close(self):
342+
if self in self.clock_pin.spi_devices:
343+
self.clock_pin.spi_devices.remove(self)
344+
if self.select_pin is not None:
345+
self.select_pin.spi_device = None
346+
347+
def on_select(self):
348+
if self.select_pin.state == self.select_high:
349+
self.on_start()
350+
351+
def on_clock(self):
352+
# Don't do anything if this SPI device isn't currently selected
353+
if self.select_pin is None or self.select_pin.state == self.select_high:
354+
# The XOR of the clock pin's values, polarity and phase indicates
355+
# whether we're meant to be acting on this edge
356+
if self.clock_pin.state ^ self.clock_polarity ^ self.clock_phase:
357+
self.rx_bit += 1
358+
if self.mosi_pin is not None:
359+
self.rx_buf.append(self.mosi_pin.state)
360+
if self.miso_pin is not None:
361+
try:
362+
tx_value = self.tx_buf.pop(0)
363+
except IndexError:
364+
tx_value = 0
365+
if tx_value:
366+
self.miso_pin.drive_high()
367+
else:
368+
self.miso_pin.drive_low()
369+
self.on_bit()
370+
371+
def on_start(self):
372+
"""
373+
Override this in descendents to detect when the mock SPI device's
374+
select line is activated.
375+
"""
376+
self.rx_bit = 0
377+
self.rx_buf = []
378+
self.tx_buf = []
379+
380+
def on_bit(self):
381+
"""
382+
Override this in descendents to react to receiving a bit.
383+
384+
The :attr:`rx_bit` attribute gives the index of the bit received (this
385+
is reset to 0 by default by :meth:`on_select`). The :attr:`rx_buf`
386+
sequence gives the sequence of 1s and 0s that have been recevied so
387+
far. The :attr:`tx_buf` sequence gives the sequence of 1s and 0s to
388+
transmit on the next clock pulses. All these attributes can be modified
389+
within this method.
390+
391+
The :meth:`rx_word` and :meth:`tx_word` methods can also be used to
392+
read and append to the buffers using integers instead of bool bits.
393+
"""
394+
pass
395+
396+
def rx_word(self):
397+
result = 0
398+
bits = reversed(self.rx_buf) if self.lsb_first else self.rx_buf
399+
for bit in bits:
400+
result <<= 1
401+
result |= bit
402+
return result
403+
404+
def tx_word(self, value, bits_per_word=None):
405+
if bits_per_word is None:
406+
bits_per_word = self.bits_per_word
407+
bits = [0] * bits_per_word
408+
for bit in range(bits_per_word):
409+
bits[bit] = value & 1
410+
value >>= 1
411+
assert not value
412+
if not self.lsb_first:
413+
bits = reversed(bits)
414+
self.tx_buf.extend(bits)
415+

gpiozero/spi.py

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,16 @@ def _set_clock_mode(self, value):
8585
self._device.mode = value
8686

8787
def _get_clock_polarity(self):
88-
return bool(self.mode & 2)
88+
return bool(self.clock_mode & 2)
8989

9090
def _set_clock_polarity(self, value):
91-
self.mode = self.mode & (~2) | (bool(value) << 1)
91+
self.clock_mode = self.clock_mode & (~2) | (bool(value) << 1)
9292

9393
def _get_clock_phase(self):
94-
return bool(self.mode & 1)
94+
return bool(self.clock_mode & 1)
9595

9696
def _set_clock_phase(self, value):
97-
self.mode = self.mode & (~1) | bool(value)
97+
self.clock_mode = self.clock_mode & (~1) | bool(value)
9898

9999
def _get_lsb_first(self):
100100
return self._device.lsbfirst
@@ -130,9 +130,6 @@ def __init__(self, clock_pin, mosi_pin, miso_pin):
130130
self.miso = None
131131
super(SPISoftwareBus, self).__init__()
132132
self.lock = RLock()
133-
self.clock_phase = False
134-
self.lsb_first = False
135-
self.bits_per_word = 8
136133
try:
137134
self.clock = OutputDevice(clock_pin, active_high=True)
138135
if mosi_pin is not None:
@@ -166,33 +163,27 @@ def closed(self):
166163
def _shared_key(cls, clock_pin, mosi_pin, miso_pin):
167164
return (clock_pin, mosi_pin, miso_pin)
168165

169-
def read(self, n):
170-
return self.transfer((0,) * n)
171-
172-
def write(self, data):
173-
return len(self.transfer(data))
174-
175-
def transfer(self, data):
166+
def transfer(self, data, clock_phase=False, lsb_first=False, bits_per_word=8):
176167
"""
177168
Writes data (a list of integer words where each word is assumed to have
178169
:attr:`bits_per_word` bits or less) to the SPI interface, and reads an
179170
equivalent number of words, returning them as a list of integers.
180171
"""
181172
result = []
182173
with self.lock:
183-
shift = operator.lshift if self.lsb_first else operator.rshift
174+
shift = operator.lshift if lsb_first else operator.rshift
184175
for write_word in data:
185-
mask = 1 if self.lsb_first else 1 << (self.bits_per_word - 1)
176+
mask = 1 if lsb_first else 1 << (bits_per_word - 1)
186177
read_word = 0
187-
for _ in range(self.bits_per_word):
178+
for _ in range(bits_per_word):
188179
if self.mosi is not None:
189180
self.mosi.value = bool(write_word & mask)
190181
self.clock.on()
191-
if self.miso is not None and not self.clock_phase:
182+
if self.miso is not None and not clock_phase:
192183
if self.miso.value:
193184
read_word |= mask
194185
self.clock.off()
195-
if self.miso is not None and self.clock_phase:
186+
if self.miso is not None and clock_phase:
196187
if self.miso.value:
197188
read_word |= mask
198189
mask = shift(mask, 1)
@@ -205,6 +196,9 @@ def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin):
205196
self._bus = None
206197
super(SPISoftwareInterface, self).__init__(select_pin, active_high=False)
207198
try:
199+
self._clock_phase = False
200+
self._lsb_first = False
201+
self._bits_per_word = 8
208202
self._bus = SPISoftwareBus(clock_pin, mosi_pin, miso_pin)
209203
except:
210204
self.close()
@@ -230,16 +224,17 @@ def __repr__(self):
230224
return "software SPI closed"
231225

232226
def read(self, n):
233-
return self._bus.read(n)
227+
return self.transfer((0,) * n)
234228

235229
def write(self, data):
236-
return self._bus.write(data)
230+
return len(self.transfer(data))
237231

238232
def transfer(self, data):
239233
with self._bus.lock:
240234
self.on()
241235
try:
242-
return self._bus.transfer(data)
236+
return self._bus.transfer(
237+
data, self._clock_phase, self._lsb_first, self._bits_per_word)
243238
finally:
244239
self.off()
245240

@@ -250,40 +245,37 @@ def _set_clock_mode(self, value):
250245
value = int(value)
251246
if not 0 <= value <= 3:
252247
raise ValueError('clock_mode must be a value between 0 and 3 inclusive')
253-
with self._bus.lock:
254-
self._bus.clock.active_high = not (value & 2)
255-
self._bus.clock.off()
256-
self._bus.clock_phase = bool(value & 1)
248+
self.clock_polarity = bool(value & 2)
249+
self.clock_phase = bool(value & 1)
257250

258251
def _get_clock_polarity(self):
259-
return not self._bus.clock.active_high
252+
with self._bus.lock:
253+
return not self._bus.clock.active_high
260254

261255
def _set_clock_polarity(self, value):
262256
with self._bus.lock:
263257
self._bus.clock.active_high = not value
258+
self._bus.clock.off()
264259

265260
def _get_clock_phase(self):
266-
return self._bus.clock_phase
261+
return self._clock_phase
267262

268263
def _set_clock_phase(self, value):
269-
with self._bus.lock:
270-
self._bus.clock_phase = bool(value)
264+
self._clock_phase = bool(value)
271265

272266
def _get_lsb_first(self):
273-
return self._bus.lsb_first
267+
return self._lsb_first
274268

275269
def _set_lsb_first(self, value):
276-
with self._bus.lock:
277-
self._bus.lsb_first = bool(value)
270+
self._lsb_first = bool(value)
278271

279272
def _get_bits_per_word(self):
280-
return self._bus.bits_per_word
273+
return self._bits_per_word
281274

282275
def _set_bits_per_word(self, value):
283276
if value < 1:
284277
raise ValueError('bits_per_word must be positive')
285-
with self._bus.lock:
286-
self._bus.bits_per_word = int(value)
278+
self._bits_per_word = int(value)
287279

288280
def _get_select_high(self):
289281
return self.active_high

0 commit comments

Comments
 (0)