Improve encoder_portable.py

pull/29/head
Peter Hinch 2022-04-19 13:14:19 +01:00
rodzic 3778b17918
commit 7379c9085a
2 zmienionych plików z 44 dodań i 44 usunięć

Wyświetl plik

@ -34,10 +34,11 @@ latency or input pulse rates.
# 2. 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 MicroPython platform, but please
read the following notes as there are potential issues.
This illustrates the basic algorithm used in these drivers, which is the
simplest and fastest way I know. In practice the interrupt service routines are
slightly more complex for reasons discussed below, but this code can be run on
any MicroPython platform. Note the adaptation for platforms that don't support
hard IRQ's.
```python
from machine import Pin
@ -70,6 +71,9 @@ class Encoder:
If the direction is incorrect, transpose the X and Y pins in hardware or in the
constructor call.
Contrary to common opinion a state table is not necessary to produce a correct
algorithm: see [section 7](./ENCODERS.md#7-algorithm).
# 3. Problem 1: Interrupt latency
By default, pin interrupts defined using the `machine` module are soft. This
@ -77,10 +81,6 @@ 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.
# 4. Problem 2: Jitter
The picture above is idealised. In practice it is possible to receive a
@ -92,11 +92,10 @@ matching an edge. An arbitrarily long sequence of pulses on one line is the
result. A correct algorithm must be able to cope with this: the outcome will be
one digit of jitter in the output count but no systematic drift.
Contrary to common opinion a state table is not necessary to produce a correct
algorithm: see [section 7](./ENCODERS.md#7-algorithm).
In practice the frequency of such edges may be arbitrarily high. This imposes
a need for synchronisation.
a need for synchronisation to limit the possible rate if bit-perfect results
are required. In any solution based on interrupts it is necessary to avoid the
condition where multiple pulses arrive during the latency period.
## 4.1 Synchronisation
@ -115,13 +114,8 @@ determine the maximum permissible rotation speed of the encoder.
Contact bounce on mechanical encoders can also result in invalid logic levels.
This can cause a variety of unwanted results: conditioning with a CR network
and a Schmitt trigger should be considered.
In practice bit-perfect results are often not required: synchronisation can
simply be ignored, relying on software to provide reasonably accurate tracking
of position. Applications using encoders for user controls normally provide a
form of user feedback. The occasional missed pulse caused by fast contact
bounce will not be noticed.
and a Schmitt trigger should be considered. That said, remarkably accurate
tracking can be achieved in code.
Where bit-perfect results are required the simplest approach is to use a target
which supports hardware decoding and which pre-synchronises the signals. STM32
@ -250,18 +244,20 @@ be stable when an edge occurs on `pin_x`, the state of `pin_x` may have changed
by the time the latency has elapsed and the ISR reads its value. In this case
the change will be registered with the wrong direction.
Further, this second pin change will trigger another interrupt. The consequence
of this depends on hardware and firmware implementations. The interrupt may be
missed, it may execute after the first has completed, or re-entrancy may take
place. However, by this time an error has already occurred as described above.
There is nothing to gain by trying to fix this (e.g. by disabling interrupts in
the ISR).
In a careful test of a software decoder on a Pyboard 1.1 with an optical
encoder pulses were occasionally missed. This confirms that, even with clean
logic levels and hard IRQ's, on rare occasions pulses arrive too fast for the
ISR to track.
This is handled by the following adaptation:
```python
def x_callback(self, pin_x):
if (x := pin_x()) != self._x:
self._x = x
self._v += 1 if x ^ self._pin_y() else -1
```
If an interrupt occurs and no change has taken place since the previous one, it
is ignored on the basis that a second edge must have occurred during the
latency period. That second edge will trigger another interrupt which will be
ignored for the same reason.
While this works remarkably well with a mechanical encoder connected directly,
it cannot be expected to handle multiple transitions during the latency period.
If bit-perfect results are required, hardware rate limiting must be applied.
## 7.4 Encoders with mechanical detents
@ -271,15 +267,13 @@ producing one complete cycle of the state transition diagram above. If it is
required to track these exactly, for example triggering a callback on each
"click", the
[asynchronous driver](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/DRIVERS.md#6-quadrature-encoders)
with a division ratio of 4. Rate limiting is essential. Testing with a
mechanical encoder with Schmitt trigger preconditioning (see below) produced
good results with tracking maintained exactly. Some encoders, described as
"half step", have two detents per revolution. These can be handled by setting
`div=2` on this driver.
with a division ratio of 4. Some encoders, described as "half step", have two
detents per revolution. These can be handled by setting `div=2` on this driver.
It is almost certainly impossible to provide exact tracking on platforms which
support only soft IRQ's because garbage collection results in interrupt latency
which exceeds the time between edges from the encoder.
which exceeds the time between edges from the encoder. On platforms with SPIRAM
GC can take hundreds of ms.
# 8. Preconditioning and rate limiting

Wyświetl plik

@ -3,7 +3,7 @@
# Encoder Support: this version should be portable between MicroPython platforms
# Thanks to Evan Widloski for the adaptation to use the machine module
# Copyright (c) 2017-2021 Peter Hinch
# Copyright (c) 2017-2022 Peter Hinch
# Released under the MIT License (MIT) - see LICENSE file
from machine import Pin
@ -14,6 +14,8 @@ class Encoder:
self.forward = True
self.pin_x = pin_x
self.pin_y = pin_y
self._x = pin_x()
self._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)
@ -22,17 +24,21 @@ class Encoder:
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 x_callback(self, pin_x):
if (x := pin_x()) != self._x: # Reject short pulses
self._x = x
self.forward = x ^ 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 y_callback(self, pin_y):
if (y := pin_y()) != self._y:
self._y = y
self.forward = y ^ self.pin_x() ^ 1
self._pos += 1 if self.forward else -1
def position(self, value=None):
if value is not None:
self._pos = round(value / self.scale) # # Improvement provided by @IhorNehrutsa
self._pos = round(value / self.scale) # Improvement provided by @IhorNehrutsa
return self._pos * self.scale
def value(self, value=None):