├── .gitignore ├── ClickEncoder.cpp ├── ClickEncoder.h ├── LICENSING.txt ├── README.md ├── examples └── ClickEncoderTest │ └── ClickEncoderTest.ino └── library.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | 6 | # Compiled Dynamic libraries 7 | *.so 8 | *.dylib 9 | 10 | # Compiled Static libraries 11 | *.lai 12 | *.la 13 | *.a 14 | -------------------------------------------------------------------------------- /ClickEncoder.cpp: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Rotary Encoder Driver with Acceleration 3 | // Supports Click, DoubleClick, Long Click 4 | // 5 | // (c) 2010 karl@pitrich.com 6 | // (c) 2014 karl@pitrich.com 7 | // 8 | // Timer-based rotary encoder logic by Peter Dannegger 9 | // http://www.mikrocontroller.net/articles/Drehgeber 10 | // ---------------------------------------------------------------------------- 11 | 12 | #include "ClickEncoder.h" 13 | 14 | // ---------------------------------------------------------------------------- 15 | // Button configuration (values for 1ms timer service calls) 16 | // 17 | #define ENC_BUTTONINTERVAL 10 // check button every x milliseconds, also debouce time 18 | #define ENC_DOUBLECLICKTIME 600 // second click within 600ms 19 | #define ENC_HOLDTIME 1200 // report held button after 1.2s 20 | 21 | // ---------------------------------------------------------------------------- 22 | // Acceleration configuration (for 1000Hz calls to ::service()) 23 | // 24 | #define ENC_ACCEL_TOP 3072 // max. acceleration: *12 (val >> 8) 25 | #define ENC_ACCEL_INC 25 26 | #define ENC_ACCEL_DEC 2 27 | 28 | // ---------------------------------------------------------------------------- 29 | 30 | #if ENC_DECODER != ENC_NORMAL 31 | # ifdef ENC_HALFSTEP 32 | // decoding table for hardware with flaky notch (half resolution) 33 | const int8_t ClickEncoder::table[16] __attribute__((__progmem__)) = { 34 | 0, 0, -1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1, 0, 0 35 | }; 36 | # else 37 | // decoding table for normal hardware 38 | const int8_t ClickEncoder::table[16] __attribute__((__progmem__)) = { 39 | 0, 1, -1, 0, -1, 0, 0, 1, 1, 0, 0, -1, 0, -1, 1, 0 40 | }; 41 | # endif 42 | #endif 43 | 44 | // ---------------------------------------------------------------------------- 45 | 46 | ClickEncoder::ClickEncoder(uint8_t A, uint8_t B, uint8_t BTN, uint8_t stepsPerNotch, bool coderActive, bool btnActive) 47 | : doubleClickEnabled(true), accelerationEnabled(true), 48 | delta(0), last(0), acceleration(0), 49 | button(Open), steps(stepsPerNotch), 50 | pinA(A), pinB(B), pinBTN(BTN), coderPinsActive(coderActive), btnPinActive(btnActive) 51 | { 52 | uint8_t coderType = (coderPinsActive == LOW) ? INPUT_PULLUP : INPUT; 53 | pinMode(pinA, coderType); 54 | pinMode(pinB, coderType); 55 | uint8_t btnType = (btnPinActive == LOW) ? INPUT_PULLUP : INPUT; 56 | pinMode(pinBTN, btnType); 57 | 58 | if (digitalRead(pinA) == coderPinsActive) { 59 | last = 3; 60 | } 61 | 62 | if (digitalRead(pinB) == coderPinsActive) { 63 | last ^=1; 64 | } 65 | } 66 | 67 | // ---------------------------------------------------------------------------- 68 | // call this every 1 millisecond via timer ISR 69 | // 70 | void ClickEncoder::service(void) 71 | { 72 | bool moved = false; 73 | unsigned long now = millis(); 74 | 75 | if (accelerationEnabled) { // decelerate every tick 76 | acceleration -= ENC_ACCEL_DEC; 77 | if (acceleration & 0x8000) { // handle overflow of MSB is set 78 | acceleration = 0; 79 | } 80 | } 81 | 82 | #if ENC_DECODER == ENC_FLAKY 83 | last = (last << 2) & 0x0F; 84 | 85 | if (digitalRead(pinA) == coderPinsActive) { 86 | last |= 2; 87 | } 88 | 89 | if (digitalRead(pinB) == coderPinsActive) { 90 | last |= 1; 91 | } 92 | 93 | uint8_t tbl = pgm_read_byte(&table[last]); 94 | if (tbl) { 95 | delta += tbl; 96 | moved = true; 97 | } 98 | #elif ENC_DECODER == ENC_NORMAL 99 | int8_t curr = 0; 100 | 101 | if (digitalRead(pinA) == coderPinsActive) { 102 | curr = 3; 103 | } 104 | 105 | if (digitalRead(pinB) == coderPinsActive) { 106 | curr ^= 1; 107 | } 108 | 109 | int8_t diff = last - curr; 110 | 111 | if (diff & 1) { // bit 0 = step 112 | last = curr; 113 | delta += (diff & 2) - 1; // bit 1 = direction (+/-) 114 | moved = true; 115 | } 116 | #else 117 | # error "Error: define ENC_DECODER to ENC_NORMAL or ENC_FLAKY" 118 | #endif 119 | 120 | if (accelerationEnabled && moved) { 121 | // increment accelerator if encoder has been moved 122 | if (acceleration <= (ENC_ACCEL_TOP - ENC_ACCEL_INC)) { 123 | acceleration += ENC_ACCEL_INC; 124 | } 125 | } 126 | 127 | // handle button 128 | // 129 | #ifndef WITHOUT_BUTTON 130 | if (pinBTN > -1 // check button only, if a pin has been provided 131 | && (now - lastButtonCheck) >= ENC_BUTTONINTERVAL) // checking button is sufficient every 10-30ms 132 | { 133 | lastButtonCheck = now; 134 | 135 | if (digitalRead(pinBTN) == btnPinActive) { // key is down 136 | keyDownTicks++; 137 | if (keyDownTicks > (ENC_HOLDTIME / ENC_BUTTONINTERVAL)) { 138 | button = Held; 139 | } 140 | } 141 | 142 | if (digitalRead(pinBTN) == !btnPinActive) { // key is now up 143 | if (keyDownTicks /*> ENC_BUTTONINTERVAL*/) { 144 | if (button == Held) { 145 | button = Released; 146 | doubleClickTicks = 0; 147 | } 148 | else { 149 | #define ENC_SINGLECLICKONLY 1 150 | if (doubleClickTicks > ENC_SINGLECLICKONLY) { // prevent trigger in single click mode 151 | if (doubleClickTicks < (ENC_DOUBLECLICKTIME / ENC_BUTTONINTERVAL)) { 152 | button = DoubleClicked; 153 | doubleClickTicks = 0; 154 | } 155 | } 156 | else { 157 | doubleClickTicks = (doubleClickEnabled) ? (ENC_DOUBLECLICKTIME / ENC_BUTTONINTERVAL) : ENC_SINGLECLICKONLY; 158 | } 159 | } 160 | } 161 | 162 | keyDownTicks = 0; 163 | } 164 | 165 | if (doubleClickTicks > 0) { 166 | doubleClickTicks--; 167 | if (--doubleClickTicks == 0) { 168 | button = Clicked; 169 | } 170 | } 171 | } 172 | #endif // WITHOUT_BUTTON 173 | 174 | } 175 | 176 | // ---------------------------------------------------------------------------- 177 | 178 | int16_t ClickEncoder::getValue(void) 179 | { 180 | int16_t val; 181 | 182 | cli(); 183 | val = delta; 184 | 185 | if (steps == 2) delta = val & 1; 186 | else if (steps == 4) delta = val & 3; 187 | else delta = 0; // default to 1 step per notch 188 | 189 | sei(); 190 | 191 | if (steps == 4) val >>= 2; 192 | if (steps == 2) val >>= 1; 193 | 194 | int16_t r = 0; 195 | int16_t accel = ((accelerationEnabled) ? (acceleration >> 8) : 0); 196 | 197 | if (val < 0) { 198 | r -= 1 + accel; 199 | } 200 | else if (val > 0) { 201 | r += 1 + accel; 202 | } 203 | 204 | return r; 205 | } 206 | 207 | // ---------------------------------------------------------------------------- 208 | 209 | #ifndef WITHOUT_BUTTON 210 | ClickEncoder::Button ClickEncoder::getButton(void) 211 | { 212 | ClickEncoder::Button ret = button; 213 | if (button != ClickEncoder::Held) { 214 | button = ClickEncoder::Open; // reset 215 | } 216 | return ret; 217 | } 218 | #endif 219 | -------------------------------------------------------------------------------- /ClickEncoder.h: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Rotary Encoder Driver with Acceleration 3 | // Supports Click, DoubleClick, Long Click 4 | // 5 | // (c) 2010 karl@pitrich.com 6 | // (c) 2014 karl@pitrich.com 7 | // 8 | // Timer-based rotary encoder logic by Peter Dannegger 9 | // http://www.mikrocontroller.net/articles/Drehgeber 10 | // ---------------------------------------------------------------------------- 11 | 12 | #ifndef __have__ClickEncoder_h__ 13 | #define __have__ClickEncoder_h__ 14 | 15 | // ---------------------------------------------------------------------------- 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include "Arduino.h" 22 | 23 | // ---------------------------------------------------------------------------- 24 | 25 | #define ENC_NORMAL (1 << 1) // use Peter Danneger's decoder 26 | #define ENC_FLAKY (1 << 2) // use Table-based decoder 27 | 28 | // ---------------------------------------------------------------------------- 29 | 30 | #ifndef ENC_DECODER 31 | # define ENC_DECODER ENC_NORMAL 32 | #endif 33 | 34 | #if ENC_DECODER == ENC_FLAKY 35 | # ifndef ENC_HALFSTEP 36 | # define ENC_HALFSTEP 1 // use table for half step per default 37 | # endif 38 | #endif 39 | 40 | // ---------------------------------------------------------------------------- 41 | 42 | class ClickEncoder 43 | { 44 | public: 45 | typedef enum Button_e { 46 | Open = 0, 47 | Closed, 48 | 49 | Pressed, 50 | Held, 51 | Released, 52 | 53 | Clicked, 54 | DoubleClicked 55 | 56 | } Button; 57 | 58 | public: 59 | ClickEncoder(uint8_t A, uint8_t B, uint8_t BTN = -1, 60 | uint8_t stepsPerNotch = 1, bool coderActive = LOW, bool btnActive = LOW); 61 | 62 | void service(void); 63 | int16_t getValue(void); 64 | 65 | #ifndef WITHOUT_BUTTON 66 | public: 67 | Button getButton(void); 68 | #endif 69 | 70 | #ifndef WITHOUT_BUTTON 71 | public: 72 | void setDoubleClickEnabled(const bool &d) 73 | { 74 | doubleClickEnabled = d; 75 | } 76 | 77 | const bool getDoubleClickEnabled() 78 | { 79 | return doubleClickEnabled; 80 | } 81 | #endif 82 | 83 | public: 84 | void setAccelerationEnabled(const bool &a) 85 | { 86 | accelerationEnabled = a; 87 | if (accelerationEnabled == false) { 88 | acceleration = 0; 89 | } 90 | } 91 | 92 | const bool getAccelerationEnabled() 93 | { 94 | return accelerationEnabled; 95 | } 96 | 97 | private: 98 | const uint8_t pinA; 99 | const uint8_t pinB; 100 | const uint8_t pinBTN; 101 | const bool coderPinsActive; 102 | const bool btnPinActive; 103 | volatile int16_t delta; 104 | volatile int16_t last; 105 | uint8_t steps; 106 | volatile uint16_t acceleration; 107 | bool accelerationEnabled; 108 | #if ENC_DECODER != ENC_NORMAL 109 | static const int8_t table[16]; 110 | #endif 111 | #ifndef WITHOUT_BUTTON 112 | volatile Button button; 113 | bool doubleClickEnabled; 114 | uint16_t keyDownTicks = 0; 115 | uint8_t doubleClickTicks = 0; 116 | unsigned long lastButtonCheck = 0; 117 | #endif 118 | }; 119 | 120 | // ---------------------------------------------------------------------------- 121 | 122 | #endif // __have__ClickEncoder_h__ 123 | -------------------------------------------------------------------------------- /LICENSING.txt: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) 2010-2014, karl@pitrich.com 4 | // Copyright by authors stated in the source code files 5 | // All rights reserved. 6 | // 7 | // Redistribution and use in source and binary forms, with or without 8 | // modification, are permitted provided that the following conditions 9 | // are met: 10 | // 11 | // 1. Redistributions of source code must retain the above copyright 12 | // notice, this list of conditions and the following disclaimer. 13 | // 2. Redistributions in binary form must reproduce the above copyright 14 | // notice, this list of conditions and the following disclaimer in the 15 | // documentation and/or other materials provided with the distribution. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 18 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20 | // IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 22 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | // ----------------------------------------------------------------------------- 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ClickEncoder 2 | ============= 3 | 4 | > Arduino library to handle rotary encoders **with** buttons as a user input device. 5 | Arduino RotaryEncoder with Button Implementation. 6 | 7 | 8 | - Timer-Based: Works on any IO-Pin. 9 | - Supports rotary acceleration, so when the encoder is rotated faster, the encoders value will increment faster 10 | - Button reports multiple states: `Clicked`, `DoubleClicked`, `Held` and `Released` 11 | 12 | Encoder and button can be connected to any input pin, as this library requires it's timer interrupt service routine ClickEncoder:service() to be called every millisecond. The example uses [TimerOne] for that. 13 | 14 | See the example application [ClickEncoderTest] for details, 15 | or see it in action at my modified [reflow oven controller] 16 | 17 | ### Encoder 18 | The library supports **acceleration**, so when the encoder is rotated faster, the encoders value will increment faster. 19 | 20 | Acceleration can be enabled or disabled at runtine using `setAccelerationEnabled(bool)`. 21 | 22 | For instance, it makes sense to disable acceleration when entering a configuration menu that will be navigated using the encoder. 23 | 24 | **Please note** that the acceleration parameters have been tuned for **1ms timer** intervals, and need to be changed if you decide to call the service method in another interval. (You'd need to increase ENC_ACCEL_INC and ENC_ACCEL_INC). 25 | 26 | Depending on the type of your encoder, you can define use the constructors parameter `stepsPerNotch` an set it to either `1`, `2` or `4` steps per notch, with `1` being the default. 27 | 28 | If you have trouble with certain encoders, try 29 | 30 | #define ENC_DECODER (1 << 2) 31 | 32 | to use a table-based decoder, which can then be tuned using 33 | 34 | #define ENC_HALFSTEP 35 | 36 | The default is ENC_HALFSTEP 1. 37 | 38 | ### Button 39 | The Button reports multiple states: `Clicked`, `DoubleClicked`, `Held` and `Released`. You can fine-tune the timings in the library's header file. 40 | 41 | If your encoder does not have a button, and you need to save program memory, use `#define WITHOUT_BUTTON 1` 42 | prior including `ClickEncoder.h`, and ignore the third parameter `BTN` of the constructor. 43 | 44 | 45 | [TimerOne]:http://playground.arduino.cc/Code/Timer1 46 | [Branch arduino]:https://github.com/0xPIT/encoder/tree/arduino 47 | [ClickEncoderTest]:https://github.com/0xPIT/encoder/blob/arduino/examples/ClickEncoderTest/ClickEncoderTest.ino 48 | [reflow oven controller]:https://github.com/0xPIT/reflowOvenController 49 | 50 | -------------------------------------------------------------------------------- /examples/ClickEncoderTest/ClickEncoderTest.ino: -------------------------------------------------------------------------------- 1 | #define WITH_LCD 1 2 | 3 | #include 4 | #include 5 | 6 | #ifdef WITH_LCD 7 | #include 8 | 9 | #define LCD_RS 8 10 | #define LCD_RW 9 11 | #define LCD_EN 10 12 | #define LCD_D4 4 13 | #define LCD_D5 5 14 | #define LCD_D6 6 15 | #define LCD_D7 7 16 | 17 | #define LCD_CHARS 20 18 | #define LCD_LINES 4 19 | 20 | LiquidCrystal lcd(LCD_RS, LCD_RW, LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7); 21 | #endif 22 | 23 | ClickEncoder *encoder; 24 | int16_t last, value; 25 | 26 | void timerIsr() { 27 | encoder->service(); 28 | } 29 | 30 | #ifdef WITH_LCD 31 | void displayAccelerationStatus() { 32 | lcd.setCursor(0, 1); 33 | lcd.print("Acceleration "); 34 | lcd.print(encoder->getAccelerationEnabled() ? "on " : "off"); 35 | } 36 | #endif 37 | 38 | void setup() { 39 | Serial.begin(9600); 40 | encoder = new ClickEncoder(A1, A0, A2); 41 | 42 | #ifdef WITH_LCD 43 | lcd.begin(LCD_CHARS, LCD_LINES); 44 | lcd.clear(); 45 | displayAccelerationStatus(); 46 | #endif 47 | 48 | Timer1.initialize(1000); 49 | Timer1.attachInterrupt(timerIsr); 50 | 51 | last = -1; 52 | } 53 | 54 | void loop() { 55 | value += encoder->getValue(); 56 | 57 | if (value != last) { 58 | last = value; 59 | Serial.print("Encoder Value: "); 60 | Serial.println(value); 61 | #ifdef WITH_LCD 62 | lcd.setCursor(0, 0); 63 | lcd.print(" "); 64 | lcd.setCursor(0, 0); 65 | lcd.print(value); 66 | #endif 67 | } 68 | 69 | ClickEncoder::Button b = encoder->getButton(); 70 | if (b != ClickEncoder::Open) { 71 | Serial.print("Button: "); 72 | #define VERBOSECASE(label) case label: Serial.println(#label); break; 73 | switch (b) { 74 | VERBOSECASE(ClickEncoder::Pressed); 75 | VERBOSECASE(ClickEncoder::Held) 76 | VERBOSECASE(ClickEncoder::Released) 77 | VERBOSECASE(ClickEncoder::Clicked) 78 | case ClickEncoder::DoubleClicked: 79 | Serial.println("ClickEncoder::DoubleClicked"); 80 | encoder->setAccelerationEnabled(!encoder->getAccelerationEnabled()); 81 | Serial.print(" Acceleration is "); 82 | Serial.println((encoder->getAccelerationEnabled()) ? "enabled" : "disabled"); 83 | #ifdef WITH_LCD 84 | displayAccelerationStatus(); 85 | #endif 86 | break; 87 | } 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ClickEncoder", 3 | "keywords": "encoder, button, input", 4 | "description": "Arduino library to handle rotary encoders with buttons as a user input device", 5 | "repository": 6 | { 7 | "type": "git", 8 | "url": "https://github.com/0xPIT/encoder.git" 9 | }, 10 | "frameworks": "arduino", 11 | "platforms": "atmelavr" 12 | } 13 | --------------------------------------------------------------------------------