Zig 嵌入式底层库选择与 build.zig 组织指南
适用于 STM32、GD32 等 ARM Cortex-M MCU。Zig 不会像 ARM GCC / Keil / IAR 那样自动为不同 MCU 挑选厂商 HAL 或 C 运行库;底层依赖由target、build.zig、芯片包、链接脚本、启动文件共同决定。5
1. 决定链
| 层级 | 谁决定 | 实际作用 |
|---|---|---|
| 目标三元组 | target/b.standardTargetOptions | 决定架构、ABI、是否有 libc,例如thumb-freestanding-eabi |
| 构建脚本 | build.zig | 决定是否链接 libc、入口点、链接脚本、启动文件 |
| 芯片包 | stmicro-stm32、microzig等 | 按芯片型号选择 startup、寄存器定义、内存布局 |
| 链接脚本 | .ld | 决定 Flash/RAM 地址、段布局、栈顶符号 |
| 启动文件 | startup_*.s | 决定向量表、栈初始化、data/bss 初始化、跳转到 main |
2. 如何判断当前工程是否使用了 C 库
- 看 target 是否是 freestanding
- 如果是 freestanding,Zig 默认不会自动链接 libc。
- 看 build.zig 是否显式开启 libc
- 没有这一行,通常就是纯裸机工程。
- 看芯片包如何按型号选文件
- 例如根据
-Dchip=stm32f103c8t6选择:startup_stm32f103.sstm32f103.ld- 寄存器定义
- 看链接脚本和启动文件是否匹配芯片
- 选错会导致链接报错、烧录后不运行、HardFault、中断异常等问题。
3. 纯 Zig 裸机工程
const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.resolveTargetQuery(.{ .cpu_arch = .thumb, .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m3 }, .os_tag = .freestanding, .abi = .eabi, }); const optimize = b.standardOptimizeOption(.{ .default = .ReleaseSmall }); const exe = b.addExecutable(.{ .name = "firmware", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); // 链接脚本:决定 Flash / RAM 布局 exe.setLinkerScript(b.path("linker/stm32f103.ld")); // 启动文件:向量表、栈初始化、跳转到 main exe.addAssemblyFile(b.path("src/startup_stm32f103.s")); // 链接阶段回收未使用段 exe.link_gc_sections = true; exe.link_data_sections = true; // 生成 .bin const bin = b.addObjCopy(exe, .{ .format = .bin }); bin.step.dependOn(&exe.step); b.installArtifact(bin); b.installArtifact(exe); }这里没有任何 C 库,属于纯 freestanding 固件。
4. 加入 C 源文件
如果底层驱动是 C 写的,例如drivers/uart.c:
exe.addCSourceFile(.{ .file = b.path("drivers/uart.c"), .flags = &.{ "-mcpu=cortex-m3", "-mthumb", "-ffreestanding", "-Wall", }, });addCSourceFile只是把 C 文件交给 Zig 内置的 Clang 前端编译,并不会自动引入完整 C 运行库。
5. 引入厂商寄存器头文件
exe.addIncludePath(b.path("include"));C 文件里可以写:
#include"stm32f10x.h"Zig 代码里如果想直接调用 C 头文件里的符号,可以用:
const c = @cImport({ @cInclude("stm32f10x.h"); });注意:@cImport通常要求目标已经链接 libc。
6. 开启 C 运行时
exe.linkLibC();这会让 Zig 根据 target 选择可用的 C 库实现。
但在 STM32 这种freestanding目标上,linkLibC()并不等于自动拿到 ST HAL、CMSIS 或 newlib-nano。很多嵌入式工程仍然需要自己提供:
startup_*.s*.ldSystemInit()- 寄存器访问封装
所以更常见的做法是:不开**linkLibC()**,而是手动引入启动文件、链接脚本和寄存器头文件。
7. 完整混合工程示例
const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.resolveTargetQuery(.{ .cpu_arch = .thumb, .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m3 }, .os_tag = .freestanding, .abi = .eabi, }); const optimize = b.standardOptimizeOption(.{ .default = .ReleaseSmall }); const exe = b.addExecutable(.{ .name = "firmware", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); // 链接脚本 exe.setLinkerScript(b.path("linker/stm32f103.ld")); // 启动汇编 exe.addAssemblyFile(b.path("src/startup_stm32f103.s")); // C 驱动源文件 exe.addCSourceFile(.{ .file = b.path("drivers/uart.c"), .flags = &.{ "-mcpu=cortex-m3", "-mthumb", "-ffreestanding", "-Wall", }, }); // 寄存器头文件路径 exe.addIncludePath(b.path("include")); // 如果确实需要 C 运行时,再开启: // exe.linkLibC(); exe.link_gc_sections = true; exe.link_data_sections = true; const bin = b.addObjCopy(exe, .{ .format = .bin }); bin.step.dependOn(&exe.step); b.installArtifact(bin); b.installArtifact(exe); }8. 判断当前工程用了哪一层
| 你想确认什么 | 在 build.zig 里看什么 |
|---|---|
| 是否裸机 | os_tag = .freestanding |
| 是否链接 C 库 | 是否有linkLibC() |
| 是否引入 C 文件 | 是否有addCSourceFile/addCSourceFiles |
| 是否引入寄存器头 | 是否有addIncludePath |
| 是否使用芯片启动文件 | 是否有addAssemblyFile |
| 是否配置 Flash/RAM | 是否有setLinkerScript |
9. 按芯片型号自动切换底层文件
const std = @import("std"); const ChipInfo = struct { name: []const u8, ld: []const u8, startup: []const u8, cpu_model: std.Target.arm.CpuModel, }; const chips = std.StaticStringMap(ChipInfo).initComptime(.{ .{ "stm32f103c8t6", .{ .name = "stm32f103c8t6", .ld = "linker/stm32f103.ld", .startup = "src/startup_stm32f103.s", .cpu_model = std.Target.arm.cpu.cortex_m3, } }, .{ "stm32f401re", .{ .name = "stm32f401re", .ld = "linker/stm32f401.ld", .startup = "src/startup_stm32f401.s", .cpu_model = std.Target.arm.cpu.cortex_m4, } }, }); pub fn build(b: *std.Build) void { const chip_name = b.option([]const u8, "chip", "芯片型号") orelse "stm32f103c8t6"; const chip = chips.get(chip_name) orelse { std.log.err("未知芯片: {s}", .{chip_name}); return; }; const target = b.resolveTargetQuery(.{ .cpu_arch = .thumb, .cpu_model = .{ .explicit = &chip.cpu_model }, .os_tag = .freestanding, .abi = .eabi, }); const optimize = b.standardOptimizeOption(.{ .default = .ReleaseSmall }); const exe = b.addExecutable(.{ .name = "firmware", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); exe.setLinkerScript(b.path(chip.ld)); exe.addAssemblyFile(b.path(chip.startup)); exe.link_gc_sections = true; exe.link_data_sections = true; const bin = b.addObjCopy(exe, .{ .format = .bin }); bin.step.dependOn(&exe.step); b.installArtifact(bin); b.installArtifact(exe); }构建命令:
zig build-Dchip=stm32f103c8t6 zig build-Dchip=stm32f401re这样不同 MCU 的底层文件就由chips表统一管理,切换芯片时不需要手动改build.zig。