Sunday, October 19, 2014

Updated Minima Rotary Encoder Driver


I have implemented support for rotary encoders in the Minima and provided integration of it into Eldon Brown's (WA0UWH) fine work on the Minima controller code.  The encoder works very smoothly and is very resilient to noisy, bouncing contacts on the mechanical encoders typically used.  That said, if you can avoid it, you should stay away from cheap mechanical encoders.  Spend a little more money on the main interface to your radio if you can.

The implementation uses pin-change interrupts for both the A and B inputs from the encoder.  A side effect of this is that you cannot pick completely arbitrary pins for the A and B inputs to the Arduino.  They must both be on the same port (PORTB, PORTC, PORTD).  My minima shield uses digital pins 8 and 9 freed up by replacing the 6 wire LCD with an i2c display.  Another option would be to use PD6 and PD7.  PD7 is currently targeted to be used by the RF386 amplifier.  However, the tuning control pot input A2 could be used freeing up PD6 and PD7 (both on PORTD) for use by the rotary encoder.

When an interrupt is received, the encoder is read and if it has changed, a 3ms debounce timer is reset.  As long as the pins continue to interrupt, the debounce timeout continues to be reset.  When the debounce timer expires, the encoder hasn't changed in the last 3ms and the value is processed.

I have provided integration to Eldon Brown's (WA0UWH) radiono code and completed initial testing.  I have turned it over to him to release with his code if he wishes to do so.

In general there are three basic functions that need to be implemented to support rotary encoders.

  1. Set up pin change interrupts for encoder A and B inputs.
  2. Set up debounce timer
  3. Implement interrupt service routines (ISR) for pin change and timer

The following bunch of noise is to try to minimize the number of changes required in order to change the pins the encoder is connected to.  The implementation represents a balance between providing pin change support for a Minima radio that can be configured as compared to a general purpose (large) library that can be configured in any way desired on any hardware platform.  If you wish to use analogue aliases for encoder pin numbers (A0-A5) you should instead use 14-19 (the digital pin equivalent numbers).

It should be noted that all of this (currently) only works on the ATMega328 such as is found in the Minima, Arduino UNO, etc.  Specifically support for the ATMega2560 is not yet implemented.

// Encoder pins
#define ENC_A_PIN 8  // Change these as desired, but both pins must be on 
#define ENC_B_PIN 9  // the same port (PB, PC, PD)

// Pin change interrupts
// Digital pin     PC Pin     Vector      Ctl Reg Port PCICR Bit Pin Mask Reg
// D0-D7           PCINT16-23 PCINT2_vect PCIR2   PD   PCIE2     PCMSK2
// D8-D13          PCINT0-5   PCINT0_vect PCIR0   PB   PCIE0     PCMSK0
// D14-D19 (A0-A5) PCINT8-13  PCINT1_vect PCIR1   PC   PCIE1     PCMSK1

// Define the interrupt vector, interrupt enable bit, PCINTpin values
#if (ENC_A_PIN) >= 0 && (ENC_A_PIN) <= 7
  #define VECTOR    PCINT2_vect
  #define PCMask    PCMSK2
  #define PCICRbit  (1<<PCIE2)
  #define PCINTpinA (1<<(ENC_A_PIN))
  #define PCINTpinB (1<<(ENC_B_PIN))
#endif
#if (ENC_A_PIN) >= 8 && (ENC_A_PIN) <= 13
  #define VECTOR    PCINT0_vect
  #define PCMask    PCMSK0
  #define PCICRbit  (1<<PCIE0)
  #define PCINTpinA (1<<(ENC_A_PIN-8))
  #define PCINTpinB (1<<(ENC_B_PIN-8))
#endif
#if (ENC_A_PIN) >= 14 && (ENC_A_PIN) <= 19
  #define VECTOR    PCINT1_vect
  #define PCMask    PCMSK1
  #define PCICRbit  (1<<PCIE1)
  #define PCINTpinA (1<<(ENC_A_PIN-14))
  #define PCINTpinB (1<<(ENC_B_PIN-14))
#endif

Ok, so all that noise is defining a bunch of things that change when you change the encoder pin numbers.  Specifically, you need to define the interrupt vector, the pin mask used and the pin change interrupt enable bit used.

The encoder returns a Gray code which I normalize to a 0..3 value.  Specific patterns of values are compared to determine the encoder direction of rotation.

// Encoder patterns for clockwise and anti-clockwise rotation
static const int CCW_DIR[] = {0x01, 0x03, 0x00, 0x02};
static const int  CW_DIR[] = {0x02, 0x00, 0x03, 0x01};

