搜索
bottom↓
回复: 0

《STM32MP1 M4裸机HAL库开发指南》第九章 认识HAL库

[复制链接]

出0入234汤圆

发表于 2022-10-24 10:33:36 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2022-10-24 10:33 编辑

1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614 lQLPJxaFi2zaB4UWWrDAMgIsFEW2pwLb3abnwDMA_90_22.png
lQDPJxaFi2nfFizMjM0CbLCPlxn_FVheIQLb3aGrwFQA_620_140.jpg

lQLPJxaFi2nfFhLMkM0BXrDNvOUyeU_FPgLb3aGvQNIA_350_144.png

第九章  认识HAL库


本章节,我们来认识HAL库。HAL库文件夹是STM32Cube固件包中重要的一部分,因为HAL库比较特殊,所以我们将其作为独立的章节来专门讲解。
在讲解之前我们需要说明一点,分析HAL库中的源码或者工程中的文件,不管它有多么复杂,无非就是一些.c源文件和.h头文件,还有一些类似.s的启动文件,而头文件中遇见比较多的就是一些宏定义和函数声明,.c文件比较常见的就是一些函数的定义。在分析过程中,我们去理解这些文件重要的宏定义和函数的作用,然后把这些函数和文件的关系弄清楚,在日后的项目开发中,我们才可以亲手操刀去打造属于自己的项目设计,在使用HAL库的时候才不至于逻辑混乱,犹如庖丁解牛,游刃有余。
        本章将分为如下几个小节:
        9.1、HAL库初识;
        9.2、HAL库文件夹结构;
        9.3、如何使用HAL库;
        9.4、HAL库重要文件分析;
        9.5、章节小结;

9.1 HAL库初识
9.1.1 获取HAL库

在上一章节的STM32Cube固件包中有一个STM32MP1xx_HAL_Driver文件夹,该文件夹下存放的就是HAL库,我们所说的HAL库就是指里边的库文件。
第九章  认识HAL库528.png
图9.1.1. 1 HAL库在固件包中

在介绍HAL库文件前,我们先来理解下面几个概念。
9.1.2 什么是HAL库
在介绍HAL库之前,我们先来理解两个概念,什么是API?什么是句柄(Handle)?
前面我们多次提到API这个词,API 全称Application Programming Interface,翻译过来就是应用程序编程接口。接口,可以理解为是一些已经封装好了的可以被调用的功能函数或者方法,我们把这些函数放到我们的工程中,当我们要实现某个功能时,就可以在工程中找到对应的函数,然后进行调用。
句柄(Handle),在STM32的手册上经常看到这个词,可以理解为它是一个指针,或者是一些表的索引,或者是用于描述和标记某些资源的的标识,这些资源可以是函数、可以是一段内存、可以是一组数字、可以是一个外设等等,总之很广泛,通过句柄我们可以访问到打开的资源。我们在调用API函数的时候,可以利用句柄来说明要操作哪些资源。
HAL的含义,ST官方手册里的解释是Hardware abstraction layer,即硬件抽象层,单词的首字母组合起来就是HAL。HAL库是一些封装好的驱动程序,向下可以操作硬件,向上可以给用户提供可操作的接口。
第九章  认识HAL库1083.png
图9.1.2. 1 HAL库属于驱动程序

HAL库,笔者的理解是,ST把对不同系列MCU的操作经过一层一层的封装,将硬件进行抽象化表达出来,最后呈现给我们的就是HAL库。硬件抽象化,也就是将对寄存器的操作做了一系列封装,将外设抽象组织为句柄,使我们看不到寄存器的影子,最后分离出可以调用的API,使用者可以不去关注底层、不必关注复杂的硬件寄存器就可以进行编程,通过调用API和句柄操作可以实现对MCU的大部分外设进行操作,使用起来非常方便,而且很容易进行移植。
9.1.3 HAL库能做什么
        STM32开发方式中,可以直接配置寄存器或者可以使用官方的库来实现。
        我们在51单片机开发的时候就直接配置寄存器,但是到了32位单片机开发,如果开发大型项目,需要的功能外设很多,再使用这种方式就已经力不从心了。因为STM32的外设资源丰富,寄存器数量是51单片机寄存器的数十倍,那么多的寄存器根本无法记忆,而且开发中需要不停查找芯片手册,开发过程就显得机械和费力,完成的程序代码可读性差,可移植性不高,程序的维护成本变高了。当然了,采用直接配置寄存器方式开发会显得更直观,程序运行占用资源少。如果项目中只用少数几个外设,或者同一系列的芯片,项目组内部有一套成熟、可移植性高的祖传代码,开发起来就没那么困难了。
        为了简化开发人员的工作,减少开发工作时间和成本,针对STM32 系列芯片,ST官方推出了标准外设库(STD库)、HAL 库和LL 库 。在这些库中,有很多用结构体封装好的寄存器参数,有常用的表示参数的宏,还有封装好的对寄存器操作的API,开发者可以调用这些资源实现配置相应的寄存器,效率大大提高了。使用库的框架来开发,程序控制语句结构化,程序单元模块化,贴近人的思维,易于阅读,易于移植和维护。
        可以这么说,库是架设于寄存器与用户驱动层之间的代码,向下是直达硬件相关的寄存器,向上为用户提供配置寄存器的接口,用户代码通过这些接口来间接操作寄存器。
第九章  认识HAL库1940.png
图9.1.3. 1库和直接操作寄存器对比
9.1.4 HAL库和其它库有什么不同
前面我们提到,针对STM32 系列芯片,ST官方推出了标准外设库(STD库)、HAL 库和LL 库 ,那么HAL库和其它两种库有什么差别?下图是三种库对STM32系列产品的支持,目前STM32MP1仅支持HAL库。
第九章  认识HAL库2117.png
图9.1.4. 1 HAL库对ST系列产品的支持情况

1. 标准外设库
        STD(Standard Peripheral Libraries)标准外设库,它把实现功能中需要配置的寄存器以结构体的形式封装起来,使用者只需要配置结构体变量成员就可以修改外设的配置寄存器,比直接操作寄存器方便了不少。但标准外设库仍然接近于寄存器操作,它的方便也是针对某一系列芯片而言的,在不同系列芯片上使用标准外设库开发的程序可移植性比较差,例如,在F4上开发的程序移植到F3上,使用标准库是不通用的。目前STM32系列产品中仅F0-F4以及L1系列支持标准外设库。
2. HAL库
为了解决标准库存在的问题,ST 在标准库的基础上又推出了 HAL 库。个人认为,HAL库是用来取代之前的标准库的,因为这几年ST官方大力推广HAL 库,而且在ST新出的 STM32 芯片中, ST 直接只提供 HAL 库。
HAL库在设计的时候更注重软硬件分离,HAL库的目的应该就是尽量抽离物理层,HAL库的API集中关注各个外设的公共函数功能,以便定义通用性更好的API函数接口,从而具有更好的可移植性。HAL库写的代码在不同的STM32产品上移植,非常方便,效率得到提升。目前HAL库支持STM32各个系列产品。
3. LL库
LL库(Low Layer)是 ST 继HAL库之后新增的库,与 HAL 库捆绑发布,在前面的STM32CubeIDE第一个工程实验中,我们看到在STM32CubeMX里有选择使用HAL库还是LL库的选项。
LL库的英文名字翻译过来就是底层的意思,实际上LL 库更接近硬件层,它和STD库类似,都是直接操作的寄存器,只不过LL库可以在STM32Cube中实现。LL库提供一组轻量级、优化、面向专家的API,具有最佳的性能和运行时效率。LL库可以完全独立使用,也可以结合HAL库一起使用。当HAL库需要优化运行时,可以调用LL库来处理,而对于复杂的外设(例如:USB驱动),两者混合使用,才能正常驱动这个复杂的外设。
第九章  认识HAL库3001.png
图9.1.4. 2 HAL库和LL库

