AVR Quadrature Encoder

Interfacing a Quadrature Encoder

Quadrature encoders are commonly used for servo controls, user input, and anywhere rotational speed and distance or position are needed.

Quadrature Encoder

This is a quadrature encoder I picked up on ebay for under $8. It is unusual in that it is rated at 600 pulses per revolution, but actually has 2400 edges per revolution. The norm would be to rate it at 2400 counts per revolution. Counts per revolution assumes one is counting all four edges, and is the standard measure.

This encoder has a 6mm diameter shaft and multiple mounting hole patterns, including a standard 3-hole pattern. It features ball bearings for smooth rotation. The color code is in Chinese, but not hard to figure out.

The AVR used to interface to the encoder reads the two quadrature outputs, using both phases A and B for clocks, which provides 4 edges per cycle. All quadrature encoders of a few dozen counts per revolution or more are optical encoders, using very fine pitch optical disks as interrupters, and a pair of emitter/detectors spaced one half pulse off from each other.

Absloute Encoder vs. Incremental Encoder:

The majority of encoders in use are incremental encoders. They can only tell you where you are in relation to where you were earlier. There are other types of encoders, called absolute rotary encoders, and absolute linear encoders. They are called absolute because they keep track of the absolute position relative to some fixed starting point. Absolute encoders do not require you to tally pulses in order to determine position, but cost and complexity increase exponentially with the number of bits of resolution.

Notes:

Older CNC and NC machines used to have encoders with more than the two phases of the quadrature encoder. The multiple phases were actually clocks that drove the logic.

Quadrature encoders are incremental, and directional. They can tell us something moved, which direction, and how far. It is the how far part that people sometimes get wrong. The encoder is defined by the number of edges it has. A 1024 CPR encoder only has 256 pulses per phase per revolution. It is up to the firmware to count all of the edges in order to come up with the 1024 counts.

There are two ways to do it. One is to base the direction on the edge that just interrupted. That requires setting up an edge triggered external interrupt on phases A and B. The edge is determined by the state of each phase. When a phase is low, the edge interrupt is set to rising. When a phase is high, the edge interrupt is set to falling. In this way there will be an interrupt when either phase transisitions. The edge that interrupts is changed inside the interrupt routine to interrupt on the next transition.

The other way is to setup to interrupt on any edge at all, then look at the state of the two phases to determine direction. The way it is done in the first example is with INT0 and INT1.

Although the code takes up quite a few lines, only 4 lines are executed per transition. The main program loop has nothing to do but use the resulting position data. Adding a timer one could determine the counts per unit time, or velocity in RPM, arcseconds per second, or any other unit of measure. The maximum speed that can be run at 20MHz is 4300RPM with a 1024CPR encoder. The maximum practical speed, if the controller is running a single motor, would be around 3600RPM with a 1024CPR encoder. That is more than a typical application requires. If you are trying to run a two or more axis controller, you need to cut the maximum RPM or the encoder CPR accordingly. This is a good application for multi-processing, and I have run two ATtiny2313/ATtiny4313 MCU's - one for each axis - with an ATmega328 for the command processing, using SPI to communicate between master and slave.


#include <avr/io.h>
#include <avr/interrupt.h>

#define ENC_PORT PORTD
#define ENC_DDR  DDRD
#define ENC_A PIND2
#define ENC_B PIND3
#define ENC_CCW 0
#define ENC_CW 1

volatile uint8_t encoder_direction;
volatile int32_t encoder_count;
int32_t target_position = 2345678;

void init()
{
    ENC_DDR |= 0x00;                // Pins PD2 & PD3 are inputs.
    ENC_PORT |= 0x0C;               // Active pullups.
    MCUCR = (1<<ISC10) | (1<<ISC00);// Enable any edge.
    GIMSK = (1<<INT1) | (1<<INT0);  // Enable external interrupts.
    sei();                          // Enable global interrupts.
}

int main()
{
    int32_t last_count = 0;
    
    init();
    
    while (1)
    {
        // Has the encoder turned?
        if (last_count != encoder_count)
        {
            // encode_count is a moving target, so save it to work with it.
            last_count = encoder_count;
            if (last_count == target_position)
            {
                // Shut off motor or whatever...
            }
        }
    }
}

ISR(INT0_vect)
{
    if (ENC_B)
    {
        if (ENC_A)
        {
            encoder_direction = ENC_CCW;
            --encoder_count;
        }
        else
        {
            encoder_direction = ENC_CW;
            ++encoder_count;
        }
    }
    else
    {
        if (ENC_A)
        {
            encoder_direction = ENC_CW;
            ++encoder_count;
        }
        else
        {
            encoder_direction = ENC_CCW;
            --encoder_count;
        }
    }
}	

