3
\$\begingroup\$

I wish to write a program for my Raspberry pi that will print > when a rotary encoder is rotated clockwise and < when its rotated anti-clockwise.

Based on code for Arduino, I wrote the following program:

#include <stdio.h>
#include <string.h>
// https://github.com/WiringPi/WiringPi
#include <wiringPi.h>

#include "pinout.h" // Defines `clkPin` and `dtPin`

int main(int argc, char **argv){
    pinMode(clkPin, INPUT);
    pinMode(dtPin, INPUT);
    int clkState, clkLastState = 1;
    while(1) {
        clkState = digitalRead(clkPin);
        if (clkState != clkLastState) {
            if (digitalRead(dtPin) != clkState) {
                // Means: Clockwise rotation
                printf(">\n");
            } else {
                // Means: Anti-clockwise rotation
                printf("<\n");
            }
        }
        clkLastState = clkState;
    }
}

I'm using the following rotary encoder:

https://aliexpress.com/item/32474584136.html

However, I sometimes (too often) get two < printed when I only rotate the encoder a single click clockwise, etc. I also tried to add:

    pullUpDnControl(clkPin, PUD_UP);
    pullUpDnControl(dtPin, PUD_UP);

But it didn't seem to help much to the accuracy of the printed <>. Could it be I need real pull-up resistors instead of / in addition to the internal ones?

\$\endgroup\$
1
  • \$\begingroup\$ Comments are not for extended discussion; this conversation has been moved to chat. \$\endgroup\$
    – Voltage Spike
    Aug 30, 2022 at 20:32

3 Answers 3

3
\$\begingroup\$

Edit2: new answer below, but first some background.

I just integrated one of these cheap encoders into a project. They are made in China and resold under many different names. They are mechanical quadrature encoders. They have mechanical detents and they go through all 4 phases/quadrants for each "click" of the knob. The signals are labelled "CLK" and "DT" instead of the normal A/B. Both signals are high at the detents.

If you are sampling the clock signal fast, you need to de-bounce it (or possibly evaluate both signals with a state machine). The worst bounce that I have observed on a scope is 0.2 ms (yes, this is one part, others could be worse, and this one could get worse over time; this is just a home project and an occasional extra count is not serious). I am sampling in a loop that runs at 0.3 ms. When I get a logic 1 sample followed by 2 logic 0 samples then I "accept" the clock edge.