9.1.5 怎样学习HAL库
        (1)不管HAL库封装的有多好,本质上还是通过配置MCU/MPU的寄存器来实现我们想要的功能。所以我们学习HAL库的同时,还需要学习外设的工作原理和寄存器的配置方法,通过原理来理解HAL库是怎样实现我们想要的功能,要知其然更要知其所以然。
(2)HAL库不仅仅是底层驱动,它更是一套行业内可以公开和认可的架构。学习HAL库,我们要了解它的架构,了解库中每个文件夹下大概有哪些文件,文件之间的关系是什么,函数之间的调用关系是什么,调用条件是什么,常见的数据结构怎么用。
(3)学习HAL库,遇到疑问的地方可以查HAL的帮助文档,可以上网上查询相关说明,可以在Wiki上对共同的主题进行扩展或者探讨,多看相关的例程,跟着例程操作,多总结。
(4)要学会使用ST提供的优秀的开发工具,掌握STM32Cube工具套件的使用方法,熟话说得好,磨刀不误砍柴工。
不管怎样,我们的目的就是为了使用HAL库来开发,学会调用HAL库的API函数,配置对应外设按照我们的要求来工作,实现想要的功能。
下面,我们进入主题,分析HAL库。
9.2 HAL库文件夹结构
        STM32MP1xx_HAL_Driver文件下的Src(Source的简写)文件夹存放是所有外设的驱动程序源码,Inc(Include的简写)文件夹存放的是对应源码的头文件。Release_Notes.html是HAL库的版本更新说明信息。STM32MP157Cxx_CM4_User_Manual.chm是HAL库的用户手册。
第九章  认识HAL库3703.png
图9.2. 3 HAL库文件夹

        打开Inc文件夹,里边是一些以stm32mp1xx_hal和stm32mp1xx_ll_开头的.h文件,对应地,在Src下也有一堆以stm32mp1xx_hal和stm32mp1xx_ll_开头的.c文件,其中stm32f1xx_hal_开头的是HAL库文件, stm32f1xx_ll_开头的文件是LL库文件。
第九章  认识HAL库3903.png
图9.2. 4 HAL库文件

        HAL库下的文件很多,有一部分文件的功能可以归为一类,例如stm32mp1xx_hal_i2c.h/c、stm32mp1xx_hal_adc.h/c、stm32mp1xx_hal_dma.h/c等等这些文件,他们属于一些外设的配置文件,那么我们后面会以stm32mp1xx_hal_ppp.h/c来统称这些文件。有的是特殊文件,我们会重点介绍。HAL库关键文件介绍如下表:
lQLPJxbOmqxuCXTNAjfNAiCwcpCCWnOlkbsDU5EN_MAFAA_544_567.png
表9.2. 6 HAL库关键文件介绍

        除了HAL库的文件命名有规则以外,库中的函数以及句柄等的命名均有规律,如下表所示,其中PPP表示某个外设名。
lQLPJxbOmrOVEPXMlM0B47BNBy75YIzxigNTkRkOwHAA_483_148.png
表9.2. 7 HAL库的句柄和函数命名规律

        例如I2C相关的,如stm32mp1xx_hal_i2c.h、stm32mp1xx_hal_i2c.c、I2C_HandleTypeDef、HAL_I2C_Init()等。对于HAL的API函数,常见的有以下几种:
初始化/反初始化函数:HAL_PPP_Init(),HAL_PPP_DeInit()
外设读写数:HAL_PPP_Read(),HAL_PPP_Write(),HAL_PPP_Transmit()和HAL_PPP_Receive()
控制函数:HAL_PPP_Set (),HAL_PPP_Get ()
状态和错误:HAL_PPP_GetState (), HAL_PPP_GetError ()
HAL库封装的很多函数都是通过定义好的结构体将参数一次性传给所需的函数,参数也有一定的规律,主要有以下三种:
配置和初始化用的结构体
一般为PPP_InitTypeDef或PPP_ ConfTypeDef的结构体类型,根据外设的寄存器封装成易于理解和记忆的结构体成员。
特殊处理的结构体
专为不同外设而设置的,带有“Process”的字样,实现一些特异化的中间处理操作等。
外设控制句柄(PPP_Handler)
HAL驱动的重要参数,可以同时定义多个句柄结构以支持多外设多模式,HAL驱动的操作结果也可以通过这个句柄获得。有些HAL驱动的头文件中还定义了一些跟这个句柄相关的一些外设操作。如用外设结构体句柄与HAL定义的一些宏操作配合,即可实现一些常用的寄存器位操作,即可实现对外设的操作。
lQLPJxbOmrvtTIjNARbNAjiwewvrw-_kxpsDU5EmzED0AA_568_278.png
表9.2. 8 HAL库驱动部分与外设句柄相关的宏

但对于SYSTICK/NVIC/RCC/ GPIO这些外设,不使用PPP_HandleTypedef这类外设句柄进行控制,如HAL_GPIO_Init() 只需要初始化的GPIO编号和具体的初始化参数。
HAL_StatusTypeDef HAL_GPIO_Init (GPIO_TypeDef* GPIOx, GPIO_InitTypeDef *Init)
    {
    /*GPIO 初始化程序……*/
    }
        此外,HAL库中很多地方使用了回调函数,前面我们解释过回调函数可以被用户重定义,HAL库中的回调函数很多命名如下:
lQLPJxbOmsrUALTM6s0CQLBtB10rvOnW3gNTkT9xAAUA_576_234.png
表9.2. 9 HAL库驱动中常用的回调函数API

至此,我们大概对HAL库驱动文件的一些通用格式和命名规则有了初步印象,记住这些规则可以帮助我们快速对HAL库的驱动进行归类和判定这些驱动函数的用法。
9.3 如何使用HAL库
        HAL库下的文件实在是太多了,而且每个文件里的API函数也很多,我们学习的时候该从哪里看起呢?虽然文件多,API函数也多,不过我们可以将其进行分类,先将重要的配置文件了解一遍,然后再去学习某几个外设文件,只要学会了其中某部分外设,其它外设的就很类似了。
        针对庞大的HAL库,ST官方提供了HAL库的用户手册,例如STM32MP157系列的芯片,ST在HAL库里提供了STM32MP157Cxx_CM4_User_Manual.chm,我们双击打开此文件:
目录结构下有Modules、Data Structures、Files和Directories共4个主题,我们先来查看Modules。
Modules下有STM32MP1xx HAL_ Driver和STM32MP1xx_LL_Driver,分别对应HAL库和LL库的驱动,我们看HAL库下的驱动部分。
        打开GPIO列表下的IO operation functions / functions,查看里面的API函数接口描述,我们选择查看HAL_GPIO_WritePin函数,双击即可以打开此函数的说明,如下图所示。可以看到函数的定义、函数的作用、函数注意事项、参数说明以及函数在哪个文件,位置是在哪里等等:
