1. 初识HAL
ST 为开发者提供了三种的开发库:
其中,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:不合理方案进行函数调用*/
*(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->MODE
。GPIOC->MODE表示地址0x40800800UL,而GPIOx.MODE肯定不存放在该地址,修改GPIOx.MODE中存放的数值,自然不可能影响到内存地址0x40800800UL,
必须通过函数返回值进行赋值,而这又会带来一系列堆栈开销。
综上,对比两种设计方法,毫无疑问是HAL库提供的方式1效果更加,更加高效,占用内存更少。HAL库中,都是通过传递指针来进行API函数设计的。
4. 小结
- HAL的精髓在于Abstract抽象。
- STM32的RM、UM手册是基础,AN手册是进阶。
- 指针到底是什么?指针是变量。
- 指向int指针和指向结构体的指针的相同点在于,在32位环境中占用4个字节;不同点是存储不同类型变量的地址。
- HAL的GPIO_TypeDef之类的xxx_TypeDef是严格与RM手册中的寄存器分布一一对应的。
- HAL库通过封装xxx_TypeDef类型的指针,利用C语言的结构体实现了典型的面向对象编程的思路。
-
C语言
+关注
关注
180文章
7596浏览量
135973 -
GPIO
+关注
关注
16文章
1189浏览量
51859 -
STM32L4
+关注
关注
1文章
42浏览量
9377 -
HAL库
+关注
关注
1文章
114浏览量
6151
发布评论请先 登录
相关推荐
评论