【正点原子MP157连载】第七章 认识HAL库-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

2023-11-16

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
在这里插入图片描述

第七章 认识HAL库

第四章生成的工程是基于HAL库的,这点我们在前面有提过,在第四章的操作中,我们通过第一个工程实验熟悉了STM32CubeIDE的基本使用方法。本章节,我们来认识HAL库。HAL库文件夹是STM32Cube固件包中重要的一部分,因为HAL库比较特殊,所以我们将其作为独立的章节来专门讲解。
在讲解之前我们需要说明一点,分析HAL库中的源码或者工程中的文件,不管它有多么复杂,无非就是一些.c源文件和.h头文件,还有一些类似.s的启动文件,而头文件中遇见比较多的就是一些宏定义和函数声明,.c文件比较常见的就是一些函数的定义。在分析过程中,我们去理解这些文件重要的宏定义和函数的作用,然后把这些函数和文件的关系弄清楚,在日后的项目开发中,我们才可以亲手操刀去打造属于自己的项目设计,在使用HAL库的时候才不至于逻辑混乱,犹如庖丁解牛,游刃有余。
本章将分为如下几个小节:
7.1、HAL库初识;
7.2、HAL库文件夹结构;
7.3、如何使用HAL库;
7.4、HAL库重要文件分析;
7.5、章节小结;
7.1 HAL库初识
7.1.1 获取HAL库
在上一章节的STM32Cube固件包中有一个STM32MP1xx_HAL_Driver文件夹,该文件夹下存放的就是HAL库,我们所说的HAL库就是指里边的库文件。
在这里插入图片描述

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

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

图7.1.3. 1库和直接操作寄存器对比
7.1.4 HAL库和其它库有什么不同
前面我们提到,针对STM32 系列芯片,ST官方推出了标准外设库(STD库)、HAL 库和LL 库 ,那么HAL库和其它两种库有什么差别?下图是三种库对STM32系列产品的支持,目前STM32MP1仅支持HAL库。
在这里插入图片描述

图7.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驱动),两者混合使用,才能正常驱动这个复杂的外设。
    在这里插入图片描述

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

图7.2. 1 HAL库文件夹
打开Inc文件夹,里边是一些以stm32mp1xx_hal和stm32mp1xx_ll_开头的.h文件,对应地,在Src下也有一堆以stm32mp1xx_hal和stm32mp1xx_ll_开头的.c文件,其中stm32f1xx_hal_开头的是HAL库文件, stm32f1xx_ll_开头的文件是LL库文件。
在这里插入图片描述

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

表7.2. 1 HAL库关键文件介绍
除了HAL库的文件命名有规则以外,库中的函数以及句柄等的命名均有规律,如下表所示,其中PPP表示某个外设名。
在这里插入图片描述

表7.2. 2 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定义的一些宏操作配合,即可实现一些常用的寄存器位操作,即可实现对外设的操作。
在这里插入图片描述

表7.2. 3 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库中的回调函数很多命名如下:
在这里插入图片描述

表7.2. 4 HAL库驱动中常用的回调函数API
至此,我们大概对HAL库驱动文件的一些通用格式和命名规则有了初步印象,记住这些规则可以帮助我们快速对HAL库的驱动进行归类和判定这些驱动函数的用法。
7.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函数,双击即可以打开此函数的说明,如下图所示。可以看到函数的定义、函数的作用、函数注意事项、参数说明以及函数在哪个文件,位置是在哪里等等:
在这里插入图片描述

图7.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:
在这里插入图片描述

图7.3. 2查找功能
其它的主题,例如Data Structures、Files和Directories也可以给我们提供更多的帮助信息:
在这里插入图片描述

图7.3. 3其它主题
关于此用户手册的使用就讲解到这里,在后面的实验中,遇见不明白的地方也可以查看此手册。
7.4 HAL库重要文件分析
下面我们先分析2个非常重要的文件:stm32mp1xx_hal_conf.h和stm32mp1xx_hal.c文件。其它文件,我们会在后面对应的实验章节做专门的讲解。
7.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。如果我们配置时钟,可以直接在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(数值越低优先级越高)。

在这里插入图片描述

图7.4.1. 1 STM32CubeMX生成的代码
不因为Systick是内核外设,中断优先级就比片上外设的外部中断优先级高,所以,如果工程里有用到Systick和其它的中断,例如工程中某个中断A调用了HAL_Delay函数,而HAL_Delay函数是通过Systick来实现计时的,如果中断A优先级比Systick高,就会导致A中断优先执行,而Systick中断服务函数一直未能执行,就会导致程序卡死的情况,所以应该设置Systick的中断优先级比A中断要高,可以在第71行处设置(注意范围,内核外设的中断优先级等级是0~15),如果是STM32CubeIDE,可以直接在STM32CubeMX上采用图形界面来配置:
在这里插入图片描述

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

