Translate

Saturday, July 14, 2018

MSP430 - Reading a digital encoder

Already exists a lot of documentation about this, but just for fun, I started from the scratch and did some analysis.
I did find very interesting issues about this "common" component.



Theory of operations


Let see how to read a rotary digital encoder with a MSP430.

First of all, what is a rotary digital encoder ?
It is a mechanical or optical device, capable to generate  two digital signals that can be used to determine if the encoder shaft is turned and if is turned clockwise or counter-clockwise.
There are many different types of encoders, on this article I'll discuss about the common ones used as substitute of trimmer/potentiometers , incremental digital rotary encoders.

This type of encoders returns two digital signals, called A and B, based on the  Gray code (called also quadrature code on some data sheets).
The signals generated by the encoder,  allows only 1 bit at a time to change state.
If applied to two signals, let's call them A and B, we have this truth table:

A  B
____
0   0
0   1
1   1
1   0

The idea is to read the signals generated by the encoder and compare them with the previous reading in order to determine if the encoder was turned clockwise or counterclockwise.
That assuming of course that the micro is capable to detect every single turn, i.e. one condition is that the micro must be able to detect the 1 bit change on the code.
The key to deal with this component is "when" read the signals.

Let translate the table in a diagram:

Figure 1 - A/B signals

The vertical dotted lines represent a change.
So if we start to read between the first two dotted lines, we'll read 1 0.
If the next reading is 0 0 it means that the shaft was turned clockwise.
If the next reading is 1 1, the shaft was turned counter-clockwise.

It is apparent that in order to "read" the encoder, we need to determine that the encoder changed state comparing the last reading with the current one.
That imply to:
  • detect the change
  • store the previous reading
  • deal with missed readings

Type of encoders

There are many different types of encoder, but also among the "incremental" type there are different ones.
Here the main parameters that can be different among models and types.
  • Pulse
    The number of changes in the signals when turning the shaft of 360 degrees.
  • Detent
    The number of mechanical steps in 360 degrees. A step is the physical lock of the shaft after the movement, it gives the feeling touch of rotating the shaft and keep it in a precise position when not turned
  • Idle signals
    How the A/B signals are when the encoder is in idle/stop (i.e. not turning) position

Detect the change


To detect the change in the signals is the key to read correctly the encoder.
The system must be capable to detect changes in the A/B signals, both of them.
The timing involved here are quite big, we are talking about milliseconds between the A and B signals.

The line 1 is the Encoder A signal, line 2 Encoder B, the line 3 is the debug signal, indicating the interrupt.
The lone interrupt in the middle screen is probably caused by a spike, not visible at that scale.
Before to analyze how to read the encoder, let see a basic test circuit to be used.


The idea is to have two LEDs, to indicate the count and the direction.
The test program will turn ON and OFF the Red LED (P1.0) to indicate a change clockwise.
The Green LED (P1.6) will turn ON and OFF to indicate a change counter-clockwise.
The LED used are the default ones on the Launchpad board, this why the choice to have the LEDs on P1.0 and P1.6.

Reading the encoder


When reading a quadrature encoder, is important to remember that it should be read ONLY when there are signal changes.
Reading the encoder when is in a detent state, i.e. without any movement, gives useless results.
This because depending by the encoder, the two signals (A and B) can likely be the same, both "high" or both "low".

Because this requirement, the best way to detect a signal change is to use the I/O interrupt.
Usually an I/O interrupt can be set to react to change to signal from low to high or viceversa.
What is best it depends about the specific encoder used.

During the tests I used an Alps encoder (probably equivalent to the EC11 type) and a Bourns PEC11 type.
The main difference between the two, is on the state of the A/B signals in the detent state (see below).

The idea behind reading the encoder is simple.
We have two signals coming out from the encoder, usually called A and B.
Between the two signals there is a delay , so that the two signals raise or go down not at the same moment.
An interrupt is generated on "any" of the two signals, so we'll have one interrupt for every signal change.
The first interrupt indicates a change on one of the two signals.
Every time an interrupt happens, we are going to read both signals.

Then we need to compare the previous reading, i.e. the value of the signals to the previous read, and determine if the shaft was turned clockwise or counterclockwise.
It is important to remember:

  • we will have TWO interrupts for every change state
    Each change of state affects both signals
  • the interrupts are triggered on the raise or descend of the signal
    Usually the descend is used, i.e. when the signal goes from high to low.
    Depending how the signals are handled by the encoder, is possible to "lose" one reading.
    For example the Alps toggle between low and high every detent, i.e. the level in the detent state is
    The Bourns always have the A/B signals high after the variations.
    Because of this the main effect is that the Alps encoder "lose" one step because at one turn starts with the signals low and thus the interrupt are not triggered.
