Rotary Encoders

http://playground.arduino.cc/Main/RotaryEncoders

https://forum.arduino.cc/index.php?topic=242356.15

 

 

Rotary Encoder1

 

WIRING INFORMATION
===================
Connect CLK to Pin 2 on Arduino Board  (CLK is Data Output 1 of KY-040)
Connect DT  to Pin 3 on Arduino Board  (DT is Data Output 2 of KY-040)
Connect SW  to Pin 4 on Arduino Board  (Switch – goes LOW when pressed)
Connect GND to ground
Connect +   to +5V  (this will pull up CLK and DT with 10 KiloOhm resistors)
—————————————————————————-
Connect a 0,47µ capacitor from ground to CLK   (debouncing)
Connect a 0,47µ capacitor from ground to DT    (debouncing)
Connect a 10 KiloOhm resistor from +5V to SW (no integrated pullup for SW !!)
—————————————————————————-
It is better NOT to use internal pull-up resistors on the Arduino, instead
use the integrated pull-ups of KY-040 (this requires “+” to be connected to 5V).
You can check if your version of the KY-040 has pull-up resistors on the bottom
side ouf the printed circuit board.
If not, use internal pull-ups from Arduino or external pull-ups.
—————————————————————————–
In the stopping positions the KY-040 has always HIGH signals on both CLK and DT.
When you turn the encoder from one position to another, either CLK or DT goes LOW
before the other signal goes LOW as well.
The signal that goes LOW first determines if the encoder is turned left or right.
Once you reach the next stopping position both signals will be HIGH again.

If you press the push button, the current count can be reset to ZERO.

For faster response you might increase the speed of the serial connection.
(Make sure, that the Serial Monitor is also set to a higher speed,
otherwise you will get no output).

My rotary encoder came from china for 2 dollars, and after some debugging i found out, that it sometimes
behaves strange (e.g. it gives a signal to the SW pin – although I did not press the button.)
So for serious projects invest a dollar more and buy a quality product.
———————————————————————————-

volatile boolean TurnDetected;
volatile boolean up;

const int PinCLK=2;                   // Used for generating interrupts using CLK signal
const int PinDT=3;                    // Used for reading DT signal
const int PinSW=4;                    // Used for the push button switch

void isr ()  {                    // Interrupt service routine is executed when a HIGH to LOW transition is detected on CLK
if (digitalRead(PinCLK))
up = digitalRead(PinDT);
else
up = !digitalRead(PinDT);
TurnDetected = true;
}

void setup ()  {
pinMode(PinCLK,INPUT);
pinMode(PinDT,INPUT);
pinMode(PinSW,INPUT);
attachInterrupt (0,isr,FALLING);   // interrupt 0 is always connected to pin 2 on Arduino UNO
Serial.begin (9600);
Serial.println("Start");
}

void loop ()  {
static long virtualPosition=0;    // without STATIC it does not count correctly!!!

if (!(digitalRead(PinSW))) {      // check if pushbutton is pressed
virtualPosition=0;              // if YES, then reset counter to ZERO
Serial.print ("Reset = ");      // Using the word RESET instead of COUNT here to find out a buggy encoder

Serial.println (virtualPosition);
}

if (TurnDetected)  {    // do this only if rotation was detected
if (up)
virtualPosition++;
else
virtualPosition--;
TurnDetected = false;          // do NOT repeat IF loop until new rotation detected
Serial.print ("Count = ");
Serial.println (virtualPosition);
}
}

}

 

----------------------------------------------

 

Try this state machine based code. It uses two pins for pin-change interrupts. It’s nearly unbreakable.

It’s really pointless to try to filter an encoder with caps. What are the characteristics of your “noise”? What is the worst case period and rise-time of your bounce? What is the typical frequency of your bounce? No one knows, but somehow they know what capacitor value to use. On top of that, you are also filtering your “real” signal. You’re slowing the rise time and shortening the period of the signal you ARE interested in.

This state machine just follows the bounce. You can use a high dollar encoder or a very cheap one that has terrible bounce. The state machine will handle them both without the extra cost more components and a degraded signal.

 

