6

Scenario

I have 4 ACZ16 quadrature rotary encoders connected to GPIO Port D on an ATMega168P. I am only trying to extract rotation direction from them. Position is irrelevant, and it is guaranteed only one encoder will rotate at any given time. Debouncing is handled in hardware using the recommended debounce filter in the datasheet and there is no switch bounce visible on the scope.

Problem

Differentiating between the encoders is not a problem. Where I run into hurdles is extracting the direction. My first thought after some Googling was a pin change interrupt.

Resulting ISR:

ISR(PCINT2_vect)
{
unsigned static char int_count = 0;
unsigned char pins = PIND;
unsigned char send;

if(int_count == 0)
{
    switch(pins)
    {
        case 0x7F: send = MIRROR_X_L; break;
        case 0xBF: send = MIRROR_X_R; break;
        case 0xDF: send = MIRROR_Y_D; break;
        case 0xEF: send = MIRROR_Y_U; break;
        case 0xF7: send = LASER_X_L; break;
        case 0xFB: send = LASER_X_R; break;
        case 0xFD: send = LASER_Y_D; break;
        case 0xFE: send = LASER_Y_U; break;
        default: send = NOTHING; break;
    }

    if(send != NOTHING)
    {
        sendSPI(send);
    }
    int_count++;
}
else
{
    if(int_count == 3)
    {
        int_count = 0;
    }
    else
    {
        int_count++;
    }
}
}

This detected rotation in the correct rotary encoder, but only one direction. A clockwise rotation is decoded correctly, a counterclockwise rotation is still decoded as a clockwise rotation.

After further Googling, I tried a polling approach, using similar code.

Resulting main loop:

while(1)
{
    switch(PIND)
    {
        case 0x7F: send = MIRROR_X_L; break;
        case 0xBF: send = MIRROR_X_R; break;
        case 0xDF: send = MIRROR_Y_D; break;
        case 0xEF: send = MIRROR_Y_U; break;
        case 0xF7: send = LASER_X_L; break;
        case 0xFB: send = LASER_X_R; break;
        case 0xFD: send = LASER_Y_D; break;
        case 0xFE: send = LASER_Y_U; break;
        default: send = NOTHING; break;
    }

    if(send != NOTHING)
    {
        sendSPI(send);
        _delay_ms(40);
    }   
}

I played with multiple delay values following my SPI transmission, thinking that would fix the issue (actual cycle time is about 20ms), but this approach exhibits identical behavior to the pin change interrupt example. A clockwise rotation is decoded correctly, a counterclockwise rotation is still decoded as a clockwise rotation.

Most methods I've found, such as look up tables do not scale well past one, maybe two rotary encoders. What is a better way to do with with multiple rotary encoders?

Matt Young
  • 13,734
  • 5
  • 34
  • 61

3 Answers3

6

The key is how a quadrature encoding works: two signals are out of phase, so you can detect direction by which signal follows the other one. Combined, they have 4 states they pass through, but they will do so in opposite order for the opposite direction. I.e. 00-01-11-10- for right, 00-10-11-01- for left. As you see, they'll pass both the 01 and 10 states you're looking for - and the only way to know which way is by checking the next or previous state.

Given that you can guarantee only one encoder rotates at any time, the scaling of the quadrature decoder isn't really an issue. You can start by finding where the port changed and then decode only that transition.

Otherwise, we have the interesting challenge of finding a parallel algorithm for quadrature decoding applicable to microprocessors. A fundamentally parallel operation most of them have is bitwise operations on wider registers. Let's start by finding each channel where a change has happened, given the port arrangement a1b1a2b2 etc, i.e. every 2-bit group belongs to one channel.

If we do ((value&0xaa)>>1)^(value&0x55)) we get a parity value. This can then be xored with the previous parity value, and presto, we have a step signal. Next comes direction.

Let's set up a Karnaugh map, using inputs a, b, a' and b' (where ' means prior):

phase diagram ___/"""\___/"""  a
              _/"""\___/"""\_  b
             a=0     a=1
           b=0 b=1 b=1 b=0   1 means right, 0 means left, x don't care
a'=0 b'=0   x   1   x   0
a'=0 b'=1   0   x   1   x
a'=1 b'=1   x   0   x   1
a'=1 b'=0   1   x   0   x

We have a diagonal pattern, which tends to occur with xor functions. We also have a margin of values that should not be counted (meaning either no step or a missed step). We already found the step function to eliminate those. In essense, all we need is to find the diagonal with 0s in it, so we can invert step to get direction. It looks like the remaining discrimination can be done with b^a':

  b^a'       a=0     a=1
           b=0 b=1 b=1 b=0
a'=0 b'=0   0   1   1   0
a'=0 b'=1   0   1   1   0
a'=1 b'=1   1   0   0   1
a'=1 b'=0   1   0   0   1

So, given that we need a'^b' for step and a' for direction, we can save those two bits from the previous step. Our functions are step=a'^b'^a^b, dir=step&(b^a').

old_a_axb = ((oldpin&0xaa)>>1) ^ oldpin
# This has a serious bug, in that the ROL step actually used B from
# the next channel over. Let's fix it. 
#b_axb = ROL(pin)^(pin&0x55)
b_axb = ((pin&0xaa)>>1)^(pin&0x55)|((pin&0x55)<<1)
dir_step = old_a_axb ^ b_axb

