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!