Main Takeaway
记录我的基于STM32Cube框架的第一个stm32程序从新建到编译上传全流程,感谢机协学长学姐的帮助!
流程梳理
- 通过STM32CubeMX选择芯片、配置外设,并生成代码
- 在选用的编辑器中编辑代码,编写程序
- 编译代码,生成二进制文件并上传
- 调试代码的运行效果
芯片配置
选择芯片/开发板
但是需要注意,若选择官方开发板作为硬件,则会提供“按照开发板上的电路及布局默认配置硬件”的选项
配置内核与外设硬件
系统配置:时钟配置+调试配置
时钟配置(必要):STM32有四个时钟源:LSI(低速内部时钟)、LSE(低速外部时钟)、HIS(高速内部时钟)和HSE(高速外部时钟);有的型号仅有其中某几个。默认的配置是采用芯片内部自带的振荡器、主频配置为8MHz。Nucleo F103RB中芯片外接了8MHz的晶振,故将“HSE”项配置为“Crystal/Ceramic Resonator”,这就是说使用高速外部时钟,且时钟源为晶振。具体频率则在时钟树界面中配置(在使用内部振荡器时F1系列芯片最高主频是64MHz,在使用8MHz外部晶振时最高主频是72MHz。)
调试配置:用于配置调试的类型(如果芯片未被配置为调试模式就强行对其进行调试,则会造成芯片内部启动程序错误而导致之后无法正常烧录程序,此时需擦除其Flash以修复)
主流的调试方式有SWD和JTAG两种,而SWD是更方便的方法。若要使用SWD进行调试,则必须将芯片配置为“Serial Wire”调试。
外设硬件:芯片中的所有外设都可以在软件中配置,包括DMA、ADC、定时器等
配置工程,生成代码
工程名称,路径
应用结构:
应用结构有“Basic”和“Advanced”两个选项。如果观察仔细的话,会发现STM32CubeIDE工程中的CubeMX项目默认的是“Advanced”结构,而单独使用CubeMX默认的是“Basic”结构
工具链/IDE:
STM32CubeMX除了配置硬件外,还提供了直接生成工程 的功能,即由该“工具链/IDE”选项确定。STM32CubeMX预先包含了7种 开发方式,分别是:EWARM、MDK-ARM、SW4STM32、TrueStudio、 STM32CubeIDE、Makefile和Other Toolchains(GPDSC)
*MDK-ARM*:Keil uVision采用的开发环境,使用此选项可以直接生成Keil完整工程。
*TrueStudio、STM32CubeIDE*:TrueStudio是STM32CubeIDE的前身。TrueStudio本身是以Eclipse为基础配置而成的STM32开发工具,在被ST收购被ST与STM32CubeMX合并,形成了STM32CubeIDE。故该两种选项生成的项目并无较大区别。对于使用Eclipse自行配置来为STM32开发的同学,也可以选择该选项。
*Makefile*:Makefile实际上是“make”这个程序为了实现自动化构建工程而读取的一种文件。make从Makefile文件中读取生成的对象、编译的顺序、文件的依赖等信息,然后使用gcc或者g++来编译。Makefile不仅适用于系统下的标准C/C++工程,也适用于嵌入式开发。为Arm架构芯片开发的gcc叫做“arm-none-eabi-gcc”,可以在GNU的官网上下载。PlatformIO使用gcc来编译STM32的程序,故采用Makefile选项更搭配。
但实际上PlatformIO自带一套使用Python来构建工程的方法,因此用不到Makefile;真正需要的只是“Inc”和“Src”两个文件夹中的代码而已。如果有同学有兴趣的话,可以尝试使用gcc搭配make自行搭建一套工具链,这个时候STM32CubeMX生成的Makefile文件就会派上用场。
包管理:
包管理有三种选项,翻译过来就是:仅复制用到的库文件、复制所有库文件、仅添加引用。
- 仅复制用到的库文件
- 复制所有库文件
- 仅添加引用:最简洁但不能移植
当选择了复制库文件到工程时,除了Inc和Src两个文件夹(或Advanced模式下的Core文件夹)外,还会额外生成一个Driver文件夹,其中放置的便是STM32的库文件。当启用了USB或者FreeRTOS等中间件后,也还会额外生成一个Middleware文件夹,其中放置的便是中间件的库文件。文件结构如下所示:
生成选项:

