Decodificación de múltiples codificadores rotatorios de cuadratura

Guión

Tengo 4 codificadores rotatorios de cuadratura ACZ16 conectados al puerto D GPIO en un ATMega168P . Solo 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 antirrebote se maneja en el hardware utilizando el filtro antirrebote recomendado en la hoja de datos y no hay un rebote visible en el osciloscopio.

Problema

La diferenciación entre los codificadores no es un problema. Donde me encuentro con obstáculos es en la extracción de la dirección. Lo primero que pensé después de buscar en 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ó la 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 todavía se decodifica como una rotación en el sentido de las agujas del reloj.

Después de buscar más en 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 retraso 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 todavía 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 escalan mucho más allá de uno, tal vez dos codificadores rotatorios. ¿Cuál es una mejor manera de hacerlo con múltiples codificadores rotatorios?

En el hardware, un FlipFlop "D", la salida H/L "Q" identifica la dirección. Simplemente codifique la función.

Respuestas (3)

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

Dado que puede garantizar que solo gire un codificador 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 decodificar 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 ellos son las operaciones bit a bit en registros más amplios. Comencemos por encontrar cada canal en el que se haya producido un cambio, dada la disposición de puertos a1b1a2b2, etc., es decir, cada grupo de 2 bits pertenece a un canal.

Si hacemos ((value&0xaa)>>1)^(value&0x55)) obtenemos un valor de paridad. Luego, esto se puede comparar 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 antes):

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 funciones xor. También tenemos un margen de valores que no deben contarse (lo que significa que no hay ningún paso o que se ha perdido un paso). Ya encontramos la función de paso para eliminarlos. En esencia, todo lo que necesitamos es encontrar la diagonal con ceros, para 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 a'^b' para el paso y a' 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 necesitaba a o b en los otros bits, se lo dejo a otra persona. Además, este método no discrimina entre canales en absoluto; use otra máscara y encuentre bits establecidos para detectar qué canales realmente intervinieron.

Anexo: el algoritmo en realidad se vuelve mucho más limpio si no emparejamos las señales en bits adyacentes, sino que usamos 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 conteo 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).

Estos codificadores rotatorios emiten un conjunto de pulsos para cada retén. El pin que baja primero va a determinar la dirección. ¿Por qué decodificar todos los estados adicionales? ¿Cómo se escalaría esto a 4 canales?
No se garantiza que se detengan en un tope; es meramente estímulo mecánico. Incluso si usa solo el primer evento, aún necesita pasar los otros (vea los dos diagramas en la página 2), o cuenta dos veces los empujones. Y esto está escrito para 4 canales en un puerto de 8 bits; es por eso que 0x55 y 0xaa tienen 4 bits configurados (para puertos más anchos, continúe con los patrones 01 o 10). En otras palabras, esto se escala por la cantidad de puertos que usa, no por la cantidad de codificadores (aunque su relación es algo así como ceil(2*encs/portwidth)). Si desea específicamente un paso por detención, modifique la detección de pasos.
Por cierto, me acabo de dar cuenta de que escalaría mejor si lo dividiera en un puerto para a y b; te desharías de todos los bitshifts. Casi tan bueno simplemente agrupándolos, ya que un turno es suficiente para alinearlos. Se necesitaría otro registro, pero es probable que tenga suficiente.
Originalmente, quería repartirlos en 4 puertos, pero habría dificultado otras cosas. Le daré la aceptación y agregaré otra respuesta con lo que realmente termino más tarde.
Por cierto, si modifica el paso para obtener menos conteos, asegúrese de mantener la misma transición en ambas direcciones, porque de lo contrario reaparecerá el conteo múltiple de un problema de empujón (o estado inestable). En otras palabras, no cuente solo las transiciones al estado A, cuente las transiciones entre los estados adyacentes A y B (es decir, cualquier borde Tn en la hoja de datos). Esta es la razón por la cual contar solo la transición desde el tope tiene un problema; cuenta con bordes separados, cada uno de los cuales se puede cruzar en ambos sentidos sin llegar al otro. Recuerdo que el Atari ST realmente contó el movimiento del mouse de esa manera, lo que provocó que el puntero se desviara.

Creo que muchos codificadores rotatorios tienen salidas de dos bits, A y B, con B ligeramente desfasado con A. Girando CW, el flujo de pulsos de A conduce a B (A cambia antes que B), CCW; B lleva a A.

Le sugiero que lleve un registro de dónde están sus codificadores y qué ha pedido que hagan los espejos, 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 un tictac del temporizador, o posiblemente algo más)

... 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 garantizará que incluso si uno está limitado en la rapidez con la que puede enviar datos SPI, e incluso si los codificadores pueden moverse más rápido que eso, el sistema terminará enviando la cantidad adecuada de comandos para mover todo. Hay un recuento de contragolpe integrado en el código de movimiento del espejo (decir >0en lugar de >1eliminar eso). Las goto DONEdeclaraciones se proporcionan asumiendo que el código solo querrá enviar un comando cada vez que se invoque, incluso si se han movido varios codificadores (si spi_sendinicia el proceso de envío y no espera a que finalice, el código podría evitar perder el tiempo esperando Para elsendoperación a completar; además, el dispositivo al que se envían los comandos puede necesitar algún 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.