Rotary Encoder Tutorial
Rotary Encoder Tutorial
Rotary Encoders are the modern digital equivalent of the potentiometer. They have taken over from the potentiometer for use in stereos and many other applications due to their robustness, fine digital control and the fact that they can fully rotate without end stops.
With a rotary encoder we have two square wave outputs (A and B) which are 90 degrees out of phase with each other. The number of pulses or steps generated per complete turn varies. The Sparkfun Rotary Encoder has 12 steps but others may have more or less. The diagram below shows how the phases A and B relate to each other when the encoder is turned clockwise or counter clockwise.
Every time the A signal pulse goes from positive to zero, we read the value of the B pulse. We see that when the encoder is turned clockwise the B pulse is always positive. When the encoder is turned counter-clockwise the B pulse is negative. By testing both outputs with a microcontroller we can determine the direction of turn and by counting the number of A pulses how far it has turned. Indeed, we could go one stage further and count the frequency of the pulses to determine how fast it is being turned. We can see that the rotary encoder has a lot of advantages over a potentiometer.
There are a number of methods we can use to read a rotary encoder with a microcontroller
- Use a port change interrupt
- Use the Capture Compare module to detect signal change. This is the best method if you also want to check how fast the encoder is being turned
- Use of a timer to check the values of A and B pulse x times per second
Lets make an application
We will now use the rotary encoder in the simplest of applications, we will use it to control the brightness of an led by altering a pwm signal. We will use the easiest method to read the encoder, that is the use of a timer interrupt to check on the values.
We will use the sparkfun encoder as discussed above. The first thing is to determine how fast we need our timer to operate. If you imagine that at best we could turn the encoder through 180 degrees in 1/10th of a second, that would give us 6 pulses in 1/10th second or 60 pulse per second. In reality its never likely to be this fast. As we need to detect both high and low values this gives us a minimum frequency of 120Hz. Lets go for 200Hz just to be sure. (Note: as these units are mechanical switches, there is the possibility of switch bounce. Using a fairly low frequency allows us to effectively filter out any switch bounce)
Each time our timer interrupt triggers, we compare the value of our A pulse with its previous value. If it has gone from positive to zero, we then check the value of the B pulse to see if it is positive or zero. Depending on the outcome we can increment of decrement a counter. We then use this to control the PWM value to increase or decrease the brightness of the LED
We will use the Microchip C18 compiler for this application along with an 18F14K22 chip. This is an arbitrary chip that just happened to be available. Is has a built-in clock so we need minimal components. The schematic is shown below
And the source code is shown below. You can also download it here
/*
Rotary Encoder Demo
Use a rotary encoder to control the brightness of an LED
Copyright HobbyTronics 2010
*/
#include <p18f14k22.h>
#pragma config FOSC = IRC
#pragma config WDTEN = OFF
#pragma config BOREN = OFF
#pragma config PWRTEN = ON
#pragma config MCLRE = OFF
#pragma config LVP = OFF
#pragma config HFOFST = OFF
#pragma config PLLEN = OFF
//Define Interrupt Locations
void hi_interrupt(void);
void lo_interrupt(void);
#pragma code high_vector_section=0x8
void high_vector (void){
_asm GOTO hi_interrupt _endasm
}
#pragma code low_vector_section=0x18
void low_vector(void){
_asm GOTO lo_interrupt _endasm
}
#pragma code
#define ENCODER_A PORTCbits.RC0 // Encoder A Pin
#define ENCODER_B PORTCbits.RC1 // Encoder B Pin
unsigned char encoder_counter=0; // Used to control brightness of LED, values 0 to 25 (we dont need 250 increments)
unsigned char encoder_A;
unsigned char encoder_B;
unsigned char encoder_A_prev=0;
unsigned char flag_set_pwm=1;
#pragma interruptlow lo_interrupt
void lo_interrupt(void){
// -----------------------
// Low Priority Interrupts
// -----------------------
if (INTCONbits.TMR0IF)
{
encoder_A = ENCODER_A;
encoder_B = ENCODER_B;
if((!encoder_A) && (encoder_A_prev)){
// A has gone from high to low
if(encoder_B) {
// B is high so clockwise
if(encoder_counter<25) encoder_counter++;
}
else {
// B is low so counter-clockwise
if(encoder_counter>0) encoder_counter--;
}
flag_set_pwm = 1; // Set flag to indicate change to PWM needed
}
encoder_A_prev = encoder_A; // Store value for next time
TMR0L = 178; // 200 Hz
INTCONbits.TMR0IF = 0; // Clear interrupt flag
}
}
#pragma interrupt hi_interrupt
void hi_interrupt(void)
{
}
void SetPWM(unsigned char pwm_width){
// set pwm values
// input of 0 to 25
// PWM output is on P1A (pin 5)
unsigned char pwm_lsb;
pwm_width*=10; // change value from 0-25 to 0-250
//10 Bits - 2 LSB's go in CCP1CON 5:4, 8 MSB's go in CCPR1L
pwm_lsb = pwm_width & 0b00000011; // Save 2 LSB
CCPR1L = pwm_width >> 2; // Remove 2 LSB and store 8 MSB in CCPR1L (only 6 bits as max duty value = 250)
pwm_lsb = pwm_lsb << 4; // Move 2 LSB into correct position
CCP1CON = pwm_lsb + 0b00001100; // duty lowest bits (5:4) + PWM mode
}
void main(void){
OSCCON = 0b01110010; // Int osc at 16 MHz
RCONbits.IPEN = 1; // Enable interrupt priority
INTCONbits.PEIE = 1; // interrupts allowed
INTCONbits.GIE = 1;
INTCONbits.GIEH = 1;
INTCONbits.GIEL = 1;
T0CON = 0b11000111; // 8 bit timer, prescaler 1:256, TMR0 on
TMR0L = 178; // 200Hz
INTCON2bits.TMR0IP = 0; // interrupt priority 0
INTCONbits.TMR0IE = 1; // timer0 interrupt enabled
// Clear the peripheral interrupt flags
PIR1 = 0;
ANSEL=0; // Digital
ANSELH=0; // Digital
ADCON0=0; // A2D Off
CM1CON0=0; // Comparators off
CM2CON0=0; // Comparators off
TRISA = 0b00000000; // Set Ports
TRISB = 0b00000000; //
TRISC = 0b00000011; // Encoder inputs on RC0 and RC1
/*
* PWM Register Values
* Oscillator Frequency Fosc = 16000000
* Clock Frequency Fclk = 4000000
* PWM Freq = 250 - allows us to use a duty value of 0 to 250
* Prescaler Value = 16
* Postscaler Value = 16
* PR2 = 62
* Maximum duty value = 250
*/
T2CON = 0b01111111; // prescaler postscaler to give 250Hz + turn on TMR2;
PR2 = 62; // gives 250Hz
CCPR1L = 0b00000000; // set duty MSB - initially 0 - off
CCP1CON = 0b00001100; // duty lowest bits + PWM mode
while(1)
{
if(flag_set_pwm) {
//Set PWM values
SetPWM(encoder_counter);
}
}
}