/*
 * rotary_sm.ino -- State machine implementation of rotary encoder driver
 * Interrupt driven; supports 2 types of interrupts:
 * * Polled interrupts; enable by defining TIMER2INT
 * * Pin change interrupts: enalbe by defining PINCHANGEINT
 * (Do NOT enable both at the same time)
 *
 * This program is developed from the code at: 
 * http://www.buxtronix.net/2011/10/rotary-encoders-done-properly.html
 * Since this code was in the form of an arduino library, and I had unresolvable
 * references that I could not figure out, I just modified it as a single sketch.
 * The only library support required is if you want to use interrupt polling,
 * and then the MsTimer2 library needs to be installed.
 */

/* Define PINCHANGEINT if you want to interrupt on any encoder pin change */
#define PINCHANGEINT
/* --- OR --- */
/* Define TIMER2INT if you want to use periodic interrupts to poll the encoder */
//#define TIMER2INT

/* Define ENABLEPULLUPS if there are no external pull-ups on encoder AB pins */
//#define ENABLEPULLUPS

/* Define to enable the ISR debug flag */
//#define ISRFLAG


/* You may need to install this library (MsTimer2) from the arduino site */
#ifdef TIMER2INT
#include <MsTimer2.h>
#endif


#define DIR_NONE 0x00 // No complete step yet.
#define DIR_CW 0x10 // Clockwise step.
#define DIR_CCW 0x20 // Anti-clockwise step.

unsigned int state;
unsigned int A = 2; // pins connected to the encoder (digital_pin 2)
unsigned int B = 3; // " (digital_pin 3)
unsigned int ISRflag = 5; // " (digital_pin 3)
 int count = 0; // count each indent
 int old_count = 0; // check for count changed


/*
 * The below state table has, for each state (row), the new state
 * to set based on the next encoder output. From left to right in,
 * the table, the encoder outputs are 00, 01, 10, 11, and the value
 * in that position is the new state to set.
 */

// State definitions state table (emits a code at 00 only)
// states are: (NAB) N = 0: clockwise; N = 1: counterclockwiswe
#define R_START 0x3
#define R_CW_BEGIN 0x1
#define R_CW_NEXT 0x0
#define R_CW_FINAL 0x2
#define R_CCW_BEGIN 0x6
#define R_CCW_NEXT 0x4
#define R_CCW_FINAL 0x5

const unsigned char ttable[8][4] = {
 {R_CW_NEXT, R_CW_BEGIN, R_CW_FINAL, R_START}, // R_CW_NEXT
 {R_CW_NEXT, R_CW_BEGIN, R_CW_BEGIN, R_START}, // R_CW_BEGIN
 {R_CW_NEXT, R_CW_FINAL, R_CW_FINAL, R_START | DIR_CW}, // R_CW_FINAL
 {R_START, R_CW_BEGIN, R_CCW_BEGIN, R_START}, // R_START
 {R_CCW_NEXT, R_CCW_FINAL, R_CCW_BEGIN, R_START}, // R_CCW_NEXT
 {R_CCW_NEXT, R_CCW_FINAL, R_CCW_FINAL, R_START | DIR_CCW}, // R_CCW_FINAL
 {R_CCW_NEXT, R_CCW_BEGIN, R_CCW_BEGIN, R_START}, // R_CCW_BEGIN
 {R_START, R_START, R_START, R_START} // ILLEGAL
};

void setup( ) {
 pinMode( A, INPUT );
 pinMode( B, INPUT );

#ifdef ENABLEPULLUPS
 digitalWrite( A, HIGH ); // set pullups
 digitalWrite( B, HIGH ); // "
#endif

#ifdef TIMER2INT
 MsTimer2::set( 1, T2_isr ); // interrupt polling:
 MsTimer2::start( );
#endif

#ifdef PINCHANGEINT
 attachInterrupt( 0, AB_isr, CHANGE ); // pin-change interrupts: 
 attachInterrupt( 1, AB_isr, CHANGE );
#endif

#ifdef ISRFLAG
 pinMode( ISRflag, OUTPUT ); // time the ISR
 digitalWrite( ISRflag, HIGH ); // set pull-up ON
#endif

 state = (digitalRead( A ) << 1) | digitalRead( B ); // Initialise state.
 old_count = 0;

 Serial.begin( 9600 );
 Serial.println( "Rotary Encoder Tests" );
}


