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);
}
}
Thanks!
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.
i can’t see any code for debounce in you program.
That’s correct. I did that in hardware.