domingo, 18 de octubre de 2015

Arduino a bajo nivel (II): Programando el ADC

Como decía en la entrada anterior, Arduino tiene múltiples ventajas, pero también tiene bastantes limitaciones por la orientación "abierta" que no extrae todos el jugo al hardware. El otro día comentaba el PWM, hoy comento el ADC.

El ADC en un ATmega328 en Arduino

El ADC en arduino funciona mediante analogRead(puerto). Por defecto funciona a 125kHz, y tarda sobre 100µs en hacer la conversión, que devuelve en un integer a pesar de ser un valor de 10bits. Por defecto usa como referencia la tensión de alimentación, que se asume de 5.0V pero en realidad variará entre 4.7V y 5.2V. Además, el programa se quedará bloqueado hasta que recojamos el valor del ADC.

Cómo conseguir lecturas del ADC de mejor calidad y más eficientes:

Antes de nada, lo primero sería entender exactamente cómo funciona un ADC y qué parámetros lo definen. Esta nota de aplicación de Atmel nos da algo de información al respecto. También en esta entrada de OpenMusicLabs hablan de los esquemas equivalentes de entrada del ADC.
Intentaré ir punto por punto por las diferentes cosas que se pueden tratar.



Primero: Muestreo de la entrada, impedancia de fuente

El ADC se basa en muestreo (sample&hold) y conversión posterior. Eso quiere decir que se realiza una adquisición de el pin de entrada, entre 1.5 y 13.5 ciclos después de solicitar la conversión, dependiendo del modo de funcionamiento. Es crítico que cuando se realiza el muestreo S&H de la entrada (que dura muy poco) la tensión sea estable, y provenga de una fuente de baja impedancia, para garantizar que el muestreo se hace correctamente. 
Esquema del S&H del ADC
A efectos prácticos, la forma más simple de tener una baja impedancia de fuente es utilizar un condensador de 1-10nF en paralelo a la entrada del ADC, que cargará el condensador del S&H relativamente rápido. A cambio se enlentecerá la respuesta de lo que queramos medir.
Poner un condensador o no ponerlo puede marcar la diferencia entre que funcione correctamente o no cuando medimos, por ejemplo, un potenciómetro de 33k.

Segundo: Tensión de referencia

Una vez la entrada se ha muestreado, el ADC compara esa tensión con la tensión de referencia. Si la tensión de referencia es una castaña, también lo serán las medidas. Por eso el ATmega tiene una referencia interna de 1.1V, que deberemos activar con analogReference(INTERNAL). Si aún a pesar de ser menos estable quisiéramos mantener la referencia de Vcc, sugiero calibrarla según el código disponible aquí.

Tercero: Frecuencia máxima de conversión y número efectivo de bits

En el caso del ATmega328, la resolución del ADC está limitada a 10 bits, no obstante, el número efectivo de bits depende de la frecuencia del ADC seleccionada. Según esta entrada de OpenMusicLabs la calidad cae drásticamente cuando superamos los 500kHz de frecuencia del ADC, por lo que tendremos que decidir si queremos medir rápido o medir bien.
  1. 125kHz - 9.7bits o 10bits según especificaciones ATmega
  2. 250kHz - 9.5bits
  3. 500kHz - 9.4bits
  4. 1MHz - 8.75bits
  5. 2MHz - 7.4bits
  6. 4MHz - <6bits

Cuarto: Sobremuestreo y promediado para aumentar resolución

Tal y como se explica en esta nota de aplicación, se puede aumentar la resolución del ADC mediante sobremuestreo y promediado. Para que funcione correctamente se han de cumplir determinados puntos, pero dado que tiene ventajas también a nivel de reducción de ruido pues no está de más implementarlo.
Es otra forma de ver lo de medir rápido o medir bien, dado que sumaremos varias medidas y luego las dividiremos.
Según la siguiente tabla extraída de la nota de aplicación, la cantidad de medidas ha de subir en potencias de 4, y la división para calcular el promedio en potencias de 2, por cada bit extra de resolución.

Tabla de sobremuestreo en un ADC y promediado

Por ejemplo, en el caso de usar un ATmega328 a 125kHz, asumiendo que diera los 10bits, si quisiéramos subir a 12bits deberíamos tomar 16 medidas, sumarlas, y dividirlas por 4. Nunca dividir y luego sumar, ya que entonces como partiríamos de medidas "recortadas" no aportaría nada. La idea es recortar "los flecos" sólo al final.
A la hora de sumar medidas, es importante recordar que el ATmega es un micro de 8bits, por lo que cuanto más grandes sean los tipos de datos peor. Las conversiones del ADC son de 10bits, por lo que, para no desbordar un "unsigned int" no deberíamos tomar más de 64 medidas consecutivas, por lo que la resolución máxima sería de 13bits (8096), sumando 64 medidas y dividiendo por 8.
Si fuera necesario se podría subir a "unsigned long", pero si deseamos tener un código rápido y compacto será mejor evitarlo.

