Interface a rotary encoder with Atmel AVR (interrupt style)

When I wanted to gently interface an incremental rotary encoder with an 8-bit Atmel AVR microcontroller, I couldn’t find a nice example. That’s why I wrote this little text. Many other howtos, manuals and descriptions are fuzzy, incomplete or too complicated to do the (easy) job.

This article describes three things: the hardware (just the minimal hardware) and two pieces of code. The first is just a simple interrupt based program and the other uses timers to make bigger steps when you turn faster.

I use Atmel AVR studio as IDE and the Through hole USBprog to program the microcontroller.

Hardware

First the hardware. I use a alphanumeric display on my development system, but it’s not displayed here. The rotary encoder (a Bourns PEC11) uses a two bit gray-code. It goes through one complete cycle each detent.

(click to see the bigger pdf-version)

Software (simple)

This software is quite easy.

#include
#include <avr/interrupt.h>
#include <util/delay.h>
#include "lcd.h"
#include "lcd.c"

// I use a 4x40 alphanumeric display which has two controllers
// I modified the LCD driver to support two controllers
// Set the two enable pins of the LCD here
#define LCD_ENABLE_TOP 5
#define LCD_ENABLE_BOTTOM 7

#define F_CPU 8000000UL

// Some declarations
char str[4];
int enc = 1000;

// Subroutine declarations
void initInterrupts(void);

// The Interrupt Service Routine for external INT1
ISR(INT1_vect)
{
// When an interrupt occurs, we only have to check the level of
// of pin PD5 to determine the direction
if (PIND & _BV(PD5))
// Increase enc
enc++;
else
// Decrease enc
enc--;
}

// Routine to setup INT1
void initInterrupts(void)
{
// Assure that pin PD3 (INT1) and PD5 are inputs
DDRD &= ~(1<<PD5);
DDRD &= ~(1<<PD3);

// Enable the pull-up resistors
PORTD |= (1<<PD5)|(1<<PD3);

// Falling edge in INT1 (PD3 / pin17) to cause interrupt
MCUCR |= (1<<ISC11);

// Enable and INT1
GICR |= (1<<INT1);
}

// Main function
int main(void)
{
// Set all the pins en registers
initInterrupts();

// Turn on interrupts
sei();

//Select the top two rows of the display
setEnable(LCD_ENABLE_TOP);
// Initialize display, cursor off
lcd_init(LCD_DISP_ON);
// Clear display and home cursor
lcd_clrscr();

//Select the bottom two rows of the display
setEnable(LCD_ENABLE_BOTTOM);
// Initialize display, cursor off
lcd_init(LCD_DISP_ON);
// Clear display and home cursor
lcd_clrscr();

//Select the top two rows of the display
setEnable(LCD_ENABLE_TOP);

// Put a test string on the display
lcd_gotoxy(0,0);
lcd_puts("Encoder test with interrupt");

// Infinite loop
while (1)
{
// Write the value of enc to the LCD
lcd_gotoxy(0,1);
lcd_puts(" ");
lcd_gotoxy(0,1);
lcd_puts(itoa(enc, str, 10));

// Wait some milliseconds
_delay_ms(10);
}
}

Software (with timers)

#include <stdlib.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include "lcd.h"
#include "lcd.c"

// I use a 4x40 alphanumeric display which has two controllers
// I modified the LCD driver to support two controllers
// Set the two enable pins of the LCD here
#define LCD_ENABLE_TOP        5
#define LCD_ENABLE_BOTTOM    7

#define F_CPU 8000000UL

// Some declarations
char str[4];
int enc = 1000;
unsigned int timercounter = 0;

// Subroutine declarations
void initInterrupts(void);
void TimerInit(void);

// The Interrupt Service Routine for external INT1
ISR(INT1_vect)
{
// When an interrupt occurs, we only have to check the level of
// of pin PD5 to determine the direction.
// We must also evaluate timercounter for bigger steps when the encoder
// is turned faster.

if (PIND & _BV(PD5) && (timercounter > 25))
{
if (timercounter > 50)
enc += 10;
else
enc += 250;
}
else if (!(PIND & _BV(PD5)) && (timercounter > 25))
{
if (timercounter > 50)
enc -= 10;
else
enc -= 250;
}

timercounter = 0;
}

// Timer1 compare A interrupt every 10ms
ISR(TIMER1_COMPA_vect)
{
// Reset the counter first
TCNT1 = 0;

// If timercounter is less than 1000, increase it. This way we prevent
// it to overflow. We don't need to time something longer than a second.
if (timercounter < 1000)
timercounter++;
}

// Routine to setup INT1
void initInterrupts(void)
{
// Asure that pin PD3 (INT1) and PD5 are inputs
DDRD &= ~(1<<PD5);
DDRD &= ~(1<<PD3);

// Enable the pull-up resistors
PORTD |= (1<<PD5)|(1<<PD3);

// Falling edge in INT1 (PD3 / pin17) to cause interrupt
MCUCR |= (1<<ISC11);

// Enable and INT1
GICR |= (1<<INT1);
}

void TimerInit(void)
{
// We'll use timer1 for this. It's a 16 bit counter.

// Set the prescaler (clk/64)
TCCR1B |= _BV(CS10);
TCCR1B |= _BV(CS11);

// 125 ticks -> every 1 ms a Timer 1 Compare match A
OCR1A = 125;

// Clear the OCF1A flag
TIFR |= _BV(OCF1A);

// Enable Timer1 compare match A interrupt
TIMSK |= _BV(OCIE1A);
}

// Main function
int main(void)
{
// Set all the pins en registers
initInterrupts();
TimerInit();

// Turn on interrupts
sei();

//Select the top two rows of the display
setEnable(LCD_ENABLE_TOP);
// Initialize display, cursor off
lcd_init(LCD_DISP_ON);
// Clear display and home cursor
lcd_clrscr();

//Select the bottom two rows of the display
setEnable(LCD_ENABLE_BOTTOM);
//  Initialize display, cursor off
lcd_init(LCD_DISP_ON);
// Clear display and home cursor
lcd_clrscr();

//Select the top two rows of the display
setEnable(LCD_ENABLE_TOP);

// Put a test string on the display
lcd_gotoxy(0,0);
lcd_puts("Encoder test with interrupt");

// Infinite loop
while (1)
{
// Write the value of enc to the LCD
lcd_gotoxy(0,1);
lcd_puts("       ");
lcd_gotoxy(0,1);
lcd_puts(itoa(enc, str, 10));

// Wait some milliseconds
_delay_ms(10);
}
}

4 thoughts on “Interface a rotary encoder with Atmel AVR (interrupt style)

  1. Pretty simple code. Thank you.
    Which AVR is this written for?
    The first line of the ISR code is “#include” and should be “#include “.

    Peace and blessings.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.