Update encoders docs and samples.

pull/25/head
Peter Hinch 2021-07-19 08:43:25 +01:00
rodzic e245f6f37a
commit 72ca2425dc
6 zmienionych plików z 187 dodań i 74 usunięć

Wyświetl plik

@ -197,34 +197,11 @@ connection details where possible.
## 4.7 Rotary Incremental Encoder
Classes for handling incremental rotary position encoders. Note Pyboard timers
can do this in hardware, as shown
[in this script](https://github.com/dhylands/upy-examples/blob/master/encoder.py)
from Dave Hylands. These samples cater for cases where that solution can't be
used. The [encoder_timed.py](./encoders/encoder_timed.py) sample provides rate
information by timing successive edges. In practice this is likely to need
filtering to reduce jitter caused by imperfections in the encoder geometry.
There are other algorithms but this is the simplest and fastest I've
encountered.
These were written for encoders producing logic outputs. For switches, adapt
the pull definition to provide a pull up or pull down as required.
The [encoder_portable.py](./encoders/encoder_portable.py) version should work on
all MicroPython platforms. Tested on ESP8266. Note that interrupt latency on
the ESP8266 limits performance. ESP32 has similar limitations.
See also [this asynchronous driver](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/DRIVERS.md#6-quadrature-encoders)
intended for control knobs based on quadrature switches like
[this Adafruit product](https://www.adafruit.com/product/377). There are two
aspects to this. Firstly, the solutions in this repo run callbacks in an
interrupt context. This is necessary in applications like NC machines where
performance is paramount, but it can be problematic in applications for control
knobs where a user adjustment can trigger complex application behavior. The
asynchronous driver runs the callback in a `uasyncio` context. Secondly there
can be practical timing and sensitivity issues with control knobs which the
driver aims to address.
These devices produce digital signals from a shaft's rotary motion in such a
way that the absolute angle may be deduced. Specifically they measure
incremental change: it is up to the code to keep track of absolute position, a
task which has some pitfalls. [This doc](./encoders/ENCODER.md) discusses this
and points to some solutions in MicroPython code.
## 4.8 Pseudo random number generators

Wyświetl plik

@ -0,0 +1,131 @@
# Incremental encoders
There are three technologies that I am aware of:
1. Optical.
2. Magnetic.
3. Mechanical (using switch contacts).
All produce quadrature signals looking like this:
![Image](./quadrature.jpg)
consequently the same code may be used regardless of encoder type.
They have two primary applications: control knobs for user input and shaft
position and speed measurements on machines. For user input a mechanical
device, being inexpensive, usually suffices. See
[this Adafruit product](https://www.adafruit.com/product/377).
In applications such as NC machines longevity and reliability are paramount:
this normally rules out mechanical devices. Rotational speed is also likely to
be too high. In machine tools it is vital to maintain perfect accuracy over
very long periods. This may impact the electronic design of the interface
between the encoder and the host. High precision comes at no cost in code, but
there may be issues in devices with high interrupt latency such as ESP32,
especially with SPIRAM.
The ideal host, especially for precison applications, is a Pyboard. This is
because Pyboard timers can decode in hardware, as shown
[in this script](https://github.com/dhylands/upy-examples/blob/master/encoder.py)
from Dave Hylands. Hardware decoding eliminates all concerns over interrupt
latency or input pulse rates.
# Basic encoder script
This comes from `encoder_portable.py` in this repo. It uses the simplest and
fastest algorithm I know. It should run on any MicrPython platform, but please
read the following notes as there are potential issues.
```python
from machine import Pin
class Encoder:
def __init__(self, pin_x, pin_y, scale=1):
self.scale = scale
self.forward = True
self.pin_x = pin_x
self.pin_y = pin_y
self._pos = 0
try:
self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback, hard=True)
self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback, hard=True)
except TypeError:
self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback)
self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback)
def x_callback(self, pin):
self.forward = pin() ^ self.pin_y()
self._pos += 1 if self.forward else -1
def y_callback(self, pin):
self.forward = self.pin_x() ^ pin() ^ 1
self._pos += 1 if self.forward else -1
def position(self, value=None):
if value is not None:
self._pos = value // self.scale
return self._pos * self.scale
```
If the direction is incorrect, transpose the X and Y pins in the constructor
call.
# Problem 1: Interrupt latency
By default, pin interrupts defined using the `machine` module are soft. This
introduces latency if a line changes state when a garbage collection is in
progress. The above script attempts to use hard IRQ's, but not all platforms
support them (notably ESP8266 and ESP32).
Hard IRQ's present their own issues documented
[here](https://docs.micropython.org/en/latest/reference/isr_rules.html) but
the above script conforms with these rules.
# Problem 2: Jitter
The picture above is idealised. In practice it is possible to receive a
succession of edges on one input line, with no transitions on the other. On
mechanical encoders this may be caused by
[contact bounce](http://www.ganssle.com/debouncing.htm). On any type it can
result from vibration, where the encoder happens to stop at an angle exactly
matching an edge. Code must be designed to accommodate this. The above sample
does this. It is possible that the above latency issue may cause pulses to be
missed, notably on platforms which don't support hard IRQ's. In such cases
hardware may need to be adapted to limit the rate at which signals can change,
possibly with a CR low pass filter and a schmitt trigger. This clearly won't
work if the pulse rate from actual shaft rotation exceeds this limit.
# Problem 3: Concurrency
The presented code samples use interrupts in order to handle the potentially
high rate at which transitions can occur. The above script maintains a
position value `._pos` which can be queried at any time. This does not present
concurrency issues. However some applications, notably in user interface
designs, may require an encoder action to trigger complex behaviour. The
obvious solution would be to adapt the script to do this by having the two ISR
methods call a function. However the function would run in an interrupt context
which (even with soft IRQ's) presents concurrency issues where an application's
data can change at any point in the application's execution. Further, a complex
function would cause the ISR to block for a long period with the potential for
data loss.
A solution to this is an interface between the ISR's and `uasyncio` whereby the
ISR's set a `ThreadSafeFlag`. This is awaited by a `uasyncio` `Task` which runs
a user supplied callback. The latter runs in a `uasyncio` context: the state of
any `Task` can only change at times when it has yielded to the scheduler in
accordance with `uasyncio` rules. This is implemented in
[this asynchronous driver](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/DRIVERS.md#6-quadrature-encoders).
This also handles the case where a mechanical encoder has a large number of
states per revolution. The driver has the option to divide these down, reducing
the rate at which callbacks occur.
# Code samples
1. `encoder_portable.py` Suitable for most purposes.
2. `encoder_timed.py` Provides rate information by timing successive edges. In
practice this is likely to need filtering to reduce jitter caused by
imperfections in the encoder geometry. With a mechanical knob turned by an
anthropoid ape it's debatable whether it produces anything useful :)
3. `encoder.py` An old Pyboard-specific version.
These were written for encoders producing logic outputs. For switches, adapt
the pull definition to provide a pull up or pull down as required, or provide
physical resistors. This is my preferred solution as the internal resistors on
most platforms have a rather high value.

Wyświetl plik

@ -1,6 +1,11 @@
# encoder.py
# Copyright (c) 2016-2021 Peter Hinch
# Released under the MIT License (MIT) - see LICENSE file
import pyb
class Encoder(object):
class Encoder:
def __init__(self, pin_x, pin_y, reverse, scale):
self.reverse = reverse
self.scale = scale

Wyświetl plik

@ -1,32 +1,36 @@
# encoder_portable.py
# Encoder Support: this version should be portable between MicroPython platforms
# Thanks to Evan Widloski for the adaptation to the machine module
# Thanks to Evan Widloski for the adaptation to use the machine module
# Copyright (c) 2017-2021 Peter Hinch
# Released under the MIT License (MIT) - see LICENSE file
from machine import Pin
class Encoder(object):
def __init__(self, pin_x, pin_y, reverse, scale):
self.reverse = reverse
class Encoder:
def __init__(self, pin_x, pin_y, scale=1):
self.scale = scale
self.forward = True
self.pin_x = pin_x
self.pin_y = pin_y
self._pos = 0
self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback)
self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback)
try:
self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback, hard=True)
self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback, hard=True)
except TypeError:
self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback)
self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback)
def x_callback(self, line):
self.forward = self.pin_x.value() ^ self.pin_y.value() ^ self.reverse
def x_callback(self, pin):
self.forward = pin() ^ self.pin_y()
self._pos += 1 if self.forward else -1
def y_callback(self, line):
self.forward = self.pin_x.value() ^ self.pin_y.value() ^ self.reverse ^ 1
def y_callback(self, pin):
self.forward = self.pin_x() ^ pin() ^ 1
self._pos += 1 if self.forward else -1
@property
def position(self):
def position(self, value=None):
if value is not None:
self._pos = value // self.scale
return self._pos * self.scale
@position.setter
def position(self, value):
self._pos = value // self.scale

Wyświetl plik

@ -1,57 +1,53 @@
import pyb, utime
# encoder_timed.py
def tdiff():
new_semantics = utime.ticks_diff(2, 1) == 1
def func(old, new):
nonlocal new_semantics
if new_semantics:
return utime.ticks_diff(new, old)
return utime.ticks_diff(old, new)
return func
# Copyright (c) 2016-2021 Peter Hinch
# Released under the MIT License (MIT) - see LICENSE file
ticksdiff = tdiff()
import utime
from machine import Pin, disable_irq, enable_irq
class EncoderTimed(object):
def __init__(self, pin_x, pin_y, reverse, scale):
self.reverse = reverse
self.scale = scale
class EncoderTimed:
def __init__(self, pin_x, pin_y, scale=1):
self.scale = scale # Optionally scale encoder rate to distance/angle
self.tprev = 0
self.tlast = 0
self.forward = True
self.pin_x = pin_x
self.pin_y = pin_y
self._pos = 0
self.x_interrupt = pyb.ExtInt(pin_x, pyb.ExtInt.IRQ_RISING_FALLING, pyb.Pin.PULL_NONE, self.x_callback)
self.y_interrupt = pyb.ExtInt(pin_y, pyb.ExtInt.IRQ_RISING_FALLING, pyb.Pin.PULL_NONE, self.y_callback)
try:
self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback, hard=True)
self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback, hard=True)
except TypeError:
self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback)
self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback)
def x_callback(self, line):
self.forward = self.pin_x.value() ^ self.pin_y.value() ^ self.reverse
self.forward = self.pin_x.value() ^ self.pin_y.value()
self._pos += 1 if self.forward else -1
self.tprev = self.tlast
self.tlast = utime.ticks_us()
def y_callback(self, line):
self.forward = self.pin_x.value() ^ self.pin_y.value() ^ self.reverse ^ 1
self.forward = self.pin_x.value() ^ self.pin_y.value() ^ 1
self._pos += 1 if self.forward else -1
self.tprev = self.tlast
self.tlast = utime.ticks_us()
@property
def rate(self): # Return rate in edges per second
self.x_interrupt.disable()
self.y_interrupt.disable()
if ticksdiff(self.tlast, utime.ticks_us) > 2000000: # It's stopped
def rate(self): # Return rate in signed distance/angle per second
state = disable_irq()
tlast = self.tlast # Cache current values
tprev = self.tprev
enable_irq(state)
if utime.ticks_diff(utime.ticks_us(), tlast) > 2_000_000: # It's stopped
result = 0.0
else:
result = 1000000.0/(ticksdiff(self.tprev, self.tlast))
self.x_interrupt.enable()
self.y_interrupt.enable()
result = 1000000.0/(utime.ticks_diff(tlast, tprev))
result *= self.scale
return result if self.forward else -result
@property
def position(self):
return self._pos*self.scale
return self._pos * self.scale
def reset(self):
self._pos = 0

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 13 KiB