Skip to content

Lectura del Memsic 2125 mediante interrupciones

Una gran mejora es leer las señales del Memsic 2125 sin realizar una espera activa en la lectura, es decir, sin utilizar pulseIn() ya que éste realiza la lectura mediante un bucle y mantiene el procesador al 100% de trabajo.

Por la forma en que está programada la función pulseIn(), la mitad de las lecturas del Memsic se pierden ya que se leen alternativamente los datos del eje X y el Y y no se puede solapar dicha lectura.

La solución propuesta hace uso del Timer/Counter 2, con una resolución de 8 bits, a costa de perder la funcionalidad PWM en los pines digitales 3 y 11, y leer los datos mediante interrupciones.

Programación

  1. Configurar la ejecución de interrupciones Pin Change para los pines en los que se conecta el memsic (D2 y D3) (en setup()):
    PCMSK2 |= B00001100 ; // PCINT18-19 (D2,D3) - Pin Change Mask
     PCICR |= B00000100 ; // Enable PCIE2 - Pin Change Interrupt Enable 2
  2. Definir la función que servirá la interrupción:
    //PC en D2-3 = PCINT18,19 de PCINT2_vect(23..16)
    ISR(PCINT2_vect,ISR_BLOCK) {
      // Código del servicio
    }
  3. Configurar el Timer 2 para que se ejecute 100 veces por segundo (en setup())
     OCR2A = TC2_TOP_VAL ; // Valor máximo de conteo
    TCCR2A = B00000010 ; // TCT mode
    TIMSK2 = B00000010 ; // Activar T/C2 Output Compare Match A Interrupt
    TCCR2B = B00000111 ; // Prescaler de 1024 . Empezar a contar!
  4. Definir la función de la interrupción asociada Timer 2 Compare Match A, que se ejecuta cada vez que el Timer/Counter2 llega a TC2_TOP_VAL
    // Timer 2 Compare Match A
    ISR(TIMER2_COMPA_vect,ISR_BLOCK) {
    
      // Código del servicio
    
      // Bug: OCR2A está siendo borrado sin explicación cuando se ejecuta la ISR
      // Recargamos el valor, pero esto no debería ser necesario... :(
      OCR2A = TC2_TOP_VAL ;
    }

El valor TC2_TOP_VAL para que se ejecute la interrupción 100 veces por segundo se calcula según la fórmula:

#define CICLOS_POR_PWM_HZ  (F_CPU/100)
#define PRESCALER          1024
#define TC2_TOP_VAL        (unsigned char)((CICLOS_POR_PWM_HZ)/PRESCALER)-1

Descarga del código completo correspondiente a Arduino.

Descarga del código del visualizador de datos en Processing.

Enlace a la Sección de descarga de software.

Funcionamiento

Las señales PWM en los pines 2 y 3 correspondientes al memsic 2125 se leen mediante las  interrupciones de Pin Change, que son ejecutadas cuando cambia el valor de un pin. Para el caso de los pines digitales D2 y D3, la interrupción es PCINT2.

Los momentos temporales se guardan en la estructura TimeStamp definida como:

typedef struct  {
 unsigned int hi ;
 unsigned char lo ;
} Timestamp ;

Como ejemplo, dentro de la interrupción Pin Change Interrupt 2 (PCINT2) se puede ver cómo la parte ‘hi’ es el valor t2Overflow, que es incrementada por el timer2 cada vez que hay un overflow del valor TOP (ver la ISR de TIMER2_COMPA_vect), y la parte ‘lo’ es el valor del contador: TCNT2.

ISR(PCINT2_vect,ISR_BLOCK) {
 Timestamp tsNow = {t2Overflow, TCNT2} ;
  ...
}

Cuando se detecta que una de las señales en un pin pasa a HIGH, se guarda el timestamp del momento (momento en que se llama a la ISR).

Cuando se detecta que una de las señales en un pin pasa a LOW, se calcula el tiempo que ha estado a alta (resta de tiempos) y se indica que el valor ha de ser enviado.

En el bucle loop() se comprueba si hay algún valor para enviar, y en caso afirmativo se envía por puerto serie.

