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

Aquisição contínua “tipo DMA” no ESP32 (ADC continuous mode), VAD delimitando stream e envio para o consumidor

Aqui eu vou transformar o produtor “simulado” em um produtor real, usando o ADC Continuous Mode Driver da Espressif (o modo contínuo foi feito exatamente para amostragem em alta taxa, com resultados transferidos para memória via DMA) (Espressif Systems), e vou encaixar um VAD para gerar eventos VOICE_START/VOICE_END. No final desta seção o consumidor ainda não calcula a frequência principal (isso entra na seção 4), mas ele já recebe “streams” delimitados.

Nota importante de plataforma: o VAD em hardware existe documentado para ESP32-P4 (Espressif Systems). Para outros ESP32, você pode usar VAD por software (ex.: ESP-SR/ADF) ou, no mínimo, um VAD simples por energia (RMS) como fallback. Nesta seção eu vou entregar:

  • caminho A: “VAD simples por energia” (funciona em qualquer ESP32)
  • caminho B: ponto de encaixe para “VAD hardware (P4)” (quando aplicável)

3.1 Ajustes de prj.conf para usar o driver contínuo do ESP-IDF

Como o Zephyr no ESP32 usa o HAL da Espressif por baixo, você consegue incluir headers e chamar APIs do ESP-IDF (com cuidado). O que você vai precisar, no mínimo, é habilitar o que for necessário para o build aceitar os includes e a aplicação ter heap suficiente.

Acrescente:

# prj.conf (adições)

CONFIG_NEWLIB_LIBC=y
CONFIG_HEAP_MEM_POOL_SIZE=32768

# Logs úteis para debug de streaming
CONFIG_LOG=y
CONFIG_LOG_MODE_DEFERRED=y

# (Opcional) se você for usar GPIO/IRQ para debug/medição
CONFIG_GPIO=y

O driver “ADC continuous mode” é do ESP-IDF (Espressif Systems); o exemplo oficial de uso fica no repositório da Espressif (continuous_read) (GitHub).


3.2 Produtor com ADC contínuo (DMA): inicialização + loop de leitura

A base do produtor agora é:

  1. criar adc_continuous_handle_t
  2. configurar adc_continuous_config_t (frequência, modo, padrões de canal)
  3. adc_continuous_start()
  4. loop chamando adc_continuous_read() e convertendo o buffer bruto em int16_t para seu pipeline

Código (focado em clareza; você pode refinar depois para múltiplos canais):

/* src/adc_dma_producer.c */

#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(adc_dma_prod, LOG_LEVEL_INF);

#include "app_stream.h"

/* ===== ESP-IDF headers (ADC continuous) ===== */
#include "esp_err.h"
#include "driver/adc_continuous.h"
#include "driver/adc_common.h"

/* Ajuste conforme seu alvo: sample rate do ADC continuous */
#define ADC_SAMPLE_RATE_HZ     16000u   /* típico para voz */
#define ADC_READ_BYTES         2048u    /* buffer bruto do driver */
#define ADC_TIMEOUT_MS         50

/* VAD simples: energia (RMS aproximado) */
#define VAD_RMS_THRESHOLD      1200     /* ajuste na prática */
#define VAD_HANGOVER_FRAMES    12       /* quantos frames manter “ativo” após cair */

extern struct k_msgq stream_q;
extern struct k_mem_slab frame_slab;

/* Estado do produtor */
static adc_continuous_handle_t s_handle;
static uint8_t s_raw[ADC_READ_BYTES];

/* Estado do VAD simples */
static bool s_voice_active;
static int  s_hangover;

/* Converte amostra para int16 (depende do formato do driver) */
static inline int16_t sample_to_s16(uint16_t x12)
{
    /* ADC geralmente entrega 12 bits; centraliza em zero (offset) */
    int32_t v = (int32_t)x12 - 2048;   /* 12-bit mid = 2048 */
    return (int16_t)(v << 4);          /* escala para 16-bit “aprox” */
}

/* RMS aproximado (sem sqrt): usa média de abs */
static uint32_t frame_energy_absmean(const int16_t *x, uint16_t n)
{
    uint32_t acc = 0;
    for (uint16_t i = 0; i < n; i++) {
        int32_t a = x[i];
        if (a < 0) a = -a;
        acc += (uint32_t)a;
    }
    return (n > 0) ? (acc / n) : 0;
}

/* Emite evento VAD */
static void emit_vad_event(enum stream_msg_type t)
{
    struct stream_msg m = { .type = t, .frame = NULL };
    k_msgq_put(&stream_q, &m, K_FOREVER);
}

