基于 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 种类型:

  1. (红色)电源引脚:其中 VDD 和 VSS 是数字电源,主要为片内的数字外设供电;VDDA/VREF+ 和 VSSA/VREF- 为模拟电源,主要为片内的模拟外设供电,同时作为 AD/DA 转换器的电压基准;VBAT 用于连接外部的备用电池,确保片内的实时时钟在掉电之后,依然能够正常工作;VCAP_1 用于片内电压调节器输出,通常会接入一枚 2.2uF 电容,然后再连接至 GND;
  2. (蓝色)复位与启动模式引脚:NRST 为复位引脚,低电平有效;启动模式引脚包括 BOOT0 和 PB2/BOOT1,通过其电平组合来配置微控制器的启动模式;
  3. (绿色)时钟引脚:OSC32_IN 和 OSC32_OUT 用于连接芯片外部的低速时钟,通常是一枚工作频率为 32.768 kHz 的晶振;而 OSC_IN 和 OSC_OUT 则用于连接外部高速时钟,通常是工作频率为 4mHz ~ 26mHz 范围的晶振;
  4. (橙色)仿真调试引脚:PA13 和 PA14 作为串行线调试(SWD,Serial Wire Debug)接口;
  5. (其它)通用输入输出引脚:除了作为 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 的重置标志位,以及备份域当中的寄存器之外,其它所有寄存器都会被重置为默认值,系统复位通常发生于如下场景:
    1. 复位引脚 NRST 处于低电平状态时,即外部复位;
    2. 窗口看门狗计数条件结束,即窗口看门狗复位;
    3. 独立看门狗计数条件结束,即独立看门狗复位;
    4. 进入待机或者停止模式时,所发生的低功耗管理复位;
    5. 出现软件复位的情况,复位源可以通过检查 RCC 时钟控制状态寄存器 RCC_CSR 的复位标志来进行识别;
  • 电源复位(Power Reset):发生如下事件时就会产生电源复位:
    1. 发生电源开启或者关闭,以及出现低电压的时候;
    2. 退出待机模式的时候;
  • 备份域复位(Backup Domain Reset):备份域仅拥有 2 种类型的复位,分别发生在如下的场景:
    1. 设置 RCC 备份域控制寄存器 RCC_BDCR 的 BDRST 位,触发软件复位的情况;
    2. 电源 \(V_{DD}\) 或者 \(V_{BAT}\) 关闭之后再重新上电的时候;

总线架构 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 固件驱动程序都会包含有如下三种类型的数据结构:

  1. 外设操作结构体:PPP_HandleTypeDef 是 HAL 驱动程序当中主要的实现结构,用于配置外设、注册和嵌入外设相关的结构体与变量,例如 stm32f4xx_hal_usart.c 固件库文件当中定义的 USART_HandleTypeDef 结构体:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    typedef 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;
  2. 初始化与配置结构体:PPP_InitTypeDef 结构体定义在通用固件驱动程序的 .h 头文件当中;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef 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)
  3. 指定流程结构体: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
    7
    HAL_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 */
    #if defined(STM32F427xx) || defined(STM32F437xx) || defined(STM32F429xx) || defined(STM32F439xx)
    HAL_StatusTypeDef HAL_FLASHEx_OB_SelectPCROP(void);
    HAL_StatusTypeDef HAL_FLASHEx_OB_DeSelectPCROP(void);
    #endif /* STM32F427xx || STM32F437xx || STM32F429xx || STM32F439xx || */

下面表格总结了不同类型的 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
2
3
HAL_StatusTypeDef HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *Init) {
/* GPIO 初始化体 */
}

每个外设驱动程序当中都定义有处理中断和特定时钟配置的宏,这些宏会被导出到外设驱动的头文件,以便于扩展文件使用,这些用于处理中断和特定时钟配置的宏如下所示:

宏定义 功能描述
__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
2
3
STATUS = XX | (YY << 16)
/* 或者 */
STATUS = XX | (YY << 8) | (YY << 16) | (YY << 24)

外设 PPP 的指针在调用 HAL_PPP_Init() 之前有效,初始化函数会在修改指针字段之前进行检查:

1
2
3
4
HAL_PPP_Init(PPP_HandleTypeDef)
if (hppp == NULL) {
return HAL_ERROR;
}

可以使用条件式宏定义或者伪代码宏定义:

  • 条件式宏定义:
    1
    #define ABS(x) (((x) > 0) ? (x) : -(x))
  • 伪代码宏定义(多指令宏):
    1
    2
    3
    4
    5
    #define __HAL_LINKDMA(__HANDLE__, __PPP_DMA_FIELD_, __DMA_HANDLE_) \
    do { \
    (__HANDLE__)->__PPP_DMA_FIELD_ = &(__DMA_HANDLE_); \
    (__DMA_HANDLE_).Parent = (__HANDLE__); \
    } while (0)

中断处理程序与回调函数

