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

发布时间:2023-10-24  

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。

图片

2.代码补全效果。

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

图片

3.HAL支持哪些函数?

如下图所示,点击MDK左侧工程栏下方的Functions,点开对应的hal_xx.c文件,即可显示出所有的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下载相关手册。

图片

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)


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

3.1 回顾指针

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

图片

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

图片

  • 上图中,每一个格子表示1个字节,一个字节的无符号数的表示范围是

  • 为了存储更大的数,我们也可以将4个字节看作一个单元,在32位计算机中,4个字节即一个字word

图片

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

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

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

图片

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

图片

  • 不得不说,如果直接通过地址编号去读取/修改这些数据,是一件让人为难的事情 ;

  • 高级语言提供了解决方案,支持通过变量名进行访问;

  • 通过变量名来访问变量,对于开发者非常友好。但是要时刻记住计算机硬件依然是通过地址来访问内存单元(Hardware still accesses memory locations using addresses)。

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

图片

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。

图片

图片

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

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复位寄存器。

图片

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

图片

图片

结合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个字节,图示如下:

图片

  • 图中,每个地址都是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的声明如下图所示:

图片

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:不合理方案进行函数调用*/

文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。

我们与500+贴片厂合作,完美满足客户的定制需求。为品牌提供定制化的推广方案、专属产品特色页,多渠道推广,SEM/SEO精准营销以及与公众号的联合推广...详细>>

利用葫芦芯平台的卓越技术服务和新产品推广能力,原厂代理能轻松打入消费物联网(IOT)、信息与通信(ICT)、汽车及新能源汽车、工业自动化及工业物联网、装备及功率电子...详细>>

充分利用其强大的电子元器件采购流量,创新性地为这些物料提供了一个全新的窗口。我们的高效数字营销技术,不仅可以助你轻松识别与连接到需求方,更能够极大地提高“闲置物料”的处理能力,通过葫芦芯平台...详细>>

我们的目标很明确:构建一个全方位的半导体产业生态系统。成为一家全球领先的半导体互联网生态公司。目前,我们已成功打造了智能汽车、智能家居、大健康医疗、机器人和材料等五大生态领域。更为重要的是...详细>>

我们深知加工与定制类服务商的价值和重要性,因此,我们倾力为您提供最顶尖的营销资源。在我们的平台上,您可以直接接触到100万的研发工程师和采购工程师,以及10万的活跃客户群体...详细>>

凭借我们强大的专业流量和尖端的互联网数字营销技术,我们承诺为原厂提供免费的产品资料推广服务。无论是最新的资讯、技术动态还是创新产品,都可以通过我们的平台迅速传达给目标客户...详细>>

我们不止于将线索转化为潜在客户。葫芦芯平台致力于形成业务闭环,从引流、宣传到最终销售,全程跟进,确保每一个potential lead都得到妥善处理,从而大幅提高转化率。不仅如此...详细>>