掌握HAL API中面向对象设计的思想

电子说

1.2w人已加入

描述

1. 初识HAL

ST 为开发者提供了三种的开发库:

  1. 标准外设库(Standard Peripheral Library, SPL库)
  2. 硬件抽象层库(Hardware Abstraction Layer,HAL库)
  3. 底层库(Low-Layer,底层库)

其中,ST CubeMX软件支持STM32全线产品的HAL和LL库;SPL已经停更,部分芯片如STM32F7xx没有推出SPL库。

相比标准外设库,STM32 HAL库拥有更好的抽象整合水平,HAL API(HAL Application Programming Interface,HAL应用程序接口)集中关注各个外设(Peripheral)的公共函数功能,通过定义一套通用的、用户友好的API函数接口,支持不同STM32系列产品之间的轻松移植。

以点亮LED的工程举例。

1.首先配置MDK的代码补全

Edit Configuration Text Completion Symbols after 3 Characters。

HAL库

2.代码补全效果。

HAL库函数都以HAL作为开头。打开代码自动补全后,输入HAL_GPIO即可弹出一系列支持的函数,如下图的Init(初始化)、LockPin(锁引脚)、ReadPin(读引脚)、TogglePin(翻转引脚)等。

HAL库

3.HAL支持哪些函数?

如下图所示,点击MDK左侧工程栏下方的Functions,点开对应的hal_xx.c文件,即可显示出所有的HAL库函数。

HAL库

ST的HAL库通过高度抽象化,使用统一的HAL API对硬件进行操作。无论是使用STM32F1系列、L4系列、F7系列、H7系列等,对GPIO的初始化、读、写、翻转操作都是如下的统一接口,极大地方便了开发者将相同的代码移植到不同的ST系列芯片中。

  • void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
  • GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
  • void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
  • void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

CubeMX通过图形化界面操作,配置各个引脚、外设的工作状态,自动生成驱动初始化代码,方便用户快速进行底层功能部署,开发者只关注CubeMX图形化界面的配置,可以不关注写底层硬件寄存器,通过调用统一的HAL API实现外设各种功能,这是HAL的一个典型特点。

2. STM32 Manual

关于STM32L4系列的手册,可以在https://www.st.com/zh/microcontrollers-microprocessors/stm32l4-series.html下载相关手册。

HAL库

ST系列常见文档的命名规则如下:

1.AN, Application Note ,应用手册。一般是一些相对复杂、精细、精巧的应用原理与结果介绍,阅读门槛较高,建议熟悉芯片、熟悉嵌入式系统后,再根据具体开发工作需求进行查找与阅读。
2.DS, Data Sheet ,规格书。芯片手册,说明芯片容量、芯片时序、芯片封装等情况的文档,一般用于硬件选型阶段。
3.UM, User Manual ,用户手册,为开发者提供HAL库使用说明、硬件使用说明等情况的文档,开发阶段可以作为参考书。浏览https://www.st.com/zh/embedded-software/stm32cubel4.html可以找到STM32L4系列的HAL库UM手册。本课程要求下载UM1884 Description of STM32L4/L4+ HAL and low-layer drivers.pdf手册。建议将该手册作为参考书,有需要时再查阅,不要通读,以后该文件简称为UM1884.pdf文件。
RM, Reference Manual ,参考手册。说明芯片内部寄存器如何配置的手册,本课程要求下载RM0394_STM32L41xxx/42xxx/43xxx/44xxx/45xxx/46xxx advanced Arm®-based 32-bit MCUs.pdf文件,对应例程逐步深入了解。以后该文件简称为RM0394.pdf
4.PM, Programming Manual ,编程手册,针对具体芯片,一般是RISC汇编指令的解读,不推荐给初学者。
5.TN, Technical Note ,技术手册,一般是一些芯片规格、封装、PCB制版、Toolchains等软硬件方面的杂项技术要点和进一步解读,不推荐给初学者。

3. 熟悉GPIO HAL Driver

STM32L431RCT6芯片有GPIOA~GPIOE、GPIOH等6个IO口,其中,每个IO口都有16个引脚,从GPIOx的PIN0 ~ PIN15。

在第一个EVB MX+的GPIO例程中,我们翻转GPIOC的引脚13,实现LED的点亮和熄灭。

HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);

/* 其函数原型为 */
/**
  * @brief  Toggle the specified GPIO pin.
  * @param  GPIOx where x can be (A..H) to select the GPIO peripheral for STM32L4 family
  * @param  GPIO_Pin specifies the pin to be toggled.
  * @retval None
  */
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

我们依次认识GPIOCGPIO_PIN_13,从HAL库的数据结构、操作原理、STM32的GPIO结构的角度,来逐步深入了解。GPIO是最基础的内容,掌握了GPIO的HAL操作原理,也就理解了USART、SPI、ADC、IIC等更复杂外设的HAL库工作原理。

3.1 回顾指针

3.1.1 内存中的数据与数据类型

HAL库

计算机的内存,可以简单看作一条长街上的一行房子,每一个房子内能容纳数据,并且每一个房子具有独一无二的编号。

HAL库

  • 上图中,每一个格子表示1个字节,一个字节的无符号数的表示范围是
  • 为了存储更大的数,我们也可以将4个字节看作一个单元,在32位计算机中,4个字节即一个字word

HAL库

计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样。如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。

我们将内存中字节的编号称为地址(Address)。地址从 0 开始依次增加。对于32位环境,程序能够使用的内存为 4GB,最小的地址为0x00000000,最大的地址为0XFFFFFFFF

下图是 4G 内存中每个字的编号(以十进制表示):

HAL库

举个简单例子:下图表明计算机中, 5个连续的字单元中的存储内容。

HAL库

  • 不得不说,如果直接通过地址编号去读取/修改这些数据,是一件让人为难的事情
  • 高级语言提供了解决方案,支持通过变量名进行访问;
  • 通过变量名来访问变量,对于开发者非常友好。但是要时刻记住计算机硬件依然是通过地址来访问内存单元(Hardware still accesses memory locations using addresses)

下图和代码表示通过变量名访问内存:

HAL库

int a = 112, b = -1;
float c = 3.14;
int *d = &a;
float *e = &c;

在上述代码中,变量d和e是指针,它们不是int和float类型,而分别是(int *)和(float *)类型它们是变量,也存储在内存中。在变量d中,可以存储int类型变量的地址,在变量e中,可以存储float类型变量的地址。

通过前面的图,我们已经知道,变量a存储在地址编号为100的格子中。如果需要将变量a的数值修改为200,则下面语句互相完全等价:

a = 200;
*d = 200; /*变量d之前的*,是指针变量的解引用操作符,derefrence,返回存储在指针地址中的值*/
*( (int *)(100) ) = 200;
  • 第三条语句是典型的C语言Cast,即类型转换。
  • 第三条语句将无符号数100强制转换成了(int *)的指针,然后在编号为100的地址中写入数据200。
  • 但是,务必要注意,这种写法很危险。我们在编译程序之后,一般并不知道某个变量在内存中的存放地址,通过直接地址编号进行数据操作,很容易造成程序崩溃。
  • 但是,ST HAL库对内部寄存器操作,却主动采用了这种看似危险的做法。后文会清晰说明原因。

3.1.2 指针是变量

假设声明的变量被依次存放在0x20000000UL地址开始的单元格内。

unsignedint  a    = 0xFFFFFFFF; /*无符号数据,4294967295*/
signedint    b    = -1;         /*有符号数,-1*/
unsignedint  c    = 0xFFFFFFFD; /*无符号数据,4294967293*/
signedint    d    = -2;         /*有符号数,-2*/
unsignedint *pa   = &a;         /*指针变量pa指向a,即,将a的地址赋值给变量pa*/
unsignedint **ppa = &pa;        /*指针变量ppa指向pa,即,将pa的地址赋值给变量ppa*/
typedefstruct{
    unsignedint a;
    signedint b;
    unsignedint c;
    signedint   d;
}User_Typedef; /*自定义某个数据类型,将其命名为User_Typedef*/