This is an example of code I wrote :

/*
 *  digenctest.c 
 *  Copyright (c) 2015 - TheFwGuy
 *
 *  This code is to read a rotary digital encorder 
 *  The board used is a LaunchPad, using a MSP430F2012 and the code is 
 *  compiled using MSPGCC 4.9 using the -mmcu=msp430x2012  parameter
 *
 *  The code reads a rotary digital encoder and uses the two LED on board
 *  to indicate the status.
 *  The red LED will change state at every count, i.e. every time the 
 *  shaft is turned.
 *  The green LED will indicate the direction. ON clockwise, OFF counterclockwise
 *
 *  This program uses the timer to generate the PWM !
 */
#include<msp430.h>
#include <signal.h>

#define OFF 0
#define ON 1

/*
 *  Set I/O
 *   P1.0  -> Count indicator(OUTPUT - red LED)
 *   P1.1  -> Debug - output
 *   P1.2  -> PWM - output
 *   P1.3  -> Encoder SW (pushbutton) - input
 *   P1.4  -> Encoder A - input
 *   P1.5  -> Encoder B - input
 *   P1.6  -> Direction indicator (OUTPUT - green LED)
 *   P1.7  -> not used - input
 */

#define LED_PORT P1OUT
#define  ENCODER_PORT P1IN

#define COUNT_LED    BIT0
#define INDICATOR_LED  BIT6

#define ENCODER_A BIT4
#define ENCODER_B BIT5
#define ENCODER_SW BIT3
#define  DEBUGSIG BIT1
#define  PWM BIT2 /* Must be this one for the PWM !! */

#define ENCODER_MASK (ENCODER_A+ENCODER_B)
#define ENCODER_SHIFT 4 /* Number of shift to normalize the reading */

/*
 *  I/O managment defines
 */
#define COUNT_ON  Port1Shadow |= COUNT_LED
#define COUNT_OFF Port1Shadow &= ~COUNT_LED
#define COUNT_TOGGLE Port1Shadow ^= COUNT_LED

#define DIRECTION_CLOCKWISE  Port1Shadow|= INDICATOR_LED
#define DIRECTION_ANTICLOCKWISE  Port1Shadow &= ~INDICATOR_LED
#define DIRECTION_TOGGLE Port1Shadow ^= INDICATOR_LED

/*
 *  The macro DEBUG_TOGGLE update both the shadow and the port at the same time
 */
#define DEBUG_TOGGLE Port1Shadow ^= DEBUGSIG; P1OUT ^= DEBUGSIG

/*
 *  Encoder related defines
 */
#define CLOCKWISE 0
#define ANTICLOCKWISE 1

#define ENCODER_NOCHANGE 0
#define ENCODER_INCREMENT 1
#define ENCODER_DECREMENT 2
#define ENCODER_ERROR 3

/*
 *  PWM managment defines
 */
#define PWMSTART  22   /* original 30 */  
#define PWMSTOP   74  /* original 64 */  

/*
 *  Timer related defines
 */
#define TMRVALUE 660 /* Orig 330 */

/*
 *  Global variables
 */
unsigned short EncCounter  = PWMSTART;

unsigned char  Pushbutton  = 0;
unsigned char  Port1Shadow = 0;
unsigned char  TogglePosition = 0;
/*
 *  Function prototypes
 */
void init(void);
void read_encoder();

/*
 ****************************** Start Code *******************************
 */

int main(void) 
{
init(); /* Initialize the system */
while(1)
{
if(Pushbutton == ON)
{
DEBUG_TOGGLE;
switch(TogglePosition)
{
default:
case 0:
      EncCounter = PWMSTART;
TogglePosition = 1;
break;
 
case 1:
      EncCounter = PWMSTOP;
   TogglePosition = 0;
   break;
}
Pushbutton = OFF;
DEBUG_TOGGLE;
}

TA0CCR1 = EncCounter;

    /*
    *  Update the Output port 
    */
   LED_PORT = Port1Shadow;
}
}

/**
 *  @fn Init
 *  @author TheFwGuy
 *  @brief Init clock, timer and initialize variables
 *
 *  @param None
 *  @return None
 */