除了各种 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 组成:

  1. 初始化与反向初始化函数 HAL_PPP_Init()、HAL_PPP_DeInit():初始化函数 HAL_PPP_Init() 用于初始化外设并且配置底层硬件资源,主要是时钟、GPIO、AF 以及可能的 DMA 与中断,而反向初始化函数 HAL_PPP_DeInit() 则用于恢复外设的默认状态,释放底层硬件资源;
  2. IO 操作函数 HAL_PPP_Read()、HAL_PPP_Write()、HAL_PPP_Transmit()、HAL_PPP_Receive():通过读/写操作来访问外设上的各种负载数据;
  3. 控制函数 HAL_PPP_Set()、HAL_PPP_Get():控制函数用于动态调整外设的配置,以及设置其它的操作模式;
  4. 状态与错误函数 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
2
3
4
5
6
/* stm32f4xx_hal_flash_ex.c/h */
#if defined(STM32F427xx) || defined(STM32F437xx) || defined(STM32F429xx) ||
defined(STM32F439xx)
HAL_StatusTypeDef HAL_FLASHEx_OB_SelectPCROP(void);
HAL_StatusTypeDef HAL_FLASHEx_OB_DeSelectPCROP(void);
#endif /* STM32F427xx ||STM32F437xx || STM32F429xx || STM32F439xx */

添加产品系列的功能

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

1
2
3
4
5
/* stm32f4xx_hal_adc_ex.c/h */
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);

添加新的外设

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

1
#define HAL_NEWPPP_MODULE_ENABLED

更新现存的通用 API

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

更新现存的数据结构

HAL 固件库的外设数据结构(例如 PPP_InitTypeDef)可以拥有不同字段,这些数据结构被定义在扩展头文件当中,并通过 STM32 微控制器型号进行分隔:

1
2
3
4
5
#if defined(STM32F401xx)
typedef struct {
(…)
} PPP_InitTypeDef;
#endif /* STM32F401xx */

源文件包含关系

通用 HAL 固件驱动程序的 stm32f4xx_hal.h 头文件,包含有整个 HAL 固件库的配置,它既是用户源代码文件 main.h 唯一包含的头文件,同时也使得 HAL 固件库的 .c 源文件能够使用其它 HAL 库资源,下面的示意图展示了源文件之间的这种依赖关系:

公用资源定义

头文件 stm32f4xx_hal_def.h 当中定义了 HAL 固件库里的公用资源,例如公用的枚举、结构体、宏定义,其中最为重要的是枚举类型 HAL_StatusTypeDef。

  1. HAL 状态被几乎所有 API 使用,用于返回当前 API 操作的状态,其具有如下 4 个可能的值:
    1
    2
    3
    4
    5
    6
    Typedef enum {
    HAL_OK = 0x00,
    HAL_ERROR = 0x01,
    HAL_BUSY = 0x02,
    HAL_TIMEOUT = 0x03
    } HAL_StatusTypeDef;
  2. HAL 锁同样也被所有 API 使用,用于防止意外的访问共享资源,其具有如下 2 个可能的值:
    1
    2
    3
    4
    typedef enum {
    HAL_UNLOCKED = 0x00, /*!<Resources unlocked */
    HAL_LOCKED = 0x01 /*!< Resources locked */
    } HAL_LockTypeDef;
  3. 通用的宏定义,例如 HAL_MAX_DELAY:
    1
    #define HAL_MAX_DELAY 0xFFFFFFFF
    链接名称为 PPP 的外设至 DMA 结构体指针的宏:
    1
    2
    3
    4
    5
    #define __HAL_LINKDMA(__HANDLE__, __PPP_DMA_FIELD_, __DMA_HANDLE_) \
    do { \
    (__HANDLE__)->__PPP_DMA_FIELD_ = &(__DMA_HANDLE_); \
    (__DMA_HANDLE_).Parent = (__HANDLE__); \
    } while (0)

注意:除此之外,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void SystemClock_Config(void) {
RCC_ClkInitTypeDef RCC_ClkInitStruct;
RCC_OscInitTypeDef RCC_OscInitStruct;
/* 使能 HSE 晶振,并且以其作为时钟源激活 PLL */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 25;
RCC_OscInitStruct.PLL.PLLN = 336;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 7;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
/* 选择 PLL 作为系统时钟源,并且配置 HCLK、PCLK1、PCLK2 分频器 */
RCC_ClkInitStruct.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5);
}

初始化 MSP

外设的初始化是通过 HAL_PPP_Init() 完成的,而外设所使用硬件资源的初始化是通过调用 MSP 回调函数 HAL_PPP_MspInit() 来执行的,MspInit 回调函数用于执行 RCC、GPIO、NVIC、DMA 等各种附加硬件资源相关的低级初始化。所有带有指针的 HAL 驱动程序,都包含有两个分别用于初始化与反向初始化 MSP 的回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @brief 初始化 PPP 外设主堆栈指针 MSP
* @param hppp: 外设 PPP 指针
* @retval 无 */
void __weak HAL_PPP_MspInit(PPP_HandleTypeDef *hppp) {
/* 该函数不能修改,当需要使用回调时,可以在用户代码当中实现 HAL_PPP_MspInit */
}

/**
* @brief 反向初始化 PPP 外设主堆栈指针 MSP
* @param hppp: 外设 PPP 指针
* @retval 无 */
void __weak HAL_PPP_MspDeInit(PPP_HandleTypeDef *hppp) {
/* 该函数不能修改,当需要使用回调时,可以在用户代码当中实现 HAL_PPP_MspDeInit */
}

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
2
3
4
5
6
7
8
9
10
11
12
HAL_StatusTypeDef HAL_PPP_Transmit(PPP_HandleTypeDef *phandle, uint8_t pData, int16_tSize, uint32_tTimeout) {
if ((pData == NULL) || (Size == 0)) {
return HAL_ERROR;
}
(…)
while (data processing is running) {
if (timeout reached) {
return HAL_TIMEOUT;
}
}
(…) return HAL_OK;
}
中断模式

