Em sistemas embarcados, “tirar uma frequência específica do sinal” costuma aparecer em dois cenários muito práticos: remover uma interferência estreita e persistente (por exemplo, 50/60 Hz de rede, ou um tom de comutação) e aumentar SNR (relação sinal-ruído) quando o fenômeno de interesse é repetitivo e existe uma referência de fase (por exemplo, rotação com encoder, comutação PWM, ou zero-cross da rede). O filtro notch discreto resolve muito bem o primeiro caso porque ele cria uma atenuação profunda numa frequência central (f_0) com pouca alteração do resto do espectro. Já a média sincronizada resolve muito bem o segundo porque ela soma ciclos “alinhados” no tempo/fase, reforçando o componente coerente e cancelando ruído não correlacionado e componentes que não encaixam exatamente naquele período.
Quando o firmware precisa ser previsível (tempo real), o desenho do filtro não é só “matemático”: você escolhe estruturas e limites para garantir custo computacional constante, estado pequeno e estabilidade numérica, principalmente se você for para ponto fixo ou tiver ADC+DMA alimentando o pipeline. (Essa preocupação de arquitetura e previsibilidade é típica de projetos de tempo real.)
1) Notch discreto: o “biquad” que apaga uma frequência
Um notch digital clássico pode ser implementado como um biquad IIR (segunda ordem) com zeros exatamente na frequência que você quer cancelar e polos próximos, controlando a largura do “buraco” via fator de qualidade (Q). Quanto maior o (Q), mais estreito é o notch (ótimo para interferência tonal estável), mas maior a sensibilidade a variação de (f_0) e a quantização dos coeficientes. Em termos práticos: para hum de rede em 60 Hz, se o sinal tiver variação de frequência (rede oscilando, ou a interferência “escorregando”), um (Q) alto demais pode deixar “vazar” parte do ruído. Para tom de comutação de um inversor ou fonte chaveada, se o clock for estável, dá para usar (Q) alto e atacar de forma cirúrgica.
O biquad notch mais comum pode ser escrito como:
\[
H(z)=\frac{1 – 2\cos(\omega_0)z^{-1} + z^{-2}}{1 – 2r\cos(\omega_0)z^{-1} + r^2 z^{-2}}
\]
onde \(\omega_0=2\pi f_0/F_s\) e (r) controla a “proximidade” dos polos aos zeros (quanto mais perto de 1, mais estreito e mais profundo). Uma ligação prática entre (r) e (Q) é aproximar a largura de banda por \(BW \approx f_0/Q\) e usar \(r \approx e^{-\pi BW/F_s}\). Isso funciona muito bem para firmware porque você calcula coeficientes uma vez (ou quando \(f_0\) muda) e o processamento por amostra fica constante.
Código em C: biquad notch (float) com inicialização por \(F_s, f_0, Q\)
#include <math.h>
#include <stdint.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
typedef struct {
// Coeficientes normalizados (a0 = 1)
float b0, b1, b2;
float a1, a2;
// Estados (forma direta II transposta)
float z1, z2;
} NotchBiquad;
/**
* @brief Inicializa um filtro notch IIR (biquad) em forma direta II transposta.
* @param f Ponteiro para a estrutura do filtro.
* @param Fs Frequência de amostragem (Hz).
* @param f0 Frequência central do notch (Hz).
* @param Q Fator de qualidade (adimensional). Ex.: 10..50 típicos.
*
* Observação prática:
* - Q maior => notch mais estreito, mais sensível a variações de f0 e quantização.
* - Q menor => notch mais largo, remove mais “vizinhança” de frequências.
*/
static inline void notch_init(NotchBiquad *f, float Fs, float f0, float Q)
{
const float w0 = 2.0f * (float)M_PI * (f0 / Fs);
// Largura de banda aproximada (Hz)
const float BW = f0 / Q;
// Polo raio r (aprox.) -> define a largura do notch
const float r = expf(-(float)M_PI * (BW / Fs));
const float c = cosf(w0);
// Numerador (zeros no círculo unitário em ±w0)
const float b0 = 1.0f;
const float b1 = -2.0f * c;
const float b2 = 1.0f;
// Denominador (polos em raio r)
const float a0 = 1.0f;
const float a1 = -2.0f * r * c;
const float a2 = r * r;
// Normaliza por a0 (a0=1 aqui, mas mantemos o padrão)
f->b0 = b0 / a0;
f->b1 = b1 / a0;
f->b2 = b2 / a0;
f->a1 = a1 / a0;
f->a2 = a2 / a0;
f->z1 = 0.0f;
f->z2 = 0.0f;
}
/**
* @brief Processa 1 amostra pelo biquad notch (DF-II Transposta).
* @param f Estrutura do filtro.
* @param x Amostra de entrada.
* @return Amostra filtrada.
*/
static inline float notch_process(NotchBiquad *f, float x)
{
// DF-II transposta:
// y = b0*x + z1
// z1 = b1*x - a1*y + z2
// z2 = b2*x - a2*y
const float y = (f->b0 * x) + f->z1;
const float z1 = (f->b1 * x) - (f->a1 * y) + f->z2;
const float z2 = (f->b2 * x) - (f->a2 * y);
f->z1 = z1;
f->z2 = z2;
return y;
}
Melhores usos do notch: quando você conhece (ou mede) a frequência indesejada e ela é estreita, relativamente estável e você quer preservar quase tudo ao redor. Em instrumentação, isso aparece em leitura de shunt/ADC contaminada por rede; em áudio, hum; em controle, ressonância mecânica estreita (desde que não destrua margem de fase indevidamente); em eletrônica de potência, uma componente tonal em corrente/tensão.
2) Média sincronizada: “soma coerente” alinhada à fase
A média sincronizada (às vezes chamada de synchronous averaging, time synchronous averaging) não é “só uma média móvel”. A sacada é que você escolhe uma janela exatamente igual a um período do fenômeno repetitivo (ou múltiplos inteiros) e reinicia/alinha essa janela usando um evento de referência: um zero-cross, um pulso de encoder, um índice, ou um marcador derivado do PWM. Se você tem (N) amostras por período e faz uma média sobre (K) períodos alinhados, o componente que se repete com a mesma fase soma e cresce proporcionalmente, enquanto ruído aleatório cai como (\sqrt{K}) no RMS. Além disso, componentes que não “encaixam” naquele período tendem a se cancelar.
Em termos de filtro, isso se comporta como um filtro tipo comb (pente) extremamente eficiente para rejeitar tudo que não é coerente com o período escolhido. A diferença para um notch é que você não está mirando uma única frequência: você está favorecendo toda a forma de onda repetitiva no domínio do tempo, desde que sincronizada.
Código em C: média sincronizada por períodos (com referência externa)
A seguir vai uma implementação simples (e bem útil) para firmware: você acumula amostras de cada posição dentro do período e, a cada período completo, você atualiza a média. Isso é perfeito quando você tem um “gatilho de início de período” (por exemplo, interrupção de índice do encoder ou zero-cross).
#include <stdint.h>
#include <string.h>
typedef struct {
uint16_t N; // amostras por período
uint16_t idx; // posição atual no período [0..N-1]
uint32_t K; // quantos períodos já acumulados (limite/controle externo)
// Buffers de acumulação (use int64 se seu range for grande)
int64_t *acc; // soma por posição
int32_t *avg; // média por posição (resultado)
} SyncAvg;
/**
* @brief Inicializa a média sincronizada.
* @param s Estrutura.
* @param acc Buffer de acumulação de tamanho N (int64_t).
* @param avg Buffer de saída média de tamanho N (int32_t).
* @param N Amostras por período.
*/
static inline void syncavg_init(SyncAvg *s, int64_t *acc, int32_t *avg, uint16_t N)
{
s->N = N;
s->idx = 0;
s->K = 0;
s->acc = acc;
s->avg = avg;
memset(s->acc, 0, (size_t)N * sizeof(int64_t));
memset(s->avg, 0, (size_t)N * sizeof(int32_t));
}
/**
* @brief Deve ser chamado quando ocorre o "marcador" de início de período (ex.: zero-cross ou índice do encoder).
* Isso força alinhamento de fase.
*/
static inline void syncavg_period_reset(SyncAvg *s)
{
s->idx = 0;
}
/**
* @brief Alimenta a média sincronizada com uma amostra do ADC já alinhada ao relógio de amostragem.
* @param s Estrutura.
* @param x Amostra (ex.: ADC já convertido para int32).
*
* Funcionamento:
* - A cada amostra, acumula na posição idx.
* - Ao completar N amostras, fecha um período e atualiza avg[].
* - A referência de fase vem de syncavg_period_reset() (externa).
*/
static inline void syncavg_push_sample(SyncAvg *s, int32_t x)
{
s->acc[s->idx] += (int64_t)x;
s->idx++;
if (s->idx >= s->N) {
s->idx = 0;
s->K++;
// Atualiza a média inteira por posição (custo O(N) por período).
// Se N for grande e seu MCU for apertado, dá para atualizar de forma incremental.
for (uint16_t i = 0; i < s->N; i++) {
s->avg[i] = (int32_t)(s->acc[i] / (int64_t)s->K);
}
}
}
Melhores usos da média sincronizada: quando existe um evento/clock de referência que define o período do fenômeno e você quer extrair o comportamento repetitivo com máxima imunidade a ruído. Isso aparece em análise vibroacústica sincronizada com rotação (encoder), em medição de ripple sincronizada com PWM, em leitura de sinais biomédicos quando há marcador, e em sistemas de potência quando você quer “ver” a forma média ao longo de ciclos de rede sem ser enganado por ruído ou eventos transientes fora de fase.
3) Exemplo prático combinando os dois: remover hum e depois reforçar a forma repetitiva
Em pipeline real, é comum usar notch primeiro para derrubar uma interferência tonal forte e, em seguida, média sincronizada para aumentar SNR do que restou (principalmente se o sinal útil é repetitivo e você tem referência). A ordem pode inverter dependendo do caso: se a interferência também for coerente com o mesmo período, a média pode reforçá-la, então o notch antes costuma ser mais seguro.
Abaixo, um esqueleto de uso. Imagine ADC a 4 kHz, hum em 60 Hz, e você quer fazer média sincronizada por ciclo de 60 Hz usando zero-cross (logo \(N \approx 4000/60 \approx 66\) amostras por ciclo; na prática você ajusta para manter N inteiro e usar PLL/medição de período se a rede variar).
#include <stdio.h>
// Reaproveita NotchBiquad e SyncAvg já definidos acima.
#define FS_HZ 4000.0f
#define NOTCH_F0_HZ 60.0f
#define NOTCH_Q 25.0f
#define N_SAMPLES_PER_PERIOD 66 // exemplo (depende do seu sincronismo real)
static int64_t acc_buf[N_SAMPLES_PER_PERIOD];
static int32_t avg_buf[N_SAMPLES_PER_PERIOD];
int main(void)
{
NotchBiquad notch;
notch_init(¬ch, FS_HZ, NOTCH_F0_HZ, NOTCH_Q);
SyncAvg s;
syncavg_init(&s, acc_buf, avg_buf, N_SAMPLES_PER_PERIOD);
// Exemplo: loop de aquisição (mock)
for (int n = 0; n < 20000; n++) {
// Em firmware real: x_raw vem do ADC (DMA buffer), e zero-cross chama syncavg_period_reset(&s)
int32_t x_raw = (int32_t)(1000 * sinf(2.0f * (float)M_PI * 10.0f * (n / FS_HZ))); // sinal útil 10 Hz
x_raw += (int32_t)(300 * sinf(2.0f * (float)M_PI * 60.0f * (n / FS_HZ))); // hum 60 Hz
// Notch (float) -> converte de volta para int32
float y_notch = notch_process(¬ch, (float)x_raw);
int32_t y = (int32_t)y_notch;
// Evento externo de sincronismo: aqui é só demonstração (a cada N amostras)
if ((n % N_SAMPLES_PER_PERIOD) == 0) {
syncavg_period_reset(&s);
}
// Média sincronizada
syncavg_push_sample(&s, y);
// Quando s.K aumenta, avg_buf contém a forma média por período
if (s.K > 0 && (n % (N_SAMPLES_PER_PERIOD * 20)) == 0) {
printf("K=%lu, avg[0]=%ld, avg[10]=%ld\n",
(unsigned long)s.K, (long)avg_buf[0], (long)avg_buf[10]);
}
}
return 0;
}
Fechando a ideia: o notch discreto e a média sincronizada resolvem problemas parecidos (melhorar a qualidade do sinal), mas por “mecanismos” bem diferentes, e isso muda totalmente quando cada um é a melhor escolha. O notch é a ferramenta certa quando você conhece uma frequência indesejada bem definida e relativamente estável e quer arrancá-la do sinal com o mínimo de impacto no restante do espectro. Em firmware, isso costuma ser “hum” de 50/60 Hz, tons de chaveamento, ou uma ressonância estreita que aparece como pico bem localizado. O ponto crítico é que o notch é tão bom quanto a precisão do seu (f_0) e a escolha de (Q): se a interferência varia de frequência, um notch muito estreito deixa passar; se você alarga demais, começa a “machucar” conteúdo útil perto de (f_0). Além disso, como é um IIR, você precisa cuidar de estabilidade numérica e do formato de implementação (a forma direta II transposta tende a ser mais robusta em ponto flutuante e também costuma ser a melhor porta de entrada para depois migrar para ponto fixo).
Já a média sincronizada não é “um filtro de frequência” no sentido clássico; ela é uma técnica de extração por coerência: tudo que está alinhado com o período de referência fica mais forte, e o que não está alinhado tende a desaparecer. Por isso ela é superior quando o sinal útil é repetitivo e você tem um marcador de fase confiável, como encoder em máquina rotativa, o próprio PWM em conversores/inversores, ou zero-cross da rede. O ganho prático é enorme porque ela aumenta SNR sem precisar “inventar” um modelo espectral do ruído, mas ela também tem uma fragilidade: se o sincronismo for ruim (jitter, período variável, marcador inconsistente) a média “borrará” a forma de onda e pode até criar artefatos que parecem sinal real. Em projetos de rede elétrica, por exemplo, se você fixa (N) como “amostras por ciclo” sem acompanhar a variação real da frequência, a média começa a perder fase ao longo dos ciclos; nesse caso, ou você mede o período e ajusta (N) dinamicamente, ou você reamostra o ciclo para um grid fixo antes de acumular.
Na prática, em pipeline embarcado, uma combinação muito comum é usar notch primeiro para remover uma interferência tonal forte e depois usar média sincronizada para revelar a forma repetitiva de interesse com ruído bem mais baixo. Isso funciona especialmente bem quando a interferência não é coerente com o período que você está usando para sincronizar; se for coerente, a média pode reforçar a interferência, e aí o notch vira praticamente obrigatório antes. Se o seu sistema estiver no limite de CPU, o notch custa um número fixo e pequeno de multiplicações por amostra, enquanto a média sincronizada pode custar pouco por amostra mas “cobra” um custo por período quando você atualiza a forma média; dá para manter determinismo atualizando médias de forma incremental, ou reduzindo taxa, ou usando buffers e processamento em tarefa de menor prioridade.