第九章  认识HAL库8546.png
图9.3. 1 HAL库用户手册

        通过说明,函数是viod类型,没有返回值。第一个形参GPIO_TypeDef *GPIOx,x可以是A~K,通过此参数来选择对应的GPIO外围设备。第二个形参uint16_t GPIO_Pin是指定要写入的端口位(某个pin),这个参数可以是GPIO_PIN_x中的一个,其中x可以是0~15。第三个形参GPIO_PinState PinState是指定要写入到所选位的值,GPIO_PinState参数可以是枚举类型中的GPIO_PIN_RESET(复位)或者GPIO_PIN_SET(置位)之一。
        提示中说明,这个函数使用GPIOx_BSRR寄存器来允许原子的读/修改访问,即操作的是GPIOx_BSRR寄存器,关于此寄存器,我们可以查看参考手册中的说明。
所以,如果我们要将某个GPIO的某个pin置1的话,例如GPIOA的第2位,可以这样调用此函数:
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);
        如果要查找某个函数或者参数,可以直接在搜索框中进行搜索,如下图查找GPIO_PIN_RESET:
第九章  认识HAL库9074.png
图9.3. 2查找功能

        其它的主题,例如Data Structures、Files和Directories也可以给我们提供更多的帮助信息:
第九章  认识HAL库9171.png
图9.3. 3其它主题

        关于此用户手册的使用就讲解到这里,在后面的实验中,遇见不明白的地方也可以查看此手册。
9.4 HAL库重要文件分析
        下面我们先分析2个非常重要的文件:stm32mp1xx_hal_conf.h和stm32mp1xx_hal.c文件。其它文件,我们会在后面对应的实验章节做专门的讲解。
9.4.1 stm32mp1xx_hal_conf.h文件
        stm32mp1xx_hal_conf_template.h是HAL库的配置文件模板,用于用户自定义驱动。如果使用MDK来开发,用户可以复制此文件到自己的工程目录,然后将其改名字为stm32mp1xx_hal_conf.h,如果使用STM32CubeIDE来开发,在生成工程的时候已经自动为我们做好了这一步,我们不用管。下面我们将stm32mp1xx_hal_conf_template.h文件的代码分为3部分进行讲解。
