Sunday, February 8, 2015

Reading encoders with Arduino

I know that the encoders topic has been done to the death already (it seems it's next in line with the blink example) but I've nowhere seen it done the way I'll present here.

1. We know that 0b10 == 2, 0b11 == 3, 0b00 == 0 and 0b01 == 1 - those are just basic 2-bit values
2. We can put 2 values like that in a nibble (half-byte) so a uint8_t is more than able to house that value
3. We'll assume pins 2&3 (PC1 and PC0)

So the reading could be like so:

volatile uint8_t state = 0;
volatile int32_t counter = 0, oldCounter = 0;
// for full-stop encoder reads only use this state changes
volatile int8_t QEM[16]  = {
 0, 0,  0, 0, -1, 0, 0, 1, 1, 0, 0, -1, 0,  0, 0, 0
};
// for full quadrature decoding use this state changes
// volatile int8_t QEM[16]  = {
// 0, 1, -1, 0, -1, 0, 0, 1, 1, 0, 0, -1, 0, -1, 1, 0
// };

void setup() {
 // configure pin direction
 DDRC &= ~(1<<PC1);
 DDRC &= ~(1<<PC0);

 // enable pullups
 PORTC |= (1<<PC1) || (1<<PC0);

 // load initial encoder values
 if ((PINC & (1 << PC1)) != 0) state |= 0b00000010;
 if ((PINC & (1 << PC0)) != 0) state |= 0b00000001;
}

void loop() {
 // make space for current state; we just want the lower nibble
 // mask everything else out
 state = (state << 2) & 0x0F;

  // read the current state into the lower part of the nibble
 // is half a nibble a nib? ;)
 if ((PINC & (1 << PC1)) != 0) state |= 0b00000010;
 if ((PINC & (1 << PC0)) != 0) state |= 0b00000001;

 // At this stage the state variable contains 4 bits of information
 // containing the previous state and the new state that can be
 // directly used as an index - just the array needs to be a little
 // bit different.
 // 0b0010 = -1
 // 0b0001 =  1
 // 0b1000 =  1
 // 0b1011 = -1
 // 0b1110 =  1
 // 0b1101 = -1
 // 0b0100 = -1
 // 0b0111 =  1
 // which results in the array defined above

  // next we use the state value as an index to the array
 // we do this only on full encoder stops
 counter += QEM[state];

  // react on counter change
 if (oldCounter != counter) {
  oldCounter = counter;

   // counter changed - process
 }
}

Now this is all nice if your loops are quick but if they aren't you're going to loose precision. To get it back we need to change to interrupts - it's not that difficult!

volatile uint8_t state = 0;
volatile int32_t counter = 0, oldCounter = 0;
// for full-stop encoder reads only use this state changes
volatile int8_t QEM[16]  = {
 0, 0,  0, 0, -1, 0, 0, 1, 1, 0, 0, -1, 0,  0, 0, 0
};
// for full quadrature decoding use this state changes
// volatile int8_t QEM[16]  = {
// 0, 1, -1, 0, -1, 0, 0, 1, 1, 0, 0, -1, 0, -1, 1, 0
// };

ISR(PCINT1_vect) {
 // make space for current state; we just want the lower nibble
 // mask everything else out
 state = (state << 2) & 0x0F;

 // read the current state into the lower part of the nibble
 // is half a nibble a nib? ;)
 if ((PINC & (1 << PC1)) != 0) state |= 0b00000010;
 if ((PINC & (1 << PC0)) != 0) state |= 0b00000001;

 // At this stage the state variable contains 4 bits of information
 // containing the previous state and the new state that can be
 // directly used as an index - just the array needs to be a little
 // bit different.
 // 0b0010 = -1
 // 0b0001 =  1
 // 0b1000 =  1
 // 0b1011 = -1
 // 0b1110 =  1
 // 0b1101 = -1
 // 0b0100 = -1
 // 0b0111 =  1
 // which results in the array defined above

 // next we use the state value as an index to the array
 // we do this only on full encoder stops
 counter += QEM[state];
}

void setup() {
 // configure pin direction
 DDRD &= ~(1<<PC1);
 DDRD &= ~(1<<PC0);

 // enable pullups
 PORTC |= (1<<PC1) || (1<<PC0);

 // enable pin-change interrupts for PC1 and PC0
 PCICR |= (1<<PCIE2);
 PCMSK2 |= (1<<PCINT18) | (1<<PCINT19);

 // load initial encoder values
 if ((PINC & (1<<PC1)) != 0) state |= 0b00000010;
 if ((PINC & (1<<PC0)) != 0) state |= 0b00000001;

 // enable interrupts
 sei();
}

void loop() {
 // react on counter change
 if (oldCounter != counter) {
  oldCounter = counter;

  // counter changed
 }
}

As you can see the ISR-driven method isn't all that different from the polling one and gives a significant advantage in terms of flexibility and reliability.

Many thanks for dr Robert Paz for his series of lectures on Arduino programming!

Have fun!

No comments: