Decodificación de múltiples codificadores rotatorios en cuadratura

6

Escenario

Tengo 4 ACZ16 codificadores rotatorios en cuadratura conectados al GPIO Port D en un ATMega168P . Sólo estoy tratando de extraer la dirección de rotación de ellos. La posición es irrelevante, y se garantiza que solo un codificador girará en un momento dado. El anuncio se maneja en hardware utilizando el filtro de rebote recomendado en la hoja de datos y no hay un rebote del conmutador visible en el alcance.

Problema

La diferenciación entre los codificadores no es un problema. Donde me encuentro con obstáculos es extrayendo la dirección. Mi primer pensamiento después de un poco de Google fue una interrupción de cambio de pin.

ISR resultante:

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++;
    }
}
}

Esto detectó rotación en el codificador rotatorio correcto, pero solo en una dirección. Una rotación en el sentido de las agujas del reloj se decodifica correctamente, una rotación en el sentido contrario a las agujas del reloj aún se decodifica como una rotación en el sentido de las agujas del reloj.

Después de seguir con Google, probé un enfoque de sondeo, usando un código similar.

Bucle principal resultante:

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);
    }   
}

Jugué con varios valores de retardo después de mi transmisión SPI, pensando que eso solucionaría el problema (el tiempo de ciclo real es de aproximadamente 20 ms), pero este enfoque muestra un comportamiento idéntico al del ejemplo de interrupción de cambio de pin. Una rotación en el sentido de las agujas del reloj se decodifica correctamente, una rotación en el sentido contrario a las agujas del reloj aún se decodifica como una rotación en el sentido de las agujas del reloj.

La mayoría de los métodos que he encontrado, como las tablas de búsqueda, no se pueden escalar más allá de uno, quizás dos codificadores rotatorios. ¿Cuál es la mejor manera de hacerlo con varios codificadores rotatorios?

    
pregunta Matt Young

3 respuestas

5

La clave es cómo funciona una codificación en cuadratura: dos señales están fuera de fase, por lo que puede detectar la dirección por la que la señal sigue a la otra. Combinados, tienen 4 estados por los que pasan, pero lo harán en orden opuesto en dirección opuesta. Es decir. 00-01-11-10- para la derecha, 00-10-11-01- para la izquierda. Como ves, pasarán los estados 01 y 10 que estás buscando, y la única forma de saber qué camino es marcando el estado siguiente o anterior.

Dado que puede garantizar que solo un codificador gire en cualquier momento, la escala del decodificador en cuadratura no es realmente un problema. Puede comenzar por encontrar dónde cambió el puerto y luego descodificar solo esa transición.

De lo contrario, tenemos el interesante desafío de encontrar un algoritmo paralelo para la decodificación en cuadratura aplicable a los microprocesadores. Una operación fundamentalmente paralela que tienen la mayoría de ellas es operaciones bitwise en registros más amplios. Comencemos por encontrar cada canal donde ha ocurrido un cambio, dada la disposición de puertos a1b1a2b2, etc., es decir, cada grupo de 2 bits pertenece a un canal.

Si hacemos ((valor & 0xaa) > > 1) ^ (valor & 0x55)) obtenemos un valor de paridad. Esto puede luego ser xored con el valor de paridad anterior, y listo, tenemos una señal de paso. Luego viene la dirección.

Configuremos un mapa de Karnaugh, usando las entradas a, b, a 'y b' (donde 'significa anterior):

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

Tenemos un patrón diagonal, que tiende a ocurrir con las funciones xor. También tenemos un margen de valores que no se deben contar (es decir, sin pasos o pasos perdidos). Ya encontramos la función de paso para eliminarlos. En esencia, todo lo que necesitamos es encontrar la diagonal con 0 en ella, de modo que podamos invertir el paso para obtener la dirección. Parece que la discriminación restante se puede hacer con 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

Entonces, dado que necesitamos una '^ b' para el paso y una 'para la dirección, podemos guardar esos dos bits del paso anterior. Nuestras funciones son 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

Podría ser posible optimizar la operación a ^ b para que ocurra solo una vez, pero dado que necesito aob en los otros bits, se lo dejo a otra persona. Además, este método no discrimina en absoluto entre los canales; use otra máscara y busque los bits establecidos para detectar qué canales realmente escalonaron.

Addendum: el algoritmo se limpia mucho si no vinculamos las señales en bits adyacentes, pero utilizamos posiciones coincidentes de variables separadas:

# 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

Entonces, para un registro de ancho de registro de decodificadores en cuadratura, se necesitan dos variables almacenadas, tres operaciones xor y un registro temporal adicional (que rara vez importa).

    
respondido por el Yann Vernier
1

Creo que muchos codificadores giratorios tienen dos salidas de bit, A y B con B fuera de fase ligeramente con A. Girando CW el flujo de pulsos de A conduce B (A cambia antes que B), CCW; B lidera A.

    
respondido por el Paul Crouch
0

Le sugiero que lleve un registro de dónde se encuentran sus codificadores y de lo que le pidió a los espejos que haga, y luego haga algo como:

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.
}

luego, en algún intervalo conveniente (posiblemente una marca del temporizador, o posiblemente otra cosa)

... 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:

Este estilo de programación asegurará que incluso si uno está limitado en cuanto a la rapidez con la que puede enviar datos SPI, e incluso si los codificadores se mueven más rápido, el sistema seguirá enviando el número adecuado de comandos para mover todo. Hay un valor de contragolpe incorporado en el código de movimiento de espejo (decir que >0 en lugar de >1 eliminaría eso). Las declaraciones goto DONE se proporcionan en el supuesto de que el código solo desea enviar un comando cada vez que se invoca, incluso si se han movido varios codificadores (si spi_send inicia el proceso de envío y no espera su finalización, el código por lo tanto, podría evitar perder el tiempo esperando a que se complete la operación send ; además, el dispositivo al que se envían los comandos puede necesitar un poco de retraso después de cada comando antes de manejar el siguiente). Algunos programadores son reacios a usar goto , pero en algunos contextos puede ser más limpio que cualquier alternativa práctica.

    
respondido por el supercat

Lea otras preguntas en las etiquetas