MCU & FPGA lwIP,RTOS Zephyr no ESP32: ADC Contínuo com DMA, VAD e Processamento de Sinais em Tempo Real

Zephyr no ESP32: ADC Contínuo com DMA, VAD e Processamento de Sinais em Tempo Real

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:

  1. O produtor não pode ficar fixo em ADC_UNIT_1 + ADC_CHANNEL_0. Vamos ler isso do DeviceTree via adc_dt_spec (o mesmo nó /app com io-channels que você já configurou).
  2. Janela deslizante com overlap (ex.: 50%) para uma estimativa mais estável e “contínua” durante o stream.
  3. 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 com DT_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, s2 em 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 cosf só 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_FMAX com DSP_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_id e mapeia para adc_channel_t (IDF)
  • Produtor roda adc_continuous_* e manda frames só durante stream ativo
  • Consumidor faz overlap 50% e imprime Fdom continuamente
  • (Opcional) troca Goertzel float -> Q31 + LUT

0 0 votos
Classificação do artigo
Inscrever-se
Notificar de
guest
0 Comentários
mais antigos
mais recentes Mais votado
Feedbacks embutidos
Ver todos os comentários

Related Post

0
Adoraria saber sua opinião, comente.x