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.