1   #ifndef STM32MP1xx_HAL_CONF_H
2   #define STM32MP1xx_HAL_CONF_H
3  
4   #ifdef __cplusplus
5    extern "C" {
6   #endif
7   /*
8   * 模块选择
9   */
10  #define HAL_MODULE_ENABLED
11  #define HAL_ADC_MODULE_ENABLED
12  #define HAL_CEC_MODULE_ENABLED
13  ......
14  #define HAL_USART_MODULE_ENABLED
15  #define HAL_WWDG_MODULE_ENABLED
16  /*
17  * 注册回调函数选择
18  */
19  #define USE_HAL_ADC_REGISTER_CALLBACKS    0u
20  #define USE_HAL_CEC_REGISTER_CALLBACKS    0u
21  ......
22  #define USE_HAL_USART_REGISTER_CALLBACKS  0u
23  #define USE_HAL_WWDG_REGISTER_CALLBACKS   0u
24  /*
25  * 在HAL SPI驱动中激活CRC功能
26  */
27  #define USE_SPI_CRC                     1U
        第4行到第6行,对这个语法我们前面有讲解,这里不讲。
        第10行到第15行,这是将在HAL驱动程序中使用的模块列表,一旦使用了某个模块,那么就要使能相应的模块。例如,如果要使用TIMER功能,就在文件里添加:
#define HAL_TIM_MODULE_ENABLED
        相应的,在stm32mp1xx_hal_conf.h文件最后也要加载对应模块的头文件。如果的是在在STM32CubeIDE上通过图形界面来配置的话,生成的工程已经自动为我们处理好了,如需要修改,可以再返回配置界面重新配置。
#ifdef HAL_TIM_MODULE_ENABLED
#include "stm32mp1xx_hal_tim.h"
#endif /* HAL_TIM_MODULE_ENABLED */
        第19行到第23行,这段代码也是一些宏定义,表示将某个宏定义为0。这些宏定义有什么作用呢?例如USE_HAL_USART_REGISTER_CALLBACKS带有USART,应该和USART有关。打开Inc下的stm32mp1xx_hal_usart.h和stm32mp1xx_hal_usart.c文件,可以找到很多条件编译选项#if (USE_HAL_USART_REGISTER_CALLBACKS == 1)。例如stm32mp1xx_hal_usart.h文件中声明的函数指针,void (* TxHalfCpltCallback)(struct __USART_HandleTypeDef *husart)中,TxHalfCpltCallback是一个指向函数的指针,(* TxHalfCpltCallback)是一个带有参数*husart、返回类型为void的函数,参数husart也是一个指针:
1   #if (USE_HAL_USART_REGISTER_CALLBACKS == 1)
2     void (* TxHalfCpltCallback)(struct __USART_HandleTypeDef *husart);
3     void (* TxCpltCallback)(struct __USART_HandleTypeDef *husart);      
4     void (* RxHalfCpltCallback)(struct __USART_HandleTypeDef *husart);  
5     void (* RxCpltCallback)(struct __USART_HandleTypeDef *husart);      
6     void (* TxRxCpltCallback)(struct __USART_HandleTypeDef *husart);   
7     void (* ErrorCallback)(struct __USART_HandleTypeDef *husart);      
8     void (* AbortCpltCallback)(struct __USART_HandleTypeDef *husart);  
9     void (* RxFifoFullCallback)(struct __USART_HandleTypeDef *husart);  
10    void (* TxFifoEmptyCallback)(struct __USART_HandleTypeDef *husart);
11
12    void (* MspInitCallback)(struct __USART_HandleTypeDef *husart);      
13    void (* MspDeInitCallback)(struct __USART_HandleTypeDef *husart);   
14  #endif  /* USE_HAL_USART_REGISTER_CALLBACKS */
        stm32mp1xx_hal_usart.c文件中USART_InitCallbacksToDefault函数的参数就是stm32mp1xx_hal_usart.h文件中的函数的指针,例如TxHalfCpltCallback就是它的参数(别忘了,TxHalfCpltCallback有自己的参数)
1   #if (USE_HAL_USART_REGISTER_CALLBACKS == 1)
2   void USART_InitCallbacksToDefault(USART_HandleTypeDef *husart)
3   {
4     /* Init the USART Callback settings */
5     husart->TxHalfCpltCallback        = HAL_USART_TxHalfCpltCallback;     
6     husart->TxCpltCallback             = HAL_USART_TxCpltCallback;         
7     husart->RxHalfCpltCallback        = HAL_USART_RxHalfCpltCallback;      
8     husart->RxCpltCallback             = HAL_USART_RxCpltCallback;           
9     husart->TxRxCpltCallback          = HAL_USART_TxRxCpltCallback;         
10    husart->ErrorCallback              = HAL_USART_ErrorCallback;            
11    husart->AbortCpltCallback         = HAL_USART_AbortCpltCallback;        
12    husart->RxFifoFullCallback        = HAL_USARTEx_RxFifoFullCallback;     
13    husart->TxFifoEmptyCallback       = HAL_USARTEx_TxFifoEmptyCallback;     
14  }
15  #endif /* USE_HAL_USART_REGISTER_CALLBACKS */
        上面的就是回调(Callback)过程。(* TxHalfCpltCallback)叫做回调函数。回调函数就是一个通过函数指针调用的函数。以上的过程可以这么形容:
        A是回调函数,B是调用函数。A函数有参数a,B函数有参数b。参数b是一个指向A的函数指针,这样一来,就是B调用A,A有参数a,既然有参数就要给参数赋值,所以B函数内部给A的参数a赋值。那么,就是B调用A,A又利用B给A的参数a赋值,这个过程就叫做回调。
        总之,第19行到第23行,我们可以通过将0U改为1U来改变条件编译选项,从而实现我们前面说的回调过程。关于回调函数的使用,我们后面会进行讲解。
        我们继续往下看剩下的代码,这部分主要是时钟配置:
28  /*
29  * 时钟配置
30  */
31  #if !defined  (HSE_VALUE)
32    #define HSE_VALUE    24000000U                  /*高速外部振荡器HSE的值,24MHz */
33  #endif
34  
35  #if !defined  (HSE_STARTUP_TIMEOUT)
36    #define HSE_STARTUP_TIMEOUT  100U          /* HSE启动超时,100ms*/
37  #endif /* HSE_STARTUP_TIMEOUT */
38
39  #if !defined  (HSI_VALUE)
40    #define HSI_VALUE            64000000U  /*高速内部振荡器HSI的值,64MHz */
41  #endif
42
43  #if !defined  (HSI_STARTUP_TIMEOUT)
44    #define HSI_STARTUP_TIMEOUT  5000U          /*HSI启动超时,5000ms */
45  #endif /* HSI_STARTUP_TIMEOUT */  
46
47  #if !defined  (LSI_VALUE)
48    #define LSI_VALUE    32000U                           /*低速内部振荡器LSI的值,32KHz */
49  #endif      
50  
51  #if !defined  (LSE_VALUE)
52    #define LSE_VALUE     32768U    /*低速外部振荡器LSE的值,32.768KHz */
53  #endif
54
55  #if !defined  (LSE_STARTUP_TIMEOUT)
56    #define LSE_STARTUP_TIMEOUT  5000U          /*LSE启动超时,5000ms */
57  #endif /* LSE_STARTUP_TIMEOUT */
58
59  #if !defined  (CSI_VALUE)
60    #define CSI_VALUE    4000000U                         /*低功耗内部振荡器CSI的值,4MHz */
61  #endif
62
63  #if !defined  (EXTERNAL_CLOCK_VALUE)
64    #define EXTERNAL_CLOCK_VALUE    12288000U /*外部时钟的值,单位是Hz */
65  #endif
        STM32MP157的M4内核有5个时钟可以用:2个外部振荡器HSE、LSE和3个内部振荡器HSI、CSI、LSI。
        HSE (High-speed external oscillator)是高速外部振荡器,可通过外接有源晶振驱动。官方HSE_VALUE默认是配置24Hz,这个参数表示外部高速晶振的频率,如果要使用外部晶振,请根据板子上外部焊接的晶振频率来配置,正点原子STM32MP157开发板的核心板上外接了24MHz有源晶振。
        LSE (Low-speed external oscillator)是低速外部振荡器,通过外接有源或者无源晶振驱动,官方默认配置32.768 kHz。正点原子STM32MP157开发板的核心板上外接了32.768KHz无源晶振,主要用于驱动RTC 实时时钟。
        HSI (High-speed internal oscillator)是高速内部振荡器,频率可以是8、16、32、64 MHz。这里默认配置为64MHz。
        CSI (Low-power internal oscillator)是低功耗内部振荡器,这里频率默认配置为 4MHz。
        LSI是内部低速振荡器,这里频率默认配置为32 kHz,实际值可能会因为电压和温度而变化。使用内部时钟优势是成本低,缺点是精度差。
        这里是默认的值,如果我们不配置时钟,M4内核会默认采用HSI作为时钟源,频率为64MHz。如果我们配置时钟,在MDK下时手动配置,如果是使用STM32CubeMX或者STM32CubeIDE来开发的话,可以直接在Clock Configuration图形界面来配置,配置好以后保存,然后生成工程,在生成的stm32mp1xx_hal_conf.h文件中,这里的值就会跟着变。关于时钟的配置,我们后面会有专门的章节来讲解。
        下面我们看最后部分代码:
66  /*
67  * HAL系统配置
68  */
69  #define  VDD_VALUE                                    3300U                  /* VDD的值,单位是mv */  
70  /*  滴答定时器初始中断优先级 */
71  #define  TICK_INT_PRIORITY   ((uint32_t)(1U<<4U) - 1U)                        
72  
73  #define  USE_RTOS                      0U
74  #define  PREFETCH_ENABLE              0U
75  #define  INSTRUCTION_CACHE_ENABLE   0U
76  #define  DATA_CACHE_ENABLE            0U
77  /*
78  * 模块库文件引用
79  */
80  #ifdef HAL_RCC_MODULE_ENABLED
81   #include "stm32mp1xx_hal_rcc.h"
82  #endif /* HAL_RCC_MODULE_ENABLED */
83  ......
84  #ifdef HAL_WWDG_MODULE_ENABLED
85   #include "stm32mp1xx_hal_wwdg.h"
86  #endif /* HAL_WWDG_MODULE_ENABLED */
87  /*
88  * 断言配置
89  */
90  #ifdef  USE_FULL_ASSERT
91    #define assert_param(expr) ((expr) ? (void)0U : \                                                                                                 assert_failed((uint8_t *)__FILE__, __LINE__))
92    void assert_failed(uint8_t* file, uint32_t line);
93  #else
94    #define assert_param(expr) ((void)0U)
95  #endif /* USE_FULL_ASSERT */   
96      
97  #ifdef __cplusplus
98  }
99  #endif
100
101 #endif /* STM32MP1xx_HAL_CONF_H */
        第69行表示VDD值3.3V;
        第71行,不同的固件库版本,可能此值不一样,这里表示Systick(滴答定时器)的中断优先级(默认最低为15),((uint32_t)(1U<<4U) - 1U)为0X0F(十进制的值为15)。这里插入一些内容,Systick属于M4内核外设,在前面的core_cm4.h文件中有定义。内核外设和其它片上外设不一样,它们的中断优先级有所差别,内核外设的中断优先级等级为0~15,数值越低,中断优先级越高。片上外设也有自己的中断优先等级,数值越低等级越高。
        以上是stm32mp1xx_hal_conf_template.h文件中的配置,如果用STM32CubeMX生成的工程,此值为0,也就是说STM32CubeMX默认配置Systick为最高优先级0(数值越低优先级越高)。
第九章  认识HAL库16675.png
图7.4.1. 2 STM32CubeMX生成的代码

        不因为Systick是内核外设,中断优先级就比片上外设的外部中断优先级高,所以,如果工程里有用到Systick和其它的中断,例如工程中某个中断A调用了HAL_Delay函数,而HAL_Delay函数是通过Systick来实现计时的,如果中断A优先级比Systick高,就会导致A中断优先执行,而Systick中断服务函数一直未能执行,就会导致程序卡死的情况,所以应该设置Systick的中断优先级比A中断要高。在后面的MDK工程中我们可以通过手动设置此优先级。
如果使用STM32CubeIDE,可以直接在STM32CubeMX上通过用图形界面来配置:
第九章  认识HAL库17008.png
图7.4.1. 3 STM32CubeMX上配置中断优先级

        如上图,在之前的第一个工程实验中配置,Time base:System tick timer就是Systick的中断选项,勾选此项以后使能中断,然后再将Systick的中断的Preemption Priority(抢占优先)设置为1,修改好后按下“Ctrl+S”保存工程,在生成工程的stm32mp1xx_hal_conf.h文件中TICK_INT_PRIORITY值已经变为1。
