Home » Back to basics: Dual concentric rotary encoders

Back to basics: Dual concentric rotary encoders

by Bits and Droids
Published: Last Updated on 0 comment

With all the previous components we looked at the inner working and wrote our own code implementations. This is a good way to practice your coding skills and get a good understanding of why something is done the way it’s done.

Even though we already covered the common rotary encoders I couldn’t pass on the opportunity to dig into the dual concentric rotary encoder. These are usually a fair bit more expensive than the regular ones. I’ve managed to pick them up for €7 apiece from Aliexpress. There might be some slight variations depending on the model you have (to clarify the code and inner workings of this article are based on this model: EC11EBB24C03, there is a high chance yours will work the same).

Wiring

The regular rotary encoders each had 5 pins, the click pin, data pin, switch, 5V, and ground. The pins were clearly marked and were relatively easy to mount.
The dual-concentric rotary encoders have 8 pins without any markings on them whatsoever. This is a prime example to emphasize that it’s important to remember where you’ve bought which item. Usually, when buying components, the seller will provide you most information through data sheets or quick summaries. When we take a look at the store page we encounter these images:

Datasheet circuitry
Datasheet pin locations

The dual concentric rotary encoder has 8 pins marked A x2, Bx2, Cx2, D and E. Without the datasheet we wouldn’t be able to see which pin does what on the device itself. As you’re able to see on the pictures of the component itself there are absolutly no markings to be found.

This is the part where our previous knowledge comes into play. We know that a default encoder has the click pin and the data pin. In our previous coding examples, we used to call this the A and B pin. We also know that the dual concentric rotary encoder is basically 2 rotary encoders in 1. So seeing the datasheet we can almost instantly see that there are 2x A’s and 2x B’s so these would probably be our familiar click and data pins.

In the datasheet’s left image (Datasheet circuitry), we can investigate the inner workings and spot a slight variation from the regular encoders. With the default encoders, we had a dedicated ground and 5v pin. These dual concentric ones seem to be constructed differently. Let’s break it down to see what is drawn out for us.

Pin A and pin B expect a 5V signal. It also tells us it requires a resistor (R) of 5000 Ohm (5k). Because we still want to use pin A and B to code our logic, it wouldn’t make sense to define pinA or B as an output to provide the 5V. To achieve this, we could enable the internal pull-up resistors of our Arduino (or any other board that supports this) to provide a HIGH (5V) signal when there is no connection and a LOW signal when the connection is made. The moment we start turning, a connection is made between pin A/B and the bottom part. The moment this happens, the internal pullup resistors will make the signal LOW when the power runs towards the common ground line (C). In the previous article about rotary encoders, we’ve covered how we can convert this HIGH and LOW state of the pins into logic. We’ll come back to this in the coding section.

Logic to determine the rotational direction

This leaves pin D and E as the pins for the switch, which (pin D/E) can be wired to any digital/analog pin on your Arduino and the other (pin D/E) to the ground. This makes it function like a regular pushbutton.

In summary we learned that:

  • Pins A and Pin B require a HIGH signal.
  • They share a common ground line C
  • In order to achieve this we’re going to use the INTERNAL_PULLUP resistors
  • Pins D and E belong to the switch
  • They can be wired like a regular switch

Libraries

Like I mentioned before; usually, I explain how to code everything from the ground up. Since these encoders aren’t vastly different, it wouldn’t make sense to repeat me. I could point you towards the old article and be done for the day. While I do appreciate the love and care we put in our code, I have to emphasize the famous quote:

not all heroes wear capes!

Some person on the internet a long time ago

Sometimes an unknown individual has already paved the road we want to take and eliminate the grunt work. I’m talking about libraries. Libraries are chunks of code made by someone else that contains a certain functionality to lighten the workload. These libraries are usually free and can be obtained either through the Arduino IDE. Go to sketch -> include libraries -> manage libraries -> look for the Encoder library by Paul Stoffregen.

Another method would be to download the library from the indicated Github page.

When this is downloaded, you’re able to go to the Arduino IDE. Click sketch -> include library -> add .ZIP library -> select your downloaded .ZIP file. This works the same way we usually include the Bits and droids flight connector.

If we use the #, include at the top of our code, we (I explain this very simplified) tell the IDE to include all the code in the library bundled with our own code. In theory, we could create our own library, put all our code in it and upload an almost empty sketch while all the functionality is in the library. Another great use of a library is that you’re able to write code once and re-use it in the future. Let us start by including the Encoder and BitsAndDroidsFlightConnector.

#include <BitsAndDroidsFlightConnector.h>
#include <Encoder.h>

Between the <> we define which library we want to add to our code. We append the name of the library with.h. .h points to a header file. To provide some background info a library usually has 2 main components. A header file and a cpp file. The header file contains (again very simplified) variable definitions, function definitions and dirigates the acces to all of the above. The cpp (C Plus Plus)file contains all the logic behind these functions. There are thousands of exceptions and variations on this rule but I hope it gives you a global understanding of the basic library structure.

The code

Lets start by creating two objects.

  1. A Bits and Droids connector object
  2. An Encoder object from the Encoder library

An object is an instance of a class that looks a bit like a real object. The object has several properties (variables) and things we can do with it (functions). Let’s take the encoder as an example. The encoder has an A pin, a B pin, and for the functionality, we want to check which side the rotary encoder is turning towards.

#include <BitsAndDroidsFlightConnector.h>
#include <Encoder.h>

//-1 Encoder object
//The encoder object takes pin A and pin B as arguments. 
//In this example, the encoder inner ring A+B is connected to pin 5 and 6 and the outer ring A+B to pin 7 and 8
Encoder encoderObjectInner(5, 6);
Encoder encoderObjectOuter(7, 8);

