3.2 实验:构建库函数雏形(续)
3.2.5 定义初始化结构体 GPIO_InitTypeDef
定义位操作函数后,控制 GPIO 输出电平的代码得到了简化,但在控制 GPIO 输出电平前还需要初始化 GPIO 引脚的各种模式,这部分代码涉及的寄存器有很多。
1、先根据 GPIO 初始化时涉及到的初始化参数以结构体的形式封装起来 2、声明一个名为 GPIO_InitTypeDef 的结构体类型
typedef struct
{
uint16_t GPIO_Pin;
uint16_t GPIO_Speed;
uint16_t GPIO_Mode;
} GPIO_InitTypeDef;
这个结构体中包含了初始化 GPIO 所需要的信息,包括引脚号、工作模式、输出速率。
设计这个结构体的思路是: 1、初始化 GPIO前,先定义一个这样的结构体变量 2、根据需要配置 GPIO 的模式,对这个结构体的各个成员进行赋值 3、然后把这个变量作为“GPIO 初始化函数”的输入参数 4、该函数能根据这个变量值中的内容去配置寄存器,从而实现 GPIO 的初始化。
3.2.6 定义引脚模式的枚举类型
使用 C 语言中的枚举定义功能,根据手册把每个成员的所有取值都定义好。
GPIO_Speed 和 GPIO_Mode 这两个成员对应的寄存器是 CRL 和 CRH 这两个端口配置寄存器,具体见下图 代码如下:
typedef enum
{
GPIO_Speed_10MHz = 1,
GPIO_Speed_2MHz,
GPIO_Speed_50MHz
} GPIOSpeed_TypeDef;
typedef enum
{
GPIO_Mode_AIN = 0x0,
GPIO_Mode_IN_FLOATING = 0x04,
GPIO_Mode_IPD = 0x28,
GPIO_Mode_IPU = 0x48,
GPIO_Mode_Out_OD = 0x14,
GPIO_Mode_Out_PP = 0x10,
GPIO_Mode_AF_OD = 0x1C,
GPIO_Mode_AF_PP = 0x18
} GPIOMode_TypeDef;
GPIO引脚工作模式真值表分析图 有了这些枚举定义, GPIO_InitTypeDef 结构体就可以使用枚举类型来限定输入参数
typedef struct
{
uint16_t GPIO_Pin;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode_TypeDef GPIO_Mode;
} GPIO_InitTypeDef;
利用这些枚举定义,给 GPIO_InitTypeDef 结构体类型赋值配置就变得非常直观,代码如下:
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
3.2.7 定义GPIO初始化函数
接着前面的思路,对初始化结构体赋值后,把它输入到 GPIO 初始化函数,由它来实现寄存器配置。
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
uint32_t currentmode =0x00,currentpin = 0x00,pinpos = 0x00,pos = 0x00;
uint32_t tmpreg = 0x00, pinmask = 0x00;
currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) &
((uint32_t)0x0F);
if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) &
((uint32_t)0x10)) != 0x00)
{
currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
}
if (((uint32_t)GPIO_InitStruct->GPIO_Pin &
((uint32_t)0x00FF)) != 0x00)
{
tmpreg = GPIOx->CRL;
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
pos = ((uint32_t)0x01) << pinpos;
currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
if (currentpin == pos)
{
pos = pinpos << 2;
pinmask = ((uint32_t)0x0F) << pos;
tmpreg &= ~pinmask;
tmpreg |= (currentmode << pos);
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
GPIOx->BRR = (((uint32_t)0x01) << pinpos);
}
else
{
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
}
}
}
}
GPIOx->CRL = tmpreg;
}
if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
{
tmpreg = GPIOx->CRH;
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
pos = (((uint32_t)0x01) << (pinpos + 0x08));
currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);
if (currentpin == pos)
{
pos = pinpos << 2;
pinmask = ((uint32_t)0x0F) << pos;
tmpreg &= ~pinmask;
tmpreg |= (currentmode << pos);
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
}
}
GPIOx->CRH = tmpreg;
}
}
这个函数有 GPIOx 和 GPIO_InitStruct 两个输入参数,分别是 GPIO 外设指针和 GPIO初始化结构体指针。分别用来指定要初始化的 GPIO 端口及引脚的工作模式。
3.2.8 使用固件库点亮LED
int main(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2ENR |= (1<<3);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_ResetBits(GPIOB,GPIO_Pin_0);
while (1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_0);
Delay(0xFFFF);
GPIO_SetBits(GPIOB,GPIO_Pin_0);
Delay(0xFFFF);
}
}
使用函数来控制 LED 灯与之前直接控制寄存器已经有了很大的区别: 1、main 函数中先定义了一个 GPIO 初始化结构体变量GPIO_InitStructure 2、然后对该变量的各个成员按点亮 LED 灯所需要的 GPIO 配置模式进行赋值 3、赋值后,调用 GPIO_Init 函数,让它根据结构体成员值对 GPIO 寄存器写入控制参数,完成 GPIO 引脚初始化。 4、控制电平时,直接使用 GPIO_SetBits 和 GPIO_Resetbits 函数控制输出。 5、如若对其它引脚进行不同模式的初始化,只要修改 GPIO 初始化结构体 GPIO_InitStructure 的成员值,把新的参数值输入到 GPIO_Init 函数再调用即可。
代码中 Delay 函数,主要功能是延时,属于软件延迟,不准确。 要准确的延迟,需要使用STM32的定时器来实现。
3.3 总结
本章的学习,是照搬了ST标准库,目的是满足求知欲,学习库函数的编程方式和思想。 与直接配置寄存器相比,执行效率上会有额外的消耗,例如: 初始化变量赋值的过程、库函数被调用的时候耗费的时间等等。
它的宏、枚举等解释操作都是编译过程完成的,这部分不消耗时间。
为什么学习函数库? 学习函数库的目的是: 1、我们可以快速上手STM32控制器; 2、配置外设状态时,不需要纠结向寄存器写入什么数值; 3、交流方便,便于差错
在以后开发的工程中,一般不会去分析 ST 的库函数的实现。因为外设的库函数是很类似的,库外设都包含初始化结构体,以及特定的宏或枚举标识符,这些封装被库函数这些转化成相应的值,写入到寄存器之中,函数内部的具体实现是十分枯燥和机械的工作。 如果您有兴趣,在您掌握了如何使用外设的库函数之后,可以查看一下它的源码实现
|