VictorJAM.zapto.org
Programación  Arduino 

2025-12-11

Filtros Software

El filtro más sencillo: un condensador.

Partamos de un circuito muy sencillo: un divisor de tensión formado por dos resistencias de igual valor. Cabría pensar, que al tratarse de dos resistencias iguales el valor del ADC debería ser 2.5v justos. Pero si observamos la lectura del ADC de Arduino vemos que no es fija, si no que la tensión fluctúa.

En la gráfica anterior podemos observar en azul la tensión que nos devuelve el ADC de Arduino y se aprecia la fluctuación. Realmente la señal cambia aproximadamente 0.01V que podría ser despreciable, pero las pruebas se han hecho en un entorno limpio y sin ruidos eléctricos apreciables.

El filtro más simple que podemos hacer es un filtro hardware a base de un simple condensador cerámico. En la gráfica la línea roja es la medición del ADC colocando un condensador de 100uF. Se puede observar que el condensador hace que la señal sea mas estable.

Mi consejo es poner siempre el condensador si se puede, es más práctico y resuelve muchos problemas. Sin embargo, puede ser interesante también resolver este problema vía software, sobre todo si la señal cambia mas en el tiempo y no es un simple divisor de tensión fijo.

// # Código 2
// Obtener datos de un divisor de tensión, limitando las muestras a 100 cada
// 100 ms.
const float alpha = 0.5;
int índex=0;
float v;
float av;

void setup() {
  Serial.begin(9600);
  index=0;
  while ( index < 100 ) {
    v = analogRead(A0)*5.0/1024.0;
    
    Serial.print(v);
    Serial.print(",");
    delay(100);
    index++;
    if ( index%10==0 ) Serial.println();
  }
}

void loop() {

}

Filtro Media

Lo primero que nos viene a la cabeza cuando queremos "filtrar" una señal es usar una media aritmética. En este caso iremos tomando muestras y sumándolas obteniendo la media dividiendo dicha suma por la cantidad de muestras.

Para este experimento he hecho una modificación, he colocado una LDR en serie con una resistencia y cuando mido tapo y destapo la LDR continuamente para hacer que la tensión fluctúe dentro del rango de alimentación. No voy a usar condensador de filtro en este experimento.

Una cosa a tener en cuenta es el hecho de usar un número de muestras fijo, de un tamaño relativamente pequeño, dado que si solo nos dedicamos a sumar valores durante toda la señal obtendremos valores no deseados. Usaremos este programa para demostrarlo:

// Ejemplo de media sin tener en cuenta una ventana.
const int maxMuestras = 100;

int count = 0;
float sum = 0;

int muestras;

void setup() {
  Serial.begin(9600);
  sum = analogRead(A0)*5.0/1024.0;
  count=1;
  Serial.print(sum);
  Serial.print(",");
  Serial.print(sum);
  Serial.println();
}

void loop() {
  float v = analogRead(A0)*5.0/1024.0;
  sum = sum + v;
  count++;
  Serial.print(v);
  Serial.print(",");
  Serial.print(sum/count);
  Serial.println();
  delay(100);
  muestras++;
  if ( muestras>maxMuestras ) while(1);
}

Si tomamos los datos de la salida y hacemos una gráfica podemos observar lo siguiente:

Las dos puntas de la señal amarilla ocurren al tapar la LDR haciendo que el valor de la media, pero se puede observar que el valor de la media queda muy por encima del valor real a lo largo del tiempo.

Así que lo mejor es tomar un número de muestras pequeño, por ejemplo 10, y hacer la media.

Este número pequeño de muestras se le suele llamar ventana.

He preparado una clase Media para obtener hacer los cálculos más cómodos.

class MediaSimple {
  private:
    float sum;
    int   muestras;
    float media;
    int   maxMuestras;
  public:
    MediaSimple(int mm) {
      sum = 0;
      muestras = 0;
      media=0;
      maxMuestras = mm;
    }
    void addValue(float value) {
      sum = sum+value;
      muestras++;
      if ( muestras > maxMuestras ) {
        media = sum/muestras;
        sum = 0;
        muestras = 0;
      }
    }
    float getMedia() {
      return media;
    }
};

