1)实验平台:正点原子MiniPro H750开发板
2)平台购买地址:
https://detail.tmall.com/item.htm?id=677017430560
3)全套实验源码+手册+视频下载地址:
http://www.openedv.com/thread-336836-1-1.html
4)对正点原子STM32感兴趣的同学可以加群讨论:879133275
第七章 认识HAL库
HAL,英文全称Hardware Abstraction Layer,即硬件抽象层。HAL库是ST公司提供的外设驱动代码的驱动库,用户只需要调用库的API函数,便可间接配置寄存器。我们要写程序控制STM32芯片,其实最终就是控制它的寄存器,HAL库就为了更方便我们去控制寄存器,从而节约开发时间。
本章将分为如下几个小节:
7.1 初识STM32 HAL库
7.2 HAL库驱动包
7.3 HAL库框架结构
7.4 如何使用HAL库
7.5 HAL库使用注意事项
7.1 初识STM32 HAL库
STM32开发中常说的HAL库开发,指的是利用HAL库固件包里封装好的c语言编写的驱动文件,来实现对STM32内部和外围电器元件的控制的过程。但只有HAL库还不能直接驱动一个STM32的芯片,其它的组件已经由ARM与众多芯片硬件、软件厂商制定的通用的软件开发标准CMSIS实现了,本文只简单介绍这个标准,等大家熟悉开发后再研究这个框架。
简单地了解HAL库的发展和作用,可以方便学习者确定HAL库是否适合作为学习者自己长期开发STM32的工具,以降低开发、学习的成本。
7.1.1 CMSIS标准
根据一些调查研究表明,软件开发已经被嵌入式行业公认为最主要的开发成本,为了降低这个成本,ARM与Atmel、IAR、KEIL、SEGGER和ST等诸多芯片和软件工具厂商合作,制定了一个将所有Cortex芯片厂商的产品的软件接口标准化的标准CMSIS(Cortex Microcon troller Software Interface Standard)。下面来看ARM官方提供的CMSIS规范架构图,如图7.1.1.1所示:
图7.1.1.1 CorteX芯片的CMSIS分级实现
从图中可以看出这个标准分级明显,从用户程序到内核底层实现做了分层。按照这个分级,HAL库属于CMSIS-Pack中的“Peripheral HAL”层。CMSIS规定的最主要的3个部分为:核内外设访问层(由ARM负责实现)、片上外设访问层和外设访问函数(后面两个由芯片厂商负责实现)。ARM整合并提供了大量的模版,各厂商根据自己的芯片差异修改模版,这其中包括汇编文件startup_device.s、system_.h和system_.c这些与初始化和系统相关的函数。
结合STM32H7的芯片来说,其CMSIS应用程序的简单结构框图,不包括实时操作系统和中间设备等组件,其结构如图7.1.1.2所示。
图7.1.1.2 CMSIS分级下的STM32H7的文件分布
上面的框架是根据我们现在已经学习到的知识回过头来作的一个总结,这里只是作简单介绍,告诉大家它们之间存在一定联系,关于组成这些部分的文件、文件的作用及各文件如何组合、各分层的作用和意义,我们会在今后的学习过程中慢慢学习。
7.1.2 HAL库简介
库函数的引入,大大降低STM主控芯片开发的难度。ST公司为了方便用户开发STM32芯片开发提供了三种库函数,从时间产生顺序是:标准库、HAL库和LL库。目前ST已经逐渐暂停对部分标准库的支持,ST的库函数维护重点对角已经转移到HAL库和LL库上,下面我们分别为这三种库作一下简单的介绍。
1.标准外设库(Standard Peripheral Libraries)
标准外设库(Standard Peripherals Library)是对STM32芯片的一个完整的封装,包括所有标准器件外设的器件驱动器,是ST最早推出的针对STM系列主控的库函数。标准库的设计的初衷是减少用户的程序编写时间,进而降低开发成本。几乎全部使用C语言实现并严格按照“Strict ANSI-C”、MISRA-C 2004等多个C语言标准编写。但标准外设库仍然接近于寄存器操作,主要就是将一些基本的寄存器操作封装成了C函数。开发者仍需要关注所使用的外设是在哪个总线之上,具体寄存器的配置等底层信息。
图7.1.2.1 ST的标准库函数家族
ST为各系列提供的标准外设库稍微有些区别。例如,STM32F1x的库和STM32F3x的库在文件结构上就有些不同,此外,在内部的实现上也稍微有些区别,这个在具体使用(移植)时,需要注意一下!但是,不同系列之间的差别并不是很大,而且在设计上是相同的。
STM32的标准外设库涵盖以下3个抽象级别:
• 包含位,位域和寄存器在内的完整的寄存器地址映射。
• 涵盖所有外围功能(具有公共API的驱动器)的例程和数据结构的集合。
• 一组包含所有可用外设的示例,其中包含最常用的开发工具的模板项目。
关于更详细的信息,可以参考ST的官方文档《STM32 固件库使用手册中文翻译版》,文档中对于标准外设库函数命名、文件结构等都有详细的说明,这里我们就不多介绍了。
值得一提的是由于STM32的产品性能及标准库代码的规范和易读性以及例程的全覆盖性,使STM32的开发难度大大下降,更多。但ST从L1以后的芯片L0、L4和F7等系列就没有再推出相应的标准库支持包了。
2.HAL库
HAL是Hardware Abstraction Layer的缩写,即硬件抽象层。是ST为可以更好的确保跨STM32产品的最大可移植性而推出的MCU操作库。这种程序设计由于抽离应用程序和硬件底层的操作,更加符合跨平台和多人协作开发的需要。
HAL库是基于一个非限制性的BSD许可协议(Berkeley Software Distribution)而发布的开源代码。ST制作的中间件堆栈(USB主机和设备库,STemWin)带有允许轻松重用的许可模式, 只要是在ST公司的MCU 芯片上使用,库中的中间件(USB主机/设备库,STemWin)协议栈即被允许修改,并可以反复使用。至于基于其它著名的开源解决方案商的中间件(FreeRTOS,FatFs,LwIP和PolarSSL)也都具有友好的用户许可条款。
HAL库是从ST公司从自身芯片的整个生产生态出发,为了方便维护而作的一次整合,以改变标准外设库带来各系列芯片操作函数结构差异大、分化大、不利于跨系列移植的情况。相比标准外设库,STM32Cube HAL库表现出更高的抽象整合水平,HAL库的API集中关注各外设的公共函数功能,这样便于定义一套通用的用户友好的API函数接口,从而可以轻松实现从一个STM32产品移植到另一个不同的STM32系列产品。但由于封闭函数为了适应最大的兼容性,HAL库的一些代码实际上的执行效率要远低于寄存器操作。但即便如此,HAL库仍是ST未来主推的库。
3.LL库
LL库(Low Layer)目前与HAL库捆绑发布,它的设计为比HAL库更接近于硬件底层的操作,代码更轻量级,代码执行效率更高的库函数组件,可以完全独立于HAL库来使用,但LL库不匹配复杂的外设,如USB等。所以LL库并不是每个外设都有对应的完整驱动配置程序。使用LL库需要对芯片的功能有一定的认知和了解,它可以:
• 独立使用,该库完全独立实现,可以完全抛开HAL库,只用LL库编程完成。
• 混合使用,和HAL库结合使用。
对于HAL库和LL库的关系,如图7.1.2.2 Cube的软件框架所示,可以看出它们设计为彼此独立的分支,但又同属于HAL库体系。
图7.1.2.2 Cube的软件框架
通过以上简介我们对目前主流的STM32开发库有了一个初步的印象。标准库和HAL库、LL库完全相互独立,HAL库更倾向于外设通用化,扩展组件中解决芯片差异操作部分;LL倾向于最简单的寄存器操作,ST在未来还将重点维护和建设HAL库,标准库已经部分停止更新。HAL库和LL库的应用将是未来的一个趋势。
7.1.3 HAL库能做什么
用过标准库的朋友应该知道,使用标准库可以忽略很多芯片寄存器的细节,根据提供的接口函数快速配置和使用一个STM32芯片,使用HAL库也是如此。不论何种库,本质都是配置指定寄存器使芯片工作在我们需要的工作模式下。HAL库在设计的时候会更注重软硬件分离。HAL库的API集中关注各个外设的公共函数功能,便于定义通用性更好、更友好的API函数接口,从而具有更好的可移植性。HAL库写的代码在不同的STM32产品上移植,非常方便。
我们需要学会调用HAL库的API函数,配置对应外设按照我们的要求工作,这就是HAL库能做的事。但是无论库封装得多高级,最终还是要通过配置寄存器来实现。所以我们学习HAL库的同时,也建议同时学习外设的工作原理和寄存器的配置。只有掌握了原理,才能更好的使用HAL库,一旦发生问题也能更快速了定位和解决问题。
HAL库还可以和STM32CubeMX(图形化软件配置工具)配套一起使用,开发者可以使用该工具进行可视化配置,并且自动生成配置好的初始化代码,大大的节省开发时间。
7.2 HAL库驱动包
HAL库是一系列封装好的驱动函数,本节将从下载渠道、固件包的内容分析及在实际开发中用到的几个文件的详细介绍。
7.2.1 如何获取HAL库固件包
HAL库是ST推出的STM32Cube软件生态下的一个分支。STM32Cube是ST公司提供的一套免费开发工具和STM32Cube 固件包,旨在通过减少开发工作、时间和成本来简化开发人员的工作,并且覆盖整个STM32产品。它包含两个关键部分:
1、允许用户通过图形化向导来生成C语言工程的图形配置工具STM32CubeMX。可以通过CubeMX实现方便地下载各种软件或开发固件包。
2、包括由STM32Cube硬件抽象层(HAL),还有一组一致的中间件组件(RTOS、USB、FAT文件系统、图形、TCP/IP和以太网),以及一系列完整的例程组成的STM32Cube固件包。
ST提供了多种获取固件包的方法。本节只介绍从ST官方网站上直接获取固件库的方法。网页登陆:www.st.com,在打开的页面中依次选择:“Tools & Software”->“Ecosystem”-> “STM32Cube”->新页面->选择“Prodcut selector”,具体如下图所示:
图7.2.1.1 找到STM32CubeH7固件包的下载位置
在展开的页面中选择我们需要和固件,这里展开“STM32CubeH7”即可看到我们需要的H7的固件包,按下图操作,在新的窗口中拉到底部,选择适合自己的下载方式,注册帐号即可获取相应的驱动包。
图7.2.1.2 下载STM32CubeH7固件包
STM32Cube固件包,我们已经给大家下载好并且放到A盘8,STM32参考资料1,STM32CubeH7固件包,当前固件包版本是:STM32Cube_FW_H7_V1.6.0。因为现在是STM32H750的学习,所以我们准备好的固件包是H7的。大家要根据自己学习的芯片,下载对应的固件包。如果需要最新的固件包,大家按照上述的方法到官网重新获取即可。
7.2.2 STM32Cube固件包分析
STM32Cube 固件包完全兼容STM32CubeMX。对于图形配置工具STM32CubeMX入门使用,由于需要STM32F1基础才能入门使用,所以我们安排在后面第十章给大家讲解。本小节,我们主要讲解STM32Cube固件包的结构。
解压STM32CubeH7固件包后,我们看看其目录结构,如图7.2.2.1所示。
图7.2.2.1 STM32CubeH7固件包的目录结构
下面对STM32CubeH7固件包进行简要介绍。对于Documentation文件夹,里面是一个STM32CubeH7英文说明文档,这里我们就不做过多解释。接下来我们通过几个表格依次来介绍一下STM32CubeH7中几个关键的文件夹。
(1)Drivers文件夹
Drivers文件夹包含BSP,CMSIS和STM32H7xx_HAL_Driver三个子文件夹。三个子文件夹具体说明请参考下表7.2.2.1:
表7.2.2.1 Drivers文件夹介绍
(2)Middlewares文件夹
该文件夹下面有ST和Third_Party 2个子文件夹。ST文件夹下面存放的是STM32相关的一些文件,包括STemWin和USB库等。Third_Party文件夹是第三方中间件,这些中间价都是非常成熟的开源解决方案。具体说明请见下表7.2.2.2:
表7.2.2.2 Middlewares文件夹介绍
(3)Projects文件夹
该文件夹存放的是ST官方的开发板的适配例程,每个文件夹对应一个ST官方的Demo板,根据型号的不同提供MDK和IAR等类型的例程。里面有很多实例,读者可以根据自己的需要来作为参考。比如我们要查看STM32H750相关工程,所以我们直接打开子文件夹STM32H750B-DK即可。里面有很多实例,我们都可以用来参考。这里大家注意,每个工程下面都有一个MDK-ARM子文件夹,该子文件夹内部会有名称为Project.uvprojx的工程文件,我们只需要双击它就可在MDK中打开工程。
(4)Utilities文件夹
该文件夹是一些公用组件,也是主要为ST官方的Demo板提供的,在我们的例程中使用得不多。有兴趣的同学可以深入研究一下,这里我们不做过多介绍。
(5)其它几个文件
文件夹中还有几个单独的文件,用于声明软件版本或者版权信息,我们使用ST的芯片已经默认得到这个软件的版权使用授权,可以简单了解一下各文件的内容,实际项目中我们一般不添加。
License.md:用于声明软件版权信息的文件。
package.xml:描述固件包版本信息的文件。
Release_Notes.html:超文本文件,用浏览器打开可知它是对固件包的补充描述和固件版本更新的记录说明。
7.2.3 CMSIS文件夹关键文件
上一节中我们对STM32cube固件包的主要目录结构做了分析。这一小节在上一小节的基础上,我们来分析一下CMSIS文件夹:由命名可知该文件夹和7.1.1小节中提到的CMSIS标准是一致的,CMSIS为软件包的内容制定了标准,包括文件目录的命名和内容构成,CMSIS版本5.7.0的规定软件包目录如表7.2.3.1所示:
表7.2.3.1 CMSIS v5.7.0的文件夹规范
知道了CMSIS规定的组件及其文件目录的大概内容后,我们再来看看ST提供的CMSIS文件夹,如上节提到的,它的位置是“STM32Cube_FW_H7_V1.6.0\Drivers\CMSIS”。打开文件夹内容如图7.2.3.1所示,可以发现它的目录结构完全按照CMSIS标准执行,仅仅是作了部分删减。
图7.2.3.1 STM32CubeH7固件包的CMSIS文件夹
CMSIS文件夹中的Device和Include这两个文件夹中的文件是我们工程中最常用到的。下面对这两个文件夹作简单的介绍:
(1)Device文件夹
Device文件夹关键文件介绍如下表7.2.3.1所示:
表7.2.3.1 Device文件夹关键文件介绍
表7.1.2.1列出的文件都是正式工程中必须的文件。固件包的CMSIS文件包括了所有STM32H7芯片型号的文件,而我们只用到STM32H750系列,所以只是挑我们用到的系列文件来讲。
(2)Include文件夹
Include文件夹存放了符合CMSIS标准的 Cortex-M 内核头文件。 想要深入学习内核的朋友可以配合内核相关的手册去学习。对于STM32H7的工程,我们只要把我们需要的添加到工程即可,需要的头文件有:cmsis_armcc.h、cmsis_armclang.h、cmsis_compiler.h、cmsis_version.h、core_cm7.h和mpu_armv7.h。这几个头文件,对比起来,我们会比较多接触的是core_cm7.h。
core_cm7.h是内核底层的文件,由ARM公司提供,包含一些AMR内核指令,如软件复位,开关中断等功能。今后在需要的例程再去讲解其程序,现在之所以提到,是因为它包含了一个重要的头文件stdint.h。
7.2.4 stdint.h简介
stdint.h是从c99中引进的一个标准C库的文件。在2000年3月,ANSI 采纳了 C99 标准。ANSI C被几乎所有广泛使用的编译器(如:MDK、IAR)支持。多数C代码是在ANSI C基础上写的。任何仅仅使用标准C并且没有和任何硬件有依赖的代码实际上能保证在任何平台上用遵循C标准的编译器编译成功。就是说这套标准不依赖硬件,独立于任何硬件,可以跨平台。
stdint.h可以在MDK安装目录下找到,如MDK5安装在C盘时,可以在路径:C:\Keil_v5\ARM\ARMCC\include找到。stdint.h的作用就是提供了类型定义,其部分类型定义代码如下:
/* exact-width signed integer types */
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed __INT64 int64_t;
/* exact-width unsigned integer types */
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned __INT64 uint64_t;
在今后的程序,我们都将会使用这些类型,比如:uint32_t(无符号整型)、int16_t等。
7.3 HAL库框架结构
这一节我们将简要分析一下HAL驱动文件夹下的驱动文件,
7.3.1 HAL库文件夹结构
HAL库头文件和源文件在STM32Cube固件包的STM32H7xx_HAL_Driver文件夹中,打开该文件夹,如图7.3.1.1所示。
图7.3.1.1 STM32H7xx_HAL_Driver文件夹目录结构
STM32H7xx_HAL_Driver文件夹下的Src(Source的简写)文件夹存放是所有外设的驱动程序源码,Inc(Include的简写)文件夹存放的是对应源码的头文件。Release_Notes.html是HAL库的版本更新信息。最后三个是库的用户手册,这个需要可以去熟悉一下,查阅起来很方便。
打开Src和Inc文件夹,大家会发现基本都是stm32h7xx_hal_和stm32h7xx_ll_开头的.c和
.h文件。刚学HAL库的朋友可能会说,stm32h7xx_hal_开头的是HAL库,我能理解。那么stm32h7xx_ll_开头的文件又是什么?stm32h7xx_ll_开头的文件是前面介绍过的LL库的文件。
7.3.2 HAL库文件介绍
HAL库关键文件介绍如下表7.3.2.1所示,表中ppp代表任意外设。
表7.3.2.1 HAL库关键文件介绍
以上是HAL库最常见的文件的列表,在Src/Inc下面还有Legacy文件夹,用于特殊外设的补充说明。我们的教程中用到的比较少,这里不展开描述。
不止文件命名有一定规则,stm32h7xx_hal_ppp (c/h)中的函数和变量命名也严格按照命名规则,如表7.3.2.2所示的命名规则在大部分情况下都是正确的:
表7.3.2.2 HAL库函数、变量命名规则
对于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”的字样,实现一些特异化的中间处理操作等。
• 外设句柄结构体
HAL驱动的重要参数,可以同时定义多个句柄结构以支持多外设多模式。HAL驱动的操作结果也可以通过这个句柄获得。有些HAL驱动的头文件中还定义了一些跟这个句柄相关的一些外设操作。如用外设结构体句柄与HAL定义的一些宏操作配合,即可实现一些常用的寄存器位操作。
表7.3.2.3 HAL库驱动部分与外设句柄相关的宏
但对于SYSTICK/NVIC/RCC/FLASH/ GPIO这些内核外设或共享资源,不使用PPP_HandleTypedef这类外设句柄进行控制,如:HAL_GPIO_Init() 只需要初始化的GPIO编号和具体的初始化参数。
HAL_StatusTypeDef HAL_GPIO_Init (GPIO_TypeDef* GPIOx, GPIO_InitTypeDef
Init)
{
/
GPIO 初始化程序…… */
}
最后要分享的是HAL库的回调函数,这部分允许用户重定义,并在其中实现用户自定义的功能,也是我们使用HAL库的最常用的接口之一:
表7.3.2.4 HAL库驱动中常用的回调函数接口
至此,我们大概对HAL库驱动文件的一些通用格式和命名规则有了初步印象,记住这些规则可以帮助我们快速对HAL库的驱动进行归类和判定这些驱动函数的用法。
ST官方给我们提供了快速查找API函数的帮助文档。在路径“STM32Cube_FW_H7 _V1.6.0\Drivers\STM32H7xx_HAL_Driver”下有几个chm格式的文档,根据我们开发板主控芯片STM32H750VBT6我们没有找到直接可用的,但可以查看型号接近的:STM32H753xx_User _Manual.chm。双击打开后,可以看到左边目录下有四个主题,我们来查看Modules。以外设GPIO为例,讲一下怎么使用这个文档。点击GPIO外设的主题下的IO operation functions /functions看看里面的API函数接口描述,如图7.3.2.1所示。
图7.3.2.1 文档的API函数描述
这个文档提供的信息很全,不看源码都可以直接使用它来编写代码,还给我们指示源码位置,非常方便。大家多翻一下其他主题了解一下文档的信息结构,很容易使用。
下面举个例子,比如我们要让PB4输出高电平。先看函数功能,HAL_GPIO_WritePin函数就是我们的GPIO口输出设置函数。
函数有三个形参:
第一个形参是GPIO_TypeDef *GPIOx,形参描述说:x可以是A到K之间任何一个,而我们是PB4引脚,所以第一个形参确认是GPIOB。
第二个形参是uint16_t GPIO_Pin,看形参描述:该参数可以是GPIO_PIN_x,x可以1到15,那么我们第二个形参就是GPIO_PIN_4。
第三个形参是GPIO_PinState PinState,看形参描述:该参数可以是枚举里的两个数,一个是GPIO_PIN_RESET:表示该位清零,另一个是GPIO_PIN_SET:表示设置该位,即置1,我们要输出1,所以要置1该位,那么我们第三个形参就是GPIO_PIN_SET。
最后看函数返回值:None,没有返回值。
所以最后得出我们要调用的函数是:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4, GPIO_PIN_SET);
文档的使用就讲到这。
7.4 如何使用HAL库
我们要先知道STM32芯片的某个外设的性能和工作模式,才能借助HAL库来帮助我们编程,甚至修改HAL库来适配我们的开发项目。HAL库的API虽多,但是查找和使用有规律可循,只要学会其中一个,其他的外设就是类似的,只是添加自己的特性的API而已。
7.4.1 学会用HAL库组织开发工具链
需要按照芯片使用手册建议的步骤去配置芯片。HAL库驱动提供了芯片的驱动接口,但我们需要强调一个概念是使用HAL库的开发是对芯片功能的开发,而不是开发这个库,也不是由这个库能就直接开发。如果我们对芯片的功能不作了解的话,仍然不知道按照怎样的步骤和寻找哪些可用的接口去实现想要实现的功能。
ST提供芯片使用手册《STM32H7xx参考手册_V7(英文版).pdf》告诉我们使用某一外设功能时如何具体地去操作每一个用到的寄存器的细节,后面我们的例程讲解过程也会结合这个手册来分析配置过程。
嵌入式的软件开发流程总遵循以下步骤:组织工具链、编写代码、生成可执行文件、烧录到芯片、芯片根据内部指令执行我们编程生成的可执行代码。
在HAL库学习前期,建议以模仿和操作体验为基础,通过例程来学习如何配置和驱动外设。下面根据我们后续要学习的工程梳理出来的基于CMSIS的一个HAL库应用程序文件结构,帮助读者学习和体会这些文件的组成意义,如下表7.4.1.1所示。
表7.4.1.1基于CMSIS应用程序文件描述
把这些文件组织起来的方法,我们会在后续章节新建工程中介绍,这只提前告诉一下大家组成我们需要的编译工具链大概需要哪些文件。
7.4.2 HAL库的用户配置文件
stm32h7xx_hal_conf.h用于裁剪HAL库和定义一些变量,官方没有直接提供这个文件,但在STM32Cube_FW_H7_V1.6.0\Drivers\STM32H7xx_HAL_Driver\Inc这个路径下提供了一个模版文件《stm32h7xx_hal_conf_template.h》,我们可以直接复制这个文件重命名为stm32h7xx_ hal_conf.h,做一些简单的修改即可,也可以从在官方的例程中直接复制过来。我们一般都直接从官方的模板例程中直接复制过来即可。因为我们的芯片是STM32H750系列,所以选择的路径是:STM32Cube_FW_H7_V1.6.0\Projects\STM32H750B-DK\Templates\Template_Project\ Inc。
(1)stm32h7xx_hal_conf.h文件里面的内容不多,对我们来说最重要的是HSE_VALUE这个参数,这个参数表示我们的外部高速晶振的频率。这个参数请务必根据我们板子外部焊接的晶振频率来修改,官方默认是25M。正点原子STM32H750MINI PRO开发板外部高速晶振的频率是8MHZ。注意事项:我们要修改这个参数,源码在99行,具体修改如下:
#if !defined (HSE_VALUE)
#define HSE_VALUE ((uint32_t)8000000) /* 外部高速振荡器的值,单位HZ */
#endif /* HSE_VALUE */
(2)还有一个参数就是外部低速晶振频率,这个官方默认是32.768KHZ,我们开发板的低速晶振也是这个频率,所以不用修改,源码在128行。
#if !defined (LSE_VALUE)
#define LSE_VALUE ((uint32_t)32768) /* 外部低速振荡器的值,单位HZ */
#endif /* LSE_VALUE */
其他源码都可以不作修改,按照默认的配置即可。下面我们再来了解一下其他程序。
(3)用户配置文件可以用来选择使能何种外设,源码配置在37行到90行,代码如下。
/* ########################## Module Selection ############################# */
/**
* @brief This is the list of modules to be used in the HAL driver
*/
#define HAL_MODULE_ENABLED
#define HAL_ADC_MODULE_ENABLED
#define HAL_CEC_MODULE_ENABLED
#define HAL_COMP_MODULE_ENABLED
#define HAL_CORTEX_MODULE_ENABLED
...中间省略...
#define HAL_UART_MODULE_ENABLED
#define HAL_USART_MODULE_ENABLED
#define HAL_WWDG_MODULE_ENABLED
我们只要屏蔽某个外设的宏,则这个外设的驱动代码机会被屏蔽,从而不可用。比如我们屏蔽GPIO外设的宏,源码在53行,屏蔽就是把这个宏注释掉,具体如下。
#define HAL_GPIO_MODULE_ENABLED
然后打开stm32h7xx_hal_gpio.c文件,看到第118行。
#ifdef HAL_GPIO_MODULE_ENABLED
#include "stm32h7xx_hal_gpio.h"
#endif
这是一个条件编译符,与#endif配合使用。这里的要表达的意思是,只要工程中定义了HAL_GPIO_MODULE_ENABLED这个宏,#ifdef到#endif之间的程序(119行到550行)就会参与编译,否则不编译。所以只要我们屏蔽了stm32h7xx_hal_conf.h文件53行的宏,GPIO的驱动代码就不被编译。也就起到选择使能何种外设的功能,其他外设同理。
可以看官方的示范例程,就是通过屏蔽外设的宏的方法来选择使能何种外设。好处就是编译时间会变短,因为屏蔽了没有用的程序,编译时间自然就短了。正点原子的例程选择另外一中方法,就是工程中只保留需要的stm32h7xx_hal_ppp.c,不需要的不添加到工程里,这样编译时间就不会太长。
(4)大家看到stm32h7xx_hal_conf.h文件的159行。
#define TICK_INT_PRIORITY ((uint32_t)0x0F) /*!< tick interrupt priority */
宏定义TICK_INT_PRIORITY是滴答定时器的优先级。这个优先级很重要,因为如果其它的外设驱动程序的延时是通过滴答定时器提供的时间基准,来实现延时的话,又由于实现方式是滴答定时器对寄存器进行计数,所以当我们在其它中断服务程序里调用基于此时间基准的延迟函数 HAL_Delay,那么假如该中断的优先级高于滴答定时器的优先级,就会导致滴答定时器中断服务函数一直得不到运行,从而程序卡死在这里。所以滴答定时器的中断优先级一定要比这些中断高。
请注意这个时间基准可以是滴答定时器提供,也可以是其他的定时器,默认是用滴答定时器。
(5)下面说一下关于断言这个功能,这个功能用来判断函数的形参是否有效,在HAL库的API里面有用到。这个功能的使能开关代码是一个宏,在源码的180行,默认是关闭的,代码如下。
/* #define USE_FULL_ASSERT 1 */
通过宏USE_FULL_ASSERT来选择功能,在源码413行到432,代码如下。
/* Exported macro -----------------------------------------------------------*/
#ifdef USE_FULL_ASSERT
/**
* @brief The assert_param macro is used for function's parameters check.
* @param expr: If expr is false, it calls assert_failed function
* which reports the name of the source file and the source
* line number of the call that failed.
* If expr is true, it returns no value.
* @retval None
*/
#define assert_param(expr) ((expr) ? (void)0U : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------ */
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0U)
#endif /* USE_FULL_ASSERT */
#ifdef __cplusplus
}
#endif
#endif /* __STM32H7xx_HAL_CONF_H */
也是通过条件编译符来选择对应的功能。当用户自己需要使用断言功能,怎么做呢?首先需要定义宏USE_FULL_ASSERT来使能断言功能,即把源码的180行的注释去掉即可。然后看到源码423行的assert_failed()这个函数。其实这个函数是需要我们自己实现的,我们把这个函数定义在正点原子提供的sys.c文件里面。后面再跟大家讲sys.c文件,现在我们把assert_failed()这个函数拿出来给大家先讲,assert_failed()函数的定义在sys.c的176行到190行,具体如下:
#ifdef USE_FULL_ASSERT
/**
* @brief 当编译提示出错的时候此函数用来报告错误的文件和所在行
* @param file:指向源文件
* line:指向在文件中的行数
* @retval 无
*/
void assert_failed(uint8_t* file, uint32_t line)
{
while (1)
{
}
}
#endif
可以看到这个函数里面没有实现如何功能,就是一个什么不做的死循环,具体功能请根据自己的需求去实现。file是指向源文件的指针,line是指向源文件的行数。__FILE__是表示源文件名,__LINE__是表示在源文件中的行数。比如我们可以实现打印出这个错误的两个信息等等。
总的来说断言功能就是,在HAL库中,如果定义了USE_FULL_ASSERT这个宏,那么所有的HAL库函数将会检查函数的形参是否正确。如果错误将会调用assert_failed()这个函数,这个函数我们默认是个什么事不做的死循环,用户请根据自己的需求设计功能。使用断言功能将会增加了代码量,减慢运行速度等,所以一般只是在调试的时候用,正式发布的软件是不推荐的。
7.4.3 stm32h7xx_hal.c文件
这个文件内容比较多,包括HAL库的初始化、系统滴答、基准电压配置、IO补偿、低功耗、EXTI配置等都集合在这个文件里面。下面我们对该文件进行讲解。
-
HAL_Init()函数
源码在134行到172行,简化函数如下:
HAL_StatusTypeDef HAL_Init(void)
{
/* 设置中断优先级分组 */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* 使用滴答定时器作为时钟基准,配置1ms滴答(重置后默认的时钟源为HSI) */
if(HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK)
{
return HAL_ERROR;
}
/* 初始化硬件 */
HAL_MspInit();
/* 返回函数状态 */
return HAL_OK;
}
该函数是HAL库的初始化函数,在程序中必须优先调用,其主要实现如下功能:
1)设置NVIC优先级分组为4。
2)配置滴答定时器每1ms产生一个中断。
3)在这个阶段,系统时钟还没有配置好,因此系统还是默认使用内部高速时钟源HSI在跑程序。对于H7来说,HSI的主频是64MHZ。所以如果用户不配置系统时钟的话,那么系统将会使用HIS作为系统时钟源。
4)调用HAL_MspInit函数初始化底层硬件,HAL_MspInit函数在stm32h7xx_hal.c文件里面做了弱定义。关于弱定义这个概念,后面会有讲解,现在不理解没关系。正点原子的HAL库例程是没有使用到这个函数去初始化底层硬件,而是单独调用需要用到的硬件初始化函数。用户可以根据自己的需求选择是否重新定义该函数来初始化自己的硬件。
注意事项:
为了方便和兼容性,正点原子的HAL库例程中的中断优先级分组设置为分组2,即把源码的145行改为如下代码:
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
中断优先级分组为2,也就是2位抢占优先级,2位响应优先级,抢占优先级和响应优先级的值的范围均为0-3。
2. HAL_DeInit ()函数
源码在179行到214行,函数如下:
HAL_StatusTypeDef HAL_DeInit(void)
{
/* 复位所有外设 */
__HAL_RCC_AHB3_FORCE_RESET();
__HAL_RCC_AHB3_RELEASE_RESET();
__HAL_RCC_AHB1_FORCE_RESET();
__HAL_RCC_AHB1_RELEASE_RESET();
__HAL_RCC_AHB2_FORCE_RESET();
__HAL_RCC_AHB2_RELEASE_RESET();
__HAL_RCC_AHB4_FORCE_RESET();
__HAL_RCC_AHB4_RELEASE_RESET();
__HAL_RCC_APB3_FORCE_RESET();
__HAL_RCC_APB3_RELEASE_RESET();
__HAL_RCC_APB1L_FORCE_RESET();
__HAL_RCC_APB1L_RELEASE_RESET();
__HAL_RCC_APB1H_FORCE_RESET();
__HAL_RCC_APB1H_RELEASE_RESET();
__HAL_RCC_APB2_FORCE_RESET();
__HAL_RCC_APB2_RELEASE_RESET();
__HAL_RCC_APB4_FORCE_RESET();
__HAL_RCC_APB4_RELEASE_RESET();
/* 对底层硬件初始化进行复位 */
HAL_MspDeInit();
/* 返回函数状态 */
return HAL_OK;
}
该函数取消初始化HAL库的公共部分,并且停止systick,是一个可选的函数。该函数做了一下的事:
1)复位了AHB1、AHB2、AHB3、AHB4、APB1L、APB1H、APB2、APB3、APB4的时钟。
2)调用HAL_MspDeInit函数,对底层硬件初始化进行复位。HAL_MspDeInit也在stm32h7xx _hal.c文件里面做了弱定义,并且与HAL_MspInit函数是一对存在。HAL_MspInit函数负责对底层硬件初始化,HAL_MspDeInit函数则是对底层硬件初始化进行复位。这两个函数都是需要用户根据自己的需求去实现功能,也可以不使用。
3. HAL_InitTick ()函数
源码在254行到302行,简化函数如下:
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
/* uwTickFreq是个枚举类型,如果检测到uwTickFreq为零,则返回 */
if((uint32_t)uwTickFreq == 0UL)
{
return HAL_ERROR;
}
/* 配置滴答定时器1ms产生一次中断 */
if (HAL_SYSTICK_Config(SystemCoreClock /(1000UL / (uint32_t)uwTickFreq))> 0U)
{
return HAL_ERROR;
}
#endif
/* 配置滴答定时器中断优先级 */
if (TickPriority < (1UL << __NVIC_PRIO_BITS))
{
HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
uwTickPrio = TickPriority;
}
else
{
return HAL_ERROR;
}
/* 返回函数状态 */
return HAL_OK;
}
该函数用于初始化滴答定时器的时钟基准,主要功能如下:
1)配置滴答定时器1ms产生一次中断。
2)配置滴答定时器的中断优先级。
3)该函数是__weak定义的“弱函数”,用户可以重新定义该函数。
该函数可以通过HAL_Init()或者HAL_RCC_ClockConfig()重置时钟。在默认情况下,滴答定时器是时间基准的来源。如果其他中断服务函数调用了HAL_Delay(),必须小心,滴答定时器中断必须具有比调用了HAL_Delay()函数的其他中断服务函数的优先级高(数值较低),否则会导致滴答定时器中断服务函数一直得不到执行,从而卡死在这里。
4. 滴答定时器相关的函数
源码在331行到463行,相关函数如下:
/* 该函数在滴答定时器时钟中断服务函数中被调用,一般滴答定时器1ms中断一次,
所以函数每1ms让全局变量uwTick计数值加1 */
__weak void HAL_IncTick(void)
{
uwTick += (uint32_t)uwTickFreq;
}
/* 获取全局变量uwTick当前计算值 */
__weak uint32_t HAL_GetTick(void)
{
return uwTick;
}
/* 获取滴答时钟优先级 */
uint32_t HAL_GetTickPrio(void)
{
return uwTickPrio;
}
/* 设置滴答定时器中断频率 */
HAL_StatusTypeDef HAL_SetTickFreq(HAL_TickFreqTypeDef Freq)
{
HAL_StatusTypeDef status = HAL_OK;
HAL_TickFreqTypeDef prevTickFreq;
assert_param(IS_TICKFREQ(Freq));
if (uwTickFreq != Freq)
{
/* 备份滴答定时器中断频率 */
prevTickFreq = uwTickFreq;
/* 更新被HAL_InitTick()调用的全局变量uwTickFreq */
uwTickFreq = Freq;
/* 应用新的滴答定时器中断频率 */
status = HAL_InitTick(uwTickPrio);
if (status != HAL_OK)
{
/* 恢复之前的滴答定时器中断频率 */
uwTickFreq = prevTickFreq;
}
}
return status;
}
/* 获取滴答定时器中断频率 */
HAL_TickFreqTypeDef HAL_GetTickFreq(void)
{
return uwTickFreq;
}
/*HAL库的延时函数,默认延时单位ms */
__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)
{
}
}
/* 挂起滴答定时器中断,全局变量uwTick计数停止 */
__weak void HAL_SuspendTick(void)
{
/* 禁止滴答定时器中断 */
SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
}
/* 恢复滴答定时器中断,恢复全局变量uwTick计数 */
__weak void HAL_ResumeTick(void)
{
/* 使能滴答定时器中断 */
SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;
}
这些函数不是很难,请参照注释理解。注意:如果函数被前缀__weak定义,则用户可以重新定义该函数。更多的内容可以参考8.1.5小节。
5. HAL库版本相关的函数
源码在465行到517行,相关函数声明如下:
uint32_t HAL_GetHalVersion(void); /* 获取HAL库驱动程序版本 */
uint32_t HAL_GetREVID(void); /* 获取设备修订标识符 */
uint32_t HAL_GetDEVID(void); /* 获取设备标识符 */
uint32_t HAL_GetUIDw0(void); /* 获取唯一设备标识符的第一个字 */
uint32_t HAL_GetUIDw1(void); /* 获取唯一设备标识符的第二个字 */
uint32_t HAL_GetUIDw2(void); /* 获取唯一设备标识符的第三个字 */
这些函数了解一下就好了,用得不多。
6. 芯片内部电压基准相关函数
源码在519行到602行,函数声明如下:
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函数用于禁止内部电压基准参考。
7. 以太网PHY接口选择函数
源码在605行到619行,函数声明如下:
void HAL_SYSCFG_ETHInterfaceSelect(uint32_t SYSCFG_ETHInterface);
该函数用于以太网PHY接口的选择,可以是MII或RMII接口。
8. HAL_SYSCFG_AnalogSwitchConfig()函数
源码在622行到650行,函数声明如下:
void HAL_SYSCFG_AnalogSwitchConfig(uint32_t SYSCFG_AnalogSwitch ,
uint32_t SYSCFG_SwitchState );
当PA0、PA1、PC2、PC3引脚复用为ADC的时候,还有一组对应的可选引脚PA0_C、PA1_C、PC2_C、PC3_C。该函数的作用就是切换这些可选的引脚。关于这个不理解,请参考图7.4.3.1。该函数操作了SYSCFG_PMCR寄存器,关于该寄存器请查阅参考手册。
图7.4.3.1 连接到ADC输入的模拟输入
9. Booster的使能和禁止函数(用于ADC)
源码在653行到676行,函数声明如下:
void HAL_SYSCFG_EnableBOOST(void); /* 使能Booster
/
void HAL_SYSCFG_DisableBOOST(void); /
禁止Booster */
如果使能Booster,当供电电压低于2.7V时,能够减少模拟开关总的谐波失真。这样就使得模拟开关的性能和供电正常的情况时一样,能够正常工作。
10. HAL_SYSCFG_CM7BootAddConfig()函数
源码在680行到712行,函数如下:
void HAL_SYSCFG_CM7BootAddConfig(uint32_t BootRegister, uint32_t BootAddress)
该函数用于配置程序启动模式,BOOT=0或者BOOT=1,来选择启动地址。详细的内容请看第九章的9.1小节。
11. IO补偿、低功耗、EXTI等相关函数
IO补偿、低功耗、EXTI等相关函数,这里先不进行讲解了,后面用到再进行说明。
7.4.4 HAL库中断处理
中断是STM32开发的一个很重要的概念,这里我们可以简单地理解为:STM32暂停了当前手中的事并优先去处理更重要的事务。而这些“更重要的事务”是由软件开发人员在软件中定义的。关于STM32中断的概念,我们会在中断例程的讲解再跟大家详细介绍。
由于HAL库中断处理的逻辑比较统一,我们将这个处理过程抽象为图7.4.4.1所表示的业务逻辑:
图7.4.4.1 HAL驱动中断处理流程
结合以前的HAL库文件介绍章节,以上的流程大概就是:设置外设的控制句柄结构体PPP_HandleType和初始化PPP_InitType结构体的参数,然后调用HAL库对应这个驱动的初始化HAL_PPP_Init(),由于这个API中有针对外设初始化细节的接口Hal_PPP_Mspinit(),我们需要重新实现这个函数并完成外设时钟、IO等细节差异的设置, 完成各细节处理后,使用HAL_NVIC_SetPriority()、HAL_NVIC_EnableIRQ()来使能我们的外设中断;定义中断处理函数PPP_IRQHandler,并在中断函数中调用HAL_ppp_function_IRQHandler()来判断和处理中断标记; HAL库中断处理完成后,根据对应中的调用我们需要自定义的中断回调接口HAL_ PPP_ProcessCpltCallback();如串口接收函数HAL_UART_RxCpltCallback(),我们在这个函数中实现我们对串口接收数据想做的处理;中断响应处理完成后,stm32芯片继续顺序执行我们定义的主程序功能,按照以上处理的标准流程完成了一次中断响应。
7.4.5 正点原子对HAL库用法的个性化修改
前面按ST官方建议的HAL库的使用方法给介绍了一个HAL库。
1、将中断处理函数独立到每个外设中,便于独立驱动;同类型的外设驱动处理函数不使用HAL回调函数接口处理操作而直接在中断函数中处理判断对应中断。
2、我们把原来的中断分组进行了修改,由抢占式无子优先级改为中断分组2;便于管理同类外设的优先级响应。
3、在很多芯片的初始化过程中,我们使用到了delay_ms()、delay_us()等函数进行初始化,使用的是Systick作的精准延时,而HAL库默认也使用Systick作延时处理,为解决这种冲突和兼容我们大部分的驱动代码,我们在例程中使用delay.c中的延时函数取代Hal_Delay();取消原来HAL库的Systick延时设置。
7.5 HAL库使用注意事项
本小节根据经验跟大家讲述一些关于HAL库使用的注意事项,供读者参考。
1、即使我们已经在使用库函数作为开发工具了,我们可以忽略很多芯片的硬件外设使用上的细节,但当发生问题时,我们仍需要回归到芯片使用手册查看当前操作是否违规或缺漏。
2、使用HAL库和其它第三方的库开发类似,把我们需要编写的软件和第三方的库分开成相互独立的文件,开发过程中我们尽量不去修改第三方的软件源码,需要修改的部分尽量在自己的代码中实现;这样一旦我们需要更新第三方库时,我们原来编写的功能也能很快地匹配新的库去执行功能。
3、即使HAL库目前较以前已经相对更完善了,但它仍无法覆盖我们要想实现的所有细节功能,甚至可能存在错误,我们要有怀疑精神,辩证地去使用好这个工具;如我们在PWM一节编码时发现HAL库中有个宏定义TIM_RESET_CAPTUREPOLARITY括号不匹配导致编译报错,这时我们不得不修改一下HAL库的源码了。
4、注意HAL库的执行效率。由于HAL库的驱动对相同外设大多是可重入的,在执行HAL驱动的API函数的效率没有直接寄存器操作来得高,如果在对时序要求比较严苛的代码,建议使用简洁的寄存器操作代替。
5、我们在例程中使用delay.c中的延时函数取代Hal_Delay();取消原来HAL库的Systick延时设置;但这会有一个问题:原来HAL库的超时处理机制不再适用,所以对于设置了超时的函数,可能会导致停留在这个函数的处理中,无法按正常的超时退出。