Tips:点击.ioc文件可以打开CubeMX重新配置
程序编写
/* USER CODE BEGIN x /
/* USER CODE END x */
其中的x可以是数字,也可以是别的一些标识符。STM32CubeMX希望用户将自己的代码插入到“BEGIN”和“END”当中,
reason:
之所以要这样做是因为STM32CubeMX在更新代码时不会修改这两句注释之间的内容。比如当用户的需求发生了变动,需要修改硬件配置时,就需要使用STM32CubeMX来修改并重新生成代码。如果将用户代码放在这两句注释之间,STM32CubeMX就不会在重新生成代码的时候覆盖掉这部分。这使得硬件配置的更改变得更加方便
编译上传
单片机、乃至目前一切电脑都不能直接运行代码文件,必须通过编译器将代码文件编译成CPU能够处理的机器语言——即二进制机器指令——才能烧录到芯片执行。STM32主流的编译器——也即arm芯片所用的编译器——有两种:arm-gcc和Arm Compiler。Keil默认采用Arm Compiler,而STM32CubeIDE和PlatformIO则采用arm-gcc。二者在编译原理上有差别,但是在代码层面很难体现出来
调试程序
ST-Link不只是程序下载器,还是调试器,能够通过JTAG接口或者SWD接口控制芯片的运行、读取芯片的寄存器、内存等。我们可以观察到芯片中每个寄存器的值、每个变量的值及其变化,可以通过断点让程序运行到指定的位置、检测中断的执行;从而能够方便地调试代码。
代码结构
结构概述
STM32CubeMX软件生成出来的代码具有高度的结构性。按文件予以区分,各个文件的功能如下:
| 文件 | 简介 |
|---|---|
| stm32f1xx_it.h/stm32f1xx_it.c | 实现stm32的所有中断程序 |
| stm32f1xx_hal_conf.h | HAL库配置文件 |
| system_stm32f1xx.c | 在内核层面上设置系统时钟 |
| stm32f1xx_hal_msp.c | 特定单片机具体配置 |
| main.h | 包含了应用程序全局的定义 |
| main.c | 实现主程序 |
| gpio.h/gpio.c(以及类似的tim、usart等) | 专门配置某个外设硬件 |
接下来将逐个讲解这些文件的功能。
文件与程序功能
stm32f1xx_hal_conf.h:
该文件定义了启用的HAL模块,并设置了一些宏参数——比如晶振频率等。
C/C++中的宏常用于选择性编译的程序,此处即使用了此原理。通过一系列宏来确定需要引用哪些头文件1
2
3
4
5
6
7
8/* Includes ----------------------------------------------------------*/
/**
\* @brief Include module's header file
*/
#ifdef HAL_RCC_MODULE_ENABLED
#include "stm32f1xx_hal_rcc.h"
#endif /* HAL_RCC_MODULE_ENABLED */
Tips:只有定义了对应的宏,才会引用对应的头文件,对应的模块才能够在应用中使用。这些宏不需要使用者手动定义,STM32CubeMX会自动根据在软件中选择的配置来定义这些宏、启用对应的模块。
system_stm32f1xx.c
该文件不涉及任何HAL库中的内容,仅仅从内核上使用寄存器的方式来初始化时钟,并提供了时钟频率值更新的函数。该文件中包含两个函数,如下所示:
| SystemInit() | 按照默认配置设置系统时钟、嵌入式Flash等 |
|---|---|
| SystemCoreClockUpdate() | 根据当前的配置计算芯片主频 |
这两个函数在代码中都没有显式调用。实际上SystemInit()的调用是写在启动文件中的:1
2/* Call the clock system intitialization function.*/
bl SystemInit
Notes:其调用发生在复位后、进入主函数前。其实本来可以通过修改该函数的参数就能实现时钟树的配置,但是HAL库选择了以自己的方法实现时钟配置,因此SystemInit()函数仅仅实现了默认的时钟配置,也就是采用内部时钟、主频为8MHz。真正的时钟配置是在主函数中进行的。
stm32f1xx_it.h/stm32f1xx_it.c
这两个文件包含了单片机用到的所有中断服务程序,主要分为两个部分:内核中断和外设中断。
内核中断
在代码中内核中断以以下注释作为开始:1
/**********************************************************************/ /* Cortex-M3 Processor Interruption and Exception Handlers */ /**********************************************************************/
内核中断指的是那些由STM32芯片中内核部分——包括F1系列采用的Cortex-M3内核、F4系列采用的Cortex-M4内核等——产生的中断。这些中断多是一些通用的、系统性的中断,如SysTick_Handler(系统滴答定时器中断)、HardFault_Handler(硬件错误中断)等。STM32CubeMX将其自动列出来,方便使用者修改。比如HardFault_Handler,当系统出现硬件错误的时候便会跳转到这里,可以利用这一中断函数很快定位bug的来源。
注意,内核中断中的SysTick_Handler默认被HAL库用于计时。如果没有实现该中断函数则会使HAL库失去计时功能。当然这也是已经由STM32CubeMX软件自动完成了,可以在SysTick_Handler函数中看见被调用的库函数HAL_IncTick。
系统滴答定时器中断:由系统滴答定时器产生
硬件错误中断:当发生内存溢出、访问越界、堆栈溢出时进入
HAL库计时功能默认采用系统滴答定时器来进行计时,但也可以换用TIM1、TIM2等外设计时器:可在STM32CubeMX的SYS部分配置
外设中断
在代码中外设中断以以下注释作为开始:1
/**********************************************************************/ /* STM32F1xx Peripheral Interrupt Handlers */ /* Add here the Interrupt Handlers for the used peripherals. */ /* For the available peripheral interrupt handler names, */ /* please refer to the startup file (startup_stm32f1xx.s). */ /**********************************************************************/
外设中断指的是由单片机内非内核部分的外设硬件产生的中断,如定时器中断、外部中断、串口中断等。当在STM32CubeMX中选择了相应的中断时便会自动在此处添加中断服务函数。
stm32f1xx_hal_msp.c
MSP指的是MCU Specific Package,即单片机具体方案。HAL库体系下,初始化分为两个步骤:抽象层初始化+MSP初始化。
以串口为例。串口的属性包括波特率、数据位、校验位等;但是这些属性属于抽象属性,无论这个串口是STM32单片机的串口1还是串口2,或者是Arduino上的甚至51单片机上的串口,都拥有这些属性。抽象属性的初始化即为抽象层的初始化。但是要让串口真正工作起来,仅仅告诉它波特率多少、数据位多少、有无校验位是不够的,还要配置串口的时钟、串口的DMA、串口的复用引脚等,而这些属性在不同单片机上是不一样的。这些初始化即为MSP初始化。只有经过了这两层初始化,开发者才能按照需求使用抽象硬件。
该文件实现的即是单片机全局的MSP初始化函数:HAL_MspInit()。如下所示,实现了复用引脚和电源的时钟使能,并且关闭了JTAG调试而改用SWD调试:
Tips:因为在CubeMX中的配置是SWD
1 | /** |
该函数并非在源文件中进行调用,而是在库文件中进行调用。HAL库的初始化函数在进行完毕抽象层的初始化后便会调用相应的MSP初始化函数。
main.h
该文件包含了全局的宏定义,并且包含了单片机的库文件。从STM32Cube的设计上来看,官方希望开发者将全局的定义、包含、常量等都写在该文件中,并在各个源文件中都包含该main.h
文件中为开发者预留了填写包含(Private includes)、类型(Exported types)、常量(Exported constants)、宏函数(Exported macro)、函数原型(Exported functions prototypes)和宏定义(Private defines)。
main.c
- 在main函数中,首先初始化了HAL库:
1 | /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ |
- 然后配置了系统时钟。此处的时钟配置和之前的SystemInit函数不一样,SystemInit函数中直接修改寄存器配置时钟,而此处则使用HAL库的库函数来配置;该函数也在main.c文件中实现,将单片机的时钟配置成软件中时钟树所配置成的样子。
1 | /* Configure the system clock */ SystemClock_Config(); |
- 其次初始化了所有外设。本例中仅有GPIO进行了初始化,因此此处也仅有GPIO的初始化函数。以后若有其他外设需要初始化,CubeMX也会将其放在该位置。
1 | /* Initialize all configured peripherals */ MX_GPIO_Init(); |
- 接下来便进入死循环。单片机上的程序不像我们电脑上的程序;电脑上的程序运行结束后就由操作系统回收资源了,单片机上的程序则会一直运行,从主函数返回后就回到了汇编启动文件中,接下来单片机的行为就不是我们在C语言代码中可以控制的了。因此在main函数中写入一个死循环,程序就不会从main函数退出。而实际的应用场景中,我们需要实现的功能很多时候也正是需要写在死循环中不断去执行。(callback function)
代码组织
stm32的工程为C语言工程,编译时不会产生元数据,全靠头文件的包含和源文件的编译链接来实现工程组成,因此尤其需要开发者设计好合适的工程结构。
关于C语言工程的结构,网上有许多的文章都提供了如何组织代码的方法。实际上代码的组织并没有一个非常统一的方案,大多都是按照开发者自己的喜好来设计。此处也不会要求同学们使用某种代码组织方法,仅仅提供笔者使用的代码结构供参考。

当程序的逻辑逐渐复杂的时候,也可以不仅仅使用一对App.h/c文件来实现全部逻辑,而可以使用多个文件分别实现一部分,此处不详述。
一些别的原则
不要包含源文件,而是包含头文件;头文件中也不要包含函数或者全局变量的定义,而仅仅包含其声明。将函数的定义放在源文件里,函数的声明放在头文件中;变量的定义放在源文件里,再在头文件中将其extern

使用易理解函数、类型、变量名称。杜绝使用意义不明的命名,如a、b、l、m等,而要使用意义明确的名字,如使用单词或者易于理解的缩写来描述。此处不推荐使用拼音进行命名。如下展示的是STM32的HAL库中的一个函数命名作为示例
*函数名:HAL_GPIO_WritePin* *HAL* *GPIO* *WritePin* 该函数隶属于HAL库 该函数与GPIO相关 该函数的功能是写引脚的值 使用规律的命名规则。常见的几种基础的命名规则如下:
| 驼峰命名 | 组成变量名的第一个单词的首字母小写,其余的首字母大写。Arduino采用的便是驼峰命名规则,如analogWrite,attachInterrupt等。 |
|---|---|
| 帕斯卡命名 | 组成变量名的各个单词的首字母均大写。C#就常用帕斯卡命名规则,如Console.WriteLine,XmlSerializer等。 |
| 下划线命名 | 组成变量名的单词均为小写,其间使用下划线连接。树莓派Pico的SDK采用的便是下划线命名,如hw_set_bits、sleep_ms |
这些命名规则仅仅作为参考,实际上可以使用混合的命名规则,比如公共成员使用帕斯卡命名、私有成员使用驼峰命名,或者在一个变量中使用组合式的命名规则。但是在整个工程中对象的命名规则务必有迹可循。