|
| 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