基于 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.c stm32f4xx_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.c stm32f4xx_hal_ppp_ex.h |
外设/模块驱动程序扩展的 .c
源文件以及 .h 头文件,通常用于定义某个指定型号独有的
API,例如 stm32f4xx_hal_adc_ex.c 以及
stm32f4xx_hal_flash_ex.c ; |
stm32f4xx_hal.c stm32f4xx_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_Function HAL_PPP_FeatureFunction_MODE |
HAL_PPPEx_Function HAL_PPPEx_FeatureFunction_MODE |
HAL_PPPEx_Function HAL_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* PPPx LL_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* PPPx LL_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* PPPx LL_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* PPPx LL_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_BITNAME
LL_PPP{_CATEGORY}_IsItem_BITNAME_Action
LL_RCC_IsActiveFlag_LSIRDY
LL_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_BITNAME
DMA 启用 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}_IsItemAction
LL_ADC_Enable()
LL_ADC_StartCalibration()
LL_ADC_IsCalibrationOnGoing
LL_RCC_HSI_Enable()
LL_RCC_HSI_IsReady()
外设配置管理,即设置/获取外设的配置:
名称 示例 LL_PPP{_CATEGORY}_{Set/Get}ConfigItem
LL_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)时钟:由内部的
16mHz
RC 振荡器产生,可以直接用于系统时钟,或者是输入到锁相环,虽然其启动较为迅速,但是频率精度和温度飘移性能不如 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 开发实践