在中断模式下,HAL 函数会在启动数据处理并且响应中断之后返回处理的状态;操作的结束由声明为弱函数的回调来指示,该回调函数可以由用户自定义,以实时通知流程的完成情况;除此之外,用户还可以通过 HAL_PPP_GetState() 函数来获取处理状态。在中断模式下,驱动程序当中声明有下面四个函数:

  1. HAL_PPP_Process_IT():启用中断处理;
  2. HAL_PPP_IRQHandler():全局 PPP 外设中断;
  3. weak HAL_PPP_ProcessCpltCallback():处理完成回调函数;
  4. weak HAL_PPP_ProcessErrorCallback():处理错误回调函数;

一个中断模式下的处理过程,会调用到用户代码当中的 HAL_PPP_Process_IT(),以及 stm32f4xx_it.c 库文件当中的 HAL_PPP_IRQHandler,而 HAL_PPP_ProcessCpltCallback() 函数由于在 HAL 固件驱动当中被声明为弱函数,这意味着用户可以在应用程序当中再次进行声明,下面是一个代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* main.c */
UART_HandleTypeDef UartHandle;
int main(void) {
/* 设置用户参数 */
UartHandle.Init.BaudRate = 9600;
UartHandle.Init.WordLength = UART_DATABITS_8;
UartHandle.Init.StopBits = UART_STOPBITS_1;
UartHandle.Init.Parity = UART_PARITY_NONE;
UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
UartHandle.Init.Mode = UART_MODE_TX_RX;
UartHandle.Init.Instance = USART3;
HAL_UART_Init(&UartHandle);
HAL_UART_SendIT(&UartHandle, TxBuffer, sizeof(TxBuffer));
while (1)
;
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
}
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) {
}

/* stm32f4xx_it.c */
extern UART_HandleTypeDef UartHandle;
void USART3_IRQHandler(void) {
HAL_UART_IRQHandler(&UartHandle);
}
DMA 模式

HAL 可以通过 DMA 执行数据处理,并且在启用相应的 DMA 中断之后返回处理状态;操作的结束由一个声明为弱函数的回调来标识,用户可以自定义该回调函数,以便实时通知处理情况。除此之外,用户还可以通过 HAL_PPP_GetState() 函数来获取处理状态;在 DMA 模式下,驱动程序当中主要声明有如下四个函数:

  1. HAL_PPP_Process_DMA():启用 DMA 处理
  2. HAL_PPP_DMA_IRQHandler():外设 PPP 使用的 DMA 中断;
  3. __weak HAL_PPP_ProcessCpltCallback():处理完成回调函数;
  4. __weak HAL_PPP_ErrorCpltCallback():处理错误回调函数;

一个 DMA 模式下的处理过程,需要调用用户文件当中的 HAL_PPP_Process_DMA(),以及 stm32f4xx_it.c 当中的 HAL_PPP_DMA_IRQHandler();除此之外,DMA 的初始化在 HAL_PPP_MspInit() 回调函数当中完成;用户同样也可以将 DMA 指针关联到外设 PPP 的指针,因而所有使用到 DMA 的外设驱动程序指针必须声明为下面的形式:

1
2
3
4
5
6
7
typedef struct {
PPP_TypeDef *Instance; /* 寄存器基地址 */
PPP_InitTypeDef Init; /* 外设 PPP 通信参数 */
HAL_StateTypeDef State; /* 外设 PPP 通信状态 */
(…)
DMA_HandleTypeDef *hdma; /* 关联的 DMA 指针 */
} PPP_HandleTypeDef;

以 UART 外设为例,其对应的初始化过程如下面代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(void) {
/* 设置用户参数 */
UartHandle.Init.BaudRate = 9600;
UartHandle.Init.WordLength = UART_DATABITS_8;
UartHandle.Init.StopBits = UART_STOPBITS_1;
UartHandle.Init.Parity = UART_PARITY_NONE;
UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
UartHandle.Init.Mode = UART_MODE_TX_RX;
UartHandle.Init.Instance = UART3;
HAL_UART_Init(&UartHandle);
(..)
}
void HAL_USART_MspInit(UART_HandleTypeDef *huart) {
static DMA_HandleTypeDef hdma_tx;
static DMA_HandleTypeDef hdma_rx;
(…)
__HAL_LINKDMA(UartHandle, DMA_Handle_tx, hdma_tx);
__HAL_LINKDMA(UartHandle, DMA_Handle_rx, hdma_rx);
(…)
}