No me he quebrado la cabeza en su diseño. Básicamente cuando creamos el objeto le decimos el número de muestras que ha de tener en cuenta. Con cada llamada a la función addValue se suma el valor de la lectura. Con la función getMedia obtiene la media.

#include "MediaSimple.h"

float v;
MediaSimple media1(5);
MediaSimple media2(10);
int muestras;
const int maxMuestras = 100;


void setup() {
  Serial.begin(9600);
}

void loop() {
  v = analogRead(A0)*5.0/1024.0;
  media1.addValue(v); 
  media2.addValue(v); 

  Serial.print(v);
  Serial.print(",");
  Serial.print(media1.getMedia());
  Serial.print(",");
  Serial.print(media2.getMedia());
  Serial.println();

  delay(100);
  muestras++;
  if ( muestras>maxMuestras ) while(1);

}

En este programa he creado dos objetos media para que se pueda apreciar bien el efecto de usar un tamaño de muestre diferente:

En la gráfica podemos observar varias cosas. Lo primero es apreciar un desplazamiento de la señal. Cuanto mayor es el tamaño de la ventana mayor se puede apreciar el desplazamiento. Este es un efecto inherente a las propiedades de la media.

Lo otra característica que podemos observar es el "escalonamiento" de la señal. Si solo aceptamos como válido el valor de un número de muestras, hasta que no se hayan tomado el valor permanece fijo, luego se calcula y vuelve a cambiar.

El problema del escalonamiento lo solucionaremos usando una media móvil

Media móvil

Para evitar el escalonamiento de la señal, podemos cambiar la forma en la que tomamos las muestras y así obtener un mejor resultado. Por ejemplo, podemos usar un búfer circular, que no es más que un array que recorremos de forma que cuando llegamos al final, volvemos al principio. Así vamos guardando los valores continuos de la señal y calculamos la media real de los últimos valores añadidos.

Se mantiene la suma de todos los puntos del array, pero cuando se inserta un nuevo elemento en él, hay que restar el valor que hay en esa posición para que no altere la media.

He creado una plantilla para poder usar de ejemplo:

#ifndef _mean_h_
#define _mean_h_

template <class T>
class Mean {
  private:
    T   *data;
    int index;
    int size;
    int count;
    T   sum;
  public:
    Mean<T>(const int sizet) {
      data = new T[sizet];
      size = sizet;
      index = 0;  
      count = 0;  
    }
    addValue(T value) {
      if ( count>=size ) {
        sum = sum-data[index];
      }
      data[index]=value;
      sum = sum+data[index];
      index++;
      if ( index>=size ) index=0;
      if ( count<size ) count++;
    }
    T get() {
      return sum/count;
    }
};

#endif

La clase solo tiene dos métodos: addValue que añade un valor; y getMedia para obtener el valor de la media.

Así al ejecutar el código de muestreo:

#include "mean.h"

Mean<float> media1(3);
Mean<float> media2(7);
int muestras=100, i;

void setup() {
  Serial.begin(9600);
}

void loop() {
  float v = analogRead(A0)*5.0/1024;
  media1.addValue(v);
  media2.addValue(v);
  //media.print();
  Serial.print(v);
  Serial.print(",");
  Serial.print(media1.get());
  Serial.print(",");
  Serial.print(media2.get());
  Serial.println();
  i++;
  if ( i>muestras ) while(1);
  delay(100);
}

Obtenemos la siguiente gráfica:

Podemos observar que la señal se suaviza bastante y que ya no tenemos el escalonamiento obteniendo una señal muy parecida a la original, solo que sigue estando desplazada en el tiempo. Cuanto más grande sea el número de muestras mayor será el desplazamiento.

Filtro mediana.

Debido a la naturaleza de la media, cuando una señal recibe un pico de valor exagerado, entonces la media sufre una variación que se mantendrá en el tiempo.

