Debug em Sistemas Embarcados com Zig

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

  1. Target configurado corretamente (CPU, arch, freestanding)
  2. Linker script corresponde ao hardware
  3. Startup code inicializa RAM e BSS
  4. OpenOCD ou probe conectados e funcionando
  5. Tamanho do binário cabe na flash
  6. Sem uso de heap (ou FixedBufferAllocator)
  7. Registradores de hardware acessados com volatile

Veja Também

Continue aprendendo Zig

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