O objetivo principal é avaliar o desempenho de um filtro de média móvel (variando de 4 à 10 passos) usado para filtrar dados de iluminação capturados usando um sensor de luz (LDR). Este algoritmo deve ser executado dentro de uma ISR à taxa de 1 à 10 Hz. O valor filtrado deve ser usado para realizar um controle de intensidade luminosa de um led.
Componentes necessários:
Conexões:
A equação geral de um filtro de média móvel é dado abaixo:
Onde: saída filtrada; amostra atual; instante de amostragem; número de passos do filtro; uma amostra passada do sinal à ser filtrado.
A equação anterior pode ser modificada para:
Onde: .
Note que necessitamos amostras atrasadas do sinal de entrada .
ou generalizando:
Ou você pode rever a teoria em: Filtro de Média Móvel.
Exemplo 1: Filtro de média móvel simples de 4 passos.
Neste caso, o diagrama de fluxo de sinal deste filtro fica como mostra a figura abaixo:
Código exemplo:
xxxxxxxxxx
/***
Testando filtro de média móvel de 4 passos
Fernando Passold, em 18/06/2024
Dado de entrada: valor lido por um LDR em série com resistor de 10 KOhms,
o ponto central está conectado à entrada analógica A0; a outra ponta do
resistor está no GND e a outra ponta do LDR está em 5 Volts (Vcc)
Este LDR está "acoplado" com um led externo conectado no pino 5 (saída PWM),
cujo duty-cycle poderia ser controlado
****/
const byte analogPin = A0; // pino (A/D) que recebe sinal analógico
const byte PIN_LED = 5; // Led à ser "controlado", no pino 5 (ou 6)
float b;
float x, x1, x2, x3; // sinal de entrada + amostras atrasadas até x3=x[n-3]
float y; // saída filtrada
void Init_Filtro(void){
x1=0; x2=0; x3=0; y=0;
b=1.0/(float)NUM_SAMPLES; // peso de cada amostra para este filtro
}
void setup() {
// put your setup code here, to run once:
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, LOW); // led inicia apagado
Serial.begin(115200); // setup serial
while (!Serial) {
; // Aguarda até que a porta serial esteja pronta - normalmente 10 ms
}
Init_Filtro();
Serial.println(" ");
Serial.print("Filtro média móvel ");
Serial.print(NUM_SAMPLES);
Serial.println(" amostras");
Serial.print("b=");
Serial.println(b, 4);
Serial.println(" "); // apenas pula 1 linha
delay(500); // pausa para usuário ver valor de b
}
float Filtro(void){
float y = 0;
y = b*x + b*x1 + b*x2 + b*x3;
// atualizando amostras passadas de x:
x3 = x2;
x2 = x1;
x1 = x;
return y;
}
void loop() {
// put your main code here, to run repeatedly:
x = analogRead(analogPin)*(5.0/1024.0); // converte para faixa 0 á 5 Volts
y = Filtro();
Serial.print(x, 4);
Serial.print(" \t");
Serial.println(y, 4);
delay(100); // para tentar garantir taxa de amostragem de 10 Hz
}
O código anterior deve gerar uma saída como (usando "Serial Plotter" do Arduino):
A linha azul representa o sinal medido (de entrada), e a linha vermelha, o sinal filtrado .
Este filtro reage mais ou menos rápido mas pode ser percebeido que neste caso, não é capaz de remover todas as oscilações. Por exemplo:
Provavelmente aumentando o número de passos, se consiga melhorar a rejeição aos picos.
Exemplo: Seja , um filtro de média móvel de 10 passos. Ou número qualquer de passos ←NUM_SAMPLES.
xxxxxxxxxx
/***
Testando filtro de média móvel de NUM_SAMPLES passos
Fernando Passold, em 18/06/2024
Dado de entrada: valor lido por um LDR em série com resistor de 10 KOhms,
o ponto central está conectado à entrada analógica A0; a outra ponta do
resistor está no GND e a outra ponta do LDR está em 5 Volts (Vcc)
Este LDR está "acoplado" com um led externo conectado no pino 5 (saída PWM),
cujo duty-cycle poderia ser controlado
****/
const byte analogPin = A0; // pino (A/D) que recebe sinal analógico
const byte PIN_LED = 5; // Led à ser "controlado", no pino 5 (ou 6)
float b;
float x[NUM_SAMPLES]; // sinal de entrada + amostras atrasadas até x3=x[n-3]
float y; // saída filtrada
void Init_Filtro(){
for (int i = 0; i < NUM_SAMPLES; i++) {
x[i] = 0;
}
b=1.0/(float)NUM_SAMPLES; // peso de cada amostra para este filtro
}
void setup() {
// put your setup code here, to run once:
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, LOW); // led inicia apagado
Serial.begin(115200); // setup serial
while (!Serial) {
; // Aguarda até que a porta serial esteja pronta - normalmente 10 ms
}
Init_Filtro();
Serial.println(" ");
Serial.print("Filtro média móvel ");
Serial.print(NUM_SAMPLES);
Serial.println(" amostras");
Serial.print("b=");
Serial.println(b, 4);
Serial.println(" "); // apenas pula 1 linha
delay(500); // pausa para usuário ver valor de b
}
float Filtro(void) {
float y = 0;
int i;
for(i = 0; i < NUM_SAMPLES; i++) {
y += b*x[i];
}
// atualizando amostras passadas de x
for(i=NUM_SAMPLES-1; i>0; i--){
x[i]=x[i-1];
}
return y;
}
void loop() {
// put your main code here, to run repeatedly:
x[0] = analogRead(analogPin)*(5.0/1024.0); // converte para faixa 0 á 5 Volts
y = Filtro();
Serial.print(x[0], 4);
Serial.print(" \t");
Serial.println(y, 4);
delay(100); // para tentar garantir taxa de amostragem de 10 Hz
}
Este código produz a seguinte saída:
No gráfico anterior, a linha azul se refere aos valores brutos lidos pelo sensor e a linha vermelha representa estes dados filtrados.
Note que o LDR está tendo sua iluminação bloqueada (propositalmente) em certos instantes de tempo (motivo das perturbações).
Note que como o fitlro é de 10 passos. Porém esta quantidade pode provocar um certo atraso na resposta (no sinal filtrado).
Este filtro pode ser útil para filtrar situações como a mostrada na figura abaixo:
Apesar do sensor parecer se encontrar "estático" (sem perturbações visuais aparentes), seu sinal oscila ou apresenta certos picos, e neste caso, percebe-se que o filtro é capaz de suprimir estes picos.
Você pode tentar testar este filtro e outros usando um arquivo de dados gerado com capture_serial_data.py (detalhado em: Capturando e Analisando Dados (via porta serial)), e depois eventualmente usar a função filter
do MATLAB para avaliar os resultados que poderiam ser obtidos.
Um buffer circular ou circular é uma estrutura de dados de tamanho fixo comumente usada em aplicativos de software em tempo real para armazenar um número predefinido de valores. A analogia de um anel com um número fixo de posições é bastante útil para capturar a natureza FIFO (First-In-First-Out) de tal estrutura de dados. Quando o buffer estiver cheio, o primeiro elemento que foi gravado (“In”) no buffer é o primeiro a ser substituído (“Out”) pelo próximo elemento recebido.
Os buffers circulares são particularmente úteis em situações em que os dados são continuamente amostrados e os cálculos precisam ser feitos usando um tamanho de amostra predefinido ou é necessária uma visualização contínua.
Neste caso, definimos um "anel" (vetor) do tamanho requerido para o número de amostras e simplesmente controlador um apontador para a posição da próxima amostra. Enquanto o final deste buffer não é atingido, nenhum valor é sobre-escrito, mas depois que o número máximo de amostras é ultrapassado, os novos dados passar a sobre-escrever os dados mais antigos. A tabela a seguir ilustra o funcionamento de um buffer circular para 10 amostras::
Primeiras 4 amostras | 10a-amostra | 11a-amostra |
---|---|---|
Mais referências:
Exemplo 3: Um código mais aprimorado usando "buffer circular":
Código:
xxxxxxxxxx
volatile int y; // Variável que contém a nova amostra lida
volatile int samples[NUM_SAMPLES]; // Array para armazenar as últimas 10 amostras
volatile long sum = 0; // Soma das últimas 10 amostras (usando long para evitar overflow)
volatile int sampleIndex = 0; // Índice circular para o array de amostras
volatile int numSamplesCollected = 0; // Contador de amostras coletadas
void setup() {
// Configuração inicial do Arduino
Serial.begin(9600);
// Inicializa o array de amostras com zeros
for (int i = 0; i < NUM_SAMPLES; i++) {
samples[i] = 0;
}
// Configura a interrupção para leitura do sensor
attachInterrupt(digitalPinToInterrupt(2), readSensor, RISING);
}
void loop() {
// O loop principal pode estar vazio, pois a leitura e o cálculo são feitos na interrupção
}
void readSensor() {
// Simulação da leitura do sensor
y = analogRead(A0); // Suponha que o sensor está conectado ao pino A0
// Remove a amostra mais antiga da soma
sum -= samples[sampleIndex];
// Armazena a nova amostra no array
samples[sampleIndex] = y;
// Adiciona a nova amostra à soma
sum += y;
// Avança o índice circular
sampleIndex = (sampleIndex + 1) % NUM_SAMPLES;
// Conta o número de amostras coletadas até que tenhamos coletado 10 amostras
if (numSamplesCollected < NUM_SAMPLES) {
numSamplesCollected++;
}
// Calcula a média móvel continuamente
int movingAverage = sum / numSamplesCollected;
Serial.println(movingAverage); // Envia a média móvel para o Serial Monitor
}
Explicação do Código
samples[NUM_SAMPLES]
armazena as últimas 10 amostras.sampleIndex
indica a posição atual no array onde a nova amostra será armazenada.sum
é a soma das amostras no array, usada para calcular a média.numSamplesCollected
mantém o número de amostras coletadas até que atinja 10.Funcionamento
Inicialização: No setup
, inicializamos o array de amostras e configuramos a interrupção.
Interrupção: Em readSensor
, que é chamada a cada interrupção:
y
.sampleIndex
.sampleIndex
de forma circular.Essa abordagem garante que a média móvel das últimas 10 amostras é sempre atualizada de forma eficiente a cada nova leitura de amostra.
Atenção: código sugerido pelo ChatGPT.
Note que você poderia aumentar a taxa de amostragem do sinal do LDR, realizar uma FFT sobre uma amostra deste sinal e descobrir os componentes ruidosos presentes neste sinal e então aplicar um filtro passa-baixas sobre o sinal do LDR. Eventualmente o resultado desta filtragem será igual ou melhor ao filtro de média móvel com a certeza de que, neste caso, você estará filtrando componentes não desejadas no sinal de entrada (ruídos).
🌊 Fernando Passold 📬 ,