上一节看完手册以后,你或许已经明白怎么配置一个GPIO接口让它实现输入输出的各种功能了。但是如果我问起你:如果要让GPIOA端口的P0和P1配置成推挽输出10MHz模式,你该怎么做,你会做些什么?
绝大多数人做的事情应该还是翻开手册里GPIO_CRL的寄存器定义,然后照着手册编程。GPIO外设的功能比较简单,需要配置的功能不算太多,这么做没什么难度。但有些外设的配置寄存器有许许多多控制位,比如下面这位:
这是串口通信外设的控制寄存器1(对没错,还有个控制寄存器2),里面的14个有效位都与通信的各种规则和状态密切相关,通过查手册一位一位地改显然有点不太经济且不甚直观。
标准固件库函数就是为这个而生的。ST官方将常用的对外设的操作和外设的各种状态封装成可读性更高的函数和结构供用户使用。我们就着GPIIO的库函数来看看,这玩意到底有什么魔力。
请从固件库文件夹里找出stm32f10x_gpio.h,stm32f10x_gpio.c和stm32f10x_rcc.h、stm32f10x_rcc.c四个文件,添加到寄存器编程的模板里。
并且在你的main里包含两个.h。打开这gpio的两个文件,里面虽然看着纷繁,但核心思路我们一讲便通。
打开stm32f10x_gpio.h,用Ctrl+F找到“GPIO_Exported_Functions”,其下便是所有的gpio库函数了。
GPIO_Init函数就是初始化外设使用的函数了。有一个形参类型“GPIO_InitTypeDef”我们没有见过,你可以在上边右键单击后选择“Go to Defnition”查看它的内容:
typedef struct
{
uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */
GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIOSpeed_TypeDef */
GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIOMode_TypeDef */
}GPIO_InitTypeDef;
与之配套地,这些字段的有意义值被定义成了相关的枚举或宏,你可以在.h中找到这些东西:
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;
typedef enum
{
GPIO_Speed_10MHz = 1,
GPIO_Speed_2MHz,
GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;
#define GPIO_Pin_0 ((uint16_t)0x0001) /*!< Pin 0 selected */
#define GPIO_Pin_1 ((uint16_t)0x0002) /*!< Pin 1 selected */
#define GPIO_Pin_2 ((uint16_t)0x0004) /*!< Pin 2 selected */
#define GPIO_Pin_3 ((uint16_t)0x0008) /*!< Pin 3 selected */
#define GPIO_Pin_4 ((uint16_t)0x0010) /*!< Pin 4 selected */
#define GPIO_Pin_5 ((uint16_t)0x0020) /*!< Pin 5 selected */
#define GPIO_Pin_6 ((uint16_t)0x0040) /*!< Pin 6 selected */
#define GPIO_Pin_7 ((uint16_t)0x0080) /*!< Pin 7 selected */
#define GPIO_Pin_8 ((uint16_t)0x0100) /*!< Pin 8 selected */
#define GPIO_Pin_9 ((uint16_t)0x0200) /*!< Pin 9 selected */
#define GPIO_Pin_10 ((uint16_t)0x0400) /*!< Pin 10 selected */
#define GPIO_Pin_11 ((uint16_t)0x0800) /*!< Pin 11 selected */
#define GPIO_Pin_12 ((uint16_t)0x1000) /*!< Pin 12 selected */
#define GPIO_Pin_13 ((uint16_t)0x2000) /*!< Pin 13 selected */
#define GPIO_Pin_14 ((uint16_t)0x4000) /*!< Pin 14 selected */
#define GPIO_Pin_15 ((uint16_t)0x8000) /*!< Pin 15 selected */
#define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< All pins selected */
这个结构体用来装载开发者对外设的配置参数并作为参数输入给Init函数。因此你在执行Init函数前需要像这样定义一个结构体并设置它的值:
GPIO_InitTypeDef GPIO_InitStruct;//定义该结构体
//接下来在结构体字段中1保存初始状态
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出模式
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;//设定要初始化的引脚,思考一下为什么可以使用或运算
在它上边右键单击后选择“Go to Defnition”可以查看这个函数的内容。我将它复制过来,并添加了中文注释进行讲解。请仔细阅读。
`void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)//两个形参,第一个是GPIO端口的寄存器结构体,如GPIOABCDEFG等;第二个结构体里面装载了对这个GPIO接口的配置参数。
{
uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00;//临时变量,顾名思义即可,决定当前设定的模式、引脚、偏移量等等
uint32_t tmpreg = 0x00, pinmask = 0x00;
/* Check the parameters *///检测函数的有效性,这都对于传地址的函数非常重要
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));
assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));
/*---------------------------- GPIO Mode Configuration -----------------------*/
currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);//查阅手册里的CRL和CRH可知,一个IO口的模式设置占4位,所以将Mode与0x0F相与
if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00)//查阅.h里GPIO_Mode的宏定义可知,这一句的进入条件是GPIO为输出或者复用输出模式
{
/* Check the parameters */
assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));
/* Output mode */
currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
}//这个代码块设置了CurrentMode中暂存的GPIO速度,至此,GPIO模式已经读取完毕,可以初始化特待定引脚
/*---------------------------- GPIO CRL Configuration ------------------------*/
/* Configure the eight low port pins */
if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)//要开启的引脚是P0-P8中的,需要配置CRL
{//代码块中的内容自行阅读,知道它们是在照着初始化结构体配置寄存器即可
tmpreg = GPIOx->CRL;
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
pos = ((uint32_t)0x01) << pinpos;
/* Get the port pins position */
currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
if (currentpin == pos)
{
pos = pinpos << 2;
/* Clear the corresponding low control register bits */
pinmask = ((uint32_t)0x0F) << pos;
tmpreg &= ~pinmask;
/* Write the mode configuration in the corresponding bits */
tmpreg |= (currentmode << pos);
/* Reset the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
GPIOx->BRR = (((uint32_t)0x01) << pinpos);
}
else
{
/* Set the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
}
}
}
}
GPIOx->CRL = tmpreg;
}
/*---------------------------- GPIO CRH Configuration ------------------------*/
/* Configure the eight high port pins */
if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
{//需要配置P8-P16,配置CRH
tmpreg = GPIOx->CRH;
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
pos = (((uint32_t)0x01) << (pinpos + 0x08));
/* Get the port pins position */
currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);
if (currentpin == pos)
{
pos = pinpos << 2;
/* Clear the corresponding high control register bits */
pinmask = ((uint32_t)0x0F) << pos;
tmpreg &= ~pinmask;
/* Write the mode configuration in the corresponding bits */
tmpreg |= (currentmode << pos);
/* Reset the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
/* Set the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
}
}
GPIOx->CRH = tmpreg;
}
}
函数的运行原理请多加阅读和理解,里边的一些代码习惯也是你学习的榜样。
于是乎,我们只需要在刚才定义好初始化结构体之后运行一句Init函数就好:
GPIO_Init(GPIOA, &GPIO_InitStruct);//注意,这里的第二个参数是指针。
虽然初始化结构体没必要一定和初始化函数在一个作用域,但我还是希望你能把他们两个放在同一个代码块(最好是为它们单独开辟一个大括号),这样逻辑更清晰,也更加节省空间。
最后,别忘了我们最开始就讲过的配置外设常用步骤,我们还差“挂时钟”一步,这你只需要在运行初始化之前先来一下:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
这个函数在_rcc.h里声明,可以去看看,我们之后会讲。
运行Init函数之后,你想打开的端口就都启动了。你还可以用修改寄存器的办法去操作它们的电平,不过,你在刚才的函数中还有十几个没有用,它们都可以用来修改我们上节讲的那些寄存器,进而实现对IO
口的各种操作。你也可以“Go to
Definition”一下,去看看它们的内容并理解它们的功能。以下是一个例子,开启GPIOA的所有端口做输出,并开启GPIOB的所有端口做输入。
//常用的输出函数演示
GPIO_SetBits(GPIOA, GPIO_Pin_0);//PA0输出高
GPIO_ResetBits(GPIOA, GPIO_Pin_0);//PA0输出低
GPIO_WritePin(GPIOA, GPIO_Pin_1|GPIO_Pin_2,SET);//PA!和PA2输出高,SET的原始值是1,对应的0是RESET
GPIO_Write(GPIOA, 0xAAAA);//一次性设置GPIOA所有端口的电平,当然是小端的
uint8_t status_of_pa7 = GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_7);//读取PA7的输出状态,不出意外的话应该是1
//常用的输入函数演示
uint8_t status_of_pb0 = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0);//读取PB0的输入电平
uint16_t status_of_pb = GPIO_ReadInputData(GPIOB);//读取PB所有引脚的输入电平
这里边不仅有对一个引脚的位操作,还有对字节和半字的操作,它们在电机控制、并口通讯等领域应用广泛。