Zig para Embarcados — HAL, Microcontroladores e IoT

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
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(&microzig.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

  1. Use ReleaseSmall para otimizar tamanho do binário
  2. Evite alocação dinâmica — prefira buffers fixos
  3. Documente registradores com comptime assertions
  4. Teste no hardware real regularmente
  5. 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.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.