Resumen | Este codelab fue creado para comprender una posible implementación de algoritmos de filtrado de señales como el filtro de Kalman y el filtro complementario. También se revisa la implementación de otros filtros suavizadores de señales usando un algoritmo para su posible implementación en ESP32. Se espera que usted al finalizar esté en capacidad de:
|
Fecha de Creación: | 2024/03/01 |
Última Actualización: | 2024/03/01 |
Requisitos Previos: | |
Adaptado de: | |
Referencias: | |
Escrito por: | Fredy Segura-Quijano |
El filtro de Kalman es un algoritmo recursivo que proporciona una estimación óptima del estado de un sistema dinámico a partir de una serie de medidas ruidosas. Es ampliamente utilizado en sistemas de control y navegación, así como en el procesamiento de señales, debido a su capacidad para predecir y corregir estimaciones en tiempo real. Es útil en Sistemas Embebidos debido a su capacidad para procesar datos en tiempo real y su eficiencia computacional. Este filtro es óptimo en el sentido de que minimiza el error cuadrático medio de las estimaciones.
El modelo del sistema se describe mediante dos ecuaciones:
Donde
es el estado del sistema en el instante
.
es la matriz de transición de estado.
es la matriz de control.
es el vector de control en el instante
es el ruido del proceso (asumido de media cero y covarianza
)
es la medición en el instante
.
es la matriz de observación
es el ruido de medición (asumido de media cero y covarianza
)
El filtro de Kalman realiza dos pasos en cada iteración: predicción y actualización.
Para implementar un filtro de Kalman en una ESP32, se deben definir los estados del sistema, las matrices de transición y de medición, y las matrices de covarianza del proceso y de la medida. A continuación, se muestra un ejemplo simplificado para la estimación de una señal de posición a partir de medidas ruidosas:
struct KalmanFilter {
float Q; // Variancia del proceso
float R; // Variancia de la medida
float A; // Factor de escala del estado (modelo de transición)
float B; // Control de la entrada
float C; // Factor de escala de la medida
float x; // Estimación del estado
float cov; // Covarianza del estado
KalmanFilter(float q, float r, float a, float b, float c)
: Q(q), R(r), A(a), B(b), C(c), x(0), cov(0) {}
void predict(float u) {
x = A * x + B * u;
cov = A * cov * A + Q;
}
void update(float z) {
float K = cov * C / (C * cov * C + R);
x = x + K * (z - C * x);
cov = (1 - K * C) * cov;
}
float getState() {
return x;
}
};
KalmanFilter kf(0.1, 1.0, 1.0, 0.0, 1.0); // Inicializa el filtro de Kalman con parámetros
void setup() {
Serial.begin(115200);
}
void loop() {
float measurement = analogRead(34); // Leer el canal ADC
kf.predict(0); // No hay entrada de control en este ejemplo
kf.update(measurement);
Serial.println(kf.getState());
delay(100);
}
Estructura del Filtro de Kalman:
Método predict:
Método update:
Inicialización del Filtro:
Bucle Principal:
El filtro de Kalman es ampliamente utilizado en diversas aplicaciones de Sistemas Embebidos, como Sistemas de Navegación Inercial para la fusión de datos de acelerómetros y giroscopios, proporcionando estimaciones precisas de la posición y la orientación. En robótica para el seguimiento de la posición y la velocidad de robots móviles. También se usa en el procesamiento de Señales Biomédicas para la estimación de parámetros fisiológicos a partir de medidas ruidosas.
A continuación se presenta el código completo para implementar un Filtro Kalman en la ESP32:
#include <driver/adc.h>
// Definiciones de constantes
#define ADC_CHANNEL ADC1_CHANNEL_0 // GPIO36
#define SAMPLE_RATE 1000 // Frecuencia de muestreo en Hz
#define BUFFER_SIZE 1000 // Tamaño del buffer para almacenar muestras
// Estructura del Filtro de Kalman
struct KalmanFilter {
float Q; // Variancia del proceso
float R; // Variancia de la medida
float A; // Factor de escala del estado (modelo de transición)
float B; // Control de la entrada
float C; // Factor de escala de la medida
float x; // Estimación del estado
float cov; // Covarianza del estado
// Constructor del Filtro de Kalman
KalmanFilter(float q, float r, float a, float b, float c)
: Q(q), R(r), A(a), B(b), C(c), x(0), cov(1) {} // Inicializamos cov a 1 en lugar de 0 para evitar división por cero
// Método de predicción
void predict(float u) {
x = A * x + B * u;
cov = A * cov * A + Q;
}
// Método de actualización
void update(float z) {
float K = cov * C / (C * cov * C + R);
x = x + K * (z - C * x);
cov = (1 - K * C) * cov;
}
// Método para obtener el estado estimado
float getState() {
return x;
}
};
// Inicializa el filtro de Kalman con parámetros
KalmanFilter kf(0.1, 1.0, 1.0, 0.0, 1.0);
void setup() {
Serial.begin(115200); // Inicializa la comunicación serial
adc1_config_width(ADC_WIDTH_BIT_12); // Configura el ADC para un ancho de 12 bits
adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN_DB_0); // Configura la atenuación del canal ADC
}
void loop() {
float measurement = analogRead(34); // Leer el valor del canal ADC
kf.predict(0); // Realiza la predicción (sin entrada de control)
kf.update(measurement); // Actualiza el filtro con la nueva medida
Serial.println(kf.getState()); // Imprime el estado estimado
delay(100); // Espera 100 ms antes de tomar la siguiente muestra
}
El filtro complementario es una técnica sencilla pero eficaz para fusionar datos de múltiples sensores, para combinar señales de sensores que tienen diferentes características de ruido y precisión; particularmente útil en Sistemas Embebidos. Se utiliza comúnmente para combinar datos de acelerómetros y giroscopios en aplicaciones de navegación inercial y control de actitud. El filtro complementario aprovecha las características complementarias de diferentes sensores para proporcionar una estimación más precisa y estable.
En el caso de fusión de datos de un acelerómetro y un giroscopio:
El filtro complementario pondera las señales de ambos sensores de tal manera que se aprovechen sus fortalezas y se minimicen sus debilidades.
Un filtro complementario simple para combinar datos de un acelerómetro y un giroscopio puede implementarse de la siguiente manera:
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_MPU6050.h>
// Crear un objeto para el sensor MPU6050
Adafruit_MPU6050 mpu;
// Parámetros del filtro complementario
float angle = 0.0; // Ángulo inicial
float alpha = 0.98; // Factor de ponderación
float dt = 0.01; // Intervalo de tiempo en segundos (10 ms)
void setup() {
Serial.begin(115200);
// Inicializar el sensor MPU6050
if (!mpu.begin()) {
Serial.println("Failed to find MPU6050 chip");
while (1) {
delay(10);
}
}
// Configurar el rango del acelerómetro y giroscopio
mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
mpu.setGyroRange(MPU6050_RANGE_250_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
}
void loop() {
// Leer los valores del sensor
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
// Calcular el ángulo del acelerómetro (en grados)
float accelAngle = atan2(a.acceleration.y, a.acceleration.z) * 180 / PI;
// Calcular el ángulo del giroscopio (integrar la velocidad angular)
float gyroRate = g.gyro.x; // Velocidad angular en grados por segundo
angle = alpha * (angle + gyroRate * dt) + (1 - alpha) * accelAngle;
// Imprimir el ángulo estimado
Serial.print("Angle: ");
Serial.println(angle);
// Esperar 10 ms antes de la siguiente lectura
delay(10);
}
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_MPU6050.h>
Adafruit_MPU6050 mpu;
float angle = 0.0; // Ángulo inicial
float alpha = 0.98; // Factor de ponderación
float dt = 0.01; // Intervalo de tiempo en segundos (10 ms)
void setup() {
Serial.begin(115200);
if (!mpu.begin()) {
Serial.println("Failed to find MPU6050 chip");
while (1) {
delay(10);
}
}
mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
mpu.setGyroRange(MPU6050_RANGE_250_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
}
void loop() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
float accelAngle = atan2(a.acceleration.y, a.acceleration.z) * 180 / PI;
float gyroRate = g.gyro.x;
angle = alpha * (angle + gyroRate * dt) + (1 - alpha) * accelAngle;
Serial.print("Angle: ");
Serial.println(angle);
delay(10);
}
En esta función, se realiza el ciclo principal del programa:
El filtro complementario es ampliamente utilizado en aplicaciones donde se requiere una estimación precisa de la orientación o el ángulo de inclinación, tales como:
Los métodos suavizadores de señales son técnicas utilizadas para reducir el ruido y las fluctuaciones no deseadas en una señal, haciendo que la señal resultante sea más suave y más fácil de analizar. En sistemas embebidos, donde los recursos computacionales y de memoria son limitados, es crucial utilizar métodos de suavizado eficientes que no sobrecarguen el sistema. A continuación, se explican algunos de los métodos suavizadores más comunes y su implementación en sistemas embebidos como la ESP32.
El filtro de media móvil es uno de los métodos de suavizado más simples. Calcula el promedio de un número fijo de puntos de datos adyacentes en la señal. Este promedio se desplaza a lo largo de la señal para suavizar las fluctuaciones.
#define WINDOW_SIZE 10
float window[WINDOW_SIZE];
int windowIndex = 0;
float sum = 0;
void setup() {
Serial.begin(115200);
}
void loop() {
float newValue = analogRead(34) * (5.0 / 1023.0); // Leer el canal ADC y convertir a voltaje
sum = sum - window[windowIndex] + newValue;
window[windowIndex] = newValue;
windowIndex = (windowIndex + 1) % WINDOW_SIZE;
float average = sum / WINDOW_SIZE;
Serial.println(average);
delay(100); // Esperar 100 ms antes de la siguiente muestra
}
El filtro de media ponderada exponencial aplica un peso decreciente a los datos históricos. Cada nuevo valor se multiplica por un factor de suavizado, y se combina con la señal suavizada previa.
float alpha = 0.1; // Factor de suavizado
float smoothedValue = 0;
void setup() {
Serial.begin(115200);
}
void loop() {
float newValue = analogRead(34) * (5.0 / 1023.0); // Leer el canal ADC y convertir a voltaje
smoothedValue = alpha * newValue + (1 - alpha) * smoothedValue;
Serial.println(smoothedValue);
delay(100); // Esperar 100 ms antes de la siguiente muestra
}
El filtro de Savitzky-Golay suaviza la señal ajustando un polinomio de menor grado a un subconjunto de puntos de datos mediante regresión lineal. Es eficaz para mantener las características importantes de la señal, como picos y valles, mientras reduce el ruido.
Para implementar un filtro de Savitzky-Golay en una ESP32, se pueden utilizar librerías disponibles o escribir el algoritmo de convolución manualmente. A continuación, se muestra un ejemplo simplificado utilizando una ventana y coeficientes predefinidos.
#define WINDOW_SIZE 5
int coefficients[WINDOW_SIZE] = {-3, 12, 17, 12, -3}; // Coeficientes de Savitzky-Golay
int normalizing_factor = 35; // Suma de los coeficientes
float window[WINDOW_SIZE];
int windowIndex = 0;
void setup() {
Serial.begin(115200);
}
void loop() {
float newValue = analogRead(34) * (5.0 / 1023.0); // Leer el canal ADC y convertir a voltaje
window[windowIndex] = newValue;
windowIndex = (windowIndex + 1) % WINDOW_SIZE;
float smoothedValue = 0;
for (int i = 0; i < WINDOW_SIZE; i++) {
smoothedValue += window[(windowIndex + i) % WINDOW_SIZE] * coefficients[i];
}
smoothedValue /= normalizing_factor;
Serial.println(smoothedValue);
delay(100); // Esperar 100 ms antes de la siguiente muestra
}
Los métodos suavizadores de señales son fundamentales en diversas aplicaciones de sistemas embebidos, tales como: