阅读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,分别对应着每一个引脚。