static int      last_code;  // Last stable encoder output
static int  encoder_count;  // tracks incremental rotation CW (+), CCW (-)

// Structure used to track encoder pin state in ISR
typedef struct 
{
  union 
  {
    byte encoder;
    struct 
    {
      int enc_A:1;
      int enc_B:1;
    };
  };
} PINSTATUS_t;

// Returns current encoder pin reading normalized to 0..3 value.
static inline int getEncoderValue(void)
{
  int value = 0;
  
  if (digitalRead(ENC_A_PIN)) value += 1;
  if (digitalRead(ENC_B_PIN)) value += 2;
    
  return value;
}

The pin change interrupt service routine (ISR) now needs to be defined.  We only look at the current value of the encoder pins and compare it to the last value read.  If it has changed, we just reset the 3ms debounce timer.  We are not going to process the encoder value until it has not changed for 3 ms.

// Encoder pin change ISR - If a pin changed, reset debounce timer
ISR(VECTOR)
{
  static PINSTATUS_t last;
  byte prior;

  prior = last.encoder;
  last.enc_A = digitalRead(ENC_A_PIN);  // Get encoder current pin values
  last.enc_B = digitalRead(ENC_B_PIN);
  
  if (prior != last.encoder)
  {
    TCNT1  = 0;
    TIMSK1 |= (1 << OCIE1A);
  } 
}

The 3 ms timer is implemented using timer 1.  I chose this timer because internal libraries for Arduino use timer 0 (the delay() function for example) and I plan to use timer 2 in my CW keyer code going forward.  Timer 1 is a 16 bit timer and is set up to provide an interrupt after 3 ms.

Currently, I decode and debounce 4 edges per detent on the encoder.  I could increment the encoder for each of these events which might make sense for an encoder without detents.  However, getting an increment/decrement of 4 for every click of the encoder detent would be confusing, and not useful, so currently I only return an increment for every 4 encoder edges decoded resulting in incrementing by one per detent click.  This could be used to speed up tuning when moving a long distance, but that is for another day...

// Timer ISR - We get here if the debounce timer times out.  Compare the new
// code with the previous one to see which direction the encoder has turned.
// Update the rotation counter to reflect the change.  The timer and interrupt
// are left disabled.  I can get four counts per detent and effectively divide
// this by 4 to get one tick per detent.
ISR(TIMER1_COMPA_vect)
{
  int new_code = getEncoderValue();
  
  if (new_code != last_code)
  {
    if (new_code == CW_DIR[last_code]) {
      if (0x00 == new_code) encoder_count++;
    } else if (new_code == CCW_DIR[last_code]) {
      if (0x00 == new_code) encoder_count--;
    }
    last_code = new_code;
  }
  TIMSK1 &= ~(1 << OCIE1A);  // Disable Timer
}

The initialization code for the encoder sets up pin change interrupts and Timer 1.  No magic here.

// Initialize the encoder
void initEncoder()
{
  cli();
  
  // Encoder pins
  pinMode(ENC_A_PIN, INPUT_PULLUP);
  pinMode(ENC_B_PIN, INPUT_PULLUP);
  
  // Set pin change interrupt enable and pin mask for encoder pins
  PCMask |= PCINTpinA;    // Set pin mask for A and B inputs
  PCMask |= PCINTpinB;
  PCICR  |= PCICRbit;     // Enable interrupts on correct port
  
  // Set timer 1 for 3 ms debounce timeout
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0
  // set compare match register 3 ms
  OCR1A = 48000;
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set bits for no prescaler
  TCCR1B |= (1 << CS10); 
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);
  sei();
  
  // Set the initial value from the encoder
  last_code = getEncoderValue();
  encoder_count = 0;
}

All that is left is to read and return a signed value indicating the direction and amount that the encoder has moved.  If this is called in the main loop of your sketch, it will typically return a +/- 1 value depending on the latency of your main loop.  In Eldon's Minima controller, it nicely returns a single increment or decrement per call.  The only magic here is to disable interrupts while reading the multi-byte value of the encoder in order to prevent the value from being changed before all bytes can be read.

// Return encoder count.  Negative - Anti-clockwise, positive - clockwise
int getEncoderDir()
{
  uint8_t oldSREG = SREG;
  cli();
  int val = encoder_count;
  encoder_count = 0;
  SREG = oldSREG;
  return val;
}

As always, your mileage may vary, but I hope you find this code useful.  I am happy to help if anyone has problems with it.  I will not be publishing this separately, but I expect that Eldon will use the integration in his Minima controller and publish it in due course after he has had a chance to review it.

I am happy to help with any problems.  Drop me an email note at ko7m at arrl dot net and I will do my best to help you with any issues.

No comments:

Post a Comment