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
|
## 4.7 Rotary Incremental Encoder
|
||||||
|
|
||||||
Classes for handling incremental rotary position encoders. Note Pyboard timers
|
These devices produce digital signals from a shaft's rotary motion in such a
|
||||||
can do this in hardware, as shown
|
way that the absolute angle may be deduced. Specifically they measure
|
||||||
[in this script](https://github.com/dhylands/upy-examples/blob/master/encoder.py)
|
incremental change: it is up to the code to keep track of absolute position, a
|
||||||
from Dave Hylands. These samples cater for cases where that solution can't be
|
task which has some pitfalls. [This doc](./encoders/ENCODER.md) discusses this
|
||||||
used. The [encoder_timed.py](./encoders/encoder_timed.py) sample provides rate
|
and points to some solutions in MicroPython code.
|
||||||
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.
|
|
||||||
|
|
||||||
## 4.8 Pseudo random number generators
|
## 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
|
import pyb
|
||||||
|
|
||||||
class Encoder(object):
|
class Encoder:
|
||||||
def __init__(self, pin_x, pin_y, reverse, scale):
|
def __init__(self, pin_x, pin_y, reverse, scale):
|
||||||
self.reverse = reverse
|
self.reverse = reverse
|
||||||
self.scale = scale
|
self.scale = scale
|
||||||
|
|
|
@ -1,32 +1,36 @@
|
||||||
|
# encoder_portable.py
|
||||||
|
|
||||||
# Encoder Support: this version should be portable between MicroPython platforms
|
# 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
|
from machine import Pin
|
||||||
|
|
||||||
class Encoder(object):
|
class Encoder:
|
||||||
def __init__(self, pin_x, pin_y, reverse, scale):
|
def __init__(self, pin_x, pin_y, scale=1):
|
||||||
self.reverse = reverse
|
|
||||||
self.scale = scale
|
self.scale = scale
|
||||||
self.forward = True
|
self.forward = True
|
||||||
self.pin_x = pin_x
|
self.pin_x = pin_x
|
||||||
self.pin_y = pin_y
|
self.pin_y = pin_y
|
||||||
self._pos = 0
|
self._pos = 0
|
||||||
self.x_interrupt = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.x_callback)
|
try:
|
||||||
self.y_interrupt = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self.y_callback)
|
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):
|
def x_callback(self, pin):
|
||||||
self.forward = self.pin_x.value() ^ self.pin_y.value() ^ self.reverse
|
self.forward = pin() ^ self.pin_y()
|
||||||
self._pos += 1 if self.forward else -1
|
self._pos += 1 if self.forward else -1
|
||||||
|
|
||||||
def y_callback(self, line):
|
def y_callback(self, pin):
|
||||||
self.forward = self.pin_x.value() ^ self.pin_y.value() ^ self.reverse ^ 1
|
self.forward = self.pin_x() ^ pin() ^ 1
|
||||||
self._pos += 1 if self.forward else -1
|
self._pos += 1 if self.forward else -1
|
||||||
|
|
||||||
@property
|
def position(self, value=None):
|
||||||
def position(self):
|
if value is not None:
|
||||||
|
self._pos = value // self.scale
|
||||||
return self._pos * 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():
|
# Copyright (c) 2016-2021 Peter Hinch
|
||||||
new_semantics = utime.ticks_diff(2, 1) == 1
|
# Released under the MIT License (MIT) - see LICENSE file
|
||||||
def func(old, new):
|
|
||||||
nonlocal new_semantics
|
|
||||||
if new_semantics:
|
|
||||||
return utime.ticks_diff(new, old)
|
|
||||||
return utime.ticks_diff(old, new)
|
|
||||||
return func
|
|
||||||
|
|
||||||
ticksdiff = tdiff()
|
import utime
|
||||||
|
from machine import Pin, disable_irq, enable_irq
|
||||||
|
|
||||||
class EncoderTimed(object):
|
class EncoderTimed:
|
||||||
def __init__(self, pin_x, pin_y, reverse, scale):
|
def __init__(self, pin_x, pin_y, scale=1):
|
||||||
self.reverse = reverse
|
self.scale = scale # Optionally scale encoder rate to distance/angle
|
||||||
self.scale = scale
|
|
||||||
self.tprev = 0
|
self.tprev = 0
|
||||||
self.tlast = 0
|
self.tlast = 0
|
||||||
self.forward = True
|
self.forward = True
|
||||||
self.pin_x = pin_x
|
self.pin_x = pin_x
|
||||||
self.pin_y = pin_y
|
self.pin_y = pin_y
|
||||||
self._pos = 0
|
self._pos = 0
|
||||||
self.x_interrupt = pyb.ExtInt(pin_x, pyb.ExtInt.IRQ_RISING_FALLING, pyb.Pin.PULL_NONE, self.x_callback)
|
try:
|
||||||
self.y_interrupt = pyb.ExtInt(pin_y, pyb.ExtInt.IRQ_RISING_FALLING, pyb.Pin.PULL_NONE, self.y_callback)
|
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):
|
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._pos += 1 if self.forward else -1
|
||||||
self.tprev = self.tlast
|
self.tprev = self.tlast
|
||||||
self.tlast = utime.ticks_us()
|
self.tlast = utime.ticks_us()
|
||||||
|
|
||||||
def y_callback(self, line):
|
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._pos += 1 if self.forward else -1
|
||||||
self.tprev = self.tlast
|
self.tprev = self.tlast
|
||||||
self.tlast = utime.ticks_us()
|
self.tlast = utime.ticks_us()
|
||||||
|
|
||||||
@property
|
def rate(self): # Return rate in signed distance/angle per second
|
||||||
def rate(self): # Return rate in edges per second
|
state = disable_irq()
|
||||||
self.x_interrupt.disable()
|
tlast = self.tlast # Cache current values
|
||||||
self.y_interrupt.disable()
|
tprev = self.tprev
|
||||||
if ticksdiff(self.tlast, utime.ticks_us) > 2000000: # It's stopped
|
enable_irq(state)
|
||||||
|
if utime.ticks_diff(utime.ticks_us(), tlast) > 2_000_000: # It's stopped
|
||||||
result = 0.0
|
result = 0.0
|
||||||
else:
|
else:
|
||||||
result = 1000000.0/(ticksdiff(self.tprev, self.tlast))
|
result = 1000000.0/(utime.ticks_diff(tlast, tprev))
|
||||||
self.x_interrupt.enable()
|
|
||||||
self.y_interrupt.enable()
|
|
||||||
result *= self.scale
|
result *= self.scale
|
||||||
return result if self.forward else -result
|
return result if self.forward else -result
|
||||||
|
|
||||||
@property
|
|
||||||
def position(self):
|
def position(self):
|
||||||
return self._pos*self.scale
|
return self._pos * self.scale
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self._pos = 0
|
self._pos = 0
|
||||||
|
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 13 KiB |
Ładowanie…
Reference in New Issue