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étrica | C (anterior) | Zig (novo) |
|---|---|---|
| Bugs de memória em produção | 12/ano | 0 |
| Linhas de código | 45.000 | 31.000 |
| Tempo de compilação (cross) | 8 min | 45 seg |
| Tamanho do binário (STM32F4) | 78 KB | 72 KB |
| Cobertura de testes | 23% | 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
- Zig em Módulos do Kernel Linux — Zig no kernel
- Sistemas Embarcados com Zig — Tutorial de embedded
- Série Embedded — Série completa de embedded
- Sistemas de Tempo Real com Zig — Real-time
- Cross-Compilation em Zig — Compilação cruzada