作为一名在嵌入式领域摸爬滚打多年的开发者,我深知构建系统对于 STM32 项目的重要性。记得我第一次尝试在 Linux 下搭建 STM32 开发环境时,光是让 CMake 正确识别交叉编译工具链就花了整整两天时间。本文将带你深入理解如何从零构建一个完整的 STM32 构建系统,避开那些我曾经踩过的坑。
在嵌入式开发中,Makefile 曾经是主流选择,但随着项目复杂度提升,CMake 的优势愈发明显:
对于 STM32 项目,CMake 能完美处理:
让我们从最基础的 CMake 配置开始:
cmake复制cmake_minimum_required(VERSION 3.20)
project(STM32F103C8T6_Project C CXX ASM)
这里指定了 CMake 最低版本要求,并声明项目支持 C、C++ 和汇编三种语言。接下来是关键的交叉编译设置:
cmake复制set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR ARM)
Generic 系统类型告诉 CMake 这是一个裸机项目,不要尝试查找标准库头文件。我曾经错误地设置为 Linux,结果 CMake 疯狂报找不到头文件的错误。
工具链配置是核心中的核心:
cmake复制set(CROSS_COMPILE arm-none-eabi-)
set(CMAKE_C_COMPILER ${CROSS_COMPILE}gcc)
set(CMAKE_CXX_COMPILER ${CROSS_COMPILE}g++)
set(CMAKE_ASM_COMPILER ${CROSS_COMPILE}gcc)
set(CMAKE_OBJCOPY ${CROSS_COMPILE}objcopy)
set(CMAKE_SIZE ${CROSS_COMPILE}size)
这里有个实用技巧:在终端执行 arm-none-eabi-gcc --version 确认工具链已正确安装。如果报错,可能需要将工具链路径加入 PATH 环境变量。
裸机环境与常规系统最大的区别在于:
cmake复制set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
这个设置解决了 CMake 配置阶段的一个关键问题:默认情况下 CMake 会尝试编译并运行测试程序,但在交叉编译环境中,生成的 ARM 二进制无法在开发机上运行。设置为静态库后,CMake 只编译不运行,完美避开了这个问题。
另一个实用配置是:
cmake复制set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
这会生成 compile_commands.json 文件,供 clangd 等语言服务器使用,实现精准的代码补全和跳转。没有这个文件,你的 IDE 会显示大量虚假错误。
STM32 项目需要两个关键启动文件:
cmake复制file(GLOB STARTUP_SRC
${STM32_CMSIS_ROOT}/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103xb.s
)
list(APPEND STARTUP_SRC
${STM32_CMSIS_ROOT}/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c
)
特别注意:STM32F103C8T6 属于 medium-density 系列,对应的启动文件是 startup_stm32f103xb.s。我曾经错误使用 x8 后缀的文件,导致链接阶段出现各种奇怪问题。
HAL 库源文件需要特别注意:
cmake复制file(GLOB HAL_SRC
${STM32_HAL_DRIVER_ROOT}/Src/*.c
)
list(FILTER HAL_SRC EXCLUDE REGEX ".*_template\\.c$")
这里使用正则表达式过滤掉所有 _template.c 文件。这些模板文件包含默认实现,如果一起编译会导致函数重复定义。我第一次构建时就遇到了 HAL_InitTick 的多重定义错误,花了半天才找到原因。
cmake复制add_compile_options(
-mcpu=cortex-m3
-mthumb
-O2
-g3
-Wall
-Wextra
-ffunction-sections
-fdata-sections
)
关键选项说明:
-mthumb:使用 Thumb 指令集,代码密度提高约30%-ffunction-sections:每个函数独立段,便于链接时优化-fdata-sections:每个数据独立段,减少最终固件大小实测表明,开启这些选项后,典型项目的代码体积可减小15-20%,对于Flash有限的STM32F103C8T6尤为重要。
C和C++需要不同的编译选项:
cmake复制add_compile_options(
"$<$<COMPILE_LANGUAGE:C>:-std=c11>"
"$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>"
"$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>"
"$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>"
)
这里使用了 CMake 的 generator expression 来区分语言。特别提醒:裸机环境下必须禁用异常和RTTI,否则会引入大量运行时开销。
我曾经忘记加 -fno-exceptions,结果简单的 try-catch 就使固件增大了近10KB。
cmake复制add_link_options(
-mcpu=cortex-m3
-mthumb
-nostartfiles
-specs=nano.specs
-specs=nosys.specs
-Wl,--gc-sections
-Wl,-Map=${CMAKE_BINARY_DIR}/output.map
-T${PROJECT_ROOT}/ld/STM32F103XB_FLASH.ld
)
关键选项解析:
-nostartfiles:使用自定义启动文件而非标准库的-specs=nano.specs:使用精简版C库(newlib-nano)-specs=nosys.specs:禁用系统调用存根--gc-sections:删除未使用的代码段output.map 文件对调试非常有用,可以查看各段的精确分布情况。
STM32F103C8T6 的典型链接脚本包含:
ld复制MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS {
.isr_vector : {
KEEP(*(.isr_vector))
} > FLASH
.text : {
*(.text*)
*(.rodata*)
} > FLASH
.data : {
*(.data*)
} > RAM AT > FLASH
}
特别注意:
.isr_vector 必须用 KEEP 保留,否则可能被优化掉.data 段的 AT > FLASH 表示初始值存储在Flash中我曾经忘记 KEEP 导致程序无法启动,调试了整整一天才发现是中断向量表被优化掉了。
cmake复制add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${PROJECT_NAME}> ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.bin
COMMENT "Generating ${PROJECT_NAME}.bin"
)
这个步骤将ELF转换为原始二进制格式,适合直接烧录。同时建议添加固件大小检查:
cmake复制add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${PROJECT_NAME}>
COMMENT "Firmware size:"
)
创建方便的烧录命令:
cmake复制add_custom_target(flash
COMMAND ${PROJECT_ROOT}/scripts/flash.sh ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.bin
DEPENDS ${PROJECT_NAME}
COMMENT "Flashing firmware to STM32..."
)
对应的 flash.sh 脚本示例:
bash复制#!/bin/bash
st-flash write $1 0x08000000
启动文件未找到:
startup_stm32f103xb.sHAL库多重定义:
_template.c 文件链接错误:
利用map文件:
优化策略:
-Os 替代 -O2 可进一步减小代码体积__attribute__((section())) 控制代码布局内存泄漏检测:
通过CMake选项支持不同型号:
cmake复制option(STM32_F1 "Build for STM32F1 series" ON)
option(STM32_F4 "Build for STM32F4 series" OFF)
if(STM32_F1)
set(MCU_TYPE STM32F103xB)
set(STARTUP_FILE startup_stm32f103xb.s)
elseif(STM32_F4)
set(MCU_TYPE STM32F407xx)
set(STARTUP_FILE startup_stm32f407xx.s)
endif()
虽然裸机环境难以运行测试,但可以:
cmake复制if(ENABLE_TESTING)
add_subdirectory(tests)
endif()
示例GitLab CI配置:
yaml复制stm32-build:
image: ubuntu:20.04
script:
- apt-get update && apt-get install -y gcc-arm-none-eabi cmake make
- mkdir build && cd build
- cmake .. -DCMAKE_BUILD_TYPE=Release
- cmake --build .
-ffunction-sections 和 -fdata-sections 配合 --gc-sections-Os 优化级别实测案例:通过上述优化,一个简单的GPIO控制项目从28KB降到了12KB。
__attribute__((section(".fast_code")))-O3 优化关键模块优化前后对比:
版本控制:
文档:
依赖管理:
持续集成:
通过这套CMake构建系统,我们实现了:
记住,好的构建系统应该像优秀的助手一样,默默工作而不引人注意。当你不再为构建问题分心时,才能专注于真正的嵌入式开发工作。