Visão geral do exemplo e por que não existe main() neste src/main.c
/*
* Copyright (c) 2017 Linaro Limited
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/printk.h>
#include <zephyr/sys/__assert.h>
#include <string.h>
/* size of stack area used by each thread */
#define STACKSIZE 1024
/* scheduling priority used by each thread */
#define PRIORITY 7
#define LED0_NODE DT_ALIAS(led0)
#define LED1_NODE DT_ALIAS(led1)
#if !DT_NODE_HAS_STATUS_OKAY(LED0_NODE)
#error "Unsupported board: led0 devicetree alias is not defined"
#endif
#if !DT_NODE_HAS_STATUS_OKAY(LED1_NODE)
#error "Unsupported board: led1 devicetree alias is not defined"
#endif
struct printk_data_t {
void *fifo_reserved; /* 1st word reserved for use by fifo */
uint32_t led;
uint32_t cnt;
};
K_FIFO_DEFINE(printk_fifo);
struct led {
struct gpio_dt_spec spec;
uint8_t num;
};
static const struct led led0 = {
.spec = GPIO_DT_SPEC_GET_OR(LED0_NODE, gpios, {0}),
.num = 0,
};
static const struct led led1 = {
.spec = GPIO_DT_SPEC_GET_OR(LED1_NODE, gpios, {0}),
.num = 1,
};
void blink(const struct led *led, uint32_t sleep_ms, uint32_t id)
{
const struct gpio_dt_spec *spec = &led->spec;
int cnt = 0;
int ret;
if (!device_is_ready(spec->port)) {
printk("Error: %s device is not ready\n", spec->port->name);
return;
}
ret = gpio_pin_configure_dt(spec, GPIO_OUTPUT);
if (ret != 0) {
printk("Error %d: failed to configure pin %d (LED '%d')\n",
ret, spec->pin, led->num);
return;
}
while (1) {
gpio_pin_set(spec->port, spec->pin, cnt % 2);
struct printk_data_t tx_data = { .led = id, .cnt = cnt };
size_t size = sizeof(struct printk_data_t);
char *mem_ptr = k_malloc(size);
__ASSERT_NO_MSG(mem_ptr != 0);
memcpy(mem_ptr, &tx_data, size);
k_fifo_put(&printk_fifo, mem_ptr);
k_msleep(sleep_ms);
cnt++;
}
}
void blink0(void)
{
blink(&led0, 100, 0);
}
void blink1(void)
{
blink(&led1, 1000, 1);
}
void uart_out(void)
{
while (1) {
struct printk_data_t *rx_data = k_fifo_get(&printk_fifo,
K_FOREVER);
printk("Toggled led%d; counter=%d\n",
rx_data->led, rx_data->cnt);
k_free(rx_data);
}
}
K_THREAD_DEFINE(blink0_id, STACKSIZE, blink0, NULL, NULL, NULL,
PRIORITY, 0, 0);
K_THREAD_DEFINE(blink1_id, STACKSIZE, blink1, NULL, NULL, NULL,
PRIORITY, 0, 0);
K_THREAD_DEFINE(uart_out_id, STACKSIZE, uart_out, NULL, NULL, NULL,
PRIORITY, 0, 0);
O arquivo src/main.c do exemplo (“threads”) listado acima, foi escrito para demonstrar uma ideia central do Zephyr: o seu aplicativo pode ser composto por threads declaradas estaticamente, que o kernel cria e inicia automaticamente durante o boot, sem você precisar de um “superloop” com main() controlando tudo.
Em muitos firmwares baremetal (ou mesmo com alguns RTOS), o padrão é:
- o startup do microcontrolador chama
main(); main()inicializa periféricos;- cria tarefas/threads;
- entra em um
while(1).
No Zephyr, isso pode existir (há muitos exemplos que têm void main(void)), mas não é obrigatório. Neste sample, os autores optaram por um modelo ainda mais “RTOS puro”:
- O Zephyr sobe (boot + init do kernel).
- O kernel inicializa infraestrutura (scheduler, heap, drivers, etc.).
- As threads declaradas via
K_THREAD_DEFINE(...)já entram no sistema como “threads do aplicativo”. - Cada thread roda seu próprio loop e coopera via primitivas do kernel (aqui, uma FIFO).
Ou seja: quem faz o papel de “fluxo principal” é o próprio kernel. O aplicativo é “plugado” nele por meio de definições estáticas (macros) que geram objetos e registram tudo para o boot.
Onde “entra” o seu código então?
Neste exemplo, o código entra em três pontos:
blink0()→ thread que pisca o LED0 e publica mensagens na FIFO.blink1()→ thread que pisca o LED1 e publica mensagens na FIFO.uart_out()→ thread que consome a FIFO e imprime viaprintk().
Essas três funções viram threads por causa destas três linhas no final do arquivo:
K_THREAD_DEFINE(blink0_id, STACKSIZE, blink0, NULL, NULL, NULL, PRIORITY, 0, 0);
K_THREAD_DEFINE(blink1_id, STACKSIZE, blink1, NULL, NULL, NULL, PRIORITY, 0, 0);
K_THREAD_DEFINE(uart_out_id, STACKSIZE, uart_out, NULL, NULL, NULL, PRIORITY, 0, 0);
A macro K_THREAD_DEFINE não é “só açúcar sintático”: ela efetivamente gera (em tempo de compilação) estruturas de thread, stack, metadados e a forma de o kernel iniciar essas threads no boot. Por isso dá para ter um app “100% thread-based” sem main().
Por que isso é uma boa ideia no Zephyr?
- Reprodutibilidade de boot: as threads já existem na imagem final, com stacks definidos e conhecidos.
- Menos código de “cola”: não precisa de um
main()só para “criar threads”. - Integração com o modelo do Zephyr: o kernel tem etapas de init bem definidas; seu app pode se encaixar nelas sem reinventar o fluxo.
Na próxima seção, eu faço a leitura guiada do topo do arquivo (includes + #defines), e começo a destrinchar as macros de DeviceTree (DT_ALIAS, DT_NODE_HAS_STATUS_OKAY, GPIO_DT_SPEC_GET_OR) e por que elas existem no Zephyr.
Includes, #defines e a filosofia de portabilidade do Zephyr
Vamos agora caminhar do topo do src/main.c para baixo, entendendo por que cada include e cada macro existem — e como isso se conecta ao modelo arquitetural do Zephyr.
Includes principais
Logo no início do arquivo, aparecem includes semelhantes a estes:
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/printk.h>
Esses headers revelam muito sobre o estilo de programação do Zephyr.
<zephyr/kernel.h>
É o coração do RTOS. Aqui estão:
- tipos básicos do kernel (
k_tid_t,k_timeout_t, etc.), - primitivas de sincronização,
- definição das macros de thread (
K_THREAD_DEFINE,K_FIFO_DEFINE, etc.), - funções como
k_sleep(),k_fifo_put(),k_fifo_get().
Sem esse include, você literalmente não “fala” com o kernel.
<zephyr/device.h>
Introduz o tipo struct device, que representa instâncias concretas de hardware (GPIO, UART, I2C, SPI…).
No Zephyr:
- você não instancia drivers manualmente,
- os dispositivos são criados a partir do DeviceTree,
- o aplicativo apenas obtém uma referência para eles.
Esse detalhe é essencial para entender por que não existe GPIOA, GPIOB, ou registradores “na mão”, como em código baremetal tradicional.
<zephyr/devicetree.h>
Aqui começa uma das maiores diferenças entre Zephyr e firmwares clássicos.
O DeviceTree é um modelo declarativo de hardware, herdado do mundo Linux, mas adaptado para sistemas embarcados.
Esse header fornece macros que:
- acessam nós do DeviceTree em tempo de compilação,
- validam se um periférico existe,
- extraem pinos, portas, flags elétricas, etc.
Isso permite escrever o mesmo código C para placas completamente diferentes, sem #ifdef STM32, #ifdef NRF, etc.
<zephyr/drivers/gpio.h>
Define a API genérica de GPIO do Zephyr:
gpio_pin_configure_dt()gpio_pin_toggle_dt()gpio_is_ready_dt()
Repare no sufixo _dt: ele indica que a função trabalha diretamente com dados vindos do DeviceTree, encapsulados em estruturas como gpio_dt_spec.
<zephyr/sys/printk.h>
Fornece printk(), que é:
- uma saída de debug simples,
- segura para uso em contexto de kernel,
- muito usada em exemplos.
Diferente de printf, printk não depende de libc completa e é mais previsível em RTOS.
Macros de configuração: STACKSIZE e PRIORITY
Logo após os includes, aparecem definições como:
#define STACKSIZE 1024
#define PRIORITY 7
Essas macros não são decorativas.
STACKSIZE
No Zephyr, cada thread tem sua própria pilha, explicitamente definida.
Isso é fundamental por dois motivos:
- Determinismo
Em sistemas embarcados críticos, saber exatamente quanto de stack cada thread usa é parte do projeto. - Proteção e debug
O Zephyr pode aplicar stack canaries e stack overflow detection.
Aqui, cada thread criada com K_THREAD_DEFINE receberá uma pilha de 1024 bytes.
PRIORITY
No Zephyr:
- números menores → prioridade maior
- prioridades negativas são reservadas a threads do sistema
- prioridades positivas são para o aplicativo
Logo, PRIORITY 7 significa:
thread de aplicação, com prioridade relativamente baixa.
Isso é coerente com o exemplo:
- piscar LED e imprimir mensagens não são tarefas críticas.
Introdução ao DeviceTree no código
Agora chegamos a um trecho essencial:
#define LED0_NODE DT_ALIAS(led0)
#define LED1_NODE DT_ALIAS(led1)
Aqui entra um conceito que costuma confundir quem vem do baremetal.
O que é DT_ALIAS(led0)?
No arquivo .dts da placa (ou em um overlay), existe algo como:
aliases {
led0 = &led0;
led1 = &led1;
};
O DT_ALIAS(led0):
- resolve, em tempo de compilação, o nó correspondente,
- gera um identificador interno usado pelas macros seguintes.
Isso elimina:
- números mágicos de pino,
- dependência direta da placa,
#ifdefpor fabricante.
Validação do hardware em compile time: DT_NODE_HAS_STATUS_OKAY e GPIO_DT_SPEC_GET_OR
Agora entramos em uma das partes mais importantes e menos triviais do código: o trecho que liga o DeviceTree ao código C de forma segura, portável e verificável em tempo de compilação.
O código é similar a este:
static const struct gpio_dt_spec led0 =
GPIO_DT_SPEC_GET_OR(LED0_NODE, gpios, {0});
static const struct gpio_dt_spec led1 =
GPIO_DT_SPEC_GET_OR(LED1_NODE, gpios, {0});
Mas antes disso, normalmente aparece um #if de proteção:
#if !DT_NODE_HAS_STATUS_OKAY(LED0_NODE)
#error "Unsupported board: led0 devicetree alias is not defined"
#endif
Vamos entender cada parte com profundidade.
DT_NODE_HAS_STATUS_OKAY(...)
O que essa macro realmente faz?
Essa macro pergunta ao DeviceTree:
“Existe um nó com esse identificador e ele está marcado como
status = "okay"?”
No .dts, um LED típico é descrito assim:
led0: led_0 {
gpios = <&gpioa 5 GPIO_ACTIVE_LOW>;
label = "User LED";
status = "okay";
};
Se:
- o alias não existir, ou
- o nó existir mas estiver desativado (
status = "disabled"),
então DT_NODE_HAS_STATUS_OKAY(LED0_NODE) será falso.
Por que o Zephyr prefere falhar em compile time?
Este #error não é um capricho.
Ele garante que:
- você não gera um firmware inválido,
- o erro aparece antes de gravar no hardware,
- o mesmo código fonte denuncia automaticamente placas incompatíveis.
Em código baremetal clássico, isso só seria percebido:
- em runtime,
- ou pior, como um LED que “não pisca” sem explicação.
struct gpio_dt_spec
Agora vamos à estrutura que tudo gira em torno:
struct gpio_dt_spec {
const struct device *port;
gpio_pin_t pin;
gpio_flags_t dt_flags;
};
Ela encapsula tudo que você precisa para manipular um GPIO, sem saber:
- qual MCU é,
- qual registrador usar,
- se é STM32, nRF, ESP32, etc.
O DeviceTree preenche isso para você.
GPIO_DT_SPEC_GET_OR(...)
Essa macro é uma obra-prima de engenharia de firmware orientada a portabilidade.
GPIO_DT_SPEC_GET_OR(LED0_NODE, gpios, {0})
Vamos decompor:
1. LED0_NODE
É o nó do DeviceTree resolvido pelo alias (led0).
2. gpios
É o nome da propriedade dentro do nó.
No .dts:
gpios = <&gpioa 5 GPIO_ACTIVE_LOW>;
Ou seja:
- controlador GPIO,
- número do pino,
- flags elétricas.
3. {0}
Valor padrão caso a propriedade não exista.
Isso evita:
- ponteiros inválidos,
- acesso a hardware inexistente,
- falhas silenciosas.
O que essa macro gera na prática?
Ela cria uma instância constante de gpio_dt_spec, preenchida em tempo de compilação, algo conceitualmente assim:
led0.port = DEVICE_DT_GET(DT_NODELABEL(gpioa));
led0.pin = 5;
led0.dt_flags = GPIO_ACTIVE_LOW;
Mas sem você escrever nada disso manualmente — e de forma portátil.
Checagem em runtime: gpio_is_ready_dt()
Mais adiante, dentro da thread, aparece:
if (!gpio_is_ready_dt(&led0)) {
return;
}
Aqui há uma distinção importante:
- compile time: o nó existe e está válido
- runtime: o driver foi inicializado corretamente
Essa separação é arquiteturalmente correta:
- DeviceTree valida o hardware declarado
gpio_is_ready_dt()valida o estado do driver
Seção 4 — Comunicação entre threads: struct msg e K_FIFO_DEFINE
Agora entramos no mecanismo central de comunicação entre as threads deste exemplo: a FIFO do Zephyr.
Aqui o código deixa claro que o objetivo do sample não é piscar LEDs, mas demonstrar concorrência e troca de dados entre threads.
A estrutura struct msg
O código define algo conceitualmente assim:
struct msg {
void *fifo_reserved;
uint32_t led;
uint32_t cnt;
};
Essa estrutura tem um detalhe que costuma passar despercebido por quem vem de FreeRTOS ou CMSIS-RTOS:
void *fifo_reserved;
Esse campo não é opcional.
No Zephyr, as FIFOs (k_fifo) funcionam de maneira diferente de queues tradicionais:
- o kernel não copia dados para dentro da fila,
- ele encadeia objetos já existentes na memória,
- para isso, o primeiro campo da estrutura deve ser reservado para o kernel.
Ou seja:
- o kernel usa esse ponteiro para formar a lista encadeada interna da FIFO,
- você nunca deve tocar nesse campo manualmente.
📌 Regra de ouro no Zephyr
Se um objeto vai para uma FIFO ou LIFO, o primeiro membro da struct deve ser um ponteiro reservado ao kernel.
Campos de dados da mensagem
uint32_t led;
uint32_t cnt;
Aqui temos o payload real:
led→ identifica qual thread/LED gerou a mensagemcnt→ contador de quantas vezes aquele LED já piscou
Isso permite que uma thread produza dados e outra consuma, sem qualquer dependência direta entre elas.
Definição da FIFO: K_FIFO_DEFINE
Em seguida, aparece algo como:
K_FIFO_DEFINE(printk_fifo);
Essa macro:
- cria uma FIFO global,
- alocada estaticamente,
- pronta para uso desde o boot.
Não há:
malloc,init,create.
Tudo é resolvido em tempo de compilação, o que combina perfeitamente com sistemas embarcados determinísticos.
Internamente, essa macro cria:
- uma
struct k_fifo, - os objetos auxiliares necessários,
- e registra tudo para o kernel.
Produzindo mensagens: k_fifo_put()
Dentro das threads blink0() e blink1(), aparece um trecho parecido com:
struct msg msg;
msg.led = 0;
msg.cnt = cnt++;
k_fifo_put(&printk_fifo, &msg);
Aqui há um ponto extremamente importante de engenharia de firmware:
Onde está alocada msg?
Se msg for:
- variável local na stack → ERRO GRAVE
- variável estática ou global → CORRETO
O exemplo real do Zephyr usa variáveis estáticas, exatamente porque:
- a FIFO guarda apenas o ponteiro,
- se a variável sair de escopo, o ponteiro ficará inválido,
- isso gera comportamento indefinido.
Esse detalhe é intencional no sample:
ele força o leitor a entender o modelo de memória do kernel.
📌 Comparação com FreeRTOS
No FreeRTOS, xQueueSend() copia os dados.
No Zephyr, k_fifo_put() encadeia o objeto.
Consumindo mensagens: k_fifo_get()
Na thread uart_out():
struct msg *msg;
msg = k_fifo_get(&printk_fifo, K_FOREVER);
Aqui vemos três conceitos importantes:
- Retorno por ponteiro
O kernel devolve o endereço do objeto original. - Bloqueio controlado
K_FOREVERindica que a thread ficará bloqueada até chegar uma mensagem. - Eficiência extrema
Não há cópia de dados, apenas manipulação de ponteiros.
Depois disso, o código faz:
printk("Toggled led%d; counter=%d\n", msg->led, msg->cnt);
A thread consumidora:
- não sabe quem produziu,
- não controla LEDs,
- apenas interpreta dados.
Isso é arquitetura orientada a eventos, aplicada de forma simples e elegante.
As threads blink0, blink1 e uart_out: escalonamento, k_sleep() e modelo de execução
Agora vamos analisar as funções que viram threads. Aqui está o “comportamento” real do sistema, e onde o Zephyr mostra claramente que não existe superloop — existem fluxos concorrentes cooperando sob controle do kernel.
Assinatura das funções de thread
Todas as funções têm uma assinatura semelhante a:
void blink0(void *a, void *b, void *c)
Isso não é arbitrário.
O Zephyr define que toda thread criada via K_THREAD_DEFINE deve aceitar três argumentos genéricos (void *), mesmo que você não os use.
Por que três parâmetros?
- Permite passar contexto para a thread
- Mantém ABI estável
- Evita múltiplas variações de protótipos
No exemplo:
- os parâmetros não são usados,
- mas a assinatura precisa ser respeitada.
Thread blink0 e blink1: produtoras de eventos
As duas threads são quase idênticas, mudando apenas:
- o LED controlado,
- o período de pisca.
Estrutura típica:
static void blink0(void *a, void *b, void *c)
{
uint32_t cnt = 0;
while (1) {
gpio_pin_toggle_dt(&led0);
struct msg msg0;
msg0.led = 0;
msg0.cnt = cnt++;
k_fifo_put(&printk_fifo, &msg0);
k_sleep(K_MSEC(1000));
}
}
Vamos destrinchar isso em partes.
while (1) não é “superloop”
Apesar de parecer um superloop, não é.
Aqui:
- cada thread tem seu próprio loop,
- o escalonamento é feito pelo kernel,
- a thread “some” do processador quando dorme.
Isso é radicalmente diferente de:
while (1) {
delay_ms(1000);
}
em baremetal.
gpio_pin_toggle_dt(&led0)
Essa chamada:
- acessa o driver GPIO genérico,
- usa informações vindas do DeviceTree,
- não toca em registradores diretamente.
O mesmo código roda:
- em STM32,
- nRF,
- ESP32,
- QEMU (com GPIO simulado).
Criação da mensagem
struct msg msg0;
msg0.led = 0;
msg0.cnt = cnt++;
O exemplo real usa variáveis estáticas, justamente para garantir que:
- a memória continue válida após o
k_fifo_put().
Isso reforça a ideia de que:
no Zephyr, memória e tempo de vida do objeto importam.
k_fifo_put()
Quando a thread chama:
k_fifo_put(&printk_fifo, &msg0);
o kernel:
- não copia dados,
- apenas encadeia o objeto,
- acorda qualquer thread bloqueada em
k_fifo_get().
Isso é uma sincronização implícita.
k_sleep(K_MSEC(1000))
Aqui está um ponto-chave.
k_sleep():
- coloca a thread em estado sleeping,
- libera o processador imediatamente,
- permite que outras threads rodem.
K_MSEC(1000) é uma macro que:
- converte milissegundos para
k_timeout_t, - evita erros de unidade,
- torna o código legível e portátil.
📌 Importantek_sleep() não é busy-wait.
Ele coopera com o scheduler.
Thread uart_out: consumidora de eventos
Agora a thread que não controla hardware crítico, apenas processa mensagens.
Estrutura típica:
static void uart_out(void *a, void *b, void *c)
{
while (1) {
struct msg *msg;
msg = k_fifo_get(&printk_fifo, K_FOREVER);
printk("Toggled led%d; counter=%d\n",
msg->led, msg->cnt);
}
}
k_fifo_get(..., K_FOREVER)
Esse é um exemplo clássico de thread orientada a eventos:
- se não há mensagem → thread dorme
- se chega mensagem → thread acorda
Não existe polling, não existe delay.
printk() dentro de uma thread dedicada
Essa decisão arquitetural é excelente:
- evita
printk()em múltiplas threads concorrentes, - reduz risco de interleaving de mensagens,
- centraliza I/O lento em uma única thread.
Isso é exatamente o tipo de padrão que se espera em sistemas RTOS bem projetados.
Prioridades iguais: por quê?
Todas as threads usam:
#define PRIORITY 7
Isso significa:
- escalonamento round-robin entre elas,
- nenhuma é mais importante que a outra,
- comportamento previsível para um exemplo didático.
Em um sistema real:
- threads de controle teriam prioridade maior,
- logging teria prioridade menor.
K_THREAD_DEFINE: o que essa macro realmente cria e por que ela substitui o main()
Agora chegamos ao ponto mais conceitual e arquitetural do tutorial.
Entender K_THREAD_DEFINE é entender por que o Zephyr não precisa de main() neste exemplo.
Vamos partir da macro usada no código:
K_THREAD_DEFINE(blink0_id,
STACKSIZE,
blink0,
NULL, NULL, NULL,
PRIORITY,
0,
0);
À primeira vista, parece “apenas” uma forma compacta de criar threads.
Na prática, ela define objetos globais que o kernel conhece antes mesmo de iniciar o scheduler.
Anatomia completa da macro
A assinatura conceitual é:
K_THREAD_DEFINE(
name,
stack_size,
entry,
p1, p2, p3,
priority,
options,
delay
)
Vamos destrinchar item por item.
name — Identificador da thread
blink0_id
Esse nome:
- vira um símbolo global,
- identifica a thread no linker,
- pode ser usado para debug e tracing.
Internamente, o Zephyr cria:
- uma
struct k_thread, - uma stack associada,
- metadados de inicialização.
stack_size — Pilha dedicada
STACKSIZE
Diferente de muitos exemplos em outros RTOS, aqui:
- a pilha é obrigatória,
- é conhecida em tempo de compilação,
- não depende de heap.
Isso é essencial para:
- sistemas determinísticos,
- análise estática,
- aplicações safety-critical.
entry — Função da thread
blink0
Essa função:
- não é chamada manualmente,
- será invocada pelo kernel,
- começa a rodar quando o scheduler inicia.
É aqui que o modelo do Zephyr diverge completamente do “main() cria tudo”.
p1, p2, p3 — Parâmetros genéricos
NULL, NULL, NULL
Esses ponteiros:
- permitem passar contexto para a thread,
- evitam uso de globais,
- mantêm ABI uniforme.
No exemplo, não são usados, mas em sistemas reais:
- podem carregar ponteiros de estruturas,
- ou referências a drivers.
priority — Prioridade da thread
PRIORITY
Relembrando:
- números menores → prioridade maior
- prioridades negativas → threads do sistema
Aqui todas são iguais para fins didáticos.
options — Flags de comportamento
0
Aqui poderiam entrar opções como:
K_FP_REGS→ thread usa FPUK_USER→ thread em modo usuário (se MPU estiver ativa)
O exemplo mantém tudo simples.
delay — Início retardado
0
Indica:
- a thread inicia imediatamente no boot.
Em sistemas reais, você pode:
- atrasar threads,
- escalonar inicializações,
- reduzir pico de consumo no boot.
O que o Zephyr gera por trás da macro?
Conceitualmente, o compilador/linker acabam gerando algo equivalente a:
- uma stack alocada em
.bssou.noinit - uma
struct k_thread - uma entrada em uma seção especial (ex:
.z_thread_data)
Durante o boot:
- O kernel percorre essas seções.
- Inicializa todas as threads estáticas.
- Insere as threads prontas no scheduler.
- Inicia a execução sem jamais chamar
main().
📌 Isso é fundamental
O fluxo não é:
reset → main() → scheduler
É:
reset → kernel init → threads do sistema → threads do app
Mas o Zephyr pode ter main()?
Sim. E muitos exemplos têm.
Se existir:
void main(void)
{
...
}
O Zephyr:
- cria uma thread especial chamada
main, - executa essa função como uma thread normal,
- com prioridade e stack configuráveis via Kconfig.
Neste sample, os autores optaram por não usar main(), para mostrar que:
- o kernel é o centro da aplicação,
- não existe mais “função principal” no sentido clássico.
Comparação direta com outros modelos
Baremetal
int main(void)
{
init();
while (1) { ... }
}
FreeRTOS clássico
int main(void)
{
xTaskCreate(...);
vTaskStartScheduler();
while (1);
}
Zephyr (este exemplo)
K_THREAD_DEFINE(...);
K_THREAD_DEFINE(...);
K_THREAD_DEFINE(...);
Nenhum fluxo explícito.
O linker + kernel fazem o trabalho.
Ciclo de vida completo do sistema, quando usar ou não main() e boas práticas reais
Para fechar o tutorial, vamos juntar todas as peças e olhar o sistema do reset até a execução contínua, e depois discutir decisões arquiteturais reais: quando esse modelo é ideal e quando o main() ainda faz sentido.
Ciclo de vida do firmware neste exemplo
O fluxo real de execução do Zephyr, neste sample, é aproximadamente o seguinte:
- Reset do microcontrolador
- Código de startup (CMSIS / SoC específico)
- Inicialização mínima de stack e memória
- Inicialização do kernel Zephyr
- Scheduler
- Estruturas internas
- Heap (se habilitado)
- Infraestrutura de drivers
- Inicialização baseada em DeviceTree
- Criação dos dispositivos declarados
- Binding de drivers (GPIO, UART, etc.)
- Verificação de
status = "okay"
- Criação das threads estáticas
- Threads do sistema
- Threads definidas por
K_THREAD_DEFINE - Pilhas já alocadas e conhecidas
- Scheduler entra em execução
- Threads
blink0,blink1euart_outpassam a disputar CPU - Não existe chamada a
main() - Não existe “loop central”
- Threads
- Sistema em regime permanente
- Threads produtoras geram eventos
- FIFO coordena comunicação
- Thread consumidora bloqueia e acorda conforme necessário
Esse fluxo deixa claro que o kernel é o “programa principal”.
O seu código apenas descreve o que existe, não quando chamar.
Quando esse modelo é ideal
Esse estilo (sem main()) é excelente quando:
- o sistema é 100% orientado a eventos,
- todas as tarefas são naturalmente threads,
- o comportamento é reativo (sensores, mensagens, filas),
- você quer inicialização determinística e estática,
- o projeto cresce em complexidade e modularidade.
Exemplos típicos:
- gateways IoT,
- sistemas de logging e monitoramento,
- aplicações industriais com múltiplas tarefas independentes,
- firmware educacional focado em RTOS “puro”.
Quando main() ainda faz sentido no Zephyr
Apesar do poder desse modelo, há cenários em que usar main() é mais adequado:
- inicializações sequenciais complexas;
- criação dinâmica de threads;
- lógica de boot dependente de estado externo;
- migração gradual de firmware baremetal;
- aplicações simples, onde threads estáticas seriam “overkill”.
Nesse caso, o Zephyr trata main() assim:
void main(void)
{
// isso roda como uma thread normal
}
Ou seja:
main()não quebra o modelo do Zephyr,- ela apenas vira uma thread especial,
- com prioridade e stack configuráveis via Kconfig.
Boa prática arquitetural (opinião técnica)
Em projetos reais e bem estruturados, o padrão mais saudável costuma ser:
- usar threads estáticas (
K_THREAD_DEFINE) para:- tarefas permanentes,
- serviços do sistema,
- pipelines de dados;
- usar
main()apenas para:- orquestrar inicializações,
- criar threads dinâmicas,
- decidir configurações em runtime.
O sample threads existe justamente para mostrar que:
você não é obrigado a pensar em firmware como “um loop principal”.
Conclusão técnica do tutorial
Este exemplo simples demonstra conceitos profundos:
- Zephyr não é “baremetal com RTOS por cima”;
- o kernel é o centro da aplicação;
- DeviceTree elimina dependência de hardware no código;
- FIFOs trabalham com objetos, não cópias;
- threads são entidades declarativas;
main()é opcional, não fundamental.
Quem entende esse sample entende a filosofia do Zephyr.