jklincn


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 目标

1
add_executable(${CMAKE_PROJECT_NAME})

在这加入了 stm32cubemx 的 CMake 配置。

1
add_subdirectory(cmake/stm32cubemx)

剩下的就是提前生成的用户自定义区域,可以添加自己的源文件、头文件路径、链接路径等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Link directories setup
target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE
    # Add user defined library search paths
)

# Add sources to executable
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
    # Add user sources here
)

# Add include paths
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
    # Add user defined include paths
)

# Add project symbols (macros)
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE
    # Add user defined symbols
)

有一个特定的 CMake 问题修复,在使用某些版本的 GCC 交叉工具链时,编译器可能会输出类似 -lob,CMake 就会认为要去链接 ob 这个库,但这个库实际并不存在,因此进行手动的去除。

1
2
# Remove wrong libob.a library dependency when using cpp files
list(REMOVE_ITEM CMAKE_C_IMPLICIT_LINK_LIBRARIES ob)

最后将 stm32cubemx 库链接到 test1 上(这里其实是一个配置应用,详见下文)

1
2
3
4
5
6
# Add linked libraries
target_link_libraries(${CMAKE_PROJECT_NAME}
    stm32cubemx

    # Add user defined libraries
)

从文件开头的注释中可以知道,这个文件只在初始时生成一次,后续不会进行覆盖,因此这个文件是可以后续由用户自由修改的。

cmake/stm32cubemx/CMakeLists.txt

stm32cubemx 配置文件。

创建 stm32cubemx 接口库(配置了头文件路径和宏定义,用于其他目标直接复制)

1
2
3
add_library(stm32cubemx INTERFACE)
target_include_directories(stm32cubemx INTERFACE ${MX_Include_Dirs})
target_compile_definitions(stm32cubemx INTERFACE ${MX_Defines_Syms})

创建 STM32_Drivers 驱动库(静态链接文件)

1
2
3
4
# Create STM32_Drivers static library
add_library(STM32_Drivers OBJECT)
target_sources(STM32_Drivers PRIVATE ${STM32_Drivers_Src})
target_link_libraries(STM32_Drivers PUBLIC stm32cubemx)

为 test1 目标加入应用代码源文件、链接路径、链接库(驱动和工具链)

1
2
3
4
5
6
7
8
# Add STM32CubeMX generated application sources to the project
target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${MX_Application_Src})

# Link directories setup
target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${MX_LINK_DIRS})

# Add libraries to the project
target_link_libraries(${CMAKE_PROJECT_NAME} ${MX_LINK_LIBS})

cmake/gcc-arm-none-eabi.cmake

CMake 工具链文件,定义了交叉编译相关的配置。

告诉 CMake 我们正在进行交叉编译

1
2
set(CMAKE_SYSTEM_NAME               Generic)
set(CMAKE_SYSTEM_PROCESSOR          arm)

指定编译器工具链

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Some default GCC settings
# arm-none-eabi- must be part of path environment
set(TOOLCHAIN_PREFIX                arm-none-eabi-)

set(CMAKE_C_COMPILER                ${TOOLCHAIN_PREFIX}gcc)
set(CMAKE_ASM_COMPILER              ${CMAKE_C_COMPILER})
set(CMAKE_CXX_COMPILER              ${TOOLCHAIN_PREFIX}g++)
set(CMAKE_LINKER                    ${TOOLCHAIN_PREFIX}g++)
set(CMAKE_OBJCOPY                   ${TOOLCHAIN_PREFIX}objcopy)
set(CMAKE_SIZE                      ${TOOLCHAIN_PREFIX}size)

定义输出文件后缀(修改为 .elf)

1
2
3
set(CMAKE_EXECUTABLE_SUFFIX_ASM     ".elf")
set(CMAKE_EXECUTABLE_SUFFIX_C       ".elf")
set(CMAKE_EXECUTABLE_SUFFIX_CXX     ".elf")

编译器优化设置

1
2
3
4
5
6
7
# 设置硬件架构标志
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${TARGET_FLAGS}")
set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS} -x assembler-with-cpp -MMD -MP")
# 把每个函数和每个变量都放在独立的段
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -fdata-sections -ffunction-sections")
...
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -fno-rtti -fno-exceptions -fno-threadsafe-statics")

调试与发布设置

1
2
3
4
set(CMAKE_C_FLAGS_DEBUG "-O0 -g3")
set(CMAKE_C_FLAGS_RELEASE "-Os -g0")
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g3")
set(CMAKE_CXX_FLAGS_RELEASE "-Os -g0")

链接器优化设置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 设置硬件架构标志
set(CMAKE_EXE_LINKER_FLAGS "${TARGET_FLAGS}")
# 指定链接脚本
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -T \"${CMAKE_SOURCE_DIR}/STM32F103XX_FLASH.ld\"")
# 使用 Newlib-nano 库
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --specs=nano.specs")
# 生成映射文件,执行垃圾回收
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Map=${CMAKE_PROJECT_NAME}.map -Wl,--gc-sections")
# 打印 Flash 和 RAM 占用情况
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--print-memory-usage")
# 链接数学库
set(TOOLCHAIN_LINK_LIBRARIES "m")

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 。


本站不记录浏览量,但如果您觉得本内容有帮助,请点个小红心,让我知道您的喜欢。