嵌入式C教程实战之Linux下的单片机编程从零搭建 STM32 开发工具链 —— 交叉编译原理与安装指南本教程是嵌入式C教程实战的实战篇开篇之作。其他基础的理论知识如模板编程将会进一步作为理论教程交替进行更新。仓库已经开源欢迎大佬围观https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP写给所有想在 Linux 下搞 STM32、却被一堆工具链名词搞得晕头转向的朋友。本篇记录我们从零开始搭建 ARM 交叉编译环境的完整过程包括为什么要交叉编译、每个工具是干什么的以及在 Ubuntu 和 Arch Linux 下分别如何安装。小请假条笔者从今天开始出差今天更新完毕之后到4月1日前不会更新公众号了也就是3月29日30日和31日更新的话可能只会缓慢更新手头有的存货。为什么我要写这套教程说句实话我实在绷不住 Keil 那套老旧的工作流了。今年都 2024 年了还在用只能跑在 Windows 上的闭源 IDE代码提示残废调试界面像上个世纪的软件关键是还占了我好几 GB 的 C 盘空间。最要命的是我已经习惯了 Linux 下的开发环境 —— Vim/Neovim 写代码clangd 做补全CMake 管构建这套工具链用在任何项目上都顺手得不行。但事情没那么简单。当我第一次尝试在 Linux 下给 STM32F103C8T6也就是那块几块钱的 Blue Pill 开发板烧程序时我发现网上的教程简直是一场灾难。有的还在用 Makefile 手写编译规则有的直接掏出 PlatformIO 这种把一切都封装好的黑盒还有的干脆说你就用 Keil 吧Linux 下折腾不划算。最离谱的是那些所谓从零开始的教程上来就给你一堆命令让你复制粘贴完全不说 arm-none-eabi-gcc 是干嘛的、newlib 又是什么、为什么需要链接脚本。你照着做确实能跑通但只要稍微出点问题你就完全不知道从哪下手排查。我花了整整一个周末把这套工具链从里到外折腾了一遍踩了无数坑之后终于理清了整个编译烧录的链条。现在我要把这个过程完整地记录下来不是给你一份复制就能跑的 cheat sheet而是带你真正理解每一步在做什么、为什么这么做。这样当你以后遇到报错时能知道问题出在哪个环节而不是像无头苍蝇一样到处搜答案。先说清楚什么是交叉编译在我们开始敲命令之前有一个概念必须先讲明白 —— 交叉编译Cross-Compilation。如果你平时写的是运行在 x86-64 CPU 上的普通程序编译过程很直接你用 gcc 编译代码生成的可执行文件也是在同一台机器上运行的。编译器和程序运行的目标平台是同一个这叫本地编译Native Compilation。但 STM32F103C8T6 用的是 ARM Cortex-M3 核心指令集和你电脑上的 x86-64 完全不同。你在电脑上用普通的 gcc 编译出来的代码STM32 根本读不懂就像你对着一个只懂中文的人念阿拉伯语一样。所以我们需要一个翻译官 —— 一个运行在 x86-64 Linux 上、但能生成 ARM 机器码的编译器。这就是交叉编译器。那为什么叫arm-none-eabi-gcc这么一长串奇怪的名字拆开来解释就很清楚了arm是目标 CPU 架构生成的代码是给 ARM 用的none表示没有操作系统厂商后面会讲eabi是 Embedded Application Binary Interface嵌入式应用二进制接口的缩写gcc就是我们熟悉的 GNU Compiler Collection这里有个细节值得展开。none这个字段原本是用来标注操作系统厂商的比如arm-linux-eabi表示给跑 Linux 的 ARM 设备编译。但我们的 STM32 是裸机程序没有操作系统撑腰所以这里填none。而eabi和eabihf的区别在于后者支持硬件浮点但 F103C8T6 的 Cortex-M3 只有单精度浮点单元所以用普通的eabi就够了。理解交叉编译之后你就会明白为什么不能直接用系统自带的 gcc也知道为什么需要一整套专门的工具链编译器、链接器、调试器、objcopy用来把 ELF 转成二进制、size用来查看生成的固件大小这些工具都必须是交叉版本的。整个工具链长什么样在正式安装之前我想先把整体框架搭起来让你知道我们最终要凑齐哪些零件。编译一个 STM32 程序并烧到板子上大概需要这么一套流水线首先是源代码层面。你写的 C/C 代码需要经过预处理、编译、汇编变成一个个目标文件.o文件。这一步用的是arm-none-eabi-gccC 代码和arm-none-eabi-gC 代码。但光有目标文件还不行它们需要被胶水粘在一起。这个胶水就是链接器arm-none-eabi-ld它的工作是把所有目标文件、库文件按照指定的规则拼成一个完整的程序。对于 STM32 来说链接过程尤其特殊 —— 你需要告诉它 Flash 从哪个地址开始、RAM 在哪里、堆栈怎么分配这些规则写在链接脚本Linker Script.ld文件里。链接器会按照脚本里的地图把代码段、数据段放到正确的位置。链接完成之后你得到的是一个 ELF 格式的文件.elf里面包含了代码、数据、符号表等一堆信息。但 STM32 的 Flash 只认纯粹的二进制数据不需要什么符号表。所以需要用arm-none-eabi-objcopy把 ELF 文件里的干货提取出来生成一个.bin二进制文件。这个文件才是真正要烧进 Flash 的东西。烧录工具有好几种选择。最常见的是 ST-Link V2这是 ST 官方出的调试器/烧录器通过 SWDSerial Wire Debug协议和 STM32 通信。在 Linux 下我们需要一个软件来驱动 ST-Link这个软件就是 OpenOCDOpen On-Chip Debugger。它能扮演两个角色一是把固件写到 Flash 里烧录二是充当 GDB Server让你用 GDB 调试板子上的程序。说到库文件这里有个新手容易混淆的点。ARM 裸机程序没法直接用你电脑上的 glibcGNU C Library因为 glibc 是给操作系统环境设计的依赖一堆系统调用。嵌入式环境需要的是 newlib —— 一个专门为裸机/嵌入式系统设计的 C 标准库实现。更具体地说我们用的是 newlib-nano它是 newlib 的精简版针对代码体积做了优化。安装arm-none-eabi-newlib之后编译器就能找到stdint.h、string.h这些头文件链接时也能拿到必要的库函数实现。最后一环是调试。OpenOCD 可以以 GDB Server 模式运行监听某个端口默认 3333。你用arm-none-eabi-gdb连上去就能像调试普通程序一样单步执行、打断点、查看变量。VSCode 的 Cortex-Debug 插件就是把这整套流程图形化了你不用手动敲 GDB 命令。把这些串起来完整的链条是源代码 → 交叉编译 → 链接带链接脚本→ objcopy 提取二进制 → OpenOCD 烧录 → GDB 调试。理解这个链条之后你就会知道每个工具在哪个环节起作用出问题时能快速定位是编译、链接还是烧录阶段出了岔子。好了现在开始上号前面铺垫了这么多概念现在我们终于可以动手了。我会分 Ubuntu 和 Arch 两条线来讲但你很快会发现命令其实差不多都是包管理器那一套。先说 Ubuntu。这里我用的是 22.04 LTS但 20.04 和 24.04 的命令基本一致毕竟是同一个软件源。打开终端先更新一下包索引这是个好习惯sudoaptupdate然后一口气把需要的包装上sudoaptinstall-y\gcc-arm-none-eabi\gdb-arm-none-eabi\openocd\cmake\build-essential让我解释一下这几个包都干嘛的。gcc-arm-none-eabi是个大礼包里面包含了交叉编译器、链接器、objcopy、size 等一整套工具。gdb-arm-none-eabi是 ARM 版本的 GDB用来调试嵌入式程序。openocd我们前面说过了是烧录和 GDB Server。cmake和build-essential则是构建工具后者包含了 make 等基础编译工具。安装完成之后我们可以验证一下工具链是不是真的装上了arm-none-eabi-gcc--version正常的话你会看到类似这样的输出arm-none-eabi-gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is no warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.版本号可能不一样但只要能打印出版本信息就说明安装成功了。这里还有个小细节Ubuntu 的包名是gcc-arm-none-eabi不带版本号软件源会自动选一个稳定且大多数人用的版本。如果你需要特定版本比如想用最新的 GCC 14那就得去 ARM 官方下载预编译的工具链手动解压到某个目录然后把路径加到PATH环境变量里。不过对于 F103C8T6 这种老芯片GCC 11 已经足够了没必要折腾太新的版本。Arch Linux 用户的路线如果你用的是 Arch Linux或者我用的 Manjaro包管理就更直接了。Arch 的优势是软件更新快你能拿到比较新的工具链版本。安装命令比 Ubuntu 简短一些sudopacman-Sarm-none-eabi-gcc arm-none-eabi-binutils arm-none-eabi-gdb openocd cmakemake这里有个和 Ubuntu 不同的地方Arch 把工具拆分成了多个包。arm-none-eabi-gcc是编译器本身arm-none-eabi-binutils包含了 ld、objcopy、size 这些工具arm-none-eabi-gdb是调试器。Ubuntu 把这些都打包进了gcc-arm-none-eabi所以需要装的包更少。验证一下安装是否成功arm-none-eabi-gcc--versionArch 上你大概率会看到 GCC 13 或者 14因为滚得快arm-none-eabi-gcc (GCC) 13.2.0 Copyright (C) 2023 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is no warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.这里有个坑需要提前预警一下。Arch 上装完arm-none-eabi-gcc之后你可能会发现编译时找不到stdint.h这类头文件或者链接时报cannot read spec file nano.specs。原因都是同一个 —— Arch 的arm-none-eabi-gcc包不包含 newlib你需要额外装一个 AUR 上的包yay-Sarm-none-eabi-newlib如果你没有装yay那得先装这个 AUR helper或者手动从 AUR 克隆 PKGBUILD 来装。这个过程我就不展开了用 Arch 的人应该都熟。装完 newlib 之后stdint.h、string.h这些头文件就有了nano.specs和nosys.specs也能正常使用。这两个 specs 文件是干嘛的nano.specs告诉链接器用 newlib-nano精简版 C 库nosys.specs则提供一个空的系统调用实现 —— 毕竟裸机环境没有操作系统像read()、write()这类函数根本没法实现用 nosys.specs 能让链接时不报错。到哪一步了到这里我们的工具链安装就算完成了。你现在的系统上应该有交叉编译器arm-none-eabi-gcc/g链接器和工具链arm-none-eabi-ld, objcopy, size调试器arm-none-eabi-gdb烧录工具OpenOCD构建系统CMakeC 标准库newlib但光有工具还不够下一篇文章我们会讲项目结构 —— 怎么获取 ST 官方的 HAL 库、那个坑人的 submodule 问题、启动文件到底选哪个、链接脚本怎么写。那部分才是真正的踩坑集中营现在我们先把地基打牢。你可以先验证一下所有工具都能正常调用# 验证编译器arm-none-eabi-gcc--version# 验证调试器arm-none-eabi-gdb--version# 验证烧录工具openocd--version# 验证 CMakecmake--version如果这些命令都能打印出版本信息恭喜你工具链安装这一关就算过了。下一篇文章我们会直接进入项目结构开始搭建真正的 STM32 C 项目。