Home ยป Back to basics: Rotary encoders in mfs2020

Back to basics: Rotary encoders in mfs2020

by Bits and Droids
Published: Last Updated on 0 comment

In the last part of this series, we took a deep dive into momentary pushbuttons. If you are reading this and still don’t know what a library is or how the connector can be obtained, I’d suggest reading that article first.

But today we’re going to shove our buttons aside and focus on rotary encoders.

A volume knob of a stereo is usually made out of a potentiometer. We’re able to close it and open it where we can read the values in between. On the other hand, my microwave has a knob I can turn in either direction to increase or decrease the time my food gets nuked. The software might eventually reset my timer if I keep turning in a single direction, but there will never be a physical endpoint (perhaps if you apply too much force).

The rotary encoder model that is most common when starting out is the KY-40. This encoder has 5 pins.

  • Ground
  • 5V
  • A button
  • Pin A
  • Pin B

This simplified drawing can explain the internals of the rotary encoder with this simplified drawing. The red and green dot represents pin A and pin B. They both return either a high or low signal if the yellow part hits the pin.

If we rotate the disc counterclockwise, the green dot will hit the yellow part first. The red dot will, in that case, hit the yellow zone second. And when we rotate the disk clockwise, we could invert the logic.

Here we see the green dot left the yellow zone while the red dot is still in it.

Being in the yellow zone can be read as a digital signal returning either a HIGH(1, in the zone) or LOW(0, not in the zone) state. We could visualize this even further.

The blue line indicates our measuring point in time. These lines represent the state of the pins flowing chronologically from left to right. If the B pin’s High signal hits the blue line first as it does in the picture, we know that the rotary encoder turns counterclockwise. If the green HIGH state hit the blue line first, we would know the rotary encoder was being turned counterclockwise.

Wiring diagram of a Ky-40 encoder

Breaking the problem down

We could break this down into a logical problem we can solve with code. What is it we want to achieve? First of all, we want to know which side the rotary encoder is turning. This will come in handy when we assign controls to the motion. It would be great if we could turn it one way and increment a value while we decrement it the other way (think com frequencies or AP variables). For this, we need to know which pin got hit first by the HIGH state. Once we determined which pin got hit first, we need to save the current state to compare against this new variable in the next cycle.

Lets code

Let’s begin by declaring the pins at which the rotary encoder is connected to the board. For ease of use, we add and initiate the BitsAndDroidsFlightConnector library as well.

#include <BitsAndDroidsFlightConnector.h>

#define rotary1PinA 4
#define rotary1PinB 5
#define rotary1Switch 6

BitsAndDroidsFlightConnector(false);

Besides the pins, we also want some variables that help us keep track of the pin states. We need to create a variable for the current state of the A pin and the old state of the A pin. The reason we declare this variable at the top of the file is that these variables will always be present. The only thing that changes is the value assigned to the variable.

byte rotary1PinACurrentState = LOW;
byte rotary1PinALastState = LOW;

Where variables are placed can determine which part of your code can access it. If we declared byte rotary1PinACurrentState in the setup block, our loop function could not access the variable. To the loop block, this variable would be nonexistent. The same applies if we would declare a variable in the loop block; this variable would be nonexistent to the setup block. By declaring it at the top of the file, our entire code can access the variable and its assigned value.

In the setup block, we don’t have to define any input mode for the rotary encoder pins A and B. Suppose our rotary encoder has a switch. We need to declare that as an INPUT_PULLUP. While we’re on the subject of the setup block, let’s add the Serial initializer as well.

void setup(){
    Serial.begin(115200);
    pinMode(rotary1Switch, INPUT_PULLUP);
}

In the loop function, we’re going to work our magic with the encoders. We start by checking what the current state of rotary1PinACurrentState (which we declared at the top of the file) is and store that value to the variable.

void loop(){
    rotary1PinACurrentState = digitalRead(rotary1PinA);
}

Now in order to have a pleasant experience we’re going to check if a full indent has been passed.

If we look at the first picture in this post, we can spot that when the rotary encoder moves a fraction to the right, there could be a movement, but the green dot will still be in the grey area. This could lead to noise and readings that bounce harder than a kangaroo on thanksgiving.

This image has an empty alt attribute; its file name is image-16.png
This image is simplified. A real rotary encoder has more yellow parts (indents)

In order to remove these unreliable readings, we implement a check to ensure the A pin currentState is equal to HIGH and the oldState is equal to LOW. We only need to check the A pin since we can derive the rotary movement afterward by comparing the current A state with the current B state.

void loop(){
    rotary1PinACurrentState = digitalRead(rotary1PinA);
    if((rotary1PinACurrentState == HIGH) && (rotary1PinAOldState == LOW)){
      //Our logic will be place here    
    }
}

In c++ (and many other coding languages) the && sign represents an AND statement. The example above would make our code check if the currentState == HIGH AND oldState == LOW. Just like in high school math, we first check the parts in between (). IF either of the brackets EQUALS results to FALSE, the code between the {} won’t be excecuted.

In the logic part of our if statement the only part that is left is to check the state of the B pin.

