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 é:
- criar
adc_continuous_handle_t - configurar
adc_continuous_config_t(frequência, modo, padrões de canal) adc_continuous_start()- loop chamando
adc_continuous_read()e convertendo o buffer bruto emint16_tpara 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()ouk_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);