# Rewrite since the selections get messy
old_al = oldpin&0xaa
old_ar = old_al>>1
old_br = oldpin&0x55
al = pin&0xaa
ar = al>>1
br = pin&0x55
bl = br<<1
axb_r = ar^br
axb_l = axb_r<<1
old_a_axb = oldpin ^ old_ar
b_axb = bl | axb_r = br*3^ar
dir_step = old_a_axb ^ b_axb
next_old_a_axb = axb_l^b_axb

It might be possible to optimize the a^b operation to occur only once, but given that I needed either a or b in the other bits I leave that to someone else. Also, this method doesn't discriminate between channels at all; use another mask and finding set bits to detect which channels actually stepped.

Addendum: The algorithm actually gets a lot cleaner if we do not pair the signals in adjacent bits, but use matching positions of separate variables:

# assume, for instance, a[3:0] in pin[7:4] and b[3:0] in pin[3:0]
a=pin>>4
b=pin&0x0f     # Separate signals into individual variables
axb=a^b
step=oldaxb^axb
dir=olda^b
olda=a
oldaxb=axb

So, for one register width count of quadrature decoders, it takes two stored variables, three xor operations, and one extra temporary register (which rarely matters).

Yann Vernier
  • 2,814
  • 17
  • 15
  • These rotary encoders put out one set of pulses for each detent. The pin that goes low first is going to determine the direction. Why decode all the extra states? How would this scale to 4 channels? – Matt Young Mar 22 '13 at 03:49
  • They are not guaranteed to stop at a detent; it is merely mechanical encouragement. Even if you use only the first event you still need to pass the others (see the two diagrams on page 2), or you double count nudges. And this is written for 4 channels in one 8-bit port; that is why 0x55 and 0xaa have 4 bits set (for wider ports, continue the 01 or 10 patterns). In other words, this scales by how many ports you use, not how many encoders (though their relation is something like ceil(2*encs/portwidth)). If you specifically want one step per detent, alter the step detect. – Yann Vernier Mar 22 '13 at 06:24
  • By the way, I just realized that it would scale better if you split it out to one port each for a and b; you'd get rid of all the bitshifts. Nearly as good by just grouping them, as one shift is enough to align them. It would take another register, but you likely have enough. – Yann Vernier Mar 22 '13 at 06:30
  • Originally I wanted to spread them across 4 ports, but it would have made other things more difficult. I'll give you the accept, and add another answer with what I actually end up with later. – Matt Young Mar 22 '13 at 06:38
  • By the way, if you do alter step to get fewer counts, be sure to keep the same transition in both directions, because otherwise the multiple count of a nudge (or unsteady state) problem reappears. In other words, do not count only transitions *to* state A, count transitions *between* adjacent states A and B (i.e. any edge Tn in the datasheet). This is why counting only transition from detent has a problem; it counts on separate edges, each of which can be crossed both ways without reaching the other. I remember the Atari ST actually did count mouse movement that way, causing pointer drift. – Yann Vernier Mar 22 '13 at 07:09
1

I believe many rotary encoders have two bit outputs, A and B with B being out of phase slightly with A. Turning CW the pulse stream from A leads B (A changes before B), CCW; B leads A.

Outsider
  • 136
  • 1
  • 2
0

I would suggest that you keep counts of where your encoders are, and what you have asked the mirrors to do, and then do something like:

unsigned char enc_pos0, enc_pos1, enc_pos2, enc_pos3;
unsigned char req_pos0, req_pos1, req_pos2, req_pos3;
ISR(PCINT2_vect)
{
  unsigned char pins = PIND;
  unsigned char delta = 0;
  if (pins & 0x80) // One input from encoder
    delta ^= 1;
  if (pins & 0x40) // Other input from encoder
    delta ^= 3;
  // At this point, delta is what the bottom two bits of count "should" be
  delta = (delta - enc_pos0) & 3; // Amount to adjust enc_pos0
  if (delta & 2) // Instead of moving up by 3 or 2, move down by 1 or 2
    enc_pos0  += delta-4;
  else
    enc_pos0 += delta;
  ... do other three encoders likewise.
}

then at some convenient interval (possibly a timer tick, or possibly something else)

... when convenient (note that if spi_send may take any significant amount of
... time, the pin-change interrupts should not be enabled while it's happening!

  unsigned char delta;

  delta = enc_pos0 - req_pos0;
  if ((delta & 0x80) != 0) // Need to decrease it
  {
    spi_send(MIRROR_X_L);
    req_pos0--;
    goto DONE;
  }
  else if (delta > 1) // Need to increase it
  {
    spi_send(MIRROR_X_R);
    req_pos0++;
    goto DONE;
  }
  delta = enc_pos1 - req_pos1;
  if ((delta & 0x80) != 0) // Need to decrease it
  {
    spi_send(MIRROR_Y_L);
    req_pos1--;
    goto DONE;
  }
  else if (delta > 1) // Need to increase it
  {
    spi_send(MIRROR_Y_R);
    req_pos1++;
    goto DONE;
  }
  delta = ...do likewise for other two encoders
  ...
DONE:

This style of programming will ensure that even if one is limited in how fast one can send SPI data, and even if encoders can move faster than that, the system will still end up sending the proper number of commands to move everything. There is one count worth of backlash built into the mirror-movement code (saying >0 rather than >1 would eliminate that). The goto DONE statements are provided on the assumption that code will only want to send one command each time it is invoked, even if multiple encoders have moved (if spi_send starts the sending process and doesn't wait for its completion, code could thus avoid wasting time waiting for the send operation to complete; additionally, the device to which commands are being sent might need some delay after each command before handling the next one). Some programmers are averse to using goto, but in some contexts it can be cleaner than any practical alternative.

supercat
  • 45,939
  • 2
  • 84
  • 143