#ifdef PINCHANGEINT
void AB_isr( ) {
 // Grab state of input pins.
 unsigned char pinstate = (digitalRead( A ) << 1) | digitalRead( B );

 // Determine new state from the pins and state table.
 state = ttable[state & 0x07][pinstate];

 if( state & DIR_CW ) count++;
 if( state & DIR_CCW ) count--;
}
#endif


#ifdef TIMER2INT
void T2_isr( ) {

#ifdef ISRFLAG
 digitalWrite( ISRflag, HIGH );
#endif

 // Grab state of input pins.
 unsigned char pinstate = (digitalRead( A ) << 1) | digitalRead( B );

 // Determine new state from the pins and state table.
 state = ttable[state & 0x07][pinstate];

 if( state & DIR_CW ) count++; // count up for clockwise
 if( state & DIR_CCW ) count--; // count down for counterclockwise

#ifdef ISRFLAG
 digitalWrite( ISRflag, LOW );
#endif
}
#endif


void loop( ) {
 if( old_count != count ) {
 Serial.println( count );
 old_count = count;
 }
}

If you count a pulse every time aVal changes state, (LOW to HIGH and HIGH to LOW) and don’t otherwise account for it, you will negate your counts and only count the spurious ones. Therefore use aVal *only* when it transitions from HIGH to LOW (or visa-versa, but this works, so I use it.) This seems to be the reason many have resorted to using Interrupts so they can use FALLING or RISING in their code. A change in the ‘if’ statement mimics the FALLING interrupt and made it all work with my encoders:

if ((aVal != aLast)&&(aVal==LOW)) {

The bold text above is the change. Now we use the datapoint only when transitioning from HIGH to LOW to indicate that the knob moved. And…

if(bVal == LOW){ encoderCount++; }
else { encoderCount–; } }

Gives direction. If bVal is also LOW, then the knob was turned CW, so lets increment our counter by one step. Else, if bVal was HIGH, CCW and let’s decrement our counter.

This works from fairly slow up to a fairly fast knob rotation. And NO USE OF INTERRUPTS!

Full Code with Serial Print to Monitor:

//Constants
int pinA = 7;  // Connected to CLK on KY-040
int pinB = 6;  // Connected to DT on KY-040
int encoderCount = 0;
int aLast;
int aVal;
int bVal;


void setup() {
  Serial.begin(9600);
  pinMode (pinA, INPUT);
  pinMode (pinB, INPUT);
  aLast = digitalRead(pinA);
}

void loop() {
  aVal = digitalRead(pinA);
  bVal = digitalRead(pinB);
  if ((aVal != aLast)&&(aVal==LOW)) { // Means the knob is rotating
    if(bVal == LOW){ encoderCount++;} // If bVal is Low, too, CW  ++
    else {encoderCount--;}            // else, CCW --
    
    Serial.print("PinA: "); Serial.print(aVal);   //Always 0 due to 'if'
    Serial.print(" PinB: "); Serial.print(bVal);  // Changes with Direction
    Serial.print(" Counter: ");Serial.println(encoderCount); 
  }
  aLast = aVal;    //Don't forget to reset aLast

}

LIBRARY

http://www.pjrc.com/teensy/td_libs_Encoder.html

Board Interrupt
Pins
LED Pin
(do not use)
Teensy 3.0 All Digital Pins 13
Teensy 2.0 5, 6, 7, 8 11
Teensy 1.0 0, 1, 2, 3, 4, 6, 7, 16
Teensy++ 2.0 0, 1, 2, 3, 18, 19, 36, 37 6
Teensy++ 1.0 0, 1, 2, 3, 18, 19, 36, 37
Arduino Due All Digital Pins 13
Arduino Uno 2, 3 13
Arduino Leonardo 0, 1, 2, 3 13
Arduino Mega 2, 3, 18, 19, 20, 21 13
Sanguino 2, 10, 11 0
/* Encoder Library - TwoKnobs Example
 * http://www.pjrc.com/teensy/td_libs_Encoder.html
 *
 * This example code is in the public domain.
 */

#include <Encoder.h>

// Change these pin numbers to the pins connected to your encoder.
// Best Performance: both pins have interrupt capability
// Good Performance: only the first pin has interrupt capability
// Low Performance: neither pin has interrupt capability
Encoder knobLeft(5, 6);
Encoder knobRight(7, 8);
// avoid using pins with LEDs attached

void setup() {
 Serial.begin(9600);
 Serial.println("TwoKnobs Encoder Test:");
}

