Zig para Embarcados — HAL, Microcontroladores e IoT
O Zig está se posicionando como uma alternativa moderna e segura ao C no desenvolvimento de sistemas embarcados. Com compilação cruzada nativa, zero overhead de runtime, controle total de memória e a capacidade de gerar binários para dezenas de arquiteturas, o Zig oferece tudo o que desenvolvedores embarcados precisam, com a vantagem de segurança e ergonomia superiores ao C.
Por Que Zig para Embarcados
Desenvolvedores de sistemas embarcados tradicionalmente usam C (e ocasionalmente C++) por necessidade: são as únicas linguagens que oferecem o controle e a eficiência necessários para hardware com recursos limitados. O Zig desafia esse monopólio oferecendo:
- Zero runtime: Nenhum código de inicialização oculto
- Sem alocação implícita: Todas as alocações são explícitas e rastreáveis
- Compilação cruzada nativa: Compile para ARM, RISC-V, MIPS e outras arquiteturas sem toolchains externas
- Interoperabilidade C total: Use qualquer HAL ou SDK existente em C
- Segurança de tipos: Detecte erros em tempo de compilação que em C seriam undefined behavior
- Tamanho binário mínimo: Binários frequentemente menores que equivalentes em C
MicroZig — O Framework Embarcado Principal
O MicroZig é o framework embarcado mais importante do ecossistema Zig. Ele fornece uma camada de abstração de hardware (HAL) unificada para diversos microcontroladores:
Microcontroladores Suportados
- STM32 (ARM Cortex-M): STM32F1, STM32F4, STM32H7 e mais
- nRF (Nordic Semiconductor): nRF52, nRF53
- RP2040 (Raspberry Pi Pico)
- ESP32 (Espressif) — suporte experimental
- AVR (Arduino/ATmega)
- RISC-V: GD32VF103, CH32V
Exemplo: Blink LED no RP2040
const microzig = @import("microzig");
const rp2040 = microzig.hal;
const led = rp2040.gpio.num(25); // LED onboard do Pico
pub fn main() !void {
led.setFunction(.sio);
led.setDirection(.out);
while (true) {
led.put(1);
rp2040.time.sleepMs(500);
led.put(0);
rp2040.time.sleepMs(500);
}
}
Configuração do build.zig
const std = @import("std");
const microzig = @import("microzig");
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const firmware = microzig.addFirmware(b, .{
.name = "meu-firmware",
.root_source_file = b.path("src/main.zig"),
.target = microzig.targets.rp2040,
.optimize = optimize,
});
microzig.installFirmware(b, firmware, .{});
// Step para flash
const flash_step = b.step("flash", "Flash firmware no dispositivo");
flash_step.dependOn(µzig.addFlashStep(b, firmware).step);
}
GPIO — Entrada e Saída Digital
const gpio = microzig.hal.gpio;
// Configurar pinos
const botao = gpio.num(13);
const led = gpio.num(25);
const buzzer = gpio.num(18);
pub fn init() void {
// Saídas
led.setDirection(.out);
buzzer.setDirection(.out);
// Entrada com pull-up
botao.setDirection(.in);
botao.setPullUp(true);
}
pub fn loop() void {
// Ler botão (ativo baixo com pull-up)
if (botao.read() == 0) {
led.put(1);
buzzer.put(1);
} else {
led.put(0);
buzzer.put(0);
}
}
Comunicação Serial (UART)
const uart = microzig.hal.uart;
const serial = uart.num(0);
pub fn init() void {
serial.apply(.{
.baud_rate = 115200,
.tx_pin = uart.Pin.gpio0,
.rx_pin = uart.Pin.gpio1,
.data_bits = .eight,
.stop_bits = .one,
.parity = .none,
});
}
pub fn enviarDados(dados: []const u8) void {
for (dados) |byte| {
serial.writeByte(byte);
}
}
pub fn lerDados(buffer: []u8) usize {
var i: usize = 0;
while (i < buffer.len) {
if (serial.readByte()) |byte| {
buffer[i] = byte;
i += 1;
if (byte == '\n') break;
}
}
return i;
}
SPI e I2C
SPI
const spi = microzig.hal.spi;
const display_spi = spi.num(0);
pub fn init() void {
display_spi.apply(.{
.clock_pin = spi.Pin.gpio2,
.mosi_pin = spi.Pin.gpio3,
.miso_pin = spi.Pin.gpio4,
.cs_pin = spi.Pin.gpio5,
.baud_rate = 1_000_000,
.mode = .mode_0,
});
}
pub fn enviarComando(cmd: u8) void {
display_spi.writeBlocking(&[_]u8{cmd});
}
I2C
const i2c = microzig.hal.i2c;
const sensor_i2c = i2c.num(0);
const SENSOR_ADDR: u7 = 0x48; // Endereço do sensor de temperatura
pub fn lerTemperatura() !f32 {
var buf: [2]u8 = undefined;
try sensor_i2c.readFromDevice(SENSOR_ADDR, &buf);
const raw = (@as(u16, buf[0]) << 8) | buf[1];
return @as(f32, @floatFromInt(raw)) * 0.0625;
}
Interrupções
const irq = microzig.hal.irq;
// Handler de interrupção para timer
fn timerHandler() callconv(.C) void {
// Executar tarefa periódica
toggleLed();
clearTimerInterrupt();
}
pub fn configurarTimer() void {
const timer = microzig.hal.timer.num(0);
timer.configure(.{
.period_ms = 1000,
.callback = timerHandler,
});
timer.enable();
}
// Handler de interrupção para GPIO
fn botaoPressionado() callconv(.C) void {
// Debounce
const agora = microzig.hal.time.getTimestamp();
if (agora - ultimo_clique > 200_000) { // 200ms debounce
processar_evento();
ultimo_clique = agora;
}
}
DMA (Direct Memory Access)
const dma = microzig.hal.dma;
pub fn transferirDadosComDma(origem: []const u8, destino: []u8) void {
const canal = dma.channel(0);
canal.configure(.{
.source = @ptrToInt(origem.ptr),
.destination = @ptrToInt(destino.ptr),
.transfer_count = origem.len,
.data_size = .byte,
.increment_source = true,
.increment_destination = true,
});
canal.trigger();
canal.waitForCompletion();
}
Bare Metal sem MicroZig
Para targets não suportados pelo MicroZig ou para controle total:
// Linker script customizado via build.zig
const exe = b.addExecutable(.{
.name = "firmware",
.root_source_file = b.path("src/main.zig"),
.target = b.resolveTargetQuery(.{
.cpu_arch = .thumb,
.cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m4 },
.os_tag = .freestanding,
.abi = .eabi,
}),
.optimize = .ReleaseSmall,
});
exe.setLinkerScript(b.path("linker.ld"));
// Código bare metal
export fn _start() callconv(.C) noreturn {
// Inicializar stack pointer
// Inicializar .bss e .data
// Chamar main
@call(.auto, main, .{});
while (true) {
asm volatile ("wfi");
}
}
fn main() void {
// Acessar registradores diretamente
const GPIOA_BASE = 0x40020000;
const GPIOA_MODER = @as(*volatile u32, @ptrFromInt(GPIOA_BASE + 0x00));
const GPIOA_ODR = @as(*volatile u32, @ptrFromInt(GPIOA_BASE + 0x14));
// Configurar PA5 como saída
GPIOA_MODER.* = (GPIOA_MODER.* & ~@as(u32, 0x3 << 10)) | (0x1 << 10);
// Toggle LED
while (true) {
GPIOA_ODR.* ^= (1 << 5);
busyWait(1_000_000);
}
}
Casos de Uso em Produção
Zig para embarcados está sendo adotado em cenários reais. Confira nosso case de Zig em sistemas embarcados industriais e veja como empresas de telecomunicações utilizam Zig em dispositivos de rede.
Boas Práticas
- Use
ReleaseSmallpara otimizar tamanho do binário - Evite alocação dinâmica — prefira buffers fixos
- Documente registradores com comptime assertions
- Teste no hardware real regularmente
- Use a stdlib com cuidado — nem todas as funções estão disponíveis em freestanding
Próximos Passos
Explore as ferramentas de debug para depuração em hardware, as ferramentas de profiling para otimização, e as bibliotecas de rede para conectividade IoT. Confira nossos tutoriais para projetos práticos e a seção de carreira para oportunidades em embarcados com Zig.