Category Archives: Arduino

Rotary Encoder Routines for Teensy

In this post I describe some code I wrote to handle the input from an incremental rotary encoder connected to a Teensy. The code should work well also on an Arduino, provided the rotary encoder signals are connected to pins that have interrupt functionality.

A rotary encoder is great for many kinds of user interfaces. It physically looks like a potentiometer, but it is not limited to less than one turn. The output is digital, so it can relatively easily be interpreted by a micro controller, even one without a built-in analog to digital converter.

A rotary encoder.
A rotary encoder. Looks like a potentiometer, but it is digital.

The number of pulses per rotation can vary, but between 12 and 24 is common. An encoder contains two switches and if it is rotated clockwise, switch A closes before switch B for each “click” and if it is turned counter clockwise, switch B closes before switch A. This is called quadrature encoding since the two square waves are 90 degrees out of phase from each other.

By looking at the order in which the switches close, it is possible to determine in which direction the knob is turned. The figure below illustrates the waveforms, assuming the switches connects their pins to ground and that the two signals are otherwise pulled up to a positive voltage by pull-up resistors.

Quadrature waveforms from a rotary encoder.
Quadrature waveforms from a rotary encoder. “D” marks a detent or click state.

Below is a diagram showing how a rotary encoder and a push button (which is often integrated into the encoder) can be hooked up to a Teensy 3.1.

Encoder and button connected to pull-up resistors and a Teensy 3.1.
Encoder and button connected to pull-up resistors and a Teensy 3.1.

A problem when trying to use a rotary encoder as part of a user interface in an embedded application is that the pulses can be pretty short and it is important to not miss any, as that would mean missed rotations or even rotations detected in the wrong direction. Furthermore, like for almost all switches, there is also contact bounce that one must handle in some way. Typically, the processor has to do other things while also responding to input from the rotary encoder.

This means that it is often very hard or impossible to robustly interpret the signals from an encoder by polling the signals from within the main loop of the program. To get around this problem, one can instead use interrupt routines that are triggered when a signal from the encoder changes level. This is precisely what the code below does. It was written for a Teensy 3.1 (or 3.2), but should work on other platforms as long as the encoder signals are connected to pins with interrupt capability.

The following function call

attachInterrupt(digitalPinToInterrupt(rotAPin), ISRrotAChange, CHANGE);

sets up an the function “ISRrotAChange()” to be called every time the rotAPin changes state. This function in turn looks at the pin and sets the rotAval variable accordingly before calling the more complicated function UpdateRot() which contains a state machine that keeps track of what the rotary encoder is doing and increments or decrements the rotAcc (rotary encoder accumulator) variable when it has determined that the encoder has moved a notch. No debounce timer is necessary since it is pretty safe to assume that the bounces of one switch will have died out before the next switch will be activated.

The rotAcc variable is changed only after the second signal becomes activated (low) while the first signal is still active since it is pretty common that one signal is activated and later deactivated without the second signal becoming active. This happens when the operator rotates the knob a fraction of a notch and then lets it return to the original position without going all the way to the next notch.

The main loop must not look directly at the rotAcc variable since it can be changed at any point within the program by an interrupt. It is only safe to access it while interrupts are disabled, so accesses have to be surrounded by cli() (clear interrupt enable bit) and sei() (set interrupt enable bit) function calls. This is done in the GetRotAcc() function which thus provides a convenient way of safely looking at the value of the accumulator. The interrupt service routines (ISRs) themselves do not need to mess with cli() and sei() when accessing the interrupt state variables since interrupts are automatically temporarily disabled while servicing an interrupt.

Since the global variables used by the interrupt routines can be changed at any point within the course of the main program, they have to be be declared with the keyword “volatile” to prevent the compiler from making optimizations that assume that they behave like normal variables that will never magically change under the feet of the main program code.

Expand the Teensy code
[cpp] /* Interrupt driven rotary encoder routines.
   Target: Teensy 3.1
   
   Written by Per Magnusson, http://www.axotron.se
   v 1.0 2015-11-15
   This program is public domain.
*/

// Rotary encoder push button pin, active low
static const int pushPin = 16;
// Rotary encoder phase A pin
static const int rotBPin = 17;
// Rotary encoder phase B pin
static const int rotAPin = 18;

#define DEMO 1

// Rotary encoder variables, used by interrupt routines
volatile int rotState = 0;
volatile int rotAval = 1;
volatile int rotBval = 1;
volatile int rotAcc = 0;

