1. 初识HAL
ST 为开发者提供了三种的开发库:
标准外设库(Standard Peripheral Library, SPL库)
硬件抽象层库(Hardware Abstraction Layer,HAL库)
底层库(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:不合理方案进行函数调用*/