由于 HAL_PPP_ProcessCpltCallback() 函数在 HAL 固件驱动程序当中被声明为弱函数,这意味着用户可以在代码当中再次进行声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* main.c */
UART_HandleTypeDef UartHandle;
int main(void) {
/* 设置用户参数 */
UartHandle.Init.BaudRate = 9600;
UartHandle.Init.WordLength = UART_DATABITS_8;
UartHandle.Init.StopBits = UART_STOPBITS_1;
UartHandle.Init.Parity = UART_PARITY_NONE;
UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
UartHandle.Init.Mode = UART_MODE_TX_RX;
UartHandle.Init.Instance = USART3;
HAL_UART_Init(&UartHandle);
HAL_UART_Send_DMA(&UartHandle, TxBuffer, sizeof(TxBuffer));
while (1)
;
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *phuart) {
}
void HAL_UART_TxErrorCallback(UART_HandleTypeDef *phuart) {
}

/* stm32f4xx_it.c */
extern UART_HandleTypeDef UartHandle;
void DMAx_IRQHandler(void) {
HAL_DMA_IRQHandler(&UartHandle.DMA_Handle_tx);
}

HAL_USART_TxCpltCallback() 和 HAL_USART_ErrorCallback() 应当通过类似下面这样的语句链接到 HAL_PPP_Process_DMA() 函数的 DMA 传输完成与错误回调函数:

1
2
3
4
5
6
HAL_PPP_Process_DMA(PPP_HandleTypeDef *hppp, Params….) {
(…)
hppp->DMA_Handle->XferCpltCallback = HAL_UART_TxCpltCallback;
hppp->DMA_Handle->XferErrorCallback = HAL_UART_ErrorCallback;
(…)
}

超时与错误管理

超时管理

