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
dtsda 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).