第九章  认识HAL库17258.png
图7.4.1. 4中断优先级变了

        关于Systick我们后面有专门的章节做讲解。
        第73行,表示是否使用RTOS操作系统,目前HAL库不支持RTOS,所以这里默认配置为0。
        第74行到第75行,表示分别配置Flash预读取使能、指令缓存使能和数据缓存使能。M4内核没有可用的Flash,这里配置为0。
        第80到第85行,和最前面第10行到第15行介绍的相关,一旦使能了某个模块,就要加载对应的模块头文件。
        第90行到95行,表示断言。
        断言,就是一种代码调试技术,可以在调试阶段,帮助程序员选择满足规定范围的参数。比如某个参数只能选0或1两种,如果程序员写了3,那么在运行程序的时候就会给出报错提示。工程代码中加入了断言以后,虽然降低了程序的运行效率,但是方便了代码调试。一般程序在调试阶段采用Dubug版本,等项目开发完成以后,会给用户提供Release版本,Release版本的代码经过了各种优化,也去掉了所有的断言assert_param检验,最终的代码大小和运行速度都是最优的。
        第91行:
#define assert_param(expr) ((expr) ? (void)0U :assert_failed((uint8_t \                                                                                                                 *)__FILE__, __LINE__))
        包含了3目运算符,表示如果expr大于0,则assert_param(expr)为0,如果expr小于或等于0,则assert_param(expr)为函数assert_failed((uint8_t *)__FILE__, __LINE__),其中__FILE__、__LINE__这两个是宏定义,表示当前所在的文件名和行号, 用来指示出错的行数和文件。
        第91行,表示声明一个函数void assert_failed(uint8_t* file, uint32_t line),如果使用MDK来开发,需要此函数的话,可以在自己定义该函数。如果使用的STM32CubeIDE来开发,函数的实体在STM32CubeIDE生成的工程的main.c函数有定义,或者在官方的模板文件main.c中也可以找到,可以看到有定义assert_failed函数,函数的参数file表示发生错误的文件名,line表示发生错误的行号。
第九章  认识HAL库18263.png
图9.4.1. 5断言相关函数

        上图虽然main.c函数中有断言的代码,但是还没有开启断言,所以看到#ifdef  USE_FULL_ASSERT和#endif /* USE_FULL_ASSERT */之间的代码背景颜色呈现灰暗色。
        第94行,#define assert_param(expr) ((void)0U)中((void)0U)表示不执行任何操作。
        默认情况下宏USE_FULL_ASSERT是没有定义的,也就是没有开启断言功能,如果有需要,我们可以定义宏USE_FULL_ASSERT,然后我们就可以在代码中使用断言assert_param来检验程序中的参数是否正确了。
        启用断言以后,可以在assert_failed函数内部添加以下代码,使错误的文件名和对应的行号可以通过串口打印出来(如果要用串口打印,工程中必须先配置好串口的功能才可以打印信息)。
    printf("Wrong parameters value: file %s on line %ld\r\n", file, line);
第九章  认识HAL库18755.png
图9.4.1. 6添加判断函数

        下面我们分析HAL库中另一个重要的文件:stm32mp1xx_hal.c文件。
9.4.2 stm32mp1xx_hal.c文件
        stm32mp1xx_hal.c文件对应的头文件是Inc下的stm32mp1xx_hal.h文件。
stm32mp1xx_hal.c文件的内容比较多,它包括HAL库的初始化、系统滴答、基准电压配置、IO补偿、低功耗、EXTI配置等,下面我们分几个小节对该文件进行介绍。
1. HAL_Init函数
        HAL_Init函数主要用于初始化HAL库,该函数在程序中必须优先调用,在生成的工程中,main函数优先调用了HAL_Init 函数。
1   HAL_StatusTypeDef HAL_Init(void)
2   {
3     /* 设置中断优先级分组为4 */
4   #if defined (CORE_CM4)
5     HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
6   #endif
7
8     /* 更新SystemCoreClock全局变量 */
9     SystemCoreClock = HAL_RCC_GetSystemCoreClockFreq();
10
11    /* 使用滴答定时器作为时钟基准,配置1ms滴答(重置后默认的时钟源为HSI)*/
12    if(HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK)
13    {
14      return HAL_ERROR;
15    }
16
17    /* 初始化底层硬件 */
18    HAL_MspInit();
19
20    /*返回 HAL_OK */
21    return HAL_OK;
22  }
        HAL_Init 函数主要实现如下功能:
        (1)默认设置NVIC优先级分组为4。可以在IDE上通过配置来改变此优先级,后面的实验章节会详细介绍这部分。
        (2)配置SysTick(滴答定时器)每1ms产生一个中断。
        (3)系统复位以后,在跳到主程序main之前先执行startup_stm32mp1xx.s启动文件,启动文件显示调用了SystemInit函数,然后才跳到main函数,SystemInit函数主要是初始化FPU设置、配置SRAM中的向量表和禁用所有中断和事件,并没有初始化RCC。所以,在进入main函数以后,系统还是默认使用内部高速时钟源HSI在跑程序,HSI的频率默认是64MHz。直到调用系统时钟配置函数以后,时钟才会发生变化。
        (4)调用HAL_MspInit函数初始化底层硬件,HAL_MspInit函数在stm32mp1xx_hal.c文件里面做了弱定义:
        __weak void HAL_MspInit(void)
{
  /* 注意:这个函数不应该修改,当需要回调时,HAL_MspInit可以在用户文件中实现* /
}
        HAL_MspInit函数前有一个weak,表示对HAL_MspInit进行弱(weak)定义。弱就是表示此函数可以进行重写(重新定义),如果用户在其它地方重新定义一个同名函数,最终编译器编译的时候,就会选择用户定义的函数,如果用户没有重新定义这个函数(或者函数名字写错了),那么编译器就会默认执行弱定义的函数,并且编译器不会报错。是弱定义的函数都可以进行重写。关于弱定义的函数怎么使用,我们后面会有专门的例程进行讲解。
        如果使用MDK来开发,我们可以手动重新定义一个HAL_MspInit函数。
        如果使用STM32CubeIDE创建的工程,系统会生成stm32mp1xx_hal_msp.c文件,此文件中会重会新定义HAL_MspInit函数,此函数一般会打开AHB3外设的时钟,初始化中断优先级等,没有使用HAL_MspInit函数去初始化全部的底层硬件,而系统时钟初始化或者外设时钟初始化等操作其实是在main.c文件以及外设驱动文件中完成的。
        HAL_Init 函数的返回值是HAL_OK,表示成功。返回值是枚举常量,在stm32mp1xx_hal_def.h文件中有定义:
typedef enum
{
  HAL_OK       = 0x00U,
  HAL_ERROR    = 0x01U,
  HAL_BUSY     = 0x02U,
  HAL_TIMEOUT  = 0x03U
} HAL_StatusTypeDef;
2. HAL_DeInit函数
        HAL_DeInit函数主要用于复位HAL库的,不过函数中没有实现什么功能,如果有需要,可以在里边添加相应的代码。
1   HAL_StatusTypeDef HAL_DeInit(void)
2   {
3     /*重置所有外设*/
4     
5     /*对底层硬件进行初始化*/
6     HAL_MspDeInit();
7
8     /*返回功能状态*/
9     return HAL_OK;
10  }
        HAL_MspDeInit函数主要是对底层硬件初始化进行复位,和HAL_Init里面调用的HAL_MspInit函数是一对,它在stm32mp1xx_hal.c文件中有定义,只不过是弱定义,如果用户需要使用该函数,可以在用户文件中进行重写。
