Zig 嵌入式底层库选择与 build.zig 组织指南

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 库

  1. 看 target 是否是 freestanding
  2. 如果是 freestanding,Zig 默认不会自动链接 libc。
  3. 看 build.zig 是否显式开启 libc
  4. 没有这一行,通常就是纯裸机工程。
  5. 看芯片包如何按型号选文件
  6. 例如根据 -Dchip=stm32f103c8t6 选择:
    • startup_stm32f103.s
    • stm32f103.ld
    • 寄存器定义
  7. 看链接脚本和启动文件是否匹配芯片
  8. 选错会导致链接报错、烧录后不运行、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
  • *.ld
  • SystemInit()
  • 寄存器访问封装

所以更常见的做法是:不开 **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。

相关推荐
漂流瓶jz2 小时前
UVA-11491 奖品的价值 题解答案代码 算法竞赛入门经典第二版
c++·算法·图论·题解·aoapc·算法竞赛入门经典·uva
weixin_307779136 小时前
C++代码实现MATLAB中的dlarray函数功能
开发语言·c++·算法·matlab
Frank_refuel9 小时前
C++11之一场名为“搬家”的 C++ 之旅
开发语言·c++
hetao173383711 小时前
2026-10-03~04 hetao1733837 的刷题记录
c++·算法
longlongzihan12 小时前
从LeetCode 70. 爬楼梯看动态规划:如何精准推导状态转移方程?
c++·算法·leetcode·动态规划
女神下凡12 小时前
芯参谋(38):NAND_MCP_电路设计指南
arm开发·单片机·嵌入式硬件·设计规范
嵌入式学习菌13 小时前
FreeRTOS 中断管理实战:BASEPRI 寄存器实验
单片机·嵌入式硬件
潼心1412o15 小时前
C++初阶(长期更新)第6讲:类和对象(下)
开发语言·c++·算法
-森屿安年-15 小时前
按身高排序
c++·算法