图7.4.1. 3中断优先级变了
关于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),函数的实体在STM32CubeIDE生成的工程的main.c函数有定义,或者在官方的模板文件main.c中也可以找到,如下是之前第一个工程实验的main.c函数,可以看到有定义assert_failed函数,函数的参数file表示发生错误的文件名,line表示发生错误的行号。
在这里插入图片描述

图7.4.1. 4断言相关函数
上图虽然main.c函数中有断言的代码,但是还没有开启断言,所以看到#ifdef USE_FULL_ASSERT和#endif /* USE_FULL_ASSERT */之间的代码背景颜色呈现灰暗色。
第94行,#define assert_param(expr) ((void)0U)中((void)0U)表示不执行任何操作。
默认情况下宏USE_FULL_ASSERT是没有定义的,也就是没有开启断言功能,如果有定义,我们就可以在代码中使用断言assert_param来检验程序中的参数是否正确了。我们打开第一个工程,在STM32CubeMX插件的Project Manager配置界面中选中Enable Full Assert开启断言,此操作方法我们在前面的4.2.5的第1小节有讲解,如图:
在这里插入图片描述

图7.4.1. 5在CubeMX插件上开启断言
选中以后按下“Ctrl+S”保存并重新生成工程代码,系统会自动开启断言功能。
或者可以在M4工程的Properties中添加USE_FULL_ASSERT宏定义开启断言:
在这里插入图片描述

图7.4.1. 6通过定义宏开启断言
启用断言以后,main.c函数的断言部分的代码背景颜色不再是灰暗的颜色。可以在assert_failed函数内部添加以下代码,使错误的文件名和对应的行号可以通过串口打印出来(如果要用串口打印,工程中必须先配置好串口的功能才可以打印信息)。
printf(“Wrong parameters value: file %s on line %ld\r\n”, file, line);
在这里插入图片描述

图7.4.1. 7添加判断函数
下面我们分析HAL库中另一个重要的文件:stm32mp1xx_hal.c文件。
7.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)定义。弱就是表示此函数可以进行重写(重新定义),如果用户在其它地方重新定义一个同名函数,最终编译器编译的时候,就会选择用户定义的函数,如果用户没有重新定义这个函数(或者函数名字写错了),那么编译器就会默认执行弱定义的函数,并且编译器不会报错。是弱定义的函数都可以进行重写。关于弱定义的函数怎么使用,我们后面会有专门的例程进行讲解。
我们用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;
  1. 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。
在这里插入图片描述

图7.4.2. 1 ST官方模板的中断函数文件
或者在前面第一个工程实验中找到,SysTick_Handler函数就是处理SysTick定时器中断服务函数。
在这里插入图片描述

图7.4.2. 2CubeIDE中的中断服务函数
(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的优先级,然后在主函数中通过一个静态变量获取此优先级,例如下图,先配置SysTick的Preemption Priority(抢占优先)为2,保存配置重新生成工程:

在这里插入图片描述

图7.4.2. 3配置SysTick的优先级
然后生成的工程的主函数中添加如下代码,表示把滴答时钟优先级赋值给static变量priority:
static uint32_t priority = 0;
priority = HAL_GetTickPrio();
保存工程,编译无报错,进入Debug配置界面,此时箭头指在HAL_Init();的地方,表示从此处开始运行,此时,priority变量显示值为0。
在这里插入图片描述

图7.4.2. 4观察变量
我们在while处添加断点,然后点击Step Over单步调试,当箭头经过whlie以后,可以看到priority变量显示值为2,此值正是我们之前设置的优先级。
在这里插入图片描述

图7.4.2. 5单步调试
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值,即获取滴答定时器中断频率。
过程分析完了,是否如此?我们用前面单步调试的方法测试一下就知道。设置一个static局部变量freq,使用HAL_GetTickFreq函数获取此时滴答定时器中断频率的值,将此值赋值给freq:

static uint32_t freq = 0; /* 定义一个static局部变量 /
freq = HAL_GetTickFreq(); /
读取此时滴答定时器中断频率 */
保存并编译工程,然后单步调试,观察变量freq的变化,如下freq默认值为1:
在这里插入图片描述

图7.4.2. 6 uwTickFreq值默认为1
我们在添加如下语句:

HAL_SetTickFreq(10);			/* 设置滴答定时器中断频率为10KHz */
static uint32_t freq = 0;	/* 定义一个static局部变量 */
freq = HAL_GetTickFreq();	/* 读取此时滴答定时器中断频率 */
重新编译工程,此时freq的值变成了10,说明设置成功,证实了我们前面的分析。

在这里插入图片描述

图7.4.2. 7成功修改uwTickFreq值
/* 挂起滴答定时器中断,全局变量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增量被暂停。函数被声明为弱函数,可以在其他函数中被覆盖时使用。
  1. HAL库版本相关的函数
    相关函数声明如下,这些函数了解一下就好了,用得不多。
uint32_t HAL_GetHalVersion(void); /* 获取HAL库驱动程序版本 */
uint32_t HAL_GetREVID(void);       /* 获取设备修订标识符 */
uint32_t HAL_GetDEVID(void);       /* 获取设备标识符 */
  1. 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)	/* 关闭调试模块(待机模式下) */
  1. 芯片内部电压基准相关函数
    源码可在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