__weak void HAL_MspInit(void)
{
  /* 注意:这个函数不应该修改,当需要回调时,HAL_MspInit可以在用户文件中实现* /
}
3. HAL_InitTick函数
        HAL_InitTick函数主要用于初始化SysTick(系统滴答定时器)的时钟基准为1ms。函数前有weak弱定义,表示此函数可以被用户重新定义。STM32MP1有A7内核和M4内核,第4行到第48行是针对A7内核的代码,第51行到第73行是针对M4内核的代码。
1   __weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
2   {
3     /* 配置SysTick在1ms的时间产生一次中断 */
4   #if defined (CORE_CA7)
5   #if defined(USE_ST_CASIS)
6     HAL_SYSTICK_Config(SystemCoreClock/1000);
7   #elif defined (USE_PL1_SecurePhysicalTimer_IRQ)
8     /* 停止计时器 */
9     PL1_SetControl(0x0);
10
11    PL1_SetCounterFrequency(HSI_VALUE);
12
13    /* 初始化计数器 */
14    PL1_SetLoadValue(HSI_VALUE/1000);
15
16    /* 禁用相应的中断 */
17    IRQ_Disable(SecurePhysicalTimer_IRQn);
18    IRQ_ClearPending(SecurePhysicalTimer_IRQn);
19
20    /* 将定时器优先级设置为最低 (在MP1的A7的GIC中断中,只有7:3位可以实现) */
21    /* TickPriority基于16级优先级(来自MCUs),因此在7:4中设置,且位3=0 */
22    if (TickPriority < (1UL << 4))
23    {
24      IRQ_SetPriority(SecurePhysicalTimer_IRQn, TickPriority << 4);
25      uwTickPrio = TickPriority;
26    }
27    else
28    {
29      return HAL_ERROR;
30    }
31
32    /* 设置边沿触发的中断请求 */
33    IRQ_SetMode(SecurePhysicalTimer_IRQn, IRQ_MODE_TRIG_EDGE);
34
35    /* 启用相应的中断 */
36    IRQ_Enable(SecurePhysicalTimer_IRQn);
37
38    /* 启动定时器 */
39    PL1_SetControl(0x1);
40  #else
41    /* 设置计数器频率 */
42    PL1_SetCounterFrequency(HSI_VALUE);
43   // __set_CNTFRQ(HSI_VALUE);
44    /* 初始化计数器 */
45    PL1_SetLoadValue(0x1);
46   // __set_CNTP_TVAL(0x1);
47  #endif
48  #endif /* CORE_CA7 */
49
50
51  #if defined (CORE_CM4)
52  /* uwTickFreq是个枚举类型,如果检测到uwTickFreq为零,则返回1 */
53    if ((uint32_t)uwTickFreq == 0U)
54    {
55      return HAL_ERROR;
56    }
57
58    /* 将SysTick配置为在1ms的时间产生一次中断 */
59    if (HAL_SYSTICK_Config(SystemCoreClock /(1000U / uwTickFreq)) > 0U)
60    {
61      return HAL_ERROR;
62    }
63    /* 配置 SysTick的中断优先级 */
64    if (TickPriority < (1UL << __NVIC_PRIO_BITS))
65    {
66      HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
67      uwTickPrio = TickPriority;
68    }
69    else
70    {
71      return HAL_ERROR;
72    }
73  #endif /* CORE_CM4 */
74
75    /* 返回函数状态 */
76    return HAL_OK;
77  }
        本教程主要是介绍M4相关的实验,对于A7部分我们这里只是简单介绍。
        第4和第5行是条件编译选项,如果同时定义CORE_CA7和USE_ST_CASIS宏,则调用函数HAL_SYSTICK_Config(SystemCoreClock/1000)。其中SystemCoreClock在system_stm32mp1xx.c文件中有定义:
uint32_t SystemCoreClock = HSI_VALUE;
        HSI_VALUE 在文件stm32mp1xx_hal_conf.h文件中有定义:
  #if !defined  (HSI_VALUE)
    #define HSI_VALUE            64000000U  /* 内部高速振荡器HSI的值,64MHz */
  #endif
        HSI_VALUE为64MHz,则SystemCoreClock/1000等于64000Hz。
        HAL_SYSTICK_Config函数在stm32mp1xx_hal_cortex.c文件中有定义:
uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
{
   return SysTick_Config(TicksNumb);
}
        SysTick_Config函数在core_cm4.h文件中定义(SysTick属于内核的外设):
#if defined (__Vendor_SysTickConfig) && (__Vendor_SysTickConfig == 0U)

__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
  if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
  {
    return (1UL);  /* 函数非正常终止,退出,递减计数器不会再重载 */                                               
  }

  SysTick->LOAD  = (uint32_t)(ticks - 1UL);         /* 重新加载寄存器 */  
/* 设置Systick中断的优先级 */
  NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL);
  SysTick->VAL   = 0UL;                                                    /* 加载SysTick计数器值 */   
/*启用SysTick中断和SysTick定时器 */  
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                           SysTick_CTRL_TICKINT_Msk   |
                           SysTick_CTRL_ENABLE_Msk;                        
  return (0UL);                                                                           /* 函数正常终止 */   
}
        我们简单分析此段代码,SysTick定时器是一个24位向下递减计数器,启动后,LOAD寄存器的值赋给VAL 寄存器,VAL寄存器递减,当递减到0的时候,会产生一次中断,然后再从LOAD寄存器取值,然后再从所得值开始递减,递减到0的时候又产生一次中断,如此反复,从而实现计时。LOAD寄存器的值是从SysTick_Config函数的参数ticks获取的,根据上述分析,ticks值默认为64000,复位后,系统默认工作采用64MHz的内部高速时钟源HSI来工作,可以计算SysTick产生一次中断的时间为:(1/64MHz)*64000=1ms。关于SysTick定时器我们后面会有专门的章节进行讲解。
        第7行到第39行,如果A7内核使用Secure PL1 physical timer来计时的话,则先停止A7的计时、初始化计数器、禁用相应中断、设置中断优先级和触发条件、启动中断、启动定时器等操作。
        我们重点看M4部分。
        第51行,如果定义CORE_CM4宏,则初始化SysTick配置。
        第53行到第56行,主要是判断参数uwTickFreq是否为零。
         stm32mp1xx_hal.c文件中有定义:
HAL_TickFreqTypeDef uwTickFreq = HAL_TICK_FREQ_DEFAULT;
        而stm32mp1xx_hal.h文件中定义:
typedef enum
{
  HAL_TICK_FREQ_10HZ         = 100U,
  HAL_TICK_FREQ_100HZ        = 10U,
  HAL_TICK_FREQ_1KHZ         = 1U,
  HAL_TICK_FREQ_DEFAULT      = HAL_TICK_FREQ_1KHZ
} HAL_TickFreqTypeDef;
        所以,第59行到第62中的uwTickFreq值为1。根据前面的分析,SystemCoreClock的值为64000 000,所以HAL_SYSTICK_Config函数的参数是64000,HAL_SYSTICK_Config(64000))按照前面计算方法就是1ms,表示配置SysTick每隔1ms产生一次中断。至此,不管是A7内核还是M4内核,默认情况下,SysTick中断配置的都是1ms。
        第64到68行,形参TickPriority用于设置滴答定时器优先级。
HAL_InitTick函数可以通过HAL_Init()或者HAL_RCC_ClockConfig()重置时钟。在默认情况下,滴答定时器是时间基准的来源,这里再次强调,如果其他中断服务函数调用了HAL_Delay(),要注意,滴答定时器中断必须具有比调用了HAL_Delay()函数的其他中断服务函数的优先级高(数值较低),否则会导致滴答定时器中断服务函数一直得不到执行,从而卡死在这里。
4. 滴答定时器相关的函数
(1)HAL_IncTick函数
__weak void HAL_IncTick(void)
{
  uwTick += (uint32_t)uwTickFreq;
}
        函数前面有weak 定义,表示用户可以在别的文件中进行重定义。
        HAL_IncTick函数在滴答定时器时钟中断服务函数 SysTick_Handler中被调用,滴答定时器每隔1ms中断一次,所以此函数每1ms让全局变量uwTick计数值加1 。滴答定时器时钟中断服务函数在ST官方的工程模板中,文件名是stm32mp1xx_it.c。