long positionLeft = -999;
long positionRight = -999;

void loop() {
 long newLeft, newRight;
 newLeft = knobLeft.read();
 newRight = knobRight.read();
 if (newLeft != positionLeft || newRight != positionRight) {
 Serial.print("Left = ");
 Serial.print(newLeft);
 Serial.print(", Right = ");
 Serial.print(newRight);
 Serial.println();
 positionLeft = newLeft;
 positionRight = newRight;
 }
 // if a character is sent from the serial monitor,
 // reset both back to zero.
 if (Serial.available()) {
 Serial.read();
 Serial.println("Reset both knobs to zero");
 knobLeft.write(0);
 knobRight.write(0);
 }
}

https://exploreembedded.com/wiki/Interactive_Menus_for_your_project_with_a_Display_and_an_Encoder

 /*
|
| For tutorial visit:
| http://exploreembedded.com/wiki/index.php?title=Interactive_Menus_for_your_project_with_a_Display_and_an_Encoder 
|
*/


#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>

#define SECS_PER_MIN (60UL)
#define SECS_PER_HOUR (3600UL)
#define SECS_PER_DAY (SECS_PER_HOUR * 24L)

/* Useful Macros for getting elapsed time */
#define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN) 
#define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN) 
#define numberOfHours(_time_) (( _time_% SECS_PER_DAY) / SECS_PER_HOUR)
#define elapsedDays(_time_) ( _time_ / SECS_PER_DAY) 

#define maxItemSize 10

const int itemsPerScreen = 7;
const int fontSize = 8;


static int pinA = 2; // Our first hardware interrupt pin is digital pin 2
static int pinB = 3; // Our second hardware interrupt pin is digital pin 3
static int enSW = 9;

volatile byte aFlag = 0; // let's us know when we're expecting a rising edge on pinA to signal that the encoder has arrived at a detent
volatile byte bFlag = 0; // let's us know when we're expecting a rising edge on pinB to signal that the encoder has arrived at a detent (opposite direction to when aFlag is set)
volatile uint16_t encoderPos = 0; //this variable stores our current value of encoder position. Change to int or uin16_t instead of byte if you want to record a larger range than 0-255
volatile uint16_t
oldEncPos = 0; //stores the last encoder position value so we can compare to the current reading and see if it has changed (so we know when to print to the serial monitor)
volatile byte reading = 0; //somewhere to store the direct values we read from our interrupt pins before checking to see if we have moved a whole detent

// Software SPI (slower updates, more flexible pin options):
// pin 7 - Serial clock out (SCLK)
// pin 6 - Serial data out (DIN)
// pin 5 - Data/Command select (D/C)
// pin 4 - LCD chip select (CS)
// pin 8 - LCD reset (RST)
Adafruit_PCD8544 display = Adafruit_PCD8544(7, 6, 5, 4, 8);

//Declare the Menus you need. 
char menu[][maxItemSize] ={"Date","Time","Alarm","Format","Zone","Daylight","BACK"};
// Only 2 sub-menus are shown. You can add as many as you wish.
char subMenu0[][maxItemSize] = {"Date", "Month", "Year", "BACK"};
char subMenu1[][maxItemSize] = {"Hours", "Min", "Secs", "BACK"};


int cnt = 0; 
int itemSelected, subMenuSelected;
int itemsToDisplay=0;
unsigned long startmillis, milliSecs,mins,secs,hour; 
void setup() {

 display.begin();
 pinMode(enSW, INPUT_PULLUP); 
 pinMode(pinA, INPUT_PULLUP); 
 pinMode(pinB, INPUT_PULLUP);
 attachInterrupt(0,PinA,RISING);
 attachInterrupt(1,PinB,RISING); 
 Serial.begin(115200); 
 display.setContrast(100);
 display.display();
 delay(2000);
 display.clearDisplay(); 
 //display.setTextSize(fontSize/8);
 display.setTextColor(BLACK);

 startmillis = millis();

}

