> 文章列表 > 阅读HAL源码之重点总结

阅读HAL源码之重点总结

阅读HAL源码之重点总结

HAL库的封装特点 

HAL封装中有如下特点(自己总结的):

特定外设要设置的参数组成一个结构体;

特定外设所有寄存器组成一个结构体;

地址基本都是通过宏来定义的,定义了各外设的起始地址,也就是对应寄存器结构体的地址,因为结构体里定义的是32位的地址,所以直接通过寄存器结构体指针->即可访问各寄存器。

实现过程基本都是:先根据要设置的数据赋值给参数结构体,之后根据这些参数来设置寄存器结构体。

最终的目的,都是将特定数据赋值给对应的寄存器。只不过用结构体实现了封装。

示例:

首先定义时钟源结构体的变量以及时钟配置结构体的变量;

然后给各参数赋值。

时钟源类型

HSE状态设置为开启

这三行分别是:HSEON在时钟控制寄存器中所处的位置号、对应的十六进制数、再给到一个宏定义RCC_CR_HSEON

HSE分频值

……

更多对着数据手册的寄存器查看吧。

HAL里函数都是直接调用的方式,并没有将函数都用结构体封装起来,其实多文件本身也可以看做一种封装。 

嵌入式的分层思想 

软件封装,抛去操作系统不说,裸机开发可分为以下三层:

1、寄存器层面的封装;

2、特定硬件层面的封装;

3、业务层面的使用;

以最简单的点亮LED为例说明。

寄存器层面就是指实现了特定GPIO的设置,可以对特定引脚进行一些操作,这一步主要是把芯片操作给封装起来。

特定硬件层面就是根据具体的外设以及其连接方式,实现其功能,比如点亮和熄灭LED,实现特定外设所有可能的操作。也就是说,把电路板调通。

业务层面就是根据业务,需要灯点亮还是熄灭,到这一步,更多的就是要处理业务逻辑了。

HAL初始化

HAL_Init();

/* Reset of all peripherals, Initializes the Flash interface and the Systick. */

配置Flash预取;

配置中断组;

配置滴答时钟;

初始化MSP;

SystemClock_Config();

配置时钟主要包括系统驱动时钟,CPU、AHB和APB总线时钟;

以上,系统初始化完成;

接下来就要初始化自己配置的外设了。

这里以GPIO为例

MX_GPIO_Init()

开启时钟;

将相应的引脚置高或低电平;

引脚相关参数;

接下来就可以编写用户代码了。

HAL编程规范

HAL库使用的编码规范接近于Linux的风格。

学习使用。

基本都是用下划线_分隔;

目录名:大驼峰加下划线;

文件名:全小写加下划线;

注释使用的都是/*   */

且基本每一个函数代码处都有必要的注释

函数名:大驼峰(专有名字全大写)加下划线

变量名:大驼峰,也有大驼峰加下划线的

宏定义:全大写加下划线

十六进制表示的数后基本上都要加UL

十进制表示的数后面基本都要加U

但是如果已经用以下数据类型定义了,则没必要加。

数据类型定义基本都用的是stdint.h里面的

uint8_t

uint16_t

uint32_t

自定义类型名:大驼峰加下划线

程序的大括号在下一行开启

STM32的FLASH编程

STM32的FLASH不但可以存储程序,而且还是可以当EEPROM用。32的FLASH一般都比较大,FLASH的前面部分可以放代码,而最后几页可以存储数据,用于掉电记忆还是挺不错的。

STM32的FLASH是按页类操作的,也就是说每次擦除都必须整页擦除,而不能只擦除一页的一部分,读数据的话不存在这种限制。大容量的芯片每页是2k,而小容量的芯片每页是1k。

STM32的FLASH地址是从0x08000000开始的。比如要操作大容量512k芯片的最后一页FLASH,那么地址是0x08000000+2048*255.其中0x08000000是FLASH的起始地址,512k的芯片共有256页,每页2k,所以地址就是起始地址+前面255页的大小。比如操作64k的小容量的芯片的最后一页,那么地址就是0x08000000+1024*63.因为小容量芯片每页只有1k,所以地址=起始地址+前面63页大小。

在利用FLASH存储数据的过程中发现一个小问题,存储浮点数精度会丢失,所以只好把浮点数放大再存储,读出来的时候再还原回去。在利用库函数操作的时候,发现库函数的底层会把数据强制转换为无符号整型才存储进FLASH里面,但是如果把负数存储进去,读出来的时候还是负数,就好像强制转换语句失效了一样,其实库函数是没问题的,这个和正数负数在内存中的存储结构有关,在这里不过多的解释。
 

在STM32的技术手册里是找不到FLASH的相关寄存器和操作的,在《STM32的FLASH编程》里面才有。操作STM32的FLASH如果用寄存器的话是非常麻烦的,而且还非常容易出错,但是如果用库操作的话会非常简单,比如关键字、延时什么的库都帮我们做好了,我们只要调用接口就行了。 

__IO

在很多类型定义前面,看到了这个标志__IO

一开始我还以为是跟流相关的标志。

直接跳转到原定义,才发现就是一个宏定义

在core_cm3.h中,属于cm3内核里的定义。

而且这个宏定义竟然就是个volatile

__I的含义就是将变量限制在“只读状态”,“只读状态”表示的是“变量的值只能通过读取寄存器的值改变自身,而且可以连续的改变,但是就是不可以人为的在程序中对变量进行改变”。

其实这就是“嵌入式输入的含义”,我们只允许通过读取外设寄存器的值来改变变量,不允许人为的在程序中改变变量的值。

__O的含义是将变量限制在“输出状态”,输出状态有别于输入状态的本质就在于,输出状态去掉了对于变量的const常量限定,使得我们可以通过任意方式(寄存器/人为修改)来改变变量的属性(值和数据类型)进而对变量进行处理。

__IO指的是“该变量可以进行输入输出操作——读/写操作”。它的作用和__O相同。

_weak

函数名称前面加上__weak 修饰符,我们一般称这个函数为“弱函数”。

加上了__weak 修饰符的函数,用户可以在用户文件中重新定义一个同名函数,最终编译器编译的时候,会选择用户定义的函数,如果用户没有重新定义这个函数,那么编译器就会执行__weak 声明的函数,并且编译器不会报错。所以我们可以在别的地方定义一个相同名字的函数,而不必也尽量不要修改之前的函数,。
 

可见,弱函数就是个“备胎”,先用着,有了正式的就自然而然地被替换掉了。

断言机制

什么是Assert断言?

编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式

断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真。

可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言,而在部署时禁用断言

同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。 ---来自百度百科

这里的概念,可能不好理解,简单举一个例子来说明吧。

有这么一个数组和函数:

int Array[5] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5};int Fun(char i)
{return Array[i];
}

如果我们函数中不加Assert断言语句,你觉得直接调用会这个函数会有风险吗? 假如这么调用:

int a;a = Fun(8);

很明显,就这么调用,数组越界会出错,且我们不容易发现错误在哪里。

但是,假如添加有Assert断言语句,错误就能一下找出来。

Assert断言实际应用

其实,Assert断言在很多标准的代码中,基本都有。我们还是拿STM32的代码来说明吧。

不管是STM32标准外设库、还是HAL、LL库源代码里面都有Assert断言机制。

不知道大家有没有注意过assert_param函数

比如在HAL库中:

相信大家都看到过STM32库中的参数断言语句,他的作用就是:

用于检查函数传入参数是否正确

STM32的assert_param参数断言函数默认是没有使能的,如下:

也就是assert_param不起作用。

要想开启,HAL中stm32f1xx_hal_conf.h里:

也可以在CubeMX中选择开启:

如果没有开启断言,那么assert_param就相当于空

#define assert_param(expr) ((void)0U),不起作用。

如果开启断言,则:

#define assert_param(expr) ((expr) ? (void)0U : assert_failed((uint8_t *)__FILE__, __LINE__)) 

如果检测内容为真,则不管,如果检测内容有错误,则输出一些信息。

void assert_failed(uint8_t* file, uint32_t line);

在main.c中,用户可自定义内容:

有错误时,会输出错误在哪个文件的哪一行,方便定位。

我们来看看实际应用吧。

举例:

assert_param(IS_RCC_OSCILLATORTYPE(RCC_OscInitStruct->OscillatorType));

注意,不是把函数的参数传进去,而是要传入一个逻辑表达式

这里的逻辑判断函数就是IS_RCC_OSCILLATORTYPE()

问题来了,这个逻辑判断函数是哪里来的呢?

这是个带参宏,在相应外设的头文件中stm32f1xx_hal_rcc.h

无非就是对参数进行判断。

自己定义一些内容都行。

错误处理

断言通常都是检查函数的输入参数的合法性。

另外,如果程序有一些可预见的出错情况,我们可以直接进行错误处理。

可以自定义错误处理函数。

在HAL中,就自己定义了一个,我们可以自己添加内容。

其他

 各端口的引脚定义:

从最低位为1,一直到最高位为1,分别对应着每一个引脚。