Skip to content

Commit 3661ee9

Browse files
committed
primitives/encoder uses interrupts.
1 parent 8c273d6 commit 3661ee9

File tree

3 files changed

+63
-52
lines changed

3 files changed

+63
-52
lines changed

v3/docs/DRIVERS.md

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -340,29 +340,28 @@ this for applications requiring rapid response.
340340

341341
# 6. Quadrature encoders
342342

343+
This is a work in progress. Changes may occur.
344+
343345
The `Encoder` class is an asynchronous driver for control knobs based on
344346
quadrature encoder switches such as
345-
[this Adafruit product](https://www.adafruit.com/product/377). This is not
346-
intended for high throughput encoders such as those used in CNC machines where
347-
[an interrupt based solution](https://github.com/peterhinch/micropython-samples#47-rotary-incremental-encoder)
348-
is required. This is because the driver works by polling the switches. The
349-
latency between successive readings of the switch state will depend on the
350-
behaviour of other tasks in the application, but if changes occur rapidly it is
351-
likely that transitions will be missed.
347+
[this Adafruit product](https://www.adafruit.com/product/377). The driver is
348+
not intended for applications such as CNC machines where
349+
[a solution such as this one](https://github.com/peterhinch/micropython-samples#47-rotary-incremental-encoder)
350+
is required. Drivers for NC machines must never miss an edge. Contact bounce or
351+
vibration induced jitter can cause transitions to occur at a high rate; these
352+
must be tracked.
352353

353-
In the context of a rotary dial this is usually not a problem, firstly because
354-
changes occur at a relatively low rate and secondly because there is usually
355-
some form of feedback to the user. A single missed increment on a CNC machine
356-
is a fail. In a user interface it usually is not.
354+
This driver runs the user supplied callback in an `asyncio` context, so it runs
355+
only when other tasks have yielded to the scheduler. This ensures that the
356+
callback can run safely. The driver allows limits to be assigned to the control
357+
so that a dial running from (say) 0 to 100 may be implemented. If limits are
358+
used, encoder values no longer represent absolute angles.
357359

358-
The API uses a callback which occurs whenever the value changes. Alternatively
359-
the `Encoder` may be queried to retrieve the current position.
360+
The callback only runs if a change in position has occurred.
360361

361-
A high throughput solution can be used with rotary dials but there is a
362-
difference in the way contact bounce (or vibration induced jitter) are handled.
363-
The high throughput solution results in +-1 count jitter with the callback
364-
repeatedly occurring. This driver uses hysteresis to ensure that transitions
365-
due to contact bounce are ignored.
362+
A consequence of the callback running in an `asyncio` context is that, by the
363+
time it runs, the encoder's position may have changed by more than one
364+
increment.
366365

367366
## 6.1 Encoder class
368367

@@ -375,14 +374,17 @@ Constructor arguments:
375374
Optionally maximum and/or minimum limits can be set.
376375
5. `vmax=None`
377376
6. `callback=lambda *_ : None` Optional callback function. The callback
378-
receives two args, `v` being the encoder's current value and `fwd` being
379-
`True` if the value has incremented of `False` if it decremented. Further args
380-
may be appended by the following.
377+
receives two args, `v` being the encoder's current value and `delta` being
378+
the signed difference between the current value and the previous one. Further
379+
args may be appended by the following.
381380
7. `args=()` An optional tuple of args for the callback.
382381

383382
Synchronous method:
384383
* `value` No args. Returns an integer being the `Encoder` current value.
385384

385+
Class variable:
386+
* `LATENCY=50` This sets a minumum period (in ms) between callback runs.
387+
386388
###### [Contents](./DRIVERS.md#1-contents)
387389

388390
# 7. Additional functions

v3/primitives/encoder.py

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,52 @@
77
# suitable for NC machine applications. Please see the docs.
88

99
import uasyncio as asyncio
10+
from machine import Pin
1011

1112
class Encoder:
13+
LATENCY = 50
14+
1215
def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None,
1316
callback=lambda a, b : None, args=()):
17+
self._pin_x = pin_x
18+
self._pin_y = pin_y
1419
self._v = v
15-
asyncio.create_task(self._run(pin_x, pin_y, vmin, vmax,
16-
callback, args))
20+
self._tsf = asyncio.ThreadSafeFlag()
21+
try:
22+
xirq = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._x_cb, hard=True)
23+
yirq = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._y_cb, hard=True)
24+
except TypeError:
25+
xirq = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._x_cb)
26+
yirq = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._y_cb)
27+
asyncio.create_task(self._run(vmin, vmax, callback, args))
28+
29+
30+
# Hardware IRQ's
31+
def _x_cb(self, pin):
32+
fwd = pin() ^ self._pin_y()
33+
self._v += 1 if fwd else -1
34+
self._tsf.set()
1735

18-
def _run(self, pin_x, pin_y, vmin, vmax, callback, args):
19-
xp = pin_x() # Prior levels
20-
yp = pin_y()
21-
pf = None # Prior direction
36+
def _y_cb(self, pin):
37+
fwd = pin() ^ self._pin_x() ^ 1
38+
self._v += 1 if fwd else -1
39+
self._tsf.set()
40+
41+
async def _run(self, vmin, vmax, cb, args):
42+
pv = self._v # Prior value
2243
while True:
23-
await asyncio.sleep_ms(0)
24-
x = pin_x() # Current levels
25-
y = pin_y()
26-
if xp == x:
27-
if yp == y:
28-
continue # No change, nothing to do
29-
fwd = x ^ y ^ 1 # y changed
30-
else:
31-
fwd = x ^ y # x changed
32-
pv = self._v # Cache prior value
33-
nv = pv + (1 if fwd else -1) # New value
34-
if vmin is not None:
35-
nv = max(vmin, nv)
44+
await self._tsf.wait()
45+
cv = self._v # Current value
3646
if vmax is not None:
37-
nv = min(vmax, nv)
38-
if nv != pv: # Change
39-
rev = (pf is not None) and (pf != fwd)
40-
if not rev:
41-
callback(nv, fwd, *args)
42-
self._v = nv
43-
44-
pf = fwd # Update prior state
45-
xp = x
46-
yp = y
47+
cv = min(cv, vmax)
48+
if vmin is not None:
49+
cv = max(cv, vmin)
50+
self._v = cv
51+
#print(cv, pv)
52+
if cv != pv:
53+
cb(cv, cv - pv, *args) # User CB in uasyncio context
54+
pv = cv
55+
await asyncio.sleep_ms(self.LATENCY)
4756

4857
def value(self):
4958
return self._v

v3/primitives/tests/encoder_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
px = Pin(33, Pin.IN)
1212
py = Pin(25, Pin.IN)
1313

14-
def cb(pos, fwd):
15-
print(pos, fwd)
14+
def cb(pos, delta):
15+
print(pos, delta)
1616

1717
async def main():
1818
while True:

0 commit comments

Comments
 (0)