AdaEncoder: a library for use with simple quadrature encoders

Announcing the release of Version 0.5 of the AdaEncoder library.

This library was intended to make it easy for you to use 1 or more rotary encoders as found at Sparkfun or Adafruit on your Arduino. Don't worry about debouncing, the library's functions take care of that for you.

This library interfaces with 2-pin encoders (2 pins A and B, then a common pin C). It does not indicate every state change, rather, it reports only when the decoder is turned from one detent position to the next. It is interrupt-driven and designed to be fast and easy to use. The interrupt routine is lightweight, and the programmer is then able to read the direction the encoder turned at their leisure (within reason; what's important is that the library is reasonably forgiving). The library is designed to be easy to use (it bears repeating :slight_smile: ) and it is reasonably immune to switch bounce.

The library uses tight code (no digitalRead() or digitalWrite()) and in use requires only two methods. The amount of code in the interrupt routines is kept small.

The library can be found at Google Code Archive - Long-term storage for Google Code Project Hosting.

The library requires the PinChangeInt library, version 1.70beta or newer, found at Google Code Archive - Long-term storage for Google Code Project Hosting.

I want to add two encoders to my robot's wheels but I don't understand the example sketch that is included with this library. It just seems to show two similar methods for how to get if CW or CCW direction. How do I accumulate encoder counts into two variables (add if CW, subtract if CCW)? Or how to get and log counts from the encoders? Also, the description about what pins to use on my Uno R3 is very confusing (it uses some sort of ASCII diagram and a strange table and symbols). Not sure what Arduino model it is describing.

This is the example code, for explanation purposes:

#include <PinChangeInt.h> // necessary otherwise we get undefined reference errors.
#include <AdaEncoder.h>

#define a_PINA 2
#define a_PINB 3
#define b_PINA 5
#define b_PINB 6

int8_t clicks=0;
char id=0;

void setup()
{
  Serial.begin(115200);
  AdaEncoder::addEncoder('a', a_PINA, a_PINB);
  AdaEncoder::addEncoder('b', b_PINA, b_PINB);  
}

void loop()
{
  encoder *thisEncoder;
  thisEncoder=AdaEncoder::genie(&clicks, &id);
  if (thisEncoder != NULL) {
    Serial.print(id); Serial.print(':');
    if (clicks > 0) {
      Serial.println(" CW");
    }
    if (clicks < 0) {
       Serial.println(" CCW");
    }
  }
}

When you call AdaEncoder::genie(&clicks, &id) the variable "clicks" will be set with the new number of counts detected and "id" will be either "a" or "b" (in this example -- note that when the AddEncoder was called it defined one encoder as 'a' and the other as 'b'). Watch for the ampersand in front of the variable; it's sending the variable's address rather than the contents of the variable, the function adjusts the contents of that variable, and when the subroutine returns the variable is holding the new value. This is referred to as "passing by address" and I assume it's where you're getting confused. As an example, if clicks was set to 10, you call genie and two clicks have occurred, then clicks will be set to 12.

Note also that you never request changes from a specific encoder; when you call the "genie" function you might get back the counts for either 'a' or 'b', assuming one of those encoders has seen changes, or (thisEncoder != NULL) might be false which means neither encoder has changed.

So a couple notes on using this library:

  1. You probably want to set "clicks" to zero before you call genie so you only get the recent changes, or in your case you want to know how far the wheel has moved since you checked last.
  2. Call genie repeatedly until thisEncoder comes back NULL so that you know you know you've got all the recent changes to the encoders.

The ASCII diagram of the ATMega328 chip can be ignored. I assume you understand that your encoder has three wires; the common wire is connected to ground and the other two pins must be kept together in the digital pin groupings 0 to 7, or 8 to 13, or analog 0 to 5. e.g. digital 8 and 9 for one encoder are OK, but digital 7 and 8 are not.

Chagrin, thanks a lot! That is great! Thanks for your detailed reply!

Announcing version 0.7beta of the AdaEncoder library. This release incorporates some bugfixes in the ooPinChangeInt library (which it uses).

I have moved the library to using the ooPinChangeInt library because, well, I like OO programming. So using the AdaEncoder library is now a bit different. Check the included "MyEncoder" sketch for an example. Most significantly, instead of calling genie() with reference arguments whose values are filled in, you now call genie() and an AdaEncoder object is returned. Then you query the object for details.

The older AdaEncoder 0.5 will continue to work with the latest PinChangeInt library, which repaired some bugs in the PinChangeInt library itself and makes AdaEncoder more reliable. If you don't like the OO-ness of this release, continue to use 0.5 but make sure to upgrade your PinChangeInt library to 2.19beta or above!

From the Release Notes:
Now works with the latest ooPinChangeInt. Should be much more reliable.

Bugfixes: the clicks variable was supposed to be int8_t, but I had query() and getClicks() returning uint8_t types.

Updated to utilize the ByteBuffer to fill a buffer of text for debug purposes.

turnOffPWM has been moved into its own file, as that code is distributed under the LPGL license. I use the GPL.

I've encountered a bug with the AdaEncoder (old version).

So you power up, turn the encoder CCW... the counter decrement. All is good.

You stop turning, and then turn one click CW... the counter instead of incrementing as expected, instead decrements!
Turn it again by another click CW, then the counter increments correctly.

Stop turning. Turn encoder one click CCW... same bug. The counter will increment again, instead of decrementing (since we turned CCW).
Turn it again by another click CCW, now the counter decrements correctly.

It seems to be getting "lost" during that first click when you change from CW to CCW (and vice versa). It still thinks it was going the previous direction.

Does this update fix this bug?

Thanks.

Well, I tried using the 0.7beta. And changed the necessary lines in my code to match the new format of Adaencoder 0.7beta. Compiled successfully and all that.

But I can't get it to work properly. My program just crashes when I turn the encoder using the 0.7beta... as if somebody pressed the reset button. I'm going back to 0.5.


I do have a few questions on the sample MyEncoder in 0.7beta

On Line# 15, we have this code
AdaEncoder encoderA = AdaEncoder('a', ENCA_a, ENCA_b);

but nowhere else in the program is "encoderA" used. What's the purpose of line#15?

Then on line#30, I see this
AdaEncoder *thisEncoder=NULL;

*thisEncoder is used in the loop() program. But how does Arduino know how *thisEncoder is setup? We defined encoder pins 2 and 3 for "encoderA", not "thisEncoder".

I'm confused.

vasquo:
I've encountered a bug with the AdaEncoder (old version).

So you power up, turn the encoder CCW... the counter decrement. All is good.

You stop turning, and then turn one click CW... the counter instead of incrementing as expected, instead decrements!
Turn it again by another click CW, then the counter increments correctly.

Stop turning. Turn encoder one click CCW... same bug. The counter will increment again, instead of decrementing (since we turned CCW).
Turn it again by another click CCW, now the counter decrements correctly.

It seems to be getting "lost" during that first click when you change from CW to CCW (and vice versa). It still thinks it was going the previous direction.

Does this update fix this bug?

Thanks.

It turns out, not all Bourns encoder are the same with regards to detent position.
See PEC09 series and PEC11 series. The library works fine with PEC11 series encoders but requires a slight change with PEC09 series.
Here's the mod I did in the library to work with my particular Bourns encoder.
http://arduino.cc/forum/index.php/topic,138403.0.html

vasquo:
I've encountered a bug with the AdaEncoder (old version).

So you power up, turn the encoder CCW... the counter decrement. All is good.

You stop turning, and then turn one click CW... the counter instead of incrementing as expected, instead decrements!
Turn it again by another click CW, then the counter increments correctly.
...
Does this update fix this bug?

I don't think the update fixes that bug- not directly anyways. The bug actually came from the PinChangeInt library, which was not properly testing the state of the pins when interrupts came in quickly. If you have bouncing switches (yes- if you have a mechanical encoder, then you do have bouncing switches), then quite likely you had some issues. I recommend you continue to use AdaEncoder 0.5 but update PinChangeInt to 2.19beta or greater.

Thanks for the reply. It's got nothing to do with the PinChange or AdaEncoder library.

It turns out some Encoders have different states at their detent positions.

For example, your Adaencoder works perfect with Bourns PEC11 series but has the weird behavior with PEC09 series.

I think it's because the AdaEncoder assumes that both A and B lines are both LOW at the Detent position.
But some encoders (like the PEC09), both A and B lines are HIGH at the Detent positions.

Maybe, create a function in AdaEncoder to let the user select what kind of encoder they have... A/B low at Detent position, or A/B high at Detent position.

vasquo:
Well, I tried using the 0.7beta. ... But I can't get it to work properly. ... I'm going back to 0.5.


I do have a few questions on the sample MyEncoder in 0.7beta

On Line# 15, we have this code
AdaEncoder encoderA = AdaEncoder('a', ENCA_a, ENCA_b);

but nowhere else in the program is "encoderA" used. What's the purpose of line#15?

Then on line#30, I see this
AdaEncoder *thisEncoder=NULL;

*thisEncoder is used in the loop() program. But how does Arduino know how *thisEncoder is setup? We defined encoder pins 2 and 3 for "encoderA", not "thisEncoder".

I'm sorry it didn't work out for you. It is a little more Object-Oriented like, so it's not everyone's cup of tea.

Also I'm sorry I am late in replying. This thread is not intended for support; there is a support link on the code.google.com page for that. Anyway, I will answer your questions for posterity:

The code is an example that shows you how to set up 2 encoders, A and B. encoderA and encoderB are designed to encapsulate all the important data within, so you can think about "encoders" rather than the pins of your Arduino. It's a conceptual change over version 0.5 of the library, really.

In the old days, then, of version 0.5, the genie() method would fill in the values for "clicks" and "id" for you, so you could then query the variables and ask, "Who turned? How far?".

Now in version 0.7, the genie() method returns a pointer to an AdaEncoder object. The "thisEncoder" variable is set to NULL on every iteration of loop. Then, if genie() finds a changed encoder, it will send back the pointer to the AdaEncoder object, and now you can do whatever you want with this encoder that you've found. It sends back either encoderA or encoderB- whichever one was turned. "thisEncoder", then, changes its identity to that of either A or B. I have done the simplest thing, which is to have it query itself and return the number of times it was clicked.

By using Object Oriented techniques, you are not bound to if/then statements based on values (like id numbers) of the encoders. Each encoder object intrinsically knows who it is; you can even subclass the encoder and have your subclass do special things simply based on its identity.

On this small of a scale it may be difficult to see any kind of win at all, that I'll grant. But I think it's interesting, and I like the way Object Oriented programming makes me adjust my way of thinking, so I have decided to go that route. Thankfully the AdaEncoder library is simple enough that, in the last year, I haven't moved from the functionality I created in version 0.5. The old one should continue to be useful to people. Although I do recommend an upgrade of the underlying PinChangeInt library.

Thanks for your library, but my problem is solved. It's working fine now.

As I said, I just need to change a couple of lines in your AdaEncoder.cpp.
But of course, the changes I did are hardcoded specific to the PEC09 Bourns encoder I'm using.

If on another project, I use a different encoder, I may need to change the AdaEncoder.cpp again (back to your original code).

I'll try to submit some code to maybe implement either the LOW/LOW-Detent or the HIGH/HIGH-Detent condition so the library can execute what's appropriate for the encoder type.

Thanks, vasquo. Anyway, your post prompted me to create some test code, and I hope to use this to ensure the integrity of the library in the future... (this works for v. 0.7beta and above only, but the basic algorithm applies to 0.5 as well)

// Version 1.0: OO version

#include <ByteBuffer.h>
#include <digitalWriteFast.h>
#include <ooPinChangeInt.h> // necessary otherwise we get undefined reference errors.
#define DEBUG
#ifdef DEBUG
ByteBuffer printBuffer(200);
#endif
#define SWINTR_DEBUG // To debug using software interrupts on your two pins, define this.
                    // Then in your sketch, set your pins as outputs.  Initialize them as you
                    // desire, attach an interrupt, then the interrupt code will be called.
                    // CAUTION: Make sure you do NOT have any switches connected to those outputs,
                    // or you may end up frying your ATmega328!
#include <AdaEncoder.h>

/*
I like to keep this diagram around so I know which Arduino pin
is part of which port.
                  +-\/-+
            PC6  1|    |28  PC5 (AI 5)
      (D 0) PD0  2|    |27  PC4 (AI 4)
      (D 1) PD1  3|    |26  PC3 (AI 3)
      (D 2) PD2  4|    |25  PC2 (AI 2)
 PWM+ (D 3) PD3  5|    |24  PC1 (AI 1)
      (D 4) PD4  6|    |23  PC0 (AI 0)
            VCC  7|    |22  GND
            GND  8|    |21  AREF
            PB6  9|    |20  AVCC
            PB7 10|    |19  PB5 (D 13)
 PWM+ (D 5) PD5 11|    |18  PB4 (D 12)
 PWM+ (D 6) PD6 12|    |17  PB3 (D 11) PWM
      (D 7) PD7 13|    |16  PB2 (D 10) PWM
      (D 8) PB0 14|    |15  PB1 (D 9) PWM
                  +----+*/
// An encoder for every port
#define ENCD_a 2
#define ENCD_b 3
#define ENCC_a A3
#define ENCC_b A4
#define ENCB_a 9
#define ENCB_b 10

AdaEncoder encoderD = AdaEncoder('d', ENCD_a, ENCD_b);
AdaEncoder encoderC = AdaEncoder('c', ENCC_a, ENCC_b);
AdaEncoder encoderB = AdaEncoder('b', ENCB_a, ENCB_b);

int8_t clicks=0;
char id=0;

void setup()
{
  Serial.begin(115200); Serial.println("---------------------------------------");
  Serial.println("AdaEncoder test. Assumes detent position is 1,1 (pin a, pin b).");
  Serial.println("Encoder states: 11->01->00->10->11 (== CCW 1 click)");
  volatile uint8_t portmask0=PCMSK0;
  volatile uint8_t portmask1=PCMSK1;
  volatile uint8_t portmask2=PCMSK2;
  PCMSK0=0; PCMSK1=0; PCMSK2=0; // turn off PinChange interrupts for a minnit
  pinMode(ENCB_a, OUTPUT); pinMode(ENCB_b, OUTPUT);
  digitalWriteFast2(ENCB_a, HIGH); digitalWriteFast2(ENCB_b, HIGH);
  pinMode(ENCC_a, OUTPUT); pinMode(ENCC_b, OUTPUT);
  digitalWriteFast2(ENCC_a, HIGH); digitalWriteFast2(ENCC_b, HIGH);
  pinMode(ENCD_a, OUTPUT); pinMode(ENCD_b, OUTPUT);
  digitalWriteFast2(ENCD_a, HIGH); digitalWriteFast2(ENCD_b, HIGH);
  PCMSK0=portmask0; // interrupts back on.
  PCMSK1=portmask1;
  PCMSK2=portmask2;
}

void changeEncoder(uint8_t pin) {
  if (digitalReadFast2(pin)) { digitalWriteFast2(pin, LOW); }
  else digitalWriteFast2(pin, HIGH);
}

void printEncoderState(uint8_t pina, uint8_t pinb) {
  Serial.print(digitalReadFast2(pina), DEC);
  Serial.println(digitalReadFast2(pinb), DEC);
}

void loop() 
{
  char inChar, outChar;
  // 00 -> 10 -> 11 -> 01 -> 00
  if(Serial.available()) {
    inChar=(Serial.read()); // get command from serial input
    switch (inChar) {
    case '1':
        Serial.print("B-a "); changeEncoder(ENCB_a); printEncoderState(ENCB_a, ENCB_b);
      break;
    case '2':
        Serial.print("B-b "); changeEncoder(ENCB_b); printEncoderState(ENCB_a, ENCB_b);
      break;
    case '5':
        Serial.print("C-a "); changeEncoder(ENCC_a); printEncoderState(ENCC_a, ENCC_b);
      break;
    case '6':
        Serial.print("C-b "); changeEncoder(ENCC_b); printEncoderState(ENCC_a, ENCC_b);
      break;
      case '9':
        Serial.print("D-a "); changeEncoder(ENCD_a); printEncoderState(ENCD_a, ENCD_b);
      break;
      case '0':
        Serial.print("D-b "); changeEncoder(ENCD_b); printEncoderState(ENCD_a, ENCD_b);
      break;
    }
  }
  while ((outChar=(char)printBuffer.get()) != 0) Serial.print(outChar);
  AdaEncoder *thisEncoder=NULL;
  thisEncoder=AdaEncoder::genie();
  if (thisEncoder != NULL) {
    Serial.print(thisEncoder->getID()); Serial.print(':');
    clicks=thisEncoder->query();
    if (clicks != 0) { Serial.print("Clicks: "); Serial.print(clicks, DEC); }
    if (clicks > 0) {
      Serial.println(" CW");
    }
    if (clicks < 0) {
       Serial.println(" CCW");
    }
  }
}

...If I then hit a sequence of numbers in the Monitor screen, I can then demonstrate that the library behaves as expected.

For example, if I hit the following sequence, I simulate switch bounce on the two pins and it registers "CCW" exactly when I expect it:

Number	Output
Typed	
1	B-a 01
1	B-a 11
1	B-a 01
1	B-a 11
1	B-a 01
2	B-b 00
2	B-b 01
2	B-b 00
2	B-b 01
2	B-b 00
1	B-a 10
1	B-a 00
1	B-a 10
1	B-a 00
1	B-a 10
2	B-b 11
 	b:Clicks: -1 CCW

Resurrecting an old thread, but there appears to be a minor bug in the routine;

Looking at the AdaEncoder::query() routine in AdaEncoder.cpp, clicks is only incremented/decremented by one each time the routine is called. The problem arrises if the encoder is turned more than one click between queries. If, for example, it is turned 3 clicks and then queried, it will correctly return 3 the first time, but then at the following queries it will return 2 & 1, even though the encoder hasn't moved.

I tested this by simply adding a delay and the end of the example sketch. (The variable total is meant to hold the net number of clicks the encoder has turned):

// Version 1.1: OO version
#include <ooPinChangeInt.h> // necessary otherwise we get undefined reference errors.
#include <AdaEncoder.h>

#define ENCA_a 2
#define ENCA_b 3

AdaEncoder encoderA = AdaEncoder('a', ENCA_a, ENCA_b);
int8_t clicks=0;
char id=0;

void setup()
{
  Serial.begin(9600); Serial.println("---------------------------------------");
}
int total = 0;
void loop() {
  char outChar;
  AdaEncoder *thisEncoder=NULL;
  thisEncoder=AdaEncoder::genie();
  if (thisEncoder != NULL) {
    Serial.print(thisEncoder->getID()); Serial.print(':');
    clicks=thisEncoder->query();
    total = total + clicks;
   if (clicks > 0) {
      Serial.print(" CW "); Serial.print(clicks); Serial.println(" Clicks");
    }
    if (clicks < 0) {
       Serial.println(" CCW");
    }
    Serial.print("position: "); Serial.println(total);
   }
  delay(1000);
}

Below is the output - the fist two iterations, the encoder was turned one click and the routine returned 1, as expected. The 3rd time, it the encoder was turned two clicks between queries. The next time it was queried, it returned "CW 2 Clicks" and the position correctly indicates '4,' but then the at the next time through the main loop, clicks has only been decremented to 1, not 0 and the routine returns 'CW 1 click' despite the encoder not actually moving. It would appear that the query routine should simply reset clicks to 0 after returning the number of clicks, or am I missing something?

---------------------------------------
a: CW 1 Clicks
   position: 1
a: CW 1 Clicks
   position: 2
a: CW 2 Clicks
   position: 4
a: CW 1 Clicks
   position: 5

This is actually a feature, not a bug. 8) Perhaps it should only have returned a positive or a negative, to indicate any movement since the last query(). But the idea is this:

