1、自动化基石:嵌入式开发环境搭建与工具链配置(GCC、Makefile、CMake)

说实话,我见过太多工程师在开发环境上栽跟头了。明明代码逻辑没问题,编译就是过不去。最后发现,是工具链没配好。嵌入式开发的第一步,就是把你的「武器库」收拾利索。今天我们就聊聊 GCC、Makefile 和 CMake 这三样东西。

1.1 为什么非得自己搭环境?

你想想看,嵌入式开发不像写个 Python 脚本。你的代码最终要跑在单片机或者 ARM 芯片上。那芯片的指令集跟你的电脑不一样。所以你需要一个「翻译官」——交叉编译器。

IDE 虽然方便,但隐藏了太多细节。我在项目中遇到过好几次,IDE 升级后,原来的工程就编译不过了。你根本不知道它背后调了哪些参数。自己搭环境,说白了就是掌握主动权。

1.2 GCC:编译器界的瑞士军刀

GCC(GNU Compiler Collection)是嵌入式开发的核心。它能把 C/C++ 代码变成机器码。我个人习惯用 arm-none-eabi-gcc 这个版本,专门针对 ARM Cortex-M 系列芯片。

安装其实不复杂。以 Ubuntu 为例:

sudo apt-get update
sudo apt-get install gcc-arm-none-eabi

装完验证一下:

arm-none-eabi-gcc --version

看到版本号就说明成了。嗯,这里要注意:Windows 用户建议用 MSYS2 或者 WSL。别在 Windows 原生 cmd 里折腾,坑太多。

避坑指南: 我曾经在 Windows 下用 Cygwin 装 GCC,结果路径分隔符的问题搞了我一整天。后来改用 WSL,十分钟搞定。建议新手直接上 Linux 虚拟机或者 WSL。

1.3 Makefile:自动化编译的基石

有了编译器,你总不能每次都手动敲一长串命令吧?

arm-none-eabi-gcc -c main.c -o main.o
arm-none-eabi-gcc -c uart.c -o uart.o
arm-none-eabi-gcc main.o uart.o -o firmware.elf

项目小还好说。一旦有几十个源文件,手动编译会疯掉的。Makefile 就是来解决这个问题的。

一个最简单的 Makefile 长这样:

CC = arm-none-eabi-gcc
CFLAGS = -Wall -O2 -mcpu=cortex-m4

firmware.elf: main.o uart.o
    $(CC) $^ -o $@

main.o: main.c
    $(CC) $(CFLAGS) -c $< -o $@

uart.o: uart.c
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    rm -f *.o firmware.elf

这里有几个关键点:

  • $@ 表示目标文件
  • $^ 表示所有依赖文件
  • $< 表示第一个依赖文件

你只要在终端敲 make,它就会自动检查哪些文件改了,只重新编译改过的文件。这就是增量编译,省时间。

我的习惯: 我会在 Makefile 里加一个 flash 目标,直接调用 OpenOCD 或者 JLink 烧录。这样一条 make flash 就搞定编译+烧录,省心。

1.4 CMake:跨平台的构建系统

Makefile 虽然好用,但跨平台就麻烦了。Linux 用一套路径,Windows 用另一套。这时候 CMake 就派上用场了。

CMake 不直接编译代码。它先生成 Makefile(或者 Ninja 文件),然后再调用 make 去编译。说白了,它是个「构建系统的构建系统」。

一个典型的 CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(Firmware)

set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)

set(CMAKE_C_FLAGS "-Wall -O2 -mcpu=cortex-m4")

add_executable(firmware.elf
    main.c
    uart.c
    gpio.c
)

target_include_directories(firmware.elf PRIVATE
    inc
)

使用方式:

mkdir build
cd build
cmake ..
make

为什么要先建一个 build 目录?这叫「外部构建」。所有编译产生的中间文件都在 build 里,不会污染源码目录。我刚开始用 CMake 时没注意这点,源码目录乱成一锅粥。

1.5 三者的配合关系

我画个简单的对应关系:

工具 作用 适用场景
GCC 编译代码成机器码 所有嵌入式项目
Makefile 自动化编译流程 中小型项目,Linux 环境
CMake 跨平台生成构建文件 大型项目,需要跨平台

实际项目中,我通常这样用:

  • 个人小项目:直接手写 Makefile
  • 团队协作:用 CMake + Makefile 组合
  • 商业产品:CMake + Ninja(比 Make 更快)
核心思路: 工具链的配置不是一次性的。每次换芯片、换编译器版本,都要重新验证。我建议把整个工具链的安装步骤写成脚本,放到 Git 仓库里。新同事来了,跑一遍脚本就完事。

1.6 实战:从零搭建一个 STM32 项目

光说不练假把式。我们动手搭一个完整的 STM32F4 项目。

第一步:准备启动文件和链接脚本。这些一般芯片厂商会提供。STM32 的话,从 ST 官网下载 HAL 库,里面就有。

第二步:写 CMakeLists.txt:

cmake_minimum_required(VERSION 3.15)
project(STM32F4_Demo)

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)

# 芯片型号
set(MCU_FLAGS "-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16")

set(CMAKE_C_FLAGS "${MCU_FLAGS} -Wall -O2 -g")

# 源文件
file(GLOB SOURCES
    "src/*.c"
    "startup/startup_stm32f407xx.s"
)

add_executable(${PROJECT_NAME}.elf ${SOURCES})

# 链接脚本
target_link_options(${PROJECT_NAME}.elf PRIVATE
    -T "STM32F407VGTx_FLASH.ld"
)

# 生成 hex 和 bin
add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
    COMMAND ${CMAKE_OBJCOPY} -O ihex ${PROJECT_NAME}.elf ${PROJECT_NAME}.hex
    COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin
)

第三步:编译:

mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/gcc-arm-none-eabi.cmake
make

看到生成了 .hex 和 .bin 文件,就说明环境搭好了。

注意: 链接脚本(.ld 文件)一定要跟你的芯片型号匹配。我见过有人把 STM32F103 的链接脚本用在 F407 上,结果程序跑起来就死机。因为 Flash 和 RAM 的地址范围完全不一样。

1.7 总结

环境搭建这东西,说白了就是一次配置,长期受益。你花半天时间把工具链理清楚,后面写代码、调试、烧录都会顺畅很多。

我个人建议:别急着写业务代码。先把 Makefile 或者 CMake 的模板准备好。以后每个新项目,直接复制模板,改改芯片型号和源文件列表就行。这才是自动化该有的样子。

下一章,我们会聊聊版本控制。Git 在嵌入式开发中怎么用?分支策略怎么定?到时候再细说。