There are many other de-bounce algorithms. Which one I use often depends on the surrounding code. (My standard debounce algorithm is almost exactly as TonyM described in his comment, but the code wouldn't integrate well into this project)

When I turn the knob as fast as I will ever turn it, the "DT" signal will change as soon as 2 ms after the "CLK" signal. So, I needed to be sure to read the direction signal (DT) within this time for it to be valid. Although I am reading both signals simultaneously, since I am only debouncing the Clock, the debounce adds skew that will cause issues if the skew is more than 2 ms. I could put both signals through a debounce algorithm as an alternate solution, but it isn't necessary.

Based on comments from thebusybee, I believe that contact bounce (my original answer) isn't the primary problem. Since you are looking for both edges of the clock, you should expect to get 2 counts per click! The printf is slow, and delays the next sample, so this could be the reason why you don't always see 2 counts per click. More data would need to be collected to be certain.

If you fix the edge detector logic and remove the printf delay, I am certain that you will see the switch bounce issue. So, be prepared for it.

Note that a mechanical encoder is meant to be used for a human interface (knob), or a motor that moves infrequently. If you attach this to a constantly moving motor, you will quickly wear it out.

Also note that the (poor) spec sheet says that "+" must be 5V. It is just the voltage for the pull-ups, use 3.3V if your MCU has 3.3V I/O.

A hardware solution would be an RC filter followed by a Schmitt trigger. If you have complex software performing many tasks while you need to look for knob clicks, then this is probably a better solution. Then, you can put the CLK signal on an interrupt.

enter image description here One click CW (CLK: Yellow, DT: Blue): enter image description here

One click CCW (CLK: Yellow, DT: Blue): enter image description here

Edit1: Added code that works reliably for me. The debounce only adds a few lines of code. I did remove some non-relevant stuff, hopefully, I didn't accidentally remove something essential.

Edit3: Fixed the issue in the debounce code pointed out in the comments. The debounce algorithm could be better, but it is good enough for my purposes.

#define LOOP_DELAY_USEC         300           // Loop Delay
uint8_t u8EncoderPort;
bool bClkState;
bool bPrev1ClkState = 1;
bool bPrev2ClkState = 0;
bool bRotCw;
bool bCountChanged = false;

while ( true )
{
    u8EncoderPort = ENCODER_PORT_IN;                           // Read HW Port

    // Extract the Clk and DT bits
    bClkState = u8EncoderPort & ENCODER_CLK_BIT;
    bRotCw =  u8EncoderPort & ENCODER_DT_BIT;

    // Look for negative edges (debounce: One high sample, followed by two low samples)
    if ( !bClkState && !bPrev1ClkState && bPrev2ClkState )
    {
        // Increase or decrease count based on quadrature direction
        if ( bRotCw )
            giEncoderCount++;
        else
            giEncoderCount--;

        bCountChanged = true;
    }      
    bPrev2ClkState = bPrev1ClkState;        // Remember the previous states (for debounce)
    bPrev1ClkState = bClkState;

    if ( bCountChanged )
    {
        Update Display ...
        bCountChanged  = false;
    }

    // Delay 300 uSec
    __delay_cycles( LOOP_DELAY_USEC * CLK_MHZ );     // Should be a little longer than the longest switch bounce
                                                     // But, rotary encoder debounce must be less than 2 mS,
                                                     //   else risk getting the direction wrong

}
\$\endgroup\$
8
  • 1
    \$\begingroup\$ You may well find a more reliable debouncing method is to sample the input at regular intervals and accept it into 'newLevel' when seen steady for 3 or 5 sample periods. Then edge detection is done on the filtered 'newLevel', ensuring that noisy edges can't be missed, whereas looking for a pattern in a stream of noisy samples can miss that pattern. (3x/5x sample period) should be longer than the spec'd contact bounce time but much shorter than the encoder output high/low periods. Nice answer, concise and clear, upvoted. \$\endgroup\$
    – TonyM
    Aug 28, 2022 at 17:45
  • 1
    \$\begingroup\$ The nice feature of quadrature encoders is that you do not need to debounce. Bouncing would just lead to quick +1 / -1 changes in the counter, but not to count errors. However, you need to take samples of both input sample at the same time. Additionally, if the sampling period comes into the range of the quadrature interval, you start to miss some. \$\endgroup\$ Aug 29, 2022 at 6:08
  • \$\begingroup\$ Works ... in nearly all cases. What happens if you read ...-1-1-1-0-1-0-0-0-...? According to your algorithm, it will not be detected, will it? \$\endgroup\$ Aug 30, 2022 at 14:32
  • \$\begingroup\$ @thebusybee, quite. It's such a lot of discussion here around something that's dead easy :-) Always (1) take an unreliable signal and (2) filter it to produce a reliable signal. Then, and only then, (3) process it to extract data/information. Here, it's trying to jump from (1) straight to (3). The 'counting the bounces' thing also tries to pointlessly jump from (1) to (3). Instead, use a simple, dim, reliable circuitry to do (1) then (2) then (3). Have done in FPGAs, MCUs, CPLDs with tiny gate/instruction count and rock-solid reliability for fast encoders and loads of different sensors/inputs. \$\endgroup\$
    – TonyM
    Aug 30, 2022 at 15:02
  • \$\begingroup\$ Yes, you found an issue with my algorithm. I haven't noticed missed counts. A double count would be more annoying than a missed count. My actual code also has another feature that I have removed here for simplicity; fast turning causes the count to increment/decrement by 10. It is working good enough for my purposes, so I am not going to mess with it. I can quickly adjust it to any exact value in a range of 0 to 1000. I have spent far more time answering this question than I did to get the encoder to work in my project. \$\endgroup\$
    – Mattman944
    Aug 30, 2022 at 15:26
