MCU进阶篇(二)——Linux搭建STM32基础工程
MCU进阶篇(二)——Linux搭建STM32基础工程
前言:
本篇在 Linux 下从零搭建一个 STM32F103ZET6 的裸机工程:用 arm-none-eabi-gcc 编译,用 CMake 管理工程,最后通过 GPIO 点亮 M144Z-M3 开发板上的 LED0 和 LED1。工程按 core、drv、strtup、lnk、cmake 分层组织,先不引入 RTOS,先做一个纯裸机工程。固件的下载和调试不在本文范围内,可以参考《OpenOCD 基础篇(二)——烧录和调试》。
1 目标与硬件
1.1 最终目标
完成这篇文章后,应该得到这样一个工程:
- 在 Linux 终端里一条命令完成编译,生成
.elf、.hex、.bin。 - 工程按
core、drv、strtup、lnk、cmake分层组织,结构清晰,便于后续扩展。 - 板载 LED0 每 500 ms 翻转一次,LED1 每 1000 ms 翻转一次。
本文不依赖 Keil、IAR 或 STM32CubeIDE,编译和链接全部在 Linux 命令行里完成。第一次先把最小工程跑通,后面再往里面加 UART、定时器、中断和 FreeRTOS。
1.2 开发板与芯片
本文使用正点原子 M144Z-M3 最小系统板 STM32F103 版:
| 项目 | 说明 |
|---|---|
| MCU | STM32F103ZET6,Cortex-M3,512 KB Flash,64 KB SRAM,最高 72 MHz |
| 外部高速晶振 | 8 MHz HSE |
| 外部低速晶振 | 32.768 kHz LSE |
| LED0 | PB5,红色,低电平点亮 |
| LED1 | PE5,绿色,低电平点亮 |
| USB 串口 | CH340,连接 USART1 的 PA9/PA10 |
如果你用的是其他 STM32F103 板子,只需要把 LED 引脚、HSE 频率、Flash/SRAM 大小和启动文件换成自己的,后面的步骤不变。
1.3 参考资料
本文需要两类资料:
- STM32CubeF1 固件包:包含 CMSIS、HAL 库、启动文件和官方模板工程。可以在 ST 官网的 STM32CubeF1 页面 下载。
- M144Z-M3 开发板资料:随开发板提供,包含使用指南和硬件参考手册。本文用到《M144Z-M3 最小系统板使用指南》第 10 章“跑马灯实验”和《M144Z-M3 最小系统板硬件参考手册》第 2.1.14 节 LED 原理图。
寄存器细节可以参考 ST 官方的 RM0008 参考手册。
如果你用的不是 M144Z-M3,只需要把 LED 引脚、HSE 频率、Flash/SRAM 大小和启动文件换成自己板子的参数,后文会指出需要修改的位置。如果你没有开发板资料,本文前面的表格已经给出了用到的 LED 引脚;换板子时以自己板子的原理图为准。
本文的工程目录和 CMake 脚本都按下面的步骤从零创建。
2 Linux 环境准备
2.1 安装工具链
以 Ubuntu/Debian 为例,安装交叉编译器和构建工具:
1 | sudo apt update |
安装完成后确认版本:
1 | arm-none-eabi-gcc --version |
不同发行版的包名略有差异。如果 gcc-arm-none-eabi 版本较旧也没关系,Cortex-M3 的编译不受影响。
2.2 解压官方固件包
从 ST 官网下载 STM32CubeF1 后,把压缩包放到 ~/work/stm32f103-src 并解压。下面假设压缩包名为 stm32cubef1.zip,解压后的目录为 STM32Cube_FW_F1_V1.8.0;如果你下载的版本或文件名不同,按实际名称修改:
1 | mkdir -p ~/work/stm32f103-src |
如果你已经解压过,直接把 CUBEF1 指向解压后的根目录即可,例如:
1 | export CUBEF1=~/work/stm32f103-src/STM32Cube_FW_F1_V1.8.0 |
export 只在当前终端有效,换终端后需要重新执行,或者把它写进 ~/.bashrc / ~/.zshrc。
确认关键文件存在:
1 | ls $CUBEF1/Drivers/STM32F1xx_HAL_Driver/Src |
3 工程目录设计
3.1 目录结构
本文把工程放在 ~/work/stm32f103-basic;如果你的工作目录不同,后面命令里的路径相应替换。工程结构如下:
1 | stm32f103-basic/ |
3.2 各目录的职责
工程目录的职责划分如下:
| 目录 | 作用 | 是否修改 |
|---|---|---|
cmake/ |
交叉编译工具链配置 | 自己维护 |
core/ |
应用代码、时钟、中断、HAL 配置、系统文件 | 主要修改区域 |
drv/ |
ST 官方 CMSIS 和 HAL 驱动 | 不修改,整体替换升级 |
strtup/ |
启动汇编和中断向量表 | 一般不改 |
lnk/ |
链接脚本,定义 Flash/SRAM 和段布局 | 换芯片时修改 |
build/ |
CMake 构建输出 | 不提交到 Git |
本文先直接调用 HAL,不引入额外的封装;等 LED 跑通、工程骨架稳定后,再按需扩展。
3.3 创建目录
执行:
1 | mkdir -p ~/work/stm32f103-basic |
4 从官方固件包复制文件
4.1 复制 CMSIS 和 HAL
先把 ST 官方驱动整体复制到 drv。这部分代码不要修改,后续升级 CubeF1 时直接整体替换:
1 | cd ~/work/stm32f103-basic |
复制完成后,drv/stm32/f1xx 下应该有:
1 | drv/stm32/f1xx/ |
4.2 复制启动文件、系统文件和模板
启动文件必须和芯片容量匹配。STM32F103ZET6 是大容量产品,选 startup_stm32f103xe.s;如果选成 startup_stm32f103xb.s,中断向量表会不完整。
1 | cd ~/work/stm32f103-basic |
注意这里没有复制 Nucleo 模板的 main.h。那个文件包含 stm32f1xx_nucleo.h,会依赖 Nucleo 板级 BSP。本文自己写一个最小的 main.h,只包含 HAL 头文件。
不同版本的 CubeF1 目录结构可能略有差异。如果上面的某个路径不存在,可以用 find 按文件名定位,例如:
1 | find "$CUBEF1" -name syscalls.c |
链接脚本需要按 STM32F103ZET6 的 512 KB Flash / 64 KB RAM 配置。最方便的做法是用 STM32CubeMX 新建一个(或使用你已有的)STM32F103ZETx 工程,只配置时钟和 GPIO 即可。生成代码后找到链接脚本:
1 | 把 CUBEMX_PROJ 换成你的 CubeMX 工程目录。 |
find 可能会列出多个文件,选名字里带 FLASH 的那个。把找到的文件复制到本工程(下面以 STM32F103XX_FLASH.ld 为例,文件名不同时替换):
1 | cp "$CUBEMX_PROJ/STM32F103XX_FLASH.ld" lnk/stm32/f103xx/ |
复制后检查文件里的 MEMORY 段,确认 FLASH 的 LENGTH = 512K、RAM 的 LENGTH = 64K。如果手头没有现成的 F103ZE 工程,用 CubeMX 新建一个只配置时钟和 GPIO 的工程,生成后复制它的链接脚本即可,不需要把 CubeMX 工程的其他文件带进来。
复制完成后检查一下:
1 | find core drv strtup lnk -maxdepth 4 -type f | sort | head -40 |
5 编写 CMake 工程
5.1 工具链文件
创建 cmake/gcc-arm-none-eabi.cmake:
1 | set(CMAKE_SYSTEM_NAME Generic) |
几个关键点:
-mcpu=cortex-m3 -mthumb必须和芯片内核一致。-T指向链接脚本,路径用CMAKE_CURRENT_LIST_DIR相对工具链文件计算,避免绝对路径。--specs=nano.specs使用 newlib-nano,减小代码体积。--specs=nosys.specs提供最基础的系统调用桩。-Wl,--gc-sections配合-ffunction-sections -fdata-sections删除未使用的函数和数据。-Wl,--print-memory-usage在链接结束时打印 Flash/RAM 占用。
5.2 根 CMakeLists.txt
创建根目录 CMakeLists.txt:
1 | cmake_minimum_required(VERSION 3.16) |
STM32F103xE 是 HAL/CMSIS 的器件宏,必须和 startup_stm32f103xe.s 对应。USE_HAL_DRIVER 告诉 CMSIS 头文件包含 HAL 配置。HSE_VALUE=8000000 和板载 8 MHz 晶振一致。
5.3 子目录 CMakeLists
core/CMakeLists.txt:
1 | file(GLOB CORE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c") |
drv/CMakeLists.txt:
1 | file(GLOB HAL_SOURCES |
strtup/CMakeLists.txt:
1 | target_sources(${PROJECT_NAME} PRIVATE |
这里用 file(GLOB) 把 HAL 的所有 .c 文件都加入编译。没有启用的 HAL 模块会编译成空文件,链接时再被 --gc-sections 删掉,第一次搭建工程时比较省事。后续如果在意编译速度,可以改成只列实际使用的源文件。
6 配置 HAL
6.1 启用需要的模块
打开 core/inc/stm32f1xx_hal_conf.h,只保留本文需要的模块:
1 |
其他模块(ADC、CAN、DMA、I2C、SPI、TIM、UART 等)先注释掉。本文只做 GPIO 点灯,用不到它们;后面加 UART 或定时器时再打开对应模块。
确认下面几个值与板子一致:
1 |
HSE_VALUE 也可以由 CMake 的 HSE_VALUE=8000000 编译宏提供,两处保持一致即可。
6.2 HAL_MspInit
core/src/stm32f1xx_hal_msp.c 里的 HAL_MspInit() 可以保持为空:
1 | void HAL_MspInit(void) |
如果需要提前打开 AFIO/PWR 时钟,可以写成:
1 | void HAL_MspInit(void) |
6.3 SysTick 中断
core/src/stm32f1xx_it.c 里的 SysTick_Handler() 负责给 HAL 提供 1 ms 时基:
1 | void SysTick_Handler(void) |
HAL_Init() 会配置 SysTick 为 1 ms 中断,HAL_RCC_ClockConfig() 在时钟切换后会按新的 HCLK 重新配置 SysTick。因此 HAL_Delay(500) 可以直接使用。如果后面要把 SysTick 交给 FreeRTOS,就需要重新处理这个中断,那是另一篇文章的内容。
6.4 system 和 syscalls
system_stm32f1xx.c 由启动文件调用,负责 SystemInit() 和 SystemCoreClockUpdate(),本文不需要修改。syscalls.c 提供 newlib 的 _write、_read、_sbrk 等桩函数,当前点灯用不到,但后面加 printf 时可以直接在里面重定向到 UART。
本文复制的 CubeF1 syscalls.c 已经包含 _sbrk,一般不需要额外的 sysmem.c。如果你换用了其它精简版 syscalls.c,链接时报 undefined reference to _sbrk,再从 CubeMX 工程复制一个 sysmem.c 到 core/src/ 即可。
7 编写 main.c
7.1 最小 main.h
创建 core/inc/main.h:
1 |
|
7.2 时钟和 LED
创建 core/src/main.c:
1 |
|
这段代码的关键点:
- 板载 HSE 是 8 MHz,PLL 倍频 9 得到 72 MHz。
- 72 MHz 时 Flash 需要 2 个等待周期,所以用
FLASH_LATENCY_2。 - APB1 的最大频率是 36 MHz,所以要 2 分频;APB2 可以到 72 MHz,不分频。
- LED0 在 PB5,LED1 在 PE5,都是低电平点亮;初始化时先写高电平熄灭。
- 循环每 500 ms 执行一次:LED0 每次都翻转,所以亮 500 ms、灭 500 ms;LED1 每两次循环翻转一次,所以亮 1 s、灭 1 s。
如果希望两个 LED 的闪烁节奏不同,也可以各自用一个计数变量控制翻转次数,但第一次验证不建议把逻辑写复杂。
8 编译工程
8.1 配置和构建
在工程根目录执行:
1 | cd ~/work/stm32f103-basic |
如果系统没有 Ninja,去掉 -G Ninja,CMake 会使用默认的 Unix Makefiles:
1 | cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug |
构建成功后,build/ 下会生成:
1 | build/stm32f103-basic.elf |
其中 .elf 是带符号的完整映像,.hex 和 .bin 由它转换而来;.map 用于查看符号和内存布局,compile_commands.json 可以给 clangd 提供代码补全。
8.2 检查固件
用 file 和 size 确认文件类型和占用:
1 | file build/stm32f103-basic.elf |
预期 file 输出类似:
1 | build/stm32f103-basic.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped |
size 会输出 text、data、bss 三列。text + data 是占用 Flash 的大小,data + bss 是占用 SRAM 的大小。第一次点灯工程通常只有几 KB,离 512 KB Flash 和 64 KB RAM 还很远。
8.3 常见编译问题
| 现象 | 原因 | 处理 |
|---|---|---|
arm-none-eabi-gcc: command not found |
工具链未安装或不在 PATH | 重新安装并检查 which arm-none-eabi-gcc |
stm32f1xx_hal_conf.h: No such file |
core/inc 没加入头文件路径 |
检查 core/CMakeLists.txt |
stm32f1xx.h: No such file |
CMSIS 设备头文件路径不对 | 检查 drv/CMakeLists.txt |
undefined reference to _sbrk |
使用了 malloc/printf,但 syscalls.c 不含 _sbrk |
换用本文的 CubeF1 syscalls.c,或补一个 sysmem.c |
undefined reference to _write |
缺少 syscalls.c |
把 syscalls.c 加入 core/src/ |
region RAM overflowed |
链接脚本的 RAM 大小或栈/堆设置不对 | 检查链接脚本和 _Min_Heap_Size、_Min_Stack_Size |
| 启动文件报语法错误 | 选错了启动文件或工具链 | F103ZE 使用 startup_stm32f103xe.s 的 GCC 版本 |
cannot find -lnosys |
工具链不完整 | 安装 libnewlib-arm-none-eabi 或检查工具链 |
如果修改了 CMakeLists 或新增了源文件,重新执行一次 cmake -S . -B build,让 file(GLOB) 重新收集文件。
9 常见问题
9.1 编译和链接
| 现象 | 原因 | 处理 |
|---|---|---|
undefined reference to HAL_GPIO_Init |
HAL_GPIO_MODULE_ENABLED 没打开,或 HAL Src 没加入编译 |
检查 stm32f1xx_hal_conf.h 和 drv/CMakeLists.txt |
undefined reference to HAL_InitTick |
HAL_CORTEX_MODULE_ENABLED 没打开 |
打开 CORTEX 模块 |
FLASH_LATENCY_2 未定义 |
HAL_FLASH_MODULE_ENABLED 没打开 |
打开 FLASH 模块 |
multiple definition of SysTick_Handler |
启动文件、stm32f1xx_it.c 或其它文件重复定义 |
只保留 stm32f1xx_it.c 中的实现 |
multiple definition of _write |
syscalls.c 和应用层自定义的 _write 重复 |
只保留一个;要重定向输出时直接改 syscalls.c 里的 _write |
undefined reference to SystemInit |
system_stm32f1xx.c 没加入编译 |
检查 core/src 和 core/CMakeLists.txt |
undefined reference to g_pfnVectors |
启动文件没加入编译 | 检查 strtup/CMakeLists.txt |
.elf 生成但没有 .hex/.bin |
objcopy 路径不对 |
检查工具链文件里的 CMAKE_OBJCOPY |
9.2 运行现象
| 现象 | 排查方向 |
|---|---|
| 两个 LED 都不亮 | 检查 PB5/PE5 是否配置为输出、GPIOB/GPIOE 时钟是否使能、程序是否卡在 Error_Handler() |
| LED 常亮或常灭 | LED 是低电平点亮,确认初始化时写的是 GPIO_PIN_SET,循环里用的是 TogglePin |
| LED 闪烁频率明显不对 | 检查 HSE 是否为 8 MHz、PLL 是否为 x9、HSE_VALUE 是否为 8000000、Flash 等待周期是否为 2 |
| 程序一上电就 HardFault | 检查启动文件、链接脚本、时钟配置、栈大小和中断向量表 |
HAL_Delay() 卡死 |
SysTick_Handler() 没有调用 HAL_IncTick(),或中断被关闭 |
10 小结
本文从官方 STM32CubeF1 固件包出发,在 Linux 下搭建了一个最小 STM32F103ZET6 工程:
- 安装
arm-none-eabi-gcc和 CMake。 - 从 CubeF1 复制 CMSIS、HAL、启动文件、系统文件、中断模板和 HAL 配置。
- 按
core/drv/strtup/lnk/cmake分层组织工程。 - 编写 CMake 工具链和构建脚本,用 GCC 编译生成
.elf/.hex/.bin。 - 配置 HSE 8 MHz + PLL x9 = 72 MHz,初始化 PB5/PE5,点亮板载 LED。
本文只覆盖编译和链接,固件的下载和调试可以参考《OpenOCD 基础篇(二)——烧录和调试》。这条编译链路打通后,后面的 UART、定时器、中断、DMA、FreeRTOS 都可以在这个骨架上逐步添加。








