Skip to content

Commit fb37622

Browse files
authored
Merge pull request adafruit#31 from kbsriram/fix-phase
Move SPI bit writes to the right clock phase.
2 parents 2410e85 + 7c6bc78 commit fb37622

File tree

5 files changed

+648
-42
lines changed

5 files changed

+648
-42
lines changed

adafruit_bitbangio.py

+64-42
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,6 @@ def __init__(
323323
self._mosi = None
324324
self._miso = None
325325

326-
self.configure()
327-
self.unlock()
328-
329326
# Set pins as outputs/inputs.
330327
self._sclk = DigitalInOut(clock)
331328
self._sclk.switch_to_output()
@@ -338,6 +335,9 @@ def __init__(
338335
self._miso = DigitalInOut(MISO)
339336
self._miso.switch_to_input()
340337

338+
self.configure()
339+
self.unlock()
340+
341341
def deinit(self) -> None:
342342
"""Free any hardware used by the object."""
343343
self._sclk.deinit()
@@ -372,12 +372,30 @@ def configure(
372372
self._bits = bits
373373
self._half_period = (1 / self._baudrate) / 2 # 50% Duty Cyle delay
374374

375+
# Initialize the clock to the idle state. This is important to
376+
# guarantee that the clock is at a known (idle) state before
377+
# any read/write operations.
378+
self._sclk.value = self._polarity
379+
375380
def _wait(self, start: Optional[int] = None) -> float:
376381
"""Wait for up to one half cycle"""
377382
while (start + self._half_period) > monotonic():
378383
pass
379384
return monotonic() # Return current time
380385

386+
def _should_write(self, to_active: Literal[0, 1]) -> bool:
387+
"""Return true if a bit should be written on the given clock transition."""
388+
# phase 0: write when active is 0
389+
# phase 1: write when active is 1
390+
return self._phase == to_active
391+
392+
def _should_read(self, to_active: Literal[0, 1]) -> bool:
393+
"""Return true if a bit should be read on the given clock transition."""
394+
# phase 0: read when active is 1
395+
# phase 1: read when active is 0
396+
# Data is read on the idle->active transition only when the phase is 1
397+
return self._phase == 1 - to_active
398+
381399
def write(
382400
self, buffer: ReadableBuffer, start: int = 0, end: Optional[int] = None
383401
) -> None:
@@ -392,24 +410,26 @@ def write(
392410

393411
if self._check_lock():
394412
start_time = monotonic()
413+
# Note: when we come here, our clock must always be its idle state.
395414
for byte in buffer[start:end]:
396415
for bit_position in range(self._bits):
397416
bit_value = byte & 0x80 >> bit_position
398-
# Set clock to base
399-
if not self._phase: # Mode 0, 2
417+
# clock: idle, or has made an active->idle transition.
418+
if self._should_write(to_active=0):
400419
self._mosi.value = bit_value
401-
self._sclk.value = not self._polarity
420+
# clock: wait in idle for half a period
402421
start_time = self._wait(start_time)
403-
404-
# Flip clock off base
405-
if self._phase: # Mode 1, 3
422+
# clock: idle->active
423+
self._sclk.value = not self._polarity
424+
if self._should_write(to_active=1):
406425
self._mosi.value = bit_value
407-
self._sclk.value = self._polarity
426+
# clock: wait in active for half a period
408427
start_time = self._wait(start_time)
409-
410-
# Return pins to base positions
411-
self._mosi.value = 0
412-
self._sclk.value = self._polarity
428+
# clock: active->idle
429+
self._sclk.value = self._polarity
430+
# clock: stay in idle for the last active->idle transition
431+
# to settle.
432+
start_time = self._wait(start_time)
413433

414434
# pylint: disable=too-many-branches
415435
def readinto(
@@ -433,36 +453,38 @@ def readinto(
433453
for bit_position in range(self._bits):
434454
bit_mask = 0x80 >> bit_position
435455
bit_value = write_value & 0x80 >> bit_position
436-
# Return clock to base
437-
self._sclk.value = self._polarity
438-
start_time = self._wait(start_time)
439-
# Handle read on leading edge of clock.
440-
if not self._phase: # Mode 0, 2
456+
# clock: idle, or has made an active->idle transition.
457+
if self._should_write(to_active=0):
441458
if self._mosi is not None:
442459
self._mosi.value = bit_value
460+
# clock: wait half a period.
461+
start_time = self._wait(start_time)
462+
# clock: idle->active
463+
self._sclk.value = not self._polarity
464+
if self._should_read(to_active=1):
443465
if self._miso.value:
444466
# Set bit to 1 at appropriate location.
445467
buffer[byte_position] |= bit_mask
446468
else:
447469
# Set bit to 0 at appropriate location.
448470
buffer[byte_position] &= ~bit_mask
449-
# Flip clock off base
450-
self._sclk.value = not self._polarity
451-
start_time = self._wait(start_time)
452-
# Handle read on trailing edge of clock.
453-
if self._phase: # Mode 1, 3
471+
if self._should_write(to_active=1):
454472
if self._mosi is not None:
455473
self._mosi.value = bit_value
474+
# clock: wait half a period
475+
start_time = self._wait(start_time)
476+
# Clock: active->idle
477+
self._sclk.value = self._polarity
478+
if self._should_read(to_active=0):
456479
if self._miso.value:
457480
# Set bit to 1 at appropriate location.
458481
buffer[byte_position] |= bit_mask
459482
else:
460483
# Set bit to 0 at appropriate location.
461484
buffer[byte_position] &= ~bit_mask
462485

463-
# Return pins to base positions
464-
self._mosi.value = 0
465-
self._sclk.value = self._polarity
486+
# clock: wait another half period for the last transition.
487+
start_time = self._wait(start_time)
466488

467489
def write_readinto(
468490
self,
@@ -499,34 +521,34 @@ def write_readinto(
499521
buffer_out[byte_position + out_start] & 0x80 >> bit_position
500522
)
501523
in_byte_position = byte_position + in_start
502-
# Return clock to 0
503-
self._sclk.value = self._polarity
504-
start_time = self._wait(start_time)
505-
# Handle read on leading edge of clock.
506-
if not self._phase: # Mode 0, 2
524+
# clock: idle, or has made an active->idle transition.
525+
if self._should_write(to_active=0):
507526
self._mosi.value = bit_value
527+
# clock: wait half a period.
528+
start_time = self._wait(start_time)
529+
# clock: idle->active
530+
self._sclk.value = not self._polarity
531+
if self._should_read(to_active=1):
508532
if self._miso.value:
509533
# Set bit to 1 at appropriate location.
510534
buffer_in[in_byte_position] |= bit_mask
511535
else:
512-
# Set bit to 0 at appropriate location.
513536
buffer_in[in_byte_position] &= ~bit_mask
514-
# Flip clock off base
515-
self._sclk.value = not self._polarity
516-
start_time = self._wait(start_time)
517-
# Handle read on trailing edge of clock.
518-
if self._phase: # Mode 1, 3
537+
if self._should_write(to_active=1):
519538
self._mosi.value = bit_value
539+
# clock: wait half a period
540+
start_time = self._wait(start_time)
541+
# Clock: active->idle
542+
self._sclk.value = self._polarity
543+
if self._should_read(to_active=0):
520544
if self._miso.value:
521545
# Set bit to 1 at appropriate location.
522546
buffer_in[in_byte_position] |= bit_mask
523547
else:
524-
# Set bit to 0 at appropriate location.
525548
buffer_in[in_byte_position] &= ~bit_mask
526549

527-
# Return pins to base positions
528-
self._mosi.value = 0
529-
self._sclk.value = self._polarity
550+
# clock: wait another half period for the last transition.
551+
start_time = self._wait(start_time)
530552

531553
# pylint: enable=too-many-branches
532554

tests/README.rst

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
..
2+
SPDX-FileCopyrightText: KB Sriram
3+
SPDX-License-Identifier: MIT
4+
..
5+
6+
Bitbangio Tests
7+
===============
8+
9+
These tests run under CPython, and are intended to verify that the
10+
library passes some sanity checks, using a lightweight simulator as
11+
the target device.
12+
13+
These tests run automatically from the standard `circuitpython github
14+
workflow <wf_>`_. To run them manually, first install these packages
15+
if necessary::
16+
17+
$ pip3 install pytest
18+
19+
Then ensure you're in the *root* directory of the repository and run
20+
the following command::
21+
22+
$ python -m pytest
23+
24+
Notes on the simulator
25+
======================
26+
27+
`simulator.py` implements a small logic level simulator and a few test
28+
doubles so the library can run under CPython.
29+
30+
The `Engine` class is used as a singleton in the module to co-ordinate
31+
the simulation.
32+
33+
A `Net` holds a list of `FakePins` that are connected together. It
34+
also resolves the overall logic level of the net when a `FakePin` is
35+
updated. It can optionally hold a history of logic level changes,
36+
which may be useful for testing some timing expectations, or export
37+
them as a VCD file for `Pulseview <pv_>`_. Test code can also register
38+
listeners on a `Net` when the net's level changes, so it can simulate
39+
device behavior.
40+
41+
A `FakePin` is a test double for the CircuitPython `Pin` class, and
42+
implements all the functionality so it behaves appropriately in
43+
CPython.
44+
45+
A simulated device can create a `FakePin` for each of its terminals,
46+
and connect them to one or more `Net` instances. It can listen for
47+
level changes on the `Net`, and bitbang the `FakePin` to simulate
48+
behavior. `simulated_spi_device.py` implements a peripheral device
49+
that writes a constant value onto an SPI bus.
50+
51+
52+
.. _wf: https://github.com/adafruit/workflows-circuitpython-libs/blob/6e1562eaabced4db1bd91173b698b1cc1dfd35ab/build/action.yml#L78-L84
53+
.. _pv: https://sigrok.org/wiki/PulseView

tests/simulated_spi.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# SPDX-FileCopyrightText: KB Sriram
2+
# SPDX-License-Identifier: MIT
3+
"""Implementation of testable SPI devices."""
4+
5+
import dataclasses
6+
import simulator as sim
7+
8+
9+
@dataclasses.dataclass(frozen=True)
10+
class SpiBus:
11+
enable: sim.Net
12+
clock: sim.Net
13+
copi: sim.Net
14+
cipo: sim.Net
15+
16+
17+
class Constant:
18+
"""Device that always writes a constant."""
19+
20+
def __init__(self, data: bytearray, bus: SpiBus, polarity: int, phase: int) -> None:
21+
# convert to binary string array of bits for convenience
22+
datalen = 8 * len(data)
23+
self._data = f"{int.from_bytes(data, 'big'):0{datalen}b}"
24+
self._bit_position = 0
25+
self._clock = sim.FakePin("const_clock_pin", bus.clock)
26+
self._last_clock_level = bus.clock.level
27+
self._cipo = sim.FakePin("const_cipo_pin", bus.cipo)
28+
self._enable = sim.FakePin("const_enable_pin", bus.enable)
29+
self._cipo.init(sim.Mode.OUT)
30+
self._phase = phase
31+
self._polarity = sim.Level.HIGH if polarity else sim.Level.LOW
32+
self._enabled = False
33+
bus.clock.on_level_change(self._on_level_change)
34+
bus.enable.on_level_change(self._on_level_change)
35+
36+
def write_bit(self) -> None:
37+
"""Writes the next bit to the cipo net."""
38+
if self._bit_position >= len(self._data):
39+
# Just write a zero
40+
self._cipo.value(0) # pylint: disable=not-callable
41+
return
42+
self._cipo.value(
43+
int(self._data[self._bit_position]) # pylint: disable=not-callable
44+
)
45+
self._bit_position += 1
46+
47+
def _on_level_change(self, net: sim.Net) -> None:
48+
if net == self._enable.net:
49+
# Assumes enable is active high.
50+
self._enabled = net.level == sim.Level.HIGH
51+
if self._enabled:
52+
self._bit_position = 0
53+
if self._phase == 0:
54+
# Write on enable or idle->active
55+
self.write_bit()
56+
return
57+
if not self._enabled:
58+
return
59+
if net != self._clock.net:
60+
return
61+
cur_clock_level = net.level
62+
if cur_clock_level == self._last_clock_level:
63+
return
64+
active = 0 if cur_clock_level == self._polarity else 1
65+
if self._phase == active:
66+
self.write_bit()
67+
self._last_clock_level = cur_clock_level

0 commit comments

Comments
 (0)