基于 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 内核高性能基本型微控制器,拥有 256kBytesFlash 程序存储器、64kBytesSRAM 数据存储器、512BytesOTP 一次性可编程存储器,同时内置了 3I²C3USART4SPI 总线接口、2DMA 控制器、11Timer 定时器,以及 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 种不同的启动模式,这些模式可以通过组合 BOOT0PB2/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_BDCRBDRST 位,触发软件复位的情况;
    2. 电源 \(V_{DD}\) 或者 \(V_{BAT}\) 关闭之后再重新上电的时候;

总线架构 Bus

STM32F401CC 的系统架构由 32 位相互连接的多层 AHB 总线矩阵构成,其中 AHB 称为高级高性能总线(Advanced High-performance Bus),通常用于连接高速外设;而 APB 称为高级外围总线(Advanced Peripheral Bus),通常用于连接低速外设,具体细节可以参考下面的图示:

其中,整个系统架构主要包含 6主设备总线(Master):Cortex-M4 的 I-busD-busS-bus 总线,DMA1DMA2 内存总线,DMA2 外设总线。以及 5从设备总线(Slave):内部 Flash 内存的 ICodeDCode 总线、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 桥以及 APB1APB2,提供了 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 分配给片上的 AHB1AHB2APB1APB2 总线外设 512MB 0x4000 0000 ~ 0x5FFF FFFF

STM32F401 核心电路分析

MCU 微控制器

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

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

STM32F401CCVBAT 引脚连接到了 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_DPUSB_DN 连接到 STM32F401CCPA12/RXPA11/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)分别连接至 STM32F401CCPA13PA14 引脚,这两个引脚并没有连接到 PCB 排针上面,因而只可以作为仿真调试使用。

板载 NOR Flash

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

STM32F401CC 开发板电路预留有焊接该存储芯片的位置,其 VCCGND 引脚并联了一颗 0.1uF 滤波电容,用于滤除存储芯片工作电流发生变化时造成的电路纹波。而 SPI 片选(F_CS)、串行时钟输入(SCK)、数据输入(MOSI)、数据输出(MISO)四个引脚则作为 SPI 总线连接至 STM32F401CCPA4PA5PA7PA6,这四个引脚已经被连接到开发板上的排针,复用这几个引脚的功能时时需要特别注意。

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.cstm32f4xx_hal_irda.c 以及 stm32f4xx_hal_adc.hstm32f4xx_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 头文件,包含有 DBGMCURemap 和基于 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.cstm32f4xx_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 串口时, 该前缀可以是 USARTIRDAUARTSMARTCARD
  • 一个源文件当中使用的常量,就定义在该源文件内部,而多个源文件共用的常量定义在头文件当中;除外设驱动的函数参数以外,所有常量都需要大写;
  • 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 通用命名规则

对于共有的系统外设,无需使用指针或者实例对象,这个规则适用于 GPIOSYSTICKNVICRCCFLASH 外设,例如函数 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__) 检查指定的中断源

注意NVICSYSTICK 是 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 驱动程序源文件,以及与 SystemCortex-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_xxxxxLL_LPM_xxxxx
stm32f4xx_ll_utils.h/.c 该文件当中放置的是通用 API,可以用于读取设备 ID 和电子签名、时间基准与延迟管理、系统时钟配置;
stm32f4xx_ll_system.h 系统相关的操作,例如:LL_SYSCFG_xxxLL_DBGMCU_xxxLL_FLASH_xxxLL_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 固件库

HALLL 两个固件库组合在一起使用时,同样可以达到直接操作寄存器的目的。虽然官方文档里允许进行这样的混合使用,但是应当考虑到如下因素:

  1. 建议避免同时通过 HAL 和 LL 的 API 操作相同的外设实例,如果必须要执行类似的操作,则需要修改 HAL 外设 PPP 结构体上的相应的私有字段设置;
  2. 对于不会修改指针字段(包含初始化结构体)的处理和操作,则可以让 HAL 库与 LL 库的 API 共同作用于相同的外设实例;
  3. LL 驱动程序可以不受限制的与所有不基于指针对象的 HAL 驱动程序(包括RCC公用的 HALFlashGPIO)一起共同使用;

注意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 目录,而 DebugRelease 目录当中的 Test.bin 二进制文件就是将要被下载到 STM32F401CC 微控制器当中运行的固件。

工程源码结构

