7.3 实验:构建库函数雏形
虽然库的优点很多,但很多人对库还是很忌惮的,因为一开始用库的时候会有很多代码,很多文件,而不知道如何入手。不知道你是否认同这么一句话:一切的恐惧都来源于认知的空缺。我们对库的忌惮那是因为我们不知道什么是库,不知道库是怎么实现的。
接下来,我们在寄存器点亮LED的代码上继续完善,把代码一层层封装,实现库的最初的雏形。相信经过这一步的学习后,对库的运用会游刃有余。这里我们只讲如何实现GPIO函数库,其他外设则直接参考ST标准库学习即可,不必自己写。
下面打开本章配套例程“构建库函数雏形”来阅读理解,该例程是在上一章的基础上修改得来的。
7.3.1 修改寄存器地址封装
上一章中我们在操作寄存器的时候,操作的是都寄存器的绝对地址,如果对于每个外部寄存器都这样操作,那将非常麻烦。考虑到外部寄存器的地址都基于外设基地址的偏移地址,并在外设基地址上逐个连续递增,每个寄存器占32个或者16个字节,这种方式与结构体里面的成员类似。所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序与寄存器的顺序一样。这样在操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。
在工程中的stm32f4xx.h文件中,我们使用结构体封装GPIO及RCC外设的寄存器,见代码清单7-1。结构体成员的顺序按照寄存器的偏移地址从低到高排列,成员类型与寄存器类型一样。如不理解C语言对寄存器的封装的语法原理,请参考4.5.2节。
代码清单7-1 封装寄存器列表
1 //volatile表示易变的变量,防止编译器优化 2 #define __IO volatile 3 typedef unsigned int uint32_t; 4 typedef unsigned short uint16_t; 5 6 /* GPIO寄存器列表*/ 7 typedef struct { 8 __IO uint32_t MODER; /*GPIO模式寄存器 地址偏移: 0x00 */ 9 __IO uint32_t OTYPER; /*GPIO输出类型寄存器 地址偏移: 0x04 */ 10 __IO uint32_t OSPEEDR; /*GPIO输出速度寄存器 地址偏移: 0x08 */ 11 __IO uint32_t PUPDR; /*GPIO上拉/下拉寄存器 地址偏移: 0x0C */ 12 __IO uint32_t IDR; /*GPIO输入数据寄存器 地址偏移: 0x10 */ 13 __IO uint32_t ODR; /*GPIO输出数据寄存器 地址偏移: 0x14 */ 14 __IO uint16_t BSRRL; /*GPIO置位/复位寄存器低16位部分 地址偏移: 0x18 */ 15 __IO uint16_t BSRRH; /*GPIO置位/复位寄存器高16位部分 地址偏移: 0x1A */ 16 __IO uint32_t LCKR; /*GPIO配置锁定寄存器 地址偏移: 0x1C */ 17 __IO uint32_t AFR[2]; /*GPIO复用功能配置寄存器 地址偏移: 0x20-0x24 */ 18 } GPIO_TypeDef; 19 20 /*RCC寄存器列表*/ 21 typedef struct { 22 __IO uint32_t CR; /*! < RCC时钟控制寄存器,地址偏移: 0x00 */ 23 __IO uint32_t PLLCFGR; /*! < RCC PLL配置寄存器,地址偏移: 0x04 */ 24 __IO uint32_t CFGR; /*! < RCC时钟配置寄存器,地址偏移: 0x08 */ 25 __IO uint32_t CIR; /*! < RCC时钟中断寄存器,地址偏移: 0x0C */ 26 __IO uint32_t AHB1RSTR; /*! < RCC AHB1 外部复位寄存器,地址偏移: 0x10 */ 27 __IO uint32_t AHB2RSTR; /*! < RCC AHB2 外部复位寄存器,地址偏移: 0x14 */ 28 __IO uint32_t AHB3RSTR; /*! < RCC AHB3 外部复位寄存器,地址偏移: 0x18 */ 29 __IO uint32_t RESERVED0; /*! < 保留,地址偏移:0x1C */ 30 __IO uint32_t APB1RSTR; /*! < RCC APB1 外部复位寄存器,地址偏移: 0x20 */ 31 __IO uint32_t APB2RSTR; /*! < RCC APB2 外部复位寄存器,地址偏移: 0x24 */ 32 __IO uint32_t RESERVED1[2]; /*! < 保留,地址偏移:0x28-0x2C*/ 33 __IO uint32_t AHB1ENR; /*! < RCC AHB1 外部时钟寄存器,地址偏移: 0x30 */ 34 __IO uint32_t AHB2ENR; /*! < RCC AHB2 外部时钟寄存器,地址偏移: 0x34 */ 35 __IO uint32_t AHB3ENR; /*! < RCC AHB3 外部时钟寄存器,地址偏移: 0x38 */ 36 /*RCC后面还有很多寄存器,此处省略*/ 37 } RCC_TypeDef;
这段代码在每个结构体成员前增加了一个“__IO”前缀,它的原型在这段代码的第一行,代表了C语言中的关键字“volatile”,在C语言中该关键字用于表示变量是易变的,要求编译器不要优化。这些结构体内的成员都代表寄存器,而寄存器很多时候是由外设或STM32芯片状态修改的,也就是说,即使CPU不执行代码修改这些变量,变量的值也有可能被外设修改、更新,所以每次使用这些变量的时候,我们都要求CPU从该变量的地址重新访问。若没有这个关键字修饰,在某些情况下,编译器认为没有代码修改该变量,就直接从CPU的某个缓存中获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会有出入。
7.3.2 定义访问外设的结构体指针
以结构体的形式定义好了外部寄存器后,使用结构体前还需要给结构体的首地址赋值,才能访问到需要的寄存器。为方便操作,我们给每个外设都定义好指向它首地址的结构体指针,见代码清单7-2。
代码清单7-2 指向外设首地址的结构体指针
1 /*定义GPIOA-H寄存器结构体指针*/ 2 #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) 3 #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE) 4 #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE) 5 #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE) 6 #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE) 7 #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE) 8 #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE) 9 #define GPIOH ((GPIO_TypeDef *) GPIOH_BASE) 10 11 /*定义RCC外设寄存器结构体指针*/ 12 #define RCC ((RCC_TypeDef *) RCC_BASE)
这些宏强制把外设的基地址转换成GPIO_TypeDef类型的地址,从而得到GPIOA、GPIOB等直接指向对应外设的指针,通过结构体的指针操作,即可访问对应外设的寄存器。
利用这些指针访问寄存器,我们把main文件里对应的代码进行修改,见代码清单7-3。
代码清单7-3 使用结构体指针方式控制LED
1 /** 2 * 主函数 3 */ 4 int main(void) 5 { 6 7 RCC->AHB1ENR |= (1<<7); 8 9 /* LED端口初始化 */ 10 11 /*GPIOH MODER10清空*/ 12 GPIOH->MODER &= ~( 0x03<< (2*10)); 13 /*PH10 MODER10 = 01b 输出模式*/ 14 GPIOH->MODER |= (1<<2*10); 15 16 /*GPIOH OTYPER10清空*/ 17 GPIOH->OTYPER &= ~(1<<1*10); 18 /*PH10 OTYPER10 = 0b 推挽模式*/ 19 GPIOH->OTYPER |= (0<<1*10); 20 21 /*GPIOH OSPEEDR10清空*/ 22 GPIOH->OSPEEDR &= ~(0x03<<2*10); 23 /*PH10 OSPEEDR10 = 0b 速率2MHz*/ 24 GPIOH->OSPEEDR |= (0<<2*10); 25 26 /*GPIOH PUPDR10清空*/ 27 GPIOH->PUPDR &= ~(0x03<<2*10); 28 /*PH10 PUPDR10 = 01b 上拉模式*/ 29 GPIOH->PUPDR |= (1<<2*10); 30 31 /*PH10 BSRR寄存器的 BR10置1,使引脚输出低电平*/ 32 GPIOH->BSRRH |= (1<<10); 33 34 /*PH10 BSRR寄存器的 BS10置1,使引脚输出高电平*/ 35 //GPIOH->BSRRL |= (1<<10); 36 37 while (1); 38 39 }
乍一看,除了最后一部分把BSRR寄存器分成BSRRH和BSRRL两段,与直接用绝对地址访问,其他部分只是名字改了而已,这与上一章没什么区别。这是因为我们现在只实现了库函数的基础,还没有定义库函数。
打好了基础,下面我们就来建“高楼”。接下来使用函数来封装GPIO的基本操作,以便以后应用的时候不需要再查询寄存器,而是直接通过调用这里定义的函数来实现。我们把针对GPIO外设操作的函数及其宏定义分别存放在stm32f4xx_gpio.c和stm32f4xx_gpio.h文件中。
在stm32f4xx_gpio.c文件中定义两个位操作函数,分别用于控制引脚输出高电平(置位)和低电平(复位),见代码清单7-4。
代码清单7-4 GPIO置位函数与复位函数的定义
1 /** 2 *函数功能:设置引脚为高电平 3 *参数说明:GPIOx:该参数为GPIO_TypeDef类型的指针,指向GPIO端口的地址 4 * GPIO_Pin:选择要设置的GPIO端口引脚,可输入宏GPIO_Pin_0~GPIO_Pin_15, 5 * 表示GPIOx端口的0~15号引脚。 6 */ 7 void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) 8 { 9 /*设置GPIOx端口BSRRL寄存器的第GPIO_Pin位,使其输出高电平*/ 10 /*因为BSRR寄存器写0不影响, 11 宏GPIO_Pin只是对应位为1,其他位均为0,所以可以直接赋值*/ 12 13 GPIOx->BSRRL = GPIO_Pin; 14 } 15 16 /** 17 *函数功能:设置引脚为低电平 18 *参数说明:GPIOx:该参数为GPIO_TypeDef类型的指针,指向GPIO端口的地址 19 * GPIO_Pin:选择要设置的GPIO端口引脚,可输入宏GPIO_Pin_0~GPIO_Pin_15, 20 * 表示GPIOx端口的0~15号引脚。 21 */ 22 void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) 23 { 24 /*设置GPIOx端口BSRRH寄存器的第GPIO_Pin位,使其输出低电平*/ 25 /*因为BSRR寄存器写0不影响, 26 宏GPIO_Pin只是对应位为1,其他位均为0,所以可以直接赋值*/ 27 28 GPIOx->BSRRH = GPIO_Pin; 29 }
这两个函数体内都是只有一个语句,对GPIOx的BSRRL或BSRRH寄存器赋值,从而设置引脚为高电平或低电平。其中GPIOx是一个指针变量,通过函数的输入参数可以修改它的值,如将GPIOA、GPIOB、GPIOH等结构体指针值赋予给它,这个函数就可以控制相应的GPIOA、GPIOB、GPIOH等端口的输出。
对比前面对BSRR寄存器的赋值,都是用“|=”操作来防止对其他数据位产生干扰的,为何此函数里的操作却直接用“=”操作赋值?这样不怕干扰其他数据位吗?赋值方式的对比见代码清单7-5。
代码清单7-5 赋值方式对比
1 /*使用 "|=" 来赋值*/ 2 GPIOH->BSRRH |= (1<<10); 3 /*直接使用 "=" 赋值*/ 4 GPIOx->BSRRH = GPIO_Pin;
根据BSRR寄存器的特性,对它的数据位写“0”,是不会影响输出的,只有对它的数据位写“1”,才会控制引脚输出。对低16位写“1”输出高电平,对高16位写“1”输出低电平。也就是说,假如我们对BSRRH(高16位)直接用“=”操作赋二进制值“0000 0000 0000 0001 b”,它会控制GPIO的引脚0输出低电平,赋二进制值“0000 0000 0001 0000 b”,它会控制GPIO的引脚4输出低电平,而其他数据位由于是0,所以不会受到干扰。同理,对BSRRL(低16位)直接赋值也是如此,数据位为1的位输出高电平。代码清单7-6中用两种方式赋值,功能相同。
代码清单7-6 BSRR寄存器赋值等效代码
1 /*使用 "|=" 来赋值*/ 2 GPIOH->BSRRH |= (uint16_t)(1<<10); 3 /*直接使用"=" 来赋值,二进制数(0000 0100 0000 0000)*/ 4 GPIOH->BSRRH = (uint16_t)(1<<10);
这两行代码功能等效,都把BSRRH的bit10设置为1,控制引脚10输出低电平,且其他引脚状态不变。但第2个语句的操作效率是比较高的,因为“|=”包含了读写操作,而“=”只需要一个写操作。因此在定义位操作函数中使用后者。
利用这两个位操作函数,就可以方便地操作各种GPIO的引脚电平了。控制各种端口引脚的范例见代码清单7-7。
代码清单7-7 位操作函数使用范例
1 2 /*控制GPIOH的引脚10输出高电平*/ 3 GPIO_SetBits(GPIOH, (uint16_t)(1<<10)); 4 /*控制GPIOH的引脚10输出低电平*/ 5 GPIO_ResetBits(GPIOH, (uint16_t)(1<<10)); 6 7 /*控制GPIOH的引脚10、引脚11输出高电平,使用“|”同时控制多个引脚*/ 8 GPIO_SetBits(GPIOH, (uint16_t)(1<<10)|(uint16_t)(1<<11)); 9 /*控制GPIOH的引脚10、引脚11输出低电平*/ 10 GPIO_ResetBits(GPIOH, (uint16_t)(1<<10)|(uint16_t)(1<<10)); 11 12 /*控制GPIOA的引脚8输出高电平*/ 13 GPIO_SetBits(GPIOA, (uint16_t)(1<<8)); 14 /*控制GPIOB的引脚9输出低电平*/ 15 GPIO_ResetBits(GPIOB, (uint16_t)(1<<9));
使用以上函数输入参数,设置引脚号时,还是稍感不便,为此我们把选择16个引脚的操作数都定义成宏,见代码清单7-8。
代码清单7-8 选择引脚参数的宏
1 /*GPIO引脚号定义*/ 2 #define GPIO_Pin_0 (uint16_t)0x0001) /*! < 选择引脚0 (1<<0) */ 3 #define GPIO_Pin_1 ((uint16_t)0x0002) /*! < 选择引脚1 (1<<1) */ 4 #define GPIO_Pin_2 ((uint16_t)0x0004) /*! < 选择引脚2 (1<<2) */ 5 #define GPIO_Pin_3 ((uint16_t)0x0008) /*! < 选择引脚3 (1<<3) */ 6 #define GPIO_Pin_4 ((uint16_t)0x0010) /*! < 选择引脚4 */ 7 #define GPIO_Pin_5 ((uint16_t)0x0020) /*! < 选择引脚5 */ 8 #define GPIO_Pin_6 ((uint16_t)0x0040) /*! < 选择引脚6 */ 9 #define GPIO_Pin_7 ((uint16_t)0x0080) /*! < 选择引脚7 */ 10 #define GPIO_Pin_8 ((uint16_t)0x0100) /*! < 选择引脚8 */ 11 #define GPIO_Pin_9 ((uint16_t)0x0200) /*! < 选择引脚9 */ 12 #define GPIO_Pin_10 ((uint16_t)0x0400) /*! < 选择引脚10 */ 13 #define GPIO_Pin_11 ((uint16_t)0x0800) /*! < 选择引脚11 */ 14 #define GPIO_Pin_12 ((uint16_t)0x1000) /*! < 选择引脚12 */ 15 #define GPIO_Pin_13 ((uint16_t)0x2000) /*! < 选择引脚13 */ 16 #define GPIO_Pin_14 ((uint16_t)0x4000) /*! < 选择引脚14 */ 17 #define GPIO_Pin_15 ((uint16_t)0x8000) /*! < 选择引脚15 */ 18 #define GPIO_Pin_All ((uint16_t)0xFFFF) /*! < 选择全部引脚 */
这些宏代表的参数是某位置“1”、其他位置“0”的数值,其中最后一个“GPIO_Pin_ALL”表示所有数据位都为“1”,所以用它可以一次控制设置整个端口的引脚0~15。利用这些宏,GPIO的控制代码可改为代码清单7-9。
代码清单7-9 使用位操作函数及宏控制GPIO
1 2 /*控制GPIOH的引脚10输出高电平*/ 3 GPIO_SetBits(GPIOH, GPIO_Pin_10); 4 /*控制GPIOH的引脚10输出低电平*/ 5 GPIO_ResetBits(GPIOH, GPIO_Pin_10); 6 7 /*控制GPIOH的引脚10、引脚11输出高电平,使用"|",同时控制多个引脚*/ 8 GPIO_SetBits(GPIOH, GPIO_Pin_10|GPIO_Pin_11); 9 /*控制GPIOH的引脚10、引脚11输出低电平*/ 10 GPIO_ResetBits(GPIOH, GPIO_Pin_10|GPIO_Pin_11); 11 /*控制GPIOH的所有输出低电平*/ 12 GPIO_ResetBits(GPIOH, GPIO_Pin_ALL); 13 14 /*控制GPIOA的引脚8输出高电平*/ 15 GPIO_SetBits(GPIOA, GPIO_Pin_8); 16 /*控制GPIOB的引脚9输出低电平*/ 17 GPIO_ResetBits(GPIOB, GPIO_Pin_9);
使用以上代码控制GPIO,我们就不需要再看寄存器了,直接从函数名和输入参数就可以直观地看出这个语句要实现什么操作(英文中Set表示“置位”,即高电平,Reset表示“复位”,即低电平)。
7.3.3 定义初始化结构体
定义位操作函数后,控制GPIO输出电平的代码得到了简化,但在控制GPIO输出电平前还需要初始化GPIO引脚的各种模式,这部分代码涉及的寄存器很多,我们希望初始化GPIO也能以如此简单的方法实现。为此,先将GPIO初始化时涉及的初始化参数以结构体的形式封装起来,声明一个名为GPIO_InitTypeDef的结构体类型,见代码清单7-10。
代码清单7-10 定义GPIO初始化结构体
1 typedef uint8_t unsigned char; 2 /** 3 * GPIO初始化结构体类型定义 4 */ 5 typedef struct { 6 uint32_t GPIO_Pin; /*! < 选择要配置的GPIO引脚 7 可输入GPIO_Pin_ 定义的宏 */ 8 9 uint8_t GPIO_Mode; /*! < 选择GPIO引脚的工作模式 10 可输入二进制值:00、01、10、11 11 表示输入/输出/复用/模拟 */ 12 13 uint8_t GPIO_Speed; /*! < 选择GPIO引脚的速率 14 可输入二进制值:00、01、10、11 15 表示2/25/50/100MHz */ 16 17 uint8_t GPIO_OType; /*! < 选择GPIO引脚输出类型 18 可输入二进制值:0、1 19 表示推挽/开漏 */ 20 21 uint8_t GPIO_PuPd; /*! <选择GPIO引脚的上拉/下拉模式 22 可输入二进制值:00、01、10 23 表示浮空/上拉/下拉*/ 24 } GPIO_InitTypeDef;
这个结构体中包含了初始化GPIO所需要的信息,包括引脚号、工作模式、输出速率、输出类型以及上拉/下拉模式。设计这个结构体的思路是:初始化GPIO前,先定义一个这样的结构体变量,根据需要配置GPIO的模式,对这个结构体的各个成员进行赋值,然后把这个变量作为“GPIO初始化函数”的输入参数,该函数能根据这个变量值中的内容配置寄存器,从而实现初始化GPIO。
7.3.4 定义引脚模式的枚举类型
上面定义的结构体很直接,美中不足的是,在对结构体中各个成员赋值时还需要看具体哪个模式对应哪个数值,如GPIO_Mode成员的“输入/输出/复用/模拟”模式对应二进制值“00、01、10、11”,我们不希望每次用到时都去查找这些索引值,所以使用C语言中的枚举语法定义这些参数,见代码清单7-11。
代码清单7-11 GPIO配置参数的枚举定义
1 /** 2 * GPIO端口配置模式的枚举定义 3 */ 4 typedef enum { 5 GPIO_Mode_IN = 0x00, /*! < 输入模式 */ 6 GPIO_Mode_OUT = 0x01, /*! < 输出模式 */ 7 GPIO_Mode_AF = 0x02, /*! < 复用模式 */ 8 GPIO_Mode_AN = 0x03 /*! < 模拟模式 */ 9 } GPIOMode_TypeDef; 10 11 /** 12 * GPIO输出类型枚举定义 13 */ 14 typedef enum { 15 GPIO_OType_PP = 0x00, /*! < 推挽模式 */ 16 GPIO_OType_OD = 0x01 /*! < 开漏模式 */ 17 } GPIOOType_TypeDef; 18 19 /** 20 * GPIO输出速率枚举定义 21 */ 22 typedef enum { 23 GPIO_Speed_2MHz = 0x00, /*! < 2MHz */ 24 GPIO_Speed_25MHz = 0x01, /*! < 25MHz */ 25 GPIO_Speed_50MHz = 0x02, /*! < 50MHz */ 26 GPIO_Speed_100MHz = 0x03 /*! < 100MHz */ 27 } GPIOSpeed_TypeDef; 28 29 /** 30 *GPIO上拉/下拉配置枚举定义 31 */ 32 typedef enum { 33 GPIO_PuPd_NOPULL = 0x00, /*浮空*/ 34 GPIO_PuPd_UP = 0x01, /*上拉*/ 35 GPIO_PuPd_DOWN = 0x02 /*下拉*/ 36 } GPIOPuPd_TypeDef;
有了这些枚举定义,GPIO_InitTypeDef结构体就可以使用枚举类型来限定输入了,代码清单7-12。
代码清单7-12 使用枚举类型定义的GPIO_InitTypeDef结构体成员
1 /** 2 * GPIO初始化结构体类型定义 3 */ 4 typedef struct { 5 uint32_t GPIO_Pin; /*! < 选择要配置的GPIO引脚 6 可输入GPIO_Pin_ 定义的宏 */ 7 8 GPIOMode_TypeDef GPIO_Mode; /*! < 选择GPIO引脚的工作模式 9 可输入GPIOMode_TypeDef定义的枚举值*/ 10 11 GPIOSpeed_TypeDef GPIO_Speed; /*! < 选择GPIO引脚的速率 12 可输入GPIOSpeed_TypeDef定义的枚举值 */ 13 14 GPIOOType_TypeDef GPIO_OType; /*! < 选择GPIO引脚输出类型 15 可输入GPIOOType_TypeDef定义的枚举值*/ 16 17 GPIOPuPd_TypeDef GPIO_PuPd; /*! <选择GPIO引脚的上拉/下拉模式 18 可输入GPIOPuPd_TypeDef定义的枚举值*/ 19 } GPIO_InitTypeDef;
如果不使用枚举类型,仍使用uint8_t类型来定义结构体成员,那么成员值的范围就是0~255了,而实际上这些成员都只能输入几个数值。所以使用枚举类型可以对结构体成员起到限定输入的作用,只能输入相应已定义的枚举值。
利用这些枚举定义,给GPIO_InitTypeDef结构体类型赋值就非常直观了,见代码清单7-13。
代码清单7-13 给GPIO_InitTypeDef初始化结构体赋值范例
1 GPIO_InitTypeDef InitStruct; 2 3 /* LED 端口初始化 */ 4 /*选择要控制的GPIO引脚*/ 5 InitStruct.GPIO_Pin = GPIO_Pin_10; 6 /*设置引脚模式为输出模式*/ 7 InitStruct.GPIO_Mode = GPIO_Mode_OUT; 8 /*设置引脚的输出类型为推挽输出*/ 9 InitStruct.GPIO_OType = GPIO_OType_PP; 10 /*设置引脚为上拉模式*/ 11 InitStruct.GPIO_PuPd = GPIO_PuPd_UP; 12 /*设置引脚速率为2MHz */ 13 InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
7.3.5 定义GPIO初始化函数
接着前面的思路,对初始化结构体赋值后,把它输入GPIO初始化函数,由它来实现寄存器配置。GPIO初始化函数实现见代码清单7-14。
代码清单7-14 GPIO初始化函数
1 2 /** 3 *函数功能:初始化引脚模式 4 *参数说明:GPIOx,该参数为GPIO_TypeDef类型的指针,指向GPIO端口的地址 5 * GPIO_InitTypeDef:GPIO_InitTypeDef结构体指针,指向初始化变量 6 */ 7 void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) 8 { 9 uint32_t pinpos = 0x00, pos = 0x00 , currentpin = 0x00; 10 11 /*-- GPIO Mode Configuration --*/ 12 for (pinpos = 0x00; pinpos < 16; pinpos++) { 13 /*以下运算是为了通过GPIO_InitStruct->GPIO_Pin算出引脚号0~15*/ 14 15 /*经过运算后pos的pinpos位为1,其余为0,与GPIO_Pin_x宏对应。 16 pinpos变量每次循环加1*/ 17 pos = ((uint32_t)0x01) << pinpos; 18 19 /* pos与GPIO_InitStruct->GPIO_Pin做&运算, 20 若运算结果currentpin == pos, 21 则表示GPIO_InitStruct->GPIO_Pin的pinpos位也为1, 22 从而可知pinpos就是GPIO_InitStruct->GPIO_Pin对应的引脚号:0~15 */ 23 currentpin = (GPIO_InitStruct->GPIO_Pin) & pos; 24 25 /*currentpin == pos时执行初始化*/ 26 if (currentpin == pos) { 27 /*GPIOx端口,MODER寄存器的GPIO_InitStruct->GPIO_Pin对应的引脚, 28 MODER位清空*/ 29 GPIOx->MODER &= ~(3 << (2 *pinpos)); 30 31 /*GPIOx端口,MODER寄存器的GPIO_Pin引脚, 32 MODER位设置"输入/输出/复用输出/模拟"模式*/ 33 GPIOx->MODER |= (((uint32_t)GPIO_InitStruct->GPIO_Mode) << (2 *pinpos)); 34 35 /*GPIOx端口,PUPDR寄存器的GPIO_Pin引脚, 36 PUPDR位清空*/ 37 GPIOx->PUPDR &= ~(3 << ((2 *pinpos))); 38 39 /*GPIOx端口,PUPDR寄存器的GPIO_Pin引脚, 40 PUPDR位设置"上拉/下拉"模式*/ 41 GPIOx->PUPDR |= (((uint32_t)GPIO_InitStruct->GPIO_PuPd) << (2 *pinpos)); 42 43 /*若模式为"输出/复用输出"模式,则设置速度与输出类型*/ 44 if ((GPIO_InitStruct->GPIO_Mode == GPIO_Mode_OUT) || 45 (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_AF)) { 46 /*GPIOx端口,OSPEEDR寄存器的GPIO_Pin引脚, 47 OSPEEDR位清空*/ 48 GPIOx->OSPEEDR &= ~(3 << (2 *pinpos)); 49 /*GPIOx端口,OSPEEDR寄存器的GPIO_Pin引脚, 50 OSPEEDR位设置输出速度*/ 51 GPIOx->OSPEEDR |= ((uint32_t)(GPIO_InitStruct->GPIO_Speed)<<(2 *pinpos)); 52 53 /*GPIOx端口,OTYPER寄存器的GPIO_Pin引脚, 54 OTYPER位清空*/ 55 GPIOx->OTYPER &= ~(1 << (pinpos)) ; 56 /*GPIOx端口,OTYPER位寄存器的GPIO_Pin引脚, 57 OTYPER位设置"推挽/开漏"输出类型*/ 58 GPIOx->OTYPER |= (uint16_t)(( GPIO_InitStruct->GPIO_OType)<< (pinpos)); 59 } 60 } 61 } 62 }
这个函数有GPIOx和GPIO_InitStruct两个输入参数,分别是GPIO外设指针和GPIO初始化结构体指针,分别用来指定要初始化的GPIO端口和引脚的工作模式。
函数实现主要分两个环节:
1)利用for循环,根据GPIO_InitStruct的结构体成员GPIO_Pin计算出要初始化的引脚号。这段看起来复杂的运算实际上可以这样理解:它要通过宏“GPIO_Pin_x”的参数计算出x值(宏的参数值是第x数据位为1,其余为0,参考代码清单7-8),计算得的引脚号存储在pinpos变量中。
2)得到引脚号pinpos后,利用初始化结构体各个成员的值,对相应寄存器进行配置,这部分与我们前面直接配置寄存器的操作是类似的,先将引脚号pinpos相应的配置位清空,然后根据结构体成员对配置位赋值(GPIO_Mode成员对应MODER寄存器的配置,GPIO_PuPd成员对应PUPDR寄存器的配置等)。区别是这里的寄存器配置值及引脚号都是由变量存储的。
7.3.6 使用函数点亮LED
完成以上的准备后,我们就可以用自己定义的函数来点亮LED了,见代码清单7-15。
代码清单7-15 使用函数点亮LED
1 2 /* 3 * 使用函数的方法点亮LED 4 */ 5 #include "stm32f4xx_gpio.h" 6 7 void Delay( uint32_t nCount); 8 9 /** 10 * 主函数,使用封装好的函数来控制LED 11 */ 12 int main(void) 13 { 14 GPIO_InitTypeDef InitStruct; 15 16 /*开启GPIOH时钟,使用外设时都要先开启它的时钟*/ 17 RCC->AHB1ENR |= (1<<7); 18 19 /* LED端口初始化 */ 20 21 /*初始化PH10引脚*/ 22 /*选择要控制的GPIO引脚*/ 23 InitStruct.GPIO_Pin = GPIO_Pin_10; 24 /*设置引脚模式为输出模式*/ 25 InitStruct.GPIO_Mode = GPIO_Mode_OUT; 26 /*设置引脚的输出类型为推挽输出*/ 27 InitStruct.GPIO_OType = GPIO_OType_PP; 28 /*设置引脚为上拉模式*/ 29 InitStruct.GPIO_PuPd = GPIO_PuPd_UP; 30 /*设置引脚速率为2MHz */ 31 InitStruct.GPIO_Speed = GPIO_Speed_2MHz; 32 /*调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO*/ 33 GPIO_Init(GPIOH, &InitStruct); 34 35 /*使引脚输出低电平,点亮LED1*/ 36 GPIO_ResetBits(GPIOH, GPIO_Pin_10); 37 38 /*延时一段时间*/ 39 Delay(0xFFFFFF); 40 41 /*使引脚输出高电平,关闭LED1*/ 42 GPIO_SetBits(GPIOH, GPIO_Pin_10); 43 44 /*初始化PH11引脚*/ 45 InitStruct.GPIO_Pin = GPIO_Pin_11; 46 GPIO_Init(GPIOH, &InitStruct); 47 48 /*使引脚输出低电平,点亮LED2*/ 49 GPIO_ResetBits(GPIOH, GPIO_Pin_11); 50 51 while (1); 52 53 } 54 55 //简单的延时函数,让CPU执行无意义指令,消耗时间 56 //具体延时时间难以计算,以后我们可使用定时器确定延时 57 void Delay( uint32_t nCount) 58 { 59 for (; nCount ! = 0; nCount--); 60 } 61 62 //函数为空,目的是“骗过”编译器不报错 63 void SystemInit(void) 64 { 65 }
现在看起来,使用函数来控制LED与之前直接控制寄存器已经有了很大的区别:main函数中先定义了一个初始化结构体变量InitStruct,然后对该变量的各个成员按点亮LED所需要的GPIO配置模式进行赋值,赋值后,调用GPIO_Init函数,让它根据结构体成员值对GPIO寄存器写入控制参数,完成GPIO引脚初始化。控制电平时,直接使用GPIO_SetBits和GPIO_ResetBits函数控制输出。如若对其他引脚进行不同模式的初始化,只要修改初始化结构体InitStruct的成员值,把新的参数值输入GPIO_Init函数再调用即可。
代码中新增的Delay函数的主要功能是延时,让我们可以看清楚实验现象(不延时的话指令执行太快,肉眼看不出来)。它的实现原理是让CPU执行无意义的指令,消耗时间,在此不要纠结它的延时时间,设置一个大概的输入参数值,下载到实验板中实测。若觉得太久了就把参数值改小,短了就改大即可。需要精确延时的时候,我们会用STM32的定时器外设进行延时设定的。
7.3.7 下载验证
把编译好的程序下载到开发板中并复位,可看到板子上的灯先亮红色(LED1),后亮绿色(LED2)。
7.3.8 总结
我们从寄存器映像开始,把内存与寄存器之间建立起一一对应的关系,然后操作寄存器点亮LED,再把寄存器操作封装成一个个函数。一步一步走来,我们实现了库最简单的雏形。如果我们不断地增加操作外设的函数,并且把所有的外设都写完,一个完整的库就实现了。
本章中的GPIO相关库函数及结构体定义,实际上都是从ST标准库搬过来的。这样分析它纯粹是为了满足自己的求知欲,学习其编程的方式、思想,这对提高我们的编程水平是很有好处的,顺便感受一下ST库设计的严谨性。这样设计出的代码不仅严谨且华丽优美。
与直接配置寄存器相比,从执行效率上看会有额外的消耗:初始化变量赋值的过程、库函数在被调用的时候要耗费调用时间;在函数内部,对输入参数转换所需要的额外运算也消耗一些时间(如GPIO中运算求出引脚号)。而其他的宏、枚举等解释操作是由编译过程完成的,这部分并不消耗内核的时间。那么函数库的优点是我们可以快速上手STM32控制器;配置外设状态时,不需要再纠结要向寄存器写入什么数值;交流方便,查错简单。这就是我们选择库的原因。
现在,处理器的主频越来越高,我们不需要担心CPU耗费那么多时间工作会不会超负荷,库主要应用于初始化过程,而初始化过程一般是芯片刚上电或在核心运算之前执行的,这段等待时间是0.02μs还是0.01μs在很多时候并没有什么区别。相对来说,如果都用寄存器操作,每行代码都要查《STM32F4xx规格书》中的说明,那么编写代码会很麻烦。
在以后开发的工程中,一般不会去分析ST库函数的实现了。因为外设的库函数是很类似的,库外设都包含初始化结构体,以及特定的宏或枚举标识符。这些封装被库函数转化成相应的值,写入寄存器之中,函数内部的具体实现是十分枯燥和机械的工作。如果有兴趣,在掌握了如何使用外设的库函数之后,可以查看一下它的源码实现。
通常我们只需要通过了解每种外设的“初始化结构体”就能够了解STM32的外设功能及控制了。