0
\$\begingroup\$

As some people suggested in the comments to the question, the encoder is a quadrant encoder, meaning that even without bouncing noise, the algorithm in the question should print two > or < for every rotation click.

This made me eventually use a hardware 100 nF debounce capacitor connected to the clock signal, along with a slightly more sophisticated code to deal with the quadrant behavior:

Capacitor connected to encoder

The code is:

#include <stdio.h>
#include <string.h>
// https://github.com/WiringPi/WiringPi
#include <wiringPi.h>

#include "pinout.h" // Defines `clkPin` and `dtPin`

// Support printing a counter when the rotary encoder changes
#ifdef DEBUG_COUNTER
int counter = 0;
#endif

int main(int argc, char **argv){
#ifdef DEBUG_COUNTER
    fprintf(stderr,
        "%s %s: Printing counter state due to DEBUG_COUNTER defined!\n",
        argv[0], argv[1]
    );
#endif
    pinMode(clkPin, INPUT);
    pinMode(dtPin, INPUT);
    int clkState, readOnce=0, clkLastState = 1;
    while(1) {
        clkState = digitalRead(clkPin);
        if (clkState != clkLastState) {
            if (readOnce == 0) {
                readOnce = 1;
            } else {
                readOnce = 0;
                if (digitalRead(dtPin) != clkState) {
                    // Means: Clockwise rotation
                    printf(">\n");
#ifdef DEBUG_COUNTER
                    counter++;
#endif
                } else {
                    // Means: Anti-clockwise rotation
                    printf("<\n");
#ifdef DEBUG_COUNTER
                    counter--;
#endif
                }
#ifdef DEBUG_COUNTER
                printf("count: %d\n", counter);
#endif
            }
        }
        clkLastState = clkState;
    }
}
\$\endgroup\$
-1
\$\begingroup\$

I propose an algorithm which does not do debouncing and relies on the self-canceling effect of quadrature. This is untested code. Let see the A and B signals:

A  _____-----_____-----_____

B    -----_____-----_____

The above picture shows "clockwise" rotating signals. For every change in state of either A or B (look from left to right), we can see:

If A changed, then:

if now A is HIGH: if B is high, count+1; else count-1
if now A is  LOW: if B is  low, count+1; else count-1

If B changed, then:

if now B is HIGH: if A is  low, count+1; else count-1
if now B is  LOW: if A is high, count+1; else count-1

Now look the trace from right to left, and see that, at every edge, this algorithm decrements count.

Now use a a byte to code the state of the two signals, A in bit 1 and B in bit 0:

xxxx.xxAB

The cycle is:

    newstate = 0;
    if (A) newstate |= 2;
    if (B) newstate |= 1;

    if (newstate != oldstate) {
      oldstate ^= newstate;     // see what changed
      if (oldstate & 2) {
        // A changed
        if (newstate & 2) {
          if (newstate & 1) count++; else count--;      // A is high
        } else {
          if (newstate & 1) count--; else count++;      // A is low
        }
      }
      if (oldstate & 1) {
        // B changed
        if (newstate & 1) {
          if (newstate & 2) count--; else count++;      // B is high
        } else {
          if (newstate & 2) count++; else count--;      // B is low
        }
      }
      oldstate = newstate;
    }

Now suppose there is a bounce on the first rising edge of A: (1) it raises, then (2) it goes down, then (3) it raises (bounce terminated):

        1 3
    ____-_----
         2

Suppose the software sampling happens at (1): count+1 (correct). If the sampling is slow, it will happen again after (3) and no action will be taken because no change is detected.

If the sampling is fast, it will also be done at (2), with result of count-1, "undoing" the previous increment. But if the sampling is fast, it will also happen at (3), incrementing again.

If the sampling happens at (2), nothing happens and, at (3), there will be count+1.

I think the correct frequency has to be at least 2x the maximum frequency of the signals, about 3x.

The algorithm assumes that a glitch on a signal happens only when the other signal is stable, and this should be true in the OP's situation. But I suspect that it should work even if a glitch happens to two signals at the same time... but I not enough time to analyze that (sorry...).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.