User_Typedef data     = {0xFFFFFFFF,0xFFFFFFFF,0xFFFFFFFD,0xFFFFFFFD};
User_Typedef *pdata   = &data;  /*指针变量pdata指向data*/
User_Typedef **ppdata = &pdata; /*指针变量ppdata指向pdata*/

在C语言中,字节对齐的情况下,结构体所占用的内存是连续的,且每个成员也是连续存放的。在本例中,结构体变量data中的各个成员data.a、data.b、data.c、data.d的内存地址是连续的。因此,虽然两段代码表面上完全不同,但是程序编译和运行后,数据在内存中的分布完全相同。

值得指出的是,结构体指针中,存放的数据是结构体变量第一个成员的地址。在本例中,data.a的地址,即0x20000000被赋值给了结构体指针pdata。而pdata存放在编号为0x20000010的内存地址中,所以该地址中存放的数据是0x20000000。

HAL库

HAL库

从上面的程序中可以看出:

  • C语言是强类型语言,不仅要声明变量,还要关注变量类型。a和b的内存地址中存放的数据其实是一样的,但是因为类型不同,所以程序对数据的理解完全不同。
  • 指针也是变量,所以也需要存储在某个内存地址中。指针并不特殊(Type *)类型的指针变量中,只能存储Type类型变量的地址。此处的Type,适用于C语言的基础类型数据、结构体、联合体、函数等各种类型。
  • 在32位环境中,一个指针变量占用4个字节的存储空间,无论该指针是何种类型。

在第二段代码中,可以用如下方式访问结构体中的各个成员,第5~7行完全等价。

User_Typedef data;/*data中的成员还没有初始化*/
User_Typedef *pdata   = &data;  /*指针变量,pdata指向data*/
User_Typedef **ppdata = &pdata; /*指针变量,ppdata指向pdata*/

data.a       = 0xFFFFFFFF;
pdata- >a     = 0xFFFFFFFF;
(*ppdata)- >a = 0xFFFFFFFF

3.2 初识GPIOx

在GPIOC上点击右键,选择Go To Definition of 'GPIOC'

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE               ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOH               ((GPIO_TypeDef *) GPIOH_BASE)

目前,先不管GPIO_TypeDef这种自定义的结构体中含有哪些成员,但是我们可以清楚地知道,GPIOx是一个自定义的GPIO_TypeDef *类型的指针,通过GPIOx->member的方式,可以直接访问到各个成员。

进一步在GPIOC_BASE上点击右键,依次得到:

#define GPIOA_BASE            (AHB2PERIPH_BASE + 0x0000UL)
#define GPIOB_BASE            (AHB2PERIPH_BASE + 0x0400UL)
#define GPIOC_BASE            (AHB2PERIPH_BASE + 0x0800UL)
#define GPIOD_BASE            (AHB2PERIPH_BASE + 0x0C00UL)
#define GPIOE_BASE            (AHB2PERIPH_BASE + 0x1000UL)
#define GPIOH_BASE            (AHB2PERIPH_BASE + 0x1C00UL)

#define AHB2PERIPH_BASE       (PERIPH_BASE + 0x08000000UL)

#define PERIPH_BASE           (0x40000000UL)

通过换算,GPIOA、GPIOB、GPIOC等实际上等价于:

#define GPIOA               ((GPIO_TypeDef *) (0x40800000UL))
#define GPIOB               ((GPIO_TypeDef *) (0x40800400UL))
#define GPIOC               ((GPIO_TypeDef *) (0x40800800UL))

结合C语言存储结构体变量的特点,我们可以得出推论:以GPIOC为例,从地址0x40800800UL开始,是一段连续地址空间,这段连续的空间可以完整存储GPIO_TypeDef类型的数据。但是,这一段连续地址空间到底占用了多少字节?我们还需要深入了解自定义结构体GPIO_TypeDef。

