Introdução
Diferentemente de muitos outros RTOS, o Zephyr evoluiu para muito mais do que um sistema operacional de tempo real. Atualmente, ele oferece um framework completo, composto por diversas bibliotecas e APIs que tornam seu ecossistema bastante rico e flexível.
Um bom exemplo é a camada de sistema de arquivos, que permite alternar facilmente entre implementações como LittleFS e FATFS sem grandes mudanças na aplicação. Da mesma forma, o Zephyr disponibiliza uma ampla variedade de drivers, protocolos e bibliotecas que simplificam o desenvolvimento de aplicações embarcadas.
Neste artigo, exploraremos os recursos de IoT do Zephyr utilizando o Raspberry Pi Pico W (RP2040 com Wi-Fi Infineon CYW43439) em conjunto com a placa de expansão Waveshare Pico-Relay-B.
Além dos recursos tradicionais de conectividade, esta demonstração abordará um tema que ainda é pouco explorado em artigos sobre o Zephyr: a criação de páginas web e APIs HTTP embarcadas para controle remoto da aplicação.
Hardware utilizado
O hardware da demonstração é intencionalmente barato e fácil de reproduzir:
- Placa: Raspberry Pi Pico W, RP2040 dual-core Cortex-M0+ @ 133 MHz, 264 KB de SRAM, 2 MB de flash QSPI e Wi-Fi Infineon CYW43439 (driver airoc)
- Módulo de expansão: Waveshare Pico-Relay-B, com 8 canais de relé, LED RGB WS2812 e buzzer passivo
- Zephyr RTOS: v4.4.0 com Zephyr SDK v1.0.1
- PC: Ubuntu 22.04 / 24.04
O mapa de pinos ficou assim:
| Pino | Função | Periférico / Driver |
|---|---|---|
| GP6 | Buzzer passivo | PWM (slice 3, canal A) |
| GP13 | LED RGB WS2812 | PIO1 + driver led_strip |
| GP14 – GP21 | Relés CH8 – CH1 | GPIO, aliases relay1..relay8 |
| – | Wi-Fi CYW43439 | driver airoc (ocupa a PIO0) |
| USB | Console e shell | CDC ACM |
Firmware
A aplicação utiliza um Device Tree (DTS) out-of-tree, ou seja, mantemos uma descrição de hardware independente do repositório oficial do Zephyr. Essa abordagem reduz o impacto de futuras atualizações do projeto, evitando que alterações na configuração padrão das placas afetem a compilação da aplicação.
Como exemplo, uma mudança na árvore de clocks ou na configuração dos periféricos pode alterar a instância de PWM utilizada pelo módulo de reprodução de RTTTL, quebrando o funcionamento da aplicação. Ao manter a configuração de hardware separada, preservamos a compatibilidade entre diferentes versões do Zephyr.
A aplicação de demonstração implementa os seguintes recursos:
- Shell: terminal interativo acessível pela porta serial ou via Telnet após a conexão à rede.
- Wi-Fi: varredura (scan) e conexão com a rede sem fio.
- Servidor Web: interface desenvolvida em HTML, CSS e JavaScript para controle remoto da aplicação.
- APIs HTTP: endpoints para integração e controle dos recursos da placa.
- Controle de relés: acionamento dos oito canais da placa Pico-Relay-B pela interface web.
- Controle de LED RGB (WS2812): alteração de cores diretamente pela página web.
- Reprodução de melodias RTTTL: execução de toques em um buzzer utilizando o formato RTTTL.
- Gerenciamento de arquivos: upload, download, visualização e remoção de arquivos por meio da interface web.
- RTC: base de tempo do sistema, mantida em UTC e sincronizada via SNTP após a conexão com a rede.
- Watchdog: monitoramento por hardware que reinicia a placa caso o firmware deixe de responder.
Estrutura da aplicação
Uma prática que adoto em todo projeto: a main() não deve fazer nada além de chamar a função setup_init que contém toda a cadeia de inicialização de libs e threads da aplicação.
int main(void)
{
setup_init();
while (1) {
k_msleep(MAIN_LOOP_PERIOD_MS); /* 300 ms */
setup_wdt_feed();
}
return 0;
}Toda a inicialização fica em setup.c, dividida em quatro fases explícitas, com um tratador central de erro fatal:
setup_init()
├── [1/4] sanity -> causa do último reset (hwinfo)
├── [2/4] middleware -> inicializa watchdog, RTC, LittleFS, ringtone
├── [3/4] database -> reservado para inicializar parametros de configurações
└── [4/4] tasks -> initicializa as threads da aplicacaoA fase de sanity é pequena, mas rende muito em campo. Logo no boot o firmware lê hwinfo_get_reset_cause() e classifica o motivo do último reset: um RESET_PIN ou RESET_POR é registrado como LOG_INF, enquanto RESET_WATCHDOG, RESET_BROWNOUT ou RESET_CPU_LOCKUP saem como LOG_ERR. Quando o equipamento volta do cliente com a queixa genérica de “ele reinicia sozinho”, essa linha de log responde quase tudo. Depois de reportar, o firmware limpa os flags para que o próximo boot informe apenas a sua própria causa.
O watchdog é de hardware, configurado com timeout de 5 segundos, por ser uma demo simples, ele é alimentado pelo laço ocioso da main() a cada 300 ms.
Os subsistemas ficam em bibliotecas independentes no diretório lib/ (wifi_mgr, rtc_mgr, fs_mgr e ringtone), habilitadas por Kconfig e reaproveitáveis em outros projetos. É a mesma filosofia do próprio Zephyr aplicada ao código da aplicação.
Wi-Fi
A conexão Wi-Fi é o subsistema que mais dá trabalho em um produto real, porque a rede do cliente cai, o roteador reinicia e o DHCP expira. Por isso o bring-up de rede ficou em uma thread dedicada, modelado como máquina de estados:
OFFLINE → CONNECTING → RUNNING
↑ │
└──────────────────────┘
(evento de disconnect)A biblioteca wifi_mgr produz eventos com payload (IP, gateway, máscara) em uma k_msgq, e a thread os consome no estado RUNNING com timeout de 3 segundos. Se o bring-up falhar em qualquer etapa (associação, DHCP ou servidor web), a máquina espera 10 segundos e volta para OFFLINE, tentando novamente de forma indefinida. Ao entrar em CONNECTING, a fila de eventos é drenada para que a sessão nova não seja acordada por dados de uma sessão antiga.
A sequência completa de bring-up é: conectar ao Wi-Fi, aguardar o IP do DHCP, sincronizar o RTC por SNTP contra o time.google.com e, por fim, subir o servidor web. Uma observação sobre o relógio: o RTC é mantido sempre em UTC, e o fuso (-3, BRT) é aplicado apenas no momento da consulta, em rtc_mgr_get_local(). Guardar hora local no RTC é uma daquelas decisões que só cobram a fatura meses depois, no horário de verão ou na primeira exportação de log.
O web_app_start() é chamado uma única vez. O servidor HTTP do Zephyr sobrevive a quedas e reconexões de Wi-Fi, os sockets são recuperados pela própria stack.
Com o mDNS habilitado (CONFIG_MDNS_RESPONDER e CONFIG_NET_HOSTNAME=”embarcados”), a placa passa a responder por embarcados.local, o que evita ter que descobrir o IP atribuído pelo DHCP a cada boot.
RTTTL
O RTTTL (Ring Tone Text Transfer Language) é um formato em texto criado pela Nokia para descrever ringtones monofônicos. Cada string segue o padrão <nome>:<controle>:<notas>, onde o controle traz os valores padrão (d= duração, o= oitava, b= BPM) e as notas vêm separadas por vírgula:
Mario:d=4,o=5,b=100:16e6,16e6,32p,8e6,16c6,8e6,8g6
A reprodução é feita pela biblioteca lib/ringtone/, que converte cada nota em frequência e aciona o buzzer passivo do GP6 via PWM. A biblioteca não conhece o hardware: quem resolve o pwm_dt_spec a partir do alias do Device Tree é a aplicação, o que a mantém portável para outras placas.
static const struct pwm_dt_spec buzzer_pwm = PWM_DT_SPEC_GET(DT_ALIAS(pwm_buzzer0));
ringtone_init(&buzzer_pwm);Esse é exatamente o caso citado no início da seção: o alias pwm-buzzer0 está declarado no nosso overlay out-of-tree, apontando para o canal PWM_3A_P6. Se uma atualização do Zephyr renumerar os canais de PWM do RP2040, corrigimos um único ponto no overlay e nada mais no código.
O ringtone pode ser disparado de três formas, o que é ótimo para depurar sem precisar da interface web:
uart:~$ ringtone test # tema do Mario embutido
uart:~$ ringtone play "Beep:d=4,o=5,b=100:c,e,g"
uart:~$ ringtone stop
$ curl -X POST http://embarcados.local/api/buzzer \
-H "Content-Type: application/json" \
-d '{"action":"play","rtttl":"Beep:d=4,o=5,b=100:c,e,g"}'
ringtone_play_custom("Beep:d=4,o=5,b=100:c,e,g");
ringtone_play_notification(RINGTONE_ALARM1);
ringtone_stop();
Página web
Aqui está o ponto central da demonstração. O firmware sobe um servidor HTTP na porta 80, habilitado apenas por CONFIG_HTTP_SERVER=y, e a interface de controle é servida por ele, direto da flash.
O fluxo visto pelo usuário é o de qualquer site: ao abrir http://embarcados.local/, o browser faz GET /, GET /style.css e GET /app.js, e o app.js já carregado passa a consultar os endpoints com fetch(‘/api/relays’), fetch(‘/api/time’) e assim por diante. A diferença está em onde esses três arquivos moram.
Como o HTML vai parar dentro do firmware
Quem já embutiu uma página web em um firmware, provavelmente no ambiente Arduino, conhece as duas abordagens tradicionais para esse problema.
A primeira é converter a página em um vetor de bytes com o utilitário xxd:
$ xxd -i index.html > index.h
unsigned char index_html[] = { 0x3c, 0x21, 0x44, 0x4f, 0x43, /* ... */ };
unsigned int index_html_len = 6546;
Repare que o xxd gera o vetor sem o qualificador const. Do jeito que sai da ferramenta, o array é colocado na seção de dados inicializados e copiado para a RAM durante o boot, consumindo alguns kilobytes que raramente sobram em um microcontrolador. É necessário editar o cabeçalho gerado e declarar o vetor como const, para que ele permaneça na flash:
const unsigned char index_html[] = { 0x3c, 0x21, 0x44, 0x4f, 0x43, /* ... */ };Como o xxd converte um arquivo por vez, o caminho mais comum é embutir o CSS e o JavaScript dentro do próprio index.html, em blocos <style> e <script>, de forma que um único vetor contenha a interface inteira. Mantendo os arquivos separados, é preciso gerar um cabeçalho para cada um e registrar uma rota por arquivo, já que o browser fará requisições independentes para style.css e app.js.
A segunda é declarar a página como uma string constante, marcando com %d ou %s os campos que precisam ser atualizados e montando a resposta com snprintf() a cada requisição:
const char index_html[] =
"<html><body>"
"<p>Rele 1: <b>%s</b></p>"
"<p>Uptime: <b>%d</b> s</p>"
"</body></html>";
snprintf(buf, sizeof(buf), index_html, relay_state[0] ? "ON" : "OFF", uptime);
As duas funcionam, mas cobram um preço. Na primeira, o xxd precisa ser executado manualmente a cada alteração da página, e é comum esquecer esse passo e depurar um firmware que ainda serve a versão anterior do HTML. Na segunda, a página deixa de ser um arquivo: perde-se o realce de sintaxe e a possibilidade de abri-la no browser durante o desenvolvimento, o layout se mistura à lógica da aplicação e a resposta passa a ser remontada em RAM a cada requisição, sem falar que uma página assim não pode ser comprimida.
No Zephyr, esse trabalho é feito pelo próprio sistema de build. Os arquivos de app/www/ não são gravados no sistema de arquivos: eles viram código. Em tempo de build, o CMake chama a função generate_inc_file_for_target() do Zephyr, que executa o script scripts/build/file2hex.py com a opção –gzip, comprimindo o arquivo e emitindo os bytes em hexadecimal:
foreach(web_resource index.html app.js style.css)
generate_inc_file_for_target(
app
www/${web_resource}
${gen_dir}/${web_resource}.gz.inc
--gzip
)
endforeach()
O web_app.c simplesmente inclui o resultado dentro da inicialização de um array, que o compilador coloca na flash:
static const uint8_t index_html_gz[] = {
#include "index.html.gz.inc"
};
O resource declara .content_encoding = “gzip” e o browser descomprime sozinho. O ganho é considerável e não custa nada em tempo de execução, já que a compressão acontece na máquina de desenvolvimento:
| Arquivo | Original | Comprimido |
|---|---|---|
| index.html | 6.546 B | 2.220 B |
| style.css | 7.894 B | 1.765 B |
| app.js | 14.591 B | 4.007 B |
| Total | 28,4 KB | 7,8 KB |
Ou seja, a interface web inteira ocupa menos de 8 KB de flash. Do ponto de vista da manutenção, o ganho é que o fluxo de trabalho continua sendo o de um site comum: edita-se o HTML, o CSS e o JavaScript com as ferramentas de sempre, e a compressão acontece automaticamente no build. Como os arquivos passam a fazer parte do binário, firmware e interface são distribuídos como uma única imagem, sem etapa adicional de gravação no sistema de arquivos e sem o risco de a página ficar fora de sincronia com a versão do firmware.
APIs
A interface web não é a única forma de interagir com o firmware. O mesmo servidor HTTP também atende clientes que acessam os endpoints da API diretamente, sem carregar a página. Isso inclui ferramentas como curl, coleções do Postman, aplicativos móveis ou até mesmo outro equipamento na rede consultando o estado dos relés.
Do ponto de vista do firmware, não há diferença entre essas formas de acesso. O JavaScript da interface envia exatamente as mesmas requisições HTTP que um cliente externo faria. Por exemplo, quando o usuário clica em um botão da página, o navegador envia um POST para /api/relays, exatamente como aconteceria com um comando executado via curl. Em outras palavras, a página servida pelo Pico W é apenas mais um cliente da API, sem qualquer tratamento especial.
Essa é uma das principais decisões de arquitetura do projeto. Como toda a lógica de controle está concentrada na API REST, a interface pode ser modificada, substituída por um aplicativo móvel ou integrada a um sistema supervisório sem exigir alterações no código responsável pelo controle dos relés. Essa separação torna o firmware mais reutilizável, facilita a integração com outros sistemas e simplifica a evolução da aplicação.
Todas as rotas, tanto as que servem a página quanto as que respondem a um curl, são registradas pela mesma macro HTTP_RESOURCE_DEFINE. O que muda entre elas é o tipo de recurso, e é essa escolha que determina o custo de cada requisição:
/* STATIC: tudo decidido em tempo de compilação */
static struct http_resource_detail_static info_detail = {
.common = {
.type = HTTP_RESOURCE_TYPE_STATIC,
.bitmask_of_supported_http_methods = BIT(HTTP_GET),
.content_type = "application/json",
},
.static_data = info_json,
.static_data_len = sizeof(info_json) - 1,
};
/* DYNAMIC: callback chamado a cada requisição */
static struct http_resource_detail_dynamic relay_detail = {
.common = {
.type = HTTP_RESOURCE_TYPE_DYNAMIC,
.bitmask_of_supported_http_methods = BIT(HTTP_GET) | BIT(HTTP_POST),
},
.cb = web_app_relay_handler,
};
/* Ambos são registrados pela mesma macro */
HTTP_RESOURCE_DEFINE(info_resource, web_service, "/api/info", &info_detail);
HTTP_RESOURCE_DEFINE(relay_resource, web_service, "/api/relays", &relay_detail);
No resource STATIC, o detail aponta para um array de bytes na flash e o kernel apenas copia esses bytes para o socket TCP, sem CPU custom.
No DYNAMIC, o detail aponta para uma função chamada a cada requisição, e é ela quem decide o que fazer: parsear JSON, tomar o semáforo, chamar o driver e montar a resposta.
A regra que eu uso é simples: se a resposta depende de algo que pode mudar entre boots ou entre requisições, é dynamic; caso contrário, é static. O /api/info (versão de firmware, autor, contato) é static porque é constante. O /api/time é dynamic porque o RTC avança a cada segundo. Dynamic custa CPU e RAM de buffer, static é praticamente de graça.
Nossa aplicacao demo conta com doze rotas registradas, quatro estáticas e oito dinâmicas:
| Endpoint | Método | Descrição |
|---|---|---|
| /, /style.css, /app.js | GET | Recursos estáticos (gzip, direto da flash) |
| /api/info | GET | Versão do firmware, autor e contato |
| /api/time | GET | Hora atual já convertida para o fuso local |
| /api/relays | GET/POST | Estado dos 8 relés / acionamento individual |
| /api/rgb | GET/POST | Cor atual / set RGB do WS2812 |
| /api/buzzer | GET/POST | Status / play e stop de ringtone RTTTL |
| /api/files | GET | Listagem de diretório e estatísticas do LittleFS |
| /api/upload | POST | Upload multipart de arquivo |
| /api/delete | POST | Remoção de arquivo |
| /download/* | GET | Download de arquivo em chunks |
Acionar um relé é um POST com dois campos:
$ curl -X POST http://embarcados.local/api/relays \
-H "Content-Type: application/json" \
-d '{"ch":3,"state":true}'
{"relays":[false,false,true,false,false,false,false,false]}
Do lado do firmware, o handler faz o parse com a biblioteca JSON do próprio Zephyr (CONFIG_JSON_LIBRARY), protege o acesso com um semáforo e chama o driver GPIO:
ret = json_obj_parse((char *)request_ctx->data, request_ctx->data_len,
relay_cmd_descr, ARRAY_SIZE(relay_cmd_descr), &cmd);
k_sem_take(&relay_lock, K_FOREVER);
if (ret > 0 && cmd.ch >= 1 && cmd.ch <= NUM_RELAYS) {
idx = cmd.ch - 1;
relay_state[idx] = cmd.state;
ret = gpio_pin_set_dt(&relays[idx], relay_state[idx] ? 1 : 0);
}
len = web_app_build_relays_json(resp_buf, sizeof(resp_buf));
k_sem_give(&relay_lock);
SET_JSON_RESPONSE(response_ctx, resp_buf, len);
Dois detalhes valem para qualquer handler dinâmico. O primeiro é que a resposta sempre devolve o estado completo, e não um simples “ok”: assim o cliente que perdeu uma atualização se ressincroniza sozinho. O segundo é que o handler é chamado várias vezes ao longo da mesma transação (com os headers, com o corpo e no encerramento ou aborto), então é obrigatório olhar o status e o request_ctx->data_len antes de agir. Ignorar isso é a origem mais comum de comportamento estranho com o CONFIG_HTTP_SERVER.
O LED RGB segue o mesmo padrão, com o POST recebendo a cor e o handler chamando o driver led_strip do WS2812.
Gerenciamento de arquivos
Como citado na introdução, a camada de sistema de arquivos do Zephyr é independente da implementação. Aqui usamos LittleFS, montado em /lfs1 sobre uma partição de 64 KB.
Vale um esclarecimento sobre onde esses 64 KB ficam. O RP2040 não possui flash interna: o Pico W traz uma memória QSPI externa de 2 MB, mapeada em memória por XIP (execute in place) a partir do endereço 0x10000000. É essa flash que o overlay particiona, reservando o início para o bootloader de segundo estágio, o miolo para a aplicação e o final para o sistema de arquivos:
| Partição | Offset | Endereço inicial | Endereço final | Tamanho | Uso |
|---|---|---|---|---|---|
| second_stage_bootloader | 0x000000 | 0x10000000 | 0x100000FF | 256 B | Boot2 do RP2040 (somente leitura) |
| code-partition | 0x000100 | 0x10000100 | 0x101EFFFF | 1.983,75 KB | Firmware da aplicação |
| storage | 0x1F0000 | 0x101F0000 | 0x101FFFFF | 64 KB | LittleFS montado em /lfs1 |
Esse é o motivo de o relatório de build informar 2031360 B como tamanho da região FLASH: o valor corresponde exatamente à code-partition, e não aos 2 MB do componente. O espaço destinado ao sistema de arquivos fica fora do alcance do linker, o que evita que o crescimento do firmware invada a área de dados.
Redimensionar o sistema de arquivos é uma questão de ajustar duas linhas do overlay, lembrando que o LittleFS opera em setores de 4 KB e que o total precisa ser múltiplo desse valor. Com o filesystem montado, o /api/files devolve a listagem do diretório junto com as estatísticas de ocupação:
$ curl http://embarcados.local/api/files
{
"dir":"/lfs1",
"total_bytes":65536,
"used_bytes":21888,
"used_pct":33,
"files":[
{"name":"guzman.jpeg","size":21886,"type":"file"}
]
}
O download é a parte mais interessante, porque é onde fica evidente que não dá para pensar como em um servidor de PC. Não existe RAM para carregar um arquivo inteiro antes de enviar. O handler mantém o arquivo aberto entre as chamadas e vai devolvendo blocos de 1 KB, sinalizando o fim pelo final_chunk:
bytes = fs_read(&file, chunk, sizeof(chunk)); /* DOWNLOAD_CHUNK_SIZE = 1024 */
if (bytes <= 0) {
fs_close(&file);
response_ctx->final_chunk = true;
return 0;
}
response_ctx->body = chunk;
response_ctx->body_len = bytes;
response_ctx->final_chunk = (bytes < (ssize_t)sizeof(chunk));
Como esse estado é estático e compartilhado, o handler guarda qual cliente é o dono da transferência (download_owner) e usa um semáforo para serializar os downloads. E, principalmente, trata HTTP_SERVER_TRANSACTION_ABORTED: se o usuário fecha a aba no meio do download, o arquivo é fechado e o semáforo é liberado. Sem esse tratamento, o segundo download trava para sempre, e esse é exatamente o tipo de bug que só aparece na casa do cliente.
Shell via serial e Telnet
O shell do Zephyr é habilitado com poucas linhas de Kconfig e atende simultaneamente pelo console USB CDC e pela porta 23:
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_TELNET=y
CONFIG_SHELL_TELNET_PORT=23
Com CONFIG_FILE_SYSTEM_SHELL, CONFIG_RTC_SHELL e CONFIG_NET_SHELL ativos, ganhamos comandos de filesystem, relógio e diagnóstico de rede, além dos comandos próprios da aplicação:
$ telnet embarcados.local
uart:~$ fs ls /lfs1
uart:~$ net iface
uart:~$ ringtone play "Beep:d=4,o=5,b=100:c,e,g"
Build, gravação e debug
Antes do primeiro build é preciso baixar os blobs binários exigidos pela placa. O Wi-Fi do Pico W depende do firmware proprietário do CYW43439, distribuído pela Infineon, e o RP2040 utiliza bibliotecas HAL da Raspberry Pi. Por questões de licenciamento, esses arquivos não fazem parte da árvore do Zephyr e são obtidos sob demanda pelo west:
$ west blobs fetch hal_infineon
$ west blobs fetch hal_rpi_picoOs blobs ficam armazenados dentro dos respectivos módulos e só precisam ser baixados novamente quando o workspace é recriado ou quando um west update traz uma versão diferente dos módulos. Vale conferir esse passo ao migrar o projeto para outra máquina: sem os blobs, o build falha na etapa de link do driver airoc.
Feito isso, o build segue o padrão do Zephyr, apontando o board root para a placa customizada e o diretório lib/ como módulos extras:
$ west build -b dev_rpi_pico/rp2040/w -p -s app -d app/build \
-- -DBOARD_ROOT=${PWD} \
-DZEPHYR_EXTRA_MODULES=${PWD}/lib \
-DDTC_OVERLAY_FILE=${PWD}/app/boards/dev_rpi_pico_rp2040_w.overlay
Memory region Used Size Region Size %age Used
BOOT_FLASH: 256 B 256 B 100.00%
FLASH: 733608 B 2031360 B 36.11%
RAM: 178368 B 264 KB 65.98%
Servidor HTTP, Wi-Fi, TCP/IP, LittleFS, shell, logging, JSON e a interface web inteira em 716 KB de flash. Para gravar, west flash usa o picotool com a placa em modo BOOTSEL, ou basta copiar o zephyr.uf2 para o volume RPI-RP2.
VSCODE
Todo o fluxo está automatizado no VSCode, em .vscode/tasks.json, acessível pelo Ctrl+Shift+B:
Para esta demonstração, criamos task runners no VSCode para automatizar todo o processo de build, gravação e depuração. Abaixo seguem as tasks criadas:
| Task | O que faz |
|---|---|
| app: build | Build otimizado, apenas prj.conf |
| app: build (debug.conf) | Build com símbolos extras de debug |
| app: flash | west flash |
| app: rom_report | Uso de flash por símbolo e módulo |
| app: ram_report | Uso de RAM por símbolo e módulo |
| app: clear | Limpa o diretório de build |
Os alvos rom_report e ram_report merecem destaque: eles mostram exatamente quem está consumindo memória, por símbolo e por módulo. Quando o build começa a apertar, é ali que se descobre que 20 KB foram embora em um subsistema que ninguém lembrava de ter habilitado no prj.conf.
A depuração com breakpoints também está pronta no launch.json, com dois perfis de Cortex-Debug: J-Link, para quem usa a sonda da SEGGER, e Pico-Probe, que usa OpenOCD com o Raspberry Pi Debug Probe. Vale lembrar que um Pico comum pode virar uma sonda CMSIS-DAP gravando nele o firmware debugprobe_on_pico.uf2, ou seja, dá para depurar um Pico com outro Pico, sem comprar nada.
Video da Aplicação
Conclusão
Nesta aplicação de demonstração foi possível desenvolver uma solução IoT completa, apresentando na prática como construir uma interface web embarcada, disponibilizar serviços por meio de APIs REST, acessar remotamente o terminal Shell via Telnet, realizar a transferência de arquivos entre o computador e o dispositivo e controlar recursos como um LED RGB e a reprodução de toques (ringtones).
Além de servir como demonstração, a infraestrutura criada pode ser facilmente reutilizada em aplicações reais para funcionalidades como atualização remota de firmware (OTA), coleta de logs de diagnóstico, gerenciamento de arquivos e configuração remota do dispositivo.
Com esse projeto também foi possível demonstrar o potencial do ecossistema de bibliotecas do Zephyr. Grande parte dos componentes utilizados, como a pilha de rede, servidor HTTP, sistema de arquivos, Shell e demais serviços, pode ser reaproveitada em outras plataformas suportadas pelo RTOS. Por exemplo, a mesma aplicação pode ser portada com poucas alterações para um ESP32 com conectividade Wi-Fi, aproveitando praticamente toda a arquitetura desenvolvida.
Alguns recursos importantes não foram abordados nesta demonstração, como a utilização de HTTPS com certificados TLS para comunicação segura e a integração com MQTT para conexão da placa ao Home Assistant, permitindo a criação de automações residenciais. Esses tópicos serão explorados em um artigo futuro.
Código Fonte
Todo o projeto, incluindo a placa customizada, as bibliotecas em lib/, a documentação de cada endpoint e as configurações do VSCode, está disponível em:








