Debug em Sistemas Embarcados com Zig
Zig é uma excelente escolha para sistemas embarcados, oferecendo controle de baixo nível com segurança. Este guia cobre debugging e problemas comuns em desenvolvimento embarcado.
Configurando o Target Embarcado
// build.zig para ARM Cortex-M (ex: STM32)
pub fn build(b: *std.Build) void {
const target = b.resolveTargetQuery(.{
.cpu_arch = .thumb,
.cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m4 },
.os_tag = .freestanding,
.abi = .eabi,
});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "firmware",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// Linker script customizado
exe.setLinkerScript(b.path("linker.ld"));
b.installArtifact(exe);
}
// build.zig para RISC-V (ex: ESP32-C3)
const target = b.resolveTargetQuery(.{
.cpu_arch = .riscv32,
.cpu_model = .{ .explicit = &std.Target.riscv.cpu.generic_rv32 },
.os_tag = .freestanding,
.abi = .none,
});
Problema: Binary Não Cabe na Flash
# Verificar tamanho do binário
zig build -Doptimize=ReleaseSmall
arm-none-eabi-size zig-out/bin/firmware
# Se o binário é grande demais:
# 1. Usar ReleaseSmall
# 2. Remover código não usado
# 3. Reduzir uso da std (usar implementações mínimas)
// Reduzir o uso da std para embarcados
// Em vez de std.debug.print, usar UART diretamente
fn uart_write(data: []const u8) void {
for (data) |byte| {
// Escrever diretamente no registrador UART
const UART_DR: *volatile u32 = @ptrFromInt(0x4000_0000);
UART_DR.* = byte;
}
}
Debug com OpenOCD e GDB
# 1. Instalar OpenOCD
sudo apt install openocd
# 2. Iniciar OpenOCD para seu hardware
# STM32F4:
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
# 3. Em outro terminal, conectar GDB
arm-none-eabi-gdb zig-out/bin/firmware
# Dentro do GDB:
(gdb) target remote :3333
(gdb) monitor reset halt
(gdb) load # Flash o firmware
(gdb) break main # Breakpoint em main
(gdb) continue
(gdb) print *GPIOA # Inspecionar registradores
(gdb) x/10x 0x40020000 # Examinar memória mapeada
Problema: Startup Code
Em freestanding, você precisa configurar o startup:
// src/start.zig — Entry point para Cortex-M
const std = @import("std");
// Vetor de interrupções
export const vector_table: [16]*const fn () callconv(.C) void linksection(".isr_vector") = .{
@ptrFromInt(0x2000_0000 + 0x1_0000), // Stack pointer inicial
resetHandler, // Reset handler
nmiHandler,
hardFaultHandler,
// ...outros handlers...
};
fn resetHandler() callconv(.C) void {
// Copiar .data da flash para RAM
// Zerar .bss
// Chamar main
@import("main.zig").main();
}
fn nmiHandler() callconv(.C) void {
while (true) {}
}
fn hardFaultHandler() callconv(.C) void {
while (true) {} // Travar em fault
}
Problema: Acessar Registradores de Hardware
// Acessar registradores mapeados em memória
const GPIO = struct {
const GPIOA_BASE: usize = 0x4002_0000;
const MODER = @as(*volatile u32, @ptrFromInt(GPIOA_BASE + 0x00));
const ODR = @as(*volatile u32, @ptrFromInt(GPIOA_BASE + 0x14));
const IDR = @as(*volatile u32, @ptrFromInt(GPIOA_BASE + 0x10));
};
fn led_toggle() void {
GPIO.ODR.* ^= (1 << 5); // Toggle PA5 (LED em muitos boards)
}
fn ler_botao() bool {
return (GPIO.IDR.* & (1 << 13)) != 0; // PC13 em muitos boards
}
Problema: Sem Allocator Disponível
Em embarcados, geralmente não há heap:
// Usar FixedBufferAllocator com buffer estático
var buffer: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
// Ou trabalhar apenas com stack e arrays estáticos
var dados: [256]u8 = undefined;
var tamanho: usize = 0;
Problema: Debugging sem Serial/UART
Quando não tem saída serial disponível:
// Semihosting — debug via probe SWD/JTAG
// Usa instruções especiais que o debugger intercepta
// Ou use LED para debug (blink codes)
fn debug_blink(codigo: u32) void {
var i: u32 = 0;
while (i < codigo) : (i += 1) {
led_on();
delay_ms(200);
led_off();
delay_ms(200);
}
delay_ms(1000); // Pausa entre sequências
}
Problema: Timing e Delays
// Delay simples (impreciso mas funcional para debug)
fn delay_ms(ms: u32) void {
// Para Cortex-M a ~72MHz, cada iteração ~= 4 ciclos
const ciclos_por_ms: u32 = 72_000_000 / 1000 / 4;
var count: u32 = ms * ciclos_por_ms;
while (count > 0) : (count -= 1) {
asm volatile ("nop");
}
}
// Para timing preciso, use timers de hardware
const TIM2 = @as(*volatile u32, @ptrFromInt(0x4000_0000));
Problema: Flashing (Gravar na Flash)
# Usando OpenOCD
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
-c "program zig-out/bin/firmware verify reset exit"
# Usando probe-rs (Rust tool, funciona com binários Zig)
probe-rs download --chip STM32F411CE zig-out/bin/firmware
# Usando J-Link
JLinkExe -device STM32F411CE -if SWD -speed 4000 -autoconnect 1
> loadbin zig-out/bin/firmware.bin, 0x08000000
> r
> g
Problema: Linker Script
/* linker.ld para STM32F411 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : { *(.text*) } > FLASH
.rodata : { *(.rodata*) } > FLASH
.data : { *(.data*) } > RAM AT > FLASH
.bss : { *(.bss*) } > RAM
}
Checklist para Embarcados
- Target configurado corretamente (CPU, arch, freestanding)
- Linker script corresponde ao hardware
- Startup code inicializa RAM e BSS
- OpenOCD ou probe conectados e funcionando
- Tamanho do binário cabe na flash
- Sem uso de heap (ou FixedBufferAllocator)
- Registradores de hardware acessados com
volatile
Veja Também
- FAQ Performance — Otimização para embarcados
- Cross-Compile Falha — Compilação cruzada
- FAQ Build System — Configuração de targets
- Debugar Segfaults — Crashes em runtime
- FAQ Memória — Gerenciamento sem heap