Setup de Ambiente Embedded com Zig: Compilacao Cruzada e Bare Metal

Programar sistemas embarcados com Zig e uma experiencia surpreendentemente suave. Diferente de C, onde voce precisa configurar toolchains complexas (gcc-arm-none-eabi, makefiles, etc.), Zig inclui compilacao cruzada nativa para dezenas de targets. Um unico comando compila para ARM, RISC-V, AVR ou qualquer outra arquitetura suportada. Neste artigo, configuramos tudo do zero.

Para uma visao geral de embedded com Zig, veja Zig Embedded Systems.

Por Que Zig para Embedded?

AspectoC TradicionalZig
Toolchaingcc-arm + make + scriptszig build (tudo incluido)
Cross-compilationInstalar toolchain por targetzig build -Dtarget=thumb-...
SegurancaBuffer overflows faceisVerificacao de bounds
Alocacaomalloc/free (perigoso em embedded)Allocators explicitos
Interop CNativoNativo (sem custo)
Tamanho binarioPequenoEquivalente ou menor
DebugGDBGDB (compativel)

Targets Suportados

Zig suporta nativamente estas plataformas embarcadas:

# Listar todos os targets disponiveis
zig targets | grep -E "(thumb|riscv32|avr)"

Principais targets para embedded:

FamiliaTarget ZigExemplo
ARM Cortex-M0thumb-freestanding-noneRP2040 (Pico)
ARM Cortex-M3thumb-freestanding-noneSTM32F1
ARM Cortex-M4thumb-freestanding-noneSTM32F4
RISC-V 32riscv32-freestanding-noneESP32-C3
AVRavr-freestanding-noneATmega328P

Primeiro Programa Bare-Metal

Vamos criar um programa minimo que pisca um LED em um ARM Cortex-M (o classico “blink”).

Estrutura do Projeto

meu-projeto-embedded/
├── build.zig
├── build.zig.zon
├── linker.ld
└── src/
    └── main.zig

Linker Script Basico

O linker script define o layout de memoria do microcontrolador:

/* linker.ld - Exemplo para STM32F103 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS
{
    .isr_vector :
    {
        KEEP(*(.isr_vector))
    } > FLASH

    .text :
    {
        *(.text*)
        *(.rodata*)
    } > FLASH

    .data :
    {
        _sdata = .;
        *(.data*)
        _edata = .;
    } > RAM AT > FLASH

    .bss :
    {
        _sbss = .;
        *(.bss*)
        _ebss = .;
    } > RAM

    _stack_top = ORIGIN(RAM) + LENGTH(RAM);
}

build.zig para Embedded

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.resolveTargetQuery(.{
        .cpu_arch = .thumb,
        .os_tag = .freestanding,
        .abi = .none,
        .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m3 },
    });

    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "firmware",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // Usar linker script customizado
    exe.setLinkerScript(b.path("linker.ld"));

    // Desabilitar stack protector (nao disponivel em bare metal)
    exe.root_module.stack_protector = false;

    b.installArtifact(exe);

    // Gerar arquivo .bin para flash
    const bin = b.addObjCopy(exe.getEmittedBin(), .{
        .format = .bin,
    });
    const install_bin = b.addInstallBinFile(bin.getOutput(), "firmware.bin");

    const flash_step = b.step("bin", "Gerar firmware.bin");
    flash_step.dependOn(&install_bin.step);
}

Codigo Bare-Metal

// src/main.zig

// Definicoes de hardware para STM32F103
const RCC_BASE = 0x40021000;
const GPIOC_BASE = 0x40011000;

const RCC_APB2ENR: *volatile u32 = @ptrFromInt(RCC_BASE + 0x18);
const GPIOC_CRH: *volatile u32 = @ptrFromInt(GPIOC_BASE + 0x04);
const GPIOC_ODR: *volatile u32 = @ptrFromInt(GPIOC_BASE + 0x0C);

fn registrador(comptime endereco: usize) *volatile u32 {
    return @ptrFromInt(endereco);
}

// Tabela de vetores de interrupcao
export const vector_table linksection(".isr_vector") = blk: {
    const VectorTable = extern struct {
        stack_pointer: u32,
        reset: *const fn () callconv(.C) noreturn,
    };

    break :blk VectorTable{
        .stack_pointer = 0x20005000, // Topo da RAM
        .reset = &_start,
    };
};

fn _start() callconv(.C) noreturn {
    // Inicializar .bss com zeros
    // Copiar .data da Flash para RAM
    // (simplificado para este exemplo)

    main();

    while (true) {}
}

fn delay(contagem: u32) void {
    var i: u32 = 0;
    while (i < contagem) : (i += 1) {
        asm volatile ("nop");
    }
}

pub fn main() void {
    // Habilitar clock do GPIOC
    RCC_APB2ENR.* |= (1 << 4); // IOPCEN

    // Configurar PC13 como saida push-pull (LED onboard)
    GPIOC_CRH.* &= ~@as(u32, 0xF << 20); // Limpar bits
    GPIOC_CRH.* |= (0x2 << 20); // Output mode, 2MHz

    // Piscar LED infinitamente
    while (true) {
        GPIOC_ODR.* ^= (1 << 13); // Toggle PC13
        delay(500_000);
    }
}

Compilando e Flashando

# Compilar
zig build -Doptimize=ReleaseSmall

# Gerar binario
zig build bin

# Flash com OpenOCD (STM32)
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \
    -c "program zig-out/bin/firmware.bin 0x08000000 verify reset exit"

# Ou com probe-rs (alternativa moderna)
probe-rs download --chip STM32F103C8 zig-out/bin/firmware.bin

Usando microzig

O projeto microzig oferece abstracoees de alto nivel para desenvolvimento embedded:

const microzig = @import("microzig");

const led_pin = microzig.board.led;

pub fn main() !void {
    led_pin.setDir(.output);

    while (true) {
        led_pin.toggle();
        microzig.hal.time.sleep_ms(500);
    }
}

microzig abstrai as diferencas entre microcontroladores, oferecendo uma API unificada para GPIO, UART, SPI, I2C e timers.

Ferramentas de Debug

GDB com OpenOCD

# Terminal 1: Iniciar OpenOCD
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg

# Terminal 2: Conectar GDB
arm-none-eabi-gdb zig-out/bin/firmware.elf
(gdb) target remote :3333
(gdb) monitor reset halt
(gdb) break main
(gdb) continue

Tamanho do Binario

Zig produz binarios muito pequenos para embedded:

# Verificar tamanho
arm-none-eabi-size zig-out/bin/firmware.elf

# Saida tipica para blink:
#    text    data     bss     dec     hex filename
#     284       0       0     284     11c firmware.elf

Exercicios

  1. Multi-LED: Modifique o programa para piscar 3 LEDs em sequencia com tempos diferentes.

  2. Botao: Adicione leitura de um botao (GPIO input) que muda a velocidade do blink.

  3. Tamanho otimo: Compare o tamanho do binario entre ReleaseSmall, ReleaseSafe e Debug.


Proximo Artigo

No proximo artigo, exploramos GPIO e Perifericos em profundidade, incluindo UART, SPI e I2C.

Conteudo Relacionado


Duvidas sobre embedded com Zig? Participe da comunidade Zig Brasil!

Continue aprendendo Zig

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