Quinto: Utilizar las interrupciones del ADC para recoger los datos

No aumenta la calidad de la medida, pero gracias a esto nuestro programa no se quedará bloqueado mientras el ADC realiza la conversión, por lo que podremos seleccionar una frecuencia del ADC tan baja como queramos sin que ello afecte al transcurso del programa. No sólo podremos obtener medidas de 10bits sino que además podemos bajar el consumo.

Sexto: Utilizar modo automático de conversión

Si queremos hacer tareas repetitivas de muestreo ya sea porque queremos ir adquiriendo datos continuamente o porque tenemos que hacerlo sincronizado a determinados eventos, lo ideal es utilizar los métodos "auto triggered". Aunque no requieren el uso de interrupciones, es algo altamente recomendable.
Los modos auto triggered tienen también una particularidad interesante: el muestreo de la entrada se realiza en 2 ciclos desde la activación, y la conversión completa tarda unos 14 ciclos, por lo que se realizará mucho más rápido, y de forma determinista con el evento que deseemos, por lo que es posible controlar el momento exacto de la adquisición, mientras que si se hiciera por software sería prácticamente imposible la sincronización perfecta.

Séptimo: Utilizar canales del ADC específicos o desactivar entrada digital.

Dado que algunos pines son "multiuso", tienen un buffer que va a alterar el valor de las medidas. Si podemos utilizar alguno de los canales que no tiene buffer (6 o 7) pues mejor, pero si queremos usar los canales del 0 al 5 deberemos desactivar el buffer escribiendo 1 en el bit correspondiente del registo DIDR0. 

Cómo programar los registros del ADC.

Del mismo modo que en el caso del PWM, programando los registros conseguimos cambiar los modos de funcionamiento, en este caso para configurar el ADC también deberemos cambiar varios registros.

Registros del ADC del ATmega328


ADCMUX:
Este registro nos permite especificar qué referencia utilizar, si queremos los resultados justificados a la izquierda, y qué canal del ADC queremos convertir.
El hecho de justificar los resultados a la izquierda (ADLAR=1) es útil si queremos trabajar sólo con 8bits, ya que podremos leer únicamente la parte alta, y así ahorraremos tiempo en leer el resultado de los registros internos del ADC.
A nivel de referencias, como comentaba antes, utilizaremos REFS1,0 = 11 para la referencia de 1.1V o 01 para usar como referencia la tensión de alimentación.
MUX3 a MUX0 determina qué canal utilizar. Utilizaremos 0 a 7 para los canales 0 a 7, utilizaremos 8 para el sensor de temperatura interno, o 14 para medir la tensión de referencia.
Nota: si medimos canal 14 usando como referencia Vcc, podremos medir la tensión de alimentación.

ADCSRA:
Este registro permite activar (=1) y desactivar (=0) el ADC (ADEN), 'arrancarlo' (ADSC), ponerlo en modo auto-trigger (ADATE), ponerlo en modo interrupción (ADIE) y definir al frecuencia de operación con el prescaler (ADPS2..0). La frecuencia del ADC vendrá dada por 2^(prescaler), por lo que para 125kHz usaremos 111, 110 para 250kHz, 101 para 500kHz y 100 para 1MHz.

ADCSRB:
Este registro permite controlar la fuente del auto-trigger, dependiendo del valor de ADTS2..0. Dentro de las 8 posibles fuentes, 0 pondrá el ADC en modo contínuo y 1 lo sincronizará con el comparador analógico interno. También se pueden seleccionar diferentes configuraciones con Timer0 y Timer1 como trigger.

DIDR0:
Con este registro deshabilitaremos la entrada digital de algunos de los pines multiuso, lo que nos permitirá reducir la distorsión del S&H cuando trabajemos con impedancias elevadas. Para ello, pondremos a '1' el bit del puerto correspondiente.

Ejemplo: Muestreando el canal 4 intentando llegar a 13 bits efectivos. Problemas con las interrupciones.

En el ejemplo la idea es muestrear el canal 4 del ADC aumentando la resolución a 13bits mediante sobremuestreo y promediado.
En un primer caso se hace con el "código normal" de Arduino, y en un segundo caso se programa "correctamente" para aprovechar los recursos del microcontrolador.
En ambos casos se hace sobremuestreo y promediado de 64 y 8 medidas.

Código "normal" de Arduino

Si utilizáramos el método "normal" de Arduino, implementaríamos:

  unsigned char nmedida; //contador de numero de adquisiciones, interna
  unsigned int medida;   //Valor promediado
  unsigned int valor=0;  //valor medido
  for(nmedida=0;nmedida<64;nmedida++)
    valor=valor+analogRead(4);
  medida=valor/8;
El código es especialmente simple y no ocupa demasiado espacio. El inconveniente es que el bucle de conversión tardaría más de 6 milisegundos en ejecutarse, y el microcontrolador se quedaría "bloqueado" mientras tanto. No parece mucho tiempo, pero con el micro funcionado a 16MHz, esos 6 milisegundos permitirían ejecutar unas 100.000 instrucciones (no, no hay ceros de más).