Para señales que pueden recibir este tipo de picos conviene utilizar la mediana en vez de la media.

La mediana es el valor central de un conjunto de datos ordenados.

En la siguiente gráfica podemos observar el resultado:

Cuando se produce un "pico" la media sube y se mantiene durante el tiempo alterando el valor real de la señal. En la gráfica es la línea verde. Mientras la mediana casi no altera el valor de la señal.

Pero no es oro todo lo que reluce. Si la señal tiene muchos picos importantes obtendremos que le media se va a comportar mejor y tendremos unos datos mas "suavizados". En la siguiente gráfica se ve el comportamiento.

Dependiendo de la señal si aumentamos el número de muestras para la mediana obtendremos mejores resultados, aunque siempre convendría hacer un filtrado añadido por ejemplo con una media para obtener el mejor resultado.

Uno de los mayores problemas que tiene el filtro de la mediana es que ordenar un array es un proceso costoso para el microcontrolador. En la entrada Algoritmos de ordenación en Arduino tenemos ejemplos de algunos de los algoritmos de ordenación mas extendidos.

Alternativas al algoritmo de ordenación

En el caso de la mediana solo queremos el valor central de la lista de muestras, así que no sería necesario ordenar la lista para obtener el valor deseado. O eso es lo que pensó el algoritmo Quicksort, así que pensó en como hacer una búsqueda del ese dato y obtuvo el algoritmo quickselect.

Quickselect nos devuelve el valor de la posición k de un array si este estuviera ordenado y es la mayoría de las veces más rápido que ordenar el array y luego obtenerlo.

He aquí una plantilla en Arduino:

#ifndef _quickselect_h_
#define _quickselect_h_

// -------------------------------------------------------------
// Quickselect genérico mediante plantillas
// Funciona con cualquier tipo comparable: int, float, double, etc.
// -------------------------------------------------------------

template <typename T>
void swap(T &a, T &b) {
  T t = a;
  a = b;
  b = t;
}

template <typename T>
int partition(T arr[], int left, int right, int pivotIndex) {
  T pivotValue = arr[pivotIndex];
  swap(arr[pivotIndex], arr[right]); // mover pivote al final
  int storeIndex = left;

  for (int i = left; i < right; i++) {
    if (arr[i] < pivotValue) {
      swap(arr[storeIndex], arr[i]);
      storeIndex++;
    }
  }

  swap(arr[right], arr[storeIndex]); // mover pivote a posición final
  return storeIndex;
}

template <typename T>
T quickselect(T arr[], int left, int right, int k) {
  while (true) {
    if (left == right) {
      return arr[left]; // solo queda un elemento
    }

    // Elegir pivote (aquí: punto medio)
    int pivotIndex = (left + right) / 2;

    pivotIndex = partition(arr, left, right, pivotIndex);

    if (k == pivotIndex) {
      return arr[k];
    } else if (k < pivotIndex) {
      right = pivotIndex - 1;
    } else {
      left = pivotIndex + 1;
    }
  }
}

#endif

Pero si queremos mejorar la rapidez y el uso de memoria, muy útil usando Arduino podemos usar el algoritmo de Wirth:

#ifndef _wirth_h_
#define _wirth_h_
// -------------------------------------------------------------
// Selección de Wirth (Selección directa)
// Funciona con cualquier tipo comparable (int, float, double...)
// Nota: k es un índice (0 = menor elemento)
// -------------------------------------------------------------

template <typename T>
void swap(T &a, T &b) {
  T t = a;
  a = b;
  b = t;
}

template <typename T>
T wirthSelect(T arr[], int left, int right, int k) {
  while (left < right) {

    T pivot = arr[k];
    int i = left;
    int j = right;

    // Partición tipo Wirth
    do {
      while (arr[i] < pivot) i++;
      while (pivot < arr[j]) j--;

      if (i <= j) {
        swap(arr[i], arr[j]);
        i++;
        j--;
      }

    } while (i <= j);

    // Reducir la ventana de búsqueda
    if (j < k) left  = i; // buscar en la parte derecha
    if (k < i) right = j; // buscar en la parte izquierda
  }

  return arr[k];
}

