Skip to content

Commit 64694f7

Browse files
authored
Merge pull request #41 from jepler/pulsegroup
add pulsegroup example
2 parents f257c4d + c18585e commit 64694f7

File tree

1 file changed

+264
-0
lines changed

1 file changed

+264
-0
lines changed

examples/pioasm_pulsegroup.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# SPDX-FileCopyrightText: 2022 Jeff Epler, written for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
#
5+
# Heavy inspiration from Pimoroni's "PWM Cluster":
6+
# https://github.com/pimoroni/pimoroni-pico/blob/main/drivers/pwm/pwm_cluster.cpp
7+
# https://github.com/pimoroni/pimoroni-pico/blob/main/drivers/pwm/pwm_cluster.pio
8+
9+
import array
10+
11+
import board
12+
import rp2pio
13+
import adafruit_ticks
14+
import ulab.numpy as np
15+
from adafruit_motor import servo
16+
17+
18+
import adafruit_pioasm
19+
20+
_cycle_count = 3
21+
_program = adafruit_pioasm.Program(
22+
"""
23+
.wrap_target
24+
out pins, 32 ; Immediately set the pins to their new state
25+
out y, 32 ; Set the counter
26+
count_check:
27+
jmp y-- delay ; Check if the counter is 0, and if so wrap around.
28+
; If not decrement the counter and jump to the delay
29+
.wrap
30+
31+
delay:
32+
jmp count_check [1] ; Wait a few cycles then jump back to the loop
33+
"""
34+
)
35+
36+
37+
class PulseItem:
38+
def __init__(self, group, index, phase, maxval):
39+
self._group = group
40+
self._index = index
41+
self._phase = phase
42+
self._value = 0
43+
self._maxval = maxval
44+
self._turn_on = self._turn_off = None
45+
self._mask = 1 << index
46+
47+
@property
48+
def frequency(self):
49+
return self._group.frequency
50+
51+
@property
52+
def duty_cycle(self):
53+
return self._value
54+
55+
@duty_cycle.setter
56+
def duty_cycle(self, value):
57+
if value < 0 or value > self._maxval:
58+
raise ValueError(f"value must be in the range(0, {self._maxval+1})")
59+
self._value = value
60+
self._recalculate()
61+
62+
@property
63+
def phase(self):
64+
return self._phase
65+
66+
@phase.setter
67+
def phase(self, phase):
68+
if phase < 0 or phase >= self._maxval:
69+
raise ValueError(f"phase must be in the range(0, {self._maxval})")
70+
self._phase = phase
71+
self._recalculate()
72+
73+
def _recalculate(self):
74+
self._turn_on = self._get_turn_on()
75+
self._turn_off = self._get_turn_off()
76+
self._group._maybe_update() # pylint: disable=protected-access
77+
78+
def _get_turn_on(self):
79+
maxval = self._maxval
80+
if self._value == 0:
81+
return None
82+
if self._value == self._maxval:
83+
return 0
84+
return self.phase % maxval
85+
86+
def _get_turn_off(self):
87+
maxval = self._maxval
88+
if self._value == 0:
89+
return None
90+
if self._value == self._maxval:
91+
return None
92+
return (self._value + self.phase) % maxval
93+
94+
def __str__(self):
95+
return f"<PulseItem: {self.duty_cycle=} {self.phase=} {self._turn_on=} {self._turn_off=}>"
96+
97+
98+
class PulseGroup:
99+
def __init__(
100+
self,
101+
first_pin,
102+
pin_count,
103+
period=0.02,
104+
maxval=65535,
105+
stagger=False,
106+
auto_update=True,
107+
): # pylint: disable=too-many-arguments
108+
"""Create a pulse group with the given characteristics"""
109+
self._frequency = round(1 / period)
110+
pio_frequency = round((1 + maxval) * _cycle_count / period)
111+
self._sm = rp2pio.StateMachine(
112+
_program.assembled,
113+
frequency=pio_frequency,
114+
first_out_pin=first_pin,
115+
out_pin_count=pin_count,
116+
auto_pull=True,
117+
pull_threshold=32,
118+
**_program.pio_kwargs,
119+
)
120+
self._auto_update = auto_update
121+
self._items = [
122+
PulseItem(self, i, round(maxval * i / pin_count) if stagger else 0, maxval)
123+
for i in range(pin_count)
124+
]
125+
self._maxval = maxval
126+
127+
@property
128+
def frequency(self):
129+
return self._frequency
130+
131+
def __enter__(self):
132+
return self
133+
134+
def __exit__(self, exc_type, exc_value, traceback):
135+
self.deinit()
136+
137+
def deinit(self):
138+
self._sm.deinit()
139+
del self._items[:]
140+
141+
def __getitem__(self, i):
142+
"""Get an individual pulse generator"""
143+
return self._items[i]
144+
145+
def __len__(self):
146+
return len(self._items)
147+
148+
def update(self):
149+
changes = {0: [0, 0]}
150+
151+
for i in self._items:
152+
turn_on = i._turn_on # pylint: disable=protected-access
153+
turn_off = i._turn_off # pylint: disable=protected-access
154+
mask = i._mask # pylint: disable=protected-access
155+
156+
if turn_on is not None:
157+
this_change = changes.get(turn_on)
158+
if this_change:
159+
this_change[0] |= mask
160+
else:
161+
changes[turn_on] = [mask, 0]
162+
163+
# start the cycle 'on'
164+
if turn_off is not None and turn_off < turn_on:
165+
changes[0][0] |= mask
166+
167+
if turn_off is not None:
168+
this_change = changes.get(turn_off)
169+
if this_change:
170+
this_change[1] |= mask
171+
else:
172+
changes[turn_off] = [0, mask]
173+
174+
def make_sequence():
175+
sorted_changes = sorted(changes.items())
176+
# Note that the first change time is always 0! Loop over range(len) is
177+
# to reduce allocations
178+
old_time = 0
179+
value = 0
180+
for time, (turn_on, turn_off) in sorted_changes:
181+
if time != 0: # never occurs on the first iteration
182+
yield time - old_time - 1
183+
old_time = time
184+
185+
value = (value | turn_on) & ~turn_off
186+
yield value
187+
188+
# the final delay value
189+
yield self._maxval - old_time
190+
191+
buf = array.array("L", make_sequence())
192+
193+
self._sm.background_write(loop=buf)
194+
195+
def _maybe_update(self):
196+
if self._auto_update:
197+
self.update()
198+
199+
@property
200+
def auto_update(self):
201+
return self.auto_update
202+
203+
@auto_update.setter
204+
def auto_update(self, value):
205+
self.auto_update = bool(value)
206+
207+
def __str__(self):
208+
return f"<PulseGroup({len(self)})>"
209+
210+
211+
class CyclicSignal:
212+
def __init__(self, data, phase=0):
213+
self._data = data
214+
self._phase = 0
215+
self.phase = phase
216+
self._scale = len(self._data) - 1
217+
218+
@property
219+
def phase(self):
220+
return self._phase
221+
222+
@phase.setter
223+
def phase(self, value):
224+
self._phase = value % 1
225+
226+
@property
227+
def value(self):
228+
idxf = self._phase * len(self._data)
229+
idx = int(idxf)
230+
frac = idxf % 1
231+
idx1 = (idx + 1) % len(self._data)
232+
val = self._data[idx]
233+
val1 = self._data[idx1]
234+
return val + (val1 - val) * frac
235+
236+
def advance(self, delta):
237+
self._phase = (self._phase + delta) % 1
238+
239+
240+
if __name__ == "__main__":
241+
pulsers = PulseGroup(board.SERVO_1, 18, auto_update=False)
242+
# Set the phase of each servo so that servo 0 starts at offset 0ms, servo 1
243+
# at offset 2.5ms, ...
244+
# For up to 8 servos, this means their duty cycles do not overlap. Otherwise,
245+
# servo 9 is also at offset 0ms, etc.
246+
for j, p in enumerate(pulsers):
247+
p.phase = 8192 * (j % 8)
248+
249+
servos = [servo.Servo(p) for p in pulsers]
250+
251+
sine = np.sin(np.linspace(0, 2 * np.pi, 50, endpoint=False)) * 0.5 + 0.5
252+
print(sine)
253+
254+
signals = [CyclicSignal(sine, j / len(servos)) for j in range(len(servos))]
255+
256+
t0 = adafruit_ticks.ticks_ms()
257+
while True:
258+
t1 = adafruit_ticks.ticks_ms()
259+
for servo, signal in zip(servos, signals):
260+
signal.advance((t1 - t0) / 8000)
261+
servo.fraction = signal.value
262+
pulsers.update()
263+
print(adafruit_ticks.ticks_diff(t1, t0), "ms")
264+
t0 = t1

0 commit comments

Comments
 (0)