kopia lustrzana https://github.com/peterhinch/micropython-samples
Update encoders docs and samples.
rodzic
e245f6f37a
commit
72ca2425dc
33
README.md
33
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 |
Ładowanie…
Reference in New Issue