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

Consumidor: estimando a frequência principal do stream e imprimindo na UART

Agora que o produtor já entrega apenas os trechos “ativos” (delimitados pelo VAD), o consumidor precisa responder: “qual é a frequência dominante desse trecho?” e imprimir isso continuamente na porta serial.

Em MCU, existem duas abordagens práticas:

  1. Goertzel (recomendado aqui)
    Você escolhe uma faixa de frequências e “varre” bins (por exemplo 80 Hz até 2 kHz). O Goertzel calcula a energia de cada bin sem FFT completa. É excelente quando você quer o pico dominante e quer controlar custo.
  2. FFT curta (512/1024 pontos)
    Funciona, mas aumenta dependência de libs DSP e costuma ser mais sensível a detalhes (janela, normalização, espectro de ruído, etc.). Para um artigo didático e robusto no Zephyr+ESP32, Goertzel é o melhor “primeiro degrau”.

Abaixo eu implemento Goertzel com:

  • janela Hann (reduz vazamento espectral),
  • acúmulo de janela de tamanho fixo (ex.: 1024 amostras),
  • varredura de bins com passo ajustável,
  • impressão na UART via printk().

Observação: “frequência principal” em voz humana é um tema delicado (pitch vs formantes). O algoritmo abaixo te dá a frequência dominante do espectro naquela janela. Para pitch “de verdade”, depois você pode evoluir para autocorrelação/CEPSTRUM/AMDF. Mas como requisito do artigo: frequência principal do stream coletado, isso atende bem e é ótimo como base.


4.1 Parâmetros do DSP

No seu app_stream.h (ou num dsp_cfg.h), defina:

/* DSP: tamanho da janela analisada */
#define DSP_WIN_N           1024u

/* taxa de amostragem: deve bater com o produtor (ADC_SAMPLE_RATE_HZ) */
#define DSP_FS_HZ           16000u

/* varredura: faixa e passo (ajuste para custo x resolução) */
#define DSP_FMIN_HZ         80u
#define DSP_FMAX_HZ         2000u
#define DSP_FSTEP_HZ        20u   /* 20 Hz por bin -> custo moderado */

/* “ignorar silêncio”: energia mínima para reportar */
#define DSP_ENERGY_MIN      800u

4.2 Implementação do Goertzel + janela Hann (tudo em C puro)

Crie src/dsp_goertzel.c:

#include <stdint.h>
#include <math.h>

/*
 * Janela Hann: w[n] = 0.5 - 0.5*cos(2*pi*n/(N-1))
 * Em MCU, você pode:
 *  - pré-computar w[] em float (custo de ROM),
 *  - ou calcular uma vez e manter,
 *  - ou usar aproximação fixa.
 *
 * Aqui eu calculo 1 vez e guardo em float.
 */
static float s_hann[DSP_WIN_N];
static int s_hann_init;

static void hann_init(void)
{
    if (s_hann_init) return;
    const float two_pi = 6.283185307179586f;

    for (uint32_t n = 0; n < DSP_WIN_N; n++) {
        float x = (float)n / (float)(DSP_WIN_N - 1);
        s_hann[n] = 0.5f - 0.5f * cosf(two_pi * x);
    }
    s_hann_init = 1;
}

/*
 * Goertzel:
 *   s[n] = x[n] + 2*cos(w)*s[n-1] - s[n-2]
 *   P = s[N-1]^2 + s[N-2]^2 - 2*cos(w)*s[N-1]*s[N-2]
 *
 * Onde w = 2*pi*f/fs.
 */
static float goertzel_power(const float *x, uint32_t N, float fs_hz, float f_hz)
{
    float w = 2.0f * 3.141592653589793f * (f_hz / fs_hz);
    float cw = cosf(w);
    float coeff = 2.0f * cw;

    float s0 = 0.0f, s1 = 0.0f, s2 = 0.0f;
    for (uint32_t n = 0; n < N; n++) {
        s0 = x[n] + coeff * s1 - s2;
        s2 = s1;
        s1 = s0;
    }

    float p = (s1 * s1) + (s2 * s2) - (coeff * s1 * s2);
    return p;
}

/*
 * Calcula frequência dominante:
 * - aplica Hann
 * - estima energia média (absmean) para ignorar silêncio
 * - varre bins com Goertzel e pega o maior
 */
