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

DeviceTree no Zephyr (ESP32): configurando ADC e o “nó da aplicação” para io-channels, + base do projeto

Nesta seção eu vou te entregar um “mínimo funcional bem estruturado”: overlay do DeviceTree, prj.conf, e o esqueleto das duas threads (produtor/consumidor) já amarradas por fila + slab. A aquisição contínua (DMA) e o VAD entram na próxima seção, em cima dessa base.


2.1 boards/esp32s3_devkitc.overlay (exemplo) — ADC + nó da aplicação

A ideia é você declarar “no DeviceTree” quais canais do ADC a aplicação vai usar. O padrão mais limpo no Zephyr é criar um nó app { io-channels = <&adcX ch>; } e depois o C usa ADC_DT_SPEC_GET_BY_IDX().

Observação: nomes de nós/labels variam por placa/SoC (esp32, esp32s3, esp32p4). Esse overlay é um template que você ajusta conforme o dts da sua board.

/* boards/esp32s3_devkitc.overlay */

/ {
    chosen {
        zephyr,console = &uart0;
        zephyr,shell-uart = &uart0;
        zephyr,uart-mcumgr = &uart0;
    };

    /* Nó da aplicação: descreve quais entradas analógicas vou amostrar */
    app {
        compatible = "zephyr,user";
        /* Ex.: canal 0 do ADC1 */
        io-channels = <&adc1 0>;
    };
};

/* Habilita UART0 para saída serial */
&uart0 {
    status = "okay";
    current-speed = <115200>;
};

/*
 * ADC: o nome (&adc1) precisa existir no dts da sua board/SoC.
 * Em alguns alvos o label pode ser &adc, &adc0, &adc1, etc.
 */
&adc1 {
    status = "okay";
    /* Algumas boards/SoCs expõem propriedades extras; mantenha o mínimo aqui */
};

Como você confere o label certo do ADC?
Depois do build, inspecione build/zephyr/zephyr.dts e procure por adc e status = "okay"; ali você descobre se o label é adc, adc1, adc0, etc. (eu costumo grepar o zephyr.dts).


2.2 prj.conf — drivers e infraestrutura (threads, msgq, ring buffer opcional)

Aqui o alvo é: UART pronta, ADC habilitado, logs ok e memória suficiente (já que vamos usar k_mem_slab e buffers de DSP).

# prj.conf

# Console/Log
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_PRINTK=y
CONFIG_LOG=y
CONFIG_LOG_MODE_DEFERRED=y

# Threads e sincronização
CONFIG_MULTITHREADING=y

# ADC API do Zephyr (vamos usar para ler config do DT e como fallback)
CONFIG_ADC=y

# Tamanhos mínimos de heap/stack para a demo
CONFIG_HEAP_MEM_POOL_SIZE=16384
CONFIG_MAIN_STACK_SIZE=4096

# (Opcional) Ring buffer do Zephyr, se você preferir
CONFIG_RING_BUFFER=y

2.3 Contrato produtor/consumidor: bloco de amostras + evento (VAD start/end)

Para não “copiar buffer grande” a cada mensagem, um padrão bom é:

  • k_mem_slab: armazena blocos fixos de amostras (frames)
  • k_msgq: envia ponteiros + metadados (tipo de mensagem, número de amostras, timestamp)

Estruturas:

/* src/app_stream.h */

#pragma once
#include <zephyr/kernel.h>
#include <stdint.h>

enum stream_msg_type {
    STREAM_MSG_AUDIO_FRAME = 1,
    STREAM_MSG_VOICE_START = 2,
    STREAM_MSG_VOICE_END   = 3,
};

#define ADC_FRAME_SAMPLES   256   /* tamanho do frame (ajustável) */

struct stream_frame {
    int16_t samples[ADC_FRAME_SAMPLES];
    uint16_t n;           /* quantas amostras válidas */
    uint32_t t_ms;        /* timestamp aproximado */
};

struct stream_msg {
    enum stream_msg_type type;
    struct stream_frame *frame;   /* só válido em AUDIO_FRAME */
};

2.4 Esqueleto funcional das threads (sem DMA/VAD ainda)

Aqui você já tem o pipeline montado. Por enquanto o produtor vai “simular” frames (para validar a infraestrutura). Na próxima seção, a simulação vira leitura contínua por DMA + VAD real.

/* src/main.c */

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/sys/printk.h>
#include "app_stream.h"

/* ===== DeviceTree: pega o 1º io-channel do nó /app ===== */
#define APP_NODE DT_PATH(app)
#if !DT_NODE_EXISTS(APP_NODE)
#error "Nó /app não existe no DeviceTree. Confirme seu overlay."
#endif