void setup()
{
  Serial.begin(57600);

  pinMode(rotAPin, INPUT);
  pinMode(rotBPin, INPUT);
  pinMode(pushPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(rotAPin),
    ISRrotAChange, CHANGE);
  attachInterrupt(digitalPinToInterrupt(rotBPin),
    ISRrotBChange, CHANGE);
}

void loop()
{
  static int oldRotAcc = 0; // Remember previous state
  int newRotAcc;
 
  // Do not look directly at the rotary accumulator variable,
  // it must be done in an interrupt safe manner
  newRotAcc = GetRotAcc();

  if(!digitalRead(pushPin)) {
    // The button was pushed.
    // Button detection is not interrupt driven, but detection of the
    // rotary encoder movements goes on also in the busy way below.
    Serial.println("Button down");
    delay(10); // Debounce
    while(!digitalRead(pushPin))
      ;
    delay(10); // Debounce
    Serial.println("Button up");
  } else if(newRotAcc != oldRotAcc) {
    // The encoder was rotated at least one step.
    Serial.println(newRotAcc);
    oldRotAcc = newRotAcc;
  }
}

// Return the current value of the rotaryEncoder
// counter in an interrupt safe way.
int GetRotAcc()
{
  int rot;
 
  cli();
  rot = rotAcc;
  sei();
  return rot;
}

// Interrupt routines
void ISRrotAChange()
{
  if(digitalRead(rotAPin)) {
    rotAval = 1;
    UpdateRot();
  } else {
    rotAval = 0;
    UpdateRot();
  }
}

void ISRrotBChange()
{
  if(digitalRead(rotBPin)) {
    rotBval = 1;
    UpdateRot();
  } else {
    rotBval = 0;
    UpdateRot();
  }
}

// Update rotary encoder accumulator.
// This function is called by the interrupt routines.
void UpdateRot()
{
  // Increment rotAcc if going CW, decrement it if going CCW.
  // Do not increment anything if it was just a glitch.
  switch(rotState) {
    case 0: // Idle state, look for direction
      if(!rotBval) {
        rotState = 1;  // CW 1
      }
      if(!rotAval) {
        rotState = 11; // CCW 1
      }
      break;
    case 1: // CW, wait for A low while B is low
      if(!rotBval) {
        if(!rotAval) {
          rotAcc++;
#ifdef DEMO          
          // Remove this when not in demo mode
          Serial.print(" right ");
#endif
          rotState = 2; // CW 2
        }
      } else {
        if(rotAval) {
          // It was just a glitch on B, go back to start
          rotState = 0;
        }
      }
      break;
    case 2: // CW, wait for B high
      if(rotBval) {
        rotState = 3; // CW 3
      }
      break;
    case 3: // CW, wait for A high
      if(rotAval) {
        rotState = 0; // back to idle (detent) state
      }
      break;
    case 11: // CCW, wait for B low while A is low
      if(!rotAval) {
        if(!rotBval) {
          rotAcc–;
#ifdef DEMO
          // Remove this when not in demo mode
          Serial.print(" left ");
#endif
          rotState = 12; // CCW 2
        }
      } else {
        if(rotBval) {
          // It was just a glitch on A, go back to start
          rotState = 0;
        }
      }
      break;
    case 12: // CCW, wait for A high
      if(rotAval) {
        rotState = 13; // CCW 3
      }
      break;
    case 13: // CCW, wait for B high
      if(rotBval) {
        rotState = 0; // back to idle (detent) state
      }
      break;
  }
}
[/cpp]

 

In this demo version of the code, status information is sent back over the USB serial port so that a terminal window (e.g. the one in the Arduino environment) can display it. The texts “left” or “right” are printed by the ISR when it changes the value of rotAcc and the main loop prints the value of rotAcc when it detects that it has changed.

The main loop handles the push button. This is done through normal polling. A delay is used to filter out contact bounces. While the button is held down, the main loop is stuck and cannot print updated rotAcc values, but the ISR routines happily execute if the encoder is rotated, so the “left” and “right” strings can still appear while the button is pressed.

Here is an excerpt from a terminal session:

 right 1
 right 2
 right 3
 right 4
 right 5
 left 4
 left 3
Button down
 right  right  right  right  left  right Button up
7
 right 8
 right 9
 left 8
 left 7

The encoder was rotated clockwise (CW) five steps (right 1 to right 5) and then counter clockwise (CCW) by two steps (left 3). Then the button was pushed and held down (Button down) while the encoder was rotated four notches CW, one CCW and one CW  before the button was released. After this the main loop resumed and detected that the rotAcc value had changed to 7. Then the encoder was rotated two more notches CW and two CCW.

When not using this to demo the functionality of these specific routines, the Serial.print() statements in the interrupt routines should be removed, either by not #defining DEMO or by removing those lines of code completely.

