2022-04-06 16:45:21 +00:00
|
|
|
# 1. Incremental encoders
|
2021-07-19 07:43:25 +00:00
|
|
|
|
2022-04-14 09:29:43 +00:00
|
|
|
These simple devices conceal a number of subtleties and have been the subject
|
|
|
|
of lengthy debate in the MicroPython forum. This doc aims to resolve the issues
|
|
|
|
and to offer tested solutions.
|
|
|
|
|
2021-07-19 07:43:25 +00:00
|
|
|
There are three technologies that I am aware of:
|
|
|
|
1. Optical.
|
|
|
|
2. Magnetic.
|
|
|
|
3. Mechanical (using switch contacts).
|
|
|
|
|
2021-07-19 07:51:07 +00:00
|
|
|
All produce quadrature signals looking like this:
|
2021-07-19 07:43:25 +00:00
|
|
|

|
|
|
|
consequently the same code may be used regardless of encoder type.
|
|
|
|
|
2021-07-19 07:51:53 +00:00
|
|
|
They have two primary applications:
|
|
|
|
1. Shaft position and speed measurements on machines.
|
|
|
|
2. Control knobs for user input. For user input a mechanical device, being
|
|
|
|
inexpensive, usually suffices. See [this Adafruit product](https://www.adafruit.com/product/377).
|
2021-07-19 07:43:25 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
# 2. Basic encoder script
|
2021-07-19 07:43:25 +00:00
|
|
|
|
2022-04-19 12:14:19 +00:00
|
|
|
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.
|
2021-07-19 07:43:25 +00:00
|
|
|
```python
|
|
|
|
from machine import Pin
|
|
|
|
|
|
|
|
class Encoder:
|
|
|
|
def __init__(self, pin_x, pin_y, scale=1):
|
|
|
|
self.scale = scale
|
|
|
|
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)
|
|
|
|
|
2022-04-06 16:55:34 +00:00
|
|
|
def x_callback(self, pin_x):
|
|
|
|
forward = pin_x() ^ self.pin_y()
|
|
|
|
self._pos += 1 if forward else -1
|
2021-07-19 07:43:25 +00:00
|
|
|
|
2022-04-06 16:55:34 +00:00
|
|
|
def y_callback(self, pin_y):
|
|
|
|
forward = self.pin_x() ^ pin_y() ^ 1
|
|
|
|
self._pos += 1 if forward else -1
|
2021-07-19 07:43:25 +00:00
|
|
|
|
|
|
|
def position(self, value=None):
|
|
|
|
if value is not None:
|
2021-10-14 18:01:28 +00:00
|
|
|
self._pos = round(value / self.scale)
|
2021-07-19 07:43:25 +00:00
|
|
|
return self._pos * self.scale
|
|
|
|
```
|
2022-04-14 09:29:43 +00:00
|
|
|
If the direction is incorrect, transpose the X and Y pins in hardware or in the
|
|
|
|
constructor call.
|
2021-07-19 07:43:25 +00:00
|
|
|
|
2022-04-19 12:14:19 +00:00
|
|
|
Contrary to common opinion a state table is not necessary to produce a correct
|
|
|
|
algorithm: see [section 7](./ENCODERS.md#7-algorithm).
|
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
# 3. Problem 1: Interrupt latency
|
2021-07-19 07:43:25 +00:00
|
|
|
|
|
|
|
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).
|
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
# 4. Problem 2: Jitter
|
2021-07-19 07:43:25 +00:00
|
|
|
|
|
|
|
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
|
2022-03-30 14:29:58 +00:00
|
|
|
matching an edge. An arbitrarily long sequence of pulses on one line is the
|
2022-04-06 16:45:21 +00:00
|
|
|
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.
|
2022-03-30 14:29:58 +00:00
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
In practice the frequency of such edges may be arbitrarily high. This imposes
|
2022-04-19 12:14:19 +00:00
|
|
|
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.
|
2022-03-30 14:29:58 +00:00
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
## 4.1 Synchronisation
|
2022-03-30 14:29:58 +00:00
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
Decoders of all types (including hardware implementations) can fail if edges on
|
|
|
|
one line occur at too high a rate: transitions can be missed leading to a
|
|
|
|
gradual drift of measured count compared to actual position. With an encoder
|
|
|
|
that produces good logic levels the solution is to limit the rate by
|
|
|
|
pre-synchronising the digital signals to a clock. For bit-perfect results a
|
|
|
|
single level of clock synchronisation is inadequate because of metastability.
|
|
|
|
Typically two levels are used. See
|
2022-04-14 09:29:43 +00:00
|
|
|
[this Wikipedia article](https://en.wikipedia.org/wiki/Incremental_encoder#Clock_synchronization).
|
2022-03-30 14:29:58 +00:00
|
|
|
|
|
|
|
The clock rate of a synchroniser for a software decoder must be chosen with
|
|
|
|
regard to the worst-case latency of the host. The clock rate will then
|
|
|
|
determine the maximum permissible rotation speed of the encoder.
|
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
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
|
2022-04-19 12:14:19 +00:00
|
|
|
and a Schmitt trigger should be considered. That said, remarkably accurate
|
|
|
|
tracking can be achieved in code.
|
2022-03-30 14:29:58 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
meets these criteria.
|
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
# 5. Problem 3: Concurrency
|
2021-07-19 07:43:25 +00:00
|
|
|
|
|
|
|
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
|
2022-04-06 16:45:21 +00:00
|
|
|
concurrency issues because changes to an integer are atomic.
|
|
|
|
|
|
|
|
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 which is bad practice.
|
2021-07-19 07:43:25 +00:00
|
|
|
|
2022-04-14 09:29:43 +00:00
|
|
|
The use of `micropython.schedule` avoids the problem of excessive ISR blocking
|
|
|
|
but would not solve the concurrency issue described above.
|
|
|
|
|
2021-07-19 07:43:25 +00:00
|
|
|
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.
|
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
# 6. Code samples
|
2021-07-19 07:43:25 +00:00
|
|
|
|
|
|
|
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 :)
|
2022-01-01 15:43:20 +00:00
|
|
|
3. `encoder_rp2.py` Version specific to Raspberry Pico RP2 chip. This uses the
|
|
|
|
PIO and Viper code to achieve fast response - upto ~10K transitions/s.
|
2022-04-14 11:11:59 +00:00
|
|
|
4. [Asynchronous driver](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/DRIVERS.md#6-quadrature-encoders)
|
2022-04-14 09:29:43 +00:00
|
|
|
for `uasyncio` applications.
|
2021-07-19 07:43:25 +00:00
|
|
|
|
2022-04-14 11:11:59 +00:00
|
|
|
For mechanical encoders consider the need for pull up or pull down resistors.
|
|
|
|
|
|
|
|
Applications which just require the maintenance of a position count would
|
|
|
|
normally use `encoder_portable.py`. Where callbacks are required, or tracking
|
|
|
|
of detent positions is needed, the asynchronous driver is preferred for reasons
|
|
|
|
covered in this document.
|
2022-03-31 18:01:20 +00:00
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
# 7. Algorithm
|
2022-03-31 18:01:20 +00:00
|
|
|
|
|
|
|
Discussions on the MicroPython forum demonstrate a degree of confusion about
|
|
|
|
the merits of different decoding algorithms and about contact debouncing. These
|
|
|
|
notes aim to clarify the issues and to provide an explanation for the approach
|
2022-04-06 16:45:21 +00:00
|
|
|
used in my code samples; also to describe the mechanism where errors occur.
|
2022-03-31 18:01:20 +00:00
|
|
|
|
|
|
|
Incremental encoders produce two signals `x` and `y`. Possible state changes
|
|
|
|
are shown in this state diagram.
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
The truth table includes the previous (`xp` and `yp`) and current (`x` and `y`)
|
2022-04-06 16:55:34 +00:00
|
|
|
values of the signals. It includes all logically possible combinations of these
|
2022-03-31 18:01:20 +00:00
|
|
|
signals. These include cases where no change occurs (marked `n`) and cases
|
|
|
|
which are physically impossible (`x`). The latter arise because both signals
|
|
|
|
cannot change state simultaneously.
|
|
|
|
|
|
|
|
Decoding these four bits is a problem of combinatorial logic. All such problems
|
|
|
|
may be solved by using the bits as addresses of a lookup table. In this case
|
|
|
|
there would be two output bits signifying increment, decrement or do nothing.
|
|
|
|
However, simplifications are possible if changes of `x` and `y` trigger
|
2022-04-14 09:29:43 +00:00
|
|
|
interrupts (i.e. interrupt on `IRQ_RISING | IRQ_FALLING`).
|
2022-03-31 18:01:20 +00:00
|
|
|
|
|
|
|
The truth table may then be split into two, one catering for cases where `x`
|
|
|
|
has changed, and the other for `y` changes. The illegal cases are discarded and
|
|
|
|
the "no change" cases will not trigger an interrupt. All cases in these tables
|
|
|
|
cause a change in position and inspection shows that the direction is the
|
|
|
|
`exclusive or` of the current `x` and `y` values, with opposite polarity for
|
|
|
|
the `x` and `y` interrupts.
|
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
## 7.1 Debouncing
|
2022-03-31 18:01:20 +00:00
|
|
|
|
|
|
|
Contact bounce or vibration effects cause an oscillating signal on one line.
|
|
|
|
The state diagram shows that this is logically indistinguishable from a
|
|
|
|
physical movement of the encoder backwards and forwards across one transition
|
|
|
|
point. Consequently any valid decoding algorithm will register a change in
|
|
|
|
position of one LSB forwards and backwards. There is no systematic drift, just
|
|
|
|
one LSB of positional uncertainty.
|
|
|
|
|
2022-04-06 16:45:21 +00:00
|
|
|
## 7.2 Algorithm quality
|
2022-03-31 18:01:20 +00:00
|
|
|
|
|
|
|
All valid solutions to a combinatorial logic problem are equivalent. The only
|
|
|
|
ways in which one solution can be considered "better" than another are in
|
2022-04-14 09:29:43 +00:00
|
|
|
qualities such as performance and code size. Where decoders can differ in
|
2022-04-16 12:55:52 +00:00
|
|
|
quality is in their handling of interrupts. The approach used in the
|
|
|
|
[Asynchronous driver](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/DRIVERS.md#6-quadrature-encoders)
|
|
|
|
has the following characteristics:
|
|
|
|
1. Interrupt service routines are minimal.
|
|
|
|
2. Encoders with 2 or 4 pulses per detent are handled by selection of a single
|
|
|
|
integer.
|
|
|
|
3. The algorithm doesn't care about the polarity of the encoder signals.
|
2022-04-06 16:45:21 +00:00
|
|
|
|
|
|
|
## 7.3 Interrupt issues
|
|
|
|
|
|
|
|
As discussed above, any solution will have a limit to the rate at which edges
|
|
|
|
can be tracked. This section describes the limits of a MicroPython
|
|
|
|
interrupt-driven solution and the way in which incorrect counts can arise.
|
|
|
|
|
|
|
|
Interrupts suffer from latency: there is a time delay between an edge occurring
|
|
|
|
and the ISR executing. The magnitude depends on what is running at the moment
|
|
|
|
the edge occurs and consequently varies in real time. Another ISR may be
|
|
|
|
running. Higher priority interrupts may be pending service. In the case of soft
|
|
|
|
IRQ's garbage collection may be in progress.
|
|
|
|
|
2022-04-06 16:55:34 +00:00
|
|
|
Consider the following ISR:
|
2022-04-06 16:45:21 +00:00
|
|
|
```python
|
|
|
|
def x_callback(self, pin_x):
|
|
|
|
forward = pin_x() ^ self.pin_y()
|
|
|
|
self._pos += 1 if forward else -1
|
|
|
|
```
|
|
|
|
This is necessarily triggered by either edge on `pin_x`. While `.pin_y` should
|
|
|
|
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.
|
|
|
|
|
2022-04-19 12:14:19 +00:00
|
|
|
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.
|
2022-04-06 16:45:21 +00:00
|
|
|
|
2022-04-19 12:14:19 +00:00
|
|
|
While this works remarkably well with a mechanical encoder connected directly,
|
|
|
|
it cannot be expected to handle multiple transitions during the latency period.
|
2022-04-06 16:45:21 +00:00
|
|
|
If bit-perfect results are required, hardware rate limiting must be applied.
|
2022-04-14 09:29:43 +00:00
|
|
|
|
|
|
|
## 7.4 Encoders with mechanical detents
|
|
|
|
|
|
|
|
Encoders intended for user controls often have detents with each "click"
|
|
|
|
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)
|
2022-04-19 12:14:19 +00:00
|
|
|
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.
|
2022-04-14 09:29:43 +00:00
|
|
|
|
|
|
|
It is almost certainly impossible to provide exact tracking on platforms which
|
|
|
|
support only soft IRQ's because garbage collection results in interrupt latency
|
2022-04-19 12:14:19 +00:00
|
|
|
which exceeds the time between edges from the encoder. On platforms with SPIRAM
|
|
|
|
GC can take hundreds of ms.
|
2022-04-14 09:29:43 +00:00
|
|
|
|
|
|
|
# 8. Preconditioning and rate limiting
|
|
|
|
|
|
|
|
## 8.1 Mechanical encoders
|
|
|
|
|
|
|
|
The task here is to ensure valid logic levels in addition to limiting the pulse
|
|
|
|
rate. The solution is to use a single pole low pass filter to limit the rate,
|
|
|
|
followed by a Schmitt trigger to guarantee logic levels. In practice a time
|
|
|
|
constant of 1-1.5ms is sufficient for platforms with hard IRQ's.
|
|
|
|
|
|
|
|
Typical circuit. The asymmetry in the timing resistors approximately matches
|
|
|
|
the offsets of the chip's threshold voltages.
|
|
|
|

|
|
|
|
|
|
|
|
Alternative using a dual CMOS op amp.
|
|
|
|

|
|
|
|
|
|
|
|
## 8.2 Optical encoders
|
|
|
|
|
|
|
|
These can use the above circuits, but as they produce good logic levels an
|
|
|
|
alternative is to use two d-type flip-flops on each channel, clocked using a
|
|
|
|
signal from the host. Typically this might be produced by a PWM channel running
|
|
|
|
continuously. Clock rate depends on the expected worst-case interrupt latency.
|
2022-04-14 09:43:56 +00:00
|
|
|
|
2022-04-16 13:07:50 +00:00
|
|
|
This Wikipedia image illustrates the idea, along with the metastability problem
|
|
|
|
discussed in [the article](https://en.wikipedia.org/wiki/Incremental_encoder).
|
2022-04-14 09:43:56 +00:00
|
|
|
|
2022-04-16 13:07:50 +00:00
|
|
|

|
|
|
|
By Lambtron - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=86059204
|