3.3 深入了解GPIO_TypeDef

认识GPIO_TypeDef,等于认识了ST HAL中所有外设的xxx_TypeDef。在GPIO_TypeDef上点击右键,选择Go To Definition of 'GPIO_TypeDef',它是一个结构体,包括MODER、OTYPER等成员,每个成员都是uint32_t类型(无符号32位整型),__IO表示volatile。每个成员的作用见下图的注释部分,翻译成中文分别是模式寄存器、输出模式寄存器、输出速度寄存器、上拉-下拉寄存器、输入数据寄存器、输出数据寄存器、置位-复位寄存器、锁定配置寄存器、复用功能寄存器、Bit复位寄存器

HAL库

在RM0394.pdf的274 ~ 275页,有GPIOx的寄存器布局图,其中x表示A ~ E,H

HAL库

HAL库

结合GPIOx的地址和寄存器布局图,可以得到推论:

  • 如果要设置GPIOx的各个引脚模式,需要向GPIOx的MODER寄存器中写入相应数值;
  • 如果要设置GPIOx的各个引脚输出模式,需要向GPIOx的OTYPER寄存器中写入相应数值;
  • GPIOA MODER的地址是0x40800000UL,GPIOA OTYPER的地址是0x40800004UL;
  • GPIOB MODER的地址是0x40800400UL,GPIOB OTYPER的地址是0x40800404UL
  • GPIOC MODER的地址是0x40800800UL,GPIOC OTYPER的地址是0x40800804UL

显然,对于GPIOA ~ GPIOH,所有寄存器的布局是相同的,寄存器地址依次偏移4个字节,图示如下:

HAL库

  • 图中,每个地址都是32位的,每个地址中能容纳的数据也是32位。
  • 地址0x40800000UL中写入一个32位的数据,等价于向GPIOA的MODER寄存器中写入一个32位的数据,显然,地址编号不如寄存器名称方便。
  • 在C语言中,字节对齐的情况下,结构体所占用的内存是连续的,且每个成员也是连续存放的。利用C语言的特性,HAL库中声明了一个自定义的结构体GPIO_TypeDef,该结构体的各个成员严格按照STM32L4xx系列的GPIOx各寄存器顺序进行排序,且每个成员都能容纳(存储)一个32位的数据。
  • 在STM32中,还有诸如USART、IIC、SPI、CAN、ADC等各种不同的外设,自然也就有对应的xxx_Typedef的自定义结构体类型。下图给出了USART_TypeDef的结构体定义,我们无需查看手册就知道在STM32处理器中,控制USART外设工作需要向CR1、CR2等系列寄存器写入符合芯片RM手册中规定的数据即可。USART_TypeDef的声明如下图所示:

HAL库

3.4 进一步了解GPIOx

#define GPIOC   ((GPIO_TypeDef *) (0x40800800UL))

define是一个宏,表示GPIOC等价于((GPIO_TypeDef *) (0x40800800UL))。因此,GPIOC本质上是GPIO_TypeDef *类型的指针。

Q&A

Q1: 如何对GPIOA的MODER寄存器执行写操作?如何对GPIOC的OTYPER寄存器执行写操作?

A1: ->是C语言中的指向结构体成员运算符,用于使用指向某种结构的指针来访问结构内的成员。使用GPIOA->MODE = 0x1234; GPIOC->OTYPER= 0x789A;即可完成GPIOA和GPIOC对应寄存器的数据写入。

Q2: (0x40800800UL)是一个整形数据,也能转化为指针吗?

A2: 通过前文,已经知道GPIOx的所有寄存器在STM32的内存中,是连续存放的。而C语言的结构体在字节对齐的情况下,内部成员也是连续存放的,且结构体指针指向结构体第一个成员的地址。利用这个特点,将数据0x40800800UL强制转换为(GPIO_TypeDef *)类型的指针,那么,从0x40800800UL到0x40800828UL地址段,每4个字节就对应GPIOx中的一个寄存器,完美构建了软件与硬件的沟通桥梁。

