Tuesday, January 10, 2017

Arduino and KY040 Rotary Encoder

Testing the KY040 Rotary Encoder

The KY040 Rotary Encoder is an inexpensive device that is widely used for applications like radio volume or frequency and speed controls. The encoder puts out HIGH-LOW and LOW-HIGH signals alternately from two pins that can convey direction of turn of the encoder and one or more pulses that can adjust the value of a device. A simple example is a volume control on an auto stereo. Unlike potentiometers that turn from minimum to maximum resistance, the encoder can turn an unlimited number of times in either direction. Each small click of one detent of the encoder sends out pulses to the associated electronics.

Using rotary encoders has been tricky for hobbyists--internet forums are full of questions about how to use encoders, how to decode the signals, how to deal with bouncing contacts that generate unwanted random signals, and a host of other issues. Another complication that may not be readily apparent is that different encoders toggle 1, 2, or 4 times for each detent.

This blog contains code written for an Arduino UNO that captures the signals from a KY040 rotary encoder as it triggers the Arduino interrupt pins. A sequential list of interrupts is recorded for examination. The sketch seems to do a pretty good job and helped me have a better understanding of what signals were being generated within the KY040.

The circuit is simple: Pin A (KY040 pin CLK) is connected to Arduino UNO pin D2 which is interrupt pin INT0. Pin B (KY040 pin DT) is connected to Arduino pin D3 which is interrupt pin INT1. The KY040 pin SW is a simple push button switch that connects to Arduino pin D4. A different push button or toggle switch can be used instead of the KY040 switch, with the other side of the switch connected to ground. A push of the switch then takes pin D4 LOW as it is grounded through the switch. The KY040 +V is connected to Arduino +5V and the KY040 Gnd is connected to Arduino ground (0V). The KY040 has built in 10K pull-up resistors built in for both the CLK and DT pins. The code in this blog should work with encoders that do not have the built-in resistors or switch, but has not been tested as such.

The sketch does the following:

1) the sketch initializes and displays some brief instructions on Serial Monitor. It then waits for the turn of the encoder and a button push. The interrupt pins (D2, D3) and switch pin (D4) are biased to HIGH state by "INPUT_PULLUP", as well as the resistors used as pull-ups on the KY040 rotary encoder pins CLK and DT.

2) A turn of the encoder, one or more detents, causes the interrupt pins to run the "interruptA()" or "interruptB()" functions. These functions capture the time of the interrupt in microseconds and the pin that caused the interrupt. Data for each interrupt are sequentially stored in arrays. The interrupts are set to occur on "CHANGE" so that either a HIGH-LOW or LOW-HIGH transition should be recorded.

3) When the push button is pushed, the arrays are processed and printed using the "printHeader()" function. The "printHeader" function will automatically print if 50 interrupts are recorded.

4) The array data can be examined as a report on the Serial Monitor by the user in three ways.

4.a) if the function "noDups2" is run before "printHeader", the report will display only those interrupts that are unique, effectively filtering out noise or contact bounce to a large degree. The index is retained from the original data, so the user can determine at which point in the sequence a particular interrupt occurred.

4.b) if the function "noDups" is run before "printHeader", the report will remove redundant information. However, if the time of the interrupt is the only data that has changed from a previous interrupt (noise, etc), that interrupt will be retained in the report.

4.c) if neither "noDups" or "noDups2" is run before "printHeader", the report will display each interrupt as recorded.

Once the report is displayed to Serial Monitor, the arrays are cleared, and the sketch is ready to capture a new sequence of interrupts.

The sketch minimizes Serial.print so that the slow printing process does not interfere with the fast interrupt changes. Hence the sequence of interrupts is not affected by delays due to code other than the loop() function that only handles the push button action.

The sketch code is heavily documented to help explain the purpose of the code. The sort filters used in "noDups()" and "noDups2()" are simple and could be optimized, but I wanted to get the job done using code that could be understood by a user.