/*
 * Configura “pattern” do ADC continuous mode.
 * O pattern descreve: unidade, canal, atenuação, bitwidth.
 * Veja o conceito no guia do driver: adc_continuous_config_t e adc_pattern :contentReference[oaicite:4]{index=4}
 */
static esp_err_t adc_continuous_init_single_channel(adc_unit_t unit, adc_channel_t ch)
{
    adc_continuous_handle_cfg_t hcfg = {
        .max_store_buf_size = 8 * 1024,
        .conv_frame_size    = ADC_READ_BYTES,
    };

    esp_err_t err = adc_continuous_new_handle(&hcfg, &s_handle);
    if (err != ESP_OK) return err;

    adc_digi_pattern_config_t pattern = {
        .atten     = ADC_ATTEN_DB_11,       /* voz: normalmente precisa atenuação (ajuste) */
        .channel   = ch,
        .unit      = unit,
        .bit_width = SOC_ADC_DIGI_MAX_BITWIDTH,
    };

    adc_continuous_config_t cfg = {
        .pattern_num    = 1,
        .adc_pattern    = &pattern,
        .sample_freq_hz = ADC_SAMPLE_RATE_HZ,
        .conv_mode      = (unit == ADC_UNIT_1) ? ADC_CONV_SINGLE_UNIT_1 : ADC_CONV_SINGLE_UNIT_2,
        .format         = ADC_DIGI_OUTPUT_FORMAT_TYPE2,
    };

    err = adc_continuous_config(s_handle, &cfg);
    if (err != ESP_OK) return err;

    return ESP_OK;
}

/*
 * Faz parsing do buffer do driver.
 * No formato TYPE2 (comum nos exemplos), cada “resultado” vem com metadados + valor.
 * Ajuste conforme o SoC/IDF.
 */
static uint16_t parse_type2_samples(const uint8_t *raw, uint32_t raw_len, int16_t *out, uint16_t out_max)
{
    uint16_t n = 0;

    /* Estrutura típica do IDF: adc_digi_output_data_t */
    const adc_digi_output_data_t *p = (const adc_digi_output_data_t *)raw;
    uint32_t count = raw_len / sizeof(adc_digi_output_data_t);

    for (uint32_t i = 0; i < count && n < out_max; i++) {
        /* p[i].type2.data é o valor; detalhes no exemplo continuous_read :contentReference[oaicite:5]{index=5} */
        uint16_t v = p[i].type2.data;
        out[n++] = sample_to_s16(v);
    }
    return n;
}

/* Thread produtora real */
void adc_stream_thread(void *p1, void *p2, void *p3)
{
    (void)p1; (void)p2; (void)p3;

    /* TODO: mapear isso a partir do DeviceTree (unidade/canal/pino). Por ora fixo. */
    const adc_unit_t unit = ADC_UNIT_1;
    const adc_channel_t ch = ADC_CHANNEL_0;

    esp_err_t err = adc_continuous_init_single_channel(unit, ch);
    if (err != ESP_OK) {
        LOG_ERR("adc_continuous_init failed: %d", (int)err);
        return;
    }

    err = adc_continuous_start(s_handle);
    if (err != ESP_OK) {
        LOG_ERR("adc_continuous_start failed: %d", (int)err);
        return;
    }

    LOG_INF("ADC continuous started @ %u Hz", ADC_SAMPLE_RATE_HZ);

    s_voice_active = false;
    s_hangover = 0;

    while (1) {
        uint32_t out_len = 0;
        err = adc_continuous_read(s_handle, s_raw, sizeof(s_raw), &out_len, ADC_TIMEOUT_MS);
        if (err == ESP_ERR_TIMEOUT) {
            continue;
        }
        if (err != ESP_OK) {
            LOG_ERR("adc_continuous_read err=%d", (int)err);
            continue;
        }

        /* Aloca um frame do slab */
        struct stream_frame *frame = NULL;
        if (k_mem_slab_alloc(&frame_slab, (void **)&frame, K_NO_WAIT) != 0) {
            /* Consumidor atrasou -> descarta bloco bruto */
            LOG_WRN("SLAB cheio: drop");
            continue;
        }

        frame->t_ms = (uint32_t)k_uptime_get_32();
        frame->n = parse_type2_samples(s_raw, out_len, frame->samples, ADC_FRAME_SAMPLES);

        /* ===== VAD simples por energia ===== */
        uint32_t e = frame_energy_absmean(frame->samples, frame->n);
        bool vad_now = (e >= VAD_RMS_THRESHOLD);

        if (!s_voice_active) {
            if (vad_now) {
                s_voice_active = true;
                s_hangover = VAD_HANGOVER_FRAMES;
                emit_vad_event(STREAM_MSG_VOICE_START);
            } else {
                /* silêncio e ainda não ativo -> não envia frame ao consumidor */
                k_mem_slab_free(&frame_slab, (void *)frame);
                continue;
            }
        } else {
            /* já está ativo */
            if (vad_now) {
                s_hangover = VAD_HANGOVER_FRAMES;
            } else {
                s_hangover--;
                if (s_hangover <= 0) {
                    s_voice_active = false;
                    emit_vad_event(STREAM_MSG_VOICE_END);
                    k_mem_slab_free(&frame_slab, (void *)frame);
                    continue;
                }
            }
        }

        /* Se chegou aqui, estamos “dentro do stream” -> envia frame */
        struct stream_msg msg = { .type = STREAM_MSG_AUDIO_FRAME, .frame = frame };
        if (k_msgq_put(&stream_q, &msg, K_NO_WAIT) != 0) {
            /* fila cheia -> devolve frame */
            k_mem_slab_free(&frame_slab, (void *)frame);
            LOG_WRN("MSGQ cheia: drop frame");
        }
    }
}