在这里插入图片描述

图7.4.2. 8模拟开关控制
该函数操作了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速度以及低功耗等相关函数,这里先不进行讲解了,后面用到我们再进行说明。
7.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。
在这里插入图片描述

图7.4.2. 9 固件库头文件关系
stm32mp1xx_hal_conf.h头文件基本上是一些宏定义和条件编译,用于用户自定义驱动,外设驱动配置文件可以根据需要include相关的外设头文件,不过在STM32CubeIDE上开发的话,此文件一般不需要我们手动去改,一般是通过STM32CubeMX插件完成配置。
stm32mp1xx_hal.c文件主要包括HAL库的初始化、系统滴答、基准电压配置、IO补偿、低功耗、EXTI配置、调试相关等函数的定义,其中weak定义的函数可以根据需要被用户在其它文件中进行重新定义。stm32mp1xx_hal.h头文件包含了HAL模型的所有驱动,用户的驱动文件或者main.h文件可以直接include此文件。
关于HAL库中其它文件,我们会通过后面的实验来熟悉它们。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【正点原子MP157连载】第七章 认识HAL库-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南 的相关文章

  • 有没有办法只安装mysql客户端(Linux)? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 有没有不需要安装整个mysql db安装包的Linux mysql命令行工具 我想做的是从服务器 1 应用程序服务器 执行将在服务器 2
  • 如何清理 Runtime.exec() 中使用的用户输入?

    我需要通过命令行调用自定义脚本 这些脚本需要很少的参数并在 Linux 机器上调用 当前版本容易出现各种shell注入 如何清理用户给出的参数 参数包括登录名和路径 Unix 或 Windows 路径 用户应该能够输入任何可能的路径 该路径
  • 如何从命令行执行 PHP 代码?

    我想执行单个 PHP 语句 例如if function exists my func echo function exists 直接使用命令行 无需使用单独的 PHP 文件 这怎么可能 如果您要在命令行中执行 PHP 我建议您安装phpsh
  • 直接写入 ARM Cortex A8 分支预测器中的全局历史缓冲区 (GHB) 或 BTB?

    我有兴趣直接修改 Cortex A8 上的 BTB 分支目标缓冲区 和 GHB 的内容 ARM 手册上有这样的内容 要在指令端 GHB 数组中写入一项 例如 LDR R0 0x3333AAAA MCR p15 0 R0 c15 c1 0 M
  • X 服务器使用什么像素格式?

    X 服务器使用什么像素格式 RGBA ARBG BGRA 如果有任何特定格式的话 Update 我专门寻找有关颜色分量顺序和位模式的信息 你的意思是帧缓冲区格式 或者所有支持的像素图格式 无论是哪一种 都取决于您的图形硬件 驱动程序和配置
  • _dl_runtime_resolve -- 共享对象何时加载到内存中?

    我们有一个对性能要求很高的消息处理系统 最近我们注意到第一条消息比后续消息花费的时间要长很多倍 当它通过我们的系统时 会发生大量转换和消息增强 其中大部分是通过外部库完成的 我刚刚描述了这个问题 使用 callgrind 将仅一条消息的 运
  • 选择多个模式的 awk 代码

    这是我的输入文件 比如modified txt r4544 n479826 2012 08 28 07 12 33 0400 Tue 28 Aug 2012 1 line Changed paths M branches 8 6 0 con
  • 尝试编译 git 但在 linux 中找不到 libcurl

    我想编译支持 http https 的 git 我有 ls usr include curl curlbuild h curl h curlrules h curlver h easy h mprintf h multi h stdchea
  • Docker 容器可以访问 DNS,但无法解析主机

    我在运行 docker 容器时遇到一个有趣的问题 突然间 我无法从容器内解析 DNS 这是一个概要 一切都没有解决 apt get pip 一次性 ping 容器等正在运行docker run it dns 8 8 8 8 ubuntu p
  • 你好世界,裸机 Beagleboard

    我正在尝试在我的 Beagleboard xm rev 上运行 hello world 类型的程序 C 通过调用 Cputs功能来自装配 到目前为止 我一直使用这个作为参考 http wiki osdev org ARM Beagleboa
  • 有没有办法改变vim的默认模式

    有谁知道如何更改vim的默认模式 它的默认模式是命令模式 但是我可以将其更改为插入模式吗 只需将以下行添加到您的 vimrc 中 start Vim s default mode will be changed to Insert mode
  • BlueZ D-Bus C,应用 BLE

    我正在尝试编写一个应用程序来搜索附近的蓝牙设备并与它们通信 我的应用程序将用 C 语言编写 并打算在 Linux 下工作 是否有通过 C 中的 D Bus 使用 BlueZ 的教程或示例 此应用程序的目的是从 BLE 中的文件发送数据 你能
  • 在 shell 脚本中将脚本目录更改为用户的 homedir

    在我的 bash 脚本中 我需要将当前目录更改为用户的主目录 如果我想更改为用户的foo主目录 从命令行我可以执行以下操作 cd foo 效果很好 但是当我从script它告诉我 bar sh line 4 cd foo No such f
  • 终止 ssh 会话会终止正在运行的进程

    我正在使用 ssh 连接到我的 ubuntu 服务器 我使用命令启动编码程序 然而 似乎当我的 ssh 会话关闭时 因为我在进入睡眠状态的笔记本电脑上启动它 有没有办法避免这种情况 当然 阻止我的笔记本电脑休眠并不是永久的解决方案 运行你的
  • 如何更改Linux服务器中的MySQL表名不区分大小写?

    我正在开发一个旧网站 该网站曾经托管在 Apple 服务器上 当它迁移到新的 Linux 服务器时 它停止工作 我很确定这是因为 php 脚本中使用的所有 MySQL 查询对于表名都有不同的大小写组合 我不知道为什么原始开发人员在创建表名或
  • Git - 致命:无法获取当前工作目录?

    When I git clone从回购协议中 我得到 fatal Could not get current working directory No such file or directory 我该怎么办 我检查了服务器并发现 git文
  • 带有接收器的 boost_log 示例无法编译

    我正在考虑将 boost log 用于一个项目 一开始我就遇到了以下问题 我在以下位置找到的升压日志示例 http www boost org doc libs 1 54 0 libs log example doc tutorial fi
  • Tomcat 中的 403 访问被拒绝

    我有以下内容tomcat users xml
  • 串口读取未完成

    下面的函数用于在Linux下从串口读取数据 我在调试时可以读取完整的数据 但是当我启动程序时 读缓冲区似乎并不完整 我正确接收了一小部分数据 但缓冲区的其余部分完全正确zero 可能是什么问题呢 int8 t serial port ope
  • 使用inotify监控文件

    我正在使用 inotify 来监视本地文件 例如使用 root temp inotify add watch fd root temp mask 删除该文件后 程序将被阻止read fd buf bufSize 功能 即使我创建一个新的 r

