Zig for Embedded: First Impressions From a C Engineer
Zig's comptime is compelling for embedded work. The toolchain story in 2024 is still rough at the edges. An honest first-impressions note from someone who knows C well.

Zig 0.11 (upgraded to 0.12 during this eval) is compelling for embedded — @compileLog, comptime pin config, no hidden allocations in the language model. The toolchain story in 2024 is still rough: cross-compilation works until it doesn't, HAL ecosystem is DIY compared to STM32Cube, and I'm a C engineer who'd rather ship than evangelize.
First impressions from porting a Modbus RTU sensor node from bare-metal C (STM32F103, GCC arm-none-eabi 12.2) to Zig on the same hardware.
Why I tried Zig on this project
- Comptime configuration: sensor addresses, baud rates, pin maps from single
config.zig— no#ifdefmaze - Explicit allocators: none in firmware path; Zig makes "no heap" a discipline not a convention
- C interop: call STM32 HAL and existing CMSIS without rewrite —
@cImportworked forstm32f1xx.hwith friction
Compared mentally to Rust ownership for C++ refugees — Zig felt closer to C mental model with better metaprogramming.
What impressed
Comptime pin mux
const UART_TX = gpio.Pin.init(.A, 9, .alt_push_pull);
// compile fails if pin invalid for peripheral — caught PB9 typo at build not in lab
Same in C needed macro + comment discipline. Small win, daily win.
Union enums for protocol states
Modbus parser state machine — tagged union replaced void* context casts. Generated code size within 5% of hand-written C (-Os both).
@embedFile for register map
CSV → comptime parse → const REGISTERS = ... — changed sensor vendor, rebuilt, flashed. C version used code generation step in Makefile; Zig inlined the step.
Cross compile from macOS
zig build -Dtarget=thumb-freestanding-eabi -Dcpu=cortex_m3
When it worked, one command beat CMake + tool path docs for contractors.
What hurt (toolchain rough edges)
HAL / startup files
No official STM32 pack. Used zig-stm32 community project (pinned commit) + vendor startup startup_stm32f103xb.s assembled via Zig. Worked after:
- Fixing
.vector_tablelinker section name mismatch (2 hours) - Manual
.ldscript edit from CubeIDE template
C path: CubeMX generates this in 10 minutes. Time tax real for first board.
@cImport and volatile registers
CMSIS volatile pointer semantics — had to wrap some registers in Zig structs with volatile qual on fields. @cImport pulled types; access patterns needed hand polish. C UB catalog reminders applied — don't cast away volatile.
Debug / flash integration
OpenOCD + ST-Link: same as C. Zig stack traces on hard fault — minimal without custom panic handler. Wrote 40-line panic that prints file/line to SWO — doable, not shipped by default.
VS Code Zig extension + OpenOCD: breakpoints flaky on -O ReleaseSmall — optimize for debug during bring-up.
std library assumptions
std.debug formatting pulls in more than you want at first. #![no_std] equivalent is don't import std — discipline required. Accidentally imported std.heap once; binary grew 12 KB; CI caught via size limit.
0.11 → 0.12 breakage
Build.zig API changed; @import("builtin") paths shifted. Pin Zig version in build.zig.zon — mandatory for reproducible firmware CI.
Performance vs C
| Metric | C (-Os) | Zig (-O ReleaseSmall) |
|---|---|---|
| Flash | 18.2 KB | 19.1 KB |
| RAM static | 2.1 KB | 2.2 KB |
| Modbus frame parse (1000 loops) | 412 µs | 418 µs |
Noise-level difference. Not why you'd switch.
When I'd choose Zig for embedded
- Greenfield medium complexity, no CubeMX dependency, team tolerates pinning toolchain
- Heavy comptime config from data files
- Gradual migration: Zig app logic + C HAL/drivers
When I'd stay on C
- Client expects STM32Cube ecosystem, MDK, IAR project files
- Safety certification paths needing mature compiler qualification story
- Team has 15 years C, zero Zig — bus factor matters
What I'd do next
- Try Zig 0.13+ official freestanding ARM docs if release notes deliver
- Extract reusable minimal startup template for F1/F4 — amortize bring-up cost
- Compare embassy-style async in Rust on same board — different trade (runtime vs comptime)
Zig's comptime is the real product; the embedded ecosystem is still 2015 Rust-shaped — promising, volunteer-maintained, plan an afternoon for linker scripts.
Honest first impression: I'd prototype in Zig, ship client work in C until HAL story matures — unless the client is us.
Debugging workflow comparison
| Task | C + GDB | Zig + GDB |
|---|---|---|
| Breakpoint in IRQ | Works | Works; panic handler better |
| Print float in IRQ | printf risky | @import("std").debug too heavy — still use SWO |
| Watch variable | watch x | Same |
| Stack overflow detect | -fstack-usage | compile-time stack trace optional |
Zig @panic with file/line beat GDB backtrace when hard fault in ReleaseSmall — if you implement custom panic.
Community dependency risk
zig-stm32 pinned to commit hash — upstream quiet 6 weeks once. For client work that's unacceptable without fork ownership. Internal fork cost: 4 hours/quarter merge — factor into Zig vs C decision same as any OSS HAL.
What I'd do next
Manish Bookreader
Electronics enthusiast, Embedded Systems Expert, Linux/Networking programmer, and Software Engineer passionate about AI, electronics, books, and cooking.

