Using a KY040 Rotary Encoder with Arduino

I was playing with RGB LEDs for my Anamatronic Evil Eyes Project. I had some rotary encoders sitting out that I had just received in the mail from ebay. I figured this would be a great chance to learn to use the rotary encoder – turn it to change the RGB’s color and maybe brightness.

I figured this would be slam dunk. Well no, it wasn’t I ended up having quite a few problems.

The KY040 looks like this:

First, the KY040 doesn’t seem to be very well documented, particularly in regards to connecting it to an Arduino.

My main reference ended up being this forum post in Even that didn’t give much of an explanation of the pins on the board.

Here is my take: a normal rotary encoder has 3 connections: A, B, and C. The KY040 has CLK, DT, SW, +, and GND. It is essentially a simple rotary encoder plus a push button switch and integrated pull-up resistors. The CLK is equivalent to A, DTA to B, and GND is C. SW is the push button switch and you provide a VCC signal so the integrated pull-up resistors will work.

Using the instructions in the aforementioned website, I wired the encoder to my arduino Nano like this:

rotary encoder schem

This hardware setup, plus the program in the example ‘kind’ of worked for me. The problem was, it was inconsistent. I’d rotate the knob one way slowly and most of the time in would increment by one, but sometimes by two and sometimes it would actually decrement.

The behavior was too inconsistent for what I would expect. It sounded like maybe a debounce problem, but I had put on the caps to prevent such problems.

So I pulled out my largely (so far) unused logic analyzer and connected it to CLK (channel 0) and DT (channel 1). I set the logic analyzer to start recording when pin 0 went low.

I turned the encoder on click right:

re wave 1

and one click left:

re wave 2

It looked good. On the right rotation, when CLK goes low, DT is currently low. On left rotation when CLK goes low, DT is high. Seems to work!!??

Then I decided to zoom way, way in on channel 0:

re wave 4

Uh oh. That’s not a clean signal. I took a look at channel 1 as carefully:

re wave 5

Even dirtier. But I supposedly had capacitors to keep this from happening. Well, I did BUT rather than connect the directly to pins 2 and 4 of the nano, I had them at the other end of the bread board just because it had been easier to put them there. I connected the caps directly between pins 2 and 4 and the ground. The noise is now gone.

With the noise problems solved, I began playing with the original code to see if I could make it ‘tighter’. I’ve used interrupts some, but I’m far from an expert and I found this website to be quite informative.

The following code works quite well for me and is in step with what I’m going to want for my RGB LED test circuit.

#include <Arduino.h>

// -----------------------------------------------------------------------------
// constants

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

// -----------------------------------------------------------------------------
// global vars

volatile int                             virtualPosition    = 0;

// -----------------------------------------------------------------------------
// forward decls

void isr();
void loop();
void setup();

// -----------------------------------------------------------------------------
// Interrupt service routine is executed when a HIGH to LOW transition is detected on CLK

void isr ()  {
    if (!digitalRead(PinDT))
        virtualPosition = virtualPosition + 1;
        virtualPosition = virtualPosition - 1;
    } // isr

// -----------------------------------------------------------------------------

void setup() {

    pinMode(PinDT, INPUT);
    pinMode(PinSW, INPUT);

    attachInterrupt(0, isr, FALLING);   // interrupt 0 is always connected to pin 2 on Arduino UNO


    } // setup

// -----------------------------------------------------------------------------

