在刚刚开始接触STM32系列单片机的时候,我一直对HAL库和标准库中的函数的某些形参,类似GPIO_InitTypedef* GPIOA这样的结构体指针类型变量感到困惑。
就像上图一样,在定义某个函数的时候,形参(入口参数)时有这种以结构体类型名定义的结构体指针类型变量,在刚刚开始接触的时候我很困惑为什么要用这种特别的方式作形参,并且在下次调用这个函数的时候,在入口参数里还要写成&开头的结构体变量,例如用ADC_HandleTypeDef adc1,这是用这个结构体类型名定义了一个名为adc1的结构体,然后下次再调用HAL_ADC_MspDeInit这个函数的时候就要写成HAL_ADC_MspDeInit(&adc1);
我一开始会觉得很奇怪,明明函数在定义的过程中形参(入口参数)是结构体指针类型的变量,怎么在调用的时候变成了一个取地址的东西,难道不是应该也是写一个结构体指针类型的变量上去吗?例如uint8_t Function(int a),我们都知道要下次调用这个函数的时候,入口参数应该写的是int类型的变量。
不论是在STM32的HAL库还是标准库中,结构体和结构体指针都是一种很常见的类型,typedef这样的类型别名更是被大量使用在.h文件中。
关于为什么要这样大量使用结构体:我的理解是STM32中的每个外设都有其对应的特定数据寄存器,将使能一个外设所需要配置的所有寄存器全部放到一个结构体类型里,再在需要调用的时候直接用结构体类型定义一个这样的结构体,例如我需要配置GPIOA口的PIN7引脚,那么我只需要用已经声明好的GPIO_InitTypeDef结构体类型定义一个GPIO_InitStructure,这样只需要配置GPIO_InitStructure里的成员变量就行了。在标准库中这样的方式可以得到很好的体现:用 "." 去访问结构体中的一个个成员并将其配置。
首先我们来回顾一下C语言里的结构体的定义方式:
1、第一种方式,也是最经典的
struct Fanqieyu
{
int name;
int age;
int school;
};
struct Fanqieyu fengqunyi; //定义一个名为fengqunyi的Fanqieyu的结构体类型
这种定义结构体的方式我认为比较麻烦,但是比较好理解,一般情况下在HAL中不会使用到这样的方式去定义结构体。
2、第二种方式,是简化一点的
struct Fanqieyu
{
int name;
int age;
int school;
}fengqunyi;
这样的方式是在定义结构体类型名的时候同时定义了名为fengqunyi的结构体。
3、第三种方式,也是在标准库和HAL库中大量使用的一种方式,即用关键字typedef声明一种新的结构体类型
typedef struct
{
int name;
int age;
int school;
}Fanqieyu;
Fanqieyu fengqunyi;
以上这种方式是我认为最好的一种在嵌入式领域使用的一种结构体定义的方式,因为就像之前说的,单片机的每个外设都有其相应的特定寄存器,只需要把这些寄存器的地址放到相应的外设的结构体中,在使能外设的时候只需要用其声明好的结构体类型定义一个结构体变量,然后再配置这个结构体变量就行了。
如果有接触过RoboMaster比赛电控部分的同学大概知道,在大疆提供的pid源码中,正是用这种方式定义了pid的结构体,并将pid的各个参数放入其中,例如当前误差、上次误差、输出值以及P、I、D,同时会再定义一个函数,例如pid_Init(pid_InitStructure* pid)来进行pid的初始化。
例如:
typedef struct
{
float Kp;
float Ki;
float Kd;
static int16_t Current;
static int16_t Target;
static int16_t Error;
static int16_t Last_Error;
static int16_t Output;
}pid_InitStructure;
pid_Init(pid_InitStructure* pid)
{
pid->Kp=0;
pid->Ki=0;
pid->Kd=0;
pid->Target=0;
pid->Current=0;
pid->Error=pid->Target-pid->Current;
pid->Output=pid->Kp*pid->Error+pid->Ki*pid->(pid->Error-pid->Last_Error);
pid->Last_Error=pid->Error;
}
pid_InitStructure pid1;
pid_Init(&pid1); //这样就完成了一个pid的初始化
上面的代码是我随便写的,并不是严格的pid的例程。
正是在阅读了大疆的pid源码之后,我更加深刻地理解了结构体及其结构体指针在SMT32中的应用。
首先说一下结构体指针,既然前面说到了STM32中大量使用结构体,那么为什么也要大量使用结构体指针,刚刚写到的代码里已经看到了结构体中成员变量可以用其结构体类型名定义的结构体指针来通过->这样的方式来访问和初始化呢?为什么不用.这样的方式来直接访问结构体中的成员变量呢?
我们就以标准库和HAL中都很常见、也是标准库初学者必经的点灯环节——GPIO口的初始化流程来说明一下:
首先,在标准库中如果要初始化一个GPIO口,需要先用标注库中已经声明好的一个GPIO_InitTypeDef结构体来定义一个新的结构体:
int main(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
return 0;
}
这里先是用GPIO_InitTypeDef定义了一个名为GPIO_InitStructure的结构体,然后用.的方式去配置里面的结构体成员,这里都还是很常规的,但是我们来看一下最后一条语句:GPIO_Init(&GPIO_InitStructure);是什么意思?
我们来看一下GPIO_Init这个函数是怎么被定义的:
首先函数的入口参数是两个结构体指针,分别是GPIO_TypeDef和GPIO_InitTypeDef两个类型的结构体指针,先看一下GPIO_TypeDef的里面定义了什么
很明显,包含了所有配置GPIO口所需要的寄存器,再来看下第二个的:
这个就是GPIO口所需要配置的数据了,在之前的代码里面已经配置过了
函数的其他内容都是以结构体指针的赋值来决定对寄存器的操作,这是非常底层的东西,我们一般是不用接触的,代码也是比较复杂。
最重点的来了,为什么这个函数的形参(入口参数)要写成结构体指针的类型?其实答案已经很明显了,当然是传参方便,如果入口参数里不写结构体指针,怎么方便的去利用配置的GPIO口的各个数据去决定对底层的操作?注意哦,这个刚刚配置完的GPIO口的数据(GPIO_InitTypeDef里面的那些成员变量)在这个新的函数里面是不能直接的被访问的,总不能把整个结构体再移过来判断吧?所以结构体指针应运而生成了一种在STM32中非常常用的传参工具,要干什么直接把那个结构体类型名强转的指针放在函数的形参里面就行了,不然你总不可能写像GPIO_InitStructure.GPIO_Mode=GPIO_Mode_PP这样吧,因为这样这个函数是不认识这个结构体的,但是你如果把结构体指针引进来的话,用->访问的话他当然就认识了,这是我的理解。
所以在STM32中,不只是在GPIO配置的时候会用到结构体指针,几乎所有外设的配置过程中都会用到结构体指针来进行传参,包括pid算法的代码,标准例程也是结构体指针来进行传参。
一言以蔽之,结构体指针是STM32中的精髓,是帮助你理解STM32底层代码的重中之重,理解了结构体指针,对于看懂.h文件中的底层代码,会非常有帮助。