With my upcoming robot arm project in mind, I bought a couple of these cheap Keyes KY-040 rotary encoders. Guess they may come handy for manually controlling servos with greater precision than potentiometers, especially in combination with the 12 bit resolution PWM controller that will drive the arm’s servos.
First I wanted to check that all encoders were OK (they could be Chinese clones of Chinese clones…), so I grabbed some sketches from the Internet. All sketches – with or without the use of interrupts – worked fine on all encoders, but I noticed that they would only count every second click of the encoder. As my encoders have 30 clicks (‘detents’) per revolution, using the technique from these sketches would reduce the resolution to only 15 steps per revolution of the shaft.
So, in order to find out what exactly happens with each click, I wrote this simple sketch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
const int PinA = 2; const int PinB = 3; byte CLK, DT, CLK_old, DT_old; void setup() { Serial.begin(9600); pinMode(PinA, INPUT_PULLUP); pinMode(PinB, INPUT_PULLUP); CLK_old = digitalRead(PinA); DT_old = digitalRead(PinB); } void loop() { CLK = digitalRead(PinA); DT = digitalRead(PinB); if (CLK != CLK_old || DT != DT_old) { CLK_old = CLK; DT_old = DT; Serial.print("A: "); Serial.print(CLK); Serial.print(" - B:"); Serial.println(DT); delay(50); } } |
Rotary encoders have two switches, A and B, that will be switched on and off when you turn the shaft, resulting in binary pulses on their CLK and DT connectors. Since the pulse sequence of the switches are 90 degrees out of phase, you can not only count the pulses, but also detect whether the shaft is being rotated clockwise (CW) or counter clockwise (CWW).
The output of the above sketch showed that my type of encoder will click on every blue and every red marked event within the pulse sequence, whereas all sketches I’ve seen so far will count either the blue or the red events (i.e. full pulse cycles).
Once I understood why the sketches I found on the Internet ignored every second click of my decoders, it was time to write a sketch that would count every click, and tell me the direction as well. As the picture clearly illustrates, the values of A and B will be the same after every click. The direction is defined by which of them arrived at that value first. If it was A, then rotation is CW; if it was B, then rotation is CCW.
In order to detect every click of my decoder, I decided to attach a ‘CHANGE’ interrupt to switch A (marked CLK on the encoder). Note that attaching ‘CHANGE’ interrupts to both CLK and DT is useless.
A nice bonus of the encoder is that it has a third switch (marked SW), that is pulled to GND when you push the shaft. I decided to use this feature for cycling through different incremental step sizes, offering different resolutions for sending a servo to a desired position (coarse-, medium- and fine tuning).
Time to put it to work. The following sketch controls a two-servo pan/tilt module by reading two rotary encoders. It uses a 16 channel 12 bit PWM Servo breakout to drive the servos, but the sketch can easily be simplified if you drive them directly from Arduino PWM pins. The ‘step size per encoder click’ will cycle through values 1, 5 and 10 with each push of the shaft. Parameter values for debouncing were optimized after some testing.
In the end, this setup turned out to work remarkably well for controlling my pan/tilt module with two KY-040 encoders connected to an Arduino UNO.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
#include <Wire.h> #include <Adafruit_PWMServoDriver.h> // called without address parameter: default address 0x40 Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(); // Find these limits for your servo brand/type: #define SERVOMIN 120 // this is the 'minimum' pulse length count (out of 4096) for this servo #define SERVOMAX 550 // this is the 'maximum' pulse length count (out of 4096) for this servo /**************************************************/ long debouncing_time = 20; // in microseconds; play with this value for optimal debouncing volatile unsigned long last_micros1, last_micros2; byte steps[3] = {1,5,10}; // incremental steps array, cycled through by pushing the shaft byte R1_stepIndex = 0; byte R2_stepIndex = 0; // Encoder 1 static int R1_pinA = 2; // CLK - hardware interrupt on GPIO 2 (UNO) static int R1_pinB = 4; // DT static int R1_pinSW = 8; // SW // Encoder 2 static int R2_pinA = 3; // CLK - hardware interrupt on GPIO 3 (UNO) static int R2_pinB = 5; // DT static int R2_pinSW = 9; // SW volatile byte flag1 = 0; // 'something changed' signal from interrupt routine Encoder 1 to loop() volatile byte flag2 = 0; // 'something changed' signal from interrupt routine Encoder 2 to loop() // settings for 180 degrees servo 1: volatile int Pos1 = (SERVOMAX+SERVOMIN)/2; // initial mid position int oldPos1 = (SERVOMAX+SERVOMIN)/2; // idem // settings voor 180 graden servo 2: volatile int Pos2 = (SERVOMAX+SERVOMIN)/2; // initial mid position int oldPos2 = (SERVOMAX+SERVOMIN)/2; // idem int minPos = SERVOMIN; // min pulse length on (0-4096) scale int maxPos = SERVOMAX; // max pulse length on (0-4096) scale volatile byte reading1 = 0; volatile byte reading2 = 0; /**************************************************/ void setup() { //Serial.begin(115200); // used for finding servo limits pwm.begin(); pwm.setPWMFreq(60); // Analog servos run at ~60 Hz updates yield(); pwm.setPWM(0, 0, Pos1); // start position servo 1 (mid) pwm.setPWM(1, 0, Pos2); // start position servo 2 (mid) // Set pins used by rotary encoders to inputs, pulled HIGH pinMode(R1_pinA, INPUT_PULLUP); pinMode(R1_pinB, INPUT_PULLUP); pinMode(R1_pinSW, INPUT_PULLUP); pinMode(R2_pinA, INPUT_PULLUP); pinMode(R2_pinB, INPUT_PULLUP); pinMode(R2_pinSW, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(R1_pinA),ProcA1,CHANGE); attachInterrupt(digitalPinToInterrupt(R2_pinA),ProcA2,CHANGE); } void ProcA1(){ if ((long)(micros - last_micros1) > debouncing_time && flag1==0) { last_micros1 = micros(); flag1=1; // signal for loop() // NOTE: change the following two lines if you don't use pin 2 and 4 for this encoder! reading1 = PIND & 0x14; // read D0-D7 and strip away all but R1_pinA and R1_pinB's values if(reading1 == B00010100 || reading1 == B00000000) { // A==B, A came last: CCW Pos1=max(Pos1-steps[R1_stepIndex],minPos); } else { // A came first, B will follow: CW Pos1=min(Pos1+steps[R1_stepIndex],maxPos); } } } void ProcA2(){ if ((long)(micros - last_micros2) > debouncing_time && flag2==0) { last_micros2 = micros(); flag2=1; // signal to loop() // NOTE: change the following two lines if you don't use pin 3 and 5 for this encoder! reading2 = PIND & 0x28; // read D0-D7 and strip away all but R2_pinA and R2_pinB's values if(reading2 == B00101000 || reading2 == B00000000) { // A==B, A came last: CCW Pos2=max(Pos2-steps[R2_stepIndex],minPos); } else { // A came first, B will follow: CW Pos2=min(Pos2+steps[R2_stepIndex],maxPos); } } } void loop() { if ((!digitalRead(R1_pinSW))) { R1_stepIndex = (R1_stepIndex+1)%3; while (!digitalRead(R1_pinSW)) delay(10); } if ((!digitalRead(R2_pinSW))) { R2_stepIndex = (R2_stepIndex+1)%3; while (!digitalRead(R2_pinSW)) delay(10); } if(flag1 == 1) { // NOTE send command to servo BEFORE turning off interrupts. I2C needs them ON pwm.setPWM(0, 0, Pos1); cli(); //Serial.println(Pos1); // used for finding servo limits oldPos1=Pos1; last_micros1 = 0; delay(200); // play with this value for optimal debouncing flag1 = 0; sei(); } if(flag2 == 1) { // NOTE send command to servo BEFORE turning off interrupts. I2C needs them ON pwm.setPWM(1, 0, Pos2); cli(); //Serial.println(Pos2); // used for finding servo limits oldPos2=Pos2; last_micros2 = 0; delay(200); // play with this value for optimal debouncing flag2 = 0; sei(); } } |
Note that the code within interrupt routines ProcA1 and ProcA2 has been reduced to calculating the new servo position and setting an alert flag, while the overhead of polling within loop() is limited to checking these two alert flags. In sketches where single loop sequences can take long, one could improve responsiveness to encoder events by having the interrupt routines do all the work (including the actual servo control), although in general, that’s not recommended.
This approach will be used for my robot arm project, although I will have to drop the interrupt technique for some of the decoders because the UNO only has two pins for external interrupts.