Repository navigation
Expand file tree
/
Copy pathstepper.py
More file actions
93 lines (75 loc) 路 3.15 KB
/
Copy pathstepper.py
File metadata and controls
93 lines (75 loc) 路 3.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
"""Step/dir stepper driver (A4988/DRV8825/TMC-style) over the RMT primitive.
Pulse timing is generated on the ESP32; ramps and position live in Python.
from espbridge.drivers.stepper import Stepper
motor = Stepper(esp, step_pin=12, dir_pin=14)
motor.move(400, speed=800, accel=1600) # blocks until done
motor.run(-200); ...; motor.stop() # continuous
"""
from __future__ import annotations
import math
from ..rmt import Symbol
_PULSE = 10 # step-pulse high time, us (drivers need >=1-2 us)
_MAX_SYM = 0x7FFF # RMT symbol duration limit
_CHUNK = 1000 # symbols per RMT_TX request (payload-bounded)
def _step_syms(period_us: int) -> list[Symbol]:
"""One step: short high pulse + low for the rest (split to fit u15)."""
syms: list[Symbol] = [(1, _PULSE)]
low = max(period_us - _PULSE, 1)
while low > _MAX_SYM:
syms.append((0, _MAX_SYM))
low -= _MAX_SYM
syms.append((0, low))
return syms
def ramp_periods(steps: int, speed: float, accel: float) -> list[int]:
"""Pure planner: per-step periods (us) for a trapezoidal/triangular move."""
if accel <= 0:
return [round(1e6 / speed)] * steps
out = []
for i in range(steps):
v_acc = math.sqrt(2 * accel * (i + 1))
v_dec = math.sqrt(2 * accel * (steps - i))
out.append(round(1e6 / min(speed, v_acc, v_dec)))
return out
class Stepper:
def __init__(self, bridge, step_pin: int, dir_pin: int | None = None, *,
invert_dir: bool = False):
self._rmt = bridge.rmt
self._gpio = bridge.gpio
self._step = step_pin
self._dir = dir_pin
self._invert = invert_dir
self.position = 0
self._rmt.init_tx(step_pin, 1_000_000)
if dir_pin is not None:
self._gpio.mode(dir_pin, "output")
def _set_dir(self, forward: bool) -> None:
if self._dir is not None:
self._gpio.write(self._dir, int(forward ^ self._invert))
def move(self, steps: int, speed: float = 500, accel: float = 0) -> None:
"""Move `steps` (sign = direction) at `speed` steps/s; blocks.
accel > 0 ramps up/down (steps/s^2). Long moves are sent in chunks;
a chunk boundary may pause the motor for one link round-trip (~ms),
which steppers tolerate (no steps are lost, motion just hiccups).
"""
if steps == 0:
return
self._set_dir(steps > 0)
syms: list[Symbol] = []
for period in ramp_periods(abs(steps), speed, accel):
syms += _step_syms(period)
if len(syms) >= _CHUNK:
self._rmt.tx(self._step, syms)
syms = []
if syms:
self._rmt.tx(self._step, syms)
self.position += steps
def run(self, speed: float) -> None:
"""Spin continuously at `speed` steps/s (sign = direction) until stop().
Position is NOT tracked while free-running.
"""
self._set_dir(speed > 0)
self._rmt.tx_loop(self._step, _step_syms(round(1e6 / abs(speed))))
def stop(self) -> None:
self._rmt.tx_stop(self._step)
def deinit(self) -> None:
self._rmt.deinit(self._step)