4 — Goertzel em tempo real no RP2040: ADC + frames + múltiplas frequências (fundamental e harmônicos)
Agora vamos sair do “código de bancada” e colocar Goertzel para rodar em tempo real no RP2040, como fizemos no artigo anterior: amostragem estável, coleta de frame e processamento incremental.
A diferença é que aqui não montamos FFT nem cepstrum. Em vez disso, vamos calcular a energia em poucas frequências (fundamental + harmônicos) e criar um critério de decisão.
4.1 Definindo o frame e a taxa de amostragem
Vamos manter as escolhas do artigo anterior para facilitar comparação:
- \(f_s = 8000\ \text{Hz}\)
- \(N = 256\) amostras por frame
Resolução em frequência:
\[
\Delta f = \frac{f_s}{N} \approx 31{,}25\ \text{Hz}
\]
Isso é suficiente para assobio humano.
4.2 Estratégia “Goertzel multibanda”
A pergunta prática: quais frequências devo testar?
Para assobio, há duas abordagens comuns:
A) Varredura de bins na faixa (mais robusta)
- Testa vários (k) entre 800 e 3000 Hz
- Escolhe o pico
B) “Fundamental + harmônicos” (mais seletiva)
- Escolhe um pico candidato na faixa
- Confirma energia em (2f_0) e (3f_0)
Aqui vamos adotar uma estratégia híbrida, eficiente e robusta:
- Rodar Goertzel em uma pequena varredura (ex.: 800–3000 Hz, passo ~62,5 Hz = 2 bins)
- Encontrar o melhor candidato (f_0)
- Confirmar harmônicos (2f_0) e (3f_0) com mais 2 ou 3 chamadas de Goertzel
Isso reduz falsos positivos sem explodir CPU.
4.3 Aquisição em tempo real (ADC + timer), igual ao artigo anterior
Vamos estruturar igual:
- Callback de timer coleta amostra do ADC
- Empurra para buffer de frame
- Loop principal processa quando frame fecha
4.3.1 Buffer de frame (o mesmo padrão)
#define FS_HZ 8000
#define FRAME_N 256
typedef struct {
float data[FRAME_N];
uint32_t idx;
bool full;
} frame_buf_t;
static inline void frame_buf_init(frame_buf_t *b) {
b->idx = 0;
b->full = false;
}
static inline void frame_buf_push(frame_buf_t *b, float v) {
b->data[b->idx++] = v;
if (b->idx >= FRAME_N) {
b->idx = 0;
b->full = true;
}
}
4.3.2 Leitura ADC normalizada
static inline float read_adc_normalized(void)
{
uint16_t raw = adc_read(); // 0..4095
return ((float)raw - 2048.0f) / 2048.0f;
}
Observação: se seu microfone tiver offset diferente, você pode substituir por uma média móvel depois. Por enquanto, isso já funciona bem na maioria dos módulos.
4.4 Pré-processamento mínimo: remoção de DC por média móvel (mais robusto)
No mundo real, o offset do microfone varia. Vamos remover DC com um filtro de 1ª ordem:
\[
\mu[n] = \mu[n-1] + \beta (x[n] – \mu[n-1])
\]
\[
x_{ac}[n] = x[n] – \mu[n]
\]
Implementação:
typedef struct {
float mean;
} dc_block_t;
static inline void dc_block_init(dc_block_t *d) { d->mean = 0.0f; }
static inline float dc_block_step(dc_block_t *d, float x)
{
const float beta = 0.0015f; // ajuste leve
d->mean += beta * (x - d->mean);
return x - d->mean;
}
4.5 Varredura com Goertzel (faixa 800–3000 Hz)
Vamos definir a faixa:
F_MIN = 800 HzF_MAX = 3000 Hz
E usar passo de 2*Δf, ou seja, ~62,5 Hz, para reduzir custo.
Número de bins aproximado:
\[
\text{bins} \approx \frac{3000-800}{62.5} \approx 35
\]
Ou seja: ~35 detectores por frame (bem viável no RP2040).
4.5.1 Função: potência Goertzel para uma frequência f0 em um frame
Nós vamos reaproveitar as funções da Seção 3, mas agora em um helper:
static float goertzel_power_at(const float *x, float f0, float fs, int N)
{
goertzel_t g;
goertzel_init(&g, f0, fs, N);
for (int n = 0; n < N; n++) {
goertzel_step(&g, x[n]);
}
return goertzel_power(&g);
}
Sim, aqui re-inicializamos o Goertzel a cada frame. Isso é deliberado: torna o código claro e fácil de manter. Na próxima seção podemos otimizar pré-computando coeficientes.
4.6 Encontrando o candidato de fundamental (pico na faixa)
Agora vamos varrer e pegar o melhor:
#define F_MIN 800.0f
#define F_MAX 3000.0f
static float find_best_f0(const float *frame, float *best_power_out)
{
const float fs = (float)FS_HZ;
const float df = fs / (float)FRAME_N; // ~31.25 Hz
const float step = 2.0f * df; // ~62.5 Hz
float best_f = F_MIN;
float best_p = -1.0f;
for (float f = F_MIN; f <= F_MAX; f += step) {
float p = goertzel_power_at(frame, f, fs, FRAME_N);
if (p > best_p) {
best_p = p;
best_f = f;
}
}
if (best_power_out) *best_power_out = best_p;
return best_f;
}
4.7 Confirmação por harmônicos (2f0 e 3f0)
Com o candidato f0, confirmamos:
- energia em
2*f0 - energia em
3*f0(opcional) - comparações relativas (para não depender só de um limiar absoluto)
static bool harmonic_confirm(const float *frame, float f0, float p0)
{
const float fs = (float)FS_HZ;
float f2 = 2.0f * f0;
float f3 = 3.0f * f0;
// harmônicos devem estar dentro do Nyquist (fs/2)
if (f2 > fs * 0.5f) return false;
float p2 = goertzel_power_at(frame, f2, fs, FRAME_N);
float p3 = (f3 <= fs * 0.5f) ? goertzel_power_at(frame, f3, fs, FRAME_N) : 0.0f;
// Critério prático:
// - fundamental precisa ser forte
// - 2º harmônico deve existir com fração mínima
// - 3º harmônico ajuda, mas pode ser fraco dependendo do assobio
const float r2 = (p2 / (p0 + 1e-9f));
const float r3 = (p3 / (p0 + 1e-9f));
bool ok2 = (r2 > 0.08f); // ajuste prático
bool ok3 = (r3 > 0.02f); // opcional
return ok2 || ok3;
}
4.8 Integração: montar o “processador de frame” (ainda sem LED)
Agora juntamos:
- energia do frame (gate de silêncio)
- encontrar melhor f0
- confirmar harmônicos
- retornar um “score” de detecção
static float frame_energy(const float *x)
{
float acc = 0.0f;
for (int i = 0; i < FRAME_N; i++) acc += x[i] * x[i];
return acc / (float)FRAME_N;
}
static bool process_frame_goertzel(const float *frame, float *f0_out, float *p0_out)
{
const float E = frame_energy(frame);
const float E_MIN = 0.0008f; // ajuste
if (E < E_MIN) return false;
float p0 = 0.0f;
float f0 = find_best_f0(frame, &p0);
if (!harmonic_confirm(frame, f0, p0)) return false;
if (f0_out) *f0_out = f0;
if (p0_out) *p0_out = p0;
return true;
}