Visão Geral e Criação do Primeiro Projeto no Zephyr
Após a instalação correta do Zephyr OS e da ferramenta west, o próximo passo natural é compreender como estruturar um projeto real e como o Zephyr organiza sua execução concorrente por meio de threads. Diferente de abordagens bare-metal tradicionais, o Zephyr já nasce como um sistema operacional de tempo real (RTOS) completo, com escalonador, gerenciamento de pilha, prioridades e sincronização integrados ao núcleo.
Um projeto Zephyr não é apenas um conjunto de arquivos C. Ele é definido por três pilares fundamentais:
- Código-fonte da aplicação
- Configuração do kernel e subsistemas (Kconfig)
- Descrição do hardware alvo (Device Tree)
Mesmo um projeto simples precisa respeitar essa arquitetura, pois é ela que permite ao Zephyr ser altamente portátil entre arquiteturas como ARM Cortex-M, RISC-V e x86.
Estrutura mínima de um projeto Zephyr
Um projeto básico no Zephyr possui a seguinte estrutura:
meu_projeto/
├── CMakeLists.txt
├── prj.conf
└── src/
└── main.c
Cada um desses arquivos tem um papel bem definido:
- CMakeLists.txt: descreve a aplicação para o sistema de build do Zephyr.
- prj.conf: define quais recursos do kernel e drivers serão habilitados.
- main.c: contém o código da aplicação.
Exemplo mínimo de CMakeLists.txt:
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(primeiro_projeto)
target_sources(app PRIVATE src/main.c)
Esse arquivo conecta o projeto ao núcleo do Zephyr e informa qual arquivo C será compilado como aplicação.
Arquivo de configuração do projeto (prj.conf)
O prj.conf é onde se define o comportamento do kernel. Para trabalhar com threads, algumas opções são fundamentais:
CONFIG_PRINTK=y
CONFIG_LOG=y
CONFIG_MAIN_STACK_SIZE=1024
CONFIG_HEAP_MEM_POOL_SIZE=2048
Essas opções:
- Ativam saída de debug (
printke sistema de log) - Definem o tamanho da pilha da thread principal
- Reservam memória dinâmica para criação de threads adicionais
No Zephyr, nada é habilitado por padrão sem intenção explícita, o que reduz consumo de memória e aumenta previsibilidade — um ponto crítico em sistemas embarcados.
O papel da thread principal (main)
No Zephyr, a função main() não é executada em bare-metal. Ela roda dentro de uma thread já criada pelo kernel, chamada informalmente de main thread. Essa thread:
- Possui prioridade configurável
- Possui pilha própria
- Pode bloquear, dormir ou ser preemptada
Exemplo simples de main.c:
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
void main(void)
{
while (1) {
printk("Thread principal em execução\n");
k_sleep(K_SECONDS(1));
}
}
Aqui já aparecem conceitos essenciais do Zephyr:
k_sleep()coloca a thread em estado bloqueado, liberando a CPU- O escalonador decide qual thread executa em seguida
- Não há busy wait, o que melhora eficiência energética e temporal
Esse comportamento é radicalmente diferente de um while(1) típico de sistemas bare-metal.
Por que usar threads no Zephyr?
Threads são usadas quando há necessidade de:
- Executar tarefas independentes concorrentemente
- Garantir tempos de resposta previsíveis
- Isolar funcionalidades críticas
- Simplificar arquitetura orientada a eventos
No Zephyr, tudo é thread: drivers, subsistemas, stacks de rede e sua aplicação compartilham o mesmo modelo de execução, o que facilita análise temporal e depuração.
Na próxima seção, entraremos no modelo de threads do Zephyr, explicando:
- prioridades
- estados
- pilha
- criação estática e dinâmica
- boas práticas e erros comuns
Modelo de Threads no Zephyr: Prioridades, Estados e Escalonamento
O coração do Zephyr OS é o seu escalonador preemptivo de tempo real, projetado para operar de forma determinística mesmo em microcontroladores com poucos kilobytes de RAM. Para usar threads corretamente, é fundamental compreender como o Zephyr enxerga uma thread, quais estados ela pode assumir e como o kernel decide qual delas executa a cada instante.
Diferente de alguns RTOS mais antigos ou simplificados, o Zephyr adota um modelo moderno, fortemente inspirado em sistemas operacionais clássicos, porém adaptado às restrições de sistemas embarcados.
Prioridades no Zephyr: quanto menor, mais prioritária
No Zephyr, prioridade é um valor inteiro, e aqui existe um detalhe crucial:
- Valores menores representam maior prioridade
- Threads podem ser:
- Cooperativas
- Preemptivas
A separação acontece por meio de faixas de prioridade.
Exemplo típico:
| Prioridade | Tipo |
| -1 a -CONFIG_NUM_COOP_PRIORITIES | Cooperativas |
| 0 a CONFIG_NUM_PREEMPT_PRIORITIES | Preemptivas |
Threads cooperativas nunca são interrompidas por outras threads, apenas cedem a CPU voluntariamente. Já threads preemptivas podem ser interrompidas assim que uma thread de maior prioridade fica pronta para executar.
Essa distinção permite arquiteturas híbridas extremamente eficientes.
Estados possíveis de uma thread
Uma thread no Zephyr pode assumir vários estados ao longo de sua vida útil:
- READY (Pronta)
A thread está apta a executar e aguarda escalonamento. - RUNNING (Executando)
A thread está ocupando a CPU no momento. - BLOCKED (Bloqueada)
Está aguardando um evento, tempo ou recurso (ex:k_sleep,k_sem_take). - SUSPENDED (Suspensa)
Foi explicitamente suspensa por outra thread. - DEAD (Encerrada)
A execução terminou e seus recursos foram liberados.
O kernel transita entre esses estados automaticamente, garantindo previsibilidade temporal.
Escalonamento: como o Zephyr decide quem executa
O escalonador do Zephyr segue regras claras:
- Sempre executa a thread pronta de maior prioridade
- Entre threads da mesma prioridade:
- Usa round-robin, se habilitado
- Threads cooperativas nunca sofrem preempção
- Chamadas bloqueantes liberam a CPU imediatamente
Esse modelo é especialmente importante em aplicações embarcadas críticas, como controle motor, protocolos industriais e sensores em tempo real.
Criando uma thread explicitamente
No Zephyr, threads podem ser criadas de forma estática ou dinâmica. A forma mais comum e segura em sistemas embarcados é a criação estática.
Exemplo completo:
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#define STACK_SIZE 1024
#define PRIORITY 5
K_THREAD_STACK_DEFINE(thread_stack, STACK_SIZE);
struct k_thread thread_data;
void thread_func(void *arg1, void *arg2, void *arg3)
{
while (1) {
printk("Thread secundária em execução\n");
k_sleep(K_SECONDS(2));
}
}
void main(void)
{
k_thread_create(&thread_data,
thread_stack,
STACK_SIZE,
thread_func,
NULL, NULL, NULL,
PRIORITY,
0,
K_NO_WAIT);
while (1) {
printk("Thread principal\n");
k_sleep(K_SECONDS(1));
}
}
Aqui aparecem conceitos fundamentais:
K_THREAD_STACK_DEFINE: reserva pilha estaticamentestruct k_thread: controle interno da thread- Função com três argumentos genéricos: padrão do Zephyr
- Prioridade explícita
- Delay inicial configurável
Esse modelo evita alocação dinâmica descontrolada, algo crítico em sistemas embarcados.
Erros comuns no uso de threads
Alguns erros aparecem com frequência em projetos iniciais:
- Criar muitas threads sem necessidade
- Subdimensionar pilha
- Usar prioridade inadequada
- Bloquear threads críticas com
k_sleep - Misturar lógica de tempo real com lógica de aplicação
No Zephyr, menos threads bem definidas quase sempre é melhor do que muitas threads mal planejadas.
Gerenciamento Correto de Pilha (Stack) de Threads no Zephyr
Em sistemas embarcados, a pilha de uma thread é um recurso crítico e finito. Diferente de sistemas operacionais de propósito geral, não existe “memória virtual” nem crescimento automático de stack. No Zephyr, cada thread possui uma pilha dedicada, e o erro mais comum em projetos iniciais é subdimensionar ou superdimensionar esse recurso.
Gerenciar corretamente a pilha é fundamental para:
- Evitar stack overflow
- Garantir previsibilidade
- Reduzir consumo de RAM
- Aumentar confiabilidade do sistema
Como o Zephyr organiza a pilha de uma thread
Cada thread no Zephyr possui:
- Uma área de stack exclusiva
- Guard regions (quando habilitado)
- Metadados usados pelo kernel
A pilha é usada para:
- Variáveis locais
- Contexto de chamada de funções
- Salvamento de registradores
- Interrupções aninhadas (dependendo da arquitetura)
Exemplo de definição explícita:
#define STACK_SIZE 1024
K_THREAD_STACK_DEFINE(my_stack, STACK_SIZE);
Esse valor não é arbitrário. Ele precisa considerar:
- Profundidade de chamadas
- Uso de bibliotecas (ex: printf, logging)
- Contextos de interrupção
- Otimizações do compilador
Dimensionando a pilha de forma técnica
Uma regra prática inicial:
- Threads simples (controle, polling): 512 a 1024 bytes
- Threads com logs, strings, buffers: 1024 a 2048 bytes
- Threads de rede, filesystem ou criptografia: 2048 bytes ou mais
No entanto, o correto é medir, não apenas estimar.
Habilitando monitoramento de stack
O Zephyr oferece mecanismos nativos para análise de consumo de pilha.
No prj.conf:
CONFIG_THREAD_STACK_INFO=y
CONFIG_INIT_STACKS=y
CONFIG_THREAD_MONITOR=y
Essas opções permitem que o kernel:
- Inicialize a stack com um padrão conhecido
- Monitore uso máximo
- Detecte corrupção
Medindo o uso real da pilha
Com CONFIG_THREAD_STACK_INFO habilitado, é possível consultar o consumo real:
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
void print_stack_usage(struct k_thread *thread)
{
size_t unused;
k_thread_stack_space_get(thread, &unused);
printk("Stack livre: %u bytes\n", unused);
}
Esse tipo de análise é essencial durante testes e validação.
Boa prática:
- Medir em cenários de pior caso
- Executar todos os caminhos críticos
- Avaliar após integração de novas bibliotecas
Stack overflow: o inimigo silencioso
Quando ocorre um stack overflow:
- O comportamento pode ser imprevisível
- Variáveis de outras threads podem ser corrompidas
- O sistema pode travar sem mensagens claras
Para mitigar isso, habilite proteção:
CONFIG_STACK_SENTINEL=y
Em arquiteturas suportadas, isso ativa mecanismos de detecção automática de estouro de pilha.
Thread principal também tem stack
Um erro comum é esquecer que a main() também roda em uma thread.
O tamanho da pilha da thread principal é configurado em:
CONFIG_MAIN_STACK_SIZE=1024
Se você usa:
printk- buffers locais
- chamadas profundas
Esse valor deve ser ajustado cuidadosamente.
Boas práticas de engenharia
- Nunca compartilhe stack entre threads
- Evite grandes variáveis locais
- Prefira buffers estáticos ou globais controlados
- Use análise de stack durante testes
- Documente o propósito de cada thread
Em Zephyr, a arquitetura correta começa pela pilha.
Quando Usar Threads (e Quando NÃO Usar) no Zephyr OS
Um dos erros mais comuns em projetos iniciais com Zephyr é assumir que toda funcionalidade precisa virar uma thread. Embora o Zephyr facilite a criação de threads, isso não significa que elas sejam sempre a melhor escolha. Em sistemas embarcados de tempo real, threads são recursos caros: consomem RAM (stack), exigem escalonamento e aumentam a complexidade de análise temporal.
Um bom projeto Zephyr começa respondendo à pergunta:
“Essa funcionalidade realmente precisa de uma thread dedicada?”
Quando o uso de threads é justificado
Threads devem ser usadas quando a tarefa:
- Executa continuamente ou por longos períodos
- Possui requisitos temporais bem definidos
- Pode bloquear (sleep, semáforos, filas) sem impactar o restante do sistema
- Representa uma responsabilidade clara e isolável (ex: controle, comunicação, aquisição)
Exemplos típicos:
- Thread de controle de motor
- Thread de comunicação (UART, TCP/IP)
- Thread de aquisição periódica de sensores
- Thread de processamento pesado (FFT, filtros, criptografia)
Nesses casos, a thread fornece isolamento, previsibilidade e clareza arquitetural.
Quando threads NÃO são a melhor solução
Evite threads quando a funcionalidade:
- Executa rapidamente
- É acionada por eventos esporádicos
- Não pode consumir stack adicional
- Apenas reage a interrupções ou timers
Exemplos ruins de uso de thread:
- Piscar LED simples
- Debounce de botão
- Atualização de flags
- Callbacks de periféricos rápidos
Para esses cenários, o Zephyr oferece mecanismos mais eficientes.
Alternativa 1 – Timers (k_timer)
Timers são ideais para tarefas periódicas simples.
Exemplo:
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
void timer_handler(struct k_timer *timer)
{
printk("Timer expirou\n");
}
K_TIMER_DEFINE(my_timer, timer_handler, NULL);
void main(void)
{
k_timer_start(&my_timer, K_SECONDS(1), K_SECONDS(1));
}
Vantagens:
- Zero stack dedicada
- Execução curta
- Baixo overhead
Limitação:
- Não deve executar código pesado ou bloqueante
Alternativa 2 – Work Queues (k_work)
As work queues são uma das ferramentas mais poderosas do Zephyr. Elas permitem executar código em contexto de thread sem criar uma nova thread.
Exemplo:
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
void work_handler(struct k_work *work)
{
printk("Work executado\n");
}
K_WORK_DEFINE(my_work, work_handler);
void main(void)
{
k_work_submit(&my_work);
}
Características importantes:
- Executa em uma thread compartilhada
- Ideal para processamento pós-interrupção
- Reduz drasticamente consumo de RAM
Essa abordagem é extremamente comum em drivers e stacks do próprio Zephyr.
Alternativa 3 – Arquitetura orientada a eventos
Em vez de múltiplas threads, muitas aplicações Zephyr são mais eficientes quando estruturadas como:
- Uma thread principal
- Múltiplos eventos
- Filas, sinais ou notificações
Exemplo conceitual:
- ISR gera evento
- Evento dispara work
- Work atualiza estado
- Thread principal reage
Essa arquitetura:
- Simplifica análise temporal
- Reduz número de stacks
- Melhora testabilidade
Critério prático de decisão
Use este critério simples:
Se a tarefa precisa esperar algo → thread
Se a tarefa apenas reage → evento/work/timer
Em projetos profissionais com Zephyr, é comum encontrar:
- Poucas threads
- Muitas works
- Uso intenso de eventos
Isso resulta em sistemas mais estáveis, previsíveis e fáceis de manter.
Perfeito. Vamos seguir.
Gerenciamento do Ciclo de Vida das Threads no Zephyr
Criar uma thread é apenas o primeiro passo. Em sistemas embarcados reais, o ponto mais delicado está em controlar corretamente o ciclo de vida das threads, evitando situações como threads órfãs, consumo desnecessário de CPU, starvation ou comportamentos difíceis de depurar. O Zephyr fornece uma API clara para iniciar, pausar, retomar e finalizar threads, mas essas operações devem ser usadas com critério.
Um bom projeto trata threads como entidades gerenciáveis, não como laços infinitos esquecidos no código.
Criação e início de execução
Como visto anteriormente, uma thread pode ser criada já pronta para executar ou com atraso inicial:
k_thread_create(&thread_data,
thread_stack,
STACK_SIZE,
thread_func,
NULL, NULL, NULL,
PRIORITY,
0,
K_NO_WAIT);
O último parâmetro define quando a thread entra no estado READY. Em sistemas mais complexos, é comum atrasar o início para garantir que:
- Drivers já estejam inicializados
- Recursos compartilhados estejam prontos
- Outras threads críticas já estejam rodando
Exemplo com atraso inicial:
K_SECONDS(5)
Suspensão e retomada de threads
O Zephyr permite suspender explicitamente uma thread:
k_thread_suspend(&thread_data);
E retomá-la posteriormente:
k_thread_resume(&thread_data);
Esse mecanismo é útil quando:
- Uma funcionalidade é temporariamente desnecessária
- Um recurso externo não está disponível
- O sistema entra em modo de economia de energia
⚠️ Cuidado: suspender threads críticas pode causar deadlocks se elas forem responsáveis por liberar recursos.
Encerramento de threads
Uma thread pode terminar de duas formas:
- Retornando da função
- Chamando explicitamente:
k_thread_abort(&thread_data);
Após o encerramento:
- A thread entra em estado DEAD
- Seus recursos podem ser reutilizados
- Ela não pode ser retomada
Boa prática:
Threads permanentes → laço infinito
Threads temporárias → função com ciclo bem definido
Evitando starvation e inversão de prioridade
Em sistemas com múltiplas threads, dois problemas clássicos surgem:
- Starvation: uma thread nunca executa
- Inversão de prioridade: uma thread de baixa prioridade bloqueia uma de alta
O Zephyr ajuda a mitigar isso por meio de:
- Escalonamento preemptivo
- Herança de prioridade (em mutexes)
- Design correto de prioridades
Boa prática:
- Threads críticas → prioridade mais alta
- Threads de logging/UI → prioridade mais baixa
- Nunca usar prioridades extremas sem necessidade
Comunicação segura entre threads
Threads raramente vivem isoladas. Elas se comunicam por meio de:
- Filas (
k_msgq) - Semáforos (
k_sem) - Mutexes (
k_mutex) - Eventos (
k_event)
Exemplo simples com semáforo:
K_SEM_DEFINE(sync_sem, 0, 1);
void producer(void)
{
k_sem_give(&sync_sem);
}
void consumer(void)
{
k_sem_take(&sync_sem, K_FOREVER);
}
Esse modelo evita:
- Busy wait
- Condições de corrida
- Uso incorreto de flags globais
Threads e consumo de energia
Cada thread ativa:
- Aumenta wake-ups do kernel
- Reduz tempo em low power
- Aumenta consumo energético
Por isso, sistemas eficientes com Zephyr:
- Usam poucas threads
- Preferem bloqueio a polling
- Integram timers e events
- Aproveitam o idle thread do kernel
Seção 6 – Boas Práticas Arquiteturais para o Primeiro Projeto Zephyr
Ao dar os primeiros passos com o Zephyr OS, o maior diferencial entre um projeto experimental e um projeto profissional está na arquitetura escolhida desde o início. O Zephyr foi concebido para sistemas embarcados modernos, modulares e escaláveis, e isso exige uma mudança de mentalidade em relação ao desenvolvimento bare-metal tradicional.
A primeira boa prática é pensar em responsabilidades, não em laços infinitos. Cada thread deve representar uma função clara do sistema, como aquisição, controle ou comunicação. Se uma funcionalidade não precisa esperar por eventos ou executar continuamente, ela provavelmente não deveria ser uma thread, mas sim um work, timer ou callback.
Princípios fundamentais para projetos Zephyr bem-sucedidos
Um projeto Zephyr bem estruturado normalmente segue estes princípios:
- Poucas threads, bem definidas
- Uso extensivo de mecanismos de bloqueio (sem busy wait)
- Prioridades coerentes com criticidade temporal
- Pilhas dimensionadas com base em medição real
- Comunicação entre threads sempre via primitivas do kernel
Essa abordagem resulta em sistemas:
- Mais previsíveis
- Mais fáceis de depurar
- Mais eficientes energeticamente
- Mais portáveis entre placas e arquiteturas
Organização do código da aplicação
Uma organização comum e eficiente:
src/
├── main.c
├── sensor_task.c
├── comm_task.c
├── control_task.c
└── app_events.c
Cada módulo:
- Implementa uma responsabilidade
- Exporta apenas interfaces necessárias
- Evita dependências cruzadas desnecessárias
No Zephyr, modularidade não é luxo — é uma exigência para manter escalabilidade.
Erros clássicos a evitar no primeiro projeto
Alguns erros aparecem com frequência e devem ser evitados desde o início:
- Criar threads para tarefas triviais
- Ignorar o consumo de stack
- Usar prioridades “no chute”
- Compartilhar variáveis globais sem proteção
- Usar delays ativos em vez de bloqueio
Evitar esses erros economiza semanas de debug no futuro.
Fechamento do artigo
Este artigo apresentou os primeiros passos práticos com o Zephyr OS após a instalação, focando na criação do primeiro projeto, no modelo de threads, no gerenciamento correto de pilha, no uso consciente de concorrência e no controle do ciclo de vida das threads. Esses conceitos formam a base para qualquer aplicação profissional desenvolvida sobre o Zephyr.
Nos próximos artigos da série, é natural avançar para:
- Comunicação entre threads
- Arquitetura orientada a eventos
- Uso de drivers e Device Tree
- Integração com periféricos reais
Material para SEO
Título do Artigo
Primeiros Projetos com Zephyr OS: Threads, Arquitetura e Boas Práticas
Meta descrição
Frase chave foco
Palavras-chave
Se quiser, no próximo artigo podemos evoluir diretamente para sincronização entre threads no Zephyr, Device Tree aplicado à aplicação, ou arquitetura orientada a eventos.