Skip to content

Commit 2ebecc3

Browse files
authored
Merge pull request gpiozero#430 from waveform80/spi-tests
Fix gpiozero#421
2 parents b6fb8bf + 737a739 commit 2ebecc3

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)