通过 STM32CubeIDE 新建一个 STM32F401CC 基本工程的项目代码结构如下所示,这些库文件主要拷贝自 STM32Cube_FW_F4_V1.26.2 固件库的 Drivers\CMSISDrivers\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.hmain.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-LinkSTM32F401CC 开发板的SWD 串行线调试接口(GNDSWCLKSWDIO3.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 和英特尔的 HEXELF 二进制文件格式,提供了 Debug 接口(JTAGSWD)和 Bootloader 接口(UARTUSB DFUI2CSPICAN)两种下载方式,能够同时支持 STM32 内部 FlashRAMOTP 以及外部存储器的下载编程。

进入 Bootloader 模式

STM32F401CC 开发板经过如下的 3 个操作步骤,就可以进入 Bootloader 下载模式,从而正常使用 STM32 Cube Programmer 执行 USBUART 下载:

  1. 首先,同时按住 开发板上的 BOOT0NRST 按键;
  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 芯片生产期间预置,用于通过 USARTCANUSBI²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.3VGND 则分别 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 拥有两个锁相环,其中主锁相环 PLLHSE 或者 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 时钟可以由指定的 PLLPLLI2S)或者映射至 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 总线频率的 2TIM x CLK = 2 x PCLKx
  2. 如果 TIMPRE置位:当 APB 分频器配置为 1 或者 2,则定时器时钟频率 TIM x CLK 被设置为 HCLK,否则就会被设置为所连接 APB 总线频率的 4TIM x CLK = 4 x PCLKx

API 描述

指定特性

当设备复位之后,STM32F401CC 将会通过内部高速振荡器(HSI 16MHz)启动运行,此时微控制器处于 Flash 0 等待状态,并且 Flash 开始预获取缓冲区、同时 D-CacheI-Cache 都被禁用,内部 SRAM、Flash、JTAG 之外的所有外设都将会被关闭。

  • AHB 高速总线APB 低速总线上都没有预分频器,这意味着映射到这些总线上的外设都会以 HSI 的频率运行;
  • 除了 SRAM 和 FLASH 之外的所有外设时钟都将会被关闭;
  • 除了 JTAG 引脚被分配用于调试之外,所有 GPIO 都将会处于浮空输入状态;

一旦设备从复位状态开始重新启动,则用户应用程序必须进行如下一系列操作:

  • 配置用于驱动系统时钟的时钟源;
  • 配置系统时钟频率与 Flash 设置;
  • 配置 AHB 和 APB 总线的预分频器;
  • 启动当前所要使用的外设时钟;
  • 配置非系统时钟派生外设的时钟源 (I2S、RTC、ADC、全速 USB OTG 或者 SDIO、RNG) ;

使用限制

管理 STM32F401CC 外设对于寄存器进行的各种读写操作,需要考虑到 RCC 外设时钟使能有效外设使能 之间的延迟:

  • 首先,这个延迟取决于外设映射;
  • 其次,如果外设映射到 AHB 上,那么在设置寄存器时钟使能位之后,会被延迟为 2AHB 时钟周期;
  • 最后,如果外设映射到 APB 上,那么在设置寄存器时钟使能位之后,会被延迟为 2APB 时钟周期;

解决方案是在每个 _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 时钟(通过一个可配置的预分频器);

系统总线时钟配置

系统总线时钟 包括 SYSCLKAHBAPB1APB2 总线:

  • 系统时钟 SYSCLK 可以使用 HSI、HSE、PLL 多个时钟源,AHB 时钟 HCLK 是由系统时钟经过可配置的预分频器派生而来,作为微控制器核心的主要时钟源,而内存和外设则被映射至 AHB 总线(挂载有 DMA、GPIO 等外设);除此之外,APB1(PCLK1)APB2(PCLK2)时钟则是通过可配置预分频器,从 AHB 时钟派生而来作为映射到这些总线上的外设时钟;通过调用 HAL_RCC_GetSysClockFreq() 函数,可以方便的检索到这些时钟的频率状态;
  • STM32F401CCSYSCLKHCLK 最高频率为 84 MHzPCLK284 MHzPCLK142 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 = 0x00Max_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_MODERGPIOx_OTYPERGPIOx_OSPEEDRGPIOx_PUPDR),两个 32 位数据寄存器GPIOx_IDRGPIOx_ODR),一个 32 位的设置与重置寄存器GPIOx_BSRR),一个 32 位的锁定寄存器GPIOx_LCKR),两个 32 位可复用功能的选择寄存器GPIOx_AFRHGPIOx_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_InitTypeDefMode 成员可以用于选择中断和事件的类型,并且相应的触发事件(上升、下降或者两者兼有);
  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/PC14OSC32_OUT/PC15 可以被配置为 GPIO 引脚,因为 LSE 功能的优先级要高于 GPIO 功能;
  9. 当 HSE 晶振关闭时,HSE 晶振引脚 OSC_IN/PH0OSC_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

许可协议