Este blog está dedicado a mis experiencias, proyectos, dificultades y demás en todo lo relacionado a la electrónica y la programación en general sobre GNU/Linux

Arduino, Digital, Electrónica, Nivel: Intermedio

Oversampling con bajos recursos (+Bits ADC)

Nivel: Intermedio

¿Qué tengo que saber para este post?

  • Entender el uso de un ADC.
  • Programación de al menos Arduino.
  • Un poco sobre filtros analógicos y digitales.
  • Aritmética de punto fijo.
  • Desplazamiento de bits.

—————————————

Oversampling es una técnica de procesamiento de señales, la cual hace un uso provechoso del ruido presente a la entrada del ADC (del propio ADC o a la entrada del mismo) para aumentar la resolución a costa de disminuir la velocidad de conversión.

La técnica más conocida es el promedio de varias muestras, el cual si bien es un caso particular de oversampling no es la única posibilidad. El marco teórico de promediar varias muestras, y también del oversampling, es la técnica de Noise shaping + Decimation lo cual en palabras simples es samplear una señal a una tasa de muestreo mucho más alta que su ancho de banda para luego filtrarla digitalmente. Como el ruido se distribuye uniformemente en el espectro, una vez filtrada la señal, contiene menos ruido (Noise Shaping). Como la señal tiene un ancho de banda pequeño, no es necesaria la tasa de muestreo original y se puede tomar una muestra cada tanto (Decimation) sin perder información, con lo cual juntando ambas técnicas es posible reconstruir la señal y a la vez aumentar la resolución del ADC (a la salida del filtro existen valores fraccionarios que no forman parte de los valores entregados por el ADC). Es importante aplicar el filtro utilizando algún tipo de dato que permita aprovechar este beneficio, ya sea un tipo de coma flotante o un sistema de coma fija (suele ser lo óptimo para sistemas con bajos recursos).

Para la implementación del filtro pasa bajos, lo mas sencillo de implementar es un promedio móvil, pero ésta no es la única solución posible, dicho filtro tiene una pobre respuesta en frecuencia la cual no es monótona decreciente (como un filtro RC de primer orden) sino que se comporta como una función sinc. Esto perjudica nuestro sistema ya que al atenuar poco el ruido, la varianza de las muestras al final del proceso va a ser grande por el ruido que no fué atenuado. En otras palabras los valores van a «bailar», mucho menos que si no hubiésemos aplicado el filtro pero mucho peor que si hubiésemos usado otro tipo de filtro. la ventaja de este es que la muestra actual (el promedio de las últimas N muestras) no tiene en principio correlación con las muestras anteriores (N+1…) por lo que es sencillo elegir el intervalo óptimo para realizar la decimación (tomar 1 de cada N muestras) ya que tomar más de una muestra dentro del mismo N no aportaría información extra. La principal desventaja de este método es que al sumar gran cantidad de muestras podemos llegar a desbordar el tipo de dato utilizado y obtener resultados arbitrarios (para un ADC de 10 bits y un uint16 podemos sumar como máximo 2⁶ valores aproximadamente sin desbordar la variable). Por otro lado, para extraer cada resultado hay que hacer N sumas y una división.

Un filtro de implementación muy sencilla es la versión discreta del filtro RC de primer orden, el cual tiene una respuesta monótona decreciente y es de implementación por demás sencilla. También es necesario considerar que las variables intermedias no desborden como en el caso anterior, pero impone menos restricciones. Este tipo de filtro se puede implementar por software, consiste en hacer un promedio ponderado entre la salida del filtro anterior y la lectura actual del ADC, la salida del filtro actual será la entrada en la próxima iteración. Este tipo de filtro toma el nombre de Respuesta al impulso infinito (IIR) y para estos fines no requiere corrección pre-warp, dado que la frecuencia de corte debe ser mucho menor que la frecuencia de nyquist con lo cual el error es despreciable.

Comparativa entre un filtro en tiempo continuo y su aproximación en tiempo discreto para una Fs=1kHz Fc=1Hz

A diferencia del filtro de promedio móvil, que es un filtro de respuesta al impulso finita (FIR), en el filtro IIR todas las muestras actuales contienen información de las muestras anteriores como si fuese un promedio ponderado utilizando una exponencial negativa. La ultima muestra es la de mayor peso y las muestras más alejadas aportan cada vez menos. por este motivo el valor para el cual hacer la decimación está más asociado a un criterio de implementación. Una manera de implementar este tipo de filtro es realizar el sampleo y el filtrado dentro de una interrupción, la cual actualiza la salida del filtro y lo deja disponible para que el loop principal pueda acceder cuando lo disponga (ese seria el momento en el cual se realiza la decimación, a la tasa de lectura del loop principal).

