Robustez em campo: prioridades, backpressure (slab/msgq), telemetria e VAD com histerese
Quando você sai do “demo de bancada” e vai para um firmware real, os problemas aparecem sempre nos mesmos lugares: perda de amostras, jitter, buffers lotando e VAD oscilando (entra/sai do stream o tempo todo). Esta seção é para deixar o pipeline “resistente” e fácil de diagnosticar.
7.1 Prioridades e latência: o produtor precisa ser “quase intocável”
A regra prática:
- Produtor (ADC contínuo): prioridade mais alta (número menor no Zephyr), porque ele está colado no fluxo de dados.
- Consumidor (DSP/UART): prioridade abaixo, porque DSP pode atrasar um pouco; perder amostra é pior.
- UART/LOG: se você imprimir demais no caminho do produtor, você cria jitter. O produtor deve logar raramente.
Exemplo típico:
#define PRODUCER_PRIO 2
#define CONSUMER_PRIO 4
E mais importante: nunca faça FFT/Goertzel no produtor. O produtor só:
- lê do driver contínuo,
- empacota frame,
- roda VAD leve,
- envia para fila.
7.2 Backpressure: quando slab ou msgq enchem, você precisa saber “por quê”
Na prática, vai acontecer de algum buffer encher (principalmente em debug). O erro é “silenciar” isso.
Crie uma estrutura de telemetria global:
/* src/telemetry.h */
#pragma once
#include <stdint.h>
struct stream_telemetry {
uint32_t adc_reads;
uint32_t frames_alloc_ok;
uint32_t frames_alloc_fail;
uint32_t msgq_put_ok;
uint32_t msgq_put_fail;
uint32_t vad_start;
uint32_t vad_end;
uint32_t frames_dropped_silence;
uint32_t frames_dropped_backpressure;
uint32_t consumer_frames;
uint32_t consumer_windows;
};
extern volatile struct stream_telemetry g_tlm;
Defina em um .c:
/* src/telemetry.c */
#include "telemetry.h"
volatile struct stream_telemetry g_tlm;
Agora, no produtor (adc_dma_producer.c), incremente contadores nos pontos críticos:
#include "telemetry.h"
/* ... dentro do loop ... */
g_tlm.adc_reads++;
if (k_mem_slab_alloc(&frame_slab, (void **)&frame, K_NO_WAIT) != 0) {
g_tlm.frames_alloc_fail++;
g_tlm.frames_dropped_backpressure++;
continue;
}
g_tlm.frames_alloc_ok++;
/* silêncio antes do stream ativo */
if (!s_voice_active && !vad_now) {
g_tlm.frames_dropped_silence++;
k_mem_slab_free(&frame_slab, (void *)frame);
continue;
}
/* eventos VAD */
if (!s_voice_active && vad_now) {
g_tlm.vad_start++;
/* ... */
}
if (s_voice_active && end_condition) {
g_tlm.vad_end++;
/* ... */
}
if (k_msgq_put(&stream_q, &msg, K_NO_WAIT) != 0) {
g_tlm.msgq_put_fail++;
g_tlm.frames_dropped_backpressure++;
k_mem_slab_free(&frame_slab, (void *)frame);
} else {
g_tlm.msgq_put_ok++;
}
No consumidor:
g_tlm.consumer_frames++;
/* quando processar uma janela DSP: */
g_tlm.consumer_windows++;
7.3 Uma “task” leve de diagnóstico (telemetria na UART sem travar o pipeline)
Você não quer imprimir em toda iteração do consumidor, muito menos do produtor. O ideal é ter uma terceira thread (bem baixa prioridade) que imprime um resumo a cada 1s.
/* src/diag_thread.c */
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include "telemetry.h"
void diag_thread(void *p1, void *p2, void *p3)
{
(void)p1; (void)p2; (void)p3;
while (1) {
k_sleep(K_SECONDS(1));
printk("TLM: adc_reads=%u alloc_ok=%u alloc_fail=%u msg_ok=%u msg_fail=%u "
"vad_start=%u vad_end=%u drop_sil=%u drop_bp=%u cons_frames=%u cons_win=%u\n",
g_tlm.adc_reads,
g_tlm.frames_alloc_ok, g_tlm.frames_alloc_fail,
g_tlm.msgq_put_ok, g_tlm.msgq_put_fail,
g_tlm.vad_start, g_tlm.vad_end,
g_tlm.frames_dropped_silence, g_tlm.frames_dropped_backpressure,
g_tlm.consumer_frames, g_tlm.consumer_windows);
}
}
Esse “painel” te diz rapidamente:
- se o consumidor está atrasando (
alloc_fail/msg_failsobem), - se o VAD está “quebrando” stream (
vad_start/vad_endmuito frequente), - se você está descartando silêncio em excesso (talvez threshold alto demais).
7.4 VAD por energia com histerese: reduz “chattering” (entra/sai toda hora)
O VAD simples de um threshold único é sensível: ruído perto do limiar faz o estado oscilar.
A solução clássica é histerese:
THR_ON(mais alto) para entrar no stream,THR_OFF(mais baixo) para sair do stream.
Além disso, mantenha hangover para não cortar palavras.
Exemplo (substitui a lógica do produtor):
#define VAD_THR_ON 1400u
#define VAD_THR_OFF 900u
#define VAD_HANGOVER_FR 12
static bool s_voice_active;
static int s_hang;
static void vad_update(uint32_t e_absmean)
{
if (!s_voice_active) {
if (e_absmean >= VAD_THR_ON) {
s_voice_active = true;
s_hang = VAD_HANGOVER_FR;
g_tlm.vad_start++;
emit_vad_event(STREAM_MSG_VOICE_START);
}
return;
}
/* já ativo */
if (e_absmean >= VAD_THR_OFF) {
s_hang = VAD_HANGOVER_FR; /* renova */
return;
}
/* caiu abaixo do THR_OFF */
s_hang--;
if (s_hang <= 0) {
s_voice_active = false;
g_tlm.vad_end++;
emit_vad_event(STREAM_MSG_VOICE_END);
}
}
Com isso, o VAD fica muito mais estável em ambientes reais.
7.5 Diagnóstico de “perda de dados”: como interpretar os sintomas
Se você observar:
frames_alloc_failsubindo: slab pequeno demais ou consumidor lento demais.
Ações: aumentarframe_slab(número de blocos), reduzir custo do DSP (passo maior, faixa menor), reduzir prints.msgq_put_failsubindo: fila pequena demais ou consumidor não está drenando.
Ações: aumentar profundidade dak_msgq, checar prioridade do consumidor, checar travas no consumidor.- VAD “metralhando” start/end: threshold ruim e/ou sem histerese/hangover.
Ações: histerese + hangover, calibrarTHR_ON/OFF. - Frequência errada/deslocada:
DSP_FS_HZnão bate com taxa real.
Ações: validar com tom conhecido e ajustarsample_freq_hz.