|
| 1 | +#pragma once |
| 2 | + |
| 3 | +#include "util/atomic.h" |
| 4 | +#include "Arduino.h" |
| 5 | +#include "Hardware.h" |
| 6 | + |
| 7 | +/** |
| 8 | + * This class handles events from a rotary encoder with a combined |
| 9 | + * button, assuming external pullups are present on all pins. |
| 10 | + * |
| 11 | + * The encoder used has both outputs high when idle (at a detent), and |
| 12 | + * switches through a full cycle (both outputs go low and high again) |
| 13 | + * for each detent moved. |
| 14 | + * |
| 15 | + * When moving clock-wise, output A goes low before output B, and when |
| 16 | + * moving counter-clockwise, output B goes low before output A (and they |
| 17 | + * go high again in the same order). |
| 18 | + * |
| 19 | + * The moment you feel the knob "click" into the next detent matches the |
| 20 | + * moment where both outputs are high again. This is the moment where |
| 21 | + * events should be triggered. |
| 22 | + * |
| 23 | + * Clockwise: Counterclockwise: |
| 24 | + * __. ._____ ____. ._____ |
| 25 | + * A |___| A |___| |
| 26 | + * ____. .___ __. .___ |
| 27 | + * B |___| B |___| |
| 28 | + * ^ ^ |
| 29 | + * Event here Event here |
| 30 | + * |
| 31 | + * To accurately catch these events, this class registers interrupt |
| 32 | + * handlers on both pins. It seems sufficient to only trigger on rising |
| 33 | + * edges to catch the above events, but then spurious events will be |
| 34 | + * triggered when the encoder is turned slightly past a detent and back |
| 35 | + * again (the forward movement will be missed, while the movement back |
| 36 | + * will trigger a spurious event). |
| 37 | + * |
| 38 | + * Hence, this class monitors both edges on both pins, and keeps track |
| 39 | + * of the exact value of the encoder (where each edge adds or subtracts |
| 40 | + * one to the value kept). Whenever the value is divisible by 4, the |
| 41 | + * encoder is at a detent. |
| 42 | + * |
| 43 | + * For tracking the encoder value, a lookup table approach is used. This |
| 44 | + * is based on the approach described (among other places) here: |
| 45 | + * https://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino |
| 46 | + * |
| 47 | + * This uses a lookup table that maps a combination of the previous pin |
| 48 | + * states and the current pin states to the change in value that this |
| 49 | + * should result in. However, the lookup table used here is slightly |
| 50 | + * modified from the one commonly used. |
| 51 | + * |
| 52 | + * The original table contains 1 to indicate clockwise motion, -1 to |
| 53 | + * indicate counter-clockwise motion, 0 to indicate no motion (pin |
| 54 | + * values unchanged ) and also 0 to indicate an invalid transition (e.g. |
| 55 | + * both pins changed, which should never happen in quadrature encoding). |
| 56 | + * |
| 57 | + * The table used here, uses 2 and -2 for the invalid transitions |
| 58 | + * instead of 0. Even though these should never occur, it is not |
| 59 | + * unthinkable that bouncing or noise will very rarely cause them to |
| 60 | + * happen anyway. With the original table, these transitions did not |
| 61 | + * modify the value, but did modify the "previous pin state" that was |
| 62 | + * stored. Effectively, this could cause the tracked value to get an |
| 63 | + * off-by-two error relative to the actual position. Effectively, these |
| 64 | + * invalid transitions mean the encoder position has changed by 2 |
| 65 | + * instead of 1, though there is no way to tell which way the encoder |
| 66 | + * has turned. |
| 67 | + * |
| 68 | + * For this reason, the lookup table contains 2 (and -2, to allow any |
| 69 | + * errors to possibly even out) for these invalid transitions. This will |
| 70 | + * keep the value accurate in some cases, but since we're just making a |
| 71 | + * guess as to the direction, it will by off-by-four in other cases. |
| 72 | + * Even though not perfectly accurate, now at least the assumption that |
| 73 | + * the tracked value is a multiple of four will remain valid, regardless |
| 74 | + * of what values are read. |
| 75 | + * |
| 76 | + * This approach means that the reading is pretty resilient against |
| 77 | + * invalid readings. When there is an invalid reading on a single pin, |
| 78 | + * the value might be temporary off-by-one, but this will be corrected |
| 79 | + * when the reading is correct again. Since normally, only one pin |
| 80 | + * should change at a time, this means that any bouncing of the |
| 81 | + * encoder's contacts should be handled without any problems by this |
| 82 | + * code. |
| 83 | + * |
| 84 | + * When both pins return an invalid reading, the absolute position might |
| 85 | + * get off-by-four errors, but the position within a single cycle should |
| 86 | + * remain correct, meaning detents are still detected correctly. |
| 87 | + */ |
| 88 | + |
| 89 | +void assert_interrupt_pin(); |
| 90 | +void clear_interrupt_pin(); |
| 91 | + |
| 92 | +template <uint8_t button_pin, uint8_t pina, uint8_t pinb> |
| 93 | +class ButtonEncoder { |
| 94 | + public: |
| 95 | + void setup(void) { |
| 96 | + // These have external pullups |
| 97 | + pinMode(button_pin, INPUT_PULLUP /* TEMPORARY with PULLUP! */); |
| 98 | + pinMode(pina, INPUT); |
| 99 | + pinMode(pinb, INPUT); |
| 100 | + |
| 101 | + // Set Pin Change Interrupts Masks that are used for the encoder |
| 102 | + PCMSK0 = _BV(PCINT5); |
| 103 | + PCMSK1 = _BV(PCINT10); |
| 104 | + |
| 105 | + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { |
| 106 | + // Be sure that every interrupt has been set, including all the pin change interrupts |
| 107 | + GIMSK |= (_BV(PCIE1) | _BV(PCIE0)); |
| 108 | + |
| 109 | + attachInterrupt(digitalPinToInterrupt(pinb), encoder_isr, CHANGE); |
| 110 | + |
| 111 | + // Reset any queued events. attachInterrupt does not clear any |
| 112 | + // previously pending interrupts (e.g. due to startup noise, or |
| 113 | + // due to different interrupt settings), so these might trigger |
| 114 | + // right away. Clear away the result of that. See also |
| 115 | + // https://github.com/arduino/Arduino/issues/510 |
| 116 | + GIFR |= (_BV(INTF0) | _BV(PCIF1) | _BV(PCIF0)); |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + int8_t process_encoder(void) { |
| 121 | + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { |
| 122 | + int8_t ret = encoder_detents; |
| 123 | + encoder_detents = 0; |
| 124 | + return ret; |
| 125 | + } |
| 126 | + return 0; // Not reached, but keeps compiler happy |
| 127 | + } |
| 128 | + |
| 129 | + uint8_t process_button() { |
| 130 | + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { |
| 131 | + uint8_t ret = button_presses; |
| 132 | + button_presses = 0; |
| 133 | + return ret; |
| 134 | + } |
| 135 | + return 0; // Not reached, but keeps compiler happy |
| 136 | + } |
| 137 | + |
| 138 | + // Interrupt routine functions |
| 139 | + static void button_isr(void) __attribute__((__always_inline__)); |
| 140 | + static void encoder_isr(void) __attribute__((__always_inline__)); |
| 141 | + |
| 142 | + static volatile uint8_t button_presses; |
| 143 | + static volatile int8_t encoder_detents; |
| 144 | +}; |
| 145 | + |
| 146 | +template <uint8_t button_pin, uint8_t pina, uint8_t pinb> |
| 147 | +inline void ButtonEncoder<button_pin, pina, pinb>::button_isr(void) { |
| 148 | + static bool previous_state = true; |
| 149 | + |
| 150 | + // No need to debounce, since hardware filtering is used |
| 151 | + bool button_state = digitalRead(button_pin); |
| 152 | + |
| 153 | + // Falling edge detection |
| 154 | + if(previous_state && !button_state) { |
| 155 | + button_presses++; |
| 156 | + assert_interrupt_pin(); |
| 157 | + } |
| 158 | + previous_state = button_state; |
| 159 | +} |
| 160 | + |
| 161 | +template <uint8_t button_pin, uint8_t pina, uint8_t pinb> |
| 162 | +inline void ButtonEncoder<button_pin, pina, pinb>::encoder_isr(void) { |
| 163 | + // Initialize to 3, assuming the encoder starts at a detent. This |
| 164 | + // matches the initial value of `value`, so even if it is |
| 165 | + // incorrect, it should resynchronise on the first interrupt. |
| 166 | + static uint8_t prev_reading = 3; |
| 167 | + static int8_t encoder_value = 0; |
| 168 | + |
| 169 | + // This lookup table maps clockwise rotation to positive changes. |
| 170 | + static const int8_t table[] PROGMEM = { 0, -1, 1, 2, 1, 0, 2, -1, -1, -2, 0, 1, -2, 1, -1, 0 }; |
| 171 | + uint8_t reading = digitalRead(pina) << 1 | digitalRead(pinb); |
| 172 | + uint8_t key = prev_reading << 2 | reading; |
| 173 | + encoder_value += pgm_read_byte(&table[key]); |
| 174 | + prev_reading = reading; |
| 175 | + |
| 176 | + // Generate an event whenever the encoder has moved a full |
| 177 | + // detent (value changed by 4). |
| 178 | + if (encoder_value >= 4) { |
| 179 | + ++encoder_detents; |
| 180 | + encoder_value -= 4; |
| 181 | + assert_interrupt_pin(); |
| 182 | + } else if (encoder_value <= -4) { |
| 183 | + --encoder_detents; |
| 184 | + encoder_value += 4; |
| 185 | + assert_interrupt_pin(); |
| 186 | + } |
| 187 | +} |
0 commit comments