Si quisiéramos reducir el impacto, podríamos espaciar las conversiones, aunque seguiríamos con el mismo tiempo muerto total y no sabríamos cada cuanto mide el ADC.

Código con conversión automática e interrupciones

En este caso deberíamos hacer un código algo más complejo, primero con la rutina de interrupción del ADC

  volatile unsigned int medida;         //Valor promediado, asíncrono, ver comentario

  ISR(ADC_vect){  //Rutina de interrupción del ADC
    static unsigned int valor=0;  //valor medido, variable interna
    static unsigned char nmedida=0; //contador de numero de adquisiciones, interna

    valor = valor + ADCL;       //añade los bits bajos del ADC
    valor = valor +(ADCH<<8);   //añade los bits altos del ADC

    if((++nmedida)==64)){       //cuando se han hecho 64 medidas
      medida=valor/8;           //    promedia valores y actualiza variable
      valor=0;                  //    resetea contador
      nmedida=0;
    }
  }
Y con un código general tal que:  

  //Primero, inicialización
  ADMUX=0b11000100;  //Ref. 1.1V, justificado a la derecha, canal 4)
  ADCSRA=0b10101111; //Activar ADC pero no empezar. Poner modo automático, borrar flag y activar interrupcion. Seleccionar frecuencia a 125kHz. 
  ADCSRB=0b00000000; //Poner como Auto la conversión continua
  DIDR0=0b00010000;  //Ya que estamos, deshabilito la entrada digital del canal

En este caso, el valor de "medida" se irá actualizando automáticamente, de forma "asíncrona" con el programa principal (por eso el volatile) y puede cambiar en cualquier momento. Hay que tenerlo en cuenta, especialmente si las variables son de más de 8 bits, puesto que una asignación de 16bits se divide en dos asignaciones de 8 bits, y por tanto, puede cambiar el valor de "medida" a mitad de asignación.
En la siguiente imagen se muestra un ejemplo genérico de lo que podría suceder en hacer "valor_anterior=medida"

interrupcion interrumpe proceso asignacion
En el primer caso, la asignación a valor_anterior se hace correctamente, puesto que sucede posteriormente al cambio en medida. En el segundo caso, la interrupción del ADC llega a mitad de la asignación de valor_anterior, por lo que la mitad de la asignación se hace con el contenido viejo y la otra mitad con el contenido nuevo. El número resultante en valor_anterior es erróneo: ni el anterior ni el posterior.

Para evitar este problema, es conveniente trabajar con una copia "local" asegurando que no se produce ninguna interrupción durante la asignación:

  noInterrupts();           // bloqueamos interrupciones
  medida_local=medida;      // hacemos asignación, esta vez sin interrupciones
  interrupts();             // volvemos a permitir interrupciones

Con este segundo código el micro seguiría realizando sus tareas mientras el ADC va convirtiendo las medidas. En cada fin de conversión del ADC (cada ~100us) el micro pararía el programa principal para procesar datos, pero en ningún caso tendríamos el micro "descansando", sino que no pararía.

Primero vs segundo Código:

Como una imagen vale más que mil palabras, subo un par de resultados de medir 83,7mV con los dos programas.
Comentar que no convierto las medidas del ADC a valores de tensión, para ello, como se usa promediado, tendríamos que considerar que 1.1V son 8095 cuentas, por lo que la cuenta de 614-617 corresponde a 83,4 y 83.8mV respectivamente.
Programa "simple" con sobremuestreo y promediado.
Programa con interrupciones

Se puede ver que a nivel de medidas no varían (lo que cabía esperar) y no hay demasiada diferencia a nivel de espacio de programa, si bien sí la hay a nivel de programación (un poco más compleja la segunda) y de rendimiento, por la ausencia de "zona muerta" en el segundo caso, aunque en este caso no sea importante, dado que no se ejecuta nada mientras tanto.
Comparando las dos lado a lado (quitando la instrucción duplicada de extracción de datos en el puerto serie) vemos que la diferencia es de únicamente 30bytes:

Programa de ADC en modo normal o con interrupciones.
Utilizar interrupciones incrementa en 30bytes el código, pero evita una zona muerta de 6ms

Más adelante aplicaré el uso de interrupciones en el ADC y los ajustes en el PWM para mostrar la utilidad de enlazarlos y por qué es importante conocer estos detallitos a la hora de hacer programas eficientes y potentes.

Un saludo y gracias por leer hasta aquí!

Podéis comprar placas de arduino sin convertidor USB/TTL aquí por 1.20€ o aquí en formato completo con el convertidor CH340G por 2€. Y por 3€ la versión con el 32u4, que tiene controlador USB nativo y permite emular dispositivos HID.

1 comentario:

  1. hola, me resulta muy interesante este articulo, pero como soy nuevo en esto de arduino, quisiera saber si es posible obtener el código completo para comprenderlo mejor. Saludos

    ResponderEliminar