void loop() {

 //Show time on the default Screen.
 display.setTextSize(2);
 display.clearDisplay();
 display.setCursor(20,16);
 time(millis() / 1000);
 display.display();
 display.setTextSize(fontSize/8);
 
 // Enter the settings menu if select Switch is pressed
 if(digitalRead(enSW)==0){
 while(digitalRead(enSW)==0);//wait till switch is released.
 
 itemSelected = displayMenu(menu, sizeof(menu)/maxItemSize); 
 switch(itemSelected){
 
 case 0: 
 Serial.print("calling submenu");
 subMenuSelected = displayMenu(subMenu0, sizeof(subMenu0)/maxItemSize); 
 break;
 
 case 1: 
 subMenuSelected = displayMenu(subMenu1, sizeof(subMenu1)/maxItemSize); 
 break;

 //you may include other cases as required! 
 
 default: break; 
 
 }


 //if the selected option is BACK, we will go straight to the main menu. 
 if(itemSelected!= 6){
 switch(subMenuSelected){
 // Only case 0 is shown. Also the user input is not saved anywhere, which might be required in real use-case.
 case 0: display.clearDisplay();
 display.setCursor(0,0);
 display.println("Date");
 display.setCursor(28,16);
 display.println(encoderPos);
 display.display();
 
 while(digitalRead(enSW)){
 display.setCursor(0,0);
 display.clearDisplay();
 display.println("Date");
 
 display.setCursor(16,16);
 display.println(encoderPos);
 display.display(); 
 } 

 while(digitalRead(enSW)==0);
 break;
 
 default:break; 
 
 }

 } 
 
 }

 
}



// This function accepts the a 2D character array and its length and display it on the screen.
// The function montiers the encoder position and moves the menu up and down.
// It returns the selected menu option to the calling functions
 
int displayMenu(char menuInput[][maxItemSize], int menuLength){
 int curPos,startPos, endPos;
 do{ 
 
 startPos = encoderPos%menuLength; 
 Serial.println("startPos:");
 Serial.println(startPos);
 display.clearDisplay();
 
 endPos = itemsPerScreen;
 
 if(menuLength < itemsPerScreen)
 {
 endPos = menuLength -startPos; 
 }
 
 if((menuLength-startPos)<itemsPerScreen)
 {
 endPos = menuLength -startPos;
 }

 Serial.print("endPos:");
 Serial.println(endPos);
 
 for(cnt = 0; cnt<=(endPos-1); cnt++){
 if(cnt == 0)
 {
 display.setCursor(0,0);
 display.print("->");
 }
 
 display.setCursor(16, cnt*fontSize);
 display.println(menuInput[cnt+startPos]);
 Serial.println(menuInput[cnt+startPos]); 
 }
 
 display.display();
 cnt = 0;

 if(oldEncPos != encoderPos) {
 oldEncPos = encoderPos; 
 } 
 }while(digitalRead(enSW));
 while(digitalRead(enSW)==0); //wait till switch is reseleased 
 return startPos;
}

/*******************************Utility Functions *******************************/
void time(long val){ 
int days = elapsedDays(val);
int hours = numberOfHours(val);
int minutes = numberOfMinutes(val);
int seconds = numberOfSeconds(val);

 // digital clock display of current time
// display.print(days,DEC); 
// printDigits(hours); 
 display.print(minutes);
 printDigits(seconds);
 display.println(); 
 
}

void printDigits(byte digits){
 // utility function for digital clock display: prints colon and leading 0
 display.print(":");
 if(digits < 10)
 display.print('0');
 display.print(digits,DEC); 
}

void PinA(){
 cli(); //stop interrupts happening before we read pin values
 reading = PIND & 0xC; // read all eight pin values then strip away all but pinA and pinB's values
 if(reading == B00001100 && aFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
 encoderPos --; //decrement the encoder's position count
 bFlag = 0; //reset flags for the next turn
 aFlag = 0; //reset flags for the next turn
 }
 else if (reading == B00000100) bFlag = 1; //signal that we're expecting pinB to signal the transition to detent from free rotation
 sei(); //restart interrupts
}


void PinB(){
 cli(); //stop interrupts happening before we read pin values
 reading = PIND & 0xC; //read all eight pin values then strip away all but pinA and pinB's values
 if (reading == B00001100 && bFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
 encoderPos ++; //increment the encoder's position count
 bFlag = 0; //reset flags for the next turn
 aFlag = 0; //reset flags for the next turn
 }
 else if (reading == B00001000) aFlag = 1; //signal that we're expecting pinA to signal the transition to detent from free rotation
 sei(); //restart interrupts
}

 

Leave a Reply