第二章:交叉编译环境搭建
好,咱们正式开始动手了。
这一章要解决的是:怎么让你的电脑,能编译出呼吸机主控芯片能跑的程序。
你想想看,我们平时在PC上写代码,gcc编译出来的是x86指令。但呼吸机的主控芯片,通常是ARM Cortex-M系列或者RISC-V。指令集都不一样,直接编译肯定不行。所以,我们需要一个交叉编译器——它跑在PC上,但生成的目标代码是给嵌入式芯片用的。
2.1 选哪个GCC交叉编译器?
我个人习惯用arm-none-eabi-gcc。为什么是它?
- arm:目标架构是ARM
- none:没有操作系统(裸机程序)
- eabi:嵌入式应用二进制接口
呼吸机的实时控制程序,基本都是跑在裸机或者RTOS上,所以这个版本最合适。
如果你用的是STM32系列,官方推荐的是gcc-arm-none-eabi-10.3-2021.10这个版本。我试过好几个版本,这个版本对Cortex-M4/M7的支持最稳定。之前有个项目,用了更新的11.2版本,结果浮点运算库出了点小问题,折腾了两天才发现是编译器版本兼容性导致的。
下载地址:
ARM官方GCC下载页面:https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm
或者直接用我常用的这个链接(Linux x86_64版):
https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-rm/10.3-2021.10/gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2
2.2 安装步骤
我以Ubuntu 20.04 LTS为例。Windows用户可以用WSL或者Cygwin,但说实话,我建议你直接用Linux虚拟机或者双系统。做嵌入式开发,Linux环境比Windows顺手太多。
2.2.1 解压安装
# 下载完成后,解压到 /opt 目录
sudo tar -xjf gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2 -C /opt/
# 重命名一下,方便管理
sudo mv /opt/gcc-arm-none-eabi-10.3-2021.10 /opt/gcc-arm-none-eabi
嗯,这里要注意:不要用root用户直接解压到系统目录。我之前有一次手快,用sudo解压后,所有文件的owner都变成了root,后面编译时老是报权限错误。所以解压后最好检查一下:
ls -l /opt/gcc-arm-none-eabi/bin/
确保所有文件都是root:root或者你的用户名:你的用户组。如果是前者,没问题;如果是后者,记得chown -R root:root /opt/gcc-arm-none-eabi。
2.2.2 环境变量配置
这一步很关键。不配环境变量,你每次编译都要写全路径,那太痛苦了。
我习惯把配置写在~/.bashrc里,这样每次打开终端自动生效。
# 编辑 ~/.bashrc
vim ~/.bashrc
# 在文件末尾添加以下内容
export PATH="/opt/gcc-arm-none-eabi/bin:$PATH"
export CROSS_COMPILE=arm-none-eabi-
export CC=${CROSS_COMPILE}gcc
export AS=${CROSS_COMPILE}as
export LD=${CROSS_COMPILE}ld
export OBJCOPY=${CROSS_COMPILE}objcopy
export OBJDUMP=${CROSS_COMPILE}objdump
为什么我要单独定义CROSS_COMPILE?因为很多嵌入式项目的Makefile里,都会用这个变量来指定交叉编译器前缀。你定义好之后,以后编译各种开源项目(比如FreeRTOS、uC/OS-III)时,直接make CROSS_COMPILE=arm-none-eabi-就行了,省事。
配置完记得让环境变量生效:
source ~/.bashrc
小技巧:如果你同时做多个嵌入式项目(比如ARM和RISC-V),可以在每个项目的根目录下放一个setenv.sh脚本,里面只配当前项目需要的环境变量。这样切换项目时,source setenv.sh一下就行,不会互相干扰。
2.3 验证交叉编译工具链
环境配好了,咱们得验证一下能不能用。写个最简单的程序测试。
2.3.1 编写测试代码
创建一个测试文件:
mkdir -p ~/test_cross_compile
cd ~/test_cross_compile
vim test.c
输入以下内容:
#include <stdio.h>
int main(void)
{
printf("Hello, Breathing Machine!\n");
return 0;
}
等等,你可能会问:呼吸机嵌入式程序不是裸机吗?怎么还用printf?
没错,这个测试程序其实是在验证编译器和链接器能不能正常工作。printf最终会调用_write系统调用,在裸机环境下我们需要自己实现这个函数。但这里我们只是测试编译阶段,所以没问题。
2.3.2 编译测试
arm-none-eabi-gcc -c test.c -o test.o -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -O2 -Wall
解释一下这些参数:
-mcpu=cortex-m4:指定目标CPU为Cortex-M4。呼吸机主控常用STM32F4/F7系列,都是M4内核。-mthumb:使用Thumb-2指令集。ARM Cortex-M系列只支持Thumb模式。-mfloat-abi=hard -mfpu=fpv4-sp-d16:启用硬件浮点运算单元。呼吸机算法里有很多PID计算、滤波运算,没有硬件浮点会慢很多。-O2:优化等级。调试阶段可以用-O0,但正式发布一定要开优化。-Wall:开启所有警告。我建议你永远不要关这个选项。
编译成功后,检查生成的目标文件:
file test.o
你应该会看到类似这样的输出:
test.o: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), not stripped
看到ARM, EABI5了吗?这说明我们的交叉编译器确实生成了ARM架构的目标文件,而不是x86的。
2.3.3 链接测试
接下来,我们尝试链接成可执行文件。不过裸机程序需要链接脚本和启动文件,这里我们偷个懒,用-nostartfiles参数跳过启动流程,只验证链接器能不能工作:
arm-none-eabi-gcc test.o -o test.elf -nostartfiles -specs=nano.specs -lc -lgcc -mcpu=cortex-m4 -mthumb
如果链接成功,会生成test.elf文件。再用file命令看看:
file test.elf
输出应该是:
test.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped
看到statically linked了吗?这说明链接器把需要的库函数都静态链接进去了。在嵌入式系统里,我们几乎不用动态链接,因为那需要操作系统支持。
避坑指南:我曾经在链接阶段遇到过undefined reference to `_exit'的错误。这是因为nano.specs里依赖了_exit函数,但裸机环境下没有实现。解决办法是加一个空的_exit函数,或者在链接时加上-u _exit参数。不过对于咱们的测试,直接用-nostartfiles跳过就行。
2.4 生成反汇编文件
验证工具链的另一个好方法,是生成反汇编文件,看看生成的汇编代码对不对。
arm-none-eabi-objdump -d test.elf > test.dis
打开test.dis,你会看到类似这样的内容:
00008000 <main>:
8000: b580 push {lr}
8002: b082 sub sp, #8
8004: f000 f802 bl 800c <printf>
8008: 2000 movs r0, #0
800a: bd80 pop {pc}
看到push {lr}、movs r0, #0这些Thumb指令了吗?这说明编译器正确生成了Cortex-M4的指令。如果看到stmfd、ldmfd这些ARM指令,那说明你忘了加-mthumb参数——Cortex-M系列不认ARM指令,会直接跑飞。
2.5 完整测试脚本
为了方便以后快速验证工具链,我写了个小脚本,你可以直接拿去用:
#!/bin/bash
# 交叉编译工具链测试脚本
# 用法: ./test_toolchain.sh
CROSS_COMPILE=arm-none-eabi-
CC=${CROSS_COMPILE}gcc
OBJDUMP=${CROSS_COMPILE}objdump
SIZE=${CROSS_COMPILE}size
echo "=== 测试交叉编译工具链 ==="
# 检查编译器是否存在
if ! command -v ${CC} &> /dev/null; then
echo "错误: 找不到 ${CC}"
echo "请检查环境变量 PATH 是否包含交叉编译器路径"
exit 1
fi
echo "编译器版本:"
${CC} --version | head -n 1
# 编译测试
echo ""
echo "编译测试..."
cat > test.c << 'EOF'
#include <stdio.h>
int main(void)
{
printf("Toolchain OK!\n");
return 0;
}
EOF
${CC} -c test.c -o test.o -mcpu=cortex-m4 -mthumb -O2 -Wall
if [ $? -ne 0 ]; then
echo "编译失败!"
exit 1
fi
echo "目标文件信息:"
file test.o
# 链接测试
echo ""
echo "链接测试..."
${CC} test.o -o test.elf -nostartfiles -specs=nano.specs -lc -lgcc -mcpu=cortex-m4 -mthumb
if [ $? -ne 0 ]; then
echo "链接失败!"
exit 1
fi
echo "可执行文件信息:"
file test.elf
echo "代码大小:"
${SIZE} test.elf
# 反汇编
echo ""
echo "生成反汇编文件..."
${OBJDUMP} -d test.elf > test.dis
echo "反汇编文件: test.dis"
# 清理
rm -f test.c test.o test.elf
echo ""
echo "=== 测试通过! 交叉编译工具链工作正常 ==="
把这个脚本保存为test_toolchain.sh,然后chmod +x test_toolchain.sh,以后每次搭建新环境或者换电脑时,跑一遍这个脚本,几分钟就能确认工具链是否正常。
2.6 常见问题与解决
| 问题 | 原因 | 解决方法 |
|---|---|---|
arm-none-eabi-gcc: command not found |
环境变量没配好 | 检查~/.bashrc中的PATH配置,重新source |
arm-none-eabi-gcc: No such file or directory |
32位库缺失(64位系统) | sudo apt install libc6-i386 lib32stdc++6 lib32gcc-s1 |
undefined reference to `main' |
链接时没找到入口函数 | 检查是否写了main函数,或者用-e main指定入口 |
selected processor does not support `dmb' |
CPU型号选错 | 检查-mcpu参数,Cortex-M0不支持DMB指令 |
嗯,这一章的内容就到这里。交叉编译环境搭建好了,下一章我们就可以开始写真正的呼吸机启动代码了。到时候你会看到,这些基础工作做得越扎实,后面调试起来就越顺手。
记住一句话:工具链是嵌入式开发的基石。基石不稳,后面全是坑。花点时间把这一步走稳,绝对值。