static const struct adc_dt_spec adc_spec =
    ADC_DT_SPEC_GET_BY_IDX(APP_NODE, 0);

/* ===== IPC: slab + msgq ===== */
K_MEM_SLAB_DEFINE(frame_slab, sizeof(struct stream_frame), 12, 4);

K_MSGQ_DEFINE(stream_q, sizeof(struct stream_msg), 16, 4);

/* ===== Threads ===== */
#define PRODUCER_STACK_SZ 2048
#define CONSUMER_STACK_SZ 4096
#define PRODUCER_PRIO     3
#define CONSUMER_PRIO     4

K_THREAD_STACK_DEFINE(producer_stack, PRODUCER_STACK_SZ);
K_THREAD_STACK_DEFINE(consumer_stack, CONSUMER_STACK_SZ);

static struct k_thread producer_thread_data;
static struct k_thread consumer_thread_data;

/* ===== Utils ===== */
static uint32_t now_ms(void)
{
    return (uint32_t)k_uptime_get_32();
}

/*
 * Produtor (placeholder):
 * - na próxima seção: inicializa ADC contínuo (DMA) + VAD
 * - por enquanto: gera uma senoide “falsa” para testar o pipeline
 */
static void producer_thread(void *p1, void *p2, void *p3)
{
    (void)p1; (void)p2; (void)p3;

    if (!device_is_ready(adc_spec.dev)) {
        printk("ADC device not ready\n");
        return;
    }

    /* Envia um evento de início (como se VAD detectasse fala) */
    struct stream_msg ev_start = { .type = STREAM_MSG_VOICE_START, .frame = NULL };
    k_msgq_put(&stream_q, &ev_start, K_FOREVER);

    while (1) {
        struct stream_frame *frame = NULL;
        if (k_mem_slab_alloc(&frame_slab, (void **)&frame, K_MSEC(50)) != 0) {
            /* Se slab lotar, é sinal que consumidor não está acompanhando */
            printk("SLAB cheio: consumidor atrasado\n");
            continue;
        }

        frame->n = ADC_FRAME_SAMPLES;
        frame->t_ms = now_ms();

        /* Simulação: preenche com algo qualquer (substituído depois pelo ADC DMA) */
        for (uint16_t i = 0; i < frame->n; i++) {
            frame->samples[i] = (int16_t)((i % 64) * 512 - 16384);
        }

        struct stream_msg msg = { .type = STREAM_MSG_AUDIO_FRAME, .frame = frame };
        if (k_msgq_put(&stream_q, &msg, K_MSEC(20)) != 0) {
            /* Fila cheia -> descarta frame (mas devolve ao slab!) */
            k_mem_slab_free(&frame_slab, (void *)frame);
            printk("MSGQ cheia: descartando frame\n");
        }

        k_sleep(K_MSEC(10));
    }
}

/*
 * Consumidor (placeholder):
 * - na próxima seção: calcula frequência principal e imprime
 * - por enquanto: só conta frames e mostra timestamp
 */
static void consumer_thread(void *p1, void *p2, void *p3)
{
    (void)p1; (void)p2; (void)p3;

    uint32_t frames = 0;

    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");
            frames = 0;
            continue;
        }

        if (msg.type == STREAM_MSG_VOICE_END) {
            printk("[VAD] END. frames=%u\n", frames);
            continue;
        }

        if (msg.type == STREAM_MSG_AUDIO_FRAME && msg.frame) {
            frames++;
            printk("frame=%u t=%u ms n=%u\n", frames, msg.frame->t_ms, msg.frame->n);

            /* Aqui entra DSP + UART (próxima seção) */

            /* Libera o frame de volta ao slab */
            k_mem_slab_free(&frame_slab, (void *)msg.frame);
        }
    }
}

int main(void)
{
    printk("Zephyr+ESP32: ADC stream pipeline (base)\n");

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

    k_thread_create(&consumer_thread_data, consumer_stack,
                    K_THREAD_STACK_SIZEOF(consumer_stack),
                    consumer_thread, NULL, NULL, NULL,
                    CONSUMER_PRIO, 0, K_NO_WAIT);

    return 0;
}

O que essa base já te garante:

  • Separação rígida entre aquisição e processamento (evita jitter e perda de amostras).
  • Um lugar claro para inserir VAD como “gerador de eventos”.
  • Um lugar claro para inserir DSP (frequência principal) sem travar o produtor.
  • Detecta gargalo de forma explícita (slab cheio / msgq cheia).

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