超时(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define LOCAL_PROCESS_TIMEOUT 100
HAL_StatusTypeDef HAL_PPP_Process(PPP_HandleTypeDef) {
(…)
timeout = HAL_GetTick() + LOCAL_PROCESS_TIMEOUT;
(…)
while (ProcessOngoing) {
(…)
if (HAL_GetTick() ≥ timeout) {
/* 处理没有被锁定 */
__HAL_UNLOCK(hppp);
hppp->State = HAL_PPP_STATE_TIMEOUT;
return HAL_PPP_STATE_TIMEOUT;
}
}
(…)
}

接下来的示例展示了如何在轮询函数当中使用超时时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HAL_PPP_StateTypeDef HAL_PPP_Poll(PPP_HandleTypeDef *hppp, uint32_t Timeout) {
(…)
timeout = HAL_GetTick() + Timeout;
(…)
while (ProcessOngoing) {
(…)
if (Timeout != HAL_MAX_DELAY) {
if (HAL_GetTick() ≥ timeout) {
/* 处理没有被锁定 */
__HAL_UNLOCK(hppp);
hppp->State = HAL_PPP_STATE_TIMEOUT;
return hppp->State;
}
}
(…)
}
错误管理

HAL 固件驱动程序在代码当中,实现了针对如下内容的检查:

  • 参数有效性:某些处理所使用的参数应当是有效并且已经定义过的,否则系统可能发生崩溃或者进入未定义状态,这些关键参数在使用前都会经过检查:
    1
    2
    3
    4
    5
    HAL_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
    6
    HAL_StatusTypeDef HAL_PPP_Init(PPP_HandleTypeDef *hppp) {
    /* 指针不能为空 */
    if (hppp == NULL) {
    return HAL_ERROR;
    }
    }
  • 超时错误:当发生超时错误时,会使用下面的语句进行处理:
    1
    2
    3
    4
    5
    6
    7
    while (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
2
3
4
5
6
7
8
9
typedef struct {
PPP_TypeDef *Instance; /* 外设 PPP 寄存器基地址 */
PPP_InitTypeDef Init; /* 外设 PPP 初始化参数 */
HAL_LockTypeDef Lock; /* 外设 PPP 对象锁定 */
__IO HAL_PPP_StateTypeDef State; /* 外设 PPP 状态 */
__IO HAL_PPP_ErrorTypeDef ErrorCode; /* 外设 PPP 错误代码 */
(…)
/* 外设 PPP 指定参数 */
} PPP_HandleTypeDef;

外设的状态以及错误状态码,总是会在返回一个错误之前进行更新:

1
2
3
4
PPP->State = HAL_PPP_READY;    /* 设置外设状态为就绪 */
PP->ErrorCode = HAL_ERRORCODE; /* 设置错误代码 */
_HAL_UNLOCK(PPP); /* 解锁该外设资源 */
return HAL_ERROR; /* 返回 HAL error 状态 */

HAL_PPP_GetError() 方法必须在中断模式下的错误回调函数里面使用:

1
2
3
4
void HAL_PPP_ProcessCpltCallback(PPP_HandleTypeDef *hspi) {
/* 检索错误代码 */
ErrorCode = HAL_PPP_GetError(hppp);
}
运行时检查

HAL 通过检查所有 HAL 驱动程序函数的输入值来实现运行时错误检查,该特性通过 assert_param 宏定义来实现,针对所有具有输入参数的 HAL 固件驱动函数,用于验证输入值是否处于参数的允许值范围以内。通过 assert_param 宏启用运行时检查以后,还需要使得 stm32f4xx_hal_conf.h 当中的 USE_FULL_ASSERT 处于未注释状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void HAL_UART_Init(UART_HandleTypeDef *huart) {
(..)
/* 参数检查 */
assert_param(IS_UART_INSTANCE(huart->Instance));
assert_param(IS_UART_BAUDRATE(huart->Init.BaudRate));
assert_param(IS_UART_WORD_LENGTH(huart->Init.WordLength));
assert_param(IS_UART_STOPBITS(huart->Init.StopBits));
assert_param(IS_UART_PARITY(huart->Init.Parity));
assert_param(IS_UART_MODE(huart->Init.Mode));
assert_param(IS_UART_HARDWARE_FLOW_CONTROL(huart->Init.HwFlowCtl));
(..)

/** @defgroup UART_Word_Length *
@{
*/
#define UART_WORDLENGTH_8B ((uint32_t)0x00000000)
#define UART_WORDLENGTH_9B ((uint32_t)USART_CR1_M)
#define IS_UART_WORD_LENGTH(LENGTH) (((LENGTH) == UART_WORDLENGTH_8B) ||
\ ((LENGTH) == UART_WORDLENGTH_9B))

如果向 assert_param 宏传递 false,那么就会调用 assert_failed 函数,并且返回调用失败的源文件名称以及相应的行号;而传递的是 true,则不会返回任何值。宏 ssert_param 定义在 stm32f4xx_hal_conf.h 头文件当中:

1
2
3
4
5
6
7
8
9
10
11
/* 被导出的宏定义 */
#ifdef USE_FULL_ASSERT

/* 宏 assert_param 用于函数的参数检查 */
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))

/* 被导出的函数 */
void assert_failed(uint8_t *file, uint32_t line);
#else
#define assert_param(expr) ((void)0)
#endif /* USE_FULL_ASSERT */

assert_failed 函数可以定义在 main.c 或者其它任意的用户源代码文件当中:

1
2
3
4
5
6
7
8
9
10
11
12
#ifdef USE_FULL_ASSERT
/** \
* @brief 发生 assert_param 错误时,报告源文件的名称与行号; \
* @param file: 指向源文件的指针; \
* @param line: assert_param 错误行号; \
* @retval None */
void assert_failed(uint8_t *file, uint32_t line) {
/* 用户可以添加自定义实现来报告错误文件名与行号, 例如 printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* 执行无限循环 */
while (1) {
}
}

注意:由于运行时检查会带来额外的性能开销,所以建议仅在开发调试阶段进行使用。

LL 底层库

底层(LL,low-layer)固件库驱动程序是一款比 HAL 更为接近硬件的库,其 API 并不会提供非关键特性,以及需要大量软件配置或者上层堆栈较为复杂的外设(例如 USB)的优化访问。它主要基于 STM32 片上外设的硬件特性来提供相关服务,这些服务准确的反映了硬件的功能,提供了官方手册所描述编程模型的一次性操作。由于其中并没有实现任何额外的处理业务,所以也就无需耗费额外的内存资源来保存状态、计数器、数据指针,所有操作都是通过修改硬件相关的寄存器来完成的。在 LL 固件库当中,主要提供有如下四种功能函数:

  1. 一组根据指定数据结构当中的参数,初始化外设主要特性的函数;
  2. 一组用于填充初始化数据结构各个字段重置值的函数;
  3. 执行外设反向初始化(将外设相关的寄存器恢复至默认值)的函数;
  4. 一组可以用于直接进行细粒度寄存器访问的内联函数;

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 源文件当中提供了三组外设初始化相关的函数:

  1. 用于初始化外设主要特性,并以指定数据结构作为参数的函数;
  2. 一系列采用各字段预设值,填充初始化数据结构的函数;
  3. 用于外设初始化与反向初始化的函数,所谓反向初始化就是将外设相关的寄存器恢复至默认值;

这些 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 函数)的时候,需要执行如下操作来进行运行时检查:

  1. 复制 stm32_assert_template.h 到用户工程目录,并将其重命名为 stm32_assert.h,该文件当中定义了运行时错误检查所需的 assert_param 宏;
  2. 在用户应用程序入口的 main.h 头文件当中包含 stm32_assert.h 文件;
  3. 在工具链编译器预处理,或者位于 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 两个固件库组合在一起使用时,同样可以达到直接操作寄存器的目的。虽然官方文档里允许进行这样的混合使用,但是应当考虑到如下因素:

  1. 建议避免同时通过 HAL 和 LL 的 API 操作相同的外设实例,如果必须要执行类似的操作,则需要修改 HAL 外设 PPP 结构体上的相应的私有字段设置;
  2. 对于不会修改指针字段(包含初始化结构体)的处理和操作,则可以让 HAL 库与 LL 库的 API 共同作用于相同的外设实例;
  3. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
