MCU & FPGA RTOS Introdução ao Zephyr RTOS: Entendendo Threads, DeviceTree e a Ausência do main()

Introdução ao Zephyr RTOS: Entendendo Threads, DeviceTree e a Ausência do main()

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”:

  1. O Zephyr sobe (boot + init do kernel).
  2. O kernel inicializa infraestrutura (scheduler, heap, drivers, etc.).
  3. As threads declaradas via K_THREAD_DEFINE(...) já entram no sistema como “threads do aplicativo”.
  4. 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 via printk().

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:

  1. Determinismo
    Em sistemas embarcados críticos, saber exatamente quanto de stack cada thread usa é parte do projeto.
  2. 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,
  • #ifdef por 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 mensagem
  • cnt → 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:

  1. Retorno por ponteiro
    O kernel devolve o endereço do objeto original.
  2. Bloqueio controlado
    K_FOREVER indica que a thread ficará bloqueada até chegar uma mensagem.
  3. 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.

📌 Importante
k_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 FPU
  • K_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 .bss ou .noinit
  • uma struct k_thread
  • uma entrada em uma seção especial (ex: .z_thread_data)

Durante o boot:

  1. O kernel percorre essas seções.
  2. Inicializa todas as threads estáticas.
  3. Insere as threads prontas no scheduler.
  4. Inicia a execução sem jamais chamar main().

📌 Isso é fundamental
O fluxo não é:

resetmain() → scheduler

É:

resetkernel initthreads do sistemathreads 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:

  1. Reset do microcontrolador
    • Código de startup (CMSIS / SoC específico)
    • Inicialização mínima de stack e memória
  2. Inicialização do kernel Zephyr
    • Scheduler
    • Estruturas internas
    • Heap (se habilitado)
    • Infraestrutura de drivers
  3. Inicialização baseada em DeviceTree
    • Criação dos dispositivos declarados
    • Binding de drivers (GPIO, UART, etc.)
    • Verificação de status = "okay"
  4. Criação das threads estáticas
    • Threads do sistema
    • Threads definidas por K_THREAD_DEFINE
    • Pilhas já alocadas e conhecidas
  5. Scheduler entra em execução
    • Threads blink0, blink1 e uart_out passam a disputar CPU
    • Não existe chamada a main()
    • Não existe “loop central”
  6. 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.

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