4 — Integração completa em tempo real: Goertzel + features + modelo estatístico + LED no RP2040
Nesta seção vamos ligar todas as peças construídas até agora e formar um sistema funcional em tempo real, mantendo o mesmo padrão de engenharia dos artigos anteriores da série.
O foco aqui não é despejar código final, mas mostrar claramente o fluxo, as decisões de integração e os pontos onde o aprendizado estatístico entra — e onde não deve entrar.
4.1 Visão geral do fluxo em tempo real
O pipeline completo agora é:
ADC
└── DC-block
└── Frame (256 amostras)
└── Goertzel (varredura + harmônicos)
└── Vetor de features
└── Distância estatística D
├── Decisão (histerese)
├── Atualização do modelo (condicional)
└── LED
Observe dois pontos importantes:
- O modelo estatístico não substitui o Goertzel
Ele apenas decide o que fazer com o que o Goertzel mediu. - O aprendizado é condicionado à decisão
Isso evita “aprender o erro”.
4.2 Estruturas globais de estado
Vamos declarar explicitamente os estados globais do sistema:
static frame_buf_t g_frame;
static dc_block_t g_dc;
static stat_model_t g_model;
static bool g_state = false;
static float g_f0_prev = 0.0f;
Esses estados são:
- Pequenos
- Determinísticos
- Fáceis de depurar
4.3 Construção do vetor de features em tempo real
Quando um frame fecha, calculamos:
- Energia total
- Frequência fundamental \(f_0\)
- Potências \(p_0, p_2, p_3\)
- Features \(f_1, f_2, f_3, f_4\)
Código ilustrativo:
static bool build_feature_vector(const float *frame,
feature_vec_t *v,
float *f0_out)
{
const float eps = 1e-9f;
float E = frame_energy(frame);
if (E < E_MIN) return false;
float p0 = 0.0f;
float f0 = find_best_f0_fast(frame, &p0);
const float nyq = 0.5f * (float)FS_HZ;
float p2 = 0.0f;
float p3 = 0.0f;
if (2.0f * f0 <= nyq)
p2 = power_at_freq(frame, 2.0f * f0);
if (3.0f * f0 <= nyq)
p3 = power_at_freq(frame, 3.0f * f0);
v->f1 = p0 / (E + eps);
v->f2 = p2 / (p0 + eps);
v->f3 = p3 / (p0 + eps);
v->f4 = fabsf(f0 - g_f0_prev);
g_f0_prev = f0;
if (f0_out) *f0_out = f0;
return true;
}
Note que:
- Todas as features são normalizadas
- Não usamos valores absolutos
- O custo computacional permanece baixo
4.4 Avaliação estatística e decisão
Com o vetor pronto:
- Calculamos a distância estatística (D)
- Aplicamos histerese
- Atualizamos o estado
static bool update_decision(const feature_vec_t *v)
{
float D = compute_stat_distance(&g_model, v);
bool new_state = stat_decision(D, g_state);
g_state = new_state;
return new_state;
}
4.5 Atualização do modelo (aprendizado online controlado)
Aqui está o ponto mais delicado do sistema:
👉 Só aprendemos quando acreditamos que o padrão é válido.
static void update_model_if_valid(const feature_vec_t *v)
{
const float alpha = 0.02f; // aprendizado lento e estável
if (g_state) {
stat_model_update(&g_model, v, alpha);
}
}
Esse detalhe:
- Evita adaptação ao ruído
- Evita “drift” do modelo
- Mantém estabilidade ao longo do tempo
4.6 Acionamento do LED (sem lógica escondida)
O LED agora reflete apenas o estado estatístico, não valores brutos:
static void update_led(uint led_gpio)
{
gpio_put(led_gpio, g_state ? 1 : 0);
}
Isso torna o sistema:
- Previsível
- Fácil de explicar
- Fácil de validar
4.7 Loop principal: simples, limpo e determinístico
O loop principal agora se resume a:
static void process_frame_if_ready(uint led_gpio)
{
if (!g_frame.full) return;
g_frame.full = false;
feature_vec_t v;
float f0 = 0.0f;
if (!build_feature_vector(g_frame.data, &v, &f0)) {
g_state = false;
update_led(led_gpio);
return;
}
update_decision(&v);
update_model_if_valid(&v);
update_led(led_gpio);
}
Repare:
- Nenhuma decisão depende de “mágica”
- Cada etapa é separada
- Tudo é mensurável
4.8 O que temos agora (antes do código final)
Neste ponto, o sistema já:
- Detecta assobios com Goertzel
- Extrai features robustas
- Aprende estatisticamente o padrão do usuário
- Se adapta a mudanças ambientais
- Decide com base probabilística
- Aciona um LED em tempo real
E ainda:
- Cabe confortavelmente no RP2040
- Não depende de bibliotecas externas
- É explicável e calibrável