第九章  认识HAL库26030.png
图9.4.2. 1 ST官方模板的中断函数文件

(2)HAL_GetTick函数
/* 获取全局变量uwTick当前计算值 */
__weak uint32_t HAL_GetTick(void)
{
#if defined (CORE_CA7)
#if defined (USE_ST_CASIS)
  return ( Gen_Timer_Get_PhysicalCount() / (HSI_VALUE/1000));
#elif defined (USE_PL1_SecurePhysicalTimer_IRQ)
  /* tick在SecurePhysicalTimer_IRQ处理程序中递增 */
  return uwTick;
#else
  /* 直接从64位CA7寄存器得到的tick值 */
  return ( PL1_GetCurrentPhysicalValue() / (HSI_VALUE/1000));
#endif
#endif /* CORE_CA7 */


#if defined (CORE_CM4)
  /* tick在systick处理程序中递增 */
  return uwTick;
#endif /* CORE_CM4 */
}
        函数前面有weak 定义,表示用户可以在别的文件中进行重定义。
        HAL_GetTick函数用于获取32位的全局变量 uwTick 当前的值,这个值每1ms加1,那么HAL_GetTick() 应该返回的就是自启动以来经过的毫秒数。uwTick可以以毫秒为单位提供时间,那么可以用它来计时,事实上很多HAL函数都依赖它来计时的,例如我们之前使用的HAL_Delay函数。
__weak void HAL_Delay(uint32_t Delay)
{
  uint32_t tickstart = HAL_GetTick();
  uint32_t wait = Delay;

  /* Add a freq to guarantee minimum wait */
  if (wait < HAL_MAX_DELAY)
  {
    wait += (uint32_t)(uwTickFreq);
  }

  while ((HAL_GetTick() - tickstart) < wait)
  {
  }
}
        SysTick中断服务函数 SysTick_Handler 通过调用 HAL_IncTick 实现 uwTick的值每隔 1ms增加 1。HAL_GetTick返回uwTick的值,在进入HAL_Delay函数时,先记录当前 uwTick 的值,并标记为tickstart,然后不断在循环中读取uwTick 的当前值,再与记录的tickstart进行减运算,当(HAL_GetTick() - tickstart)的差值等于或大于wait的时候,跳出空循环,此时(HAL_GetTick() - tickstart)得出的差值就是延时的毫秒数。
(3)HAL_GetTickPrio函数
        #if defined (CORE_CM4)
uint32_t HAL_GetTickPrio(void)
{
  return uwTickPrio;
}
        此函数就是获取滴答时钟优先级,如果想知道SysTick的中断优先级,可以在代码中添加此函数来获。HAL库中有很多的获取某个变量的函数,例如获取系统时钟频率的函数HAL_RCC_GetSystemCoreClockFreq,我们前面介绍的HAL_GetTick函数,获取定时器的计时数值函数__HAL_TIM_GET_COUNTER,还有获取串口中断标志位状态函数USART_GetFlagStatus,获取当前 RTC 时间HAL_RTC_GetTime等众多函数,我们可以利用这些函数获取我们想要的信息。
(4)HAL_SetTickFreq和HAL_GetTickFreq函数
        HAL_SetTickFreq 函数用于重新配置滴答定时器中断中断频率,HAL_GetTickFreq函数用于获取滴答定时器中断频率。
1   /* 设置滴答定时器中断频率 */
2   HAL_StatusTypeDef HAL_SetTickFreq(HAL_TickFreqTypeDef Freq)
3   {
4     HAL_StatusTypeDef status  = HAL_OK;
5     HAL_TickFreqTypeDef prevTickFreq;
6     assert_param(IS_TICKFREQ(Freq));
7
8     if (uwTickFreq != Freq)
9     {
10      /* 备份备份滴答定时器中断频率频率 */
11      prevTickFreq = uwTickFreq;
12
13      /* 更新被HAL_InitTick()使用的全局变量uwTickFreq */
14      uwTickFreq = Freq;
15
16      /* 应用新的滴答定时器中断频率  */
17      status = HAL_InitTick(uwTickPrio);
18
19      if (status != HAL_OK)
20      {
21        /* 恢复以前的滴答定时器中断频率 */
22        uwTickFreq = prevTickFreq;
23      }
24    }
25
26    return status;
27  }
28  /* 获取滴答定时器中断频率 */
29  HAL_TickFreqTypeDef HAL_GetTickFreq(void)
30  {
31    return uwTickFreq;
32  }
        在前面的HAL_InitTick函数介绍中,uwTickFreq的值默认为1,滴答定时器中断频率默认为1KHz(中断周期是1ms),如果我们要更改滴答定时器中断频率,可以通过HAL_SetTickFreq 函数来实现。我们先分析该函数的实现过程。
        参数Freq是枚举类型,可选值是100、10和1,它是我们要设置的滴答定时器中断频率。
        第5行,定义一个枚举类型变量prevTickFreq,表示频率,值可以是100、10和1。
        第6行,使用断言assert_param检查我们设置的参数Freq是否有效(参数Freq可以是100或10或1)
        第8到第24行,比较设置的参数Freq的值是否等于默认为1的uwTickFreq,如果不等于1,先备份uwTickFreq的值到新定义的uwTickFreq变量中,然后再将Freq赋值给uwTickFreq,如果此时HAL_InitTick返回状态正常,频率修改成功,如果此时HAL_InitTick返回状态不正常,那么uwTickFreq的值将不变,此时不能进行修改频率。
        第29行到32行表示获取此时的uwTickFreq值,即获取滴答定时器中断频率。
(5)HAL_SuspendTick函数
/* 挂起滴答定时器中断,全局变量uwTick计数停止 */
__weak void HAL_SuspendTick(void)
{
#if defined (CORE_CA7)
#elif defined (CORE_CM4)
  /* 禁止滴答定时器中断 */
  SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
#endif
}
/* 恢复滴答定时器中断,恢复全局变量uwTick计数 */
__weak void HAL_ResumeTick(void)
{
#if defined (CORE_CA7)
#elif defined (CORE_CM4)
  /* 使能滴答定时器中断 */
  SysTick->CTRL  |= SysTick_CTRL_TICKINT_Msk;
#endif
}
        默认情况,SysTick计时器是时间基的来源,它以固定的时间间隔产生中断,一旦HAL_SuspendTick函数被调用时,SysTick中断将被禁用,因此Tick增量被暂停。函数被声明为弱函数,可以在其他函数中被覆盖时使用。