You have a main routine loop() which takes an arbitrary amount of time to run. Asynchronously, it is interrupted by a rotary encoder going up and down. My thinking is that because of this disconnect, you can't perfectly track the turns of the encoder.

Say I've gone 3 clicks CW and 1 click CCW. Net is 2 clicks CW. Now the main routine does a query(); we discover that we have clicked net CW so we should act on that. Do you want to act on the fact that you've gone 2 clicks CW? Maybe- maybe not. Yes, you have gone CW, so the query() decrements the counter and now indicates 1 click CW. Maybe, while you're processing the first of the two CW clicks, the user actually turns the encoder CCW. Now you are at 0. Do you still want to register the second previous click? I don't know.

If your routine is fast enough, you can query() repeatedly, count the number of query()'s that get you to 0, and move on. But again, based on the disconnected nature of the interrupt and the main loop, I don't want to make any assumptions... that you can act on the current counters fast enough to catch any intermediate moves. If your main routine is slow enough to allow the internal adaencoder counter to accumulate to 3 or 4 clicks, I'm uncomfortable in assuming that it would be able to take the counter and perform 3 or 4 actions. I leave it up to you; drain the counter using repeated query()'s until you get to 0 (should be pretty fast) and then act on that information.

Thanks for the reply. I was wondering if it was something like that - a difference in perspective, rather than a bug. I would have taken the approach that by returning a signed number, you give the program all the information it needs (direction and the number of clicks,) so just reset 'clicks' and let the original program deal with the additional clicks beyond the first one, but I can see your approach as well - If the program is slow enough that multiple clicks occur between queries, than it's probably slow enough for your concerns to be an issue.

For my purposes, I doubt it will be an issue, and if it is, it won't cause any problems anyway.

Thanks for writing such a useful library!

Announcing version 0.8-rc1 of the AdaEncoder library. See Google Code Archive - Long-term storage for Google Code Project Hosting.

In this release, I have simple changed the license to the Apache-2.0 license. I also am using Git so the source is on Github, and I'm using Bintray for the zip file downloads. See the Google Code website (linked above) for more details.

Enjoy.

-GreyGnome