This document gives a high-level overview of MicroZig's codebase. If you want to contribute or understand how things fit together, start here.
MicroZig is a framework for programming microcontrollers using Zig. It provides a unified build system, hardware abstraction layers, and device drivers across 14+ microcontroller families (ARM Cortex-M, RISC-V, AVR, MSP430).
microzig/
├── build.zig
├── build-internals/ # Target/Chip/HAL/Board type system, build-time tools
├── core/ # Foundational library: CPU, MMIO, interrupts, startup
├── port/ # Platform-specific HALs and chip definitions
├── drivers/ # HAL-agnostic device drivers
├── tools/ # Standalone CLI tools (regz, uf2, dfu, etc.)
├── modules/ # Packages maintained by ZigEmbeddedGroup, but not depended on by microzig
├── examples/ # Per-platform example applications
├── sim/ # Simulators (Currently only our AVR simulator _aviron_)
├── vendor/ # Vendored third-party code
├── website/ # Documentation site (microzig.tech)
└── scripts/ # Utility scripts
The foundational library that every firmware links against. Key files in core/src/:
- microzig.zig: Root API. Exposes
cpu,chip,hal,board,driversnamespaces. Implements the default panic handler,export_startup()for entry point generation, andstd_options()overriding defaults (currently only logging-related). - mmio.zig:
Mmiogeneric type that wraps a packed struct into a volatile MMIO register interface withread(),write(),modify(),toggle()methods. Add register access should go through the generatedMmiotypes. - interrupt.zig:
enable(),disable(), critical sections,Mutex(HAL-provided or fallbackCriticalSectionMutex),Handlerunion type. - allocator.zig: Fixed-buffer allocator.
- cpus/: CPU architecture implementations:
- cortex_m.zig (+ subdirectory with variants): interrupt vector tables, exception handling, FPU support, RAM vector tables.
- riscv32.zig: RISC-V 32-bit support.
- avr_common.zig, avr25.zig, avr5.zig: AVR architecture.
- msp430.zig, msp430x.zig: TI MSP430.
- usb.zig: USB device stack with CDC and HID class drivers.
Each port implements support for a microcontroller family. This includes creating a Microzig target
for various chips (and some well-known boards) as well as implementing a HAL to support peripherals.
Ports follow a consistent structure (using port/raspberrypi/rp2xxx/ as the reference):
port/<vendor>/<family>/
├── build.zig # Chip/board/target definitions, memory regions, SVD paths
├── src/
│ ├── hal.zig # HAL root: re-exports gpio, uart, i2c, spi, dma, etc.
│ │ # A port may have more than one, such as in the case for rp2xxx
│ ├── hal/ # Individual HAL peripheral namespaces
│ ├── boards/ # Board definitions (pin mappings, oscillator freq, etc.)
│ └── cpus/ # CPU variants if the family has multiple cores
├── patches/ # Corrections to vendor SVD/ATDF files
└── ld/ # Custom linker scripts, when needed
The build.zig in each port defines Chip values (SVD path, memory regions, patches) and Board
values (root source file with pin configs). These feed into the Target type from
build-internals/.
Current ports: ESP32 (espressif/esp), GD32 (gigadevice/gd32), ATmega/ATtiny/SAMD51 (microchip/), nRF5x (nordic/nrf5x), LPC/MCX (nxp/), RP2040/RP2350 (raspberrypi/rp2xxx), STM32 (stmicro/stm32), MSP430/MSPM0/TM4C (texasinstruments/), CH32V (wch/ch32v), CH32H (wch/ch32h).
Device-independent drivers that talk to hardware through base interface abstractions. Located in
drivers/src/:
- base/: Interface types using a pointer+vtable pattern:
- Digital_IO.zig: GPIO pin abstraction (direction, bias, read/write).
- I2C_Device.zig: I2C bus communication.
- StreamDevice.zig: Byte-stream I/O (UART-like).
- DatagramDevice.zig: Packet-based I/O.
- BlockMemory.zig: Block storage.
- ClockDevice.zig: Real-time clock.
- time/:
Absolute(microseconds since boot),Duration,Deadline. - Device drivers organized by category: display/ (SSD1306, ST77xx, HD44780), input/ (debounced buttons, rotary encoders, keyboard matrices), sensor/ (temperature, motion, magnetic), stepper/ (A4988, DRV8825), led/ (WS2812), wireless/ (CYW43 WiFi/BT), io_expander/ (PCF8574, PCA9685).
The vtable approach lets drivers work with any HAL that implements the base interfaces. Drivers never import port-specific code.
The key types in build-internals/build.zig:
Target: A complete firmware compilation target. Bundles azig_targetquery,Chip,HardwareAbstractionLayer, optionalBoard,LinkerScript,Stackplacement, and preferredBinaryFormat. Supportsderive()for creating variants.Chip: MCU definition: name,register_definition(SVD, ATDF, Zig, Embassy, or TargetDB source),memory_regions, andpatch_files.HardwareAbstractionLayer: Root source file + imports for the HAL.Board: Root source file + imports for board-specific config.MemoryRegion: Describes a memory region (flash/RAM) with offset, length, and access permissions.BinaryFormat: Default output format:.elf,.binary,.hex,.uf2,.dfu,.esp, or.custom.LinkerScript: Auto-generated or custom, controls section placement and rodata location (flash vs RAM).
This directory also contains the build-time tools that run during compilation: regz/ (register code generation), uf2/, dfu/, esp-image/ (format converters), linter/, sorcerer/ (register visualizer), printer/, generate_linker_script.zig.
Standalone versions of the build-time tools, usable outside the MicroZig build system. regz is the
most important: it parses vendor hardware description files (SVD, ATDF, Embassy, TI TargetDB) and
generates type-safe Zig MMIO register interfaces. sorcerer produces SVG visualizations of register
maps.
Shared Zig libraries: foundation-libc/ (minimal libc), freertos/ (RTOS integration), lwip/ (TCP/IP stack), rtt/ (Segger Real-Time Transfer for debug output), network/ (networking abstractions), virtual-io/, bounded-array/, riscv32-common/.
One directory per platform, mirroring the port/ structure: examples/raspberrypi/rp2xxx/,
examples/nordic/nrf5x/, etc. Each has its own build.zig that uses MicroBuild to define
firmware targets. Examples often show off different parts of the HAL.
sim/aviron/ is an AVR instruction-level simulator for testing AVR firmware without hardware.
The path from a vendor hardware description to running firmware:
- Vendor provides SVD/ATDF describing chip registers and memory layout.
- Port's build.zig references the SVD and defines
Chip(memory regions, patches) andTargetvalues. regz(at build time) processes the SVD (applying patches) and generates a Zig module with type-safe MMIO register definitions.- generate_linker_script.zig generates a linker script using the
Target's memory regions. - MicroBuild's
add_firmware()wires everything: it creates Zig modules formicrozig(core),chip(generated registers),cpu,hal,board, anddrivers, then compiles the user'sroot_source_fileagainst them. - Startup (core/src/cpus/): the CPU module provides the entry point, initializes the stack,
zeroes BSS, copies data from flash to RAM, then calls
microzig_main(the user's entry point). - User code calls into
hal(e.g.,hal.gpio.init()) which uses the generatedchipregisters to configure hardware. Drivers use thehalthrough base interfaces. - Output conversion transforms the ELF into the target format (UF2, HEX, DFU, etc.) for flashing.
The build system centers on MicroBuild, defined in the root build.zig.
const MicroBuild = microzig.MicroBuild(.{ .rp2xxx = true });
const mb = MicroBuild.init(b, mz_dep) orelse return;
const fw = mb.add_firmware(.{
.name = "blinky",
.root_source_file = b.path("src/blinky.zig"),
.target = mb.ports.rp2xxx.boards.raspberrypi.pico,
});
mb.install_firmware(fw, .{});add_firmware() invokes regz on the chip's register definition, generates the linker script,
creates all the Zig modules, and sets up the compilation. install_firmware() adds the format
conversion step (e.g. to uf2 or bin) and copies the result to zig-out/firmware/.
- Drivers never import port-specific code. They operate exclusively through the base interfaces
in
drivers/src/base/. This is what makes them portable. - Ports are self-contained.
- Core is port-agnostic. It receives chip/HAL/board configuration through comptime module imports injected by the build system.
- Register definitions are generated. The source of truth is the vendor SVD/ATDF file plus patches. Don't edit generated register code in your local build, and don't edit the svd files. Add patches in the port's patches/ directory to make corrections.
- No global mutable state in libraries. Embedded code uses explicit initialization and passed handles.
- Memory regions are the port's responsibility. Core and drivers don't assume any particular memory layout.
Comptime configuration: The build system injects microzig.config as a comptime-known struct.
User code queries it to branch on chip features, CPU type, or board identity. HALs use it to select
peripheral implementations, setup clocks, etc.
Startup sequence: Each CPU architecture in core/src/cpus/ provides a _start entry point. The
typical sequence is: set stack pointer → zero BSS → copy .data from flash to RAM → call
microzig_main.
Naming conventions: The codebase uses snake_case (C-style), not the typical Zig camelCase for function names. Types are in PascalCase.
Styling: We follow standard zig formatting. Run zig fmt for formatting.