/*
 * KY040_Interrupt_Counter_u_01
 * 2017-01-08
 * Lowell Bahner
 *
 * microsecond counter
 *
 * This code counts the transitions on a rotary encoder
 * for one detent rotation change.
 *
 * The code marks the time in micros() when an encoder
 * pin transition occurs.
 *
 * A push button signals when to print the results to
 * Serial Monitor.
 *
 * The report displays the sequence of transitions of HIGH-LOW
 * or LOW-HIGH of the interrupt pins INT0 and INT1 that occur
 * as the rotary encoder is turned one or more detents.
 *
 * A maximum of 50 transitions are recorded, then automatic report.
 * Oscillations of HIGH-LOW-HIGH on an INT pin indicate
 * contact bounces that may require a better encoder or
 * capacitor noise filtering for clean transitions.
 * ======================================================

  Serial Monitor Example:

  Starting Rotary Encoder Test

 > Wire Rotary Encoder Pin A (CLK) to Uno pin INT0 (D2).
 > Wire Rotary Encoder Pin B (DT) to Uno pin INT1 (D3).
 > Wire Rotary Encoder (SW) or Push Button SW to Uno pin D4.
 > Run Sketch with Serial Monitor window ON at 9600 baud.
 > Turn Rotary Encoder 1 detent CW (right) or CCW (left).
 > Push Rotary Encoder Switch (HIGH to LOW) to display results.

 >> Ready >> (1 detent CW, then SW pushed)

    Index  Int Pin     IntA     IntB     Time
  -------  -------  -------  -------  -------
        0        x        1        1  0
        1        B        1        0  4
        2        B        1        1  6316
        3        B        1        0  18000
        4        A        0        0  144640
        5        B        0        1  156724
        7        A        1        1  165708

Above: IntB (pin 3) goes LOW at t=4 micros.
IntB goes HIGH at t=6316.
IntB goes LOW at t=18000.
IntA goes LOW at t=144640.
IntB goes HIGH at t=156724.
IntA goes HIGH at t=165708.
Reading down the Int Pin column shows transition sequence B-A-B-A


  >> Ready >> (1 detent CCW, then SW pushed)

    Index  Int Pin     IntA     IntB     Time
  -------  -------  -------  -------  -------
        0        x        1        1  0
        1        A        0        1  4
        2        B        0        0  31968
        3        A        1        0  43992
        4        B        1        0  65068
        6        B        1        1  65108

  Above: IntA goes LOW at t=4 micros.
  IntB goes LOW at t=31968.
  IntA goes HIGH at t=43992.
  IntB goes HIGH at t=65108.
  Reading down the Int Pin column shows transition sequence A-B-A-B

 * ======================================================
 *
 */

uint8_t pinA = 2;  // Connected to CLK on KY-040
uint8_t pinB = 3;  // Connected to DT on KY-040
uint8_t pinSW = 4;  // Connected to SW on KY-040

