Zig no ESP32 com FreeRTOS: Firmware, C Interop e Build Seguro
O ESP32 aparece em projetos de IoT, automação, telemetria, produtos conectados e protótipos industriais porque combina Wi-Fi, Bluetooth, baixo custo e um ecossistema enorme em C. Para quem acompanha Zig, a pergunta natural é: dá para usar Zig no ESP32 com FreeRTOS sem jogar fora o ESP-IDF?
A resposta pragmática é sim, mas não como uma troca ingênua de toda a stack. Em 2026, o caminho mais seguro é usar Zig como linguagem de módulos bem delimitados, bibliotecas de lógica, parsers, validação, processamento de dados e componentes que se beneficiam de controle explícito de memória. O ESP-IDF, os drivers C e o FreeRTOS continuam como base operacional.
Este guia mostra uma estratégia realista para equipes que já usam C/C++ em firmware e querem introduzir Zig sem transformar o projeto em experimento frágil. Se você ainda está no começo, leia antes o guia amplo de Zig para sistemas embarcados e IoT e o tutorial de sistemas embarcados com Zig.
Onde Zig Entra em um Projeto ESP32
O ESP32 normalmente roda sobre ESP-IDF, que por sua vez usa FreeRTOS. Essa base resolve inicialização, Wi-Fi, NVS, GPIO, timers, filas, tasks e drivers. Zig não precisa substituir tudo isso para gerar valor.
As melhores áreas para começar são:
- parsing de pacotes binários recebidos por UART, BLE, LoRa ou TCP;
- validação de mensagens JSON pequenas antes de publicar via MQTT;
- filtros de telemetria e agregação de amostras de sensores;
- codificação de payloads compactos;
- regras de negócio locais, como debounce, thresholds e estado de máquina;
- bibliotecas compartilhadas entre firmware e simuladores de teste.
Evite começar por bootloader, Wi-Fi, Bluetooth, criptografia de baixo nível ou drivers sensíveis. Esses pontos dependem de detalhes do ESP-IDF, versões do chip, toolchain e certificação. O ganho inicial vem de módulos onde a interface C é clara e o risco é reversível.
Arquitetura Recomendada
Um projeto híbrido pode ficar assim:
firmware-esp32/
├── main/
│ ├── app_main.c
│ ├── zig_bridge.h
│ └── CMakeLists.txt
├── components/
│ └── zig_logic/
│ ├── build.zig
│ ├── src/
│ │ └── lib.zig
│ └── include/
│ └── zig_logic.h
└── sdkconfig
O C continua expondo o app_main, criando tasks FreeRTOS e chamando funções Zig por uma API pequena. O Zig compila uma biblioteca estática com ABI C. Isso mantém o firmware fácil de depurar com ferramentas ESP-IDF e reduz a superfície de integração.
Expondo Funções Zig para C
Comece com funções simples, sem allocator global e sem tipos complexos cruzando a fronteira:
const std = @import("std");
pub export fn zig_normalize_sensor(raw: i32, scale: i32) i32 {
if (scale <= 0) return 0;
return @divTrunc(raw * 1000, scale);
}
pub export fn zig_is_alarm(temp_milli_c: i32, limit_milli_c: i32) bool {
return temp_milli_c >= limit_milli_c;
}
No C, a interface fica deliberadamente pequena:
#pragma once
#include <stdbool.h>
#include <stdint.h>
int32_t zig_normalize_sensor(int32_t raw, int32_t scale);
bool zig_is_alarm(int32_t temp_milli_c, int32_t limit_milli_c);
Essa abordagem evita problemas comuns: ownership ambíguo, strings sem tamanho explícito, structs com layout diferente e alocações feitas de um lado e liberadas do outro. Para aprofundar a fronteira entre Zig e C, veja interoperabilidade C em Zig e como portar biblioteca C para Zig.
Chamando Zig a Partir de uma Task FreeRTOS
Uma task FreeRTOS em C pode chamar Zig como qualquer biblioteca estática:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "zig_bridge.h"
static void sensor_task(void *arg) {
while (true) {
int32_t raw = read_sensor_adc();
int32_t temp = zig_normalize_sensor(raw, 4095);
if (zig_is_alarm(temp, 70000)) {
publish_alarm(temp);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
O ponto importante é que o Zig não precisa conhecer a task. Ele recebe valores, devolve valores e não bloqueia. Isso facilita testes locais, simulação em host e rollback para C se a integração falhar.
Memória: o Erro que Mais Quebra Firmware
Em firmware, alocação dinâmica é decisão de arquitetura, não detalhe de implementação. Zig ajuda porque alocadores são explícitos, mas isso só vale se a equipe não criar atalhos perigosos.
Prefira três regras:
- passe buffers do C para Zig com ponteiro e tamanho;
- escreva no buffer recebido, sem criar ownership novo;
- retorne status e quantidade escrita.
Exemplo:
pub export fn zig_format_payload(
temp_milli_c: i32,
out_ptr: [*]u8,
out_len: usize,
) usize {
const out = out_ptr[0..out_len];
const written = std.fmt.bufPrint(out, "temp_milli_c={d}", .{temp_milli_c}) catch return 0;
return written.len;
}
Esse padrão é previsível: se o buffer não cabe, a função falha retornando 0. Não há heap escondido, malloc implícito ou string que precisa ser liberada depois. Para projetos maiores, combine isso com o guia de estratégias de alocação em Zig e com o tutorial de gerenciamento de memória.
Estados de Máquina para Dispositivos IoT
Dispositivos ESP32 costumam alternar entre estados: inicializar, conectar Wi-Fi, sincronizar relógio, coletar amostras, publicar, dormir e recuperar falhas. Zig é uma boa linguagem para modelar isso sem classes, herança ou callbacks espalhados.
const State = enum {
boot,
wifi_connecting,
sampling,
publishing,
backoff,
};
pub export fn zig_next_state(current: State, wifi_ok: bool, publish_ok: bool) State {
return switch (current) {
.boot => .wifi_connecting,
.wifi_connecting => if (wifi_ok) .sampling else .backoff,
.sampling => .publishing,
.publishing => if (publish_ok) .sampling else .backoff,
.backoff => .wifi_connecting,
};
}
Para C, muitas equipes preferem expor o estado como u8 para evitar detalhes de ABI. A lógica continua testável em Zig, mas a fronteira pública fica conservadora. Se esse padrão é importante no seu projeto, complemente com state machine em Zig.
Build e CI: Não Dependa do Notebook de Uma Pessoa
O maior risco de um projeto Zig + ESP-IDF não é a sintaxe; é build reprodutível. Documente versões e rode o build em CI desde o primeiro dia.
Checklist mínimo:
- fixe a versão do Zig usada pelo módulo;
- fixe a versão do ESP-IDF;
- gere a biblioteca Zig em modo
ReleaseSafeouReleaseSmall, conforme prioridade; - rode testes Zig em host para a lógica pura;
- rode pelo menos um build ESP-IDF completo em CI;
- publique artefatos
.elf,.bine mapa de memória; - registre tamanho de flash e RAM a cada release.
Para builds multi-plataforma e releases, veja GitHub Actions para Zig e build.zig.zon na prática.
Testes Antes de Levar para a Bancada
O jeito mais barato de ganhar confiança é separar a lógica Zig que pode rodar no host da parte que depende do ESP32. Parser de pacote, cálculo de checksum, normalização de sensor e transição de estado podem ter testes Zig normais, executados no notebook e no CI, antes de qualquer flash no dispositivo.
Na bancada, comece com um firmware de diagnóstico que exponha poucas chamadas Zig e registre entradas, saídas e tempo gasto. Compare o mesmo vetor de teste no host e no ESP32. Se houver divergência, o problema costuma estar na fronteira C/Zig, no tamanho de inteiros, no alinhamento de structs ou em buffers sem tamanho explícito.
Para equipes que trabalham com produto conectado, vale manter uma matriz simples: versão do Zig, versão do ESP-IDF, chip usado, modo de otimização, tamanho do binário, consumo aproximado de stack da task e resultado dos testes de longa duração. Essa disciplina transforma Zig em uma adoção incremental mensurável, não em uma aposta de linguagem.
Quando Não Usar Zig no ESP32
Há cenários em que Zig ainda não é a melhor escolha principal:
- firmware regulado que exige toolchain homologada específica;
- projeto com equipe sem tempo para manter integração de build;
- driver novo onde o suporte C do fabricante muda toda semana;
- produto com prazo curto e zero margem para experimentação;
- código que precisa ser entendido por técnicos acostumados apenas ao ESP-IDF em C.
Nesses casos, use Zig primeiro em simuladores, ferramentas internas, geração de payloads, testes ou bibliotecas auxiliares. O artigo sobre ferramentas internas com Zig mostra outra forma de obter retorno sem colocar o firmware crítico em risco.
Caminho de Adoção em Três Passos
Um plano seguro para uma equipe embarcada brasileira:
- Escreva uma biblioteca Zig pequena para validar ou transformar dados de sensor.
- Exponha apenas uma API C com tipos primitivos, ponteiro e tamanho.
- Integre ao ESP-IDF como componente opcional e mantenha fallback C até passar em bancada.
Depois disso, expanda para estado de máquina, parsing de protocolo, compactação de payload e testes de propriedade. Não comece prometendo reescrever todo o firmware.
Relação com Carreira e Vagas Embedded
O mercado brasileiro de baixo nível ainda pede C, C++, Linux, RTOS, CAN, QEMU, Docker e CI/CD com muito mais frequência do que Zig. Isso não torna Zig irrelevante; torna Zig uma vantagem complementar. Quem entende C/FreeRTOS e consegue introduzir Zig de forma incremental demonstra domínio de ABI, memória, build e performance.
Se seu foco é carreira, combine este guia com Zig para carreira e vagas e com a página de vagas de programação de sistemas. O melhor argumento profissional não é “Zig é mais moderno”, e sim “consigo reduzir bugs em módulos críticos sem quebrar a stack C existente”.
Conclusão
Zig no ESP32 com FreeRTOS faz sentido quando a adoção é incremental, testável e respeita o ecossistema ESP-IDF. Use C para inicialização, drivers e integração com FreeRTOS. Use Zig para lógica pura, parsing, validação, estado de máquina e componentes onde controle explícito de memória melhora a confiabilidade.
Essa combinação entrega um caminho realista: aproveitar a maturidade do ESP-IDF sem abrir mão da clareza, segurança e previsibilidade que tornam Zig atraente para sistemas embarcados modernos.