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 裸机工程
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:
zig
exe.addCSourceFile(.{
.file = b.path("drivers/uart.c"),
.flags = &.{
"-mcpu=cortex-m3",
"-mthumb",
"-ffreestanding",
"-Wall",
},
});
addCSourceFile 只是把 C 文件交给 Zig 内置的 Clang 前端编译,并不会自动引入完整 C 运行库。
5. 引入厂商寄存器头文件
zig
exe.addIncludePath(b.path("include"));
C 文件里可以写:
c
#include "stm32f10x.h"
Zig 代码里如果想直接调用 C 头文件里的符号,可以用:
zig
const c = @cImport({
@cInclude("stm32f10x.h");
});
注意:@cImport 通常要求目标已经链接 libc。
6. 开启 C 运行时
zig
exe.linkLibC();
这会让 Zig 根据 target 选择可用的 C 库实现。
但在 STM32 这种 freestanding 目标上,linkLibC() 并不等于自动拿到 ST HAL、CMSIS 或 newlib-nano。很多嵌入式工程仍然需要自己提供:
startup_*.s*.ldSystemInit()- 寄存器访问封装
所以更常见的做法是:不开 **linkLibC()**,而是手动引入启动文件、链接脚本和寄存器头文件。
7. 完整混合工程示例
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,
});
// 链接脚本
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. 按芯片型号自动切换底层文件
zig
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);
}
构建命令:
bash
zig build -Dchip=stm32f103c8t6
zig build -Dchip=stm32f401re
这样不同 MCU 的底层文件就由 chips 表统一管理,切换芯片时不需要手动改 build.zig。