From 72ca2425dc9171fc9400472ef60de5fb0430871f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 19 Jul 2021 08:43:25 +0100 Subject: [PATCH] Update encoders docs and samples. --- README.md | 33 ++------- encoders/ENCODERS.md | 131 +++++++++++++++++++++++++++++++++++ encoders/encoder.py | 7 +- encoders/encoder_portable.py | 38 +++++----- encoders/encoder_timed.py | 52 +++++++------- encoders/quadrature.jpg | Bin 0 -> 13536 bytes 6 files changed, 187 insertions(+), 74 deletions(-) create mode 100644 encoders/ENCODERS.md create mode 100644 encoders/quadrature.jpg diff --git a/README.md b/README.md index 624e245..7bad160 100644 --- a/README.md +++ b/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 diff --git a/encoders/ENCODERS.md b/encoders/ENCODERS.md new file mode 100644 index 0000000..aad15bf --- /dev/null +++ b/encoders/ENCODERS.md @@ -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. diff --git a/encoders/encoder.py b/encoders/encoder.py index 8bca507..0843dd7 100644 --- a/encoders/encoder.py +++ b/encoders/encoder.py @@ -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 diff --git a/encoders/encoder_portable.py b/encoders/encoder_portable.py index 87202d9..525ca42 100644 --- a/encoders/encoder_portable.py +++ b/encoders/encoder_portable.py @@ -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 - diff --git a/encoders/encoder_timed.py b/encoders/encoder_timed.py index 77dfa78..8095ae3 100644 --- a/encoders/encoder_timed.py +++ b/encoders/encoder_timed.py @@ -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 diff --git a/encoders/quadrature.jpg b/encoders/quadrature.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fbe831e316f20cec4210d451362e003e24a5db16 GIT binary patch literal 13536 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 literal 0 HcmV?d00001