Este método tiene muchas ventajas. Supongamos que tenemos algún parámetro que varía lento como, por ejemplo, el nivel de un tanque de agua. Sabemos que la variación del tanque es extremadamente lenta, pero si ponemos un transductor y leemos 1000 valores por segundo vamos a encontrar infinidad de variaciones. El ruido eléctrico que se filtra en los cables, el oleaje que se puede presentar en el tanque, el ruido del ADC, etc. Todas estas fuentes de ruido varían muy rápidamente (1Hz-10kHz) en comparación con el valor de la variable a medir (<1Hz) por este motivo podemos configurar una interrupción para samplear a 1kHz y aplicar un filtro con una Fc=1Hz, por que sabemos que todo lo que varíe más rápido se corresponde con ruido y es posible ser filtrado sin afectar la señal de interés. Ahora, en nuestro loop principal, cuando mostremos el valor leído, por ejemplo, en un display LCD, vamos a leer el valor a la salida del filtro el cual va a tener muy poca varianza gracias a que la señal de interés varia muy lento y el ruido se filtra de manera muy efectiva por que se corresponde con frecuencias muy superiores.

Las ventajas respecto de usar un filtro RC físico son: Que la implementación digital filtra también el ruido del ADC, aumenta la resolución del mismo, disminuye el BOM, aumenta la fiabilidad y por último la frecuencia de corte no depende de la tolerancia de los componentes físicos, sino de la tolerancia dela base de tiempo (el cristal).

Figura anterior pero con el agregado de la respuesta en frecuencia de un promedio móvil de N=100.

Para obtener cada resultado, utilizando este método para filtrar la señal, es necesario hacer dos multiplicaciones, una suma y una división (esto es independiente de la frecuencia de corte elegida). Está claro entonces que comparado con el Oversampling clásico (N sumas y una división) es mucho más económico en cuanto a ciclos de reloj para frecuencias de corte bajas (N muy grande).

A continuación el script de Octave utilizado para generar los gráficos anteriores, como así también para calcular los coeficientes del filtro IIR de primer orden. implementado de la forma Y_(n)=(1-a)*Y_(n-1)+a*X_(n) siendo X_(n) la lectura actual del ADC, Y_(n) la salida actual del filtro y Y_(n-1) la salida previa del filtro.

pkg load signal
pkg load control
close all

set(0, "defaultlinelinewidth", 2);


Fs= 1000;  % sampling rate
tsam=T=1/Fs;
f=logspace(1,log10(Fs/2),1000);
w=f*(2*pi);

wc=1*2*pi; %frecuencia de corte
s = tf ('s')
z= tf('z',tsam)

sys=@(s)  wc/(wc+s) %
%[num,den,tsample]=tfdata(sys(s))
#aplico la transformación bilinear + prewarp
sysz = c2d (sys(s), tsam, 'prewarp', wc)
%sysz = c2d (sys(s), tsam, 'bilin', wc) %Bilinear sola
[num,den,tsample]=tfdata(sysz)
disp("Coeficiente @ 1Hz:")
(num{1,1}(1))/(1+(num{1,1}(1)))
sysz=@(z) (num{1,1}(2) + num{1,1}(1)*z)/(den{1,1}(2)+den{1,1}(1)*z);

hold on

[MAG, PHA, W]=bode(sysz(z));
semilogx(W/(2*pi),20*log10(MAG));

[MAG, PHA, W]=bode(sys(s),w);
semilogx(W/(2*pi),20*log10(MAG));

N=100; % esta es la implementación del promedio mobil
num=ones(1,N);
den=[N];

sysz=tf(num,den,tsam);

[MAG, PHA, W]=bode(sysz);
semilogx(W/(2*pi),20*log10(MAG));

legend("Filtro en tiempo discreto equivalente","Filtro en tiempo continuo","Promedio movil")
ylim([-60 10])
grid minor
##set (findobj (gcf, "type", "axes"), "nextplot", "add")
xlabel('Frequencia [Hz]')

print -dpng "Comparativa_filtro_s_z.png"

Implementación practica.

