Zig em Sistemas Embarcados Industriais — Case de Sucesso

Zig em Sistemas Embarcados Industriais — Case de Sucesso

A indústria de sistemas embarcados movimenta mais de US$ 130 bilhões por ano e está em transformação. Com a crescente complexidade de dispositivos IoT industriais, PLCs programáveis, sensores inteligentes e controladores de automação, a necessidade de software seguro, eficiente e portável nunca foi tão grande. Zig está emergindo como uma alternativa moderna ao C para este domínio, oferecendo segurança de memória sem sacrificar o controle de baixo nível que sistemas embarcados exigem.

O Cenário Industrial

Desafios do Embedded Tradicional

Sistemas embarcados industriais operam sob restrições severas:

  • Memória limitada: Dispositivos com 16KB a 512KB de RAM
  • Processadores constrained: ARM Cortex-M0 a M7, RISC-V
  • Requisitos de tempo real: Respostas em microsegundos
  • Safety-critical: Falhas podem causar danos físicos ou financeiros
  • Ciclo de vida longo: Dispositivos em campo por 10-20 anos
  • Certificações: IEC 61508, ISO 26262, DO-178C

Problemas com C no Embedded

O C domina o embedded, mas seus problemas são amplificados neste ambiente:

  • Buffer overflows em protocolos de comunicação
  • Memory leaks em dispositivos que rodam 24/7
  • Undefined behavior difícil de detectar em hardware limitado
  • Toolchains complexos com Makefiles frágeis

A Solução com Zig

Controlador Industrial de Temperatura

Um fabricante brasileiro de automação industrial adotou Zig para seu novo controlador de temperatura PID, substituindo uma codebase C de 15 anos:

const std = @import("std");
const hal = @import("hal_stm32f4.zig");

/// Controlador PID com anti-windup para processos industriais
const ControladorPID = struct {
    kp: f32, // Ganho proporcional
    ki: f32, // Ganho integral
    kd: f32, // Ganho derivativo
    setpoint: f32,
    integral: f32 = 0,
    erro_anterior: f32 = 0,
    saida_min: f32,
    saida_max: f32,
    dt: f32, // Período de amostragem em segundos

    pub fn calcular(self: *ControladorPID, medicao: f32) f32 {
        const erro = self.setpoint - medicao;

        // Termo proporcional
        const p = self.kp * erro;

        // Termo integral com anti-windup
        self.integral += erro * self.dt;
        const i = self.ki * self.integral;

        // Termo derivativo (filtrado)
        const derivada = (erro - self.erro_anterior) / self.dt;
        const d = self.kd * derivada;

        self.erro_anterior = erro;

        // Saída com saturação
        var saida = p + i + d;
        saida = @max(self.saida_min, @min(self.saida_max, saida));

        // Anti-windup: limitar integral se saída saturou
        if (saida == self.saida_max or saida == self.saida_min) {
            self.integral -= erro * self.dt;
        }

        return saida;
    }

    pub fn reset(self: *ControladorPID) void {
        self.integral = 0;
        self.erro_anterior = 0;
    }
};

Sistema de Aquisição de Dados com Sensores

O sistema lê múltiplos sensores industriais (4-20mA, termopares, RTDs) e aplica filtragem digital:

const SensorType = enum {
    corrente_4_20ma,
    termopar_tipo_k,
    rtd_pt100,
    pressao_0_10v,
};

const LeituraSensor = struct {
    valor_raw: u16, // ADC 12-bit
    valor_engenharia: f32, // Unidade de engenharia
    timestamp: u64, // Tick do sistema
    status: StatusSensor,

    const StatusSensor = enum {
        ok,
        fora_de_faixa,
        sensor_aberto,
        sensor_curto,
    };
};