Q3: 如果不用宏表示GPIOC,那么GPIOC->OTYPER = 0x1234应该用什么形式实现?

A3: ( (GPIO_TypeDef *) (0x40800800UL) )->OTYPER = 0x1234;,意味着,程序将访问0x40800800UL开始的地址空间内的OTYPER成员,即将32位的十六进制数据0x1234写入地址0x40800804UL。显然,这种写法很难看,不如GPIOC->OTYPER 直观。

3.5 HAL API的设计

在C语言中,指针是最核心的内容,也是难点。通过前文分析,我们已经知道指针只是变量而已,并不复杂,HAL库中所用的指针很简单。

现在对比两种不同方式设计的HAL_GPIO_TogglePin函数,其中,方式1是ST HAL官方库的正确设计,方式2是不合理方案。

/* 方式1:HAL库官方方案*/
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
/* 方式2:不合理方案*/    
GPIO_TypeDef HAL_GPIO_TogglePin(GPIO_TypeDef GPIOx, uint16_t GPIO_Pin)

/* 方式1:HAL库官方方案进行函数调用*/
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
/* 方式2:不合理方案进行函数调用*/
*(GPIOC) = HAL_GPIO_TogglePin(*(GPIOC), GPIO_PIN_13);

C 语言使用传值调用方法来传递参数,即将形参的值复制给实参。在发生函数调用时,形参的存放地址空间来源于堆栈。

方式1:HAL库官方方案进行函数调用:

  • 第一个实参的值,GPIOC,即0x40800800UL 被复制给了形参GPIOx,占用4个字节;
  • 第二个实参的值,GPIO_PIN_13,被复制给了形参GPIO_Pin,占用4个字节。
  • 堆栈在形参上的开销至少是8个字节
  • 传递指针GPIOC的值给了临时变量GPIOx,临时变量GPIOx存放的具体地址不明,但是,可直接通过GPIOx->MODE = xx的方式,即( (GPIO_TypeDef *) (0x40800800UL) )->MODE = xx,以地址访问的形式直接修改了GPIOC MODE寄存器所对应的内存,从而成功修改寄存器的值。

方式2:不合理方案进行函数调用:

  • 第一个实参的值,*GPIOC,即从0x40800800UL到0x40800828UL地址空间内的所有数据,被复制给了形参GPIOx,合计占用44字节;
  • 第二个实参的值,GPIO_PIN_13,被复制给了形参GPIO_Pin,占用4个字节。
  • 堆栈在形参上的开销至少是48个字节
  • 由于GPIOx是个GPIO_TypeDef类型的临时变量,存放的具体地址不明,即使在程序中使用GPIOx.MODE修改了GPIOx成员MODE的数值,也不会真正影响GPIOC->MODEGPIOC->MODE表示地址0x40800800UL,而GPIOx.MODE肯定不存放在该地址,修改GPIOx.MODE中存放的数值,自然不可能影响到内存地址0x40800800UL,必须通过函数返回值进行赋值,而这又会带来一系列堆栈开销。

HAL库

综上,对比两种设计方法,毫无疑问是HAL库提供的方式1效果更加,更加高效,占用内存更少。HAL库中,都是通过传递指针来进行API函数设计的。

4. 小结

  1. HAL的精髓在于Abstract抽象。
  2. STM32的RM、UM手册是基础,AN手册是进阶。
  3. 指针到底是什么?指针是变量。
  4. 指向int指针和指向结构体的指针的相同点在于,在32位环境中占用4个字节;不同点是存储不同类型变量的地址。
  5. HAL的GPIO_TypeDef之类的xxx_TypeDef是严格与RM手册中的寄存器分布一一对应的。
  6. HAL库通过封装xxx_TypeDef类型的指针,利用C语言的结构体实现了典型的面向对象编程的思路。
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分