volatile uint8_t intPin[51];
volatile uint8_t aVal[51];
volatile uint8_t bVal[51];
volatile unsigned long milAB[51];      // current pinA micros at time of interrupt
int8_t indexAB[51];
volatile uint8_t aValLast;
volatile uint8_t bValLast;
volatile uint8_t intPinLast;
volatile unsigned long milABLast;
uint8_t flagSW = 0;
volatile uint8_t index = 0;
uint8_t indexCopy = 0;
unsigned long start, finished, elapsed;

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function setup
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void setup() {
  pinMode (pinA, INPUT_PULLUP);
  pinMode (pinB, INPUT_PULLUP);
  pinMode (pinSW, INPUT_PULLUP);

  attachInterrupt(0, interruptA, CHANGE);
  attachInterrupt(1, interruptB, CHANGE);

  // Save pin states
  aValLast = digitalRead(pinA);
  bValLast = digitalRead(pinB);
  milABLast = 0;
  intPinLast = 0;

  Serial.begin (9600);
  Serial.println(F("\n Starting Rotary Encoder Test"));

  displayInstructions();
  displayReady();
  clearArrays();
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function displayInstructions()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void displayInstructions() {
  Serial.println (F("\n"));
  Serial.println (F(" > Wire Rotary Encoder Pin A (CLK) to Uno pin INT0 (D2). "));
  Serial.println (F(" > Wire Rotary Encoder Pin B (DT) to Uno pin INT1 (D3). "));
  Serial.println (F(" > Wire Rotary Encoder (SW) or Push Button SW to Uno pin D4. "));
  Serial.println (F(" > Run Sketch with Serial Monitor window ON at 9600 baud."));
  Serial.println (F(" > Turn Rotary Encoder 1 detent CW (right) or CCW (left). "));
  Serial.println (F(" > Push Switch (HIGH to LOW) to display results. "));
}

void displayReady() {

  Serial.println (F("\n >> Ready >> "));
}



// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function interruptA()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void interruptA() {
  //Serial.print (F("\n > interruptA "));
  cli(); //stop interrupts happening before we read pin values
  if (index == 0) {
    start = micros();
    milAB[index] = 0;
  }
  index = index + 1;
  finished = micros();
  milAB[index] = finished - start;
  aVal[index] = digitalRead(pinA);
  bVal[index] = digitalRead(pinB);
  intPin[index] = 2; // which pin is connected to A interrupt
  indexCopy = index;
  flagSW = 1;
  sei(); //restart interrupts
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function interruptB()
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void interruptB() {
  //Serial.print (F("\n > interruptB "));
  cli(); //stop interrupts happening before we read pin values
  if (index == 0) {
    start = micros();
    milAB[index] = 0;
  }
  index = index + 1;
  finished = micros();
  milAB[index] = finished - start;
  bVal[index] = digitalRead(pinB);
  aVal[index] = digitalRead(pinA);
  intPin[index] = 3; // which pin is connected to B interrupt
  indexCopy = index;
  flagSW = 1;
  sei(); //restart interrupts
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function noDups
// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// do not record A or B duplicates during interrupt processing
//
void noDups() {
  byte dup = 0;

  if (milAB[index] == milABLast) { // include time of interrupt as a selector
    if (aVal[index] == aValLast) {
      if (bVal[index] == bValLast) {
        if (intPin[index] == intPinLast) {
          dup = 1;
        }
      }
    }
  }

  if (dup > 0) {
    index = index - 1;
  }

  // save the last values
  milABLast = milAB[index];
  aValLast = aVal[index];
  bValLast = bVal[index];
  intPinLast = intPin[index];
  indexCopy = index;
}


// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function noDups2
// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// remove A or B duplicates before printing arrays
// but retain sequential Index values
//
void noDups2() {
  uint8_t dup = 0;
  uint8_t prev = 0;
  unsigned long mil2[51];
  unsigned long mil2Last;
  uint8_t aVal2[51];
  uint8_t bVal2[51];
  uint8_t intPin2[51];
  uint8_t index2[51]; // use to retain index
  char buf [10]; // use to print int64 values

  // save the [0] values
  mil2[prev] = milAB[prev];
  aVal2[prev] = aVal[prev];
  bVal2[prev] = bVal[prev];
  intPin2[prev] = intPin[prev];
  index2[prev] = indexAB[prev]; // use to retain index

  for (uint8_t eID = 1; eID <= indexCopy; eID++) {
    // copy records to x2 arrays
    mil2[eID] = milAB[eID];
    aVal2[eID] = aVal[eID];
    bVal2[eID] = bVal[eID];
    intPin2[eID] = intPin[eID];
    index2[eID] = indexAB[eID]; // use to retain index
    /* debug trace
        Serial.print("\n\nindex "); Serial.print (eID);
        Serial.print(", prev "); Serial.print (prev);
        double temp = (double)mil2[prev];
        dtostrf(temp, 9, 0, buf);
        Serial.print(", mil2[prev] "); Serial.print (buf);
        temp = (double)milAB[eID];
            dtostrf(temp, 9, 0, buf);
        Serial.print(", milAB[eID] "); Serial.print (buf);
        Serial.print(", aVal2[prev] "); Serial.print (aVal2[prev]);
        Serial.print(", aVal[eID] "); Serial.print (aVal[eID]);
        Serial.print(", bVal2[prev] "); Serial.print (bVal2[prev]);
        Serial.print(", bVal[eID] "); Serial.print (bVal[eID]);
        Serial.print(", intPin2[prev] "); Serial.print (intPin2[prev]);
        Serial.print(", intPin[eID] "); Serial.print (intPin[eID]);
    */
    dup = 0;
    if (aVal[eID] == aVal2[prev]) {
      if (bVal[eID] == bVal2[prev]) {
        if (intPin[eID] == intPin2[prev]) {
          // add the following line of code to retain time as a selector
          // if (milAB[eID] == mil2[prev]) { // include time of interrupt as a selector
          dup = 1;
          //Serial.print("\n\n Duplicate [eID]: "); Serial.print (eID);
          // } // end if (milAB[eID] == mil2[prev]) {
        }
      }
    }
    // if not duplicate record, copy to next record [prev]
    if (dup < 1) {
      //Serial.print("\n\n Not Duplicate [eID]: "); Serial.print (eID);
      prev = prev + 1;
      mil2[prev] = milAB[eID];
      aVal2[prev] = aVal[eID];
      bVal2[prev] = bVal[eID];
      intPin2[prev] = intPin[eID];
      index2[prev] = indexAB[eID]; // use to retain index
    }
  }

  // copy [prev] records over [eID] records
  for (uint8_t eID = 0; eID <= prev; eID++) {
    milAB[eID] = mil2[eID];
    aVal[eID] = aVal2[eID];
    bVal[eID] = bVal2[eID];
    intPin[eID] = intPin2[eID];
    indexAB[eID] = index2[eID]; // this retains the original index rather than renumbering
  }
  index = prev;
  indexCopy = index;

  // save the last values
  milABLast = milAB[index];
  aValLast = aVal[index];
  bValLast = bVal[index];
  intPinLast = intPin[index];
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function loop
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void loop ()
{

  //Serial.println ("loop ...");
  if ((digitalRead(pinSW) == LOW) && (flagSW > 0)) {

    /*
      Serial.println (F("\n -- Print All Interrupts"));
      printHeader(); // print the original arrays with all interrupts
    */

    /*
      // Time of interrupts is included as a selector in determining duplicate records
      noDups(); // remove duplicate records and re-index
      Serial.println (F("\n -- Print No Duplicate Interrupts and re-index"));
      printHeader(); // print the trimmed arrays with changed interrutps
    */

    /* */
    // Time of interrupts is not included as a selector in determining duplicate records
    noDups2(); // remove duplicate records but retain the index
    //Serial.println (F("\n -- Print No Duplicate Interrupts"));
    printHeader(); // print the trimmed arrays with changed interrutps
    /* */

    clearArrays();
    displayReady();
  }

  // avoid array overflows
  if (index >= 50) {
    printHeader();
    clearArrays();
    displayReady();
  }

}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function clearArrays
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void clearArrays() {
  for (index = 0; index <= 50; index++ ) {
    // clear the arrays
    milAB[index] = 0;
    aVal[index] = 0;
    bVal[index] = 0;
    intPin[index] = 0;
    indexAB[index] = index;
  }
  flagSW = 0; // clear flag
  index = 0;
  indexCopy = index;
  start = micros();
  milAB[index] = 0;
  aVal[index] = digitalRead(pinA);
  bVal[index] = digitalRead(pinB);
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function printHeader
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void printHeader() {

  Serial.println (F("\n    Index  Int Pin     IntA     IntB     Time"));
  Serial.println   (F("  -------  -------  -------  -------  -------"));
  for (byte eID = 0; eID <= index; eID++)
  {
    print1(eID);
  }
  Serial.println ();

} // end of printHeader

// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// function print1
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

void print1(byte eID) {

  // print the current interrupt data

  char buf [10];
  sprintf (buf, "%9d", (int)indexAB[eID]);
  Serial.print (buf);
  if (intPin[eID] == 2) {
    Serial.print ("        A");
  } else if (intPin[eID] == 3) {
    Serial.print ("        B");
  } else {
    Serial.print ("        x");
  }
  sprintf (buf, "%9d", (int)aVal[eID]);
  Serial.print (buf);
  sprintf (buf, "%9d", (int)bVal[eID]);
  Serial.print (buf);
  Serial.print ("  "); Serial.print (milAB[eID]);
  Serial.println ();

} // end of printHeader1



// ++++++++++++++++++++++++++++++++++++++++++++++++++++
// print binary8 binary16 and hex functions
// ++++++++++++++++++++++++++++++++++++++++++++++++++++

//---------------------------------------------------------------------------------
// print 8-bit byte as 8 bit binary string
//---------------------------------------------------------------------------------

void print8Bits(uint8_t myByte) {
  for (uint8_t mask = 0x80; mask; mask >>= 1) {
    if (mask  & myByte)
      Serial.print('1');
    else
      Serial.print('0');
  }
}

//---------------------------------------------------------------------------------
// print 16-bit word as 16 bit binary string
//---------------------------------------------------------------------------------

void print16Bits(uint16_t myWord) {
  for (uint16_t mask = 0x8000; mask; mask >>= 1) {
    if (mask  & myWord)
      Serial.print('1');
    else
      Serial.print('0');
  }
}


//---------------------------------------------------------------------------------
// crPrintHEX print value as hex with specified number of digits
//---------------------------------------------------------------------------------

void crPrintHEX(unsigned long DATA, unsigned char numChars) {
  unsigned long mask  = 0x0000000F;
  mask = mask << 4 * (numChars - 1);
  Serial.print("0x");
  for (unsigned int eID = numChars; eID > 0;  --eID) {
    Serial.print(((DATA & mask) >> (eID - 1) * 4), HEX);
    mask = mask >> 4;
  }
  Serial.print("  ");
}

No comments:

Post a Comment