/// Filtro de média móvel para sinais ruidosos
fn FiltroMediaMovel(comptime N: usize) type {
    return struct {
        buffer: [N]f32 = [_]f32{0} ** N,
        indice: usize = 0,
        cheio: bool = false,

        const Self = @This();

        pub fn adicionar(self: *Self, valor: f32) f32 {
            self.buffer[self.indice] = valor;
            self.indice = (self.indice + 1) % N;
            if (self.indice == 0) self.cheio = true;

            const amostras = if (self.cheio) N else self.indice;
            var soma: f32 = 0;
            for (self.buffer[0..amostras]) |v| {
                soma += v;
            }
            return soma / @as(f32, @floatFromInt(amostras));
        }

        pub fn reset(self: *Self) void {
            self.buffer = [_]f32{0} ** N;
            self.indice = 0;
            self.cheio = false;
        }
    };
}

/// Conversão de leitura ADC para unidade de engenharia
fn converterLeitura(tipo: SensorType, raw: u16) LeituraSensor {
    const tensao = @as(f32, @floatFromInt(raw)) / 4095.0 * 3.3;
    const timestamp = hal.getTickCount();

    return switch (tipo) {
        .corrente_4_20ma => blk: {
            // 4mA = 0%, 20mA = 100%
            const corrente = tensao / 165.0 * 1000.0; // Resistor de 165 ohms
            const percentual = (corrente - 4.0) / 16.0 * 100.0;
            const status: LeituraSensor.StatusSensor =
                if (corrente < 3.5) .sensor_aberto
                else if (corrente > 21.0) .sensor_curto
                else if (percentual < 0 or percentual > 100) .fora_de_faixa
                else .ok;
            break :blk .{
                .valor_raw = raw,
                .valor_engenharia = percentual,
                .timestamp = timestamp,
                .status = status,
            };
        },
        .termopar_tipo_k => blk: {
            // Linearização simplificada do termopar tipo K
            const temp = tensao * 250.0; // Com amplificador AD8495
            break :blk .{
                .valor_raw = raw,
                .valor_engenharia = temp,
                .timestamp = timestamp,
                .status = if (temp < -20 or temp > 1300) .fora_de_faixa else .ok,
            };
        },
        .rtd_pt100 => blk: {
            const resistencia = tensao / 0.001; // Corrente de excitação de 1mA
            const temp = (resistencia - 100.0) / 0.385; // Equação linear PT100
            break :blk .{
                .valor_raw = raw,
                .valor_engenharia = temp,
                .timestamp = timestamp,
                .status = if (temp < -200 or temp > 850) .fora_de_faixa else .ok,
            };
        },
        .pressao_0_10v => blk: {
            const pressao = tensao / 10.0 * 100.0; // 0-10V = 0-100 bar
            break :blk .{
                .valor_raw = raw,
                .valor_engenharia = pressao,
                .timestamp = timestamp,
                .status = if (pressao < -5 or pressao > 110) .fora_de_faixa else .ok,
            };
        },
    };
}

Protocolo Modbus RTU

Comunicação industrial padrão implementada em Zig com verificação de CRC:

const ModbusFrame = struct {
    endereco: u8,
    funcao: FuncaoModbus,
    dados: []const u8,
    crc: u16,

    const FuncaoModbus = enum(u8) {
        ler_coils = 0x01,
        ler_holding_registers = 0x03,
        escrever_single_register = 0x06,
        escrever_multiple_registers = 0x10,
    };

    /// Calcula CRC-16 Modbus (polinômio 0xA001)
    pub fn calcularCRC(dados: []const u8) u16 {
        var crc: u16 = 0xFFFF;
        for (dados) |byte| {
            crc ^= @as(u16, byte);
            for (0..8) |_| {
                if (crc & 1 != 0) {
                    crc = (crc >> 1) ^ 0xA001;
                } else {
                    crc >>= 1;
                }
            }
        }
        return crc;
    }

    pub fn validar(self: ModbusFrame) bool {
        // Montar frame sem CRC para validação
        var buf: [256]u8 = undefined;
        buf[0] = self.endereco;
        buf[1] = @intFromEnum(self.funcao);
        @memcpy(buf[2..][0..self.dados.len], self.dados);
        const tamanho = 2 + self.dados.len;

        return calcularCRC(buf[0..tamanho]) == self.crc;
    }
};

