├── .gitignore ├── LICENSING.txt ├── README.md ├── examples ├── ArduinoStyle │ └── ArduinoStyle.ino ├── ClickEncoderTest │ └── ClickEncoderTest.ino ├── ESP8266Example │ └── ESP8266Example.ino └── TwoDevices │ └── TwoDevices.ino ├── keywords.txt ├── library.json ├── library.properties └── src ├── ClickEncoder.cpp └── ClickEncoder.h /.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 | -------------------------------------------------------------------------------- /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 runtime 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/ArduinoStyle/ArduinoStyle.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int16_t oldEncPos, encPos; 5 | uint8_t buttonState; 6 | 7 | #define pinA A2 8 | #define pinB A1 9 | #define pinSw A0 //switch 10 | #define STEPS 4 11 | 12 | ClickEncoder encoder(pinA, pinB, pinSw, STEPS); 13 | 14 | void setup() { 15 | Serial.begin(9600); 16 | 17 | Timer1.initialize(1000); 18 | Timer1.attachInterrupt(timerIsr); 19 | 20 | encoder.setAccelerationEnabled(true); 21 | 22 | Serial.print("Acceleration is "); 23 | Serial.println((encoder.getAccelerationEnabled()) ? "enabled" : "disabled"); 24 | 25 | oldEncPos = -1; 26 | } 27 | 28 | void loop() { 29 | encPos += encoder.getValue(); 30 | 31 | if (encPos != oldEncPos) { 32 | oldEncPos = encPos; 33 | Serial.print("Encoder Value: "); 34 | Serial.println(encPos); 35 | } 36 | 37 | buttonState = encoder.getButton(); 38 | 39 | if (buttonState != 0) { 40 | Serial.print("Button: "); Serial.println(buttonState); 41 | switch (buttonState) { 42 | case ClickEncoder::Open: //0 43 | break; 44 | 45 | case ClickEncoder::Closed: //1 46 | break; 47 | 48 | case ClickEncoder::Pressed: //2 49 | break; 50 | 51 | case ClickEncoder::Held: //3 52 | break; 53 | 54 | case ClickEncoder::Released: //4 55 | break; 56 | 57 | case ClickEncoder::Clicked: //5 58 | break; 59 | 60 | case ClickEncoder::DoubleClicked: //6 61 | break; 62 | } 63 | } 64 | } 65 | 66 | void timerIsr() { 67 | encoder.service(); 68 | } 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/ESP8266Example/ESP8266Example.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This example demonstrates the use of the library on an ESP8266. without using a timer interrupt 4 | 5 | */ 6 | 7 | #include 8 | //#include 9 | 10 | 11 | #define ENCODER_PINA 13 12 | #define ENCODER_PINB 14 13 | #define ENCODER_BTN 0 14 | 15 | #define ENCODER_STEPS_PER_NOTCH 4 // Change this depending on which encoder is used 16 | 17 | ClickEncoder encoder = ClickEncoder(ENCODER_PINA,ENCODER_PINB,ENCODER_BTN,ENCODER_STEPS_PER_NOTCH); 18 | 19 | void setup() { 20 | Serial.begin(115200); 21 | 22 | encoder.setButtonHeldEnabled(true); 23 | encoder.setDoubleClickEnabled(true); 24 | 25 | // Enable the button to be on pin 0. Normally pin 0 is not recognized as a valid pin for a button, 26 | // this is to maintain backward compatibility with an old version of the library 27 | // This version can have the button on pin zero, and this call enables the feature. 28 | // in this version best to use pin -1 instead of 0 to disable button functions 29 | encoder.setButtonOnPinZeroEnabled(true); 30 | 31 | } 32 | 33 | void loop() { 34 | 35 | //Call Service in loop becasue using timer interrupts may affect ESP8266 WIFI 36 | //however call no more than 1 time per millisecond to reduce encoder bounce 37 | static uint32_t lastService = 0; 38 | if (micros() - lastService >= 1000) { 39 | lastService = micros(); 40 | encoder.service(); 41 | } 42 | 43 | 44 | static int16_t last, value; 45 | value += encoder.getValue(); 46 | 47 | if (value != last) { 48 | last = value; 49 | Serial.print("Encoder Value: "); 50 | Serial.println(value); 51 | 52 | } 53 | 54 | ClickEncoder::Button b = encoder.getButton(); 55 | if (b != ClickEncoder::Open) { 56 | Serial.print("Button: "); 57 | #define VERBOSECASE(label) case label: Serial.println(#label); break; 58 | switch (b) { 59 | VERBOSECASE(ClickEncoder::Pressed); 60 | VERBOSECASE(ClickEncoder::Held) 61 | VERBOSECASE(ClickEncoder::Released) 62 | VERBOSECASE(ClickEncoder::Clicked) 63 | VERBOSECASE(ClickEncoder::DoubleClicked) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/TwoDevices/TwoDevices.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This example demonstrates the use of 2 devices controlled by the ClickEncoder library 4 | 5 | In thie example, the 2 devices are a rotary encoder with built in button, and a normal momentary button 6 | 7 | */ 8 | #include 9 | #include 10 | 11 | #define ENCODER_PINA 5 // If the encoder moved in the wrong direction, swap PINA and PINB 12 | #define ENCODER_PINB 6 13 | #define ENCODER_BTN 7 14 | 15 | #define ENCODER_STEPS_PER_NOTCH 4 // Change this depending on which encoder is used 16 | 17 | #define EXTRA_BUTTON_PIN 8 18 | 19 | // Note, using ClickEncoder to define a stand alone button has been depricated. Use DigitalButton instead 20 | 21 | DigitalButton button = DigitalButton(EXTRA_BUTTON_PIN); 22 | ClickEncoder encoder = ClickEncoder(ENCODER_PINA,ENCODER_PINB,ENCODER_BTN,ENCODER_STEPS_PER_NOTCH); 23 | 24 | void timerIsr() { //Service methods from both instances must be included 25 | encoder.service(); 26 | button.service(); 27 | } 28 | 29 | void setup() { 30 | Serial.begin(115200); 31 | 32 | Timer1.initialize(1000); 33 | Timer1.attachInterrupt(timerIsr); 34 | 35 | encoder.setButtonHeldEnabled(false); 36 | encoder.setDoubleClickEnabled(true); 37 | 38 | button.setButtonHeldEnabled(true); 39 | button.setDoubleClickEnabled(false); 40 | 41 | // Enable the button to be on pin 0. Normally pin 0 is not recognized as a valid pin for a button, 42 | // this is to maintain backward compatibility with an old version of the library 43 | // This version can have the button on pin zero, and this call enables the feature. 44 | // in this version best to use pin -1 instead of 0 to disable button functions 45 | encoder.setButtonOnPinZeroEnabled(false); 46 | 47 | } 48 | 49 | void loop() { 50 | static int16_t last, value; 51 | value += encoder.getValue(); 52 | 53 | if (value != last) { 54 | last = value; 55 | Serial.print("Encoder Value: "); 56 | Serial.println(value); 57 | 58 | } 59 | 60 | ClickEncoder::Button b = encoder.getButton(); 61 | if (b != ClickEncoder::Open) { 62 | Serial.print("Button: "); 63 | #define VERBOSECASE(label) case label: Serial.println(#label); break; 64 | switch (b) { 65 | VERBOSECASE(ClickEncoder::Pressed); 66 | VERBOSECASE(ClickEncoder::Held) 67 | VERBOSECASE(ClickEncoder::Released) 68 | VERBOSECASE(ClickEncoder::Clicked) 69 | VERBOSECASE(ClickEncoder::DoubleClicked) 70 | } 71 | } 72 | 73 | ClickEncoder::Button b2 = button.getButton(); 74 | if (b2 != ClickEncoder::Open) { 75 | Serial.print("Button2: "); 76 | #define VERBOSECASE(label) case label: Serial.println(#label); break; 77 | switch (b2) { 78 | VERBOSECASE(ClickEncoder::Pressed); 79 | VERBOSECASE(ClickEncoder::Held) 80 | VERBOSECASE(ClickEncoder::Released) 81 | VERBOSECASE(ClickEncoder::Clicked) 82 | VERBOSECASE(ClickEncoder::DoubleClicked) 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # Syntax Coloring Map for ClickEncoder 3 | ####################################### 4 | 5 | ####################################### 6 | # Datatypes (KEYWORD1) 7 | ####################################### 8 | 9 | ClickEncoder KEYWORD1 10 | 11 | ####################################### 12 | # Methods and Functions (KEYWORD2) 13 | ####################################### 14 | 15 | setAccelerationEnabled KEYWORD2 16 | getAccelerationEnabled KEYWORD2 17 | getButton KEYWORD2 18 | getValue KEYWORD2 19 | service KEYWORD2 20 | setDoubleClickTime KEYWORD2 21 | setHoldTime KEYWORD2 22 | setDoubleClickEnabled KEYWORD2 23 | getDoubleClickEnabled KEYWORD2 24 | setButtonHeldEnabled KEYWORD2 25 | getButtonHeldEnabled KEYWORD2 26 | 27 | ####################################### 28 | # Instances (KEYWORD2) 29 | ####################################### 30 | 31 | ####################################### 32 | # Constants (LITERAL1) 33 | ####################################### 34 | -------------------------------------------------------------------------------- /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/soligen2010/encoder.git" 9 | }, 10 | "frameworks": "arduino", 11 | "platforms": [ 12 | "atmelavr", 13 | "atmelsam", 14 | "teensy", 15 | "espressif" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=ClickEncoder 2 | version=1.0.1 3 | author=soligen2010 4 | maintainer=Dennis 5 | sentence=Arduino library to handle rotary encoders with buttons as a user input device. 6 | paragraph= 7 | category=Display 8 | url=https://github.com/soligen2010/encoder 9 | architectures=* -------------------------------------------------------------------------------- /src/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 | 19 | // ---------------------------------------------------------------------------- 20 | // Acceleration configuration (for 1000Hz calls to ::service()) 21 | // 22 | #define ENC_ACCEL_TOP 3072 // max. acceleration: *12 (val >> 8) 23 | #define ENC_ACCEL_INC 25 24 | #define ENC_ACCEL_DEC 2 25 | 26 | // ---------------------------------------------------------------------------- 27 | 28 | #if ENC_DECODER != ENC_NORMAL 29 | # if ENC_HALFSTEP 30 | // decoding table for hardware with flaky notch (half resolution) 31 | const int8_t ClickEncoder::table[16] __attribute__((__progmem__)) = { 32 | 0, 0, -1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1, 0, 0 33 | }; 34 | # else 35 | // decoding table for normal hardware 36 | const int8_t ClickEncoder::table[16] __attribute__((__progmem__)) = { 37 | 0, 1, -1, 0, -1, 0, 0, 1, 1, 0, 0, -1, 0, -1, 1, 0 38 | }; 39 | # endif 40 | #endif 41 | 42 | // ---------------------------------------------------------------------------- 43 | 44 | ClickEncoder::ClickEncoder(int8_t A, int8_t B, int8_t BTN, uint8_t stepsPerNotch, bool active) 45 | : doubleClickEnabled(true),buttonHeldEnabled(true), accelerationEnabled(true), 46 | delta(0), last(0), acceleration(0), 47 | button(Open), steps(stepsPerNotch), 48 | pinA(A), pinB(B), pinBTN(BTN), pinsActive(active) 49 | #ifndef WITHOUT_BUTTON 50 | , analogInput(false) 51 | #endif 52 | { 53 | pinMode_t configType = (pinsActive == LOW) ? INPUT_PULLUP : INPUT; 54 | if (pinA >= 0) {pinMode(pinA, configType);} 55 | if (pinB >= 0) {pinMode(pinB, configType);} 56 | #ifndef WITHOUT_BUTTON 57 | if (pinBTN >= 0) {pinMode(pinBTN, configType);} 58 | #endif 59 | 60 | if (digitalRead(pinA) == pinsActive) { 61 | last = 3; 62 | } 63 | 64 | if (digitalRead(pinB) == pinsActive) { 65 | last ^=1; 66 | } 67 | } 68 | 69 | // ---------------------------------------------------------------------------- 70 | #ifndef WITHOUT_BUTTON 71 | 72 | 73 | // Depricated. Use DigitalButton instead 74 | ClickEncoder::ClickEncoder(int8_t BTN, bool active) 75 | : doubleClickEnabled(true),buttonHeldEnabled(true), accelerationEnabled(true), 76 | delta(0), last(0), acceleration(0), 77 | button(Open), steps(1), analogInput(false), 78 | pinA(-1), pinB(-1), pinBTN(BTN), pinsActive(active) 79 | { 80 | pinMode_t configType = (pinsActive == LOW) ? INPUT_PULLUP : INPUT; 81 | if (pinBTN >= 0) {pinMode(pinBTN, configType);} 82 | } 83 | 84 | // ---------------------------------------------------------------------------- 85 | // Constructor for using digital input as a button 86 | 87 | DigitalButton::DigitalButton(int8_t BTN, bool active) : ClickEncoder(BTN, active) 88 | 89 | { 90 | 91 | } 92 | 93 | // ---------------------------------------------------------------------------- 94 | // Constructor for using analog input range as a button 95 | 96 | AnalogButton::AnalogButton(int8_t BTN, int16_t rangeLow, int16_t rangeHigh) : ClickEncoder(BTN, (bool)false) 97 | { 98 | pinMode(pinBTN, INPUT); 99 | 100 | anlogActiveRangeLow = rangeLow; 101 | anlogActiveRangeHigh = rangeHigh; 102 | analogInput = true; 103 | 104 | if (anlogActiveRangeLow > anlogActiveRangeHigh) { // swap values if provided in the wrong order 105 | int16_t t = anlogActiveRangeLow; 106 | anlogActiveRangeLow = anlogActiveRangeHigh; 107 | anlogActiveRangeHigh = t; 108 | } 109 | } 110 | #endif 111 | 112 | // ---------------------------------------------------------------------------- 113 | // call this every 1 millisecond via timer ISR 114 | // 115 | void ClickEncoder::service(void) 116 | { 117 | bool moved = false; 118 | 119 | if (pinA >= 0 && pinB >= 0) { 120 | if (accelerationEnabled) { // decelerate every tick 121 | acceleration -= ENC_ACCEL_DEC; 122 | if (acceleration & 0x8000) { // handle overflow of MSB is set 123 | acceleration = 0; 124 | } 125 | } 126 | 127 | 128 | #if ENC_DECODER == ENC_FLAKY 129 | last = (last << 2) & 0x0F; 130 | 131 | if (digitalRead(pinA) == pinsActive) { 132 | last |= 2; 133 | } 134 | 135 | if (digitalRead(pinB) == pinsActive) { 136 | last |= 1; 137 | } 138 | 139 | int8_t tbl = pgm_read_byte(&table[last]); 140 | if (tbl) { 141 | delta += tbl; 142 | moved = true; 143 | } 144 | #elif ENC_DECODER == ENC_NORMAL 145 | int8_t curr = 0; 146 | 147 | if (digitalRead(pinA) == pinsActive) { 148 | curr = 3; 149 | } 150 | 151 | if (digitalRead(pinB) == pinsActive) { 152 | curr ^= 1; 153 | } 154 | 155 | int8_t diff = last - curr; 156 | 157 | if (diff & 1) { // bit 0 = step 158 | last = curr; 159 | delta += (diff & 2) - 1; // bit 1 = direction (+/-) 160 | moved = true; 161 | } 162 | #else 163 | # error "Error: define ENC_DECODER to ENC_NORMAL or ENC_FLAKY" 164 | #endif 165 | 166 | if (accelerationEnabled && moved) { 167 | // increment accelerator if encoder has been moved 168 | if (acceleration <= (ENC_ACCEL_TOP - ENC_ACCEL_INC)) { 169 | acceleration += ENC_ACCEL_INC; 170 | } 171 | } 172 | } 173 | // handle button 174 | // 175 | #ifndef WITHOUT_BUTTON 176 | unsigned long currentMillis = millis(); 177 | unsigned long millisSinceLastCheck = currentMillis - lastButtonCheck; 178 | if ((pinBTN > 0 || (pinBTN == 0 && buttonOnPinZeroEnabled)) // check button only, if a pin has been provided 179 | && (millisSinceLastCheck >= ENC_BUTTONINTERVAL)) // checking button is sufficient every 10-30ms 180 | { 181 | lastButtonCheck = currentMillis; 182 | 183 | bool pinRead = getPinState(); 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | if (pinRead == !pinsActive) { // key is now up 193 | if (keyDownTicks > 1) { //Make sure key was down through 1 complete tick to prevent random transients from registering as click 194 | if (button == Held) { 195 | button = Released; 196 | doubleClickTicks = 0; 197 | } 198 | else { 199 | #define ENC_SINGLECLICKONLY 1 200 | if (doubleClickTicks > ENC_SINGLECLICKONLY) { // prevent trigger in single click mode 201 | if (doubleClickTicks < (buttonDoubleClickTime)) { 202 | button = DoubleClicked; 203 | doubleClickTicks = 0; 204 | } 205 | } 206 | else { 207 | doubleClickTicks = (doubleClickEnabled) ? (buttonDoubleClickTime) : ENC_SINGLECLICKONLY; 208 | } 209 | } 210 | } 211 | 212 | keyDownTicks = 0; 213 | } 214 | 215 | if (pinRead == pinsActive) { // key is down 216 | if ((keyDownTicks > (buttonHoldTime)) && (buttonHeldEnabled)) { 217 | button = Held; 218 | } 219 | keyDownTicks += millisSinceLastCheck; 220 | } 221 | 222 | if (doubleClickTicks > 0) { 223 | doubleClickTicks -= (uint16_t)constrain(min(millisSinceLastCheck, (unsigned long)doubleClickTicks), 0, 65536); 224 | if (doubleClickTicks == 0) { 225 | button = Clicked; 226 | } 227 | } 228 | } 229 | #endif // WITHOUT_BUTTON 230 | 231 | } 232 | 233 | // ---------------------------------------------------------------------------- 234 | 235 | int16_t ClickEncoder::getValue(void) 236 | { 237 | int16_t val; 238 | int16_t r = 0; 239 | 240 | noInterrupts(); 241 | val = delta; 242 | 243 | delta = val % steps; 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | int16_t accel = ((accelerationEnabled) ? (acceleration >> 8) : 0); 256 | interrupts(); 257 | 258 | val /= steps; 259 | 260 | if (val < 0) { 261 | r -= 1 + accel; 262 | } 263 | else if (val > 0) { 264 | r += 1 + accel; 265 | } 266 | 267 | 268 | return r; 269 | } 270 | 271 | // ---------------------------------------------------------------------------- 272 | // This is used to reset the encoder. If the encoder gets 'between' dedents 273 | // (for example only needs 3 more dedents for next click instead of 4) 274 | // it could cause changing direction of encoder to do nothing on first turn 275 | // this could happen if teh encoder misses a reansition, or if the encoder 276 | // is being turned during initialization 277 | // This routine resets the encoder to re-sync it with the dedents 278 | 279 | void ClickEncoder::resetEncoder(void) 280 | { 281 | noInterrupts(); 282 | delta = 0; 283 | acceleration = 0; 284 | interrupts(); 285 | } 286 | 287 | // ---------------------------------------------------------------------------- 288 | 289 | #ifndef WITHOUT_BUTTON 290 | ClickEncoder::Button ClickEncoder::getButton(void) 291 | { 292 | noInterrupts(); 293 | ClickEncoder::Button ret = button; 294 | if (button != ClickEncoder::Held && ret != ClickEncoder::Open) { 295 | button = ClickEncoder::Open; // reset 296 | } 297 | interrupts(); 298 | 299 | return ret; 300 | } 301 | 302 | bool ClickEncoder::getPinState() { 303 | bool pinState; 304 | if (analogInput) { 305 | int16_t pinValue = analogRead(pinBTN); 306 | pinState = ((pinValue >= anlogActiveRangeLow) && (pinValue <= anlogActiveRangeHigh)) ? LOW : HIGH; // set result to LOW (button pressed) if analog input is in range 307 | } else { 308 | pinState = digitalRead(pinBTN); 309 | } 310 | return pinState; 311 | } 312 | 313 | #endif 314 | -------------------------------------------------------------------------------- /src/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 | // ---Button defaults------------------------------------------------------------- 16 | 17 | #define BTN_DOUBLECLICKTIME 400 // second click within 400ms 18 | #define BTN_HOLDTIME 1000 // report held button after 1s 19 | 20 | 21 | // ---------------------------------------------------------------------------- 22 | 23 | #include 24 | #if defined(__AVR__) 25 | #include 26 | #include 27 | #include 28 | #endif 29 | #include "Arduino.h" 30 | 31 | // ---------------------------------------------------------------------------- 32 | 33 | #define ENC_NORMAL (1 << 1) // use Peter Danneger's decoder 34 | #define ENC_FLAKY (1 << 2) // use Table-based decoder 35 | 36 | // ---------------------------------------------------------------------------- 37 | 38 | #ifndef ENC_DECODER 39 | # define ENC_DECODER ENC_NORMAL 40 | #endif 41 | 42 | #if ENC_DECODER == ENC_FLAKY 43 | # ifndef ENC_HALFSTEP 44 | # define ENC_HALFSTEP 1 // use table for half step per default 45 | # endif 46 | #endif 47 | 48 | // ---------------------------------------------------------------------------- 49 | #if defined(__arm__) && (defined (__STM32F1__) || defined (__STM32F4__) ) 50 | typedef WiringPinMode pinMode_t; 51 | #else 52 | typedef uint8_t pinMode_t; 53 | #endif 54 | 55 | class ClickEncoder 56 | { 57 | public: 58 | typedef enum Button_e { 59 | Open = 0, 60 | Closed, 61 | 62 | Pressed, 63 | Held, 64 | Released, 65 | 66 | Clicked, 67 | DoubleClicked 68 | 69 | } Button; 70 | 71 | public: 72 | ClickEncoder(int8_t A, int8_t B, int8_t BTN = -1, 73 | uint8_t stepsPerNotch = 4, bool active = LOW); 74 | 75 | #ifndef WITHOUT_BUTTON 76 | explicit ClickEncoder(int8_t BTN, bool active = false); // Depricated. Use Digtial Button instead 77 | 78 | #endif 79 | 80 | void service(void); 81 | void resetEncoder(void); 82 | int16_t getValue(void); 83 | 84 | #ifndef WITHOUT_BUTTON 85 | public: 86 | Button getButton(void); 87 | #endif 88 | 89 | #ifndef WITHOUT_BUTTON 90 | public: 91 | void setDoubleClickTime(uint16_t durationMilliseconds) 92 | { 93 | buttonDoubleClickTime = durationMilliseconds; 94 | } 95 | 96 | public: 97 | void setHoldTime(uint16_t durationMilliseconds) 98 | { 99 | buttonHoldTime = durationMilliseconds; 100 | } 101 | 102 | public: 103 | void setDoubleClickEnabled(const bool &d) 104 | { 105 | doubleClickEnabled = d; 106 | } 107 | 108 | const bool getDoubleClickEnabled() 109 | { 110 | return doubleClickEnabled; 111 | } 112 | 113 | public: 114 | void setButtonHeldEnabled(const bool &d) 115 | { 116 | buttonHeldEnabled = d; 117 | } 118 | 119 | const bool getButtonHeldEnabled() 120 | { 121 | return buttonHeldEnabled; 122 | } 123 | 124 | public: 125 | void setButtonOnPinZeroEnabled(const bool &d) 126 | { 127 | buttonOnPinZeroEnabled = d; 128 | } 129 | 130 | const bool getButtonOnPinZeroEnabled() 131 | { 132 | return buttonOnPinZeroEnabled; 133 | } 134 | #endif 135 | 136 | public: 137 | void setAccelerationEnabled(const bool &a) 138 | { 139 | accelerationEnabled = a; 140 | if (accelerationEnabled == false) { 141 | acceleration = 0; 142 | } 143 | } 144 | 145 | const bool getAccelerationEnabled() 146 | { 147 | return accelerationEnabled; 148 | } 149 | 150 | protected: 151 | int8_t pinA; 152 | int8_t pinB; 153 | int8_t pinBTN; 154 | bool pinsActive; 155 | volatile int16_t delta; 156 | volatile int16_t last; 157 | uint8_t steps; 158 | volatile uint16_t acceleration; 159 | bool accelerationEnabled; 160 | #if ENC_DECODER != ENC_NORMAL 161 | static const int8_t table[16]; 162 | #endif 163 | #ifndef WITHOUT_BUTTON 164 | volatile Button button; 165 | bool doubleClickEnabled; 166 | bool buttonHeldEnabled; 167 | bool buttonOnPinZeroEnabled = false; 168 | bool analogInput = false; 169 | uint16_t keyDownTicks = 0; 170 | uint16_t doubleClickTicks = 0; 171 | uint16_t buttonHoldTime = BTN_HOLDTIME; 172 | uint16_t buttonDoubleClickTime = BTN_DOUBLECLICKTIME; 173 | unsigned long lastButtonCheck = 0; 174 | int16_t anlogActiveRangeLow = 0; 175 | int16_t anlogActiveRangeHigh = 0; 176 | bool getPinState(); 177 | #endif 178 | }; 179 | 180 | #ifndef WITHOUT_BUTTON 181 | class AnalogButton : public ClickEncoder 182 | { 183 | public: 184 | explicit AnalogButton(int8_t BTN, int16_t rangeLow, int16_t rangeHigh); // Constructor for using analog input range as a button 185 | }; 186 | 187 | class DigitalButton : public ClickEncoder 188 | { 189 | public: 190 | explicit DigitalButton(int8_t BTN, bool active = false); // Constructor for using a button only 191 | }; 192 | 193 | #endif 194 | 195 | // ---------------------------------------------------------------------------- 196 | 197 | #endif // __have__ClickEncoder_h__ 198 | --------------------------------------------------------------------------------