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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy