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

1. 初识halst 为开发者提供了三种的开发库:
标准外设库(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 driverstm32l431rct6芯片有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语言的结构体实现了典型的面向对象编程的思路。

电感式传感器的基本原理是什么,它的应用案例介绍
ABB机器人焊接编程程序详细介绍
Uber创始人计划入局区块链 推全新的数字货币Eco
骨传导耳机真的不伤耳吗?带你细数骨传导蓝牙耳机优缺点
武汉力源与英国艺莱创公司签署代理协议
掌握HAL API中面向对象设计的思想
中微BAT32G135单片机的超低功耗血氧仪/指夹式血氧仪方案
浅析中国动力电池与国际的差距
海能达发布了2020年一季度业绩报告
基于Nios II的多媒体广告系统原理设计
Intel Core i9处理器来了!但是散热方面存在问题
联想的造芯之路,造芯并不容易且贵在坚持
冬季风暴导致Linus Torvalds暂停Linux 6.8内核开发
软硬结合板设计,过孔到软板区域的间距设计多少合适
电气新技术的到来为一体化移动电源带来了机遇
研究表明当机器人与人交流正在做什么时人们往往变得更信任机器人
如何在MPLAB XC16编译器内建函数
iphone8什么时候上市?iphone8水滴形设计规格曝光,內存配置价格大公开
电池型号怎么看 电池型号LR6是几号电池
又一新车横空出世 启用生物识别指令控制