Pontos-chave que valem notar:

  • Você está usando um driver realmente feito para alta taxa, e o texto do guia deixa explícito que o modo contínuo é adequado para aquisição de alta frequência e que os resultados vão para memória via DMA (Espressif Systems).
  • O “VAD simples” já delimita stream e “mata” silêncio, o que reduz o custo no consumidor.

3.3 Encaixe do VAD em hardware (ESP32-P4) no mesmo contrato de eventos

No ESP32-P4 existe documentação do periférico VAD (Espressif Systems). O jeito mais limpo de integrar no Zephyr é:

  • configurar VAD e habilitar interrupções/eventos (no lado ESP-IDF / HAL),
  • no ISR/callback, apenas sinalizar para a thread (ex.: k_event_set() ou k_sem_give()),
  • e a thread produtora emitir STREAM_MSG_VOICE_START/END.

O código exato depende do driver/headers do IDF para P4, então aqui eu te deixo o “molde” (o importante é a arquitetura e os pontos de sincronização):

/* pseudo: eventos vindos do VAD hardware (P4) */
static struct k_event vad_evt;
#define EVT_VAD_START BIT(0)
#define EVT_VAD_END   BIT(1)

/* ISR/callback VAD: mínimo possível */
static void vad_isr_callback_start(void)
{
    k_event_post(&vad_evt, EVT_VAD_START);
}
static void vad_isr_callback_end(void)
{
    k_event_post(&vad_evt, EVT_VAD_END);
}

/* no loop do produtor: prioriza evento hardware */
uint32_t ev = k_event_wait(&vad_evt, EVT_VAD_START | EVT_VAD_END, false, K_NO_WAIT);
if (ev & EVT_VAD_START) { emit_vad_event(STREAM_MSG_VOICE_START); s_voice_active = true; }
if (ev & EVT_VAD_END)   { emit_vad_event(STREAM_MSG_VOICE_END);   s_voice_active = false; }

3.4 Atualizando o main.c para usar o produtor real

No main.c da seção anterior, troque a função producer_thread por adc_stream_thread (ou mantenha ambos e compile por #if).

Exemplo (só a criação da thread):

extern void adc_stream_thread(void *p1, void *p2, void *p3);

k_thread_create(&producer_thread_data, producer_stack,
                K_THREAD_STACK_SIZEOF(producer_stack),
                adc_stream_thread, NULL, NULL, NULL,
                PRODUCER_PRIO, 0, K_NO_WAIT);

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

SumárioMutex: Exclusão Mútua para Recursos CompartilhadosQuando Usar MutexQuando Evitar MutexExemplo de Uso de Mutex no STM32F411RE com FreeRTOSSemáforos: Controle de Fluxo e Contagem de RecursosQuando Usar SemáforosQuando Evitar SemáforosExemplo de

Debug, Tracing e Análise Temporal no FreeRTOS: Monitoramento Avançado de Tasks, Watermark e Confiabilidade em Tempo RealDebug, Tracing e Análise Temporal no FreeRTOS: Monitoramento Avançado de Tasks, Watermark e Confiabilidade em Tempo Real

Aprenda como aplicar debug estruturado, tracing, análise temporal e watermark no FreeRTOS para monitorar tasks, medir WCET, detectar jitter, prevenir stack overflow e aumentar a confiabilidade de sistemas embarcados em

0
Adoraria saber sua opinião, comente.x