ISR(INT1_vect)
{
    if (ENC_B)
    {
        if (ENC_A)
        {
            encoder_direction = ENC_CW;
            ++encoder_count;
        }
        else
        {
            encoder_direction = ENC_CCW;
            --encoder_count;
        }
    }
    else
    {
        if (ENC_A)
        {
            encoder_direction = ENC_CCW;
            --encoder_count;
        }
        else
        {
            encoder_direction = ENC_CW;
            --encoder_count;
        }
    }
}	
            

The code initializes the external interrupts on the ATtiny2313, then goes into a loop checking for position changes. In the real world, the code would also communicate with a master which would tell it where to go next. A proportional motor control routine would then decide how to best get the motor to the destination.

The second example is written using pin change interrupts. They are good for this because they do exactly what we need: they interrupt on any toggling of the pin(s). The code is smaller, but it doesn't necessarily run faster.


#include <avr/io.h>
#include <avr/interrupt.h>

#define ENC_PORT PORTB
#define ENC_DDR  DDRB
#define ENC_A_MSK 0x01
#define ENC_B_MSK 0x02 
#define ENC_CCW 0
#define ENC_CW 1
#define ENC_FAIL 2
#define ENCODER_STATUS_FAULT 0x01

volatile uint8_t encoder_direction;
volatile int32_t encoder_count;
int32_t target_position = 2345678;
volatile uint8_t encoder_status = 0;
volatile uint8_t last_encoder;

//  Old-> 00   01   10   11
//  00    X    CW   CCW  X          
//  01    CCW  X    X    CW        
//  10    CW   X    X    CCW    
//  11    X    CCW  CW   X      

uint8_t encoder_codes[4][4] = {
    ENC_FAIL, ENC_CW, ENC_CCW, ENC_FAIL,
    ENC_CCW, ENC_FAIL, ENC_FAIL, ENC_CW,
    ENC_CW, ENC_FAIL, ENC_FAIL, ENC_CCW,
    ENC_FAIL, ENC_CCW, ENC_CW, ENC_FAIL
};

void init()
{
    ENC_DDR |= 0x00;                // Pins PB0 & PB1 are inputs.
    ENC_PORT |= 0x03;               // Active pullups.
    GIMSK = (1<<PCIE0);       // Enable pin change interrupts on port B.
    PCMSK0 = ENC_A_MSK | ENC_B_MSK  // Enable just the two pins
    last_encoder = PINB & (ENC_A_MSK | ENC_B_MSK); // Initialize the encoder state.
    sei();                          // Enable global interrupts.
}

int main()
{
    int32_t last_count = 0;
    
    init();
    
    while (1)
    {
        // Has the encoder turned?
        if (last_count != encoder_count)
        {
            // encode_count is a moving target, so save it to work with it.
            last_count = encoder_count;
            if (last_count == target_position)
            {
                // Shut off motor or whatever...
            }
        }
    }
}

ISR(PCINT0_vect)
{
    uint8_t encoder = PINB & (ENC_A_MSK | ENC_B_MSK);
    
    encoder_direction = encoder_codes[last_encoder][encoder];
    last_encoder = encoder;

    if (encoder_direction == ENC_CW)
    {
        --encoder_count;
        encoder_status &= ~ENCODER_STATUS_FAULT;
    }
    else if (encoder_direction == ENC_CCW)
    {
        ++encoder_count;
        encoder_status &= ~ENCODER_STATUS_FAULT;
    }
    else
    {
        encoder_status |= ENCODER_STATUS_FAULT; 
    }
}
            

The example above show the use of pin change interrupts and a state table for looking up the direction. The fault cases shown are encoder codes that are not possible - you cant go from 01 to 10 directly. An encoder fault can happen if a sensor fails, or if the drive speed and the encoder transitions are higher than the ATtiny2313 can service.

Fault tolerance is left to your imagination. Some applications have zero tolerance, others can handle "skipping" a count here or there. This method can handle pretty high speeds, though. A 1024CPR encoder at 1800RPM is only 30,720 interrupts per second.

See Also:
Arduino Board Logo

 

Arduino-Board is the go-to source for information on many available Arduino and Arduino-like boards, tutorials and projects.

Help and Support

Arduino-Board

Stay updated

Sign up if you would like to receive our once monthly newsletter.