More On Voltage Delay in Lithium Thionyl Chloride Batteries

I found the three year old Tadiran batteries (TL-5101/P) that i described in the previous blog post to have too high internal resistance to be suitable for use in Sportident base stations. The datasheet of those batteries also only talk about a discharge current of up to 2 mA and the base stations use more than that for peak current. Therefore I ordered new batteries of another brand, namely SAFT LS14250 CNA. The datasheet of SAFT LS14250 recommends a maximum discharge current of 35 mA and it has near full capacity at 10 mA of current, so this seems like a much better choice for the application.

Naturally, I was curious as to what the voltage delay looked like for LS14250, so I hooked up my battery tester with the same software as before. I ran two tests on the same battery with about 5 minutes of delay in between. The plots below shows the results.

Voltage vs time during 60 s while loading the SAFT LS14250 battery with 5 mA.
Voltage vs time during 60 s while loading the SAFT LS14250 battery with 5 mA.
Voltage vs time during 3 s while loading the SAFT LS14250 battery with 5 mA.
Voltage vs time during 3 s while loading the SAFT LS14250 battery with 5 mA.

In the first run, which takes place presumably at least many days (perhaps months or years) since the battery was last delivering any current), we see the voltage under load starting out at about 2.95 V and it recovers to 3.45 V after about 15 s.

In the second runt, the initial voltage under load is above 3.4 V and it peaks at almost 3.5 V after 1.5 s. It then sags down a bit, but stays about 15 mV above the first trace between 20 and 60 s.

So the voltage delay phenomenon is (as expected) very evident also in this battery model. Also, the SAFT LS14250 seems to be much more suited for the application than the Tadiran TL-5101/P.

Update on 2015-07-11:

I also needed to change batteries on some SI master (BSM7) units and these have AA-size (14500) batteries with higher capacity than the 1/2 AA size 14250 discussed above. I tested a new SAFT 14500 battery (which has  a highest recommended discharge current of 50 mA) twice with the 5 mA one-minute test. The results are shown in the plot below.

Voltage vs time during 60 s while loading the SAFT LS14500 battery with 5 mA.
Voltage vs time during 60 s while loading the SAFT LS14500 battery with 5 mA.

The voltage delay effect is evident also in this test, but strangely enough the curves look qualitatively different compared to the LS14250 curves. In the first run, the voltage dips during the first second before it starts to recover and reaches a peak after about 25 seconds followed by a slow decay. The initial dip is a new feature.

The second test of the same battery, ten minutes later, shows a quick recovery that peaks after three seconds after which the voltage slowly decays. After about 15 seconds, the voltage dips below that of the first run, unlike what happened when testing the LS14250 battery in which case the voltage during the second run stayed above that of the first run for the full minute.

The intricacies of battery behavior are apparently complicated, but tentatively one can conclude that a “voltage delay” effect that takes place for 1-15 seconds when the battery is being loaded after a (long) time of storage is repeatable based on the findings of these few tests.

Voltage Delay in Lithium Thionyl Chloride Batteries

As I described in a previous post, I built a simple Teensy-controlled battery tester for Lithium Thionyl Chloride batteries. I had noticed that unused batteries that had been laying around, seemed to have high internal resistance and according to Wikipedia, this can be due to a passivation layer that forms on the anode and which causes a “voltage delay” when put into service.

I decided to test this using the battery tester. What I did was to modify the program I had written for it so that it loaded the battery with a constant 5 mA current while monitoring how the pole voltage developed over time. I did this three times for one minute with a few minutes of pause in between for the same previously unused battery which has been stored for at least three years. The battery type is a 1/2 AA size Tadiran TL-5101/P.

Below are plots showing the how the pole voltage varied during the tests. The three curves shows the result of the initial test (red), second test ~20 minutes later (blue) and third test ~10 minutes after the second test. The first plot shows 60 seconds while the second plot zooms in on the first 3 seconds.

Voltage vs time during 60 s while loading the battery with 5 mA.
Voltage vs time during 60 s while loading the battery with 5 mA.
Voltage vs time during 3 s while loading the battery with 5 mA.
Voltage vs time during 3 s while loading the battery with 5 mA.

The pole voltage does indeed increase at first (during 3-10 seconds) while the battery is being loaded before it starts drooping. Also, the voltage under load becomes higher the second and third times the battery is tested in this manner.

So a very short and simple test under load might give a too pessimistic view of the state of a lithium thinoyl battery that has been stored for an extended period of time. It might recover and start perform better while it is being loaded. This is somewhat counterintuitive.

The Teensy program I used to for the tester can be found here.