5. HAL库版本相关的函数
相关函数声明如下,这些函数了解一下就好了,用得不多。
uint32_t HAL_GetHalVersion(void); /* 获取HAL库驱动程序版本 */
uint32_t HAL_GetREVID(void);       /* 获取设备修订标识符 */
uint32_t HAL_GetDEVID(void);       /* 获取设备标识符 */
6. HAL库调试功能相关函数
这些是调试功能相关的函数,可以在不同模式下使能或者关闭调试器。将对应函数添加到代码中就支持DEBUG在各种模式下,比如,HAL_EnableDBGStopMode,就可以在stop模式下进行调试。源码可在stm32mp1xx_hal.c文件中查看,函数声明如下:
void HAL_EnableDBGWakeUp(void)                        /* 启用调试模块(唤醒模式下) */
void HAL_DisableDBGWakeUp(void)                        /* 关闭调试模块(唤醒模式下) */
void HAL_EnableDBGSleepMode(void)                /* 启用调试模块(休眠模式下) */
void HAL_DisableDBGSleepMode(void)                /* 关闭调试模块(休眠模式下) */
void HAL_EnableDBGStopMode(void)                /* 启用调试模块(停止模式下) */
void HAL_DisableDBGStopMode(void)                /* 关闭调试模块(停止模式下) */
void HAL_EnableDBGStandbyMode(void)        /* 启用调试模块(待机模式下) */
void HAL_DisableDBGStandbyMode(void)        /* 关闭调试模块(待机模式下) */
7. 芯片内部电压基准相关函数
源码可在stm32mp1xx_hal.c文件中查看,函数声明如下:
void HAL_SYSCFG_VREFBUF_VoltageScalingConfig(uint32_t VoltageScaling);
void HAL_SYSCFG_VREFBUF_HighImpedanceConfig(uint32_t Mode);
void HAL_SYSCFG_VREFBUF_TrimmingConfig(uint32_t TrimmingValue);
HAL_StatusTypeDef HAL_SYSCFG_EnableVREFBUF(void);
void HAL_SYSCFG_DisableVREFBUF(void);
HAL_SYSCFG_VREFBUF_VoltageScalingConfig函数用于配置芯片内部电压基准大小,形参有四个值可以选择:
1)当形参为SYSCFG_VREFBUF_VOLTAGE_SCALE0时,
电压输出基准为2.048V,条件是VDDA >= 2.4V。
2)当形参为SYSCFG_VREFBUF_VOLTAGE_SCALE1时,
电压输出基准为2.5V,条件是VDDA >= 2.8V。
3)当形参为SYSCFG_VREFBUF_VOLTAGE_SCALE2时,
电压输出基准为1.5V,条件是VDDA >= 1.8V。
4)当形参为SYSCFG_VREFBUF_VOLTAGE_SCALE3时,
电压输出基准为1.8V,条件是VDDA >= 2.1V。
HAL_SYSCFG_VREFBUF_HighImpedanceConfig函数用于配置芯片内部电压是否与VREF+引脚连接,即是否选择高阻抗模式,有两个形参选择:
1)当形参为SYSCFG_VREFBUF_HIGH_IMPEDANCE_DISABLE,表示导通。
2)当形参为SYSCFG_VREFBUF_HIGH_IMPEDANCE_ENABLE,表示高阻抗,即不导通。
HAL_SYSCFG_VREFBUF_TrimmingConfig函数用于调整校准内部电压基准。
HAL_SYSCFG_EnableVREFBUF函数用于使能内部电压基准参考。
HAL_SYSCFG_DisableVREFBUF函数用于禁止内部电压基准参考。
8. 以太网PHY接口选择函数
        该函数用于以太网PHY接口的选择,可以是MII或RMII接口。
void HAL_SYSCFG_ETHInterfaceSelect(uint32_t SYSCFG_ETHInterface)
9. HAL_SYSCFG_AnalogSwitchConfig()函数
当PA0、PA1引脚复用为ADC的时候,还有一组对应的可选引脚ANA0、ANA1。该函数的作用就是用于模拟开关控制,用于切换这些可选的引脚。源码可在stm32mp1xx_hal.c文件中查看,函数声明如下:        
void HAL_SYSCFG_AnalogSwitchConfig(uint32_t SYSCFG_AnalogSwitch , uint32_t SYSCFG_SwitchState )
        参数SYSCFG_AnalogSwitch表示选择模拟开关,可以选:
        SYSCFG_SWITCH_PA0:选择PA0模拟开关
        SYSCFG_SWITCH_PA1:选择PA1模拟开关
        参数SYSCFG_SwitchState表示打开或关闭双垫之间的模拟开关,此参数可以是以下值之一或组合:
        SYSCFG_SWITCH_PA0_OPEN
        SYSCFG_SWITCH_PA0_CLOSE
        SYSCFG_SWITCH_PA1_OPEN
        SYSCFG_SWITCH_PA1_CLOSE
第九章  认识HAL库31917.png
图9.4.2.2模拟开关控制

        该函数操作了SYSCFG_PMCR寄存器,可以查看参考手册第14.3.2节了解有关配置模拟开关的详细信息。
10. Booster的使能和禁止函数(用于ADC)
源码可在stm32mp1xx_hal.c文件中查看,函数声明如下:
void HAL_SYSCFG_EnableBOOST(void)                /* 使能Booster */
void HAL_SYSCFG_DisableBOOST(void)                /* 禁止Booster */
如果使能Booster,当供电电压低于2.7V时,能够减少模拟开关总的谐波失真。这样就使得模拟开关的性能和供电正常的情况时一样,能够正常工作。
11. 启用或者禁止IO补偿函数
源码可在stm32mp1xx_hal.c文件中查看,函数声明如下:
void HAL_EnableCompensationCell(void)     /* 使能IO补偿单元 */
void HAL_DisableCompensationCell(void)    /* 关闭IO补偿单元,默认下是关闭的 */
这两个函数用于使能或者禁止IO补偿。I / O补偿单元仅可在设备供电时使用,且在电源电压为2.4V~3.6V时,使用IO补偿功能才有意义。默认情况下,不使用I / O补偿单元,当I / O输出缓冲区速度配置为50 MHz以上模式时,建议开启I/O补偿单元来减少对电源带来的噪音。
12. IO补偿、优化IO速度以及低功耗等相关函数
IO补偿、优化IO速度以及低功耗等相关函数,这里先不进行讲解了,后面用到我们再进行说明。
9.5 章节小结
        本章节是认识HAL库的重要环节,或许分析的过程有点枯燥,代码比较多,让人看得眼花缭乱。即使HAL库的文件和函数再多,我们通过去分析一些共性的东西,先挨个分析重要的文件以及函数,然后把他们串起来,脑海里形成最初的认识,在以后的学习和实验过程中,我们还可以回过头来再看看,加深理解。通过认识、理解、实践、再理解的过程,我们可以逐渐掌握必备的知识点。
        本章节我们重点讲解了HAL库的两个重要的文件:stm32mp1xx_hal_conf.h和 stm32mp1xx_hal.c文件,结合前面的第六章节的实验,下面我们将固件库的文件关系用一张简图来描述(图中省略了部分.c文件和.h文件),其中箭头指向的文件表示要调用某个头文件的文件,例如箭头由stm32mp1xx_hal_conf.h指向stm32mp1xx_hal.h,表示stm32mp1xx_hal.h调用stm32mp1xx_hal_conf.h。
第九章  认识HAL库33022.png
图9.5.1 固件库头文件关系

        stm32mp1xx_hal_conf.h头文件基本上是一些宏定义和条件编译,用于用户自定义驱动,外设驱动配置文件可以根据需要include相关的外设头文件,不过在STM32CubeIDE上开发的话,此文件一般不需要我们手动去改,一般是通过STM32CubeMX完成配置。
        stm32mp1xx_hal.c文件主要包括HAL库的初始化、系统滴答、基准电压配置、IO补偿、低功耗、EXTI配置、调试相关等函数的定义,其中weak定义的函数可以根据需要被用户在其它文件中进行重新定义。stm32mp1xx_hal.h头文件包含了HAL模型的所有驱动,用户的驱动文件或者main.h文件可以直接include此文件。
        关于HAL库中其它文件,我们会通过后面的实验来熟悉它们。
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-5-5 03:51

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表