随机推荐

  • js -- constructor构造方法

    let aaa name Lifan age 18 class Polygon constructor aaa this name aaa name const poly1 new Polygon aaa console log poly1
  • Spring IoC容器

    转自 Spring IoC容器 一 Ioc简介 IoC容器是Spring中的核心 我们通常也将其称之为Spring容器 Spring使用IoC容器对对象的实例化和初始化的操作 及管理对象的创建到销毁的生命周期 Spring中使用的对象全部由
  • OSI网络模型(TCP/IP五层模型)

    互联网由一系列的网络协议组成 这些协议的集合叫做OSI协议 按照功能可以划分为七层 应用层 表示层 会话层 传输层 网络层 数据链路层 物理层 也就是我们常说的OSI七层模型 每一层使用下层提供的服务 并向上层提供服务 OSI七层模型只是一
  • uni-app store 状态管理学习,多写几遍就会了

    uni app使用了一段时间了 一直没有用到store 状态管理 还是应该学习一下 以后会用到的 1 使用hbuiderx创建uni app项目 2 与static同级创建store文件夹 store文件夹下创建index js 3 关键i
  • sqlserver数据类型转换(将 nvarchar 转换为数据类型 numeric 时出现算术溢出错误)

    一般情况下 sqlserver会自动完成数据转换 但这种转换有时候很容易出错 尤其是nvarchar转换为numeric时 如果能够明确数据类型 最好显式转换 举个我遇到的例子 SELECT FROM ITEM INFO TEST WHER
  • BLE学习(4):蓝牙地址类型和设备的隐私

    蓝牙地址也被称为蓝牙MAC地址 它能唯一标识一个蓝牙设备的48位的值 在蓝牙规范中 它被称为BD ADDR 蓝牙的地址类型可以分为两种 public addresses和random addresses 其中random addresses
  • visual studio2019创建运行第一个C++详细步骤与断点调试的简单认识

    是去官网下的社区版 可以用自己的微软账号登录也不需要网上找破解版了 安装过程还是很顺利的 下面在第一次使用vs2019下创建C 项目 依次helloworld 计算器类 首先明确 在vs中是使用项目来组织代码 使用解决方案来组织项目 所以首
  • adb连接及常用命令

    adb命令连接模拟器设备 以夜神模拟器为例 夜神模拟器默认端口为62001 adb connect 127 0 0 1 62001 当打开了多个模拟器 设备连接失败时 解决方法 查看连接的设备 可以查看连接的adb的设备情况 如果连接多个设
  • 02_计算机网络笔记-网络拓扑-交换机-VLAN

    文章目录 一般家庭的网络拓扑 交换机的基本原理与配置 虚拟局域网VLAN 个人博客 https blog csdn net cPen web 一般家庭的网络拓扑 光猫 调制解调器 1 光信号和电信号的转换 2 路由器的功能 可以拨号 账号和
  • MyBatis 采用注解方式批量更新数据 @Mapper @Update (包含2种方法)

    批量更新数据方法 1 注释db filed name 表示的是的数据库字段名字 entity name 表示的是你的实体字段 table name 表示你的表名 Update
  • ONNXRUNTUIME c++使用(分割网络)与相关资料(暂记)

    下面的教程是在linux系统上运行的 如果想在windows系统上运行 可以看官方链接或中文教程https bbs huaweicloud com blogs 335706 官方链接中有完整的VS的带 sln的项目 ONNXRUNTUIME
  • ES学习笔记之-ClusterState的学习

    前面研究过ES的get api的整体思路 作为编写ES插件时的借鉴 当时的重点在与理解整体流程 主要是shardOperation 的方法内部的调用逻辑 就弱化了shards 方法 实际上shards 方法在理解ES的结构层面 作用更大一些
  • JAVA获取IP地址、电脑Mac地址

    1 获取IP地址 注意 IP地址经过多次反向代理后会有多个IP值 其中第一个IP才是真实IP 所以不能通过 request getRemoteAddr 获取IP地址 如果使用了多级反向代理的话 X Forwarded For的值并不止一个
  • javaWeb项目中分页和模糊查询技术

    分页 需求 登录成功后 展现全部时 出现分页 思路 前端 1 设置分页按钮 以及分页数据 页码 总页数 总条数 2 设置分页请求 即点击上一页 下一页时发请求 后端 3 web xml映射 映射到Servlet能接收请求 4 Dao查询分页
  • opencv实践项目-人脸检测

    目录 1 opencv CascadeClassifier人脸检测步骤 2 CascadeClassifier分类器简介 2 1 从文件中加载级联分类器 2 2 目标检测方法 3 代码实现 1 opencv CascadeClassifie
  • SRVE0255E: A WebGroup/Virtual Host to handle /p2pd/servlet/dispatch has not been defined.

    Technote troubleshooting Problem Abstract When setting up IBM Cognos within IBM WebSphere the URI is not accessible The
  • 【MySQL】轻松学习 普通索引

    目录 引言 一 普通索引的创建 1 创建表时定义索引 2 已存在的表上创建索引 3 ALTER TABLE 语句创建索引 二 查看索引执行情况 引言 创建索引是指在某个表的一列或多列上建立一个索引 以便提高对表的访问速度 创建索引有3种方式
  • umijs----路由(修改路由的某一个path )

    1 在src下创建app js ts tsx 2 修改路由 export function patchRoutes routes routes为 umirc ts中设置的routes数组 可以使用数组的方法插入删除 运行时在最前面插入一个路
  • Webpack配置Vue热更新

    Webpack配置Vue热更新 需要的包 cnpm i vue webpack webpack cli webpack dev server html webpack plugin clean webpack plugin style lo
  • 【正点原子MP157连载】第七章 认识HAL库-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1 实验平台 正点原子STM32MP157开发板 2 购买链接 https item taobao com item htm id 629270721801 3 全套实验源码 手册 视频下载地址 http www openedv com t