void loop() {

    int                                    lastCount = 0;

    while (true) {
        if (!(digitalRead(PinSW))) {        // check if pushbutton is pressed
            virtualPosition = 0;            // if YES, then reset counter to ZERO
            while (!digitalRead(PinSW)) {}  // wait til switch is released
            delay(10);                      // debounce
            Serial.println("Reset");        // Using the word RESET instead of COUNT here to find out a buggy encoder
        if (virtualPosition != lastCount) {
            lastCount = virtualPosition;
            Serial.print("Count = ");
        } // while
    } //loop

After writing this post up, I played with the encoder further and if I spin it fast enough, I still have debouncing issues. This is getting beyond my understanding but it appears that if the legit transitions happen fast enough, the capacitor can’t smooth out the bogus spikes.

I did a little research on this and found the recommendation to use millis() in the ISR to determine the length of time between interrupts. If a successive interrupt happens too quickly it gets ignored.

My new isr procedure looks like this:

void isr ()  {

    static unsigned long                lastInterruptTime = 0;

    unsigned long                       interruptTime = millis();

    // If interrupts come faster than 5ms, assume it's a bounce and ignore
    if (interruptTime - lastInterruptTime > 5) {
        if (!digitalRead(PinDT))
            virtualPosition = (virtualPosition + 1);
            virtualPosition = virtualPosition - 1;
    lastInterruptTime = interruptTime;
    } // ISR

Using 5ms works pretty well. I can spin the rotary encoder pretty fast and it registers every single position. But if I spin it fast enough, it will start dropping positions; however that is preferred to what I was seeing – it would actually think the rotary encoder was being turned opposite to its actual direction.



This entry was posted in c-arduino, KY040 Rotary Encoder and tagged . Bookmark the permalink.

36 Responses to Using a KY040 Rotary Encoder with Arduino

  1. Kyle says:

    My KY040 came with the R1 missing on the bottom side. Is yours like this? What gives?

    • Dan TheMan says:

      Mine has all 3 resistors in place. R1 for mine is the pullup for the switch, not the data or clock lines. If you are using arduino, you can use the internal pullup resistor for that line.

  2. enky says:

    Good day people,

    I am constructing a polythene sheet winding machine and I am imagining using this ky module encoder as a counter.

    I have some functions to incorporate and only hope I find someone who can guide me through.
    1) I want the encoder to be able to display the rotation of the measuring roller on a simple lcd.

    2) I want the the circuitry to shut the system off after a specific length is rolled over measuring wheel.

    Please assist me.

    • Dan TheMan says:

      This should be pretty straight forward. Count the clicks in a full revolution. Then divide the roller circumference by the number of clicks to get the distance per click.

      I don’t know the resolution (number of clicks) off the top of my head, but let’s just assume it is 20. And let’s assume that the roller has a diameter of 5cm.

      If the diameter is 5cm, then the circumference is 15.7cm (

      15.7cm / 20 = .785cm / click.

      Using an LCD screen is pretty straight forward and there are many sites on the web that will explain how to connect one to the arduino.

      Shutting the system off is pretty straight forward as well. I would probably set up up/down buttons that would increment/decrement a “length” variable. Then when the ky encoder is turned, it would decrement the length the appropriate amount. Once the length is <= 0, then turn off a pin that is connected to the equipment via a relay. Again, that is simple to find on the web as well.

      • jkwilborn says:

        Unfortunately, in real world, the circumference will change as it rolls up the ‘whatever’. You need to come up with a more linear way to measure if you are looking for the number of feet or something. Like a small wheel that will roll along as the sheet is feed or on the winding drugs outer layer. I would also suggest that you use something other than this device. It will not last long as it’s mechanical switches and designed for human use, with low duty cycle. Pick something like a small wheel, and an led/detector that counts rotations. Much more dependable…

  3. Ondra says:

    Thanks for tutorial!

    In case you want to edit it, in code in loop() is typing error – Serial.print(“Count; -> Serial.print(“Count = “);

  4. Dirk says:

    Hello Dan

    Thanks for the tutorial, it is much appreciated. Just a quick question for you, does it matter whether you call the void setup function after the void loop function as mentioned in your code example above?


    void isr();
    void loop();
    void setup();

    From my understanding the program will then first execute the void loop and go into the infinite while loop without getting to the void setup loop? My coding skills isn’t that great so please correct me if I’m wrong. Thanks again.


    • Dan TheMan says:

      Hi Dirk,

      The arduino IDE will call setup() first, automatically. Then it repeatedly calls loop(). I have the tendency of putting the functions into the source file in alphabetical order, so I can find them quickly when there are a lot of them. So if you see loop() listed first, that is why, but that order doesn’t affect the actual order they are called. Setup() will always be called first.

      • Dirk says:

        Hello Dan

        Thanks for the update it is much appreciated. It shows that you can learn something new every day. (“,) Thanks again for the tutorial and the explanation.


  5. Which value of capacitor you put betwen pin 2 and 4 of arduino? thanks

  6. Pingback: Arduino AD9850 Rotary Encoder Commands - Making It Up

  7. Raymond says:

    Thanks sir for the tutorial. One doubt. should i add one 0.47uf between PIN2 & PIN4 or should i add 2 capacitors,one between PIN2 & GND and another between PIN4 & GND?

  8. Dan TheMan says:

    I used 2 – one between pin 2 and ground and one between pin 4 and ground. These were added to try and minimize bounce.

  9. Sebastiaan says:

    Hi Dan

    Your code works perfect for me. I have 2 rotary encoders and only placed the capacitors between one of the modules, both modules work fine for me (with our without capacitors). Now here is the thing… When I use your code it gives me the exact output as expected (CC & CCW), however, when I merge your code into my code (without changing any of your code) my serial monitor outputs mostly CCW when spinning CC… My existing code (OLED data graph) uses millis, is it possible the behaviour is caused by a delay() or millis elsewhere in my script? It does count correctly CCW, it’s just when spinning CW that it mostly counts CCW (only 3 out of 20 clicks when spinning CW are actually increasing, the other 17 clicks CW decreased the value as if spinning CCW).

    Kind regards,

    • Dan TheMan says:

      Wow, that is bizarre behavior. I am using this same procedure in a big project without problem so far, but now I’m wondering…

      Assuming an outside force isn’t modifying virtualPosition and you have it declared volatile, the only way for virtualPosition to decrement is for digitalpinread(pinDT) to return low instead of high.

      pinDT’s signal changes well in advance of pinClk, so I would think that any debouncing issues would already be resolved.

      The only other possibility I can think of is there is too long a delay between the time pinCLK falls and digitalpinread(pinDT) occurs. Perhaps something else is interrupting the ISR.

      Since the encoder worked standalone, it would be hard to lay the blame with it (the hardware that is).

      If it were me, I would start by making sure the capacitors are in place. I’ve seen the physical evidence they help reduce debounce, though not completely.

      Next, I would try putting nointerrupts(); at the top of the ISR (preventing other interrupts), and interrupts(); at the end. I don’t know anything about the oLED module – maybe it is producing a lot of interrupts of its own?

      If that didn’t work, I would simplify the program as far as possible by commenting out everything except the encoder interrupt and adding stuff back until it failed. Then I would know where the conflict lies and perhaps have enough of a clue to correct the problem.

      Any of the assumptions I made above could be wrong and I would eventually test them all looking for the culprit.

      I will add that these days I never use hardware interrupts to read normal buttons/switches. Debounce and interrupts don’t play well together. I use a timer instead. But I’ve not seen nor come up with a better way to read encoder switches that doesn’t use hardware interrupts because they can switched so fast.

      • Sebastiaan says:

        Hi Dan!

        Thank you for taking the time to reply.
        I have both capacitors in place and I simplified the script to only 34 lines…
        The encoder works perfect with the following script:

        One would think it’s easy to merge this into an existing script and have it working.
        I’m quite puzzled to be honest 😀

  10. Sebastiaan says:

    I have an update 🙂
    By just commenting out sections in my script I discovered strangely enough that the problem is caused by the display library, having the script for my display disabled makes the script work. I can only assume that there might be variable name or something similar causing the problem so when I have the time I will just go over my rotary encoder script again and just change every name into something unique to see if it makes a difference. At least I know in which direction I have to look.

    • Dan TheMan says:

      Looking at your code, it appears you are not using either a hardware interrupt or timer interrupt. Chances are that is the problem. Your loop with just the encoder test is fast enough to catch the signal state change. But when you go off to update the display, the state change is missed.

      The way you have your test of the encoder written, I would just move that into a procedure that is called very frequently (< 5ms) by a timer.

      • Sebastiaan says:

        Thank you for your advise Dan!
        I will continue tomorrow after work (23:34 local time now and my eyes starting to blur from staring at the screen) 😀

        Its like a puzzle, luckily I like puzzling, keeps me young 🙂

      • Dan TheMan says:

        I hear that! Let us know the solution when you get it.

      • Sebastiaan says:

        I got it to work (just finished) 🙂
        Interrupt, software debounce, capacitors all included.
        The only thing (just a little thing) is that it requires 2 clicks to increase or decrease the value. I tried the RISING, CHANGE, LOW in the attachInterrupt(0, isr, FALLING) but no results.
        I might find a solution for that later but for now I’m happy to have the code merged into my existing code again. Ah… 00:21 local time. Time to sleep 😀

  11. edwin says:

    hola estoy utilizando un encoder ky 040 para visualizar la velocidad de un motor en una interfaz grafica en matlab utilizando arduino uno r3 como tarjeta de adquisicion de datos, por lo cual estoy controlando los pines del arduino por medio de comandos desde matlab. el problema que si muevo el eje del encoder en sentido horario o anti horario en ambos sentidos se me incrementan el contador, nunca disminuye.
    Te agradezco cualquier ayuda que me puedas dar.

    • Dan TheMan says:

      Google Translate did a pretty good job of translating this question to English so I can try to answer:

      hi i am using an encoder 040 k to display the speed of a motor in a GUI in matlab using Arduino Uno r3 as data acquisition card , so I ‘m controlling the pins of arduino by commands from matlab . the problem that if I move the encoder shaft clockwise or counterclockwise in both directions I increase the counter never decreases.
      I appreciate any help you can give me.
      If you look back at the logic analyzer output in my post, when CLK goes LOW, the value of DATA (DT) determines if the counter is incremented or decremented. If the counter only increments, the DT signal is always HIGH when the CLK signal goes LOW.

      I would look at the wiring and encoder to be sure everything is correct. Maybe it is connected to the wrong pin? Since the DT signal is always HIGH, at least you are getting a signal. The question is why it never goes LOW. As an experiment, you could force the DT pin low (ground it) to verify rotating the encoder decrements.

      I hope this helps.
      Si uno mira hacia atrás en la salida del analizador lógico en mi post , cuando CLK pasa a BAJO , el valor de los datos ( DT ) determina si el contador se incrementa o decrementa . Si el contador sólo incrementa , la señal DT es siempre alto cuando la señal CLK pasa a BAJO .

      Me gustaría ver en el cableado y el codificador para asegurarse que todo está correcto . Tal vez está conectado al pin mal? Dado que la señal DT es siempre alto , al menos de que está recibiendo una señal . La cuestión es por qué nunca pasa a BAJO . A modo de experimento , se podría obligar a la baja pasador DT ( que haga masa ) para verificar la rotación de los decrementos del codificador .

      Espero que esto ayude.

  12. Pingback: KY-040 Rotary Encoders | Polygon Door

  13. Jean Khoury says:

    Hey Dan,

    There’s something I don’t quite understand in your final code, in your isr function, the final line of code says lastInterruptTime = interruptTime, however everytime that function is called the first thing it does is to affect 0 to lastInterruptTime from the line static unsigned long lastInterruptTime = 0. your value stored is then lost and millis will always be bigger than 5ms.

    • Dan TheMan says:

      lastInterruptTime is declared static. That means it is initialized with 0 when the program loads, but then ‘remembers’ its value between calls.

      You can get the same effect by moving lastInterruptTime outside of the procedure and making it global; however, since I only access the variable inside the procedure I really want it defined inside the procedure so I don’t accidentally modify it elsewhere.

  14. Mihir says:

    What do “CLK” and “DT” stand for?

  15. Sujal says:

    You have done a great job. Very light weight and easy and perfectly working program.
    But I am having a problem, please help me.
    The program is working fine except one thing – no matter which direction I rotate, the “virtualPositon” goes on increasing. What can be the problem?

  16. Ziplock9000 says:

    You’re using interrupts so that you’re not constantly polling the encoder position. Then you go and undo your good work by polling for the button. You should be using interrupts for both if and when possible.

  17. dennisseda says:

    I was looking for an alternative solution to this same problem before. I somehow was able to bake it work by having a capacitor in parallel to the resistor between SW and +.

  18. Hello Dan, I’m glad I found your site here, your debounce arrangement was very clever as it helped me solve a little timing problem I was having with a project of mine using a KY-040 encoder:

    Here is the new code snippet;
    // Interrupt routine runs if CLK goes from HIGH to LOW
    void isr () {

    static unsigned long lastInterruptTime = 0;

    unsigned long interruptTime = millis();

    //If interrupts come faster than 5ms, assume it’s a bounce and ignore
    if (interruptTime – lastInterruptTime > 5) {
    if (!digitalReadFast(2))
    rotationdirection = digitalReadFast(3);
    rotationdirection = !digitalReadFast(3);
    lastInterruptTime = interruptTime;
    // ISR

    Now it reads the detents ans displays them properly on the LCD display, thanks my man!

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.