[ Test ]
│ .cproject # Eclipse CDT 项目描述文件;
│ .mxproject # CubeMX 项目描述文件;
│ .project # Eclipse 项目描述文件;
│ STM32F401CCUX_FLASH.ld # STM32F401CCUx 的链接器脚本(256Kbytes FLASH 和 64Kbytes RAM),根据程序要求设置堆栈的大小与位置,如果使用外部存储器,则还可以用于设置存储器 Bank 的大小;
│ Test.ioc # MicroXplorer 代码生成器配置;
│
├─.settings
│ language.settings.xml # 集成开发环境语言配置文件;
│ stm32cubeide.project.prefs # STM32 Cube IDE 配置文件;
│
├─Core
│ ├─Inc
│ │ main.h # main.c 的头文件,包含了应用程序的通用定义;
│ │ stm32f4xx_hal_conf.h # HAL 配置模板文件,拷贝自 stm32f4xx_hal_conf.h 文件;
│ │ stm32f4xx_it.h # 中断处理程序 stm32f4xx_it.c 的头文件;
│ │
│ ├─Src
│ │ main.c # 应用程序的入口文件;
│ │ stm32f4xx_hal_msp.c # 包含主堆栈指针 MSP 的初始化与反向初始化代码;
│ │ stm32f4xx_it.c # 中断处理程序;
│ │ syscalls.c # STM32CubeIDE 最小系统调用;
│ │ sysmem.c # STM32CubeIDE 内存调用;
│ │ system_stm32f4xx.c # CMSIS Cortex-M4 外设访问层系统源文件;
│ │
│ └─Startup
│ startup_stm32f401ccux.s # STM32F401xCxx 基于 GCC 工具链的设备向量表,用于设置初始化堆栈 SP、程序计数器 PC、异常中断服务程序地址的向量表,最后调用程序入口文件 main.c;
│
└─Drivers
├─CMSIS
│ ├─Device
│ │ └─ST
│ │ └─STM32F4xx
│ │ ├─Include
│ │ │ stm32f401xc.h # CMSIS STM32F401xC 外设访问层头文件,包含了所有外设的数据结构与地址映射、外设寄存器的声明与位定义、宏访问外设的寄存器硬件;
│ │ │ stm32f4xx.h # CMSIS STM32F401xC 外设操作层头文件,用于选择 STM32F4xx 为目标应用程序、应用程序代码中是否使用外设驱动程序;
│ │ │ system_stm32f4xx.h # 用于 STM32F4xx 的 CMSIS Cortex-M4 设备系统源文件;
│ │ │
│ │ └─Source
│ │ └─Templates
│ └─Include
│ cmsis_armcc.h # CMSIS 编译器 ARMCC(Arm compiler 5)的头文件;
│ cmsis_armclang.h # CMSIS 编译器 armclang(Arm compiler 6)的头文件;
│ cmsis_compiler.h # CMSIS 编译器通用头文件;
│ cmsis_gcc.h # CMSIS 编译器 GCC 头文件;
│ cmsis_iccarm.h # CMSIS 编译器 ICCARM(针对 ARM 的 IAR 编译器)头文件;
│ cmsis_version.h # CMSIS 核心版本定义;
│ core_armv8mbl.h # CMSIS Armv8-M 基准核心外设访问层头文件;
│ core_armv8mml.h # CMSIS Armv8-M 主线核心外设访问层头文件;
│ core_cm0.h # CMSIS Cortex-M0 核心外设访问层头文件;
│ core_cm0plus.h # CMSIS Cortex-M0+ 核心外设访问层头文件;
│ core_cm1.h # CMSIS Cortex-M1 核心外设访问层头文件;
│ core_cm23.h # CMSIS Cortex-M23 核心外设访问层头文件;
│ core_cm3.h # CMSIS Cortex-M3 核心外设访问层头文件;
│ core_cm33.h # CMSIS Cortex-M33 核心外设访问层头文件;
│ core_cm4.h # CMSIS Cortex-M4 核心外设访问层头文件;
│ core_cm7.h # CMSIS Cortex-M7 核心外设访问层头文件;
│ core_sc000.h # CMSIS SC000 核心外设访问层头文件;
│ core_sc300.h # CMSIS SC300 核心外设访问层头文件;
│ mpu_armv7.h # 针对 Armv7-M MPU 的 CMSIS MPU API;
│ mpu_armv8.h # 针对 Armv8-M MPU 的 CMSIS MPU API;
│ tz_context.h # 针对 Armv8-M 的 TrustZone 管理;
│
└─STM32F4xx_HAL_Driver
├─Inc
│ │ stm32f4xx_hal.h # 包含 HAL 模块驱动程序的所有函数原型;
│ │ stm32f4xx_hal_cortex.h # CORTEX 模块的头文件;
│ │ stm32f4xx_hal_def.h # 包含 HAL 通用的预定义、枚举、宏和结构体的定义;
│ │ stm32f4xx_hal_dma.h # DMA 扩展模块的头文件;
│ │ stm32f4xx_hal_dma_ex.h # DMA 模块的头文件;
│ │ stm32f4xx_hal_exti.h # EXTI 模块的头文件;
│ │ stm32f4xx_hal_flash.h # FLASH 模块的头文件;
│ │ stm32f4xx_hal_flash_ex.h # FLASH 扩展模块的头文件;
│ │ stm32f4xx_hal_flash_ramfunc.h # FLASH RAMFUNC 驱动程序的头文件;
│ │ stm32f4xx_hal_gpio.h # FLASH 模块的头文件;
│ │ stm32f4xx_hal_gpio_ex.h # FLASH 扩展模块的头文件;
│ │ stm32f4xx_hal_pwr.h # PWR 模块的头文件;
│ │ stm32f4xx_hal_pwr_ex.h # PWR 扩展模块的头文件;
│ │ stm32f4xx_hal_rcc.h # RCC 模块的头文件;
│ │ stm32f4xx_hal_rcc_ex.h # RCC 扩展模块的头文件;
│ │ stm32f4xx_hal_tim.h # TIM 模块的头文件;
│ │ stm32f4xx_hal_tim_ex.h # TIM 扩展模块的头文件;
│ │
│ └─Legacy
│ stm32_hal_legacy.h # 包含 STM32Cube HAL 的常量宏,以及出于兼容性目的而维护的函数别名定义;
│
└─Src
stm32f4xx_hal.c # HAL 固件库初始化的公共部分;
stm32f4xx_hal_cortex.c # 管理 CORTEX 的初始化/反向初始化、外设控制函数;
stm32f4xx_hal_dma.c # 直接存储器访问(DMA,Direct Memory Access)的初始化/反向初始化、IO 操作、外设状态与错误函数;
stm32f4xx_hal_dma_ex.c # DMA 外设的扩展功能函数;
stm32f4xx_hal_exti.c # 扩展中断与事件控制器(EXTI,Extended Interrupts and events controller)的初始化/反向初始化、IO 操作函数;
stm32f4xx_hal_flash.c # 内置 FLASH 存储器的程序操作、存储控制、外设错误函数;
stm32f4xx_hal_flash_ex.c # 扩展 FLASH 存储器的编程操作函数;
stm32f4xx_hal_flash_ramfunc.c # 提供从内部 SRAM 执行的 FLASH 函数,包括在系统运行时停止/启动 FLASH 接口、启用/禁用 FLASH 休眠;
stm32f4xx_hal_gpio.c # 通用输入输出(GPIO,General Purpose Input/Output)的初始化/反向初始化、IO 操作函数;
stm32f4xx_hal_pwr.c # 功率控制器(PWR,Power Controller)的初始化/反向初始化、外设控制函数;
stm32f4xx_hal_pwr_ex.c # PWR 外设特性扩展函数;
stm32f4xx_hal_rcc.c # 复位和时钟控制(RCC,Reset and Clock Control)的初始化/反向初始化、外设控制函数;
stm32f4xx_hal_rcc_ex.c # RCC 扩展外设控制函数;
stm32f4xx_hal_tim.c # 定时器(Timer)的时基、PWM 输出、输入捕获、脉冲、编码器等相关功能的函数与配置;
stm32f4xx_hal_tim_ex.c # 定时器扩展外设相关的函数,例如时间霍尔传感器的初始化与启动、时间互补信号中断和死区时间配置、时间主从同步配置、定时器重映射功能配置;