uint32_t dsp_find_dominant_freq_hz(const int16_t *samples, uint32_t N, uint32_t fs_hz, uint32_t *out_energy)
{
    hann_init();

    /* buffer float local (pode ser estático para evitar stack grande) */
    static float x[DSP_WIN_N];

    /* Energia simples (absmean) + janela */
    uint32_t acc_abs = 0;
    for (uint32_t i = 0; i < N; i++) {
        int32_t a = samples[i];
        if (a < 0) a = -a;
        acc_abs += (uint32_t)a;

        /* normaliza int16 para float e aplica Hann */
        x[i] = ((float)samples[i] / 32768.0f) * s_hann[i];
    }
    uint32_t e = (N > 0) ? (acc_abs / N) : 0;
    if (out_energy) *out_energy = e;

    if (e < DSP_ENERGY_MIN) {
        return 0; /* “sem frequência” */
    }

    float best_p = 0.0f;
    uint32_t best_f = 0;

    for (uint32_t f = DSP_FMIN_HZ; f <= DSP_FMAX_HZ; f += DSP_FSTEP_HZ) {
        float p = goertzel_power(x, N, (float)fs_hz, (float)f);
        if (p > best_p) {
            best_p = p;
            best_f = f;
        }
    }

    return best_f;
}

Se você achar “float pesado”, eu te passo a versão fixa (Q15/Q31) na próxima etapa. Para ESP32, float costuma ser viável, mas depende do seu orçamento de CPU e da taxa de chamadas.


4.3 Consumidor completo: acumulando janela, processando em VOICE_END (e também “ao vivo”)

Agora substituímos o consumidor placeholder.

Estratégia de processamento (robusta):

  • Em VOICE_START: zera estado.
  • Em cada AUDIO_FRAME: copia para um buffer de janela circular até completar DSP_WIN_N.
  • Você pode:
    • calcular a frequência continuamente (a cada janela cheia), ou
    • calcular somente no final (VOICE_END) com as últimas DSP_WIN_N amostras do trecho.

Vou fazer os dois: imprime continuamente a cada janela cheia durante fala, e imprime um resumo quando termina.

/* src/consumer_dsp_uart.c */

#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <stdint.h>
#include <string.h>

#include "app_stream.h"

/* protótipo do DSP */
uint32_t dsp_find_dominant_freq_hz(const int16_t *samples, uint32_t N, uint32_t fs_hz, uint32_t *out_energy);

extern struct k_msgq stream_q;
extern struct k_mem_slab frame_slab;

/* buffer de janela */
static int16_t s_win[DSP_WIN_N];
static uint32_t s_win_fill;

/* estatística do stream */
static uint32_t s_frames_in_stream;
static uint32_t s_last_freq;
static uint32_t s_last_energy;

static void reset_stream_state(void)
{
    s_win_fill = 0;
    s_frames_in_stream = 0;
    s_last_freq = 0;
    s_last_energy = 0;
    memset(s_win, 0, sizeof(s_win));
}

/* copia amostras para janela até completar; retorna 1 quando encheu */
static int win_push(const int16_t *x, uint32_t n)
{
    while (n > 0) {
        uint32_t space = DSP_WIN_N - s_win_fill;
        uint32_t take = (n < space) ? n : space;

        memcpy(&s_win[s_win_fill], x, take * sizeof(int16_t));
        s_win_fill += take;
        x += take;
        n -= take;

        if (s_win_fill >= DSP_WIN_N) {
            return 1;
        }
    }
    return 0;
}

void dsp_uart_thread(void *p1, void *p2, void *p3)
{
    (void)p1; (void)p2; (void)p3;

    reset_stream_state();
    printk("DSP consumer started (Goertzel dominant frequency)\n");

    while (1) {
        struct stream_msg msg;
        k_msgq_get(&stream_q, &msg, K_FOREVER);

        if (msg.type == STREAM_MSG_VOICE_START) {
            printk("[VAD] START\n");
            reset_stream_state();
            continue;
        }

        if (msg.type == STREAM_MSG_VOICE_END) {
            /* resumo final */
            printk("[VAD] END frames=%u last_f=%u Hz energy=%u\n",
                   s_frames_in_stream, s_last_freq, s_last_energy);
            reset_stream_state();
            continue;
        }

        if (msg.type == STREAM_MSG_AUDIO_FRAME && msg.frame) {
            s_frames_in_stream++;

            /* empilha amostras na janela */
            int full = win_push(msg.frame->samples, msg.frame->n);

            /* devolve frame imediatamente (não segura slab) */
            k_mem_slab_free(&frame_slab, (void *)msg.frame);

            if (full) {
                /* janela completa: calcula frequência dominante */
                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);
                }

                /* “janela deslizante”: aqui eu reinicio.
                   Se quiser overlap (ex.: 50%), eu ajusto na próxima seção. */
                s_win_fill = 0;
            }
        }
    }
}

No main.c, na criação do consumidor, troque para dsp_uart_thread.


4.4 Resultado esperado na UART

Durante fala (stream ativo), você começa a ver algo como:

[VAD] START
Fdom=180 Hz (energy=1430)
Fdom=200 Hz (energy=1505)
Fdom=180 Hz (energy=1390)
...
[VAD] END frames=73 last_f=180 Hz energy=1412

O número em Hz vai depender:

  • do seu microfone/circuito analógico,
  • do DSP_FSTEP_HZ (passo de varredura),
  • do threshold do VAD e da escala das amostras,
  • e do conteúdo do sinal.

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