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?
| Aspecto | C Tradicional | Zig |
|---|---|---|
| Toolchain | gcc-arm + make + scripts | zig build (tudo incluido) |
| Cross-compilation | Instalar toolchain por target | zig build -Dtarget=thumb-... |
| Seguranca | Buffer overflows faceis | Verificacao de bounds |
| Alocacao | malloc/free (perigoso em embedded) | Allocators explicitos |
| Interop C | Nativo | Nativo (sem custo) |
| Tamanho binario | Pequeno | Equivalente ou menor |
| Debug | GDB | GDB (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:
| Familia | Target Zig | Exemplo |
|---|---|---|
| ARM Cortex-M0 | thumb-freestanding-none | RP2040 (Pico) |
| ARM Cortex-M3 | thumb-freestanding-none | STM32F1 |
| ARM Cortex-M4 | thumb-freestanding-none | STM32F4 |
| RISC-V 32 | riscv32-freestanding-none | ESP32-C3 |
| AVR | avr-freestanding-none | ATmega328P |
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
Multi-LED: Modifique o programa para piscar 3 LEDs em sequencia com tempos diferentes.
Botao: Adicione leitura de um botao (GPIO input) que muda a velocidade do blink.
Tamanho otimo: Compare o tamanho do binario entre
ReleaseSmall,ReleaseSafeeDebug.
Proximo Artigo
No proximo artigo, exploramos GPIO e Perifericos em profundidade, incluindo UART, SPI e I2C.
Conteudo Relacionado
- Zig Cross Compilation — Compilacao cruzada
- Zig Build System — Configuracao de build
- Zig e Interoperabilidade com C — Usando drivers C
Duvidas sobre embedded com Zig? Participe da comunidade Zig Brasil!