Para poder aplicar las técnicas antes descriptas hay que tomar ciertos recaudos, el primero es que la señal de entrada necesita tener ruido presente, de al menos algunos LSB’s en el ADC, esto quiere decir que si se realizan sucesivas mediciones debe arrojar valores distintos. Por otro lado es importante prestar atención a no desbordar ninguna variable durante las cuentas, como así también utilizar aritmética de coma fija con la mayor precisión que nos sea posible.

Tenemos entonces 3 algoritmos distintos a implementar:

  • Oversampling Clásico: tomar N muestras, computar el promedio, entregar un resultado cada N muestras.
  • Noise-shaping promedio móvil: Calcular el promedio de las ultimas N muestras para cada medición, Entregar el resultado caundo es requerido por el loop principal.
  • Noise-Shaping pasabajo primer orden: Filtra la señal con un filtro simple de primer orden, entrega una muestra cuando el loop principal lo requiere.

En el Oversampling clásico tenemos un algoritmo bastante sencillo de la forma:

     int N=10
     uint32_t suma = 0;
     for (int j=0 ; j<N ;  j++)   suma+= analogRead(A0);  
     suma/=N;

Como se puede apreciar en el código anterior, la técnica se resume en hacer el promedio de los N valores anteriores del ADC, pero ¿donde aumento la cantidad de bits? Si se mira con atención la escala del valor de salida es la misma que la de entrada, al realizar la división se redondea el resultado a un valor que se corresponde con la resolución original del ADC. El criterio elegido para «aumentar» la resolución del ADC según Atmel (nota de aplicación citada al inicio) es tomar N=2^{2M} siendo M el ratio de oversampling, y en lugar de dividir por el mismo valor, dividir por un valor más chico de 2^M. De esta forma «aparecen» bits de mas que no estaban en la salida original del ADC y con esto ganamos resolución. Pero OJO, que no es todo tan perfecto. Supongamos que quiero hacer un oversampling de 2 para aumentar la resolución en un bit, entonces tomo 4 muestras que coincidentemente dan 1023, las sumo y las divido por dos con lo cual 4*1023/2= 2046. Pero en un ADC de 11 bits la salida máxima es 2047 ¿qué pasó con el bit que falta? la técnica de Oversampling sacrifica rango de entrada al comienzo (cerca de 0volts) y al final (cerca de Vref) para acomodar el ruido necesario para aplicar la técnica. si tenemos valores muy cercanos a los extremos el ruido presente se deforma y deja de tener una distribución de probabilidad simétrica lo cual arruina completamente el método. Cuanto más ruido tengamos, más rango de entrada perdemos. La ventaja de la técnica propuesta por Atmel en su nota de aplicación es que al usar múltiplos de 2^N realizar una división es muy económica en términos de computo (se reduce a un simple bitshift).

Consideremos ahora cual es el valor máximo de N factible se utilizar si usamos un uint32_t. Dicho tipo de dato almacena una valor máximo de 4.294.967.296, lo cual dividido el valor máximo del ADC (1023) da como resultado 4.198.404, lo cual es un valor aproximado de 2^22 con lo cual el M máximo a usar es 11. En teoría podríamos hacer un ADC de 10+11=21 bits con un uint32 y el ADC del Arduino, pero tardaría aproximadamente 4198404/10000=420 segundos en realizar cada medición.

Para el caso del Noise-shaping con promedio móvil es igualmente válida la consideración de usar valores múltiplos de 2^N, y también las consideraciones respecto del desborde para la variable encargada de realizar la suma, pero se suma también la complejidad computacional a la ecuación.

Para realizar un promedio móvil es necesario almacenar las últimas N muestras (lo cual implica un limite directamente relacionado con la memoria RAM disponible), para cada valor de salida es necesario realizar una sumas y una división. En este caso no se realiza decimación, es el loop principal el que lee el valor de salida a intervalos mayores que la frecuencia de muestreo y realiza la decimación de facto. Los bits «aumentados» del ADC se pueden asimilar de la misma manera que los del oversampling clásico.

El código fuente: (extraído del ejemplo de Arduino smoothing)

/*

  Smoothing

  Reads repeatedly from an analog input, calculating a running average
  and printing it to the computer.  Keeps ten readings in an array and 
  continually averages them.
  
  The circuit:
    * Analog sensor (potentiometer will do) attached to analog input 0

  Created 22 April 2007
  By David A. Mellis  <dam@mellis.org>
  modified 9 Apr 2012
  by Tom Igoe
  http://www.arduino.cc/en/Tutorial/Smoothing
  
  This example code is in the public domain.


*/


