Fechando o projeto: mapeando ADC pelo DeviceTree, janela deslizante com overlap e Goertzel em ponto fixo (Q31)
Nesta seção eu amarro as três “pontas soltas” que deixam o exemplo com cara de projeto real:
- O produtor não pode ficar fixo em
ADC_UNIT_1 + ADC_CHANNEL_0. Vamos ler isso do DeviceTree viaadc_dt_spec(o mesmo nó/appcomio-channelsque você já configurou). - Janela deslizante com overlap (ex.: 50%) para uma estimativa mais estável e “contínua” durante o stream.
- Um caminho sem
float(Goertzel em Q31) para quando você quiser custo previsível e menor latência (principalmente se você também estiver rodando VAD/DSP pesado).
5.1 Produtor: traduzindo adc_dt_spec (DeviceTree) para unidade/canal do ESP-IDF
O Zephyr te entrega um struct adc_dt_spec que contém:
- qual device ADC,
- canal,
- configurações do canal (ganho, referência, aquisição, etc.).
Só que o driver contínuo do ESP-IDF quer adc_unit_t e adc_channel_t. O “pulo do gato” é criar uma pequena camada de mapeamento por SoC/board.
Crie um arquivo src/adc_dt_map_esp32.h:
#pragma once
#include <zephyr/drivers/adc.h>
#include "driver/adc_common.h" /* adc_unit_t, adc_channel_t */
/*
* Mapear canal do Zephyr -> canal do ESP-IDF:
* - No ESP32, os canais do ADC são definidos por macros do IDF (ADC_CHANNEL_0..)
* - O Zephyr costuma usar índices de canal (0..n) no DT.
*
* Em geral: canal "0" no DT -> ADC_CHANNEL_0.
* Mas atenção: em alguns SoCs há diferenças entre ADC1/ADC2 e o roteamento de pinos.
*/
static inline adc_channel_t z_chan_to_idf_channel(uint8_t z_chan)
{
return (adc_channel_t)z_chan; /* funciona quando a enum do IDF é sequencial */
}
/*
* Descobrir unit:
* Isso depende do “dev” que veio do DT. Há várias formas:
* - por Kconfig/SoC fixo (se você usar sempre ADC1)
* - por comparação com device name (menos elegante)
* - por DT: se você expuser no nó do ADC uma propriedade “unit” ou usar labels distintos adc1/adc2
*
* Para um template didático, eu assumo ADC1 (unit 1). Se você quiser ADC2, eu te passo
* um overlay com propriedade extra + leitura via DT.
*/
static inline adc_unit_t z_dev_to_idf_unit(const struct device *adc_dev)
{
ARG_UNUSED(adc_dev);
return ADC_UNIT_1;
}
Agora atualize o produtor (adc_stream_thread) para pegar unit/ch do adc_spec:
/* dentro do adc_stream_thread() */
#include <zephyr/drivers/adc.h>
#include "adc_dt_map_esp32.h"
/* adc_spec já veio do DT (seção 2) */
extern const struct adc_dt_spec adc_spec;
const adc_unit_t unit = z_dev_to_idf_unit(adc_spec.dev);
const adc_channel_t ch = z_chan_to_idf_channel(adc_spec.channel_id);
E pronto: o canal que você declarou no overlay passa a ser “fonte única de verdade”.
Se você realmente precisa escolher entre ADC1 e ADC2 via DT, o método mais limpo é: criar um nó de aplicação com uma propriedade adicional
espressif,adc-unit = <1>;e ler comDT_PROP(APP_NODE, espressif_adc_unit). Eu te passo esse overlay/dts-binding na sequência se quiser padronizar.
5.2 Consumidor: janela deslizante com overlap (50%) para suavizar a estimativa
Hoje, a cada janela cheia você zera s_win_fill = 0. Isso gera uma leitura “aos trancos”. O comportamento típico de processamento de áudio é overlap (ex.: 50%):
- Você analisa 1024 amostras.
- Depois “desloca” 512 e aproveita as últimas 512, adicionando 512 novas.
Altere o consumidor assim:
#define DSP_HOP_N (DSP_WIN_N / 2) /* 50% overlap */
static void win_shift_half(void)
{
/* move a segunda metade para o início */
memmove(&s_win[0], &s_win[DSP_HOP_N], DSP_HOP_N * sizeof(int16_t));
s_win_fill = DSP_HOP_N;
}
No ponto onde a janela fica cheia e você calcula o Goertzel, em vez de zerar:
if (full) {
uint32_t energy = 0;
uint32_t f = dsp_find_dominant_freq_hz(s_win, DSP_WIN_N, DSP_FS_HZ, &energy);
s_last_freq = f;
s_last_energy = energy;
if (f > 0) {
printk("Fdom=%u Hz (energy=%u)\n", f, energy);
}
/* overlap 50% */
win_shift_half();
}
Isso normalmente deixa o número de Hz bem mais estável e melhora a “sensação” de tracking do stream.
5.3 Goertzel em ponto fixo (Q31): quando você quer custo determinístico
Se você começar a “carregar” o ESP32 com outras tarefas (BLE, Wi-Fi, armazenamento, etc.), float pode virar gargalo. O Goertzel em Q31 te dá:
- custo mais previsível,
- sem
cosf,float, etc., - e uma base boa para otimizar com LUT.
A ideia é:
- representar amostras em Q15 ou Q31,
- pré-computar
coeff = 2*cos(w)em Q31 (LUT), - manter os estados
s1,s2em Q31 com saturação.
Molde (didático) do núcleo Q31:
#include <stdint.h>
static inline int32_t q31_mul(int32_t a, int32_t b)
{
/* (a*b)>>31 */
int64_t p = (int64_t)a * (int64_t)b;
return (int32_t)(p >> 31);
}
/* coeff_q31 = 2*cos(w) em Q31 */
static uint64_t goertzel_power_q31(const int16_t *x, uint32_t N, int32_t coeff_q31)
{
int32_t s0 = 0, s1 = 0, s2 = 0;
for (uint32_t n = 0; n < N; n++) {
/* converte int16 -> Q31 (shift 16) */
int32_t xn = ((int32_t)x[n]) << 16;
/* s0 = xn + coeff*s1 - s2 */
int32_t cs1 = q31_mul(coeff_q31, s1);
s0 = xn + cs1 - s2;
s2 = s1;
s1 = s0;
}
/* potência ~ s1^2 + s2^2 - coeff*s1*s2 (tudo em escala relativa) */
int64_t s1s1 = (int64_t)s1 * (int64_t)s1;
int64_t s2s2 = (int64_t)s2 * (int64_t)s2;
int64_t s1s2 = (int64_t)s1 * (int64_t)s2;
int64_t cs1s2 = ((int64_t)coeff_q31 * s1s2) >> 31;
int64_t p = s1s1 + s2s2 - cs1s2;
/* retorna potência “não normalizada”, só para comparação */
if (p < 0) p = 0;
return (uint64_t)p;
}
O que falta para “fechar” o Q31 completo é a LUT de coeff_q31 por frequência. O método prático:
- no build, você gera uma tabela (offline) ou em init (uma vez) usando
cosfsó no setup, - depois o loop fica 100% fixo.
Se você quiser, eu posso te entregar:
- um gerador Python que cospe a LUT em C,
- ou uma LUT mínima só para
DSP_FMIN..DSP_FMAXcomDSP_FSTEP.
5.4 Checklist de integração (para você não “perder o fio”)
- Overlay define
io-channels = <&adc1 0>; adc_spec = ADC_DT_SPEC_GET_BY_IDX(APP_NODE, 0);- Produtor pega
adc_spec.channel_ide mapeia paraadc_channel_t(IDF) - Produtor roda
adc_continuous_*e manda frames só durante stream ativo - Consumidor faz overlap 50% e imprime
Fdomcontinuamente - (Opcional) troca Goertzel float -> Q31 + LUT