Stm32F103 + Stm32CubeMX(二):CubeMX 生成代码分析
之前文章《使用 VSCode 进行 Stm32 开发环境配置》中使用 STM32CubeMX 生成代码后得到了这些文件:
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2025/12/17 15:02 cmake
d----- 2025/12/17 15:02 Core
d----- 2025/12/17 15:02 Drivers
-a---- 2025/12/17 15:02 6421 .mxproject
-a---- 2025/12/17 15:02 1584 CMakeLists.txt
-a---- 2025/12/17 15:02 906 CMakePresets.json
-a---- 2025/12/17 15:02 12022 startup_stm32f103xe.s
-a---- 2025/12/17 15:02 8376 STM32F103XX_FLASH.ld
-a---- 2025/12/17 15:02 3287 test1.ioc
本篇文章会详细分析一下这些文件的用处。
STM32CubeMX 工程文件
.mxproject
STM32CubeMX 的内部配置文件,这个文件主要是记录项目状态,充当 STM32CubeMX 图形化配置工具与实际生成的代码文件之间的“记账本”。当在 CubeMX 中点击“Generate Code”时,它会读取和更新这个文件,以确保代码生成的增量更新是正确的,避免覆盖用户自己编写的代码。
一般来说不太需要关注这个文件。
test1.ioc
STM32CubeMX 的核心配置文件,这里包括芯片的基础信息、引脚配置、时钟配置、项目管理设置等等重要信息。
在 STM32CubeMX 图形化操作后,每一处修改都会保存到这个文件中。可以看到之前对 PB5 和 PE5 的操作:
PB5.GPIOParameters=GPIO_Label
PB5.Locked=true
PB5.Signal=GPIO_Output
...
PE5.GPIOParameters=GPIO_Label
PE5.Locked=true
PE5.Signal=GPIO_Output
一般情况下不需要手动修改这个文件。
CMake 工程文件
CMakeLists.txt
用户配置文件,定义了项目名称、C++标准等基础信息。
创建 test1 目标
|
|
在这加入了 stm32cubemx 的 CMake 配置。
|
|
剩下的就是提前生成的用户自定义区域,可以添加自己的源文件、头文件路径、链接路径等。
|
|
有一个特定的 CMake 问题修复,在使用某些版本的 GCC 交叉工具链时,编译器可能会输出类似 -lob,CMake 就会认为要去链接 ob 这个库,但这个库实际并不存在,因此进行手动的去除。
|
|
最后将 stm32cubemx 库链接到 test1 上(这里其实是一个配置应用,详见下文)
|
|
从文件开头的注释中可以知道,这个文件只在初始时生成一次,后续不会进行覆盖,因此这个文件是可以后续由用户自由修改的。
cmake/stm32cubemx/CMakeLists.txt
stm32cubemx 配置文件。
创建 stm32cubemx 接口库(配置了头文件路径和宏定义,用于其他目标直接复制)
|
|
创建 STM32_Drivers 驱动库(静态链接文件)
|
|
为 test1 目标加入应用代码源文件、链接路径、链接库(驱动和工具链)
|
|
cmake/gcc-arm-none-eabi.cmake
CMake 工具链文件,定义了交叉编译相关的配置。
告诉 CMake 我们正在进行交叉编译
|
|
指定编译器工具链
|
|
定义输出文件后缀(修改为 .elf)
|
|
编译器优化设置
|
|
调试与发布设置
|
|
链接器优化设置
|
|
CMakePresets.json
预设文件,旨在标准化和简化构建配置过程。
首先设置了基础模板(default):
- 生成器(Ninja)
- 构建产物存放目录(${sourceDir}/build/${presetName})
- 工具链文件路径(${sourceDir}/cmake/gcc-arm-none-eabi.cmake)
然后在此基础上有 Debug 和 Release 两个配置,仅在 CMAKE_BUILD_TYPE 设定有区别。
链接脚本(STM32F103XX_FLASH.ld)
从注释可以看到是针对 STM32F103ZETx 系列(512KB FLASH / 64KB RAM)生成的链接脚本,设置了堆、栈大小以及栈位置等。
首先定义了入口点,程序启动后执行的第一行代码是 Reset_Handler(在汇编启动文件中)
ENTRY(Reset_Handler)
然后确定了内存布局
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K
}
- RAM:起始地址 0x20000000,大小 64KB。属性是 xrw(可执行、可读、可写)。
- FLASH:起始地址 0x08000000,大小 512KB。属性是 rx(可读、可执行)。
堆和栈的配置
_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of RAM */
_Min_Heap_Size = 0x200; /* required amount of heap */
_Min_Stack_Size = 0x400; /* required amount of stack */
段定义,其中存放 FLASH 中的内容有:
- 中断向量表 (
.isr_vector) - 程序代码 (
.text) - 只读常量 (
.rodata) - 系统初始化表 (
.preinit_array,.init_array,.fini_array) - 异常处理表 (
.ARM.extab,.ARM) - 已初始化变量的“备份值”
存放 RAM 中的内容有:
- 已初始化全局变量 (
.data) - 未初始化全局变量 (
.bss) - 线程局部存储 (
.tdata,.tbss) - 堆 (
Heap) - 栈 (
Stack)
堆栈空间检查
/* User_heap_stack section, used to check that there is enough RAM left */
._user_heap_stack (NOLOAD) :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
最后优化 FLASH 空间,将标准库中没有用到的内容进行丢弃
/* Remove information from the standard libraries */
/DISCARD/ :
{
libc.a:* ( * )
libm.a:* ( * )
libgcc.a:* ( * )
}
启动文件(startup_stm32f103xe.s)
定义了 Reset_Handler 函数,这是上电后执行的第一段代码。
复位处理函数
初始化系统时钟,定义在 Core\Src\system_stm32f1xx.c 中
bl SystemInit
搬运已初始化数据
/* Copy the data segment initializers from flash to SRAM */
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
movs r3, #0
b LoopCopyDataInit
CopyDataInit:
ldr r4, [r2, r3]
str r4, [r0, r3]
adds r3, r3, #4
LoopCopyDataInit:
adds r4, r0, r3
cmp r4, r1
bcc CopyDataInit
清零未初始化数据
/* Zero fill the bss segment. */
ldr r2, =_sbss
ldr r4, =_ebss
movs r3, #0
b LoopFillZerobss
FillZerobss:
str r3, [r2]
adds r2, r2, #4
LoopFillZerobss:
cmp r2, r4
bcc FillZerobss
调用 C 库初始化和 Main 函数
/* Call static constructors */
bl __libc_init_array
/* Call the application's entry point.*/
bl main
bx lr
默认中断处理
如果发生了某个中断(比如定时器中断),但您忘记在 C 代码中写对应的中断服务函数(如 TIM2_IRQHandler),链接器就会默认链接到这个死循环。
对于调试来说,如果程序莫名其妙卡死不动,暂停调试器发现停在这里,说明触发了一个未处理的中断。
.section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
b Infinite_Loop
.size Default_Handler, .-Default_Handler
中断向量表
.section .isr_vector,"a",%progbits
.type g_pfnVectors, %object
.size g_pfnVectors, .-g_pfnVectors
g_pfnVectors:
.word _estack /* 0x00: 初始栈指针 (SP) */
.word Reset_Handler /* 0x04: 复位向量 (PC) */
.word NMI_Handler /* 0x08: 不可屏蔽中断 */
.word HardFault_Handler /* 0x0C: 硬件错误 */
...
弱定义命名
以 EXTI0_IRQHandler 为例
.weak EXTI0_IRQHandler
.thumb_set EXTI0_IRQHandler,Default_Handler
如果用户在 C 代码中写了一个同名的函数 void EXTI0_IRQHandler(void) {...},链接器就会使用用户的函数;如果用户没写,链接器就使用这里指定的后备函数 Default_Handler。
驱动代码
这部分代码在 Drivers 文件夹中,分为 CMSIS 和 STM32F1xx_HAL_Driver
CMSIS
CMSIS (Cortex Microcontroller Software Interface Standard) 是 ARM 公司和 ST 公司共同定义的标准接口,它的作用是屏蔽不同 Cortex-M 内核和不同厂家芯片之间的差异。
核心层(CMSIS/include)
这里的文件是 ARM 公司提供的,与具体芯片无关,只与 CPU 内核(Cortex-M3)有关。
设备层(CMSIS/Device/ST/STM32F1xx/Include)
这里的文件是 ST 公司提供的,专门针对 STM32F1 系列芯片。
- stm32f103xe.h: 芯片头文件。它定义了 STM32F103ZE 所有外设的寄存器映射。
- stm32f1xx.h: 总入口头文件。它会根据宏
STM32F103xE自动包含上面的stm32f103xe.h。 - system_stm32f1xx.h: 声明了
SystemInit()函数和SystemCoreClock变量(记录当前主频)。
STM32F1xx_HAL_Driver
ST 公司提供的高级驱动库,它的作用是提供易于使用的 C 函数来操作外设。
核心文件
- stm32f1xx_hal.c/.h: HAL 库的初始化核心。包含
HAL_Init()、HAL_Delay()等基础功能。 - stm32f1xx_hal_def.h: 定义了通用的枚举类型,如
HAL_StatusTypeDef(HAL_OK, HAL_ERROR)。
外设驱动
每个外设都有对应的一对文件,例如 GPIO:stm32f1xx_hal_gpio.c / stm32f1xx_hal_gpio.h
调用关系
以点亮 LED 为例,分别是用户代码、HAL 库、CMSIS 设备层、CMSIS 核心层、硬件寄存器。
[ 用户代码 (main.c) ]
|
v
[ HAL 库 (stm32f1xx_hal_gpio.c) ] <-- 提供 HAL_GPIO_WritePin()
|
v
[ CMSIS 设备层 (stm32f103xe.h) ] <-- 提供 GPIOA->ODR 寄存器定义
|
v
[ CMSIS 核心层 (core_cm3.h) ] <-- 提供 NVIC 中断控制
|
v
[ 硬件寄存器 (0x4001080C) ] <-- 最终改变电压高低
用户代码
这部分代码在 Core 文件夹中。
主程序
main.c 中会调用 HAL_Init() 初始化库,调用 SystemClock_Config() 配置时钟,初始化各种外设,最终进入 while(1) 的死循环(业务逻辑)。
系统时钟管理
system_stm32f1xx.c 提供了两个函数和一个全局变量:
- SystemInit() 函数:初始化系统时钟的最底层配置。包括选择时钟源、设置 PLL 倍频系数、设置 AHB/APB 总线的分频系数,以及 Flash 的延时设置。在进入 main() 之前就会被调用。
- SystemCoreClock 变量:存储了当前 CPU 的核心频率(HCLK),单位是 Hz。
- SystemCoreClockUpdate() 函数:更新上面的 SystemCoreClock 变量。
负责在进入 main() 之前就把时钟配置好,并提供一个变量用于当前 CPU 核心频率的查询。
中断处理程序
stm32f1xx_it.c 是存放中断处理函数的地方,如果在 CubeMX 启用了更多的外设,比如定时器中断,那么这个文件也会自动更新将其中断处理函数添加进来。
但仅仅是模板,具体实现需要自己编写。
MCU 支持包
stm32f1xx_hal_msp.c 中的 msp 代表 MCU Support Package,用于支持 MCU 外设的底层初始化代码。其中 HAL_MspInit() 函数会由 HAL_Init() 调用,来完成更底层的硬件初始化。
系统调用接口适配
syscalls.c 和 sysmem.c 是连接标准 C 库(Newlib)与特定硬件的桥梁。因为在裸机环境下没有 Linux 内核,所以需要我们手动实现底层系统调用。
比如 Newlib 提供了 printf 等标准函数,底层会调用 _write,我们可以自己编写 _write 去控制这个 printf 是发送给 UART 还是 LCD 屏幕,类似编写 Linux 内核的 syscall 。
本站不记录浏览量,但如果您觉得本内容有帮助,请点个小红心,让我知道您的喜欢。