La interrupción Pin Change Timer 2 descrita no es suficiente para la lectura PWM porque el memsic 2125 envía señales constantes LOW o HIGH (en función de la aceleración) cuando tiene mucha aceleración en un eje duránte más de 1/100 segundos, por lo que es necesario “cortar” dichos intervalos por software. Aquí es donde entra en juego la interrupción Timer 2 Compare Match A, configurada para ejecutarse 100 veces por segundo. Cada vez que se ejecuta se incrementa el contador t2Overflow para llevar la cuenta de tiempo, y se comprueba si alguna de las señales en un pin lleva demasiado tiempo a HIGH o LOW. Si este es el caso, recarga el TimeStamp para dicha señal y la marca para enviar.

El rango de valores que toman las lecturas es [0 – T2_TOP_VALUE] por que se utiliza un contador de 8 bits.

Protocolo de envío de datos

El protocolo ha sido modificado para simplificarse, aprovechando que los valores de las lecturas ocupan 1 byte sin signo. Los mensajes son:

X<byte eje X>Y<byte eje Y>X<byte eje X>Y<byte eje Y>...

El código del visualizador de los datos ha sido modificado para adecuarse al nuevo protocolo y para tener en cuenta el valor de T2_TOP_VALUE.

El valor para el que la aceleración se considera 0 es T2_TOP_VALUE/2.

Resultados

Tras las modificaciones, la respuesta a los movimientos es muy notable y mucho más fluida. Se puede observar que el número de datos por segundo recibidos se ha duplicado. El tiempo de CPU utilizado para las lecturas, al ser mediante interrupciones, se ha reducido considerablemente.

Consideraciones posteriores

Lectura de los pines con las señales PWM

Para la lectura de los pines con las señales PWM se ha leído directamente el puerto D (PIND) en la interrupción para ahorrar tiempo: lectura de todos los pines en una sola instrucción, utilización de máscaraas de bits para comprobar los puertos afectados,…

Los pines digitales D2 y D3 del arduino se corresponen con los bits 2 y 3 de PIND respectivamente.

Ejecución de la interrupción TIMER2_COMPA

En la ejecución de la interrupción Timer 2 Compare Match A, el valor de OCRA (Output Compare Register A) es borrado y puesto a 0, por lo que ha de ser recargado con el valor T2_TOP_VALUE cada vez que se ejecuta dicha interrupción. Este no debería ser el comportamiento según lo descrito en el datasheet.

Paso a modo idle

Se ha aprovechado para poner el arduino en modo idle de bajo consumo en el bucle principal. Cada vez que ocurre una interrupción el arduino se despierta, ejecuta la interrupción, envía los datos pendientes y se vuelve a dormir.

Para poder pasar a modo idle, hay que incluir la cabecera:

#include <avr/sleep.h>

Y después, para dormir el arduino se configura y permite domir (esto se hace en una instrucción) y se llama a sleep_cpu(). Tras despertarse se recomienda impedir el dormir (para que no pase a modo idle sin querer por algún tipo de error).

 SMCR = B00000001 ; // Permitir dormir
 sleep_cpu() ;
 SMCR = B00000000 ; // Prohibir dormir

El bit 0 de SMCR es el que habilita e inhabilita el pasar a modo de bajo consumo (instrucción sleep).

Escritura por puerto serie: bloqueante

Un problema añadido es el hecho de que la librería de arduino tiene implementada la función de envío de caracteres por el puerto serie de manera que es bloqueante. En una ristra de bytes, el envío es bloqueante, excepto el último byte, que no es bloqueante.

A 9600 bps, enviar 2 bytes por segundo significa derrochar malamente unos 16500 ciclos, mientras que el hecho de enviar 11 bytes seguidos hace que se desperdicie un 10% de procesador (166Kciclos).

Una mejora es aumentar la velocidad de envío del puerto serie a 115200 bps, de manera que se desperdician 1388 ciclos por byte enviado. Concretamente, a 100 lecturas por segundo por eje, como son 2 bytes por lectura en 2 ejes, en el mejor de los casos se pierde tiempo en 2*100 bytes, y en el peor 3*100, lo que significa un desperdicion de 277Kciclos  y 416Kciclos: entre un 1,7 y un 2,6% de procesador. Deseable sería, de todos modos, que el envío no fuese bloqueante y pasar el procesador a idle.

Post a Comment

Your email is never published nor shared. Required fields are marked *