#endif

Filtro paso baso y paso alto exponencial (EMA) en Arduino.

El filtro móvil funciona bien, pero podemos conseguir una aproximación a esta sin tener que recurrir al búfer circular.

Para ello basta con aplicar esta fórmula:

An=M+(1-α)An-1

Donde AN es el valor filtrado, AN-1 es el valor filtrado anterior y α es una constante que nos define el grado del filtro en sí. El valor de α va de 0 a 1. Si alpha es igual a 0 la señal no se filtra y el resultado será 0. Mientras que si vale 1 la señal tampoco se filtra y el valor de la entrada es igual a la salida.

En la siguiente gráfica podemos observar el efecto de la señal aplicando un filtro de media móvil de 5 y un EMA con alpha igual a 0.3

Se puede observar que el resultado es casi idéntico. La diferencia radica en que el filtro EMA es más fácil de implementar.

#include "mean.h"

const float alpha = 0.3;
float raw, filtered, previus;
int i;

Mean<float> media(5);
void setup() {
  Serial.begin(9600);
  // Inicialización filtro EMA.
  raw = filtered = previus = analogRead(A0)*5.0/1024.0;
}

void loop() {
  raw = analogRead(A0)*5.0/1024.0;
  filtered = alpha*raw+(1-alpha)*previus;
  previus=filtered;
  media.addValue(raw);
  
  Serial.print(raw);
  Serial.print(",");
  Serial.print(media.get());
  Serial.print(",");
  Serial.print(filtered);
  Serial.println();

  delay(100);
  
  i++;
  if ( i>=200 ) while(1);
}

La elección de α depende del nivel de filtrado que queramos. Por ejemplo, un valor de 0.5 vendría a equivaler a un filtro de media móvil con ventana de 3 elementos, un valor de 0.2 a una ventana de 9.

Hay que tener en cuenta que al igual que ocurre con la media móvil el filtrado EMA produce un "retraso" en la señal. Cuanto mas cercano a cero sea el valor de α mayor será el filtrado y mayor será el "retraso".

En la siguiente gráfica se puede observar este efecto. Se han usado dos valores de alpha de 0.3 y 0.6, se puede observar que el mejor suavizado se obtiene con el valor de 0.3 pero la señal se ve más desplazada en el tiempo.

Obtención de la tensión de alimentación de Arduino.

Aunque la mayoría de las veces cuando usamos el ADC lo más fácil es asumir que la tensión de alimentación es 5.0V, puede ocurrir que está no sea así, dependiendo del regulador o no.

Existe una manera de poder leer la tensión de alimentación con Arduino usando la referencia interna de 1.1V. He aquí la función:

float readVcc() {
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
  delay(2);
  ADCSRA |= _BV(ADSC);
  while (bit_is_set(ADCSRA, ADSC));
  
  long result = ADCL;
  result |= ADCH << 8;
  result = 1126400L / result; // Back-calculate AVcc in mV
  return result/1000.0;
}

De todas formas tampoco la función es perfecta y lo mejor será tener una tensión estable fija.

Reflexiones

En este artículo he explicado los filtros más simples que se pueden aplicar a una señal, existen muchos mas. Sin embargo, la elección de uno u otro filtro depende mucho del tipo de señal que estamos leyendo: su velocidad de cambio, el ruido del entorno, etc.

Para sensores normales, una LDR, un sensor de temperatura cuyas variaciones generalmente son lentas, tendremos más que suficiente con aplicar un filtro EMA y poco más.

Sin embargo, hay que tener en cuenta los ruidos eléctricos y proteger bien la entrada analógica del microcontrolador, ya que aquí pueden producirse picos en la señal que pueden destruir al chip y si no llega a ello producir falsos en la medida de la señal.

Referencias

Filtro paso bajo y paso alto exponencial (EMA) en Arduino.

Filtro media móvil.

Filtro de kalman.

Medir la inclinación con IMU, Arduino y filtro complementario.