认识HAL库' S; m9 ^: K) P/ O) \% D
第四章生成的工程是基于HAL库的,这点我们在前面有提过,在第四章的操作中,我们通过第一个工程实验熟悉了STM32CubeIDE的基本使用方法。本章节,我们来认识HAL库。HAL库文件夹是STM32Cube固件包中重要的一部分,因为HAL库比较特殊,所以我们将其作为独立的章节来专门讲解。
9 s& t5 v6 J6 l, A在讲解之前我们需要说明一点,分析HAL库中的源码或者工程中的文件,不管它有多么复杂,无非就是一些.c源文件和.h头文件,还有一些类似.s的启动文件,而头文件中遇见比较多的就是一些宏定义和函数声明,.c文件比较常见的就是一些函数的定义。在分析过程中,我们去理解这些文件重要的宏定义和函数的作用,然后把这些函数和文件的关系弄清楚,在日后的项目开发中,我们才可以亲手操刀去打造属于自己的项目设计,在使用HAL库的时候才不至于逻辑混乱,犹如庖丁解牛,游刃有余。
, \! J6 ~4 ]. x; D( a
+ Y: X: d$ r, `4 ?! _! c5 \7.1 HAL库初识
9 G. K. ~5 `% p) g7.1.1 获取HAL库
" F4 q0 R3 U5 b2 I' J3 r) b1 {! c在STM32Cube固件包中有一个STM32MP1xx_HAL_Driver文件夹,该文件夹下存放的就是HAL库,我们所说的HAL库就是指里边的库文件。
. ~; w8 \7 h6 e/ K& l1 b* M8 [+ e5 d! p
4 M+ w9 H1 [/ M. l. l( E
3 o9 y; T, Z5 d8 P9 a1 j图7.1.1. 1 HAL库在固件包中1 g" a; N$ i: d2 z7 C+ @! G$ U
在介绍HAL库文件前,我们先来理解下面几个概念。+ ?8 g( Z4 m, ~$ u+ T6 ?! z$ r
7.1.2 什么是HAL库5 q2 [2 A# Y* e5 H8 l9 x4 ^5 v
在介绍HAL库之前,我们先来理解两个概念,什么是API?什么是句柄(Handle)?0 V- F9 \ M) Y% h2 P
前面我们多次提到API这个词,API 全称Application Programming Interface,翻译过来就是应用程序编程接口。接口,可以理解为是一些已经封装好了的可以被调用的功能函数或者方法,我们把这些函数放到我们的工程中,当我们要实现某个功能时,就可以在工程中找到对应的函数,然后进行调用。
' W+ V) U+ M; p* ]- b句柄(Handle),在STM32的手册上经常看到这个词,可以理解为它是一个指针,或者是一些表的索引,或者是用于描述和标记某些资源的的标识,这些资源可以是函数、可以是一段内存、可以是一组数字、可以是一个外设等等,总之很广泛,通过句柄我们可以访问到打开的资源。我们在调用API函数的时候,可以利用句柄来说明要操作哪些资源。) J- [4 v9 |/ L( q& G
HAL的含义,ST官方手册里的解释是Hardware abstraction layer,即硬件抽象层,单词的首字母组合起来就是HAL。HAL库是一些封装好的驱动程序,向下可以操作硬件,向上可以给用户提供可操作的接口。- K( P/ J3 e8 ?/ z
/ B7 r$ \. r( }) Y; W$ h2 F
2 `; V. i. n& y" }$ P
7 [! k" q3 [; Y6 k; C0 {
图7.1.2. 1 HAL库属于驱动程序 _) @6 G( M) ^
HAL库,笔者的理解是,ST把对不同系列MCU的操作经过一层一层的封装,将硬件进行抽象化表达出来,最后呈现给我们的就是HAL库。硬件抽象化,也就是将对寄存器的操作做了一系列封装,将外设抽象组织为句柄,使我们看不到寄存器的影子,最后分离出可以调用的API,使用者可以不去关注底层、不必关注复杂的硬件寄存器就可以进行编程,通过调用API和句柄操作可以实现对MCU的大部分外设进行操作,使用起来非常方便,而且很容易进行移植。$ E3 Z# |+ Z! q# x
7.1.3 HAL库能做什么3 N. p! N* L% C, K9 R! g4 l
STM32开发方式中,可以直接配置寄存器或者可以使用官方的库来实现。9 F. `8 a' v) ]! G$ H0 N6 Y n
我们在51单片机开发的时候就直接配置寄存器,但是到了32位单片机开发,如果开发大型项目,需要的功能外设很多,再使用这种方式就已经力不从心了。因为STM32的外设资源丰富,寄存器数量是51单片机寄存器的数十倍,那么多的寄存器根本无法记忆,而且开发中需要不停查找芯片手册,开发过程就显得机械和费力,完成的程序代码可读性差,可移植性不高,程序的维护成本变高了。当然了,采用直接配置寄存器方式开发会显得更直观,程序运行占用资源少。如果项目中只用少数几个外设,或者同一系列的芯片,项目组内部有一套成熟、可移植性高的祖传代码,开发起来就没那么困难了。% t2 a' g) j$ a" l, N6 {
为了简化开发人员的工作,减少开发工作时间和成本,针对STM32 系列芯片,ST官方推出了标准外设库(STD库)、HAL 库和LL 库 。在这些库中,有很多用结构体封装好的寄存器参数,有常用的表示参数的宏,还有封装好的对寄存器操作的API,开发者可以调用这些资源实现配置相应的寄存器,效率大大提高了。使用库的框架来开发,程序控制语句结构化,程序单元模块化,贴近人的思维,易于阅读,易于移植和维护。
; y9 U# f0 w% ~6 M, ]" m可以这么说,库是架设于寄存器与用户驱动层之间的代码,向下是直达硬件相关的寄存器,向上为用户提供配置寄存器的接口,用户代码通过这些接口来间接操作寄存器。6 \0 ` o9 O, L; h* i6 _ y8 z
3 m/ _7 y) Y+ V8 b1 q
$ r# f, j" M+ e, {( j! Y! S) p9 S
- f" E" _- e: q% x' Z- X, M; k7 v6 Z
图7.1.3. 1库和直接操作寄存器对比
% Z6 t6 b; k' H7.1.4 HAL库和其它库有什么不同
! T& I: B9 {; N- s- o前面我们提到,针对STM32 系列芯片,ST官方推出了标准外设库(STD库)、HAL 库和LL 库 ,那么HAL库和其它两种库有什么差别?下图是三种库对STM32系列产品的支持,目前STM32MP1仅支持HAL库。# w) B: C* d3 D' s2 D. m1 l) B
8 t7 i2 m, U4 Q' h2 X9 f
% W7 E k' t3 S, \/ @) U- J' }# ^6 a
图7.1.4. 1 HAL库对ST系列产品的支持情况
p+ R8 _/ }9 R5 }4 A ~7 ]
# J* Q7 I& q M8 c9 i7 }1.标准外设库
: w E" R5 l& `1 ?" X. PSTD(Standard Peripheral Libraries)标准外设库,它把实现功能中需要配置的寄存器以结构体的形式封装起来,使用者只需要配置结构体变量成员就可以修改外设的配置寄存器,比直接操作寄存器方便了不少。但标准外设库仍然接近于寄存器操作,它的方便也是针对某一系列芯片而言的,在不同系列芯片上使用标准外设库开发的程序可移植性比较差,例如,在F4上开发的程序移植到F3上,使用标准库是不通用的。目前STM32系列产品中仅F0-F4以及L1系列支持标准外设库。/ ^, K# k+ c, Q: i
2.HAL库
* }- I& u" z5 I. r5 n. e) q( G4 L为了解决标准库存在的问题,ST 在标准库的基础上又推出了 HAL 库。个人认为,HAL库是用来取代之前的标准库的,因为这几年ST官方大力推广HAL 库,而且在ST新出的 STM32 芯片中, ST 直接只提供 HAL 库。6 Q. K9 u- g2 I2 W
HAL库在设计的时候更注重软硬件分离,HAL库的目的应该就是尽量抽离物理层,HAL库的API集中关注各个外设的公共函数功能,以便定义通用性更好的API函数接口,从而具有更好的可移植性。HAL库写的代码在不同的STM32产品上移植,非常方便,效率得到提升。目前HAL库支持STM32各个系列产品。 \- P+ H Y! b5 p
3.LL库
6 |7 r! Z& \/ _5 s/ j. X* v+ ]LL库(Low Layer)是 ST 继HAL库之后新增的库,与 HAL 库捆绑发布,在前面的STM32CubeIDE第一个工程实验中,我们看到在STM32CubeMX插件里有选择使用HAL库还是LL库的选项。
1 b$ x [ z$ j7 L, ~LL库的英文名字翻译过来就是底层的意思,实际上LL 库更接近硬件层,它和STD库类似,都是直接操作的寄存器,只不过LL库可以在STM32Cube中实现。LL库提供一组轻量级、优化、面向专家的API,具有最佳的性能和运行时效率。LL库可以完全独立使用,也可以结合HAL库一起使用。当HAL库需要优化运行时,可以调用LL库来处理,而对于复杂的外设(例如:USB驱动),两者混合使用,才能正常驱动这个复杂的外设。% n' ^% Y9 i+ e/ m* S
! B$ o+ x2 K& a4 n/ J
2 R3 g: o, s W! ]8 I- u
O, E+ M2 V$ R% Z2 W
图7.1.4. 2 HAL库和LL库
: @/ H$ f7 C5 [- N$ W: X7.1.5 怎样学习HAL库2 A. X& a1 _ M
(1)不管HAL库封装的有多好,本质上还是通过配置MCU/MPU的寄存器来实现我们想要的功能。所以我们学习HAL库的同时,还需要学习外设的工作原理和寄存器的配置方法,通过原理来理解HAL库是怎样实现我们想要的功能,要知其然更要知其所以然。
- k; k" _2 a# L4 ^' X* d(2)HAL库不仅仅是底层驱动,它更是一套行业内可以公开和认可的架构。学习HAL库,我们要了解它的架构,了解库中每个文件夹下大概有哪些文件,文件之间的关系是什么,函数之间的调用关系是什么,调用条件是什么,常见的数据结构怎么用。
( } v8 y6 V: I/ O* ?" e! r0 i(3)学习HAL库,遇到疑问的地方可以查HAL的帮助文档,可以上网上查询相关说明,可以在Wiki上对共同的主题进行扩展或者探讨,多看相关的例程,跟着例程操作,多总结。5 Q# N/ `* V: o; u" t' l. o
(4)要学会使用ST提供的优秀的开发工具,掌握STM32Cube工具套件的使用方法,熟话说得好,磨刀不误砍柴工。
, [! P& Y3 {3 E& M3 p8 u; E不管怎样,我们的目的就是为了使用HAL库来开发,学会调用HAL库的API函数,配置对应外设按照我们的要求来工作,实现想要的功能。' x) h! [0 U1 G7 d6 Z. Q+ X
下面,我们进入主题,分析HAL库。
! K/ ^" `% B5 i2 y! r) v# {2 _# {' {7.2 HAL库文件夹结构9 y: l( a3 x; ?: q) h: I$ i3 y8 [& D" _. G
STM32MP1xx_HAL_Driver文件下的Src(Source的简写)文件夹存放是所有外设的驱动程序源码,Inc(Include的简写)文件夹存放的是对应源码的头文件。Release_Notes.html是HAL库的版本更新说明信息。STM32MP157Cxx_CM4_User_Manual.chm是HAL库的用户手册。& ^+ A* B& j( H4 u/ h$ K8 e4 h W6 U& ]
! X0 y, _. X$ K9 g+ S
$ Y& y% Z: m" B" m3 }# \3 H. i: `. t# z. K+ \5 v
图7.2. 1 HAL库文件夹
5 G5 j- S- g5 w1 k7 \/ L) i. z! |- H打开Inc文件夹,里边是一些以stm32mp1xx_hal和stm32mp1xx_ll_开头的.h文件,对应地,在Src下也有一堆以stm32mp1xx_hal和stm32mp1xx_ll_开头的.c文件,其中stm32f1xx_hal_开头的是HAL库文件, stm32f1xx_ll_开头的文件是LL库文件。( e9 s7 L) ^; k3 u! x' f
+ T8 C4 y( j6 H4 d/ `% K8 L
& w! R- A# f0 l3 m& D% i0 _4 Y$ i6 A2 T+ F
图7.2. 2 HAL库文件- L0 g: ?, E, a( Z5 P
HAL库下的文件很多,有一部分文件的功能可以归为一类,例如stm32mp1xx_hal_i2c.h/c、stm32mp1xx_hal_adc.h/c、stm32mp1xx_hal_dma.h/c等等这些文件,他们属于一些外设的配置文件,那么我们后面会以stm32mp1xx_hal_ppp.h/c来统称这些文件。有的是特殊文件,我们会重点介绍。HAL库关键文件介绍如下表:
1 g4 G5 `! L" c# o# \5 ^# N0 W( N' H+ _+ x- d- f
8 y. i6 m% H. y5 u5 Y/ X4 ~( W" r; Z0 P6 f+ p
表7.2. 1 HAL库关键文件介绍: c7 p6 D. }4 ?
除了HAL库的文件命名有规则以外,库中的函数以及句柄等的命名均有规律,如下表所示,其中PPP表示某个外设名。- j" E3 F/ n3 h
# [3 L/ E! w9 h. ^* a9 D; M
2 o' {+ g0 u) M4 |% z' v% J0 G
( T. {$ b6 A! A0 K9 @# J% D5 W表7.2. 2 HAL库的句柄和函数命名规律
* E% A; ~* T3 r% I& Z& v0 w例如I2C相关的,如stm32mp1xx_hal_i2c.h、stm32mp1xx_hal_i2c.c、I2C_HandleTypeDef、HAL_I2C_Init()等。对于HAL的API函数,常见的有以下几种:
. D& k R, i; ~' r) x" y初始化/反初始化函数:HAL_PPP_Init(),HAL_PPP_DeInit()
& l1 x I! h/ l; A( w外设读写数:HAL_PPP_Read(),HAL_PPP_Write(),HAL_PPP_Transmit()和HAL_PPP_Receive()0 B4 h- q# G7 m2 I2 P
控制函数:HAL_PPP_Set (),HAL_PPP_Get ()
! {" u; a5 N9 q状态和错误:HAL_PPP_GetState (), HAL_PPP_GetError ()
7 r; s h! }" K9 G# UHAL库封装的很多函数都是通过定义好的结构体将参数一次性传给所需的函数,参数也有一定的规律,主要有以下三种:: R: I' O' S* k. X; x
配置和初始化用的结构体( b8 K K- I4 G& N) B i! \- H
一般为PPP_InitTypeDef或PPP_ ConfTypeDef的结构体类型,根据外设的寄存器封装成易于理解和记忆的结构体成员。8 u8 e. y: p) E1 T
特殊处理的结构体
) L$ t9 ?: R: d- L3 [! x( T专为不同外设而设置的,带有“Process”的字样,实现一些特异化的中间处理操作等。3 g" H% l6 y3 F8 p1 H+ L Z2 P9 L: Y% m
外设控制句柄(PPP_Handler)
/ G' D! l# A7 e. \0 p0 [9 EHAL驱动的重要参数,可以同时定义多个句柄结构以支持多外设多模式,HAL驱动的操作结果也可以通过这个句柄获得。有些HAL驱动的头文件中还定义了一些跟这个句柄相关的一些外设操作。如用外设结构体句柄与HAL定义的一些宏操作配合,即可实现一些常用的寄存器位操作,即可实现对外设的操作。# O g% C8 ]) g3 D
/ P% x) [3 o' p! t4 x% |
- ~2 ]9 O+ |: ^4 }! @% Y' q
) R9 E# \. }& U% }0 X0 C表7.2. 3 HAL库驱动部分与外设句柄相关的宏$ h0 n% X T# S, }, V) |1 r
但对于SYSTICK/NVIC/RCC/ GPIO这些外设,不使用PPP_HandleTypedef这类外设句柄进行控制,如HAL_GPIO_Init() 只需要初始化的GPIO编号和具体的初始化参数。
$ B3 B3 d$ D+ x/ D6 @HAL_StatusTypeDef HAL_GPIO_Init (GPIO_TypeDef* GPIOx, GPIO_InitTypeDef *Init)
2 R1 t* T3 }+ h3 i" L: A9 a4 R{
5 x0 L( D6 N# V# R/GPIO 初始化程序……/$ }' o2 Y, j3 @% \; J+ B
}! q5 F H5 N: o: D1 L/ t; E! W2 k" n7 m
此外,HAL库中很多地方使用了回调函数,前面我们解释过回调函数可以被用户重定义,HAL库中的回调函数很多命名如下:
y9 W) A% b5 O1 F
! o+ k9 f3 G o- b5 r* u$ H
6 u$ h8 b# U% P6 S$ |4 y8 R
; Q6 v1 |% l0 v( w6 n5 u表7.2. 4 HAL库驱动中常用的回调函数API
: t* V( i1 r! @! @/ w3 O. S2 J4 f3 f至此,我们大概对HAL库驱动文件的一些通用格式和命名规则有了初步印象,记住这些规则可以帮助我们快速对HAL库的驱动进行归类和判定这些驱动函数的用法。 {& ` W4 X$ Q) N2 a
7.3 如何使用HAL库/ z# ~2 J5 A m$ p+ _% Y
HAL库下的文件实在是太多了,而且每个文件里的API函数也很多,我们学习的时候该从哪里看起呢?虽然文件多,API函数也多,不过我们可以将其进行分类,先将重要的配置文件了解一遍,然后再去学习某几个外设文件,只要学会了其中某部分外设,其它外设的就很类似了。
- K6 X) S/ g7 y' F针对庞大的HAL库,ST官方提供了HAL库的用户手册,例如STM32MP157系列的芯片,ST在HAL库里提供了STM32MP157Cxx_CM4_User_Manual.chm,我们双击打开此文件:* S I: L8 v( H9 Q7 k; S, V
目录结构下有Modules、Data Structures、Files和Directories共4个主题,我们先来查看Modules。' Y% R) M. \. G9 t7 e
Modules下有STM32MP1xx HAL_ Driver和STM32MP1xx_LL_Driver,分别对应HAL库和LL库的驱动,我们看HAL库下的驱动部分。
9 @: R! |5 T; A0 [5 g3 s+ t打开GPIO列表下的IO operation functions / functions,查看里面的API函数接口描述,我们选择查看HAL_GPIO_WritePin函数,双击即可以打开此函数的说明,如下图所示。可以看到函数的定义、函数的作用、函数注意事项、参数说明以及函数在哪个文件,位置是在哪里等等:
8 m6 ^( J" R' G2 n& c4 y0 E& y/ [& V7 x
1 R4 A0 ^+ ~4 Z/ H4 n4 G
4 L. S2 Q) e! `0 _, [% p- L; @2 _
图7.3. 1 HAL库用户手册
( @0 H7 y) s( Y- a' }& E% j通过说明,函数是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(置位)之一。
& z% C! {5 [( Q+ f% V提示中说明,这个函数使用GPIOx_BSRR寄存器来允许原子的读/修改访问,即操作的是GPIOx_BSRR寄存器,关于此寄存器,我们可以查看参考手册中的说明。3 ~% d/ ^) c$ [/ }; H& c/ L
所以,如果我们要将某个GPIO的某个pin置1的话,例如GPIOA的第2位,可以这样调用此函数:
# O% z/ B5 m. z! }- ~ m1 `4 fHAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);0 R; S# z& @' W/ d
如果要查找某个函数或者参数,可以直接在搜索框中进行搜索,如下图查找GPIO_PIN_RESET:4 \! o& d! |% W& L$ v
# y+ b: O% D5 C, P( y. h4 {
$ s* U; l* e: z
6 M% E. W I/ M$ W
图7.3. 2查找功能+ U4 W% N, V' w; ?3 A! h
其它的主题,例如Data Structures、Files和Directories也可以给我们提供更多的帮助信息:
% h- m. D- Z8 j4 ^4 ~6 [& Y* ]! P N; t6 L# i
& S% x+ ~! E7 t% v
4 n; P! y* M$ M4 `; u图7.3. 3其它主题
! _% G1 x6 j6 {( T关于此用户手册的使用就讲解到这里,在后面的实验中,遇见不明白的地方也可以查看此手册。! d- f% J3 x. x( n4 m& A
4 U' D# j( K- P
7.4 HAL库重要文件分析
+ [+ V3 T" Y" M下面我们先分析2个非常重要的文件:stm32mp1xx_hal_conf.h和stm32mp1xx_hal.c文件。其它文件,我们会在后面对应的实验章节做专门的讲解。3 q$ b9 J8 A3 ~% z( b+ i
7.4.1 stm32mp1xx_hal_conf.h文件
& p' G7 t \6 s, k' l7 Y9 Cstm32mp1xx_hal_conf_template.h是HAL库的配置文件模板,用于用户自定义驱动。如果使用MDK来开发,用户可以复制此文件到自己的工程目录,然后将其改名字为stm32mp1xx_hal_conf.h,STM32CubeIDE在生成工程的时候已经自动为我们做好了这一步,我们不用管。下面我们将stm32mp1xx_hal_conf_template.h文件的代码分为3部分进行讲解。
; p# n: u5 l$ i" R/ `. f
3 ?+ o% f8 G3 E# A" g M- 1 #ifndef STM32MP1xx_HAL_CONF_H; }$ I7 H2 e8 c' k3 i. E1 p/ U
- 2 #define STM32MP1xx_HAL_CONF_H7 R! `, W- y9 i
- 3 : H1 R9 j6 V9 g* x
- 4 #ifdef __cplusplus( a k1 {. L! ~ A" Z
- 5 extern "C" {9 O" I9 _4 q7 M$ ?/ S
- 6 #endif( C( u/ i$ v% a ^3 ?* {& }/ |$ r, i
- 7 /* ( _2 e6 r8 m8 m
- 8 * 模块选择
0 L2 ?9 T/ ]- H! F& y. E3 ]/ {$ n - 9 */
5 R$ D" t, [& ~& s0 l; h6 ?' S - 10 #define HAL_MODULE_ENABLED" T8 j) B0 e+ K0 O% X8 S
- 11 #define HAL_ADC_MODULE_ENABLED
, v3 A6 }: s; {* @2 c - 12 #define HAL_CEC_MODULE_ENABLED
! @" Y2 `" i; S; a$ n9 ? - 13 ......
; q* Y) ^% Q& E# b - 14 #define HAL_USART_MODULE_ENABLED3 G; C4 N" F7 c' M2 c
- 15 #define HAL_WWDG_MODULE_ENABLED
( T/ a" ]9 ]0 { - 16 /* 3 P3 ?4 f. y% _4 @- m! p4 Z1 Y/ N
- 17 * 注册回调函数选择5 p# Q. k: r. W% |8 G, N2 n n
- 18 */
4 i# Z! R' m$ Y8 a2 Y. P - 19 #define USE_HAL_ADC_REGISTER_CALLBACKS 0u
- D. w, ]( }/ l s) h" s$ u - 20 #define USE_HAL_CEC_REGISTER_CALLBACKS 0u
( u6 R+ n1 p% | - 21 ......
8 j2 [) x8 ?, w - 22 #define USE_HAL_USART_REGISTER_CALLBACKS 0u: Q, `" \* P- v1 d7 E3 f* A- [% U
- 23 #define USE_HAL_WWDG_REGISTER_CALLBACKS 0u
" u8 t, h. o! B - 24 /*
) v4 B3 p, W& Y8 [ - 25 * 在HAL SPI驱动中激活CRC功能
- h- d$ l$ ^7 e- ~+ m ^/ B; n - 26 */
( f; B) q2 S- `/ X - 27 #define USE_SPI_CRC 1U
8 { C9 X, R! a8 ^& V
复制代码 , n8 }3 D2 X2 H; {( p- I
第4行到第6行,对这个语法我们前面有讲解,这里不讲。
0 @2 Q# |6 M8 ^! G1 T/ J7 t第10行到第15行,这是将在HAL驱动程序中使用的模块列表,一旦使用了某个模块,那么就要使能相应的模块。例如,如果要使用TIMER功能,就在文件里添加:
! q# w& g9 V. G' V( u' s3 \ n1 |+ i$ A* |1 X0 r- g
#define HAL_TIM_MODULE_ENABLED+ H. }. k* v6 R" A
相应的,在stm32mp1xx_hal_conf.h文件最后也要加载对应模块的头文件。如果的是在在STM32CubeIDE上通过图形界面来配置的话,生成的工程已经自动为我们处理好了,如需要修改,可以再返回配置界面重新配置。, Z% w- {: C3 m& s" |, Y
' T& y3 T9 T: [% r( y7 o- #ifdef HAL_TIM_MODULE_ENABLED, w( g7 I, `& `
- #include "stm32mp1xx_hal_tim.h"0 g' \8 U9 h7 A/ U- k; a# N. G+ r0 Z
- #endif /* HAL_TIM_MODULE_ENABLED */
复制代码
) t( l& h* C7 K+ W/ I. O第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也是一个指针:7 b& s- c, k8 e$ S- N
0 @/ m3 }( O9 [9 J- 1 #if (USE_HAL_USART_REGISTER_CALLBACKS == 1)* y( G( k. l/ r- c
- 2 void (* TxHalfCpltCallback)(struct __USART_HandleTypeDef *husart); $ z0 K5 o) M4 n; l
- 3 void (* TxCpltCallback)(struct __USART_HandleTypeDef *husart);
7 L) ?; m/ T- o0 C - 4 void (* RxHalfCpltCallback)(struct __USART_HandleTypeDef *husart); / J5 z) p( L w7 K
- 5 void (* RxCpltCallback)(struct __USART_HandleTypeDef *husart);
/ `- t, D& A Z - 6 void (* TxRxCpltCallback)(struct __USART_HandleTypeDef *husart);
& V6 C- m) q$ f& E& K - 7 void (* ErrorCallback)(struct __USART_HandleTypeDef *husart);
8 l% n+ X; o* u+ k) J, u" M - 8 void (* AbortCpltCallback)(struct __USART_HandleTypeDef *husart); 4 t6 K& N) B# D+ a8 e$ A( S+ C% k0 S
- 9 void (* RxFifoFullCallback)(struct __USART_HandleTypeDef *husart);
$ t" q2 ^7 u) s% t - 10 void (* TxFifoEmptyCallback)(struct __USART_HandleTypeDef *husart);
/ ]! l8 O0 m0 F8 D% P- J9 e - 11
0 K3 Y+ [4 f6 Q r1 \+ f8 Q/ b - 12 void (* MspInitCallback)(struct __USART_HandleTypeDef *husart);
: o9 ?+ r! P7 l- } - 13 void (* MspDeInitCallback)(struct __USART_HandleTypeDef *husart); ! o% u/ t# G/ Q3 ]+ c1 c0 |
- 14 #endif /* USE_HAL_USART_REGISTER_CALLBACKS */
复制代码
X) {3 f; K9 R8 g/ cstm32mp1xx_hal_usart.c文件中USART_InitCallbacksToDefault函数的参数就是stm32mp1xx_hal_usart.h文件中的函数的指针,例如TxHalfCpltCallback就是它的参数(别忘了,TxHalfCpltCallback有自己的参数)
: m$ E1 g! [/ W+ U3 L1 e/ i( d# I+ c6 e, A$ e
- 1 #if (USE_HAL_USART_REGISTER_CALLBACKS == 1)0 s: O3 K% C5 w
- 2 void USART_InitCallbacksToDefault(USART_HandleTypeDef *husart)
7 H6 N5 V$ Z! v, k8 ~ - 3 {
% q4 y) x5 G0 q" Z$ h - 4 /* Init the USART Callback settings */; [7 [: |, j* H
- 5 husart->TxHalfCpltCallback = HAL_USART_TxHalfCpltCallback; * y- `% l; O2 z7 f8 k5 }9 o
- 6 husart->TxCpltCallback = HAL_USART_TxCpltCallback; ) d( ^" p) A" Y7 R" p
- 7 husart->RxHalfCpltCallback = HAL_USART_RxHalfCpltCallback;
6 i9 ~+ p6 @$ l& U5 {8 M* ^ - 8 husart->RxCpltCallback = HAL_USART_RxCpltCallback;
) k5 S6 d [# m" r7 p - 9 husart->TxRxCpltCallback = HAL_USART_TxRxCpltCallback; ! m" j/ k2 r/ T5 X( v% @% t% y
- 10 husart->ErrorCallback = HAL_USART_ErrorCallback; * L- Z& |: g; }2 r
- 11 husart->AbortCpltCallback = HAL_USART_AbortCpltCallback;
/ j6 y$ A- Y: y6 t S - 12 husart->RxFifoFullCallback = HAL_USARTEx_RxFifoFullCallback;
3 j. G! e7 X8 m4 a - 13 husart->TxFifoEmptyCallback = HAL_USARTEx_TxFifoEmptyCallback;
4 |, |: x0 ^" J( o1 J/ l! [% J - 14 }* q3 c% j' {" L1 ~: T
- 15 #endif /* USE_HAL_USART_REGISTER_CALLBACKS */
复制代码
& _" b( C' E3 T' D' `上面的就是回调(Callback)过程。(* TxHalfCpltCallback)叫做回调函数。回调函数就是一个通过函数指针调用的函数。以上的过程可以这么形容:
6 U9 @ v6 V& m+ k9 B. zA是回调函数,B是调用函数。A函数有参数a,B函数有参数b。参数b是一个指向A的函数指针,这样一来,就是B调用A,A有参数a,既然有参数就要给参数赋值,所以B函数内部给A的参数a赋值。那么,就是B调用A,A又利用B给A的参数a赋值,这个过程就叫做回调。8 ^0 f+ p( [: W/ Q7 C+ ~
总之,第19行到第23行,我们可以通过将0U改为1U来改变条件编译选项,从而实现我们前面说的回调过程。关于回调函数的使用,我们后面会进行讲解。
) @* q# {% O6 o4 Z# X6 y+ l0 @我们继续往下看剩下的代码,这部分主要是时钟配置:
2 `) J& R% v& `# x5 D+ U e7 i1 @2 A; |
- 28 /*
$ i$ Z* x; j. g, F! R - 29 * 时钟配置 {- G% I+ n5 f
- 30 */. B5 Q- Q, S8 _6 {( r( N) D2 h& X
- 31 #if !defined (HSE_VALUE) : W* w o: _( f) r+ K9 I" x
- 32 #define HSE_VALUE 24000000U /*高速外部振荡器HSE的值,24MHz */
* \* V: g- c! x; O: {+ [. J - 33 #endif ) z/ e$ p$ A' H5 b
- 34
/ `2 p7 j: {$ z6 Z6 A2 M! y" N( E - 35 #if !defined (HSE_STARTUP_TIMEOUT)
+ ~$ D0 b! d4 I Y3 H- s - 36 #define HSE_STARTUP_TIMEOUT 100U /* HSE启动超时,100ms*/6 Y; c- a( k4 K. ?- ?
- 37 #endif /* HSE_STARTUP_TIMEOUT */5 s6 X. o5 L$ ~, |
- 38 7 y* D/ Y9 `; D) j$ \; w, [+ U
- 39 #if !defined (HSI_VALUE)
f1 l8 c( N/ @. q" a- x z - 40 #define HSI_VALUE 64000000U /*高速内部振荡器HSI的值,64MHz */6 K' {" j2 Q. u/ Q7 q0 O% B
- 41 #endif
; r- V1 ^/ j! |8 E2 i - 42 / u' E5 |& B4 P2 o {& M) W7 b
- 43 #if !defined (HSI_STARTUP_TIMEOUT) ' l( z" ^1 e8 @) V! B, a) u
- 44 #define HSI_STARTUP_TIMEOUT 5000U /*HSI启动超时,5000ms */
- ] X5 `" W% V& f - 45 #endif /* HSI_STARTUP_TIMEOUT */ + E* |9 H9 k$ q! m2 `+ S* q
- 46 2 h# m, i* W, l
- 47 #if !defined (LSI_VALUE)
$ Y6 S, [3 g% @5 ~+ z2 B. l - 48 #define LSI_VALUE 32000U /*低速内部振荡器LSI的值,32KHz */3 e% r( _: ]$ ^/ n% [+ @1 V
- 49 #endif % d/ `/ K6 R4 G% Q8 q, D
- 50
8 N& N' Z. b- [ - 51 #if !defined (LSE_VALUE)8 a! I& }( O4 [/ R
- 52 #define LSE_VALUE 32768U /*低速外部振荡器LSE的值,32.768KHz */* y- M! w- D0 |$ \, [# F
- 53 #endif& F% z6 o; P* T0 L. B
- 54 . ?# M% x9 O. W: q4 T; g4 n, }
- 55 #if !defined (LSE_STARTUP_TIMEOUT)) Q. Z4 A0 |$ Y p* ]! ^% z( _
- 56 #define LSE_STARTUP_TIMEOUT 5000U /*LSE启动超时,5000ms */. B9 u9 X: }% f& T+ C2 b' @
- 57 #endif /* LSE_STARTUP_TIMEOUT */4 |5 ?7 }! \2 x$ |1 h+ ?
- 58
9 ?) ]8 B1 b% p" k - 59 #if !defined (CSI_VALUE)
6 S% H0 }5 R7 ^% G! P - 60 #define CSI_VALUE 4000000U /*低功耗内部振荡器CSI的值,4MHz */4 h4 _ x: M( S" E4 ^- W# I! b: D
- 61 #endif
) y7 r( e9 L1 ]7 h* C - 62
$ D; `2 A0 x5 T& ?4 P& `: s( } - 63 #if !defined (EXTERNAL_CLOCK_VALUE)
& v( @: p; h: b) v" s - 64 #define EXTERNAL_CLOCK_VALUE 12288000U /*外部时钟的值,单位是Hz */
& G: O0 d3 f0 {5 {+ Y - 65 #endif
复制代码 7 l! X; K! V2 ?# @% @" K
STM32MP157的M4内核有5个时钟可以用:2个外部振荡器HSE、LSE和3个内部振荡器HSI、CSI、LSI。
' x: r3 t' D7 G9 F4 X6 UHSE (High-speed external oscillator)是高速外部振荡器,可通过外接有源晶振驱动。官方HSE_VALUE默认是配置24Hz,这个参数表示外部高速晶振的频率,如果要使用外部晶振,请根据板子上外部焊接的晶振频率来配置,正点原子STM32MP157开发板的核心板上外接了24MHz有源晶振。
1 E% J) W K0 p0 f; `) E5 CLSE (Low-speed external oscillator)是低速外部振荡器,通过外接有源或者无源晶振驱动,官方默认配置32.768 kHz。正点原子STM32MP157开发板的核心板上外接了32.768KHz无源晶振,主要用于驱动RTC 实时时钟。
- W& i5 K6 P. E$ j' q9 H# FHSI (High-speed internal oscillator)是高速内部振荡器,频率可以是8、16、32、64 MHz。这里默认配置为64MHz。
% i8 |1 }7 ]! e4 O- C$ c1 GCSI (Low-power internal oscillator)是低功耗内部振荡器,这里频率默认配置为 4MHz。2 d. d! f2 u1 i0 Q* I6 _
LSI是内部低速振荡器,这里频率默认配置为32 kHz,实际值可能会因为电压和温度而变化。使用内部时钟优势是成本低,缺点是精度差。
% p/ N* v* M9 R8 E9 S, D2 ]/ a5 p这里是默认的值,如果我们不配置时钟,M4内核会默认采用HSI作为时钟源,频率为64MHz。如果我们配置时钟,可以直接在STM32CubeIDE的Clock Configuration图形界面来配置,配置好以后保存,然后生成工程,在生成的stm32mp1xx_hal_conf.h文件中,这里的值就会跟着变。关于时钟的配置,我们后面会有专门的章节来讲解。
* P& I Q, J" F下面我们看最后部分代码:) V' u. e% z; S# I# ]/ z/ h
- j8 K, r% _* w( G
- 66 /* " j) y3 o3 r& r$ m, N/ Z4 k
- 67 * HAL系统配置
2 C9 @4 u' \" V! @9 R2 {; U& c - 68 */
( ?8 S% y1 U9 X; V, n6 t. @ - 69 #define VDD_VALUE 3300U /* VDD的值,单位是mv */ " U0 `( w) J( O/ E" I: D. {8 w
- 70 /* 滴答定时器初始中断优先级 */8 m# ^: w& E1 u# T) E, H" k
- 71 #define TICK_INT_PRIORITY ((uint32_t)(1U<<4U) - 1U) 2 H4 z2 l1 `( N7 @5 Z8 x0 r' s, R3 Y
- 72
- {0 ]* @5 |1 F9 c* F5 c5 V - 73 #define USE_RTOS 0U2 ^( M; v/ X9 B1 }2 o5 y( g
- 74 #define PREFETCH_ENABLE 0U
) `( A7 u) [8 g0 r/ w( R8 A - 75 #define INSTRUCTION_CACHE_ENABLE 0U; N* }. a! o8 a @
- 76 #define DATA_CACHE_ENABLE 0U3 X2 R+ r- x. P0 ~5 y" D
- 77 /* 6 \/ L" u! G5 P6 B
- 78 * 模块库文件引用) q6 j" Z+ H# B7 d: _
- 79 */
0 N+ ]/ x! x9 A0 D" j0 ^+ `" c - 80 #ifdef HAL_RCC_MODULE_ENABLED% H4 t- D7 u* [, R1 o7 p, f
- 81 #include "stm32mp1xx_hal_rcc.h"
+ L* H* Q2 u- |" {! D' a, x0 u - 82 #endif /* HAL_RCC_MODULE_ENABLED */
5 p- c- V7 W! {$ C& v - 83 ......
. L; C$ T. m2 E0 d - 84 #ifdef HAL_WWDG_MODULE_ENABLED' c% e* w+ H Q8 l
- 85 #include "stm32mp1xx_hal_wwdg.h"
, G! ]- b( W k) d% G2 m - 86 #endif /* HAL_WWDG_MODULE_ENABLED */% G8 t. f6 X0 M
- 87 /*
: E D; O: e2 V3 I2 |4 Z/ z$ { - 88 * 断言配置: d6 h; L2 E" M* R* }: }
- 89 */
+ I9 I& `6 X6 V; ^# s - 90 #ifdef USE_FULL_ASSERT0 F Z/ T4 W! u# P) K i3 X1 b
- 91 #define assert_param(expr) ((expr) ? (void)0U : \ assert_failed((uint8_t *)__FILE__, __LINE__))
2 q# [. w- D8 w - 92 void assert_failed(uint8_t* file, uint32_t line);
E" N/ g! o5 l - 93 #else2 K, r" j d' j7 c) ]( m
- 94 #define assert_param(expr) ((void)0U)
( H4 o# h$ A/ v9 R2 [ - 95 #endif /* USE_FULL_ASSERT */
, y! a P V* C8 \/ T! p0 Q8 @$ }1 U - 96 & Y7 U$ [9 A- g- X1 @6 A. M0 |' c* z
- 97 #ifdef __cplusplus" a) K6 p) X6 j- k V e' y9 p- ~
- 98 }% Q( b5 M2 B# A" p
- 99 #endif
; c v1 P9 n* |& C8 I - 100
: S. t G: T% [( |6 ? - 101 #endif /* STM32MP1xx_HAL_CONF_H */
复制代码 * d- J3 i9 Z/ m+ d
第69行表示VDD值3.3V;
1 V6 D" ^5 b8 N: w( \3 m+ g+ Z第71行,表示Systick(滴答定时器)的中断优先级(默认最低为15),((uint32_t)(1U<<4U) - 1U)为0X0F(十进制的值为15)。这里插入一些内容,Systick属于M4内核外设,在前面的core_cm4.h文件中有定义。内核外设和其它片上外设不一样,它们的中断优先级有所差别,内核外设的中断优先级等级为0~15,数值越低,中断优先级越高。片上外设也有自己的中断优先等级,数值越低等级越高。
( L+ a% W6 J* O. G1 r" z以上是stm32mp1xx_hal_conf_template.h文件中的配置,如果用STM32CubeMX生成的工程,此值为0,也就是说STM32CubeMX默认配置Systick为最高优先级0(数值越低优先级越高)。
( ~4 `* c8 a' F/ N" L* [5 t$ e/ s6 b* D
0 i4 W2 }6 f; Z
7 ]+ X$ I) }2 z" ]6 x
图7.4.1. 1 STM32CubeMX生成的代码
" F# \$ N9 ~- E+ P8 L不因为Systick是内核外设,中断优先级就比片上外设的外部中断优先级高,所以,如果工程里有用到Systick和其它的中断,例如工程中某个中断A调用了HAL_Delay函数,而HAL_Delay函数是通过Systick来实现计时的,如果中断A优先级比Systick高,就会导致A中断优先执行,而Systick中断服务函数一直未能执行,就会导致程序卡死的情况,所以应该设置Systick的中断优先级比A中断要高,可以在第71行处设置(注意范围,内核外设的中断优先级等级是0~15),如果是STM32CubeIDE,可以直接在STM32CubeMX上采用图形界面来配置:# C B9 ^3 e9 n, o
# f% `: e* Y! \* ^" `' w: p
$ j; X: s. V$ l3 ~; B5 C7 c G' @: r' J( s% q/ v
图7.4.1. 2 STM32CubeMX插件上配置中断优先级( Q" i% J3 R2 ~3 A, W% W7 M) q
如上图,在之前的第一个工程实验中配置,Time base:System tick timer就是Systick的中断选项,勾选此项以后使能中断,然后再将Systick的中断的Preemption Priority(抢占优先)设置为1,修改好后按下“Ctrl+S”保存工程,在生成工程的stm32mp1xx_hal_conf.h文件中TICK_INT_PRIORITY值已经变为1。7 l; z8 L+ @) z o
; e3 U; j+ y$ z
: x6 l5 B0 p, B! N* y" Z8 a/ T2 X+ |0 [* N9 @( [
图7.4.1. 3中断优先级变了
: @" s* g7 v, x# K关于Systick我们后面有专门的章节做讲解。' f+ e1 {5 F, i/ s! P& X2 u; w! R! R
第73行,表示是否使用RTOS操作系统,目前HAL库不支持RTOS,所以这里默认配置为0。
$ _, X; m! M( G第74行到第75行,表示分别配置Flash预读取使能、指令缓存使能和数据缓存使能。M4内核没有可用的Flash,这里配置为0。* X8 c4 p9 u; m3 K
第80到第85行,和最前面第10行到第15行介绍的相关,一旦使能了某个模块,就要加载对应的模块头文件。
) K" h& J: i& I- U第90行到95行,表示断言。
' R0 x: K- |" I断言,就是一种代码调试技术,可以在调试阶段,帮助程序员选择满足规定范围的参数。比如某个参数只能选0或1两种,如果程序员写了3,那么在运行程序的时候就会给出报错提示。工程代码中加入了断言以后,虽然降低了程序的运行效率,但是方便了代码调试。一般程序在调试阶段采用Dubug版本,等项目开发完成以后,会给用户提供Release版本,Release版本的代码经过了各种优化,也去掉了所有的断言assert_param检验,最终的代码大小和运行速度都是最优的。
9 g* @ e: R! K: }; O S+ V3 M第91行:
# S7 M+ l4 o9 a#define assert_param(expr) ((expr) ? (void)0U :assert_failed((uint8_t \ *)FILE, LINE))
2 ^8 c3 J4 S2 s2 a9 I4 O# e包含了3目运算符,表示如果expr大于0,则assert_param(expr)为0,如果expr小于或等于0,则assert_param(expr)为函数assert_failed((uint8_t )FILE, LINE),其中__FILE__、__LINE__这两个是宏定义,表示当前所在的文件名和行号, 用来指示出错的行数和文件。7 f2 X9 t# n2 P& m# g
第91行,表示声明一个函数void assert_failed(uint8_t file, uint32_t line),函数的实体在STM32CubeIDE生成的工程的main.c函数有定义,或者在官方的模板文件main.c中也可以找到,如下是之前第一个工程实验的main.c函数,可以看到有定义assert_failed函数,函数的参数file表示发生错误的文件名,line表示发生错误的行号。
6 |7 ^& s* L3 i2 Z- d+ u( p O- u
3 Z1 J! u5 T! H2 d. v. x! n
: ^' l+ K" o8 E2 d; d8 ?9 E; C. W5 N
图7.4.1. 4断言相关函数/ k8 w6 ~* O- A& S# k8 O: @+ `
上图虽然main.c函数中有断言的代码,但是还没有开启断言,所以看到#ifdef USE_FULL_ASSERT和#endif /* USE_FULL_ASSERT */之间的代码背景颜色呈现灰暗色。
7 z1 j% G8 s9 Y' v第94行,#define assert_param(expr) ((void)0U)中((void)0U)表示不执行任何操作。
' {# e) C* [9 I) A! T默认情况下宏USE_FULL_ASSERT是没有定义的,也就是没有开启断言功能,如果有定义,我们就可以在代码中使用断言assert_param来检验程序中的参数是否正确了。我们打开第一个工程,在STM32CubeMX插件的Project Manager配置界面中选中Enable Full Assert开启断言,此操作方法我们在前面的4.2.5的第1小节有讲解,如图:: X- Z9 \. r7 w. ]' k' u5 w
$ r6 Y# w- t% G
& ]" Z6 o# L8 ~: m; E" o
7 p% s! B E' C, o+ ~% K8 Y
图7.4.1. 5在CubeMX插件上开启断言
) i* n8 D0 ~% X; S* W5 J选中以后按下“Ctrl+S”保存并重新生成工程代码,系统会自动开启断言功能。9 o, W" J0 a8 c* }$ e1 z: |) R
或者可以在M4工程的Properties中添加USE_FULL_ASSERT宏定义开启断言:
; d- @5 `4 P0 Y ]- ?$ n2 h2 o! I" Z: N v5 U, W; w
3 Q2 J, M0 g2 p. _$ ]
# G: H N& U/ E# W0 W图7.4.1. 6通过定义宏开启断言' l; k. N; G/ }" A+ D1 N
启用断言以后,main.c函数的断言部分的代码背景颜色不再是灰暗的颜色。可以在assert_failed函数内部添加以下代码,使错误的文件名和对应的行号可以通过串口打印出来(如果要用串口打印,工程中必须先配置好串口的功能才可以打印信息)。
. `. c6 @) y# C2 H/ q) Vprintf(“Wrong parameters value: file %s on line %ld\r\n”, file, line);
% o3 A) D( Y$ E; E9 b2 P" m5 @ ?" x1 b. Z
# ]/ Z+ r) o# N$ A. O6 L- U
+ v+ \- B6 i3 E) e7 u图7.4.1. 7添加判断函数
" k+ E0 Y& {5 W8 @下面我们分析HAL库中另一个重要的文件:stm32mp1xx_hal.c文件。
* { W6 ?5 w$ Z! t0 _! k7.4.2 stm32mp1xx_hal.c文件3 S( L( z& G' _, j# f2 Q/ z( R: p. L/ s/ I
stm32mp1xx_hal.c文件对应的头文件是Inc下的stm32mp1xx_hal.h文件。
3 j5 E0 w6 o u: K9 m0 Ystm32mp1xx_hal.c文件的内容比较多,它包括HAL库的初始化、系统滴答、基准电压配置、IO补偿、低功耗、EXTI配置等,下面我们分几个小节对该文件进行介绍。- @( N: y' i1 d6 a( g
: I& p: x. S. ?% Y) z4 _, l8 d' ^HAL_Init函数
& Z7 `. ]; t5 xHAL_Init函数主要用于初始化HAL库,该函数在程序中必须优先调用,在生成的工程中,main函数优先调用了HAL_Init 函数。# S5 k* u" V$ u3 ~+ L( K; m* m
- 1 HAL_StatusTypeDef HAL_Init(void)
s/ h1 _+ O$ W1 {- N; } - 2 {3 n, _9 P: L6 Y7 Q6 k$ G
- 3 /* 设置中断优先级分组为4 */8 i' G/ j2 i) P; z6 k
- 4 #if defined (CORE_CM4)
3 u7 y6 }# j' h8 ]# \ - 5 HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
1 Z' s: m, I# y" P4 f0 x9 c - 6 #endif
( v+ q5 w" m! {3 j - 7 ! N q( X" x; T! p) G
- 8 /* 更新SystemCoreClock全局变量 */
: o% k/ C$ {5 ?6 q: h - 9 SystemCoreClock = HAL_RCC_GetSystemCoreClockFreq();0 r; W0 S: A1 S! Z9 p
- 10# c! y. O5 I1 \3 o: f- \
- 11 /* 使用滴答定时器作为时钟基准,配置1ms滴答(重置后默认的时钟源为HSI)*/- N d! U) |. t! U* V
- 12 if(HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK)
6 [% ?2 l* c9 i& v1 F - 13 {
) ?0 E. X7 {5 P* ~! _+ w7 M8 S% b - 14 return HAL_ERROR;
9 ?( O9 l- {# Q8 j+ x9 B+ {. Y - 15 }9 ^, x0 }0 c' \. i$ ^6 h% g
- 16. P$ [) b8 v6 S7 u' x2 k
- 17 /* 初始化底层硬件 */
- T) T" e& t! T# K - 18 HAL_MspInit();
3 O! a. K S" N+ W+ e4 P! A/ {* x - 19
% U( _/ j+ N% D \ - 20 /*返回 HAL_OK */
# ]4 r1 i2 Y1 M5 d2 H/ V/ ^ - 21 return HAL_OK;
! { R/ E/ p$ V. r0 B' N - 22 }
复制代码
, s, ]" \. R6 T) ?9 Z/ dHAL_Init 函数主要实现如下功能:
, @ \! t H7 X+ x& x(1)默认设置NVIC优先级分组为4。可以在IDE上通过配置来改变此优先级,后面的实验章节会详细介绍这部分。
8 F, g" w/ K, n" C(2)配置SysTick(滴答定时器)每1ms产生一个中断。
' [3 a9 S4 @ B. Y(3)系统复位以后,在跳到主程序main之前先执行startup_stm32mp1xx.s启动文件,启动文件显示调用了SystemInit函数,然后才跳到main函数,SystemInit函数主要是初始化FPU设置、配置SRAM中的向量表和禁用所有中断和事件,并没有初始化RCC。所以,在进入main函数以后,系统还是默认使用内部高速时钟源HSI在跑程序,HSI的频率默认是64MHz。直到调用系统时钟配置函数以后,时钟才会发生变化。% i0 n, P9 Y9 W U; C+ b& q; b
(4)调用HAL_MspInit函数初始化底层硬件,HAL_MspInit函数在stm32mp1xx_hal.c文件里面做了弱定义:
) R: x# T$ k% C1 f__weak void HAL_MspInit(void)
6 L9 V0 z& V) N7 w' O D{
7 w9 `9 a1 L& F% I4 {/* 注意:这个函数不应该修改,当需要回调时,HAL_MspInit可以在用户文件中实现* /. N+ u9 n& J( ]' P7 e
}
; m9 {8 ~$ {/ v& M9 N$ uHAL_MspInit函数前有一个weak,表示对HAL_MspInit进行弱(weak)定义。弱就是表示此函数可以进行重写(重新定义),如果用户在其它地方重新定义一个同名函数,最终编译器编译的时候,就会选择用户定义的函数,如果用户没有重新定义这个函数(或者函数名字写错了),那么编译器就会默认执行弱定义的函数,并且编译器不会报错。是弱定义的函数都可以进行重写。关于弱定义的函数怎么使用,我们后面会有专门的例程进行讲解。5 |& z) w6 k% U2 i) [
我们用STM32CubeIDE创建的工程会生成stm32mp1xx_hal_msp.c文件,此文件中会重会新定义HAL_MspInit函数,此函数一般会打开AHB3外设的时钟,初始化中断优先级等,没有使用HAL_MspInit函数去初始化全部的底层硬件。而系统时钟初始化或者外设时钟初始化等操作其实是在main.c文件以及外设驱动文件中完成的。 l8 M# k* R9 n h
HAL_Init 函数的返回值是HAL_OK,表示成功。返回值是枚举常量,在stm32mp1xx_hal_def.h文件中有定义:! ]9 ]4 I. ]4 Q2 ]' k2 R
6 f/ R2 X" o, k6 C9 k* c- typedef enum
* x2 A G/ X$ V( T - {5 E+ u3 `% q. h5 `4 Q
- HAL_OK = 0x00U,
0 s2 K7 ]* R7 J& w - HAL_ERROR = 0x01U,3 l' N: z" U0 y, R
- HAL_BUSY = 0x02U,9 O. O% k( i3 ^8 Y5 u. I% N
- HAL_TIMEOUT = 0x03U, E1 q" w. c, Q$ S
- } HAL_StatusTypeDef;
复制代码
( j9 z% d- J8 J! x& ?1 k! oHAL_DeInit函数
! U) t+ R4 G5 R& vHAL_DeInit函数主要用于复位HAL库的,不过函数中没有实现什么功能,如果有需要,可以在里边添加相应的代码。
, W1 o/ i' x, A" @- 1 HAL_StatusTypeDef HAL_DeInit(void)
: T/ z' T3 X/ U# e) `9 q, G" E" z - 2 {" v7 c& P" y2 t6 p2 b. p8 o: w- M
- 3 /*重置所有外设*/' M) K, e! e% B7 ^+ z
- 4 - S F& m1 H+ f2 E: j
- 5 /*对底层硬件进行初始化*/
; q* j/ U+ }: V4 z8 U - 6 HAL_MspDeInit();8 h7 f6 i1 F' b
- 7 - F# E' e) _5 [2 P* l8 E: r6 B# m
- 8 /*返回功能状态*/; L" P, c& D/ H4 k; A6 q" D
- 9 return HAL_OK;- A s N! [8 u, p# c9 Z ?
- 10 }
复制代码 5 A2 K& X; a$ M _9 G9 l' s, l# y
HAL_MspDeInit函数主要是对底层硬件初始化进行复位,和HAL_Init里面调用的HAL_MspInit函数是一对,它在stm32mp1xx_hal.c文件中有定义,只不过是弱定义,如果用户需要使用该函数,可以在用户文件中进行重写。
0 p$ \' N3 z3 c5 r
0 ]: z- [% X! [& D: O7 F__weak void HAL_MspInit(void)/ C! Z1 v2 T8 `" ~' Q
{
5 w$ I! `6 ~( h9 ?+ b& E. L/* 注意:这个函数不应该修改,当需要回调时,HAL_MspInit可以在用户文件中实现* /% N2 c3 G* { m+ V
}
0 `. R4 X0 d, l( U; O Y. Y3. HAL_InitTick函数
! ~: G* H- j9 _- {9 z- Z& ^- ZHAL_InitTick函数主要用于初始化SysTick(系统滴答定时器)的时钟基准为1ms。函数前有weak弱定义,表示此函数可以被用户重新定义。STM32MP1有A7内核和M4内核,第4行到第48行是针对A7内核的代码,第51行到第73行是针对M4内核的代码。
( M+ Q7 F6 ~" _) Q
6 I* N" \' D% S- 1 __weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority), n4 z; s) { i! \% R5 Z
- 2 {% [/ h L/ z K6 h+ ?
- 3 /* 配置SysTick在1ms的时间产生一次中断 */" s1 M; E8 _* b/ k0 Y6 ?$ \8 S
- 4 #if defined (CORE_CA7)
6 J1 M; [2 Z: Q - 5 #if defined(USE_ST_CASIS)
, w4 k4 [7 D, I. M" N5 V4 ?# F4 d" y1 | - 6 HAL_SYSTICK_Config(SystemCoreClock/1000);
3 `2 R$ B' P8 w - 7 #elif defined (USE_PL1_SecurePhysicalTimer_IRQ)5 X8 v' r8 ~* ^( n3 J1 `
- 8 /* 停止计时器 */0 a, ^$ m0 g6 z& G$ e. e! P
- 9 PL1_SetControl(0x0);
3 E8 ~( f |; K( ^7 a/ F - 10 s; E$ L$ V% h4 v v
- 11 PL1_SetCounterFrequency(HSI_VALUE);
S0 \" r, a8 s% r, K: u5 c1 D! Y9 d$ p/ z - 12
+ ]! y( Z& k. v4 j; N, { - 13 /* 初始化计数器 */( s$ c# R, V0 S) z
- 14 PL1_SetLoadValue(HSI_VALUE/1000);5 v( C# P6 P! A+ j7 P
- 15
) [; n9 I x; o3 v$ T - 16 /* 禁用相应的中断 */
: J% o! ~7 X2 K& E* o. ?# z - 17 IRQ_Disable(SecurePhysicalTimer_IRQn);
, U u3 C0 s. ]9 a - 18 IRQ_ClearPending(SecurePhysicalTimer_IRQn);3 q3 k5 R/ Y+ y; }
- 19
/ F7 }! s3 Q3 T8 }. W( H, R - 20 /* 将定时器优先级设置为最低 (在MP1的A7的GIC中断中,只有7:3位可以实现) */
& V% C4 ]( z& x8 {3 | - 21 /* TickPriority基于16级优先级(来自MCUs),因此在7:4中设置,且位3=0 *// Z% ?+ q4 ~4 W4 u- q3 Q/ g
- 22 if (TickPriority < (1UL << 4))
( ~1 S, [" i1 i; H9 u+ h8 a+ S6 T3 i - 23 {
& m4 a* j7 [, R, Q! M - 24 IRQ_SetPriority(SecurePhysicalTimer_IRQn, TickPriority << 4);" ]- w- F) I5 ]" Q% e! ?4 m2 B
- 25 uwTickPrio = TickPriority;
+ Q' U9 R" o5 r: n - 26 }6 T6 @& z. o1 }* I! A
- 27 else
" ^, W# \0 f0 i - 28 {: m) ~! `: _( K b( R
- 29 return HAL_ERROR;
" b3 l' z( H" M$ }! n8 c3 b - 30 } N- \2 b2 ]1 T' r& Q2 X5 m* M
- 31$ { k9 L& e" G: @ D1 R% A4 m
- 32 /* 设置边沿触发的中断请求 */" a) v% ]! ?" }, G
- 33 IRQ_SetMode(SecurePhysicalTimer_IRQn, IRQ_MODE_TRIG_EDGE);1 T9 L/ @2 I: K! [% r
- 34
6 l+ e/ B* s6 b4 g - 35 /* 启用相应的中断 */
, I/ H) [% T L( a& w - 36 IRQ_Enable(SecurePhysicalTimer_IRQn);7 O+ }! P. H- q) N
- 37, U5 X" T& S$ t" T' P
- 38 /* 启动定时器 */
3 S! H) t8 k, O - 39 PL1_SetControl(0x1);
$ G" |1 n. ~/ w+ c4 u9 g - 40 #else
" }5 d( d4 F/ c+ C( W - 41 /* 设置计数器频率 */
8 I: R+ {; W: |, |/ |3 n- T - 42 PL1_SetCounterFrequency(HSI_VALUE);
1 i P$ q/ X! r* \6 ~2 [ - 43 // __set_CNTFRQ(HSI_VALUE);
1 | M% P9 K! e/ B8 e1 F - 44 /* 初始化计数器 */) X' k! `6 i$ K5 w
- 45 PL1_SetLoadValue(0x1);
+ n* A8 X. A( a2 | - 46 // __set_CNTP_TVAL(0x1);
7 _$ S0 P/ l/ H( l: r9 U: i! W - 47 #endif
8 t* W) I7 j/ ~6 _- w# e - 48 #endif /* CORE_CA7 */
: _" |' Q# B2 p: i! k - 49
8 Y, P( S6 T" a) W7 a - 50! {: W4 N# w. p# H" |8 U5 {
- 51 #if defined (CORE_CM4)
) m4 d8 _: v* N - 52 /* uwTickFreq是个枚举类型,如果检测到uwTickFreq为零,则返回1 */; `7 E* g. q, U$ c' m; E- D
- 53 if ((uint32_t)uwTickFreq == 0U)' V+ a. W& o" L' i
- 54 {
+ v. `7 @. S @8 x6 T- B! y/ k ` - 55 return HAL_ERROR;5 J4 S( o$ c4 K. i
- 56 }$ `. d; B/ { ~# b
- 57; u! y; Z0 e ~' p9 P1 i
- 58 /* 将SysTick配置为在1ms的时间产生一次中断 */
: P- ]+ d- X; ~ f2 ` o - 59 if (HAL_SYSTICK_Config(SystemCoreClock /(1000U / uwTickFreq)) > 0U)0 b7 v; c8 o. y; G* \. l' g
- 60 {
5 j9 T- I+ V- i0 j/ ]4 K1 U# h, H - 61 return HAL_ERROR;9 g) a& l# M4 ^
- 62 }) h' @( s4 s+ y
- 63 /* 配置 SysTick的中断优先级 */
% ~9 E2 X. e& ^8 o - 64 if (TickPriority < (1UL << __NVIC_PRIO_BITS))
' K8 ]% f% [& _) h' D. B k - 65 {
& E, n. h* y. Q- c/ t4 O - 66 HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
& M2 J) m/ f8 M0 B9 @+ j+ O/ Y - 67 uwTickPrio = TickPriority;
0 t7 g+ {+ O& ^3 _8 Z - 68 }# H% w& H* Y3 d; a# n
- 69 else
. W$ d2 K6 t' ~, [ - 70 {. i6 W" ^8 }$ ]& z8 y6 ]7 j
- 71 return HAL_ERROR;" J! m! G. A, z$ i8 p, C* A8 U; N
- 72 }
$ s/ h( c' u8 {* r - 73 #endif /* CORE_CM4 */
, G* X$ g) W% A' L9 N, `- J - 74* f @ y! C; Q, h7 @; g
- 75 /* 返回函数状态 */
1 e$ S4 k6 ]) S" N4 J" p' k2 j - 76 return HAL_OK;* x6 [& B; }- ^3 D, v0 d8 B4 T( Q
- 77 }
复制代码 0 V1 ?9 `% _- `2 B8 X! Q
本教程主要是介绍M4相关的实验,对于A7部分我们这里只是简单介绍。
1 l; X @. f3 M7 g6 O第4和第5行是条件编译选项,如果同时定义CORE_CA7和USE_ST_CASIS宏,则调用函数HAL_SYSTICK_Config(SystemCoreClock/1000)。其中SystemCoreClock在system_stm32mp1xx.c文件中有定义:4 ], z- I" y0 j* [
. q* g8 J; b. x; ~
uint32_t SystemCoreClock = HSI_VALUE;
$ B" O; ^+ r3 z" p* X8 G5 W7 X: nHSI_VALUE 在文件stm32mp1xx_hal_conf.h文件中有定义:( {$ S8 j/ I7 n5 _" ?" j
7 r& Z5 V- ]( F% I- #if !defined (HSI_VALUE). b* f$ E7 l1 Y" e
- #define HSI_VALUE 64000000U /* 内部高速振荡器HSI的值,64MHz */* j0 p/ o, q+ Q
- #endif1 d# d: ^/ e8 ?( W- g. |$ m2 u
- HSI_VALUE为64MHz,则SystemCoreClock/1000等于64000Hz。
0 g! H' b9 I1 y - HAL_SYSTICK_Config函数在stm32mp1xx_hal_cortex.c文件中有定义:
: H ]0 E, t0 s* q8 Y - uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
A" r3 r1 p8 m8 @ - {" U2 w) F. r" F/ H# c$ A1 U
- return SysTick_Config(TicksNumb);0 R1 ` C3 ~0 {; U
- }7 A8 E9 d& N7 F e+ Q
- SysTick_Config函数在core_cm4.h文件中定义(SysTick属于内核的外设):
7 I+ L. u$ h' [/ ?4 s- V - #if defined (__Vendor_SysTickConfig) && (__Vendor_SysTickConfig == 0U)# G' |; t5 l" g& [0 S
- & e+ U6 E# k. g7 X! s
- __STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
6 O+ D. T$ t' E# K - {
3 \2 i* L) ]/ N! x: ~ - if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
! D: j) n2 @) o - {0 N p: l, P! @
- return (1UL); /* 函数非正常终止,退出,递减计数器不会再重载 */
7 h3 H* P# J" L" C7 v - }1 D* P' Q* N: V' S, M; V
; h2 w1 I' O( N% z: U. O, {- SysTick->LOAD = (uint32_t)(ticks - 1UL); /* 重新加载寄存器 */ 6 C f Z; s; q1 Q
- /* 设置Systick中断的优先级 */
' h: G; q8 O+ r9 |# T+ M$ L0 | - NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL);. L' J- f& F8 j: |
- SysTick->VAL = 0UL; /* 加载SysTick计数器值 */
0 w0 G9 K1 L* V( W9 V# [2 j) i) `% X - /*启用SysTick中断和SysTick定时器 */ 2 M+ T1 w+ H, [& F
- SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | / U: I3 ~) C3 C3 j- M. }. ~
- SysTick_CTRL_TICKINT_Msk |4 a7 d7 X& z- z: q( L
- SysTick_CTRL_ENABLE_Msk;
5 y& h/ G* u1 ~ - return (0UL); /* 函数正常终止 */ 0 @# T9 R+ {. f) r$ p
- }
复制代码 我们简单分析此段代码,SysTick定时器是一个24位向下递减计数器,启动后,LOAD寄存器的值赋给VAL 寄存器,VAL寄存器递减,当递减到0的时候,会产生一次中断,然后再从LOAD寄存器取值,然后再从所得值开始递减,递减到0的时候又产生一次中断,如此反复,从而实现计时。LOAD寄存器的值是从SysTick_Config函数的参数ticks获取的,根据上述分析,ticks值默认为64000,复位后,系统默认工作采用64MHz的内部高速时钟源HSI来工作,可以计算SysTick产生一次中断的时间为:(1/64MHz)*64000=1ms。关于SysTick定时器我们后面会有专门的章节进行讲解。1 R! h; j* x- ?7 W
第7行到第39行,如果A7内核使用Secure PL1 physical timer来计时的话,则先停止A7的计时、初始化计数器、禁用相应中断、设置中断优先级和触发条件、启动中断、启动定时器等操作。3 }' _0 t% E3 X4 F( O! [
我们重点看M4部分。
7 R5 @) O0 Q" X( B7 V第51行,如果定义CORE_CM4宏,则初始化SysTick配置。4 p, M. U; R9 t
第53行到第56行,主要是判断参数uwTickFreq是否为零。
" t7 a6 P5 H W* p1 d4 m, V& q( Ystm32mp1xx_hal.c文件中有定义:6 I& g; Q2 J0 M2 h( R2 L
5 [7 }7 S: I }; `
- HAL_TickFreqTypeDef uwTickFreq = HAL_TICK_FREQ_DEFAULT;$ l. I! \+ P3 M' j' D" D; G
- 而stm32mp1xx_hal.h文件中定义: [8 C2 P* d2 j8 N
- typedef enum6 [! R& o" F* j/ Y+ q* J: T1 w' t& _
- {
& w0 @2 V: x/ L! E. k& b L - HAL_TICK_FREQ_10HZ = 100U,5 b1 c3 a+ g5 v5 d
- HAL_TICK_FREQ_100HZ = 10U,
3 l8 r1 `4 U3 Q( W3 \ - HAL_TICK_FREQ_1KHZ = 1U,+ U$ n: p- i+ M! b9 n( G+ [' E
- HAL_TICK_FREQ_DEFAULT = HAL_TICK_FREQ_1KHZ3 h: d: J% K+ i" @4 {1 N
- } HAL_TickFreqTypeDef;
复制代码
7 K* n4 Q9 z2 G' N所以,第59行到第62中的uwTickFreq值为1。根据前面的分析,SystemCoreClock的值为64000 000,所以HAL_SYSTICK_Config函数的参数是64000,HAL_SYSTICK_Config(64000))按照前面计算方法就是1ms,表示配置SysTick每隔1ms产生一次中断。至此,不管是A7内核还是M4内核,默认情况下,SysTick中断配置的都是1ms。/ F; E7 l/ T- A) k5 ~9 \3 h# m7 T
第64到68行,形参TickPriority用于设置滴答定时器优先级。
3 X% z8 y2 y5 S' u
6 X. s" l- H7 z' C6 c' MHAL_InitTick函数可以通过HAL_Init()或者HAL_RCC_ClockConfig()重置时钟。在默认情况下,滴答定时器是时间基准的来源,这里再次强调,如果其他中断服务函数调用了HAL_Delay(),要注意,滴答定时器中断必须具有比调用了HAL_Delay()函数的其他中断服务函数的优先级高(数值较低),否则会导致滴答定时器中断服务函数一直得不到执行,从而卡死在这里。
: {0 L2 p) e( k% O0 m' @. m4 t4. 滴答定时器相关的函数
4 H& t" W; W( R(1)HAL_IncTick函数
: ^4 h0 B6 s! I/ u__weak void HAL_IncTick(void)8 I6 k- c0 z+ o! f
{
; [/ w! i( a5 ruwTick += (uint32_t)uwTickFreq;3 R3 M$ R9 y- A; C9 [- {3 k: `
}
1 x6 k3 W; U- U# ^2 I5 d5 |( f" I函数前面有weak 定义,表示用户可以在别的文件中进行重定义。3 J# m3 G- ^, L. d4 p
HAL_IncTick函数在滴答定时器时钟中断服务函数 SysTick_Handler中被调用,滴答定时器每隔1ms中断一次,所以此函数每1ms让全局变量uwTick计数值加1 。滴答定时器时钟中断服务函数在ST官方的工程模板中,文件名是stm32mp1xx_it.c。
' M9 w$ C: \3 {) N( v3 R6 I+ Q/ G5 l4 r
. h; D# ^- M; n, j
2 l4 Y0 o+ t. ?: p
图7.4.2. 1 ST官方模板的中断函数文件
8 _# M# i) \0 ?# u或者在前面第一个工程实验中找到,SysTick_Handler函数就是处理SysTick定时器中断服务函数。
/ D* L9 Q* H1 n0 ^# _: _
& ] \! W8 n7 ?( T, {% o. i0 |% t4 V
. J) E- T7 _0 k: W- ]
2 K4 z0 R& u- S! n' m6 g$ h$ N图7.4.2. 2CubeIDE中的中断服务函数5 `0 X: l' v, y' N" I* c) D
(2)HAL_GetTick函数9 G" y' Q( Q5 e9 A2 X& y& g
- /* 获取全局变量uwTick当前计算值 */" e( h0 M p. a, o4 I& z
- 4 M* \6 \3 D2 W N1 |
- __weak uint32_t HAL_GetTick(void)
# \/ U; c: Q8 h5 {* f; F$ e - {/ L2 }. X! R- O, Q: { L. B
- #if defined (CORE_CA7)! k5 o' a% P( v* }$ R+ l
- #if defined (USE_ST_CASIS)
8 h" i' b: P1 k" S4 j1 A1 @ - return ( Gen_Timer_Get_PhysicalCount() / (HSI_VALUE/1000));' F7 b2 Y: `) U" L* V# H$ y _% ~
- #elif defined (USE_PL1_SecurePhysicalTimer_IRQ)
' [- o0 |' x7 k, I& C1 @1 b - /* tick在SecurePhysicalTimer_IRQ处理程序中递增 */( {' Z9 m# Q3 w1 I' C ^+ O
- return uwTick;
7 P/ B. L3 m# @ - #else V: r+ F- R1 L% N; T+ ]
- /* 直接从64位CA7寄存器得到的tick值 */4 a1 w n) c, u' h2 X9 g" k
- return ( PL1_GetCurrentPhysicalValue() / (HSI_VALUE/1000));; P; `9 h" a; S, ^) d
- #endif1 V+ g' O5 u* ?) R0 I8 U
- #endif /* CORE_CA7 */* D; ~+ \ K& l8 `( U7 A
- ' `- X9 S: T% g
5 Z# \! ?. L0 H; [, g- #if defined (CORE_CM4)
' b% s! I6 k' U! ?- I% J5 l - /* tick在systick处理程序中递增 */# R9 m% v( h7 u$ }4 ~
- return uwTick;
3 m3 c' f3 h# k. y5 O! u - #endif /* CORE_CM4 */! M# l0 ~, u& D) L: l# y8 H
- }
复制代码
0 l7 b0 k/ [2 n& \$ r- v函数前面有weak 定义,表示用户可以在别的文件中进行重定义。% d ~( I$ E7 w9 {& ?" g' J- L
HAL_GetTick函数用于获取32位的全局变量 uwTick 当前的值,这个值每1ms加1,那么HAL_GetTick() 应该返回的就是自启动以来经过的毫秒数。uwTick可以以毫秒为单位提供时间,那么可以用它来计时,事实上很多HAL函数都依赖它来计时的,例如我们之前使用的HAL_Delay函数。1 P5 M- e- f, X" ~8 E
8 l0 p$ T, S- l! ]- __weak void HAL_Delay(uint32_t Delay)
/ E3 C* l$ w# v$ @+ U - {4 o: G% v# A( ^( {
- uint32_t tickstart = HAL_GetTick();2 S/ y$ y* O z3 o3 [$ ?1 `, C
- uint32_t wait = Delay;6 G: p' Y/ H9 B# { j# H9 I6 z/ @
- $ l1 w/ i- [# [& A" {$ [
- /* Add a freq to guarantee minimum wait */
D! M# I) R! d/ s: p; E" G- w9 {) i - if (wait < HAL_MAX_DELAY)% t4 f( ~* y" c" _ @8 [# s
- {
; Q9 }0 B) ]: [ - wait += (uint32_t)(uwTickFreq);( J8 @7 H8 k& f/ w
- }
6 {! K* C& D% Q
# E; u; d& E/ _5 g# ?- while ((HAL_GetTick() - tickstart) < wait)
" I( k& m6 ^1 M, c+ X - {" |! ^, R# |" R: r6 P- _3 q I- i; ^) `& b
- }4 `1 S' n- }5 L* V
- }
复制代码
' Q" v5 Z) J' B `SysTick中断服务函数 SysTick_Handler 通过调用 HAL_IncTick 实现 uwTick的值每隔 1ms增加 1。HAL_GetTick返回uwTick的值,在进入HAL_Delay函数时,先记录当前 uwTick 的值,并标记为tickstart,然后不断在循环中读取uwTick 的当前值,再与记录的tickstart进行减运算,当(HAL_GetTick() - tickstart)的差值等于或大于wait的时候,跳出空循环,此时(HAL_GetTick() - tickstart)得出的差值就是延时的毫秒数。/ M! {2 u4 T4 `+ B& @4 K) u9 k
' s- T4 B4 H7 o9 `; K( {$ n
(3)HAL_GetTickPrio函数
5 c1 y' L9 O" d/ t- 4 R/ v) q6 o w; D
- #if defined (CORE_CM4)
! p3 X' V, a( |6 A - uint32_t HAL_GetTickPrio(void)
, U S) H0 \. l: `3 L/ H/ l; ]1 X- Q - {' N9 z1 s F" Z, {
- return uwTickPrio;+ E* D( E$ v% m& t% [9 @/ C F t
- }
复制代码
7 Z# Y0 ]; `- C u此函数就是获取滴答时钟优先级,可以在图形界面上配置好SysTick的优先级,然后在主函数中通过一个静态变量获取此优先级,例如下图,先配置SysTick的Preemption Priority(抢占优先)为2,保存配置重新生成工程:3 F) v! b. I5 S" g& Z9 E1 U
! E2 b2 H3 z% s9 ?1 ?- x% r
6 H" o. _! j- k) Q( e: d' X# [6 V
* i/ u- m/ q: ?7 j9 ?图7.4.2. 3配置SysTick的优先级
1 a, F8 N- E2 z6 c+ i! j然后生成的工程的主函数中添加如下代码,表示把滴答时钟优先级赋值给static变量priority:
+ Z g( t: R# [: d/ h0 d3 nstatic uint32_t priority = 0;
/ n/ |9 f% W. v- mpriority = HAL_GetTickPrio();
5 c1 j- b C2 }# B保存工程,编译无报错,进入Debug配置界面,此时箭头指在HAL_Init();的地方,表示从此处开始运行,此时,priority变量显示值为0。/ K2 w' g" C' l: e7 q: V( r
5 `4 |8 j4 r6 }% B, N
, V1 U g+ P, a2 R
9 U1 V) Y5 H) n2 w" K7 H图7.4.2. 4观察变量; j3 S; M2 t- H
我们在while处添加断点,然后点击Step Over单步调试,当箭头经过whlie以后,可以看到priority变量显示值为2,此值正是我们之前设置的优先级。6 v' v) X6 V* |8 z* w6 J$ x
2 F8 }' p; |5 J" G4 _1 W
/ Z$ N/ [- S/ Y* B. D2 K1 W/ K8 w
图7.4.2. 5单步调试- B, Z, u. F% l
HAL库中有很多的获取某个变量的函数,例如获取系统时钟频率的函数HAL_RCC_GetSystemCoreClockFreq,我们前面介绍的HAL_GetTick函数,获取定时器的计时数值函数__HAL_TIM_GET_COUNTER,还有获取串口中断标志位状态函数USART_GetFlagStatus,获取当前 RTC 时间HAL_RTC_GetTime等众多函数,我们可以利用这些函数获取我们想要的信息。8 z a8 @" r0 j: K+ N0 J
(4)HAL_SetTickFreq和HAL_GetTickFreq函数 Q% g# G1 l$ V- c' J1 Y
HAL_SetTickFreq 函数用于重新配置滴答定时器中断中断频率,HAL_GetTickFreq函数用于获取滴答定时器中断频率。1 o8 W' m s/ w: B n: o& m8 N5 x
: ?4 }4 i5 r) Z* @2 E
- 1 /* 设置滴答定时器中断频率 */
! {# n# R4 E/ r) p - 2 HAL_StatusTypeDef HAL_SetTickFreq(HAL_TickFreqTypeDef Freq)3 k6 h& E' G$ Z y
- 3 {- N, U& @4 N- ^ Z. b1 P
- 4 HAL_StatusTypeDef status = HAL_OK;
1 j( B q3 j; G; c - 5 HAL_TickFreqTypeDef prevTickFreq;
4 a8 @; z6 c: j( X( D/ G- `: U - 6 assert_param(IS_TICKFREQ(Freq));
" D$ s8 @: z4 L# D5 X - 7
H5 \% o, ` Q0 E8 v3 y - 8 if (uwTickFreq != Freq)
% x, @4 `6 d! g/ h, b' o0 }% F0 C5 Y - 9 {8 i2 k5 c* F) J& O3 q
- 10 /* 备份备份滴答定时器中断频率频率 */
x! _6 n% e" N - 11 prevTickFreq = uwTickFreq;) |4 C1 Y' } u# |4 ^! p. r
- 12
* e' G) @1 `) @* s" T - 13 /* 更新被HAL_InitTick()使用的全局变量uwTickFreq *// h6 k* g9 E7 x- \0 P" S5 d
- 14 uwTickFreq = Freq;
0 h) z& x3 X1 @1 J) _6 P - 15
% w2 K( D; ?0 G% G2 S7 `4 L1 N - 16 /* 应用新的滴答定时器中断频率 */! }- E, V& p) e! w+ J
- 17 status = HAL_InitTick(uwTickPrio);
% v) f; d3 [" y+ g - 18
2 U: u/ l6 k: Z9 w( I - 19 if (status != HAL_OK)% m2 B9 }9 p' K7 \+ z
- 20 {
0 R7 H6 ^. O# j0 S( h8 \! Q* h A - 21 /* 恢复以前的滴答定时器中断频率 */; C) m/ m' b# m6 ^
- 22 uwTickFreq = prevTickFreq;
5 {! G s6 q2 O5 y4 g* K4 j - 23 }4 t7 o9 ]* r/ Q0 \$ [
- 24 }
9 `; d2 D1 j; j- H% E: C - 25
; `2 g' S+ A: d( n - 26 return status;
, K7 z" S; D k q( w! d - 27 }0 m! K5 ], t+ K, H7 J& A: b2 k1 V
- 28 /* 获取滴答定时器中断频率 */
' p9 p1 G: E+ H0 t% _' d7 t - 29 HAL_TickFreqTypeDef HAL_GetTickFreq(void)5 J" R' u9 g8 p) P( G0 n
- 30 {
3 h' W4 ~6 Q" p8 g# V& x# ` - 31 return uwTickFreq;- F+ ]8 n# B2 j' D2 h; V
- 32 }
复制代码
X$ A6 [1 S# j6 ?在前面的HAL_InitTick函数介绍中,uwTickFreq的值默认为1,滴答定时器中断频率默认为1KHz(中断周期是1ms),如果我们要更改滴答定时器中断频率,可以通过HAL_SetTickFreq 函数来实现。我们先分析该函数的实现过程。( I4 A1 P/ S2 t
参数Freq是枚举类型,可选值是100、10和1,它是我们要设置的滴答定时器中断频率。
8 l) Y: M4 R+ A" x第5行,定义一个枚举类型变量prevTickFreq,表示频率,值可以是100、10和1。
1 u* V) b; }, v第6行,使用断言assert_param检查我们设置的参数Freq是否有效(参数Freq可以是100或10或1)( Q) W4 P& m7 L* B( [
第8到第24行,比较设置的参数Freq的值是否等于默认为1的uwTickFreq,如果不等于1,先备份uwTickFreq的值到新定义的uwTickFreq变量中,然后再将Freq赋值给uwTickFreq,如果此时HAL_InitTick返回状态正常,频率修改成功,如果此时HAL_InitTick返回状态不正常,那么uwTickFreq的值将不变,此时不能进行修改频率。8 v5 H# `" Z, |% u6 {
第29行到32行表示获取此时的uwTickFreq值,即获取滴答定时器中断频率。
. D& [% a$ v5 S- s+ S过程分析完了,是否如此?我们用前面单步调试的方法测试一下就知道。设置一个static局部变量freq,使用HAL_GetTickFreq函数获取此时滴答定时器中断频率的值,将此值赋值给freq:6 ]! `/ x2 I0 N2 h5 I+ A
6 M: ? q+ F4 f
static uint32_t freq = 0; /* 定义一个static局部变量 /! y! {, O9 K, z
freq = HAL_GetTickFreq(); / 读取此时滴答定时器中断频率 */9 ]9 f- ~( [7 F1 `
保存并编译工程,然后单步调试,观察变量freq的变化,如下freq默认值为1:
/ J4 H3 z y9 f7 }, p
" ^. h3 z$ |( @+ g
, p# |. k+ H" e- d6 X; ~
$ ^6 j1 _" m; v. s图7.4.2. 6 uwTickFreq值默认为1" P. v+ b3 V& \! _/ V& ~
我们在添加如下语句:4 E Y# m9 J9 G3 O
N. J: }# ]; l8 H3 o6 Q8 j
- HAL_SetTickFreq(10); /* 设置滴答定时器中断频率为10KHz */
. m8 V9 e/ Z5 l @; x - static uint32_t freq = 0; /* 定义一个static局部变量 */% A$ g+ f2 s) b4 h
- freq = HAL_GetTickFreq(); /* 读取此时滴答定时器中断频率 */
复制代码 - f T& z6 W( R8 ]
重新编译工程,此时freq的值变成了10,说明设置成功,证实了我们前面的分析。6 E# |3 b/ L, j: y5 A
- [* n- u) Q% W( Q' l* H
+ y# K7 m0 O* ?- X/ z
& I; W# g- G' H3 P图7.4.2. 7成功修改uwTickFreq值- O/ Y$ _) C, m" G/ g) {" k
/* 挂起滴答定时器中断,全局变量uwTick计数停止 */
X' W, X* S* y _4 h1 C2 n; D" o3 x, b! h3 w/ T+ k
- __weak void HAL_SuspendTick(void)
2 ~0 y4 L8 w2 E- T" ?; { - {% F9 R- d3 `) y' y" z8 M' J: H8 L
- #if defined (CORE_CA7)
3 X3 o; c5 m2 O - #elif defined (CORE_CM4) n$ _2 Y9 `" G
- /* 禁止滴答定时器中断 */; N6 S) L- R4 l3 j3 h
- SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
4 M3 J' G6 P2 q1 \ - #endif
6 m+ N- t3 @% J' O' t8 g - }
: ~; p K- K$ R! P: P - /* 恢复滴答定时器中断,恢复全局变量uwTick计数 */
4 D8 }% `( w V3 m - __weak void HAL_ResumeTick(void) C) O7 z9 ?* t3 p% Q
- {
" e& U' t( j# f1 `5 q( J8 p( Z - #if defined (CORE_CA7)
% L: G Q8 { W! Y n" ~0 d - #elif defined (CORE_CM4)
) t3 I+ N# w) j - /* 使能滴答定时器中断 *// J. [' k% n, Y
- SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;7 L5 D$ b! L( Z D" q) Q' k$ S
- #endif
+ c2 R7 t) v. y, g7 _& B - }
复制代码 + y% O8 {1 ]* [0 @
默认情况,SysTick计时器是时间基的来源,它以固定的时间间隔产生中断,一旦HAL_SuspendTick函数被调用时,SysTick中断将被禁用,因此Tick增量被暂停。函数被声明为弱函数,可以在其他函数中被覆盖时使用。
; f& D* {2 {5 R# i4 H3 @+ S2 z( R" f0 u# z- O
HAL库版本相关的函数! T/ h6 }; N. a# G! ?; R
相关函数声明如下,这些函数了解一下就好了,用得不多。4 |+ m. E! m0 E: P: F9 H
- uint32_t HAL_GetHalVersion(void); /* 获取HAL库驱动程序版本 */
( G5 ]0 t+ c3 k: c# R# D( q - uint32_t HAL_GetREVID(void); /* 获取设备修订标识符 */
' p9 T7 R5 v& c% I - uint32_t HAL_GetDEVID(void); /* 获取设备标识符 */
复制代码 2 Y b7 V8 z# \0 q* i8 u3 ^
HAL库调试功能相关函数! d9 L. R+ L, u# ^0 |
这些是调试功能相关的函数,可以在不同模式下使能或者关闭调试器。将对应函数添加到代码中就支持DEBUG在各种模式下,比如,- HAL_EnableDBGStopMode,就可以在stop模式下进行调试。源码可在stm32mp1xx_hal.c文件中查看,函数声明如下:8 v, {+ u+ u, G1 t4 f" }- Q
- void HAL_EnableDBGWakeUp(void) /* 启用调试模块(唤醒模式下) */& X% W9 ~7 ~* L. p& {
- void HAL_DisableDBGWakeUp(void) /* 关闭调试模块(唤醒模式下) */
1 l" \/ X' {5 c* Z& F' j) @ - void HAL_EnableDBGSleepMode(void) /* 启用调试模块(休眠模式下) */
3 u( X4 s* |: Y0 g) h - void HAL_DisableDBGSleepMode(void) /* 关闭调试模块(休眠模式下) */; d# r; R/ B6 @, t( j0 l
- void HAL_EnableDBGStopMode(void) /* 启用调试模块(停止模式下) */
+ X& b3 z6 ~3 b& ?5 a. _- h - void HAL_DisableDBGStopMode(void) /* 关闭调试模块(停止模式下) */
5 c$ }4 H! q2 r( j* q0 [- t* D - void HAL_EnableDBGStandbyMode(void) /* 启用调试模块(待机模式下) */
0 ?5 Z. N3 B) r$ ~ p0 I - void HAL_DisableDBGStandbyMode(void) /* 关闭调试模块(待机模式下) */
复制代码
: g9 O# ^- Z( q m# O3 A6 E) f芯片内部电压基准相关函数
" Y6 I/ u: e# i6 S源码可在stm32mp1xx_hal.c文件中查看,函数声明如下:
# J) T* I& {/ C& `1 _- void HAL_SYSCFG_VREFBUF_VoltageScalingConfig(uint32_t VoltageScaling);
% F2 L0 i2 l4 O2 I - void HAL_SYSCFG_VREFBUF_HighImpedanceConfig(uint32_t Mode);
6 y: U5 K0 v( Z# r/ O+ ? - void HAL_SYSCFG_VREFBUF_TrimmingConfig(uint32_t TrimmingValue);
# N% k; g7 @% ] - HAL_StatusTypeDef HAL_SYSCFG_EnableVREFBUF(void);8 j& U8 H2 L U4 j
- void HAL_SYSCFG_DisableVREFBUF(void);
复制代码 1 B5 m1 K% b( g, K! U
HAL_SYSCFG_VREFBUF_VoltageScalingConfig函数用于配置芯片内部电压基准大小,形参有四个值可以选择:+ o! _ v$ A O: G' L1 C- O7 f
1)当形参为SYSCFG_VREFBUF_VOLTAGE_SCALE0时,
0 W1 R6 `6 {3 i& E电压输出基准为2.048V,条件是VDDA >= 2.4V。
% M- I9 ^4 N0 r, e( }0 I2)当形参为SYSCFG_VREFBUF_VOLTAGE_SCALE1时,6 c- K6 P' }( u/ A) Y6 r
电压输出基准为2.5V,条件是VDDA >= 2.8V。
5 ]) M1 S- G( M( a3)当形参为SYSCFG_VREFBUF_VOLTAGE_SCALE2时,
4 S G K4 x" k |8 V _电压输出基准为1.5V,条件是VDDA >= 1.8V。
" Q4 w1 X) ^# I `7 c. ]4)当形参为SYSCFG_VREFBUF_VOLTAGE_SCALE3时,
; P3 O1 `& ]0 L0 U" w电压输出基准为1.8V,条件是VDDA >= 2.1V。
. n7 ?! Z8 E& K/ T' P T/ X# sHAL_SYSCFG_VREFBUF_HighImpedanceConfig函数用于配置芯片内部电压是否与VREF+引脚连接,即是否选择高阻抗模式,有两个形参选择:7 V" i7 n+ n' t
1)当形参为SYSCFG_VREFBUF_HIGH_IMPEDANCE_DISABLE,表示导通。 k: d& d6 N( G9 L
2)当形参为SYSCFG_VREFBUF_HIGH_IMPEDANCE_ENABLE,表示高阻抗,即不导通。8 E$ A2 r9 N$ a( Q, g, k0 _3 X
HAL_SYSCFG_VREFBUF_TrimmingConfig函数用于调整校准内部电压基准。) u! ~. C/ A% u+ v" }* a) T: K% D" ~
HAL_SYSCFG_EnableVREFBUF函数用于使能内部电压基准参考。
) x. b& D K# P$ L! z ]5 ?% kHAL_SYSCFG_DisableVREFBUF函数用于禁止内部电压基准参考。3 U1 I4 p7 m: Z+ Y( U2 u
8. 以太网PHY接口选择函数& q8 e, h/ u! X
该函数用于以太网PHY接口的选择,可以是MII或RMII接口。
/ ~4 v$ e; g/ l8 F6 U) r2 Qvoid HAL_SYSCFG_ETHInterfaceSelect(uint32_t SYSCFG_ETHInterface)7 K9 E# x) _5 K( q( o( F2 p, q0 ~% t
9. HAL_SYSCFG_AnalogSwitchConfig()函数
, ~2 f9 w/ s0 z I- l当PA0、PA1引脚复用为ADC的时候,还有一组对应的可选引脚ANA0、ANA1。该函数的作用就是用于模拟开关控制,用于切换这些可选的引脚。源码可在stm32mp1xx_hal.c文件中查看,函数声明如下:8 ]8 c E# m! a
void HAL_SYSCFG_AnalogSwitchConfig(uint32_t SYSCFG_AnalogSwitch , uint32_t SYSCFG_SwitchState )
$ C2 ] b# ]( T参数SYSCFG_AnalogSwitch表示选择模拟开关,可以选:) K5 v5 \0 J3 y1 O9 u1 i
SYSCFG_SWITCH_PA0:选择PA0模拟开关
3 `! d! \3 I) H4 z( bSYSCFG_SWITCH_PA1:选择PA1模拟开关
' W7 z* z n) G2 z+ f参数SYSCFG_SwitchState表示打开或关闭双垫之间的模拟开关,此参数可以是以下值之一或组合:. G( }. [1 l) H; C, o' C
SYSCFG_SWITCH_PA0_OPEN
9 B' C2 i- z8 eSYSCFG_SWITCH_PA0_CLOSE
; |+ C4 H, u4 S" h# m! U, NSYSCFG_SWITCH_PA1_OPEN0 i8 A7 _# J) I. }& ?$ V
SYSCFG_SWITCH_PA1_CLOSE5 q2 x5 l3 Z, d% A' j7 J& i
% m I0 F( n$ P3 r& A+ N8 U
$ M5 w; [% ]7 }2 R8 v3 `2 F
7 y& M; }. X/ h7 y' p! C5 j图7.4.2. 8模拟开关控制
' `. f2 ~( d; M' }: e8 K: B5 W该函数操作了SYSCFG_PMCR寄存器,可以查看参考手册第14.3.2节了解有关配置模拟开关的详细信息。, ?& b: H- X* M- v2 |/ M: P
10. Booster的使能和禁止函数(用于ADC)
: W" w1 `8 c( a3 ^) h源码可在stm32mp1xx_hal.c文件中查看,函数声明如下:
( g; {) `/ K- t$ i; g! t- R7 W/ s0 j' e& U3 d
- void HAL_SYSCFG_EnableBOOST(void) /* 使能Booster */: [, r- W# h6 `$ Q; F' G( z6 b4 A. \: t
- void HAL_SYSCFG_DisableBOOST(void) /* 禁止Booster */
复制代码 - ?- w) a( ~. C* x9 K- j
如果使能Booster,当供电电压低于2.7V时,能够减少模拟开关总的谐波失真。这样就使得模拟开关的性能和供电正常的情况时一样,能够正常工作。0 Z& N; A& L2 I' `( S* E: Z
11. 启用或者禁止IO补偿函数) w+ Q$ h! d/ L# I( l! q
源码可在stm32mp1xx_hal.c文件中查看,函数声明如下:
( [! y5 \/ |* c; r# q* F
9 U( k! y. `. s5 o5 M- void HAL_EnableCompensationCell(void) /* 使能IO补偿单元 */2 m* _8 q3 T3 c, L* }. U# Z" A8 n
- void HAL_DisableCompensationCell(void) /* 关闭IO补偿单元,默认下是关闭的 */
复制代码
6 s! o7 [# O: Z7 s7 x这两个函数用于使能或者禁止IO补偿。I / O补偿单元仅可在设备供电时使用,且在电源电压为2.4V~3.6V时,使用IO补偿功能才有意义。默认情况下,不使用I / O补偿单元,当I / O输出缓冲区速度配置为50 MHz以上模式时,建议开启I/O补偿单元来减少对电源带来的噪音。# x) x7 A4 ]# s
12. IO补偿、优化IO速度以及低功耗等相关函数, K9 Q+ _4 M- q4 o% I: O: o3 z
IO补偿、优化IO速度以及低功耗等相关函数,这里先不进行讲解了,后面用到我们再进行说明。
4 s+ r; @3 V. @, p3 Y5 r
) j6 l d9 ^: U5 x# }3 L/ }7.5 章节小结- j+ s% I* J" S; c' }' P' A, |) ]
本章节是认识HAL库的重要环节,或许分析的过程有点枯燥,代码比较多,让人看得眼花缭乱。即使HAL库的文件和函数再多,我们通过去分析一些共性的东西,先挨个分析重要的文件以及函数,然后把他们串起来,脑海里形成最初的认识,在以后的学习和实验过程中,我们还可以回过头来再看看,加深理解。通过认识、理解、实践、再理解的过程,我们可以逐渐掌握必备的知识点。
& P4 P9 a3 K9 w# I2 M本章节我们重点讲解了HAL库的两个重要的文件:stm32mp1xx_hal_conf.h和 stm32mp1xx_hal.c文件,结合前面的第六章节的实验,下面我们将固件库的文件关系用一张简图来描述(图中省略了部分.c文件和.h文件),其中箭头指向的文件表示要调用某个头文件的文件,例如箭头由stm32mp1xx_hal_conf.h指向stm32mp1xx_hal.h,表示stm32mp1xx_hal.h调用stm32mp1xx_hal_conf.h。
2 ^; R! j! o( H9 k5 O
5 F" V5 J. V2 I8 \
! u' m; ~. U( U6 O9 E& T6 ]
0 f; l! |* j$ x+ e! H+ D
图7.4.2. 9 固件库头文件关系4 y) Q3 S4 @" ?( U
stm32mp1xx_hal_conf.h头文件基本上是一些宏定义和条件编译,用于用户自定义驱动,外设驱动配置文件可以根据需要include相关的外设头文件,不过在STM32CubeIDE上开发的话,此文件一般不需要我们手动去改,一般是通过STM32CubeMX插件完成配置。7 J3 N" w; G8 Y& u) b$ \
stm32mp1xx_hal.c文件主要包括HAL库的初始化、系统滴答、基准电压配置、IO补偿、低功耗、EXTI配置、调试相关等函数的定义,其中weak定义的函数可以根据需要被用户在其它文件中进行重新定义。stm32mp1xx_hal.h头文件包含了HAL模型的所有驱动,用户的驱动文件或者main.h文件可以直接include此文件。
\2 }7 V( z% N; y+ i————————————————
, @8 b% b) g, h/ c* m9 s版权声明:正点原子( \& n. h9 s( ~$ ^: b. M
5 z# @4 @) }. H; o# p
5 Q/ r- y# X% N8 C ]" g% {4 }4 W
% v# M* R- O; @" N3 g. N+ H) \7 Q |