// Define the number of samples to keep track of.  The higher the number,
// the more the readings will be smoothed, but the slower the output will
// respond to the input.  Using a constant rather than a normal variable lets
// use this value to determine the size of the readings array.
const int numReadings = 10;

int readings[numReadings];      // the readings from the analog input
int index = 0;                  // the index of the current reading
int total = 0;                  // the running total
int average = 0;                // the average

int inputPin = A0;

void setup()
{
  // initialize serial communication with computer:
  Serial.begin(9600);                   
  // initialize all the readings to 0: 
  for (int thisReading = 0; thisReading < numReadings; thisReading++)
    readings[thisReading] = 0;          
}

void loop() {
  // subtract the last reading:
  total= total - readings[index];         
  // read from the sensor:  
  readings[index] = analogRead(inputPin); 
  // add the reading to the total:
  total= total + readings[index];       
  // advance to the next position in the array:  
  index = index + 1;                    

  // if we're at the end of the array...
  if (index >= numReadings)              
    // ...wrap around to the beginning: 
    index = 0;                           

  // calculate the average:
  average = total / numReadings;         
  // send it to the computer as ASCII digits
  Serial.println(average);   
  delay(1);        // delay in between reads for stability            
}

Por último tenemos el método de Noise-Shaping pasabajo primer orden en el cual aplicamos un sencillo filtro IIR de primer orden. Para este filtro hay que tener en cuenta varias consideraciones. La primera de ella es que se vale de valores fraccionarios por lo cual hay que implementar algún tipo de aritmética de punto fijo para que el redondeo de los enteros tenga menos impacto. Por otro lado, también hay que verificar que las variables no desborden en los cálculos intermedios.

En el código siguiente se puede apreciar la implementación práctica de este algoritmo, como se aprecia cuenta con una multiplicación, una suma y una división. Mucho mas económico en cómputo que el Oversampling clásico y mas económico en RAM que el promedio móvil, principalmente para frecuencias de corte bajas (N muy grande).

#define COMMA_SHIFT (1 << 6) // 6 bits después de la coma (aproximadamente dos ceros decimales)
#define ADC_COEFF  320 //    = 1/a   ,a=   0.0031220 para Fc=1hz, Fs=1kHz

static uint32_t Y = (uint32_t)2048 * COMMA_SHIFT;        //variable que almacena la salida del filtro
 Y = (Y * (ADC_COEFF - 1) + ((uint32_t)analogRead(inputPin)) * COMMA_SHIFT) / ADC_COEFF;     

Para implementar la aritmética de punto fijo se desplaza el valor leído en el ADC , lo cual hace lugar para los bits fraccionarios, pero aumenta el valor nominal lo cual puede hacer desbordar la variable. Vamos a analizar la parte superior de la ecuación para verificar que no desborde:

Aproximamos el valor máximo del ADC (de 10 bits) con 2^10, al aplicarle el shift quedaría 2^16, el peor caso en el numerador quedaría expresado como (2^16* (320-1) +2^16) =20.971.520 lo cual es aproximadamente 200 veces menor que el limite del uint32 y el algoritmo funcionaria correctamente. Incluso sería óptimo aumentar la cantidad de bits después de la coma hasta aprovechar por completo el margen que nos queda hasta desbordar el tipo de dato.

Existe una última cuestión a considerar, cuanto más baja es la frecuencia de corte, más grande es el valor de ADC_COEFF con lo cual menos bits quedan disponibles para implementar la aritmética de punto fijo sin desbordar el tipo de dato. Si no se utiliza la suficiente cantidad de bits, se termina redondeando al valor actual del ADC, con lo cual seria como entrar en el filtro valores de un ADC de 6 bits. Para verificar esto se puede computar la salida del filtro con valores idénticos de Y pero con una diferencia unitaria en el valor del ADC (por ejemplo Y=100, ADC=10 y ADC=11). si la salida del filtro no cambia, significa que el truncamiento está afectando el resultado y se requieren más bits después de la coma. Este fenómeno y el desborde imponen un limite práctico para la implementación de este filtro en frecuencias de sampleo demasiado altas en conjunto con frecuencias de corte demasiado bajas.

Dejar una respuesta