Skip to content

Commit b7da62f

Browse files
committed
Add primitives/encoder.
1 parent ef8be12 commit b7da62f

File tree

3 files changed

+144
-12
lines changed

3 files changed

+144
-12
lines changed

v3/docs/DRIVERS.md

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# 0. Introduction
22

3-
Drivers for switches and pushbuttons are provided, plus a retriggerable delay
4-
class. The switch and button drivers support debouncing. The switch driver
5-
provides for running a callback or launching a coroutine (coro) on contact
6-
closure and/or opening.
3+
Drivers for switches and pushbuttons are provided. Switch and button drivers
4+
support debouncing. The switch driver provides for running a callback or
5+
launching a coroutine (coro) on contact closure and/or opening. The pushbutton
6+
driver extends this to support long-press and double-click events.
77

8-
The pushbutton driver extends this to support long-press and double-click
9-
events.
8+
An `Encoder` class is provided to support rotary control knobs based on
9+
quadrature encoder switches. This is not intended for high throughput encoders
10+
as used in CNC machines where
11+
[an interrupt based solution](https://github.com/peterhinch/micropython-samples#47-rotary-incremental-encoder)
12+
is required.
1013

1114
The asynchronous ADC supports pausing a task until the value read from an ADC
1215
goes outside defined bounds.
@@ -24,9 +27,11 @@ goes outside defined bounds.
2427
5. [ADC monitoring](./DRIVERS.md#5-adc-monitoring) Pause until an ADC goes out of bounds
2528
5.1 [AADC class](./DRIVERS.md#51-aadc-class)
2629
5.2 [Design note](./DRIVERS.md#52-design-note)
27-
6. [Additional functions](./DRIVERS.md#6-additional-functions)
28-
6.1 [launch](./DRIVERS.md#61-launch) Run a coro or callback interchangeably
29-
6.2 [set_global_exception](./DRIVERS.md#62-set_global_exception) Simplify debugging with a global exception handler
30+
6. [Quadrature encoders](./DRIVERS.md#6-quadrature-encoders)
31+
6.1 [Encoder class](./DRIVERS.md#61-encoder-class)
32+
7. [Additional functions](./DRIVERS.md#7-additional-functions)
33+
7.1 [launch](./DRIVERS.md#71-launch) Run a coro or callback interchangeably
34+
7.2 [set_global_exception](./DRIVERS.md#72-set_global_exception) Simplify debugging with a global exception handler
3035

3136
###### [Tutorial](./TUTORIAL.md#contents)
3237

@@ -333,9 +338,56 @@ this for applications requiring rapid response.
333338

334339
###### [Contents](./DRIVERS.md#1-contents)
335340

336-
# 6. Additional functions
341+
# 6. Quadrature encoders
342+
343+
The `Encoder` class is an asynchronous driver for control knobs based on
344+
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.
352+
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.
357+
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+
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.
366+
367+
## 6.1 Encoder class
368+
369+
Constructor arguments:
370+
1. `pin_x` Initialised `machine.Pin` instances for the switch. Should be set
371+
as `Pin.IN` and have pullups.
372+
2. `pin_y` Ditto.
373+
3. `v=0` Initial value.
374+
4. `vmin=None` By default the `value` of the encoder can vary without limit.
375+
Optionally maximum and/or minimum limits can be set.
376+
5. `vmax=None`
377+
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.
381+
7. `args=()` An optional tuple of args for the callback.
382+
383+
Synchronous method:
384+
* `value` No args. Returns an integer being the `Encoder` current value.
337385

338-
## 6.1 Launch
386+
###### [Contents](./DRIVERS.md#1-contents)
387+
388+
# 7. Additional functions
389+
390+
## 7.1 Launch
339391

340392
Import as follows:
341393
```python
@@ -347,7 +399,7 @@ runs it and returns the callback's return value. If a coro is passed, it is
347399
converted to a `task` and run asynchronously. The return value is the `task`
348400
instance. A usage example is in `primitives/switch.py`.
349401

350-
## 6.2 set_global_exception
402+
## 7.2 set_global_exception
351403

352404
Import as follows:
353405
```python

v3/primitives/encoder.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# encoder.py Asynchronous driver for incremental quadrature encoder.
2+
3+
# Copyright (c) 2021 Peter Hinch
4+
# Released under the MIT License (MIT) - see LICENSE file
5+
6+
# This driver is intended for encoder-based control knobs. It is not
7+
# suitable for NC machine applications. Please see the docs.
8+
9+
import uasyncio as asyncio
10+
11+
class Encoder:
12+
def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None,
13+
callback=lambda a, b : None, args=()):
14+
self._v = v
15+
asyncio.create_task(self._run(pin_x, pin_y, vmin, vmax,
16+
callback, args))
17+
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
22+
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)
36+
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+
48+
def value(self):
49+
return self._v

v3/primitives/tests/encoder_test.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# encoder_test.py Test for asynchronous driver for incremental quadrature encoder.
2+
3+
# Copyright (c) 2021 Peter Hinch
4+
# Released under the MIT License (MIT) - see LICENSE file
5+
6+
from machine import Pin
7+
import uasyncio as asyncio
8+
from primitives.encoder import Encoder
9+
10+
11+
px = Pin(33, Pin.IN)
12+
py = Pin(25, Pin.IN)
13+
14+
def cb(pos, fwd):
15+
print(pos, fwd)
16+
17+
async def main():
18+
while True:
19+
await asyncio.sleep(1)
20+
21+
def test():
22+
print('Running encoder test. Press ctrl-c to teminate.')
23+
enc = Encoder(px, py, v=0, vmin=0, vmax=100, callback=cb)
24+
try:
25+
asyncio.run(main())
26+
except KeyboardInterrupt:
27+
print('Interrupted')
28+
finally:
29+
asyncio.new_event_loop()
30+
31+
test()

0 commit comments

Comments
 (0)