//-2 Connector object
//The connector doesn't need any pins it only uses a boolean as the argument. 
BitsAndDroidsFlightConnector connector(false);

We’ve now created an encoder object and called it encoderObjectInner and encoderObjectOuter. If we ever want to do something with these encoders, we can reference them by their name. The same goes for the connector.

In the setup block we only need to start out Serial object like we are used to.

#include <BitsAndDroidsFlightConnector.h>
#include <Encoder.h>

//-1 Encoder object
//The encoder object takes pin A and pin B as arguments. 
//In this example, the encoder inner ring A+B is connected to pin 5 and 6 and the outer ring A+B to pin 7 and 8
Encoder encoderObjectInner(5, 6);
Encoder encoderObjectOuter(7, 8);

//-2 Connector object
//The connector doesn't need any pins it only uses a boolean as the argument. 
BitsAndDroidsFlightConnector connector(false);

void setup(){
  Serial.begin(115200);
}

This library uses a variable to keep track of the rotary position. When we move up, it will increase this value, and if we move down, it will decrease this value. In the loop block, we have to take the following actions:

  • Read the new position
  • If the old position > newPosition do something
  • If the old position < newPosition do something
#include <BitsAndDroidsFlightConnector.h>
#include <Encoder.h>

//-1 Encoder object
//The encoder object takes pin A and pin B as arguments. 
//In this example, the encoder inner ring A+B is connected to pin 5 and 6 and the outer ring A+B to pin 7 and 8
Encoder encoderObjectInner(5, 6);
Encoder encoderObjectOuter(7, 8);

//-2 Connector object
//The connector doesn't need any pins it only uses a boolean as the argument. 
BitsAndDroidsFlightConnector connector(false);

void setup(){
  Serial.begin(115200);
}

//variable to store the old position
long oldPositionInner = -999;
long oldPositionOuter = -999;

//variable to store the new position this makes cross-checking possible
long newPositionInner = 0;
long newPositionOuter = 0;
void loop(){

 newPositionInner = encoderObjectInner.read();
 newPositionOuter = encoderObjectOuter.read();

// if the new value > old value we know we moved up
 if(newPositionInner > oldPositionInner){
    //make sure to check against the new old value to avoid any conflicts
    oldPositionInner = newPositionInner;
 }
//Apply same logic to the outer encoder
 if(newPositionOuter > oldPositionOuter){
    oldPositionOuter = newPositionOuter;
 }
// visa versa if we know the new value < old value we know it moved downwards
 if(newPositionInner < oldPositionInner){
    oldPositionInner = newPositionInner;
 }
 if(newPositionOuter < oldPositionOuter){
    oldPositionOuter = newPositionOuter;
 }
}

The last thing to do is sending a command depending on which rotary encoder is turned and towards which side. For this example, I’m going to use the increase/decrease of standby com 1. For this, I will use the connector object to fetch the correct command and send it with Serial.println(). For this, I use the following 4 functions:

  1. sendCom1WholeInc();
  2. sendCom1WholeDec();
  3. sendCom1FractInc();
  4. sendCom1FractDecr();

For this, we can place the commands into the corresponding logic block. If we move the inner ring up, we want to increase the fractal part of the frequency, and when the outer ring is moved, the whole numbers before the period need to change (and vice versa for the downward movement).

#include <BitsAndDroidsFlightConnector.h>
#include <Encoder.h>

//-1 Encoder object
//The encoder object takes pin A and pin B as arguments. 
//In this example, the encoder inner ring A+B is connected to pin 5 and 6 and the outer ring A+B to pin 7 and 8
Encoder encoderObjectInner(5, 6);
Encoder encoderObjectOuter(7, 8);

//-2 Connector object
//The connector doesn't need any pins it only uses a boolean as the argument. 
BitsAndDroidsFlightConnector connector(false);

void setup(){
  Serial.begin(115200);
}

//variable to store the old position
long oldPositionInner = -999;
long oldPositionOuter = -999;

//variable to store the new position this makes cross-checking possible
long newPositionInner = 0;
long newPositionOuter = 0;
void loop(){

 newPositionInner = encoderObjectInner.read();
 newPositionOuter = encoderObjectOuter.read();

// if the new value > old value we know we moved up
 if(newPositionInner > oldPositionInner){
    //make sure to check against the new old value to avoid any conflicts
    oldPositionInner = newPositionInner;
    Serial.println(connector.sendCom1WholeInc());
 }
//Apply same logic to the outer encoder
 if(newPositionOuter > oldPositionOuter){
    oldPositionOuter = newPositionOuter;
    Serial.println(connector.sendCom1FractInc());
 }
// visa versa if we know the new value < old value we know it moved downwards
 if(newPositionInner < oldPositionInner){
    oldPositionInner = newPositionInner;
    Serial.println(connector.sendCom1WholeDec());
 }
 if(newPositionOuter < oldPositionOuter){
    oldPositionOuter = newPositionOuter;
    Serial.println(connector.sendCom1FractDecr());
 }
}

Now you could replace these commands with whatever you’d prefer. You can find a full list of supported commands in the documentation.

Multiple solutions for the same problem

With this article, I want to show that there are multiple ways to solve an issue. Sometimes using a library will cut down the time writing code. In this case, I haven’t given my final verdict regarding this library yet. In our case, the use of a variable to determine the position seems unnecessary. In theory, we only need the logic that increases and decreases this variable to send the command directly, but that is beside the point.
If you have any questions, please feel free to contact me, and I hope to see you in the next one!

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