STM32CubeIDE 自动生成的工程当中,默认的 main.h 和 main.c 源文件内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*========== main.h ==========*/
#ifndef __MAIN_H
#define __MAIN_H
#ifdef __cplusplus
extern "C" {
#endif

#include "stm32f4xx_hal.h"
void Error_Handler(void);

#ifdef __cplusplus
}
#endif
#endif

/*========== main.c ==========*/
#include "main.h"

void SystemClock_Config(void); // 初始化系统时钟

/* 应用程序入口 */
int main(void) {
HAL_Init(); // 重置所有外设,初始 Flash 接口和 Systick
SystemClock_Config(); // 配置系统时钟
while (1) {} // 无限循环
}

/* 系统时钟配置 */
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

/** 配置内部主稳压器的输出电压 */
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2);

/** 根据 RCC_OscInitTypeDef 结构体当中的指定参数,初始化 RCC 振荡器 */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}

/* 初始化 MCU、AHB、APB 总线时钟 */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK) {
Error_Handler();
}
}

/* 发生错误时执行该函数 */
void Error_Handler(void) {
/* 用户可以添加自定义的实现来报告 HAL 错误返回状态 */
__disable_irq();
while (1) {}
}

#ifdef
/* 返回发生 assert_param 错误源文件的名称与源行号,参数 file 是指向源文件名的指针,参数 line 是 assert_param 错误行号 */
void assert_failed(uint8_t *file, uint32_t line) {
/* 用户可以添加自定义实现来报告源文件名称和行号,例如:printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
}
#endif

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
λ openocd --help

Open On-Chip Debugger 0.11.0 (2021-07-29) [https://github.com/sysprogs/openocd]
Licensed under GNU GPL v2
libusb1 09e75e98b4d9ea7909e8837b7a3f00dda4589dc3
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Open On-Chip Debugger
Licensed under GNU GPL v2
--help | -h display this help
--version | -v display OpenOCD version
--file | -f use configuration file <name>
--search | -s dir to search for config files and scripts
--debug | -d set debug level to 3
| -d<n> set debug level to <level>
--log_output | -l redirect log output to file <name>
--command | -c run <command>

然后,将 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Open On-Chip Debugger 0.11.0 (2021-07-29) [https://github.com/sysprogs/openocd]
Licensed under GNU GPL v2
libusb1 09e75e98b4d9ea7909e8837b7a3f00dda4589dc3
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "swd". To override use 'transport select <transport>'.
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : CMSIS-DAP: SWD Supported
Info : CMSIS-DAP: FW Version = 2.0.0
Info : CMSIS-DAP: Interface Initialised (SWD)
Info : SWCLK/TCK = 1 SWDIO/TMS = 1 TDI = 0 TDO = 0 nTRST = 0 nRESET = 1
Info : CMSIS-DAP: Interface ready
Info : clock speed 2000 kHz
Info : SWD DPIDR 0x1ba01477
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f4x.cpu on 3333
Info : Listening on port 3333 for gdb connections

接下来,连接 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 下载:

  1. 首先,同时按住 开发板上的 BOOT0 和 NRST 按键;
  2. 然后,松开 NRST 按键;
  3. 最后,在 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
2
Error: failed to download Segment[0]
Error: failed to download the File

通过 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)时钟,每一个时钟源都可以按需进行独立开关,从而优化系统的功耗特性。

  1. 高速外部(HSE,High Speed External)时钟:使用外部晶振作为时钟源,可以选择的频率范围在 4mHz ~ 26mHz 之间;
  2. 高速内部(HSI,High Speed Internal)时钟:由内部的 16mHz RC 振荡器产生,可以直接用于系统时钟,或者是输入到锁相环,虽然其启动较为迅速,但是频率精度和温度飘移性能不如 HSE;
  3. 锁相环(PLL,Phase Locking Loop)时钟:STM32F401CC 拥有两个锁相环,其中主锁相环 PLL 由 HSE 或者 HSI 进行驱动,可以输出 84mHz 的高速系统时钟,或者是 48mHz 的全速 USB OTG 信号、小于或等于 48mHz 的随机模拟信号、小于或等于 48mHz 的 SDIO 信号;
  4. 低速外部(LSE,Low Speed External)时钟:通过 32.768kHz 的低速外部晶振产生,用于为计时与日历功能的实时时钟(RTC,Real-Time Clock)提供低功耗高精度的时钟信号源;
  5. 低速内部(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:

  1. 来自于锁相环 PLL48CLK 输出的全速 USB OTG 系统时钟(48mHz)与 SDIO 时钟(小于 48mHz);
  2. 为实现高质量的音频性能,I2S 时钟可以由指定的 PLL(PLLI2S)或者映射至 I2S_CKIN 引脚的外部时钟源派生而来;

STM32F401CC 采用 AHB 总线时钟 HCLK 除以 8 来作为 Cortex-M4 系统定时器 SysTick 的外部时钟源,通过配置 SysTick 的控制状态寄存器,可以选择 SysTick 与该时钟源还是 HCLK 时钟源一起工作。而 STM32F401CC 的定时器时钟频率则是由硬件自动进行设置,当 APB 分频器为 1 时,定时器时钟频率与所连接 APB 总线的时钟频率保持一致,否则就会被设置为 APB 时钟频率的 2 倍。

当硬件自动设置定时器的时钟频率时,根据 RCC_DCKCFGR 寄存器当中 TIMPRE 位的取值,可以具体划分为下面两种情况:

  1. 如果 TIMPRE 被重置:当 APB 预分器的分频系数配置为 1,则定时器时钟频率 TIM x CLK 被设置为 HCLK,否则就会被设置为所连接 APB 总线频率的 2 倍 TIM x CLK = 2 x PCLKx;
  2. 如果 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)生成事件/中断请求(每条输入线都可以被独立配置为指定类型的中断/事件),并且触发相应的事件(上升、下降或者两者兼有),其中每一条输入线都可以单独进行屏蔽。

驱动使用

  1. 使用函数 __HAL_RCC_GPIOx_CLK_ENABLE() 使能 GPIO 的 AHB 时钟源;
  2. 使用 HAL_GPIO_Init() 配置 GPIO 引脚;
    • 通过 GPIO_InitTypeDef 结构体的 Mode 成员配置 IO 模式;
    • 通过 GPIO_InitTypeDef 结构体的 Pull 成员激活上下拉电阻;
    • 如果选择输出模式或者复用模式,GPIO_InitTypeDef 结构体的 Speed 成员用于配置速度;
    • 如果选择复用模式,GPIO_InitTypeDef 结构体的 Alternate 成员用于配置 GPIO 引脚的复用功能;
    • 当 GPIO 引脚以 ADC 通道或 DAC 输出方式使用时,则需要使用模拟模式;
    • 如果选择外部中断/事件,GPIO_InitTypeDef 的 Mode 成员可以用于选择中断和事件的类型,并且相应的触发事件(上升、下降或者两者兼有);
  3. 选择外部中断/事件模式的情况下,使用 HAL_NVIC_SetPriority() 配置映射到 EXTI 线的 NVIC IRQ 优先级,并且通过 HAL_NVIC_EnableIRQ() 启用;
  4. 使用 HAL_GPIO_ReadPin() 可以获得输入模式下引脚的电平状态;
  5. 使用 HAL_GPIO_WritePin() 或者 HAL_GPIO_TogglePin() 在输出模式下设置引脚的电平状态;
  6. 使用 HAL_GPIO_LockPin() 在下一次重置之前一直锁定引脚配置;
  7. 在复位期间和复位之后,GPIO 复用功能没有激活,并且 GPIO 引脚被配置为浮空输入模式(除了 JTAG 引脚);
  8. 当 LSE 晶振关闭时,LSE 晶振引脚 OSC32_IN/PC14 和 OSC32_OUT/PC15 可以被配置为 GPIO 引脚,因为 LSE 功能的优先级要高于 GPIO 功能;
  9. 当 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 开发实践

http://www.uinio.com/Embedded/STM32F401/

作者

Hank

发布于

2019-07-26

更新于

2021-11-12

许可协议