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。

相关推荐
创界工坊工作室6 小时前
55-基于STC89C52宠物智能饲喂控制系统设计
stm32·单片机·嵌入式硬件·51单片机·proteus·宠物
Zwawa6 小时前
编译器工具链:GCC、Clang、MSVC、ICC、armclang
c语言·嵌入式·编译器
学逆向的6 小时前
C 指针常量与常量指针
c语言·开发语言·网络安全
水饺编程7 小时前
第2章:Linux 基础,exit 命令
linux·c语言·汇编
CodeYp20197 小时前
第15讲:二叉搜索树
开发语言·c++·二叉搜索树
pipixia200338 小时前
C++继承面试四连问:内存叠加、私有继承、sizeof、权限重映射
c++·面试·嵌入式·继承·面向对象·对象模型
极客 - L U9 小时前
卡尔曼滤波从零到精通
c语言·数据结构·单片机·嵌入式硬件·mcu
牵猫散步的鱼儿10 小时前
C++ 内存分区
c++
码小文11 小时前
CAM350使用笔记(6)---Gerber文件铺铜间距检查
嵌入式硬件·cam350软件·cam350使用笔记
choumin11 小时前
C++ 标准容器为什么不能存引用
c++·容器·引用类型