void init(void)
{
   WDTCTL = WDTPW + WDTHOLD;  /* Stop watchdog timer */

   /*
    *  Set DCO
    */
   DCOCTL  = CALDCO_16MHZ; /* Set MCLK and SMCLK to DCO */
   BCSCTL1 = CALBC1_16MHZ;  /* Set up 16 Mhz using internal calibration value */

BCSCTL1 |= DIVA_0; /* ACLK */
   BCSCTL3 |= XCAP_3; /* 12.5pF cap- setting for 32768Hz crystal */
P1SEL |= PWM; /* Assign PWM to timer output */
P1DIR |= PWM;
P1OUT = 0;           /* Force out low */

   P1DIR |= COUNT_LED+INDICATOR_LED+DEBUGSIG;  /* Set outputs */
P1REN = ENCODER_MASK;  /* Enable pull up resistor on Encoder A/B input */
P1IE  = ENCODER_MASK+ENCODER_SW; /* Interrupt enabled on Encoder A/B input and switch */
P1IES = 0x00; /* Interrupt on low to high transiction */

   /*
    *  Set variables
    */
EncCounter = PWMSTART; 
Port1Shadow = 0;              /* Force all Output to zero */

   /*
    *  Set Timer
    */
TA0CCR0 = TMRVALUE;
TA0CCTL1 |= OUTMOD_7;
TA0CCR1 = EncCounter;
TA0CTL |= TASSEL_1 + MC_1; /* Uses ACLK */

   _BIS_SR(GIE);              /* Enable interrupt */
}

/**
 *  @fn read_encoder
 *  @author TheFwGuy
 *  @brief Read the encoder
 *    This function is called under I/O interrupt everytime the A or the B signals
 *    change state from low to high.
 *    The previous_reading variable holds the previous SIGNAL reading, NOT the
 *    the previous Gray code readout !
 *    
 *
 *  @param None
 *  @return None
 */
void read_encoder()
{
   static unsigned char previous_ab_reading = 0;
unsigned char reading;
unsigned char ab_reading;
unsigned char action = ENCODER_NOCHANGE;
/*
*  Encoder reading
*/
if(P1IFG & (ENCODER_MASK))
{
__delay_cycles(6000);

reading = ENCODER_PORT; /* Acquire A and B */

/*
*  Normalize the reading bringing the A nad B signals to position 0 and 1
*/
ab_reading = (reading & ENCODER_MASK)>>ENCODER_SHIFT;
if(ab_reading == 0x3)
{
if(previous_ab_reading == 0x1)
{
action = ENCODER_INCREMENT;
COUNT_TOGGLE;
DIRECTION_CLOCKWISE;
}
else if(previous_ab_reading == 0x2)
{
action = ENCODER_DECREMENT;
COUNT_TOGGLE;
DIRECTION_ANTICLOCKWISE;
}
}
previous_ab_reading = ab_reading;

/*
*  Counter management
*  The counter is allowed to count from 0 to PWMSTOP only
*/
switch(action)
{
case ENCODER_INCREMENT:
if(EncCounter < PWMSTOP)
EncCounter++;
break;
case ENCODER_DECREMENT:
if(EncCounter > PWMSTART)
EncCounter--;
break;
}

/*
 *  Trying to change interrupt direction 
 */
#if 0
      if(P1IES & (ENCODER_MASK))
{
      P1IE  = ENCODER_MASK+ENCODER_SW; /* Interrupt enabled on Encoder A/B input and switch */
   P1IES = 0x00; /* Interrupt on low to high transiction */
}
else
{
      P1IE  = ENCODER_MASK+ENCODER_SW; /* Interrupt enabled on Encoder A/B input and switch */
P1IES = ENCODER_MASK;
}
#endif
}
/*
*  Pushbutton reading
*/
if(P1IFG & ENCODER_SW)
{
//DEBUG_TOGGLE;
__delay_cycles(6000);
Pushbutton= ON;
//DEBUG_TOGGLE;
}
__delay_cycles(6000);
P1IFG = 0x00;
}

/*
 *  Port1 interrupt vector
 */
__attribute__((__interrupt__(PORT1_VECTOR)))
void Port1int (void)
{
//   DEBUG_TOGGLE;
read_encoder();
//   DEBUG_TOGGLE;
}





4 comments:

  1. Hi @TheFWGuy, by chance, do you have the code for this?

    ReplyDelete
  2. Hi, do you have or do you provide the code for this project of yours?

    ReplyDelete
    Replies
    1. Hi, I did add a code I wrote years ago in the article.
      Honestly I don't remember if is working, I guess so, but at least is a start.
      Have fun :)

      Delete