Monday, August 26, 2013

RGB Rotary Encoder with PWM and ISRs Using an ATmega328

Description

A long time ago I bought a couple RGB rotary encoders from Sparkfun because they were cheap and I was already spending a bunch on other stuff.  I thought they would be neat for some interfaces since it includes a push button.  The interrupt service routine (ISR) for pin changes on the rotary encoder (for terminals A and B, which will from this point be referred to as AB) can be found in this post (Oleg covers this topic in good detail, so I will not be recapping much on pin change interrupts).  We will also need to add an external interrupt for the button.  Lastly we will configure the PWM for the RGB LED that is contained withing the encoder.  (I also have three separate LEDs set up to demonstrate the the actions from the rotary encoder: forward, backward, and push button)

Oleg, in one of his earlier posts, explains how to use a rotary encoder with a look up table.  Basically the principle works by polling the current port and comparing to the previous reading.  By comparing the previous states of AB to the current we can determine forward or reverse motion.  For more detail see Oleg's post.

Parts

You will need an RGB rotary encoder (or, as you can use three LEDs if you do not have an RGB rotary encoder), three LEDs/resistors to observe correct functionality, and an Arduino or an ATmega328 (or comparable) mcu.

Circuits

The following was taken from the datasheet for the RGB rotary encoder:
This is the suggested R/C filter from the datasheet to help avoid chatter (bounce).  There is also a timing diagram in there that describes a time period that would mask chatter.  Although the R/C filter should help alleviate chatter the chatter timing mask they mention is only 3ms, so a little chatter should not really effect anything (depending on what your ISR will do).
A and B will be connected to the first two pins on whatever port we choose to use (if the first two pins aren't chosen you will have to change the way that you read the port and compare your last values).  Terminal C goes to ground.

This next diagram comes from the dimensional drawing:
In this picture note that pin 5 is a common anode and pin three is for the switch/button.


Left is from the dimensional drawing of the pins. Right is the actual dimensional drawing of the encoder.
A resistor is needed between each LED and its corresponding output from the ATmega328.  We also need a pull-down resistor on the switch.  NOTE: The switch is very easily destroyed with heat.  It will most likely break if you continue to heat it up and move it around when soldering.  Figure out where you want it and leave it there.

The three LEDs that are not part of the encoder are for testing/demonstration.
 If you are using an Arduino ignore the crystal, caps, and switch on the left (Here is a diagram of the uno with pinouts describing how they relate to the ATmega328. It's well done).
This is the circuit.  Ignore the raspberry pi connector on the right as it has nothing to do with this.
The small area directly below the encoder is the R/C filter. The status LEDs, for forwards, backwards, and button, are directly to the left of the ATmega.  Refer to the wiring diagram above for the connections.

The Program

#include <stdlib.h>
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
// #include "lcd.h"  // custom lcd library

/*
 * change these based on the chip.
 * this should work for most atmel chips
*/
// actual encoder stuff
#define ENC_CTL  DDRB
#define ENC_WR  PORTB
#define ENC_RD  PINB
#define ENC_VECT PCINT0_vect
#define ENC_PCI  PCMSK0
#define GI_MASK  PCICR
#define INT_A  PCINT0
#define INT_B  PCINT1
#define ENC_ENAB PCIE0
#define ENC_A  0
#define ENC_B  1

// encoder button stuff
#define BUT_CTL  DDRD
#define BUT_WR  PORTD
#define BUT_RD  PIND
#define BUT_VECT INT0_vect
#define BUT_EI  EICRA
#define EI_MASK  EIMSK
#define INT_BUT  ISC00
#define BUT_ENAB INT0
#define SWITCH  2

// output for checking
#define COMM_CTL DDRD
#define COMM_WR  PORTD
#define FOWARD  4
#define REVERSE  3
#define BUTTON  5
#define ENC_DELAY 50
#define BUT_DELAY 300

// led stuff
// control
#define R_CTL  DDRD
#define R_WR  PORTD
#define BG_CTL  DDRB
#define BG_WR  PORTB
// RED
#define RED   6
#define RED_TMA  TCCR0A
#define RED_TMB  TCCR0B
#define RED_OC  OCR0A
// BLUE
#define BLUE  3
#define BLUE_TMA TCCR1A
#define BLUE_TMB TCCR1B
#define BLUE_OC  OCR1B
// GREEN
#define GREEN  2
#define GREEN_TMA TCCR2A
#define GREEN_TMB TCCR2B
#define GREEN_OC OCR2A

/* forward declarations */
void togglePin(uint8_t pin, uint16_t delay);
void initPWM(void);
void initInterrupts(void);
ISR(ENC_VECT);
ISR(BUT_VECT);

/* main */
int main(void)
{ 
 // set leds to output
 COMM_CTL |= (( 1<<FOWARD )|( 1<<REVERSE )|( 1<<BUTTON ));
 
 // set up interrupts
 initInterrupts();
 
 // start uart (this is here for debugging
 // purposes and can be removed if not sending
 // anything to uart
 // uart_init();
 // clear();
 
 // set up PWM
 initPWM();
 
 // just to demonstrate that it's working
 RED_OC = 0xFF;
 _delay_ms(1000);
 RED_OC = 0x00;
 BLUE_OC = 0xFF;
 _delay_ms(1000);
 BLUE_OC = 0x00;
 GREEN_OC = 0xFF;
 _delay_ms(1000);
 GREEN_OC = 0x00;
 
 uint8_t brightness = 0;
 
 // cycle through the colors
 for(;;) {
  // from RED to BLUE
  for(brightness = 0; brightness < 0xFF; brightness++) {
   BLUE_OC = brightness;
   RED_OC = 0xFF - brightness;
   _delay_ms(10);
  }
  // from BLUE to green
  for(brightness = 0; brightness < 0xFF; brightness++) {
   GREEN_OC = brightness;
   BLUE_OC = 0xFF - brightness;
   _delay_ms(10);
  }
  // from green to RED
  for(brightness = 0; brightness < 0xFF; brightness++) {
   RED_OC = brightness;
   GREEN_OC = 0xFF - brightness;
   _delay_ms(10);
  }
 }
 
 return 0;
}

/* functions */
/* toggles pin for delay ms */
void togglePin(uint8_t pin, uint16_t delay)
{
 COMM_WR |= ( 1<<pin );
 _delay_ms(delay);
 COMM_WR &= ~( 1<<pin );
}

/* sets up PWM for RGB */
void initPWM(void)
{
 // select ouput compare mode, fast PWM, no prescaler
 // compare mode = inverting
 // clear at bottom, set on match
 RED_TMA |= (( 3<<COM0A0 )|( 3<<WGM00 ));
 RED_TMB |= ( 1<<CS00 );
 BLUE_TMA |= (( 3<<COM1B0 )|( 1<<WGM10 ));
 BLUE_TMB |= (( 1<<CS10 )|( 1<<WGM12 ));
 GREEN_TMA |= (( 3<<COM2A0 )|( 3<<WGM20 ));
 GREEN_TMB |= ( 1<<CS20 );
 
 // set as outputs
 R_CTL |= ( 1<<RED );
 BG_CTL |= (( 1<<BLUE )|( 1<<GREEN ));
 
 
 // initialize duty cylce
 RED_OC = 0x00;
 GREEN_OC = 0x00;
 BLUE_OC = 0x00;
}

/* set up interrupts for encoder */
void initInterrupts(void)
{
 // set modes and enable pullups on encoders
 ENC_WR |= (( 1<<ENC_A )|( 1<<ENC_B ));
 
 // enable pins as interrupt source
 // enable pin change interrupts
 ENC_PCI |= (( 1<<INT_A )|( 1<<INT_B ));
 GI_MASK |= ( 1<<ENC_ENAB );
 
 // enable external interrupt for button (rising)
 BUT_EI |= ( 3<<INT_BUT );
 EI_MASK |= ( 1<<BUT_ENAB );
 
 // turn on interrupts
 sei();
}

/* encoder interrupt routine */
ISR(ENC_VECT)
{
 static uint8_t old_AB = 3;  //lookup table index
 static int8_t encval = 0;   //encoder value  
 static const int8_t enc_states [] PROGMEM = 
  {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};  //encoder lookup table
 /**/
 old_AB <<=2;  //remember pREVERSEious state
 old_AB |= ( ENC_RD & 0x03 );
 encval += pgm_read_byte(&(enc_states[( old_AB & 0x0f )]));
 
 if (encval > 3) {
  encval = 0;
  togglePin(FOWARD, ENC_DELAY);
 }
 else if (encval < -3) {
  encval = 0;
  togglePin(REVERSE, ENC_DELAY);
 }
}

/* encoder button interrupt routine */
ISR(BUT_VECT)
{
 //togglePin(BUTTON, BUT_DELAY);
 
 COMM_WR |= ( 1<<BUTTON );
 while ( BUT_RD & ( 1<<SWITCH )) {
 }
 COMM_WR &= ~( 1<<BUTTON );
}
Interrupts
We use pin change interrupts for reading the encoder and a rising edge external interrupt on the button.  The general flow of setting up an interrupt is as follows (this is from the ATmega328 datasheet, so if you are looking for a little bit more information check there; if you really want to learn all about interrupts check this out as well):
  1. #include <avr/interrupt.h>
  2. Write to the interrupt control register (ICR) to enable interrupts on enabled pins
    1. Setting bits 0-3 (the first two control one interrupt, the second two control another;  this is why arduino uno only has two interrupts) on the EICRA (external interrupt register) controls the type of action that will trigger an interrupt service routine (rising edge, falling edge, bidirectional, or low level)
      1. 00 - interrupt request on low level
      2. 01 - bidirectional trigger (any pin change)
      3. 10 - falling edge
      4. 11 - rising edge
    2. Setting bits 0-2 on the PCICR (pin change interrupt register) determines what group of pins will be monitored for a pin change (interrupts are only generated if the pin is selected in the corresponding pin change mask register (PCMSKx)
      1. PCIE0 (pin change interrupt enable) controls pin changes to port B pins
      2. PCIE1 controls pin changes to port C pins
      3. PCIE2 controls pin changes to port D pins
  3. Write to the interrupt mask register to enable interrupts on specific pins.  
    1. Setting bit 0 on the EIMSK enables the interrupt as defined in step 1.1 on pin D2.  Setting bit 1 enables the interrupt on pin D3.
    2. Setting bits on the PCMSKx enables the corresponding pin on that port.  So setting bit 2 on PCMSK1 enables pin change interrupt requests for pin C2 (as long as PCIE2 is set in the PCICR).
  4. Turn on all interrupts by calling sei() (If you need to disable all interrupts call cli()).
  5. Define your actual interrupt service routine (what happens when the interrupt is actually generated).
    1. The format is ISR(interrupt_vect) { // some code }, where interrupt is, well, the name of the interrupt (INT0, INT1, PCINT1, PCINT2, etc.).
  6. Test the interrupt.  Use a barebones project, set up your interrupt, have it enter a never-ending for loop and test the interrupt.
The iniInterrupts() function in the code above sets up everything except for the ISRs.  There are two separate interrupts for this setup, one for the encoder and one for the button.

PWM
There are three timers (one 8-bit, and one 16-bit) on the ATmega328, each with two compare units (channel A and channel B).  This means that there are 6 pins that can be configured for PWM.  D3, D5, D6, and B3 support 8-bit PWM, while B1 and B2 support 16-bit PWM.  D5 and D6 are OC0B and OC0A respectively, B1 and B2 are OC1A and OC1B respectively, and B3 and D3 are OC2A and OC2B respectively.  For this example we are using the 16-bit timer for 8-bit PWM only.  8-bit means that there will be 256 levels for the output (from 0 - 255).  The general flow of setting up PWM is as follows:
  1. Set the output compare mode on the TCCRxA (timer counter control register A).  These are bits 4-7 for each timer (bits 4 and 5 for channel B, 6 and 7 for channel A).
    1. 00 - normal port operation (no PWM)
    2. 01 - toggles output on the A channel when the timer matches our set value (the value that the timer is being compared to) only when waveform generation mode (WGM) bits 0-3 = decimal 14 or 15 (channel B has normal port operation) NOTE: if WGM bits 0-3 are not decimal 14 or 15 both channels have normal port operation.
    3. 10 - clears output on a timer match and sets output when timer is 0
    4. 11 - sets output on a timer match and clears output when timer is 0 (we are using this one because the the RGB LED in the encoder is common anode, so we want to invert the output)
  2. Set the waveform generation mode in the TCCRxA and TCCRxB.  These are bits 0-1 in TCCRxA and bits 3 and 4 in TCCRxB.  These bits determine the waveform generation, specifically for us the type and resolution of PWM.  I will not put all the combinations here as there are 8 modes for the 8-bit timers and 16 modes for the 16-bit timer. Please refer to the datasheet for the different waveform generation modes.
    1. Our mode is fast PWM with 8-bit resolution (0-255).
  3. Set the clock select bits in TCCRxB (bits 0-2).  We are not using a prescaler so we select the internal clock with no prescaler (set bit 0).
  4. Set the pins as outputs to start PWM.
  5. Change the duty cycle (amount of time that the output is enabled vs disabled).  We do this by setting the OCRxA/B.  For our setup, 0xFF is full brightness because the output is cleared (ON, because our LED is common annode) when the timer is 0 and set (OFF) when the timer matches the value.  Since the timer overflows at 0xFF the output is always 0.
  6. Keep changing the duty cycle to get varying brightness (this is done in the never-ending for loop)
The initPWM() functions takes care of setting up the PWM for the RGB LED.  From that point on we just change the output compare value (duty cycle).

Conclusion

When I was first learning about mcus I sometimes forgot about the datasheets.  Some of them are straightforward, others are difficult to decipher.  In this case the datasheet is very straightforward, but I hope this helps nonetheless.
Here is the final result (sorry about the crappy video quality):
video


1 comment:

  1. Hello, really a nice post and thanks for sharing all about rotary index table and many other products, check this out and hope its useful.

    Thank you

    ReplyDelete