void loop(){
    rotary1PinACurrentState = digitalRead(rotary1PinA);
    if((rotary1PinACurrentState == HIGH) && (rotary1PinALastState == LOW)){
      if(digitalRead(rotary1PinB) == LOW){
  
} else{
}  
    }
}

If pin A is HIGH and pin B is LOW, we know that the rotary encoder has turned counterclockwise. If pin A is HIGH and pin B is HIGH, we know it is turning clockwise.

Now we have that logic in place; we could fire a Serial command based on the rotational direction. In the connector library documentation, we’re able to find all the functions available. For this example, we’re going to use the increase heading bug and decrease. The respective functions for this are:

  • sendAPAltitudeInc()
  • sendAPAltitudeDec()

In the movement logic we only need to place a Serial.println command to finish up our project.

void loop(){
    rotary1PinACurrentState = digitalRead(rotary1PinA);    
    if((rotary1PinACurrentState == HIGH) && (rotary1PinALastState == LOW)){
      if(digitalRead(rotary1PinB) == LOW){
         Serial.println(connector.sendAPAltitudeDec());
         } else{
             Serial.println(connector.sendAPAltitudeInc());
            }  
    }
    rotary1PinALastState = rotary1PinACurrentState;
    
}

Outside of the logic, it’s important to set the new state to the last state so that in the next iteration of the loop block, we check against this new value.

Because we don’t want to have any loose ends in the full example below, you’re also able to find the switch built into to rotary encoder to engage the altitude lock.

#include <BitsAndDroidsFlightConnector.h>

#define rotary1PinA 4
#define rotary1PinB 5
#define rotary1Switch 6

BitsAndDroidsFlightConnector connector(false);

byte rotary1PinACurrentState = LOW;
byte rotary1PinALastState = LOW;

void setup(){
    Serial.begin(115200);
    pinMode(rotary1Switch, INPUT_PULLUP);
}

void loop(){
    rotary1PinACurrentState = digitalRead(rotary1PinA);    
    if((rotary1PinACurrentState == HIGH) && (rotary1PinALastState == LOW)){
      if(digitalRead(rotary1PinB) == LOW){
         Serial.println(connector.sendAPAltitudeDec());
         } else{
             Serial.println(connector.sendAPAltitudeInc());
            }  
    }
    rotary1PinALastState = rotary1PinACurrentState;

    if(digitalRead(rotary1Switch) == LOW){
      Serial.println(connector.sendAPPanelAltitudeHold());
      delay(200);
    }
    
}

Delays freeze the execution of the code for a period of time. Since a rotary encoder changes positions rapidly, there must be no delays present while measuring the position. If the current position is HIGH and we freeze the code for 200ms, there is a chance that the value is HIGH again, which looks like the position made no change.

In the example above, the delay is only executed when you press the button. This assumes that you aren’t turning the rotary encoder simultaneously.

Adding more encoders

It’s possible to add more encoders to your project. When adding components, it’s important to copy the proper code. This is also where a proper plan regarding names can save you from errors. The common mistake I used to make is to use these names with rotary encoders:

  • encoderAPinA
  • encoderAPinB
  • encoderBPinA
  • encoderBPinB

When the variables are presented in a list, the difference is easily spotted. But if we would fit these in our code, it becomes harder to spot the difference quickly. Pin A of encoder B might look like Pin B encoder A causing all kinds of errors.

When picking names the ones below make more sense.

  • encoder1PinA
  • encoder1PinB
  • encoder2PinA
  • encoder2PinB
  • encoderAltiPinA
  • encoderAltPinB
  • encoderSpeedPinA
  • encoderSpeedPinB

The last two would even give a clear description of the specific functionality the rotary encoder was installed for.

If we add another encoder our code would look like this:

I hope that the colors make it easier to spot the patterns. Starting to spot patterns will help develop your programming skills and make your life easier.

I hope that this article helped you get a better understanding of rotary encoders. They may seem daunting but when we dumb it down, the logic becomes more manageable. If I had to give a tip before we say goodbye is to verbally pronounce the logic you require. if(digitalRead(rotarySwitch) == LOW) isn’t the hardest code but if you are just starting out it might seem like gibberish to you. It would be easier to break the logic down into words (notice the keywords I put in capitals). IF I READ the DIGITAL SIGNAL of ROTARYSWITCH AND the VALUE IS LOW, do something. The last step would be to put it in a list.

  • IF (we probably need an if statement)
  • READ DIGITAL SIGNAL (this tells us that we need a function that reads the value because we know it will be a digital signal; we probably need digitalRead(parameter))
  • of ROTARYSWITCH (in the previous step we noticed that digitalRead takes a parameter. The parameter asked is the pin to read. So in our case, we need to do digitalRead(rotarySwitch)
  • IF VALUE IS LOW (We already concluded we need an if statement, now we also know what the if statement needs to check if(digitalRead(rotarySwitch) == LOW)
  • do something (the if statement checks a statement. If the statement is true, we execute the code found between the {})

If you made it all the way down here, I want to thank you personally! As always, if you still have any questions or spot a mistake, let me know.

You may also like

Leave a Comment

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept