基于 HAL 与 LL 的 UINIO-MCU-STM32F401 开发实践
相较于前一篇运用标准外设库(Standard Peripherals
Library)开发的 STM32F403C8T6 微控制器,采用
UFQFPN48 封装的 STM32F401CCU6 则是基于
ARM Cortex®-M4
内核,内置有浮点运算单元(FPU,Float Point
Unit)、自适应实时加速器(ART,Adaptive Realtime
Accelerator)、数字信号处理器(DSP,Digital Signal
Processor)指令,内置 16mHz 高速与 32kHz
低速晶体振荡器,工作时钟频率高达 84mHz,采用
1.7V ~ 3.6V 电源进行供电。

因为 STM32F401CCU6 提供了较大的数据与程序存储空间,所以本文将会基于意法半导体 ST 近年来主推的硬件抽象层(HAL,Hardware Abstraction Layer)以及底层(LL,Low-layer)开发库,并且结合 STM32CubeIDE 提供的便捷图形化配置工具。本文写作过程当中,参考了意法半导体的《STM32F401xC Data Sheet》和《STM32F401xC Reference Manual》以及《Description of STM32F4 HAL & LL drivers》三份官方文档。
ARM Cortex M4 概要
外设资源 Peripheral
STM32F401CC 是一款携带有 DSP 和 FPU、ART 的 ARM
Cortex-M4 内核高性能基本型微控制器,拥有 256kBytes 的
Flash 程序存储器、64kBytes 的
SRAM 数据存储器、512Bytes 的
OTP 一次性可编程存储器,同时内置了 3 组
I²C、3 组
USART、4 组 SPI
总线接口、2 个 DMA 控制器、11
个 Timer 定时器,以及 81 个带有中断的
GPIO,具体的外设资源请参考下面表格:

引脚定义 Pin
STM32F401CC 一共拥有 48 个物理引脚,根据引脚功能的不同,可以划分为如下 5 种类型:

- (红色)电源引脚:其中
VDD 和 VSS是数字电源,主要为片内的数字外设供电;VDDA/VREF+ 和 VSSA/VREF-为模拟电源,主要为片内的模拟外设供电,同时作为 AD/DA 转换器的电压基准;VBAT用于连接外部的备用电池,确保片内的实时时钟在掉电之后,依然能够正常工作;VCAP_1用于片内电压调节器输出,通常会接入一枚2.2uF电容,然后再连接至 GND; - (蓝色)复位与启动模式引脚:
NRST为复位引脚,低电平有效;启动模式引脚包括BOOT0 和 PB2/BOOT1,通过其电平组合来配置微控制器的启动模式; - (绿色)时钟引脚:
OSC32_IN 和 OSC32_OUT用于连接芯片外部的低速时钟,通常是一枚工作频率为32.768 kHz的晶振;而OSC_IN 和 OSC_OUT则用于连接外部高速时钟,通常是工作频率为4mHz ~ 26mHz范围的晶振; - (橙色)仿真调试引脚:
PA13 和 PA14作为串行线调试(SWD,Serial Wire Debug)接口; - (其它)通用输入输出引脚:除了作为 GPIO 用途之外,还可以被映射成为片内其它外设的功能引脚;
启动模式 Boot
STM32F401CC 提供了 3
种不同的启动模式,这些模式可以通过组合 BOOT0 和
PB2/BOOT1 引脚的电平状态进行选择:
| BOOT1 | BOOT0 | 启动模式 | 描述 |
|---|---|---|---|
| × | 0 |
主 Flash 存储器模式 | 选择主 Flash 作为启动区域; |
0 |
1 |
系统存储器模式,即 ISP 模式 | 选择系统内存作为启动区域; |
1 |
1 |
内置 SRAM 模式 | 选择内置的 SRAM 作为启动区域; |
注意:上面表格当中的
BOOT0是专用引脚,而BOOT1复用了 GPIO 引脚PB2,一旦PB2作为BOOT1的电平状态被采样之后,就可以作用普通 GPIO 引脚进行使用。
系统复位 Reset
STM32F401CC 总共拥有 系统复位、电源复位、备份域复位 三种不同类型的复位方式,下面展示了内部复位电路的简化示意图:

- 系统复位(System
Reset):除开时钟控制寄存器
CSR的重置标志位,以及备份域当中的寄存器之外,其它所有寄存器都会被重置为默认值,系统复位通常发生于如下场景:- 复位引脚
NRST处于低电平状态时,即外部复位; - 窗口看门狗计数条件结束,即窗口看门狗复位;
- 独立看门狗计数条件结束,即独立看门狗复位;
- 进入
待机或者停止模式时,所发生的低功耗管理复位; - 出现软件复位的情况,复位源可以通过检查 RCC
时钟控制状态寄存器
RCC_CSR的复位标志来进行识别;
- 复位引脚
- 电源复位(Power
Reset):发生如下事件时就会产生电源复位:
- 发生电源开启或者关闭,以及出现低电压的时候;
- 退出待机模式的时候;
- 备份域复位(Backup Domain Reset):备份域仅拥有 2
种类型的复位,分别发生在如下的场景:
- 设置 RCC 备份域控制寄存器
RCC_BDCR的BDRST位,触发软件复位的情况; - 电源 \(V_{DD}\) 或者 \(V_{BAT}\) 关闭之后再重新上电的时候;
- 设置 RCC 备份域控制寄存器
总线架构 Bus
STM32F401CC 的系统架构由 32 位相互连接的多层 AHB 总线矩阵构成,其中 AHB 称为高级高性能总线(Advanced High-performance Bus),通常用于连接高速外设;而 APB 称为高级外围总线(Advanced Peripheral Bus),通常用于连接低速外设,具体细节可以参考下面的图示:

其中,整个系统架构主要包含 6
条主设备总线(Master):Cortex-M4 的
I-bus、D-bus、S-bus
总线,DMA1 和 DMA2
内存总线,DMA2 外设总线。以及 5
条从设备总线(Slave):内部 Flash 内存的
ICode 和 DCode
总线、SRAM 总线、AHB1 和(包含
AHB-APB 桥和 APB 外设)、AHB2
外设总线,这些总线之间相互连接的情况可以参考接下来的示意图:

- I-bus 总线:将 Cortex-M4
指令总线连接至总线矩阵,该总线被 Cortex-M4 核心用于获取指令,其传输目标是存放有程序代码的内部 Flash/SDRAM 存储器; - D-bus 总线:将 Cortex-M4
数据总线连接至总线矩阵,该总线被 Cortex-M4 核心用于加载字符和调试访问,其传输目标是存放有数据或者代码的内部 Flash/SDRAM 存储器; - S-bus 总线:将 Cortex-M4
的
系统总线连接至总线矩阵,该总线用于访问外设或者 SRAM 里的数据,也可以用于获取指令(效率比 ICode 要低),其传输目标是内部 SRAM、AHB1 外设(包括 APB 与 AHB2 外设); - DMA 存储总线:将
DMA 内存总线主接口连接至总线矩阵,该总线用于直接存储器存取(DMA,Direct Memory Access)执行存储器的存取操作,其传输目标是数据存储器(内部 Flash 或者 SRAM,以及包括 APB 外设在内的 AHB1/AHB2 外设); - DMA 外设总线:将
DMA 外设主总线接口连接至总线矩阵,该总线用于访问 AHB 外设或者存储器之间的数据传输,其传输目标是 AHB 和 APB 外设加上数据存储器(Flash 或者 SRAM); - 总线矩阵:用于
主设备之间的访问仲裁,并且使用轮循算法作为仲裁机制; - AHB/APB 桥(APB):两个
AHB/APB 桥以及APB1和APB2,提供了AHB与两条APB 总线之间的全同步连接,并且允许灵活的选择外围频率;
内存映射 Memory Mapping
STM32F401CC
的程序存储器、数据存储器、寄存器、I/O 端口都被组织到了一个
4GB
的线性内存地址空间,字节在内存当中以小端格式进行编码,这些可寻址的存储空间被分配为
5 个大小为 512MB
的块(Block),其中所有未分配的内存区域都被认为是预留空间,具体映射关系请参考下面的示意图以及后续的表格:

| 标识颜色 | 块编号 | 功能描述 | 容量 | 地址范围 |
|---|---|---|---|---|
| 绿色 | Block 0 | 分配给片上的 Flash 以及系统存储器; | 512MB | 0x0000 0000 ~ 0x1FFF FFFF |
| 粉色 | Block 1 | 分配给片上的 SRAM 存储器; | 512MB | 0x2000 0000 ~ 0x3FFF FFFF |
| 蓝色 | Block 2 | 分配给片上的 AHB1、AHB2、APB1、APB2 总线外设 | 512MB | 0x4000 0000 ~ 0x5FFF FFFF |
STM32F401 核心电路分析

MCU 微控制器
STM32F401CC 的高速时钟引脚 OSC_IN 和
OSC_OUT 连接到一颗贴片封装的 25mHz
无源晶振,并且并联了两颗 8 pF
的负载电容,从而组成了一个完整的晶体震荡电路。类似的,低速时钟引脚
OSC32_IN 和 OSC32_OUT 连接到一颗贴片封装的
32.768kHz 无源晶振,同时并联了两颗 1.5 pF
负载电容。而作为电压调节器用途的 VCAP_1
则按照官方数据手册接入了一枚 2.2uF 电容,然后再接入到
GND。

除此之外,3.3V 电源与 GND 之间的 3
颗并联 0.1uF
去耦电容,分别连接到数字电源引脚 VDD 与
VSS 之间。而由 1uF 并联 0.1uF
电容组成的滤波电路,则连接到了模拟电源引脚
VDDA\VREF+ 与 VSSA\VREF- 之间,并且
VDDA\VREF+ 还串联有一枚 100mHz 频率下感抗为
1kΩ 的滤波电感。

而 STM32F401CC 的 VBAT 引脚连接到了 RTC
实时时钟电路,然后并联了一枚 0.1uF 去耦电容
C2,而在该电路的一端连接至 3.3V
电源,而另一端则连接至开发板排针的 VB
端,用于外接备用电源;除此之外,该电路中间还逆向并联有 2
枚的肖特基二极管,用于在开发板上电时选择 3.3V
电源供电,而在断电之后选择 VB 排针外接的电源进行供电。
复位电路 Reset
复位引脚 NRST 低电平有效,开发板正常工作时通过一枚
10kΩ 上拉电阻连接到 3.3V
电源,当按键按下时,则引脚电平状态被 GND 拉低产生复位信号,按键并联的
0.1uF 电容用于消除按键被按下时产生的机械抖动。

启动配置按钮 Boot
当按键按下时,BOOT0 高电平 BOOT1/PB2
低电平,开发板会从系统内存启动,即进入 ISP
串口下载模式;而当按键没有被按下时,BOOT0 低电平
BOOT1/PB2 高电平,则开发板将会从 SRAM
启动;该电路当中,10kΩ 电阻与 0.1uF
电容并联成为一个高通滤波电路,而另一枚
10kΩ 电阻将会作为下拉电阻,将 BOOT1/PB2
始终控制在低电平状态。

USB Type-C 接口
开发板采用支持正反面插入的 16 针 USB Type-C
接口,其模型左右两侧引脚呈对称分布,其中 USB_DP 与
USB_DN 连接到 STM32F401CC 的
PA12/RX 和 PA11/TX
作为串口下载接口,而配置通道(Configration Channel)引脚 CC
则通过 5.1kΩ 下拉电阻接入 GND,而 VBUS
电源引脚通过一枚用于防止电涌的 D4 二极管,连接到后续的
LDO 低压差线性稳压器,同时并联有一组跳线帽
SB1,用于开发板接入较大负载时,防止电流过大导致二极管
D4 损坏。

线性稳压器 LDO
开发板采用了一枚日本特瑞仕 TOREX
公司的 XC6204 系列低压差线性稳压器(LDO,Low Dropout
Regulator),该系列是使用 CMOS 工艺制造的高精度、低噪声、正电压 LDO
稳压器,具备高纹波抑制和低压降特性,内部电路主要由标准电压源、误差校正、电流限流器、相位补偿电路加上驱动晶体管组成,输出电压可以在
0.9V ~ 6.0V 范围内按照 0.05V
的步长进行选择。

用户按键 Button
开发板提供了一枚可以由用户自定义的功能按键,通过阻值为
33Ω 的下拉电阻连接至
GND,当按下该按键的时候,就会将 PA0
引脚(对应于 PCB 丝印为 A0 的排针位置)的电平状态拉低。

状态指示 LED
由于红色 LED 的典型正向导通电压为 2V,蓝色 LED
的典型正向导通电压为 3.3V,所以电路上分别采用了
1.5kΩ 和 5.1kΩ 两颗不同的限流电阻。

串行线调试接口 SWD
串行线调试(SWD,Serial Wire
Debug)接口的串行数据线(SWDIO)和串行时钟线(SWDCLK)分别连接至
STM32F401CC 的 PA13 和 PA14
引脚,这两个引脚并没有连接到 PCB
排针上面,因而只可以作为仿真调试使用。

板载 NOR Flash
台湾华邦(Winbond)的 W25Q32/64/128JVSSIQ
是一款采用 SPI 总线连接的 NOR Flash 存储器芯片,可以选择
32Mb、64Mb、128Mb
三种不同容量,采用 8 个引脚的 SOIC 封装,工作电压介于
2.7V ~ 3.6V 范围。

STM32F401CC 开发板电路预留有焊接该存储芯片的位置,其
VCC 与 GND 引脚并联了一颗 0.1uF
滤波电容,用于滤除存储芯片工作电流发生变化时造成的电路纹波。而
SPI
片选(F_CS)、串行时钟输入(SCK)、数据输入(MOSI)、数据输出(MISO)四个引脚则作为
SPI 总线连接至 STM32F401CC 的
PA4、PA5、PA7、PA6,这四个引脚已经被连接到开发板上的排针,复用这几个引脚的功能时时需要特别注意。
HAL 硬件抽象层
硬件抽象层(HAL,Hardware Abstraction
Layer)驱动程序提供了一组功能丰富,易于与应用上层交互的
API,它们涵盖了常见的外围设备,可以非常方便的向其它型号 STM32
微控制器移植。同时还实现了用户回调函数机制,允许并发调用
USART1 以及 USART2
等外设,并且支持轮询、中断、DMA
三种 API 编程模式,HAL 固件库的驱动程序主要由如下的源代码文件构成:
| 源文件 | 功能描述 |
|---|---|
stm32f4xx_hal_ppp.cstm32f4xx_hal_ppp.h |
主要外设/模块驱动 .c
源文件以及 .h 头文件,包括所有设备通用的 API,例如
stm32f4xx_hal_adc.c、stm32f4xx_hal_irda.c 以及
stm32f4xx_hal_adc.h、stm32f4xx_hal_irda.h; |
stm32f4xx_hal_ppp_ex.cstm32f4xx_hal_ppp_ex.h |
外设/模块驱动程序扩展的 .c
源文件以及 .h 头文件,通常用于定义某个指定型号独有的
API,例如 stm32f4xx_hal_adc_ex.c 以及
stm32f4xx_hal_flash_ex.c; |
stm32f4xx_hal.cstm32f4xx_hal.h |
用于初始化 HAL 固件库的 .c
源文件以及 .h 头文件,包含有
DBGMCU、Remap 和基于 SysTick
的时间延迟函数; |
stm32f4xx_hal_msp_template.c |
需要复制到用户应用工程目录的模板文件,包含有外设的主堆栈指针(MSP,Main Stack Pointer)的初始化和反向初始化; |
stm32f4xx_hal_conf_template.h |
用于配置指定应用驱动的模板文件; |
stm32f4xx_hal_def.h |
通用的 HAL 固件库资源,例如通用的
语句、枚举、结构体、宏
等定义; |
下面的表格列出了通过 HAL 固件库,构建用户应用程序所需的最小 HAL 固件库文件集合:
| 源文件 | 功能描述 |
|---|---|
system_stm32f4xx.c |
包含系统启动时调用的
SystemInit() 方法,该方法允许重新定位内部 SRAM
中的向量表,并且配置 FSMC/FMC(如果可用)使用外置的 SRAM 或者 SDRAM
作为数据存储器; |
startup_stm32f4xx.s |
包含有重置处理器与异常向量在内的,工具链指定的文件; |
stm32f4xx_flash.icf |
(可选)EWARM 工具链的链接器文件,允许调整堆/栈大小以适应程序的需求; |
stm32f4xx_hal_msp.c |
包含用户应用程序当中使用到的外设主堆栈指针
MSP
的初始化与反向初始化(主程序与回调函数); |
stm32f4xx_hal_conf.h |
该文件允许用户为特定的应用程序定制 HAL 驱动程序; |
stm32f4xx_it.c stm32f4xx_it.c.h |
包含异常处理程序与外围设备中断服务程序; |
main.c main.c.h |
用户主程序,除了放置用户程序代码之外,还会调用
HAL_Init()、实现
assert_failed()、配置系统时钟、初始化指定外设; |
包含的数据结构
每一个 HAL 固件驱动程序都会包含有如下三种类型的数据结构:
- 外设操作结构体:
PPP_HandleTypeDef是 HAL 驱动程序当中主要的实现结构,用于配置外设、注册和嵌入外设相关的结构体与变量,例如stm32f4xx_hal_usart.c固件库文件当中定义的USART_HandleTypeDef结构体:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15typedef struct {
USART_TypeDef *Instance; /* USART 寄存器基地址 */
USART_InitTypeDef Init; /* USART 通信参数 */
uint8_t *pTxBuffPtr; /* 指向 USART Tx 发送缓冲区的指针 */
uint16_t TxXferSize; /* USART 发送大小 */
__IO uint16_t TxXferCount; /* USART 发送计数器 */
uint8_t *pRxBuffPtr; /* 指向 USART Rx 传输缓冲区的指针 */
uint16_t RxXferSize; /* USART Rx 传输大小 */
__IO uint16_t RxXferCount; /* USART Rx 传输计数器 */
DMA_HandleTypeDef *hdmatx; /* USART Tx 的 DMA 处理参数 */
DMA_HandleTypeDef *hdmarx; /* USART Rx 的 DMA 处理参数 */
HAL_LockTypeDef Lock; /* 对象锁定 */
__IO HAL_USART_StateTypeDef State; /* USART 通信状态 */
__IO HAL_USART_ErrorTypeDef ErrorCode; /* USART 错误代码 */
} USART_HandleTypeDef; - 初始化与配置结构体:
PPP_InitTypeDef结构体定义在通用固件驱动程序的.h头文件当中;除此之外,配置结构体1
2
3
4
5
6
7
8
9typedef struct {
uint32_t BaudRate; /* 配置 UART 通信波特率 */
uint32_t WordLength; /* 指定接收或者发送数据的长度 */
uint32_t StopBits; /* 指定传输的停止位数 */
uint32_t Parity; /* 指定校验模式 */
uint32_t Mode; /* 启用或者禁用收发模式 */
uint32_t HwFlowCtl; /* 启用或者禁用硬件流控制模式 */
uint32_t OverSampling; /* 启用或者禁用过采样,以达到更高的速度(可以达到 fPCLK/8)*/
} UART_InitTypeDef;HAL_PPP_Config用于初始化子模块或者子实例,例如下面 ADC 模数转换器外设示例:1
HAL_ADC_ConfigChannel (ADC_HandleTypeDef* hadc, ADC_ChannelConfTypeDef* sConfig)
- 指定流程结构体:
HAL_PPP_Process用于通用 API 当中的特定流程,通常被定义在通用固件驱动程序的.h头文件当中;1
HAL_PPP_Process (PPP_HandleTypeDef* hadc,PPP_ProcessConfig* sConfig)
HAL 库 API 的分类
HAL 固件库的 API 可以被划分为通用(Generic)和扩展(Extension)两种类型:
- 通用 API:适用于所有 STM32 微控制器,主要出现在 HAL
固件库的通用(Generic)驱动程序源文件当中;
1
2
3
4
5
6
7HAL_StatusTypeDef HAL_ADC_Init(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADC_DeInit(ADC_HandleTypeDef *hadc);
HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADC_Stop(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADC_Stop_IT(ADC_HandleTypeDef* hadc);
void HAL_ADC_IRQHandler(ADC_HandleTypeDef* hadc); - 扩展
API:可以进一步划分为指定系列与指定型号两种类型,处于
HAL 固件库的扩展(Extension)驱动程序源文件
stm32f4xx_hal_ppp_ex.c和stm32f4xx_hal_ppp_ex.h当中;1
2
3
4
5
6
7
8
9
10/* 指定系列的扩展 API */
HAL_StatusTypeDef HAL_ADCEx_InjectedStop(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADCEx_InjectedStop_IT(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADCEx_InjectedStart(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADCEx_InjectedStart_IT(ADC_HandleTypeDef* hadc);
/* 指定型号的扩展 API */
HAL_StatusTypeDef HAL_FLASHEx_OB_SelectPCROP(void);
HAL_StatusTypeDef HAL_FLASHEx_OB_DeSelectPCROP(void);
下面表格总结了不同类型的 HAL 固件库 API 在驱动程序源代码文件当中的位置:
| 通用驱动源文件 | 扩展驱动源文件 | |
|---|---|---|
| 公用 API | ✔ | ✔ |
| 产品系列 API | ✖ | ✔ |
| 指定型号 API | ✖ | ✔ |
HAL 驱动规范
HAL API 命名规则
HAL 固件库当中所使用的驱动程序命名规则如下面表格所示:
| 通用 | 系列指定 | 具体型号指定 | |
|---|---|---|---|
| 模块名称 | stm32f4xx_hal_ppp (c/h) |
stm32f4xx_hal_ppp_ex (c/h) |
stm32f4xx_ hal_ppp_ex (c/h) |
| 函数名称 | HAL_PPP_ MODULE |
HAL_PPP_ MODULE |
HAL_PPP_ MODULE |
| 头文件名称 | HAL_PPP_FunctionHAL_PPP_FeatureFunction_MODE |
HAL_PPPEx_FunctionHAL_PPPEx_FeatureFunction_MODE |
HAL_PPPEx_FunctionHAL_PPPEx_FeatureFunction_MODE |
| 指针名称 | PPP_HandleTypedef |
NA | NA |
| 初始化结构体名称 | PPP_InitTypeDef |
NA | PPP_InitTypeDef |
| 枚举名称 | HAL_PPP_StructnameTypeDef |
NA | NA |
对于上面表格当中所描述的驱动程序命名规则,需要特别注意如下几个事项:
- PPP
前缀指代的是外设的功能模式,而非外设本身,例如使用 USART 串口时,
该前缀可以是
USART、IRDA、UART、SMARTCARD; - 一个源文件当中使用的常量,就定义在该源文件内部,而多个源文件共用的常量定义在头文件当中;除外设驱动的函数参数以外,所有常量都需要大写;
typedef类型的变量名称应当以_TypeDef作为后缀;- HAL 固件库认为寄存器属于常量,大多数情况下常量名称是大写的,并且使用与官方参考手册当中相同的首字母缩写;
- 外设寄存器被声明在
stm32f4xx_hal_PPP.h头文件的PPP_TypeDef结构体当中,例如ADC_TypeDef; - 外设函数的名称以
HAL_作为前缀,然后是相应外设的首字母缩写(大写),然后再跟上一条下划线,接下来的每个单词首字母大写,例如HAL*UART_Transmit(); - 包含指定 PPP 外设初始化参数的结构体被命名为
PPP_InitTypeDef,例如ADC_InitTypeDef; - 包含指定 PPP 外设配置参数的结构体被命名为
PPP_xxxxConfTypeDef,例如ADC_ChannelConfTypeDef); - 外设指针结构体被命名为
PPP_HandleTypedef,例如DMA_HandleTypeDef; - 根据
PPP_InitTypeDef当中的参数,用于初始化 PPP 外设的函数被命名为HAL_PPP_Init,例如HAL_TIM_Init(); - 采用默认值重置 PPP 外设寄存器的函数被命名为
HAL_PPP_DeInit,例如HAL_TIM_DeInit(); - 后缀
MODE是指处理模式(轮询、中断、DMA),例如在本地资源以外使用 DMA 时,就应当调用HAL_PPP_Function_DMA()函数; - 前缀
Feature是指新的特性,例如HAL_ADCEx_InjectedStart()()表示的是 ADC 开始注入通道;
HAL 通用命名规则
对于共有的系统外设,无需使用指针或者实例对象,这个规则适用于
GPIO、SYSTICK、NVIC、RCC、FLASH
外设,例如函数 HAL_GPIO_Init() 只需要 GPIO
的地址及其配置参数。
1 | HAL_StatusTypeDef HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *Init) { |
每个外设驱动程序当中都定义有处理中断和特定时钟配置的宏,这些宏会被导出到外设驱动的头文件,以便于扩展文件使用,这些用于处理中断和特定时钟配置的宏如下所示:
| 宏定义 | 功能描述 |
|---|---|
__HAL_PPP_ENABLE_IT(__HANDLE__, __INTERRUPT__) |
使能一个特定的外设中断; |
__HAL_PPP_DISABLE_IT(__HANDLE__, __INTERRUPT__) |
失能一个特定的外设中断; |
__HAL_PPP_GET_IT (__HANDLE__, __ INTERRUPT __) |
获取一个指定外设的中断状态; |
__HAL_PPP_CLEAR_IT (__HANDLE__, __ INTERRUPT __) |
清除一个指定外设的中断状态; |
__HAL_PPP_GET_FLAG (__HANDLE__, __FLAG__) |
获取一个指定外设的标志位状态; |
__HAL_PPP_CLEAR_FLAG (__HANDLE__, __FLAG__) |
清除一个指定外设的标志位状态; |
__HAL_PPP_ENABLE(__HANDLE__) |
使能一个外设; |
__HAL_PPP_DISABLE(__HANDLE__) |
失能一个外设; |
__HAL_PPP_XXXX (__HANDLE__, __PARAM__) |
指定 PPP 外设驱动的宏; |
__HAL_PPP_GET_ IT_SOURCE (__HANDLE__, __INTERRUPT__) |
检查指定的中断源; |
注意:NVIC 和 SYSTICK 是 ARM Cortex-M4 提供的两个核心功能,与之相关的 API 都位于
stm32f4xx_hal_cortex.c源文件。
当从寄存器读取状态标志位时,其结果由移位值组成,具体取决于读取值的数量与大小。这种情况下,返回的状态宽度为 32 位,例如:
1 | STATUS = XX | (YY << 16) |
外设 PPP 的指针在调用 HAL_PPP_Init()
之前有效,初始化函数会在修改指针字段之前进行检查:
1 | HAL_PPP_Init(PPP_HandleTypeDef) |
可以使用条件式宏定义或者伪代码宏定义:
- 条件式宏定义:
1
- 伪代码宏定义(多指令宏):
1
2
3
4
5
中断处理程序与回调函数
除了各种 API 函数之外,HAL 固件库外设驱动程序当中还包含有:
- 用户回调函数;
- 由
stm32f4xx_it.c调用的HAL_PPP_IRQHandler()外设中断处理程序;
回调函数被定义为带有 weak
属性的空函数,使用时必须在用户代码当中进行定义,HAL
固件库当中存在三种类型的用户回调函数:
- 外围系统级初始化与反向初始化回调函数
HAL_PPP_MspInit()和HAL_PPP_MspDeInit; - 外理完成回调函数
HAL_PPP_ProcessCpltCallback; - 错误的回调函数
HAL_PPP_ErrorCallback;
| 回调函数 | 示例 |
|---|---|
HAL_PPP_MspInit()HAL_PPP_MspDeInit() |
例如 HAL_USART_MspInit(),由
API 函数 HAL_PPP_Init()
进行调用,用于进行外设的系统级初始化(GPIO、时钟、DMA、中断); |
HAL_PPP_ProcessCpltCallback |
例如
HAL_USART_TxCpltCallback,当处理执行完成时,由外设或者 DMA
中断处理程序进行调用; |
HAL_PPP_ErrorCallback |
例如
HAL_USART_ErrorCallback,当发生错误时,由外设或者 DMA
中断处理程序进行调用; |
HAL 通用 API
HAL 通用 API 为 STM32F401CC 微控制器提供了一系列公共通用的函数,其主要由四组不同类型的 API 组成:
- 初始化与反向初始化函数
HAL_PPP_Init()、HAL_PPP_DeInit():初始化函数HAL_PPP_Init()用于初始化外设并且配置底层硬件资源,主要是时钟、GPIO、AF 以及可能的 DMA 与中断,而反向初始化函数HAL_PPP_DeInit()则用于恢复外设的默认状态,释放底层硬件资源; - IO 操作函数
HAL_PPP_Read()、HAL_PPP_Write()、HAL_PPP_Transmit()、HAL_PPP_Receive():通过读/写操作来访问外设上的各种负载数据; - 控制函数
HAL_PPP_Set()、HAL_PPP_Get():控制函数用于动态调整外设的配置,以及设置其它的操作模式; - 状态与错误函数
HAL_PPP_GetState()、HAL_PPP_GetError():允许在运行时检索外设和数据流的状态,并且识别发生的错误类型;
下面表格当中,展示了 ADC 外设的部分通用 API:
| 功能分组 | 通用 API 名称 | 功能描述 |
|---|---|---|
| 初始化函数 | HAL_ADC_Init() |
初始化外设,配置时钟、GPIO、AF 等底层资源; |
HAL_ADC_DeInit() |
恢复外设的默认状态,释放底层资源,并且消除与硬件的全部直接依赖; | |
| IO 操作函数 | HAL_ADC_Start() |
在使用轮询模式时启用 ADC 转换; |
HAL_ADC_Stop () |
在使用轮询模式时停止 ADC 转换; | |
HAL_ADC_PollForConversion() |
在使用轮询模式时,等待转换结束; | |
HAL_ADC_Start_IT() |
在使用中断模式时启用 ADC 转换; | |
HAL_ADC_Stop_IT() |
在使用中断模式时停止 ADC 转换; | |
HAL_ADC_IRQHandler() |
处理 ADC 中断请求; | |
HAL_ADC_ConvCpltCallback() |
在中断子程序内调用的回调函数,用于标识当前处理的结束或者 DMA 传输在何时完成; | |
HAL_ADC_ErrorCallback() |
当发生外设错误或者 DMA 传输错误的时候,该回调函数会在中断子程序当中被调用; | |
| 控制函数 | HAL_ADC_ConfigChannel() |
用于配置当前选择的 ADC
常规通道,序列发生器当中相应的 Rank 与采样时间; |
HAL_ADC_AnalogWDGConfig |
该功能为选定的 ADC 配置模拟看门狗; | |
| 状态与错误函数 | HAL_ADC_GetState() |
用于在运行时获取外设与数据流的状态; |
HAL_ADC_GetError() |
获得发生在中断子程序当中的运行时错误; |
HAL 扩展 API
HAL 固件库的扩展 API 用于提供某个特定系列或者型号的
API,其代码定义在 stm32f4xx_hal_ppp_ex.c
源文件里,下面的表格展示了 ADC 外设的扩展
API:
| API 名称 | 功能描述 |
|---|---|
HAL_ADCEx_InjectedStart() |
用于轮询模式下,开始注入 ADC 转换通道; |
HAL_ADCEx_InjectedStop() |
用于轮询模式下,停止注入 ADC 转换通道; |
HAL_ADCEx_InjectedStart_IT() |
用于中断模式下,开始注入 ADC 转换通道; |
HAL_ADCEx_InjectedStop_IT() |
用于中断模式下,停止注入 ADC 转换通道; |
HAL_ADCEx_InjectedConfigChannel() |
配置所选择 ADC
的注入通道(序列发生器当中相应的 Rank 与采样时间); |
HAL 固件驱动程序会采用五种不同的方式处理特定的外设功能,接下来将分别对它们进行描述:
添加指定型号的功能
当需要为指定型号的 STM32 微控制器添加新特性时,这些新的 API
将会被添加至 stm32f4xx_hal_ppp_ex.c
扩展源文件当中,然后被命名为 HAL_PPPEx_Function():

1 | /* stm32f4xx_hal_flash_ex.c/h */ |
添加产品系列的功能
当为某个产品系列的 STM32 微控制器添加新特性时,API
会被添加至扩展驱动程序的 .c 源文件,并且被命名为
HAL_PPPEx_Function():

1 | /* stm32f4xx_hal_adc_ex.c/h */ |
添加新的外设
当需要添加一个新的外设 newppp 时,与之相对应的 API
需要添加到 stm32f4xx_hal_newppp.c,然后在
stm32f4xx_hal_conf.h 通过宏定义包含该源文件:
1 |

更新现存的通用 API
当一个通用 API
被定义为弱函数的时候,子程序会采用相同的名称定义将其在
stm32f4xx_hal_ppp_ex.c
扩展源文件,这样编译器就会采用这个新的函数来覆盖原来的定义:

更新现存的数据结构
HAL 固件库的外设数据结构(例如
PPP_InitTypeDef)可以拥有不同字段,这些数据结构被定义在扩展头文件当中,并通过
STM32 微控制器型号进行分隔:
1 |
|
源文件包含关系
通用 HAL 固件驱动程序的 stm32f4xx_hal.h
头文件,包含有整个 HAL 固件库的配置,它既是用户源代码文件
main.h 唯一包含的头文件,同时也使得 HAL 固件库的
.c 源文件能够使用其它 HAL
库资源,下面的示意图展示了源文件之间的这种依赖关系:

公用资源定义
头文件 stm32f4xx_hal_def.h 当中定义了 HAL
固件库里的公用资源,例如公用的枚举、结构体、宏定义,其中最为重要的是枚举类型
HAL_StatusTypeDef。
- HAL 状态被几乎所有 API 使用,用于返回当前 API
操作的状态,其具有如下 4 个可能的值:
1
2
3
4
5
6Typedef enum {
HAL_OK = 0x00,
HAL_ERROR = 0x01,
HAL_BUSY = 0x02,
HAL_TIMEOUT = 0x03
} HAL_StatusTypeDef; - HAL 锁同样也被所有 API
使用,用于防止意外的访问共享资源,其具有如下 2
个可能的值:
1
2
3
4typedef enum {
HAL_UNLOCKED = 0x00, /*!<Resources unlocked */
HAL_LOCKED = 0x01 /*!< Resources locked */
} HAL_LockTypeDef; - 通用的宏定义,例如
HAL_MAX_DELAY:链接名称为1
PPP的外设至 DMA 结构体指针的宏:1
2
3
4
5
注意:除此之外,
stm32f4xx_hal_def.h文件还会调用 CMSIS 库中的stm32f4xx.h文件来获取所有外设的数据结构与地址映射。
配置 HAL 固件库
头文件 stm32f4xx_hal_conf.h 用于配置 HAL
固件库,其中可以进行修改的选项如下面的表格所示:
| 配置项 | 功能描述 | 默认值 |
|---|---|---|
HSE_VALUE |
定义外部晶振的值(HSE),单位为赫兹
Hz; |
25 000 000 |
HSE_STARTUP_TIMEOUT |
HSE
启动超时时间,单位为毫秒
ms; |
5000 |
HSI_VALUE |
定义内部晶振的值(HSI),单位为赫兹
Hz; |
16 000 000 |
EXTERNAL_CLOCK_VALUE |
用于 I2S/SAI 模块计算其时钟源频率 | 12288000 |
VDD_VALUE |
VDD
的值,单位为毫伏 mV; |
3300 |
USE_RTOS |
使能嵌入式实时系统 RTOS; | FALSE |
PREFETCH_ENABLE |
使能预获取特性; | TRUE |
INSTRUCTION_CACHE_ENABLE |
使能指令缓存; | TRUE |
DATA_CACHE_ENABLE |
使能数据缓存; | TRUE |
USE HAL_PPP_MODULE |
使能模块在 HAL 驱动程序当中使用; | |
MAC_ADDRx |
配置以太网外设的 MAC 地址; | |
ETH_RX_BUF_SIZE |
配置以太网数据接收缓冲区的大小; | ETH_MAX_PACKET_SIZE |
ETH_TX_BUF_SIZE |
配置以太网数据发送缓冲区的大小; | ETH_MAX_PACKET_SIZE |
ETH_RXBUFNB |
以太网数据接收缓冲区的数量; | 4 |
ETH_TXBUFNB |
以太网数据发送缓冲区的数量; | 4 |
DP83848_PHY_ADDRESS |
DB83848 以太网 PHY 地址; | 0x01 |
PHY_RESET_DELAY |
PHY 复位延迟; | 0x000000FF |
PHY_CONFIG_DELAY |
PHY 配置延迟; | 0x000000FF |
PHY_BCR PHY_BSR |
通用 PHY 寄存器; | |
PHY_SR PHY_MICR PHY_MISR |
扩展 PHY 寄存器; |
注意:
stm32f4xx_hal_conf_template.h文件位于STM32Cube_FW_F4_V1.26.2固件库的Drivers\STM32F4xx_HAL_Driver\Inc目录下面,使用时需要将其复制到用户工程当中(STM32 Cube IDE 可以自动完成该操作),并且将其重命名为stm32f4xx_hal_conf.h。
如何使用 HAL 驱动
下面的示意图展示了 HAL 固件驱动的典型使用方法,以及用户应用程序、HAL 固件驱动、中断服务之间的交互过程。

注意:HAL 驱动程序当中实现的函数用绿色表示,从中断处理程序中调用的函数用虚线表示,在用户应用程序中实现的主堆栈 MSP 函数用红色框表示,实线表示用户应用程序功能之间的交互。
HAL 全局初始化
stm32f4xx_hal.c 提供了一组 API 来初始化 HAL
核心实现:
HAL_Init():该函数必须在应用程序启动时调用,用于初始化数据和指令,缓存预获取队列,设置 SysTick 定时器(基于 HSI 时钟)每间隔1ms产生一个最低优先级中断,将优先级分组设置为4位,调用HAL_MspInit()用户回调函数来执行系统级初始化(时钟、GPIO、DMA、中断);HAL_DeInit():重置所有外设,调用用户回调函数HAL_MspDeInit()执行系统级反向初始化;HAL_GetTick():获取当前 SysTick 定时器的计数值(在 SysTick 中断内递增),用于外设驱动程序处理超时;HAL_Delay():通过 SysTick 定时器实现一个以毫秒为单位的延迟;
时钟配置
时钟配置要在用户代码的开头部分完成,下面的示例代码体现了一个典型的时钟配置顺序:
1 | static void SystemClock_Config(void) { |
初始化 MSP
外设的初始化是通过 HAL_PPP_Init()
完成的,而外设所使用硬件资源的初始化是通过调用 MSP 回调函数
HAL_PPP_MspInit() 来执行的,MspInit
回调函数用于执行 RCC、GPIO、NVIC、DMA
等各种附加硬件资源相关的低级初始化。所有带有指针的 HAL
驱动程序,都包含有两个分别用于初始化与反向初始化
MSP 的回调函数:
1 | /** |
MSP 回调由用户工程当中的 stm32f4xx_hal_msp.c
实现,该文件可以通过 STM32 Cube IDE
自动生成与修改,其中主要包含有如下四个函数:
| 函数名称 | 功能描述 |
|---|---|
void HAL_MspInit() |
全局 MSP 初始化函数; |
void HAL_MspDeInit() |
全局 MSP 反向初始化函数; |
void HAL_PPP_MspInit() |
外设 PPP 的 MSP 初始化函数; |
void HAL_PPP_MspDeInit() |
外设 PPP 的 MSP 反向初始化函数; |
IO 操作
带有内部数据处理(发送、接收、读/写)的 HAL
函数,通常具备轮询(Polling)、中断(Interrupt)、DMA
三种处理方式:
轮询模式
在轮询模式下,当处于阻塞模式的数据被处理完成时,HAL
函数就会返回处理状态;函数返回 HAL_OK
状态表示操作完成,否则就会返回一个错误状态;用户可以通过
HAL_PPP_GetState() 函数获取更多信息;由于所有数据都是在
while
循环内部进行处理,所以还需要加入以毫秒为单位的超时判断变量,以防止处理过程被挂起;在接下来的示例代码当中,就展示了一个典型的轮询处理方式:
1 | HAL_StatusTypeDef HAL_PPP_Transmit(PPP_HandleTypeDef *phandle, uint8_t pData, int16_tSize, uint32_tTimeout) { |
中断模式
在中断模式下,HAL
函数会在启动数据处理并且响应中断之后返回处理的状态;操作的结束由声明为弱函数的回调来指示,该回调函数可以由用户自定义,以实时通知流程的完成情况;除此之外,用户还可以通过
HAL_PPP_GetState()
函数来获取处理状态。在中断模式下,驱动程序当中声明有下面四个函数:
HAL_PPP_Process_IT():启用中断处理;HAL_PPP_IRQHandler():全局 PPP 外设中断;weak HAL_PPP_ProcessCpltCallback():处理完成回调函数;weak HAL_PPP_ProcessErrorCallback():处理错误回调函数;
一个中断模式下的处理过程,会调用到用户代码当中的
HAL_PPP_Process_IT(),以及 stm32f4xx_it.c
库文件当中的 HAL_PPP_IRQHandler,而
HAL_PPP_ProcessCpltCallback() 函数由于在 HAL
固件驱动当中被声明为弱函数,这意味着用户可以在应用程序当中再次进行声明,下面是一个代码示例:
1 | /* main.c */ |
DMA 模式
HAL 可以通过 DMA 执行数据处理,并且在启用相应的 DMA
中断之后返回处理状态;操作的结束由一个声明为弱函数的回调来标识,用户可以自定义该回调函数,以便实时通知处理情况。除此之外,用户还可以通过
HAL_PPP_GetState() 函数来获取处理状态;在 DMA
模式下,驱动程序当中主要声明有如下四个函数:
HAL_PPP_Process_DMA():启用 DMA 处理HAL_PPP_DMA_IRQHandler():外设 PPP 使用的 DMA 中断;__weak HAL_PPP_ProcessCpltCallback():处理完成回调函数;__weak HAL_PPP_ErrorCpltCallback():处理错误回调函数;
一个 DMA 模式下的处理过程,需要调用用户文件当中的
HAL_PPP_Process_DMA(),以及 stm32f4xx_it.c
当中的 HAL_PPP_DMA_IRQHandler();除此之外,DMA 的初始化在
HAL_PPP_MspInit() 回调函数当中完成;用户同样也可以将
DMA 指针关联到外设 PPP
的指针,因而所有使用到 DMA
的外设驱动程序指针必须声明为下面的形式:
1 | typedef struct { |
以 UART 外设为例,其对应的初始化过程如下面代码所示:
1 | int main(void) { |
由于 HAL_PPP_ProcessCpltCallback() 函数在 HAL
固件驱动程序当中被声明为弱函数,这意味着用户可以在代码当中再次进行声明:
1 | /* main.c */ |
HAL_USART_TxCpltCallback() 和
HAL_USART_ErrorCallback() 应当通过类似下面这样的语句链接到
HAL_PPP_Process_DMA() 函数的 DMA
传输完成与错误回调函数:
1 | HAL_PPP_Process_DMA(PPP_HandleTypeDef *hppp, Params….) { |
超时与错误管理
超时管理
超时(Timeout)通常用于在轮询模式下操作的 API,其中定义了处理过程被阻塞直至错误被返回的延迟时间,下面代码是一个具有超时参数的函数调用示例:
1 | HAL_StatusTypeDef HAL_DMA_PollForTransfer(DMA_HandleTypeDef *hdma, uint32_t CompleteLevel, uint32_t Timeout) |
超时的时间取值范围,具体如下面的表格所示:
| 超时值 | 功能描述 |
|---|---|
0 |
没有轮询,立刻检查并且退出; |
1 ~ (HAL_MAX_DELAY -1) |
以毫秒作为单位的超时值; |
HAL_MAX_DELAY |
无限轮询直至处理成功; |
其中 HAL_MAX_DELAY 在 HAL 固件库头文件
stm32f4xx_hal_def.h 当中被定义为
0xFFFFFFFF;此外,在某些情况下,系统外设或者内部 HAL
驱动程序操作会使用一个固定的超时时间,这种情况下的超时都具备相同的意义:
1 |
|
接下来的示例展示了如何在轮询函数当中使用超时时间:
1 | HAL_PPP_StateTypeDef HAL_PPP_Poll(PPP_HandleTypeDef *hppp, uint32_t Timeout) { |
错误管理
HAL 固件驱动程序在代码当中,实现了针对如下内容的检查:
- 参数有效性:某些处理所使用的参数应当是有效并且已经定义过的,否则系统可能发生崩溃或者进入未定义状态,这些关键参数在使用前都会经过检查:
1
2
3
4
5HAL_StatusTypeDef HAL_PPP_Process(PPP_HandleTypeDef *hppp, uint32_t *pdata, uint32 Size) {
if ((pData == NULL) || (Size == 0)) {
return HAL_ERROR;
}
} - 指针有效性:外设 PPP
指针是一个非常重要的变量,因为其中保存了外设驱动程序的重要参数,因此总是在
HAL_PPP_Init()函数的开头进行检查:1
2
3
4
5
6HAL_StatusTypeDef HAL_PPP_Init(PPP_HandleTypeDef *hppp) {
/* 指针不能为空 */
if (hppp == NULL) {
return HAL_ERROR;
}
} - 超时错误:当发生超时错误时,会使用下面的语句进行处理:
1
2
3
4
5
6
7while (Process ongoing) {
timeout = HAL_GetTick() + Timeout;
while (data processing is running) {
if (timeout) {
return HAL_TIMEOUT;
}
}
当外设操作过程当中发生错误时,HAL_PPP_Process()
将会返回一个 HAL_ERROR 状态,HAL 的 PPP
外设驱动程序会通过 HAL_PPP_GetError()
函数来检索错误来源。
1 | HAL_PPP_ErrorTypeDef HAL_PPP_GetError(PPP_HandleTypeDef *hppp); |
所有外设指针都定义有一个用于保存最后错误代码的
HAL_PPP_ErrorTypeDef 结构体:
1 | typedef struct { |
外设的状态以及错误状态码,总是会在返回一个错误之前进行更新:
1 | PPP->State = HAL_PPP_READY; /* 设置外设状态为就绪 */ |
HAL_PPP_GetError()
方法必须在中断模式下的错误回调函数里面使用:
1 | void HAL_PPP_ProcessCpltCallback(PPP_HandleTypeDef *hspi) { |
运行时检查
HAL 通过检查所有 HAL
驱动程序函数的输入值来实现运行时错误检查,该特性通过
assert_param 宏定义来实现,针对所有具有输入参数的 HAL
固件驱动函数,用于验证输入值是否处于参数的允许值范围以内。通过
assert_param 宏启用运行时检查以后,还需要使得
stm32f4xx_hal_conf.h 当中的 USE_FULL_ASSERT
处于未注释状态。
1 | void HAL_UART_Init(UART_HandleTypeDef *huart) { |
如果向 assert_param 宏传递
false,那么就会调用 assert_failed
函数,并且返回调用失败的源文件名称以及相应的行号;而传递的是
true,则不会返回任何值。宏 ssert_param 定义在
stm32f4xx_hal_conf.h 头文件当中:
1 | /* 被导出的宏定义 */ |
assert_failed 函数可以定义在 main.c
或者其它任意的用户源代码文件当中:
1 |
|
注意:由于运行时检查会带来额外的性能开销,所以建议仅在开发调试阶段进行使用。
LL 底层库
底层(LL,low-layer)固件库驱动程序是一款比
HAL 更为接近硬件的库,其 API
并不会提供非关键特性,以及需要大量软件配置或者上层堆栈较为复杂的外设(例如
USB)的优化访问。它主要基于 STM32
片上外设的硬件特性来提供相关服务,这些服务准确的反映了硬件的功能,提供了官方手册所描述编程模型的一次性操作。由于其中并没有实现任何额外的处理业务,所以也就无需耗费额外的内存资源来保存状态、计数器、数据指针,所有操作都是通过修改硬件相关的寄存器来完成的。在
LL 固件库当中,主要提供有如下四种功能函数:
- 一组根据指定数据结构当中的参数,初始化外设主要特性的函数;
- 一组用于填充初始化数据结构各个字段重置值的函数;
- 执行外设反向初始化(将外设相关的寄存器恢复至默认值)的函数;
- 一组可以用于直接进行细粒度寄存器访问的内联函数;
LL 底层库文件
LL 固件库主要由片上外设的 .h 和 .c
驱动程序源文件,以及与 System 和 Cortex-M4
相关的源文件组成:
| LL 固件库源文件 | 功能描述 |
|---|---|
stm32f4xx_ll_bus.h |
用于核心总线控制与外设时钟的使能与失能,例如:LL_AHB2_GRP1_EnableClock; |
stm32f4xx_ll_ppp.h/.c |
stm32f4xx_ll_ppp.c 提供了
LL_PPP_Init()、LL_PPP_StructInit()、LL_PPP_DeInit()
等外设初始化函数,所有 API 都定义在 stm32f4xx_ll_ppp.h
头文件当中; |
stm32f4xx_ll_cortex.h |
包含系统滴答定时器 SysTick
与低功耗在内的 Cortex-M4 相关寄存器操作 API,例如
LL_SYSTICK_xxxxx、LL_LPM_xxxxx; |
stm32f4xx_ll_utils.h/.c |
该文件当中放置的是通用 API,可以用于读取设备 ID 和电子签名、时间基准与延迟管理、系统时钟配置; |
stm32f4xx_ll_system.h |
系统相关的操作,例如:LL_SYSCFG_xxx、LL_DBGMCU_xxx、LL_FLASH_xxx、LL_VREFBUF_xxx; |
stm32_assert_template.h |
定义用于使能运行时检查的
assert_param 宏模板文件,只会在独立使用 LL
固件驱动的场景下使用,使用时需要将其复制到用户工程当中,并且重命名为
stm32_assert.h; |
注意:LL 固件驱动并没有配置文件,其库文件可以位于与 HAL 固件驱动程序相同的目录。
LL 底层固件驱动程序当中只包含有 STM32 的 CMSIS
设备文件
#include "stm32yyxx.h",而用户应用程序里则只需要包含 LL
底层驱动程序的头文件:

外设初始化函数
LL 固件驱动程序在 stm32f4xx_ll_ppp.c
源文件当中提供了三组外设初始化相关的函数:
- 用于初始化外设主要特性,并以指定数据结构作为参数的函数;
- 一系列采用各字段预设值,填充初始化数据结构的函数;
- 用于外设初始化与反向初始化的函数,所谓反向初始化就是将外设相关的寄存器恢复至默认值;
这些 LL
初始化函数及其相关资源(结构体、字面量、原型)定义可以通过编译开关
USE_FULL_LL_DRIVER
进行切换,当需要使用这些函数时,必须将这个编译开关添加至工具链编译器的预处理当中,或者将其放置到先于任意
LL 固件驱动之前调用的通用头文件里面,下面表格展示了 LL
固件库所支持外设的通用功能:
常用的外设初始化功能:
| 函数名称 | 返回类型 | 参数 | 功能描述 |
|---|---|---|---|
LL_PPP_Init |
ErrorStatus |
PPP_TypeDef* PPPxLL_PPP_InitTypeDef* PPP_InitStruct |
根据 PPP_InitStruct
当中指定的参数,初始化外设的主要特性,例如:LL_USART_Init(USART_TypeDef *USARTx, LL_USART_InitTypeDef *USART_InitStruct); |
LL_PPP_StructInit |
void |
LL_PPP_InitTypeDef* PPP_InitStruct |
采用默认值填充 PPP_InitStruct
结构体的每一个成员,例如:LL_USART_StructInit(LL_USART_InitTypeDef *USART_InitStruct); |
LL_PPP_DeInit |
ErrorStatus |
PPP_TypeDef* PPPx |
反向初始化外设寄存器,即将其恢复至默认值,例如:LL_USART_DeInit(USART_TypeDef *USARTx); |
可选的外设初始化功能:
| 函数名称 | 返回类型 | 参数 | 示例 |
|---|---|---|---|
LL_PPP{_CATEGORY}_Init |
ErrorStatus |
PPP_TypeDef* PPPxLL_PPP{_CATEGORY}_InitTypeDef* PPP{_CATEGORY}_InitStruct |
根据 PPP_InitStruct
结构体当中指定的参数初始化外设特性,例如:LL_ADC_INJ_Init(ADC_TypeDef *ADCx, LL_ADC_INJ_InitTypeDef *ADC_INJ_InitStruct)LL_RTC_TIME_Init(RTC_TypeDef *RTCx, uint32_t RTC_Format, LL_RTC_TimeTypeDef *RTC_TimeStruct)LL_RTC_DATE_Init(RTC_TypeDef *RTCx, uint32_t RTC_Format, LL_RTC_DateTypeDef *RTC_DateStruct)LL_TIM_IC_Init(TIM_TypeDef* TIMx, uint32_t Channel, LL_TIM_IC_InitTypeDef* TIM_IC_InitStruct)LL_TIM_ENCODER_Init(TIM_TypeDef* TIMx, LL_TIM_ENCODER_InitTypeDef* TIM_EncoderInitStruct); |
LL_PPP{_CATEGORY}_StructInit |
void |
LL_PPP{_CATEGORY}_InitTypeDef* PPP{_CATEGORY}_InitStruct |
采用缺省值填充
PPP{_CATEGORY}_InitStruct
结构体的每一个成员,例如:LL_ADC_INJ_StructInit(LL_ADC_INJ_InitTypeDef *ADC_INJ_InitStruct); |
LL_PPP_CommonInit |
ErrorStatus |
PPP_TypeDef* PPPxLL_PPP_CommonInitTypeDef* PPP_CommonInitStruct |
初始化相同外设不同实例之间共享的公共特性,例如:LL_ADC_CommonInit(ADC_Common_TypeDef *ADCxy_COMMON, LL_ADC_CommonInitTypeDef *ADC_CommonInitStruct); |
LL_PPP_CommonStructInit |
void |
LL_PPP_CommonInitTypeDef* PPP_CommonInitStruct |
采用缺省值填充
PPP{_CATEGORY}_InitStruct
结构体的每一个成员,例如:LL_ADC_CommonStructInit(LL_ADC_CommonInitTypeDef *ADC_CommonInitStruct); |
LL_PPP_ClockInit |
ErrorStatus |
PPP_TypeDef* PPPxLL_PPP_ClockInitTypeDef* PPP_ClockInitStruct |
通过同步模式,初始化外设时钟的配置,例如:LL_USART_ClockInit(USART_TypeDef *USARTx, LL_USART_ClockInitTypeDef *USART_ClockInitStruct); |
LL_PPP_ClockStructInit |
void |
LL_PPP_ClockInitTypeDef* PPP_ClockInitStruct |
采用缺省值填充
ppp_clockkinitstruct
结构体的每一个成员,例如:LL_USART_ClockStructInit(LL_USART_ClockInitTypeDef *USART_ClockInitStruct); |
运行时检查
类似于 HAL 固件驱动,LL 初始化函数同样通过检查函数的输入值来实现运行时错误检查。当独立使用 LL 驱动程序(不调用任何 HAL 函数)的时候,需要执行如下操作来进行运行时检查:
- 复制
stm32_assert_template.h到用户工程目录,并将其重命名为stm32_assert.h,该文件当中定义了运行时错误检查所需的assert_param宏; - 在用户应用程序入口的
main.h头文件当中包含stm32_assert.h文件; - 在工具链编译器预处理,或者位于
stm32_assert.h之前执行的任意通用头文件当中,添加USE_FULL_ASSERT编译开关;
注意:运行时错误检查对于 LL 固件库的内联函数无效。
外设的寄存器级配置
在外设初始化函数的基础之上,LL 固件库提供了一系列能够细粒度操作寄存器的内联函数,其格式如下所示:
1 | __STATIC_INLINE return_type LL_PPP_Function(PPPx_TypeDef *PPPx, args) |
注意:此处的
Function是根据其行为类别来进行命名的。
指定的中断与 DMA 请求、状态标志管理,即设置、获取、清除、启用、禁用中断与状态寄存器上的标志:
名称 示例 LL_PPP_{_CATEGORY}_ActionItem_BITNAMELL_PPP{_CATEGORY}_IsItem_BITNAME_ActionLL_RCC_IsActiveFlag_LSIRDYLL_RCC_IsActiveFlag_FWRST()LL_ADC_ClearFlag_EOC(ADC1)LL_DMA_ClearFlag_TCx(DMA_TypeDef* DMAx)可以使用的函数格式如下面表格所示:
类型 行为 格式 标志 获取 LL_PPP_IsActiveFlag_BITNAME清除 LL_PPP_ClearFlag_BITNAME中断 启用 LL_PPP_EnableIT_BITNAME禁用 LL_PPP_DisableIT_BITNAME获取 LL_PPP_IsEnabledIT_BITNAMEDMA 启用 LL_PPP_EnableDMAReq_BITNAME禁用 LL_PPP_DisableDMAReq_BITNAME获取 LL_PPP_IsEnabledDMAReq_BITNAME注意:上面表格当中的
BITNAME是指官方参考手册当中所描述外设寄存器的位名称。外设时钟激活与失活管理,即启用、禁用、重置外设时钟:
名称 示例 LL_BUS_GRPx_ActionClock{Mode}LL_AHB2_GRP1_EnableClock (LL_AHB2_GRP1_PERIPH_GPIOA │ LL_AHB2_GRP1_PERIPH_GPIOB)LL_APB1_GRP1_EnableClockSleep (LL_APB1_GRP1_PERIPH_DAC1)注意:上面表格当中的
x对应于组索引,即关联到指定总线上被修改寄存器的索引,而bus则对应于总线的名称。外设的激活与失活管理,即启用/禁用外设,或者激活/失活指定的外设功能:
名称 示例 LL_PPP{_CATEGORY}_Action{Item}LL_PPP{_CATEGORY}_IsItemActionLL_ADC_Enable()LL_ADC_StartCalibration()LL_ADC_IsCalibrationOnGoingLL_RCC_HSI_Enable()LL_RCC_HSI_IsReady()外设配置管理,即设置/获取外设的配置:
名称 示例 LL_PPP{_CATEGORY}_{Set/Get}ConfigItemLL_USART_SetBaudRate(USART2, Clock, LL_USART_BAUDRATE_9600)外设寄存器管理,即读/写一个寄存器的内容,或者返回 DMA 相关的寄存器地址:
名称 LL_PPP_WriteReg(__INSTANCE__, __REG__, __VALUE__)LL_PPP_ReadReg(__INSTANCE__, __REG__)LL_PPP_DMA_GetRegAddr(PPP_TypeDef *PPPx, { Sub Instance if any ex : Channel }, {uint32_t Propriety})注意:上面表格当中的
proper是一个用于识别 DMA 传输方向或者数据寄存器类型的变量。
HAL & LL 组合运用
LL 固件库当中的 API 可以独立进行使用,也可以与 HAL 结合起来使用,但是并不能与 HAL 一起作用于相同的外设实例,换而言之,可以在一个外设实例上使用 LL 库的 API,而另一个外设实例上使用 HAL 库的 API,注意,LL 库的 API 可能会重写一些内容被映射至 HAL 指针的寄存器。
单独使用 LL 固件库
LL 固件库的 API
可以独立的在工程当中进行使用,只需要在用户应用程序内包含
stm32f4xx_ll_ppp.h 头文件即可,调用指定外设 LL 库 API
的顺序与官方参考手册里推荐的顺序相同。在这种情况之下,可以删除用户工程里与
LL 库操作外设相关联的 HAL 驱动程序,但是与 STM32CubeF4
的 ARM Cortex-M4 核心开发框架相关联的
系统文件、启动文件、CMSIS
代码仍然需要保留。
注意:当工程中包含有板级支持包(BSP,Board level Support Package)时,与其相关联的 HAL 固件驱动程序应当也包含在用户工程当中,即使它们并没有直接被用户应用所调用。
组合运用 HAL 和 LL 固件库
当 HAL 和 LL 两个固件库组合在一起使用时,同样可以达到直接操作寄存器的目的。虽然官方文档里允许进行这样的混合使用,但是应当考虑到如下因素:
- 建议避免同时通过 HAL 和 LL 的 API 操作相同的外设实例,如果必须要执行类似的操作,则需要修改 HAL 外设 PPP 结构体上的相应的私有字段设置;
- 对于不会修改指针字段(包含初始化结构体)的处理和操作,则可以让 HAL 库与 LL 库的 API 共同作用于相同的外设实例;
- LL 驱动程序可以不受限制的与所有不基于指针对象的 HAL
驱动程序(包括
RCC、公用的 HAL、Flash、GPIO)一起共同使用;
注意:STM32F401CC 固件包里
Projects目录下的Examples_MIX示例工程,展示了在同一个用户工程当中组合运用 HAL 和 LL 库的示例。
除了上述注意事项之后,还需要再额外注意以下几点事项:
- 当 HAL 的初始化与反向初始化 API 没有被使用,而是被 LL
库的宏定义替换掉的时候,此时
InitMsp()函数并不会被调用,而需要用户自行在应用程序当中初始化主堆栈指针 MSP; - 当某个 HAL 的处理 API 没有被使用,而是通过 LL 的 API 执行相应的函数时,此时 HAL 的回调函数并不会被自动调用,后期的处理以及错误管理都需要由用户应用程序来完成;
- 当 LL 库的 API 被用于指定的操作过程时,与 HAL 库 API 相关的 IRQ 处理程序不会被调用,此时 IRQ 需要由用户应用程序来实现,每个 LL 驱动程序实现的宏需要去读取和清除相关的中断标志;
STM32 Cube IDE 开发环境
STM32
Cube IDE 是由意法半导体推出的一款基于
Eclipse/CDT 框架和 GCC/GDB 工具链打造的
C/C++ 集成开发环境,内部整合了 STM32CubeMX
代码生成器,可以方便的用于 STM32
系列微控制器的外设配置和代码生成、编译、调试。

除此之外,STM32 Cube IDE 还集成有构建分析器【Build Analyzer】,用于为开发者提供编译构建相关的有效信息:

以及静态堆栈分析器【Static Stack Analyzer】,用于为用户提供内存堆栈方面的有用参考信息:

新建工程
开始新建工程之前,需要进入 STM32 Cube IDE
的偏好设置界面设置 STM32Cube
固件安装的位置,鼠标依次点击【Preferences → STM32Cube → Firmware
Updater】,这里选择将固件库保存至
C:\Software\Tech\STM32\Repository
目录,然后再点击应用并且关闭【Apply and
Close】按钮:

首先,选中 STM32 Cube IDE 左侧项目管理器上的【Create a New STM32 project】链接,进入如下的 STM32 MCU/MPU 选择器界面,选中 STM32F401CCUx 之后点击下一步【Next>】按钮:

然后,设置用户工程的名称,其它的设置项保持默认即可,继续点击下一步【Next>】按钮:

接下来,选择 STM32Cube
的固件版本,并且检查固件库保存的位置,选择仅拷贝工程所需的库文件,点击完成【Finish】按钮:

最后,返回到下面的 STM32 Cube IDE 主界面,此时点击工具栏上的【🔨】按钮就可以编译当前工程。这里可以通过切换【Build 'Debug' for project 'Test'】和【Build 'Release' for project 'Test'】菜单,选择当前工程的编译方式为 Debug 调试 还是 Release 编译:

此处如果选择的是 Debug
调试选项,那么生成的代码将会位于 Test 用户工程下的
Debug 目录;而如果选择 Release
编译选项,则生成的代码将会保存在 Test 工程下的
Release 目录,而 Debug 和 Release
目录当中的 Test.bin 二进制文件就是将要被下载到
STM32F401CC 微控制器当中运行的固件。
工程源码结构
通过 STM32CubeIDE
新建一个 STM32F401CC
基本工程的项目代码结构如下所示,这些库文件主要拷贝自
STM32Cube_FW_F4_V1.26.2 固件库的 Drivers\CMSIS
和 Drivers\STM32F4xx_HAL_Driver
两个目录,而其它文件则是由开发工具自动生成的工程辅助文件:
1 | [ Test ] |
STM32CubeIDE 自动生成的工程当中,默认的
main.h 和 main.c 源文件内容如下所示:
1 | /*========== main.h ==========*/ |
ST-Link 下载调试
STM32 Cube IDE 默认集成了 ST-Link 升级工具,操作之前需要先安装意法半导体官方的 ST-Link 驱动程序,并且将 ST-Link 插入电脑的 USB 接口,然后依次选择 STM32 Cube IDE 主菜单上的【Help → ST-Link 更新】:

在弹出的 ST-Link 升级界面当中,鼠标依次点击打开升级模式【Open in update mode】和升级【Upgrade】按钮,就可以开始联网进行升级:

当对话框的绿色滚动条消失,就表示此时升级操作已经执行完毕,界面上会显示升级成功的提示信息:

接下来,从电脑 USB 接口上拔出 ST-Link
再重新插入上电,就可以开始进行程序的调试与下载工作,将
ST-Link 与 STM32F401CC
开发板的SWD
串行线调试接口(GND、SWCLK、SWDIO、3.3V)
连接在一起:

然后,选择工具栏上的【Run】或者【Debug】按钮下面的【Run/Debug Configration】菜单项,在打开的界面当中勾选【接口】为 SWD,如果当前电脑连接有多台 ST-Link,则这里还需要指定当前所使用的那台 ST-Link 序列号:

最后,鼠标点击界面上的【Run】运行按钮,就可以通过 ST-Link 的 SWD 调试接口,实时的将程序下载到 STM32F401CC 开发板当中运行。
CMSIS-DAP 下载调试
CMSIS-DAP提供了一种通过 USB 访问 ARM Cortex 微控制器 Coresight 调试端口(DAP,Coresight Debug Access Port)的标准化方法,CMSIS-DAP 通常以板载接口芯片的方式进行实现,提供了从开发板到主机调试器的直接 USB 连接,并且通过联合测试行动组(JTAG,Joint Test Action Group)或者串行线调试(SWD,Serial Wire Debug)接口完成双方的相互连接。

注意:Coresight 是 ARM 公司提出的,用于对复杂的片上系统进行调试(Debug)与跟踪(Trace)的芯片设计架构。
Windows 10 操作系统上使用 CMSIS-DAP 调试器,需要下载适用于 Windows
的预编译包 OpenOCD,这是一款开源的芯片调试工具,允许使用
JTAG 通过 GDB 调试各种 ARM 设备。下载并解压安装包之后,将其
bin 目录添加到 Windows 的 PATH
环境变量当中,重新启动电脑之后,在命令行界面输入
openocd --help,如果提示如下结果就说明安装成功:
1 | λ openocd --help |
然后,将 CMSIS-DAP 调试器连接到电脑 USB 接口,此时 Windows 10
操作系统会自动适配其驱动程序。再执行下面的命令,在本地 3333
端口上启动 GDB 调试服务。注意命令参数 --search
后面的目录,需要指向当前 OpenOCD 安装的绝对路径:
1 | openocd --search D:/software/Tech/OpenOCD --file share/openocd/scripts/interface/cmsis-dap.cfg --file share/openocd/scripts/target/stm32f4x.cfg |
方便起见,也可以将上述命令保存为一个单独的 .bat
批处理文件,以便于鼠标随时双击启动 GDB 调试服务。上述命令执行之后,如果
Windows 命令行界面提示如下信息,就表明 GDB 服务已经正确的启动:
1 | Open On-Chip Debugger 0.11.0 (2021-07-29) [https://github.com/sysprogs/openocd] |
接下来,连接 CMSIS-DAP 调试器和 STM32F401CC 开发板 ,打开 STM32 Cube IDE 工程的设备配置工具【Device Configration Tool】,然后选择当前所采用的 Debug 连接模式:

最后,选择 STM32 Cube IDE
工具栏上的【Run】或者【Debug】按钮下面的【Run/Debug
Configration】菜单项,切换至【调试器】选项卡,将端口号码设置为
3333,调试探头设置为
ST-Link (OpenOCD),并且取消
Enable live expressions
的勾选,按下【Apply】应用这些设置,选择【Run】即可开始下载程序:

STM32 Cube Programmer 编程器
STM32
Cube Programmer 是意法半导体公司推出的一款 STM32
系列微控制器编程下载工具,可以支持摩托罗拉的 S19 和英特尔的
HEX、ELF 二进制文件格式,提供了
Debug 接口(JTAG 和 SWD)和
Bootloader
接口(UART、USB DFU、I2C、SPI、CAN)两种下载方式,能够同时支持
STM32 内部 Flash、RAM、OTP
以及外部存储器的下载编程。

进入 Bootloader 模式
STM32F401CC 开发板经过如下的 3 个操作步骤,就可以进入 Bootloader 下载模式,从而正常使用 STM32 Cube Programmer 执行 USB 或 UART 下载:
- 首先,同时按住 开发板上的 BOOT0 和 NRST 按键;
- 然后,松开 NRST 按键;
- 最后,在
0.5秒之后再松开 BOOT0 按键;
通过 USB 下载
打开 STM32 Cube Programmer,通过 USB 接口连接 STM32F401CC 开发板,让开发板进入 Bootloader 下载模式:

单击界面当中的【刷新】按钮,使得 STM32 Cube Programmer 扫描到当前所连接的 USB 端口,然后按下【Connect】连接按钮开始建立连接:

连接成功之后,选择界面上的【Open
file】按钮,打开需要下载到开发板上运行的 Test.bin
二进制文件:

鼠标点击界面上的【Download】下载按钮就可以开始执行下载操作:

下载完成之后,STM32 Cube Programmer
的主界面上将会弹出下面的下载完成提示信息:

STM32 的 Bootloader 自举程序存放在系统 ROM
存储器当中,由意法半导体公司在 STM32 芯片生产期间预置,用于通过
USART、CAN、USB、I²C
等串行外设,下载程序至 STM32 内部的 Flash 存储器。由于 USB
下载程序时使用的是 HSE 外部高速晶振
,而Bootloader 自举程序是通过 HSI
内部高速晶振测量 HSE 频率之后再配置时钟。如果
HSI 受到环境温度影响误差过大,就会导致
HSE 测量的频率不准确,进而导致 USB
下载时序出现错误,STM32 Cube Programmer
会提示如下错误信息:
1 | Error: failed to download Segment[0] |
通过 UART 下载
如果使用 USB 下载时遇到前面所述的错误,那么就可以选择稳定性更高的
UART 下载方式。首先将开发板与电脑的 USB 连接断开,然后插入一个 USB
转串口模块,将其 TX 引脚连接至开发板的
PA10/RX1 引脚,而 RX 引脚连接至开发板的
PA9/TX1 引脚,3.3V 和 GND 则分别
Pin to Pin 对应连接,然后打开 STM32 Cube Programmer
工具选择【UART】下载方式:

然后点击【Connect】连接按钮,使得 STM32 Cube Programmer 通过 UART 与开发板建立连接,完成之后的界面如下图所示:

类似于前面所讨论的 USB 下载方式,这里依然选择界面上的【Open
file】按钮,打开 Test.bin
二进制文件,然后点击【Download】下载按钮,下载成功之后主界面同样会弹出
File download complete 的提示信息。让 STM32 Cube
Programmer 通过 UART
实现程序下载,并不会受到环境温度的影响,下载编程的稳定性较高。
RCC 复位时钟控制
外设分析
STM32F401CC 的系统时钟
SYSCLK
可以由高速内部(HSI,High Speed
Internal)与高速外部(HSE,High Speed
External)时钟,以及主锁相环(PLL,Phase
Locking
Loop)三种不同的时钟源来进行驱动;而实时时钟可以选择
40kHz
的低速内部(LSI,Low Speed
Internal)以及 32.768kHz
的低速外部(LSE,Low Speed
External)时钟,每一个时钟源都可以按需进行独立开关,从而优化系统的功耗特性。
- 高速外部(HSE,High Speed
External)时钟:使用外部晶振作为时钟源,可以选择的频率范围在
4mHz ~ 26mHz之间; - 高速内部(HSI,High Speed
Internal)时钟:由内部的
16mHzRC 振荡器产生,可以直接用于系统时钟,或者是输入到锁相环,虽然其启动较为迅速,但是频率精度和温度飘移性能不如 HSE; - 锁相环(PLL,Phase Locking
Loop)时钟:STM32F401CC
拥有两个锁相环,其中主锁相环
PLL由 HSE 或者 HSI 进行驱动,可以输出84mHz的高速系统时钟,或者是48mHz的全速 USB OTG 信号、小于或等于48mHz的随机模拟信号、小于或等于48mHz的 SDIO 信号; - 低速外部(LSE,Low Speed
External)时钟:通过
32.768kHz的低速外部晶振产生,用于为计时与日历功能的实时时钟(RTC,Real-Time Clock)提供低功耗高精度的时钟信号源; - 低速内部(LSI,Low Speed
Internal)时钟:由
32kHz的内置低功耗时钟源产生,可以在停止和待机模式下,保持独立看门狗(IWDG,Independent Watch Dog)和自动唤醒单元(AWU,Auto Wakeup Unit)的正常运行;

时钟控制器(Clock
Control)可以高度灵活的选择外部晶振,并且同时能够确保
USB、OTG、I2S、SDIO
等外设工作在指定的频率范围。除此之外,还会通过多个预分频器来配置
AHB(最大频率范围 84mHz)、高速
APB(APB2,最大频率范围 84mHz)、低速
APB(APB1,最大频率范围
42mHz)的总线工作频率。排除下面的两种情况之外,其它所有外设的时钟频率均来自于系统时钟
SYSCLK:
- 来自于锁相环
PLL48CLK输出的全速 USB OTG 系统时钟(48mHz)与 SDIO 时钟(小于48mHz); - 为实现高质量的音频性能,I2S 时钟可以由指定的
PLL(
PLLI2S)或者映射至I2S_CKIN引脚的外部时钟源派生而来;
STM32F401CC 采用 AHB 总线时钟
HCLK 除以 8 来作为 Cortex-M4 系统定时器
SysTick 的外部时钟源,通过配置 SysTick
的控制状态寄存器,可以选择 SysTick 与该时钟源还是
HCLK 时钟源一起工作。而 STM32F401CC
的定时器时钟频率则是由硬件自动进行设置,当 APB 分频器为
1 时,定时器时钟频率与所连接 APB
总线的时钟频率保持一致,否则就会被设置为 APB 时钟频率的 2
倍。
当硬件自动设置定时器的时钟频率时,根据
RCC_DCKCFGR 寄存器当中 TIMPRE
位的取值,可以具体划分为下面两种情况:
- 如果
TIMPRE被重置:当 APB 预分器的分频系数配置为1,则定时器时钟频率TIM x CLK被设置为HCLK,否则就会被设置为所连接 APB 总线频率的 2 倍TIM x CLK = 2 x PCLKx; - 如果
TIMPRE被置位:当 APB 分频器配置为1或者2,则定时器时钟频率TIM x CLK被设置为HCLK,否则就会被设置为所连接 APB 总线频率的 4 倍TIM x CLK = 4 x PCLKx;
API 描述
指定特性
当设备复位之后,STM32F401CC
将会通过内部高速振荡器(HSI
16MHz)启动运行,此时微控制器处于 Flash 0
等待状态,并且 Flash 开始预获取缓冲区、同时 D-Cache 和
I-Cache 都被禁用,内部 SRAM、Flash、JTAG
之外的所有外设都将会被关闭。
- AHB 高速总线和 APB 低速总线上都没有预分频器,这意味着映射到这些总线上的外设都会以 HSI 的频率运行;
- 除了 SRAM 和 FLASH 之外的所有外设时钟都将会被关闭;
- 除了 JTAG 引脚被分配用于调试之外,所有 GPIO 都将会处于浮空输入状态;
一旦设备从复位状态开始重新启动,则用户应用程序必须进行如下一系列操作:
- 配置用于驱动系统时钟的时钟源;
- 配置系统时钟频率与 Flash 设置;
- 配置 AHB 和 APB 总线的预分频器;
- 启动当前所要使用的外设时钟;
- 配置非系统时钟派生外设的时钟源 (I2S、RTC、ADC、全速 USB OTG 或者 SDIO、RNG) ;
使用限制
管理 STM32F401CC
外设对于寄存器进行的各种读写操作,需要考虑到
RCC 外设时钟使能 与 有效外设使能
之间的延迟:
- 首先,这个延迟取决于外设映射;
- 其次,如果外设映射到 AHB
上,那么在设置寄存器时钟使能位之后,会被延迟为
2个 AHB 时钟周期; - 最后,如果外设映射到 APB
上,那么在设置寄存器时钟使能位之后,会被延迟为
2个 APB 时钟周期;
解决方案是在每个 _HAL_RCC_PPP_CLK_ENABLE()
宏定义当中插入一个对于外设寄存器的虚拟读取。
内外部晶振与锁相环配置
内/外部晶振与锁相环 包括 HSE、HSI、LSE、LSI、PLL、CSS、MCO:
| 英文缩写 | 英文全名 | 功能描述 |
|---|---|---|
| HSI | high-speed internal | 直接使用 16 MHz 工厂校准的 RC
振荡电路或者通过 PLL 锁相环作为系统时钟源; |
| LSI | low-speed internal | 32 KHz 低功耗 RC
振荡电路用于独立看门狗 IWDG
或者实时时钟 RTC 的时钟源; |
| HSE | high-speed external | 直接使用 4 ~ 26MHz
晶振或者通过 PLL 锁相环作为系统时钟源,也可以作为 RTC 时钟源; |
| LSE | low-speed external | 32 KHz 晶振作为 RTC
实时时钟源; |
| PLL | phase Locking Loop | 以 HSI 或 HSE
作为时钟的锁相环,具有两个不同用途的输出时钟,一种用于输出高达
168 MHz 的高速系统时钟,另一种用于生成全速 USB OTG 的
48 MHz 的时钟、随机模拟发生器的 ≤ 48 MHz
的时钟、安全数字输入输出接口 SDIO 的 ≤ 48 MHz 的时钟; |
| CSS | clock security system | 使能宏定义
__HAL_RCC_CSS_ENABLE() 之后,如果发生 HSE
时钟故障(直接使用 HSE 或者通过 PLL
作为系统时钟源),系统时钟将会自动切换到 HSI 并且产生中断,该中断链接至
Cortex-M4 的非可屏蔽中断异常向量; |
| MCO1 | microcontroller clock output | 用于通过 PA8 引脚输出 HSI、LSE、HSE、PLL 时钟(通过一个可配置的预分频器); |
| MCO2 | microcontroller clock output | 用于通过 PC9 引脚输出 HSE、PLL、SYSCLK 、PLLI2S 时钟(通过一个可配置的预分频器); |
系统总线时钟配置
系统总线时钟 包括 SYSCLK、AHB、APB1、APB2 总线:
- 系统时钟 SYSCLK 可以使用 HSI、HSE、PLL
多个时钟源,AHB 时钟
HCLK是由系统时钟经过可配置的预分频器派生而来,作为微控制器核心的主要时钟源,而内存和外设则被映射至 AHB 总线(挂载有 DMA、GPIO 等外设);除此之外,APB1(PCLK1)和 APB2(PCLK2)时钟则是通过可配置预分频器,从 AHB 时钟派生而来作为映射到这些总线上的外设时钟;通过调用HAL_RCC_GetSysClockFreq()函数,可以方便的检索到这些时钟的频率状态; - STM32F401CC 的 SYSCLK 与
HCLK 最高频率为
84 MHz、PCLK2 为84 MHz、PCLK1 为42 MHz,具体可以根据 MCU 的功率因素,相应的调整最高运行频率;
HAL 库 API
寄存器结构体
RCC_OscInitTypeDef 被定义在
stm32f4xx_hal_rcc.h 头文件当中:
| RCC_OscInitTypeDef 结构体成员 | 功能描述 |
|---|---|
uint32_t RCC_OscInitTypeDef::OscillatorType |
当前所需要配置的振荡器类型,该参数可以是
RCC_Oscillator_Type 里的值; |
uint32_t RCC_OscInitTypeDef::HSEState |
HSE
的新状态,该参数可以是 RCC_HSE_Config 里的值; |
uint32_t RCC_OscInitTypeDef::LSEState |
LSE
的新状态,该参数可以是 RCC_LSE_Config 里的值; |
uint32_t RCC_OscInitTypeDef::HSIState |
HSI
的新状态,该参数可以是 RCC_HSI_Config 里的值; |
uint32_t RCC_OscInitTypeDef::HSICalibrationValue |
HSI 校准微调值,默认为
RCC_HSICALIBRATION_DEFAULT,该参数必须是位于 Min_Data = 0x00 和 Max_Data = 0x1F
之间的一个数值; |
uint32_t RCC_OscInitTypeDef::LSIState |
LSI
的新状态,该参数可以是 RCC_LSI_Config 里的值; |
RCC_PLLInitTypeDef RCC_OscInitTypeDef::PLL |
锁相环 PLL 结构体参数; |
RCC_ClkInitTypeDef 被定义在
stm32f4xx_hal_rcc.h 头文件当中:
| RCC_ClkInitTypeDef 结构体成员 | 功能描述 |
|---|---|
uint32_t RCC_ClkInitTypeDef::ClockType |
当前所需要配置的时钟,该参数可以是
RCC_System_Clock_Type 里的值; |
uint32_t RCC_ClkInitTypeDef::SYSCLKSource |
将时钟源 SYSCLKS
用于系统时钟,该参数可以是 RCC_System_Clock_Source
里的值; |
uint32_t RCC_ClkInitTypeDef::AHBCLKDivider |
AHB 时钟 HCLK
的分频器,该时钟由系统时钟 SYSCLK 派生而来,可以是
RCC_AHB_Clock_Source 里的值; |
uint32_t RCC_ClkInitTypeDef::APB1CLKDivider |
APB1 时钟 PCLK1
的分频器,该时钟由 AHB 时钟 HCLK 派生而来,可以是
RCC_APB1_APB2_Clock_Source 里的值; |
uint32_t RCC_ClkInitTypeDef::APB2CLKDivider |
APB2 时钟 PCLK2
的分频器,该时钟由 AHB 时钟 HCLK 派生而来,可以是
RCC_APB1_APB2_Clock_Source 里的值; |
初始化与反向初始化
| 函数名称 | 功能描述 |
|---|---|
HAL_RCC_DeInit() |
重置 RCC 时钟为默认状态; |
HAL_RCC_OscConfig() |
根据 RCC_OscInitTypeDef
当中的指定参数初始化 RCC 振荡器; |
HAL_RCC_ClockConfig() |
根据 RCC_ClkInitStruct
当中指定的参数初始化微控制器、AHB、APB 总线的时钟; |
外设控制函数
| 函数名称 | 功能描述 |
|---|---|
HAL_RCC_MCOConfig() |
选择 MCO1/PA8
引脚或者MCO2/PC9 引脚上输出的时钟源; |
HAL_RCC_EnableCSS() |
打开时钟安全系统(CSS,Clock Security System); |
HAL_RCC_DisableCSS() |
关闭时钟安全系统(CSS,Clock Security System); |
HAL_RCC_GetSysClockFreq() |
返回 syscclk 时钟频率; |
HAL_RCC_GetHCLKFreq() |
返回 HCLK 时钟频率; |
HAL_RCC_GetPCLK1Freq() |
返回 PCLK1 时钟频率; |
HAL_RCC_GetPCLK2Freq() |
返回 PCLK2 时钟频率; |
HAL_RCC_GetOscConfig() |
通过内部 RCC 寄存器配置
RCC_OscInitStruct; |
HAL_RCC_GetClockConfig() |
通过内部 RCC 寄存器配置
RCC_ClkInitStruct; |
HAL_RCC_NMI_IRQHandler() |
用于处理 RCC 的 CSS 时钟安全系统中断请求; |
HAL_RCC_CSSCallback() |
RCC 时钟安全系统 CSS 的中断回调函数; |
示例代码
GPIO 通用输入输出
外设分析
每个通用 GPIO 端口都拥有四个 32
位配置寄存器(GPIOx_MODER、GPIOx_OTYPER、GPIOx_OSPEEDR、GPIOx_PUPDR),两个
32
位数据寄存器(GPIOx_IDR、GPIOx_ODR),一个
32
位的设置与重置寄存器(GPIOx_BSRR),一个
32 位的锁定寄存器(GPIOx_LCKR),两个 32
位可复用功能的选择寄存器(GPIOx_AFRH、GPIOx_AFRL)。根据每个
GPIO 端口的硬件特性,它们可以分别被配置为如下几种工作模式:
| 中文名称 | 英文名称 |
|---|---|
| 浮空输入 | Input floating |
| 上拉输入 | Input pull-up |
| 下拉输入 | Input pull-down |
| 模拟功能 | Analog |
| 带上下拉的开漏输出 | Output open-drain with pull-up or pull-down capability |
| 带上下拉的推挽输出 | Output push-pull with pull-up or pull-down capability |
| 带上下拉的可复用推挽 | Alternate function push-pull with pull-up or pull-down capability |
| 带上下拉的可复用开漏 | Alternate function open-drain with pull-up or pull-down capability |
输入配置
当 GPIO
作处于输入模式时:输出缓冲区被禁用;施密特触发器的输入被激活;根据
GPIOx_PUPDR 寄存器的设置决定上下拉电阻是否激活;在 AHB
时钟周期当中,每个 GPIO
引脚的状态值将会被采样至输入数据寄存器;通过读取输入数据寄存器,就可以获得
GPIO 的状态;浮空/上拉/下拉输入的配置如下图所示:

输出配置
当 GPIO
作处于输出模式时:输出缓冲区被启用(在开漏模式下,输出寄存器激活
N-MOS,但是输出寄存器当中的 1 会让端口保持高阻抗状态,此时
P-MOS
永远不会被激活;而在推挽模式下,输出寄存器激活
N-MOS,但是输出寄存器当中的 1 会激活
P-MOS;),施密特触发器的输入被激活,弱上下拉电阻是否被激活取决于
GPIOx_PUPDR 寄存器的值;在每个 AHB 时钟周期,GPIO
引脚上的状态值将会被采样至输入数据寄存器;通过访问输入数据寄存器可以获得
GPIO
的状态,而通过输出数据寄存器可以获得最后一次被写入的值;输出的配置如下图所示:

复用功能配置
当 GPIO
被编程为复用功能模式时:输出缓冲区可以被配置为开漏或者推挽模式,此时输出缓冲区由外设的信号驱动,施密特触发器的输入被激活,弱上下拉电阻是否被激活取决于
GPIOx_PUPDR 寄存器的设置;GPIO
引脚上的状态值将会被采样至输入数据寄存器;通过访问输入数据寄存器,就可以获得
GPIO 引脚上的状态值;复用功能的配置如下图所示:

模拟配置
当 GPIO
端口被编程为模拟配置:输出缓冲区被禁用;施密特触发器的输入被禁用,为
GPIO
引脚的每个模拟值提供零消费,施密特触发器的输出被强制定义为一个常数值
0;弱上下拉电阻被禁用;此时对输入数据寄存器进行读取操作,所获得的值为
0;高阻态模拟配置如下图所示:

API 描述
外设特性
根据数据手册当中每一个 GPIO 端口的硬件特性,每个 GPIO 端口可以被分别配置为:输入模式(Input mode)、模拟模式(Analog mode)、输出模式(Output mode)、复用功能模式(Alternate function mode)、外部中断事件线(External interrupt/event lines)。
复位期间和复位以后,复用功能和外部中断线没有激活,并且 GPIO
端口被配置为浮空输入模式;所有 GPIO
引脚都拥有可以被激活的内部弱上下拉电阻;在输出或者可复用模式下,每个
GPIO
都可以被配置为开漏或者推挽模式,并且输入输出速度可以通过
VDD 的值进行选择。
所有 GPIO
端口都拥有外部中断/事件能力,但是必须配置为输入模式才能够进行使用。所有可用的
GPIO 引脚,都被连接到了 EXTI0 ~ EXTI15 共 16
条外部中断/事件线。外部中断事件控制器,可以通过 23 个边缘检测器(其中 16
线连接至
GPIO)生成事件/中断请求(每条输入线都可以被独立配置为指定类型的中断/事件),并且触发相应的事件(上升、下降或者两者兼有),其中每一条输入线都可以单独进行屏蔽。
驱动使用
- 使用函数
__HAL_RCC_GPIOx_CLK_ENABLE()使能 GPIO 的 AHB 时钟源; - 使用
HAL_GPIO_Init()配置 GPIO 引脚;- 通过
GPIO_InitTypeDef结构体的Mode成员配置 IO 模式; - 通过
GPIO_InitTypeDef结构体的Pull成员激活上下拉电阻; - 如果选择输出模式或者复用模式,
GPIO_InitTypeDef结构体的Speed成员用于配置速度; - 如果选择复用模式,
GPIO_InitTypeDef结构体的Alternate成员用于配置 GPIO 引脚的复用功能; - 当 GPIO 引脚以 ADC 通道或 DAC 输出方式使用时,则需要使用模拟模式;
- 如果选择外部中断/事件,
GPIO_InitTypeDef的Mode成员可以用于选择中断和事件的类型,并且相应的触发事件(上升、下降或者两者兼有);
- 通过
- 选择外部中断/事件模式的情况下,使用
HAL_NVIC_SetPriority()配置映射到 EXTI 线的 NVIC IRQ 优先级,并且通过HAL_NVIC_EnableIRQ()启用; - 使用
HAL_GPIO_ReadPin()可以获得输入模式下引脚的电平状态; - 使用
HAL_GPIO_WritePin()或者HAL_GPIO_TogglePin()在输出模式下设置引脚的电平状态; - 使用
HAL_GPIO_LockPin()在下一次重置之前一直锁定引脚配置; - 在复位期间和复位之后,GPIO 复用功能没有激活,并且 GPIO 引脚被配置为浮空输入模式(除了 JTAG 引脚);
- 当 LSE 晶振关闭时,LSE 晶振引脚
OSC32_IN/PC14和OSC32_OUT/PC15可以被配置为 GPIO 引脚,因为 LSE 功能的优先级要高于 GPIO 功能; - 当 HSE 晶振关闭时,HSE 晶振引脚
OSC_IN/PH0和OSC_OUT/PH1可以被配置为 GPIO 引脚,因为 HSE 功能的优先级同样高于 GPIO 功能;
HAL 库 API
stm32f4xx_hal_gpio.h 头文件当中定义了
GPIO_InitTypeDef:
| GPIO_InitTypeDef 结构体成员 | 功能描述 |
|---|---|
uint32_t GPIO_InitTypeDef::Pin |
需要配置的振荡器,该参数可以是
GPIO_pins_define 的任意值; |
uint32_t GPIO_InitTypeDef::Mode |
指定所选引脚的工作模式,该参数可以是
GPIO_mode_define 里的值; |
uint32_t GPIO_InitTypeDef::Pull |
指定所选引脚的上拉或下拉电阻激活状态,取值可以为
GPIO_pull_define 里的值; |
uint32_t GPIO_InitTypeDef::Speed |
指定所选引脚的工作速度,该参数可以是
GPIO_speed_define 里的值; |
uint32_t GPIO_InitTypeDef::Alternate |
连接外设的指定引脚,该参数为
GPIO_Alternate_function_selection 里的值; |
寄存器结构体
初始化与反向初始化
| 函数名称 | 功能描述 |
|---|---|
HAL_GPIO_Init() |
基于 GPIO_Init
当中指定的参数初始化 GPIOx 外设; |
HAL_GPIO_DeInit() |
反向初始化 GPIOx
外设寄存器为默认重置值; |
IO 操作函数
| 函数名称 | 功能描述 |
|---|---|
HAL_GPIO_ReadPin() |
读取指定输入端口的引脚状态; |
HAL_GPIO_WritePin() |
设置或者清除指定的数据端口位; |
HAL_GPIO_TogglePin() |
切换指定的 GPIO 引脚状态; |
HAL_GPIO_LockPin() |
锁定 GPIO 引脚配置寄存器; |
HAL_GPIO_EXTI_IRQHandler() |
该函数用于处理 EXTI 中断请求; |
HAL_GPIO_EXTI_Callback() |
EXTI 线检测回调函数; |
NVIC 与 EXTI 中断
TIM 定时器
SysTick 系统滴答定时器
DMA 直接存储控制
RTC 实时时钟
USART 通用同/异步收发
I²C 内置集成电路总线
SPI 串行外设接口
| 函数名称 | 功能描述 |
|---|---|
基于 HAL 与 LL 的 UINIO-MCU-STM32F401 开发实践