Cross-Compilation: A Vantagem Zig

Um dos maiores diferenciais de Zig para embedded é a compilação cruzada trivial:

// build.zig para múltiplos targets industriais
const std = @import("std");

pub fn build(b: *std.Build) void {
    // STM32F4 — Controlador principal
    const stm32f4_target = b.resolveTargetQuery(.{
        .cpu_arch = .thumb,
        .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m4 },
        .os_tag = .freestanding,
        .abi = .eabihf,
    });

    // STM32F0 — Sensor node de baixo custo
    const stm32f0_target = b.resolveTargetQuery(.{
        .cpu_arch = .thumb,
        .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m0 },
        .os_tag = .freestanding,
        .abi = .eabi,
    });

    // RISC-V — Novo design com ESP32-C3
    const riscv_target = b.resolveTargetQuery(.{
        .cpu_arch = .riscv32,
        .os_tag = .freestanding,
        .abi = .none,
    });

    // Compilar firmware para todos os targets
    inline for (.{
        .{ stm32f4_target, "firmware_controlador" },
        .{ stm32f0_target, "firmware_sensor" },
        .{ riscv_target, "firmware_gateway" },
    }) |config| {
        const exe = b.addExecutable(.{
            .name = config[1],
            .root_source_file = b.path("src/main.zig"),
            .target = config[0],
            .optimize = .ReleaseSmall,
        });
        b.installArtifact(exe);
    }
}

Resultados Mensuráveis

Qualidade de Código

MétricaC (anterior)Zig (novo)
Bugs de memória em produção12/ano0
Linhas de código45.00031.000
Tempo de compilação (cross)8 min45 seg
Tamanho do binário (STM32F4)78 KB72 KB
Cobertura de testes23%89%

Produtividade

  • Onboarding: Novos desenvolvedores produtivos em 2 semanas (vs 2 meses com C)
  • Cross-compilation: Um comando, sem toolchains externos
  • Debug: Mensagens de erro claras, stack traces em modo debug
  • Testes: Executáveis nativamente na máquina de desenvolvimento

Segurança Funcional (IEC 61508)

Para aplicações safety-critical, Zig oferece propriedades que facilitam a certificação:

/// Watchdog de segurança — deve ser alimentado periodicamente
const WatchdogSeguranca = struct {
    intervalo_max_ms: u32,
    ultimo_feed: u64,

    pub fn alimentar(self: *WatchdogSeguranca) void {
        self.ultimo_feed = hal.getTickCount();
        hal.resetWatchdogHardware();
    }

    pub fn verificarTimeout(self: *WatchdogSeguranca) bool {
        const agora = hal.getTickCount();
        const elapsed = agora - self.ultimo_feed;
        return elapsed > self.intervalo_max_ms;
    }
};

/// Estado seguro — função chamada quando uma falha é detectada
fn entrarEstadoSeguro(razao: []const u8) noreturn {
    // Desligar todas as saídas
    hal.desligarTodasSaidas();

    // Registrar evento em memória não-volátil
    hal.gravarLogFalha(razao);

    // Ativar sinalização de falha
    hal.ativarLedFalha();

    // Loop infinito — apenas reset manual recupera
    while (true) {
        hal.alimentarWatchdog();
    }
}

Conclusão

Zig está se provando uma escolha excelente para sistemas embarcados industriais. A combinação de segurança de memória em tempo de compilação, cross-compilation trivial, binários compactos e ausência de runtime torna a linguagem ideal para dispositivos com recursos limitados que precisam operar com confiabilidade por décadas.

Para a indústria brasileira de automação, que busca modernizar suas plataformas de software embarcado sem sacrificar performance ou controle, Zig oferece um caminho pragmático de evolução a partir do C.

Conteúdo Relacionado

Continue aprendendo Zig

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