Bits and Droids logo
Mail icon

Back to basics: Dual concentric rotary encoders

3/30/2024 | by Bits and Droids

With all the previous components, we examined their inner workings and wrote our own code implementations. This is a good way to practice your coding skills and gain a good understanding of why something is done the way it is.

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 had 5 pins: the click pin, data pin, switch, 5V, and ground. The pins were 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 provides the most information through data sheets or quick summaries. When we take a look at the store page, we encounter these images:

dual concentric encoder circuit diagram
dual concentric encoder diagram

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 can see from the components' pictures, there are no markings.

dual concentric rotary encoder

This is where our previous knowledge comes into play. We know that a default encoder has the click and data pins. 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 two rotary encoders in one. 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.

dual enc 5v

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. When we start turning, a connection is made between pin A/B and the bottom part. When this happens, the internal pullup resistors will signal LOW when the power runs towards the common ground line (C). In the previous article about rotary encoders, we've covered how to 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 pins D and E as the switches' pins. One (pin D/E) can be wired to any digital/analog pin on your Arduino, and the other (pin D/E) to the ground, making 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
  • 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

As mentioned, I usually explain how to code everything from the ground up. Since these encoders aren't vastly different, repeating them wouldn't make sense. 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 eliminated 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 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 can access 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 # and include it at the top of our code, we (I explain this very simplified way) tell the IDE to include all the code in the library bundled with our code. Theoretically, we could create our library, put all our code in it, and upload an almost empty sketch while all the functionality is in it. Another great use of a library is writing code once and re-using it in the future. Let us start by including the Encoder and BitsAndDroidsFlightConnector.

cpp
#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 and function definitions and irrigates access to the above. The CPP (C Plus Plus)file contains all the logic behind these functions. This rule has thousands of exceptions and variations, but I hope it gives you a global understanding of the basic library structure.

The code

Let's start by creating two objects.

  • A Bits and Droids connector object
  • An Encoder object from the Encoder library

An object is an instance of a class that looks a bit like a real object. It 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 and a B pin, and for functionality, we want to check which side the rotary encoder is turning towards.

cpp
#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 called encoderObjectInner and encoderObjectOuter. If we 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 Serial object like we are used to.

cpp
#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; 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
cpp
#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 send a command depending on which rotary encoder is turned and towards which side. For this example, I will use the increase/decrease of standby com 1. 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:

  • sendCom1WholeInc();
  • sendCom1WholeDec();
  • sendCom1FractInc();
  • 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).

cpp
#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; connector.send(sendCom1WholeInc); } //Apply same logic to the outer encoder if(newPositionOuter > oldPositionOuter){ oldPositionOuter = newPositionOuter; connector.send(sendCom1FractInc); } // visa versa if we know the new value < old value we know it moved downwards if(newPositionInner < oldPositionInner){ oldPositionInner = newPositionInner; connector.send(sendCom1WholeDec); } if(newPositionOuter < oldPositionOuter){ oldPositionOuter = newPositionOuter; connector.send(sendCom1FractDecr); } }

You can now replace these commands with whatever you prefer. The documentation provides a full list of supported commands.

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 spent writing code. In this case, I haven't given my final verdict regarding this library yet. In our case, using 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!