第5章

GPIO端口及其应用



学过51单片机的读者都应该知道,I/O端口是51单片机的一个最基本同时也是应用最普遍的外设,STM32单片机同样也是如此。本章就介绍STM32中最基本同时也是应用最普遍的外设——GPIO端口,绝大多数基于STM32的应用程序开发都离不开GPIO端口,它是STM32与外部进行数据交换的通道,同时,它也是应用STM32内置外设的通道。
首先讲解STM32的GPIO端口的一些重要的概念和知识,包括它的8种工作模式及其各自的工作原理; 然后介绍与GPIO端口相关的7个重要的寄存器; 接着介绍ST官方固件库中包含的与GPIO端口相关的一些重要的库函数以及对它们的应用; 最后讲解与GPIO端口相关的两个典型的应用实例,让大家在实际应用中理解GPIO端口的功能,并体会ST官方固件库在应用程序开发过程中的强大作用。
大家在对本章进行学习时,可以参考《STM32中文参考手册》相关内容。
本章的学习目标如下: 
 理解并掌握GPIO端口的基础知识,重点掌握它的8种工作模式及其工作原理; 
 理解并掌握与GPIO端口相关的7个常用寄存器的各位的作用并掌握对它们进行配置的方法; 
 理解并掌握对ST官方固件库中与GPIO端口相关的一些常用库函数的应用方法; 
 理解并掌握GPIO端口的两个典型应用实例——流水灯和按键控制LED。


视频


5.1GPIO端口概述
STM32最多可以有7个GPIO(General Purpose Input/Output)端口,分别是GPIOA、GPIOB、GPIOC、GPIOD、GPIOE、GPIOF和GPIOG,每个GPIO端口最多可以有16个端口位(引脚),这样STM32最多可以有112个GPIO端口位。STM32所有的GPIO端口位都可以被用作外部中断,学过51单片机的读者应该知道,51单片机只有两个I/O端口引脚(P3.2和P3.3)可以被用作外部中断,显然,STM32的GPIO端口具有更加强大的功能。
根据每个GPIO端口在芯片数据手册中列出的具体硬件特性,每个GPIO端口位都可以通过软件方式被独立地配置为以下8种工作模式: 
 浮空输入模式; 
 上拉输入模式; 
 下拉输入模式; 
 模拟输入模式; 
 开漏输出模式; 
 推挽输出模式; 
 复用功能的开漏输出模式; 
 复用功能的推挽输出模式。
当被配置为最后两种工作模式时,GPIO端口位会被用作STM32内置外设的复用功能引脚,后面会对此进行详细介绍。
STM32的标准I/O端口位的基本结构如图51所示。


图51STM32的标准I/O端口位的基本结构


STM32单片机的每个标准的I/O端口位都具有如图51所示的基本结构,在此基础上,可以通过软件方式将其配置为以上8种工作模式之一。
大家可能对所谓的“标准”I/O端口位不是很理解,对此进行一个简单的说明。标准I/O端口位,是相对于对5V兼容的I/O端口位来说的。STM32的工作电压(VDD)为2.0~3.6V(一般选择为3.3V),即I/O端口位在高电平状态下对应的电压为3.3V,但是,有些I/O端口位对于5V电压也是能够兼容的,它们的基本结构其实与图51几乎完全相同,具体可参见《STM32中文参考手册》第106页的图14。至于具体哪些I/O端口位是5V兼容的,需要查阅芯片相关的数据手册。对于天信通STM32F107开发板,在其数据手册的第3章中有一个关于引脚定义的表格,如图52所示。


图52STM32F107引脚定义表


在如图52所示的引脚定义表中,每一行表示芯片的一个引脚的相关信息,其中带有“FT”标记的表示该引脚对于5V是兼容的。下面结合图51,具体介绍STM32I/O端口位的8种工作模式的基本原理。
首先看一下输入工作模式。当I/O端口位被配置为浮空输入、上拉输入或下拉输入这3种工作模式时,其基本结构如图53所示。


图53浮空、上拉或下拉输入模式


在如图53所示的I/O端口位在浮空、上拉或下拉输入模式下的基本结构中,输入驱动器工作,输出驱动器停止,且输入驱动器中的TTL肖特基触发器处于被激活状态,保证I/O端口位中的数据(1或0,分别对应高/低电平状态)能够被采样到输入数据寄存器中。在输入驱动器中,I/O端口位可以通过闭合或断开相关电路的开关来选择是否通过上拉电阻连接到VDD,以及是否通过下拉电阻连接到VSS。当上拉和下拉电路的开关都断开时,即为浮空输入模式; 当上拉电路开关闭合、下拉电路开关断开时,即为上拉输入模式; 当下拉电路开关闭合、上拉电路开关断开时,即为下拉输入模式。通过读输入寄存器,就可以读取到I/O端口位的数据。
当I/O端口位被配置为模拟输入工作模式时,I/O端口位的基本结构如图54所示。


图54模拟输入模式



如图54所示的I/O端口位在模拟输入工作模式下的基本结构与图53最大的不同,就是它的输入驱动器中的TTL肖特基触发器处于被禁止的状态,它被强制输出一个0给输入数据寄存器,而且输入驱动器中的上拉和下拉电路都被断开(图54中未画),这样,I/O端口位上的模拟信号会在几乎零消耗的情况下进入片上外设。
再来看输出配置。当I/O端口位被配置为开漏输出工作模式时,其基本结构如图55所示。


图55开漏输出模式


在如图55所示的I/O端口位在开漏输出模式下的基本结构中,输入、输出驱动器均启动,可以通过位设置/清除寄存器或直接对输出数据寄存器(稍后会介绍这些寄存器)进行写操作,来设置I/O端口位中的数据,数据经过输出控制后,传递给其后面的相关MOS管电路。在输出控制的后面,上面的PMOS管不工作,只有下面的NMOS管工作。如果对数据写0,则NMOS管导通,数据0会被传递到I/O端口位; 如果对数据写1,则NMOS管截止,I/O端口位处于高阻状态,即它可能是0,也可能是1。在输入驱动器中,TTL肖特基触发器处于被激活状态,可以通过输入数据寄存器读出I/O端口位的数据。也可以读输出数据寄存器,但读取的值不一定就是I/O端口位中的数据(读取的值为0的时候是,为1的时候则不一定)
当I/O端口位被配置为推挽输出模式时,其基本结构如图56所示。


图56推挽输出模式


如图56所示的I/O端口位在推挽输出模式下的基本结构与图55的最大的不同,就是它输出驱动器中输出控制后面的PMOS管和NMOS管都会工作。当向I/O端口位的输出数据寄存器写0时,NMOS管导通,PMOS管截止,数据0会被传递到I/O端口位; 当向I/O端口位的输出数据寄存器写1时,NMOS管截止,PMOS管导通,数据1会被传递到I/O端口位。这样,向输出数据寄存器写的数据总能够被传递到I/O端口位,而PMOS管和NMOS管相当于各占半个工作周期,“推挽输出”一词也由此而来。在这种工作模式下,既可以通过输入数据寄存器,也可以通过输出数据寄存器来读出I/O端口位中的数据。
当I/O端口位被配置为复用功能的开漏输出模式时,其基本结构如图57所示。


图57复用功能的开漏输出模式


如图57所示的I/O端口位在复用功能开漏输出模式下的基本结构与如图55所示的I/O端口位在开漏输出模式下的最大的不同,就是它的输出驱动器中输出控制的数据,来自片上外设的复用功能输出,并且它的输入驱动器中TTL肖特基触发器输出的数据还会作为复用功能的输入进入片上外设。
当I/O端口位被配置为复用功能的推挽输出模式时,其基本结构如图58所示。


图58推挽复用输出模式


如图58所示的I/O端口位在复用功能推挽输出模式下的基本结构与如图56所示的I/O端口位在推挽输出模式下的最大的不同,就是它的输出驱动器中输出控制的数据来源,来自片上外设的复用功能输出,并且它的输入驱动器中TTL肖特基触发器输出的数据还会作为复用功能的输入进入片上外设。
以上就是STM32的I/O端口位的8种工作模式。当芯片上电复位后,除了与JTAG相关的I/O端口位之外,其他所有的I/O端口位都会被配置为浮空输入模式。而与JTAG相关的I/O端口位则会被配置为上拉或下拉输入模式,具体如下: 
PA15——JTDI置于上拉输入模式; 
PA14——JTCK置于下拉输入模式; 
PA13——JTMS置于上拉输入模式; 
PB4——JNRTST置于上拉输入模式。


视频



5.2GPIO端口的相关寄存器
STM32最多有7个GPIO端口(GPIOA~GPIOG),每个GPIO端口有7个相关的寄存器,因此,STM32最多有7×7=49个GPIO端口相关的寄存器,分别是: GPIOx_CRL(x=A,...,G),GPIOx_CRH(x=A,...,G),GPIOx_IDR(x=A,...,G),GPIOx_ODR(x=A,...,G),GPIOx_BSRR(x=A,...,G),GPIOx_BRR(x=A,...,G),GPIOx_LCKR(x=A,...,G),它们都是32位的寄存器,并且都只能以32位(字)的形式被访问,不能以16位(半字)或8位(字节)的形式被访问。
5.2.1端口配置低寄存器
端口配置低寄存器(GPIOx_CRL)(x=A,...,G)中各位的含义如图59所示。


图59端口配置低寄存器(GPIOx_CRL)(x=A,...,G)


如图59所示的端口配置低寄存器(GPIOx_CRL)(x=A,...,G)共有32位,可读/写。从第0位开始,到第31位,每4位为一组,一共可以分为8组,每组包含端口x的一组模式位MODEy[1:0](y=0,...,7)和一组配置位CNFy[1:0](y=0,...,7),可以通过设置它们的值来配置端口x的第y位的工作模式。
当MODEy[1:0]的值被设置为00(二进制,下同)时,端口x的第y位被配置为输入模式; 当MODEy[1:0]的值分别被设置为01、10或11时,端口x的第y位被配置为输出模式,且对应的最大输出速度分别为10MHz、2MHz和50MHz。
当MODEy[1:0]的值被设置为00时(输入模式),当CNFy[1:0]的值分别被设置为00、01或10时,端口x的第y位分别被配置为模拟输入、浮空输入或上拉/下拉输入模式(至于究竟是上拉还是下拉输入模式,需要通过设置后面将要介绍的GPIOx_ODR寄存器来确定),CNFy[1:0]的值为11的情况保留未被使用。
当MODEy[1:0]的值被设置为01、10或11时(输出模式),当CNFy[1:0]的值分别被设置为00、01、10或11时,端口x的第y位分别被配置为推挽输出、开漏输出、复用功能推挽输出或复用功能开漏输出的工作模式。
注意,GPIOx_CRL在芯片上电复位后的初值为0x44444444,即在芯片上电复位后,GPIOx_CRL的每一组模式位MODEy[1:0]和每一组配置位CNFy[1:0]的值会被分别设置为00和01,因此,STM32的I/O端口位(除JTAG相关的之外)在初始状态下会被配置为浮空输入工作模式。
5.2.2端口配置高寄存器
端口配置高寄存器(GPIOx_CRH)(x=A,...,G)的内容如图510所示。


图510端口配置高寄存器(GPIOx_CRH)(x=A,...,G)


如图510所示的端口配置高寄存器(GPIOx_CRH)(x=A,...,G)和如图59所示的端口配置低寄存器(GPIOx_CRL)(x=A,...,G)非常相似,所不同的只是GPIOx_CRL对应的是端口x的低8位(第0位~第7位)的工作模式的配置,而GPIOx_CRH对应的是端口x的高8位(第8位~第15位)的工作模式的配置。这样通过设置GPIOx_CRL和GPIOx_CRH,就可以配置端口x的全部16位(0~15)的工作模式。
5.2.3端口输入数据寄存器
端口输入数据寄存器(GPIOx_IDR)(x=A,...,G)的内容如图511所示。


图511端口输入数据寄存器(GPIOx_IDR)(x=A,...,G)


在图511所示的端口输入数据寄存器(GPIOx_IDR)(x=A,...,G)中,第0~15位分别对应端口x的16位的输入电平状态(1对应输入高电平,0对应输入低电平),这些位只能读并且只能以字的形式被读出。第16~31位保留,它们始终被读为0。
在芯片上电复位后,该寄存器的初值被设置为0x0000XXXX,因为端口x的16位默认处于浮空输入模式,所以它们的电平状态也处于浮空状态,不能确定它们输入的是高电平还是低电平。
5.2.4端口输出数据寄存器
端口输出数据寄存器(GPIOx_ODR)(x=A,...,G)的内容如图512所示。


图512端口输出数据寄存器(GPIOx_ODR)(x=A,...,G)


在图512所示的端口输出数据寄存器(GPIOx_ODR)(x=A,...,G)中,第0~15位分别对应端口x的16位的输出电平状态(1对应输出高电平,0对应输出低电平),这些位可读可写并且只能以字(16位)的形式进行操作,第16~31位保留,它们始终被读为0。
在芯片上电复位后,该寄存器的初值被设置为0x00000000,即在输出模式下,端口x的16位默认都输出低电平。

前面在介绍GPIOx_CRL(x=A,...,G)寄存器时,曾经提到过,当I/O端口位被配置为上拉/下拉输入模式时,究竟是选择上拉还是下拉输入模式,需要通过设置GPIOx_ODR(x=A,...,G)寄存器来进一步确定,现在给出《STM32中文参考手册》中的关于I/O端口位工作模式与GPIOx_CRL以及GPIOx_ODR寄存器关系的两个表,如图513所示。


图513端口位配置表和输出模式位表


5.2.5端口位设置/清除数据寄存器
端口位设置/清除数据寄存器(GPIOx_BSRR)(x=A,...,G)的内容如图514所示。


图514端口位设置/清除数据寄存器(GPIOx_BSRR)(x=A,...,G)


在如图514所示的寄存器(GPIOx_BSRR)(x=A,...,G)中,第0~15位为对应端口x的第0~15位的设置位,第16~31位为对应端口x的第0~15位的清除位,这些位只能被写入,并且只能以字(16位)的形式被操作。
当对设置位的第y(y=0,...,15)位BSy进行操作时,如果对该位写0,则不会对GPIOx_ODR寄存器的第y位产生影响; 如果对该位写1,则清除对应的ODRy位为0。
当对清除位的第y(y=0,...,15)位BRy进行操作时,如果对该位写0,则不会对GPIOx_ODR寄存器的第y位产生影响; 如果对该位写1,设置对应的ODRy位为1。
这个寄存器的主要作用在于: 通过对它进行设置,可以单独地设置或清除GPIOx_ODR寄存器中的相关位,以达到更改某个端口位的输出电平状态而不影响其他端口位的效果,其实在如图512所示的GPIOx_ODR寄存器的表中的最后进行了相关的说明,大家可以回去看一下。最后,需要注意的是,如果同时设置了GPIOx_BSRR寄存器中相对应的BSy和BRy位,则BSy位起作用。
5.2.6端口位清除数据寄存器
端口位清除数据寄存器(GPIOx_BRR)(x=A,...,G)的内容如图515所示。


图515端口位清除数据寄存器(GPIOx_BRR)(x=A,...,G)


在如图515所示的寄存器(GPIOx_BSRR)(x=A,...,G)中,第0~15位对应端口x的第0~15位的清除位,这些位只能被写入,并且只能以字(16位)的形式被操作。
当对其中的第y位BRy进行操作时,如果对该位写0,则不会对GPIOx_ODR寄存器的第y位产生影响; 如果对该位写1,清除对应的ODRy位为0。GPIOx_BSRR寄存器的低16位与GPIOx_BSRR寄存器的高16位的功能基本相同,它的高16位则保留。 
以上介绍了与GPIOx(x=A,...,G)相关的6个寄存器,还有一个端口配置锁定寄存器(GPIOx_LCKR)(x=A,...,G),用得不是很多,这里就不介绍了,有兴趣的读者可以参考《STM32中文参考手册》中8.2.7节的内容进行学习。这里顺便说明一下,关于STM32的GPIO端口的内容在《STM32中文参考手册》的8.1节和8.2节中有详细介绍,整个8.2节全部是在介绍GPIO端口相关的7个寄存器,在该手册中,一般在每章的最后一节,都会将该章主讲的知识点所涉及的全部寄存器都详细地列出,因为本章还有另一个重要知识点AFIO,因此GPIO相关的寄存器放在了8.2节中。



视频


5.3GPIO端口的相关库函数
通过5.2节的学习,大家应该对GPIO端口相关的几个主要的寄存器有了一个总体的认识和基本的了解。本节将在5.2节的基础上介绍ST官方固件库中提供的与GPIO端口相关的一些重要的库函数,为5.4节GPIO端口的应用实例打下基础。
首先打开在3.4节中建立的工程模板Template,并在它的FWLIB子文件夹下找到stm32f10x_gpio.c文件,如图516所示。

打开stm32f10x_gpio.c文件,在开始处的头文件包含区域,找到与该文件相对应的stm32f10x_gpio.h头文件被包含的预处理命令,然后对其右击,在弹出的快捷菜单中选择Open document ‘stm32f10x_gpio.h’或Go to Headerfile ‘stm32f10x_gpio.h’命令,如图517所示。



图516在FWLIB子文件夹下找到stm32f10x_gpio.c文件





图517打开stm32f10x_gpio.h头文件






图518包含的头文件列表



另一个打开stm32f10x_gpio.h头文件的方法是在工程的USER或FWLIB子文件夹下的任何一个源文件所包含的头文件列表中找到并打开它。以main.c为例,如图518所示,当然,这需要首先对工程进行编译。

在stm32f10x_gpio.h头文件的最后,可以看到一系列以“GPIO”开头的函数声明,如图519所示。


图519GPIO端口相关的函数声明


它们都是与GPIO端口相关的函数,这些函数的定义都在stm32f10x_gpio.c源文件中。在3.1.3节介绍ST官方固件库包时,曾经讲到,函数库文件夹Libraries中包含一个名为STM32F10x_StdPeriph_Driver的文件夹,该文件夹中又包含了inc和src两个文件夹,它们又分别包含着ST官方提供的各种库函数相关的源文件(.c文件)和头文件(.h文件),且一个源文件对应一个相关的头文件。现在可以告诉大家,关于库函数的定义基本上都在相关的源文件中,而其声明基本上都在相关的头文件中。
5.3.1GPIO_Init()函数
首先来介绍GPIO端口的相关初始化函数GPIO_Init()。在ST官方固件库中,一般名字以“_Init”结尾的函数都是初始化函数。该函数的声明如下所示: 


void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);


在该函数声明处对GPIO_Init右击,在弹出的快捷菜单中选择Go To Definition Of ‘GPIO_Init’命令,如图520所示。


图520Go To Definition Of ‘GPIO_Init’命令


程序会跳转到stm32f10x_gpio.c源文件中的该函数定义处,如图521所示。


图521GPIO_Init()函数的定义


图521只是截取了该函数定义中的前几行代码,关于该函数的具体实现过程,此处不做详细介绍,有兴趣的读者可以自己尝试,其实该函数最终就是通过设置GPIOx_CRL或GPIOx_CRH这两个寄存器来实现配置GPIO端口引脚工作模式的功能,相关的代码如下: 


GPIOx->CRL = tmpreg;

GPIOx->CRH = tmpreg;


下面重点介绍关于该函数的应用。
可以看到,该函数的两个参数分别是GPIO_TypeDef和GPIO_InitTypeDef的指针类型的。用刚才的方法单击进入GPIO_TypeDef的定义,程序会跳转到stm32f10x.h头文件中的如下代码处: 


typedef struct

{

__IO uint32_t CRL;

__IO uint32_t CRH;

__IO uint32_t IDR;

__IO uint32_t ODR;

__IO uint32_t BSRR;

__IO uint32_t BRR;

__IO uint32_t LCKR;

} GPIO_TypeDef;


这是一个结构体类型,它有7个成员变量,实际上它们分别对应着与GPIO端口相关的7个寄存器,大家从它们的名字中也能看出它们之间是互相对应的。该结构体类型实际上对应的是一个GPIO端口。
再单击进入GPIO_InitTypeDef的定义,程序会跳转到stm32f10x_gpio.h头文件中的如下代码处:  


typedef struct

{

uint16_t GPIO_Pin;

GPIOSpeed_TypeDef GPIO_Speed;  

GPIOMode_TypeDef GPIO_Mode;

}GPIO_InitTypeDef;


这也是一个结构体类型,它有3个成员变量,其中,GPIO_Speed和GPIO_Mode分别是GPIOSpeed_TypeDef和GPIOMode_TypeDef类型的。再分别进入它们的定义,可以看到,它们的定义都在stm32f10x_gpio.h头文件中。
GPIOSpeed_TypeDef的定义如下所示: 


typedef enum

{ 

GPIO_Speed_10MHz = 1,

GPIO_Speed_2MHz, 

GPIO_Speed_50MHz

}GPIOSpeed_TypeDef;


可以看出,GPIOSpeed_TypeDef是一个枚举类型,其枚举值分别对应端口引脚在输出模式下的输出速度为10MHz、2MHz以及50MHz。
GPIOMode_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;


可以看出,GPIOSpeed_TypeDef也是一个枚举类型,其枚举值分别对应GPIO端口的8种工作模式,依次为模拟输入、浮空输入、下拉输入、上拉输入、开漏输出、推挽输出、复用功能开漏输出以及复用功能推挽输出。
结合它们的名字和注释(可以参考具体代码),不难猜出它们的含义: 
GPIO_Pin——要被初始化的GPIO端口的引脚; 
GPIO_Speed——要被初始化的GPIO端口引脚的(输出)速度; 
GPIO_Mode——要被初始化的GPIO端口引脚的工作模式。
现在,相信大家应该对该函数有一个大体的了解了。下面通过一个例子来具体说明对该函数的应用及其应用过程中的一些重要技巧。
例如,现在要将GPIOA的第5个引脚PA5配置为推挽输出工作模式,那么可以编写如下的程序代码: 


GPIO_InitTypeDef  GPIO_InitStructure;

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;//端口引脚号为5

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出模式 

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//输出速度为50MHz

GPIO_Init(GPIOA, &GPIO_InitStructure);//GPIO端口A


首先,需要说明的是,这只是一段孤立的代码,实际应用中当然还需要添加其他相关代码或命令,这里只是为了讲解怎样应用这个函数(下同)。
这段代码首先定义了一个GPIO_InitTypeDef结构体类型的变量GPIO_InitStructure,然后对GPIO_InitStructure的3个成员变量分别进行赋值,最后调用GPIO_Init()函数完成相关的初始化操作。注意,因为GPIO_Init()函数的相应形参是GPIO_InitTypeDef指针类型的,而定义的变量GPIO_InitStructure是GPIO_InitTypeDef类型的,所以,在GPIO_Init()函数的调用过程中,要用“&GPIO_InitStructure”来作相应的实参。
可能大家会有疑问,在上面的代码中,对GPIO_Init()函数的实参GPIOA 以及GPIO_InitStructure的成员变量GPIO_Pin的赋值要到哪里去找呢?下面就来回答这个问题同时以该函数为例介绍应用ST官方库函数的一些重要技巧。
首先,在Template工程的USER子文件夹下打开main.c文件,并将其中main()函数中的代码全部注释起来。注释的快速方法是选中要注释的代码,然后在MDK工具栏中单击Edit→Advanced→Comment Selection命令,取消注释的快速方法则是相应地单击Edit→Advanced→Uncomment Selection命令。
然后可以试着在main()函数中逐条输入以上的5条代码。因为要调用的GPIO_Init()函数的声明之前已经在stm32f10x_gpio.h头文件中找到,所以可以先输入这个函数的名称,然后再分别确定它的相关实参,如下所示。

GPIO_Init();


对GPIO_Init()函数的实参的选取,实际上可以通过如图520所示该函数定义中的前几行对函数形参进行有效性验证的代码来实现。具体来说,在如图520所示的GPIO_Init()函数的定义中,在定义了相关的局部变量之后,多次通过调用assert_param()函数来对GPIO_Init()函数的各个形参或其成员变量进行有效性验证,相关代码如下: 


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));

assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));


只需要对其中的宏IS_GPIO_ALL_PERIPH、IS_GPIO_MODE、IS_GPIO_PIN和IS_GPIO_SPEED右击,在弹出的快捷菜单中选择Go To Definition Of命令,进入它们的定义,就可以查看到相关取值的定义。
单击进入IS_GPIO_ALL_PERIPH的定义,可以看到,程序跳转到stm32f10x_gpio.h头文件中的如下代码处: 


#define IS_GPIO_ALL_PERIPH(PERIPH) (((PERIPH) == GPIOA) || \

((PERIPH) == GPIOB) || \

((PERIPH) == GPIOC) || \

((PERIPH) == GPIOD) || \

((PERIPH) == GPIOE) || \

((PERIPH) == GPIOF) || \

((PERIPH) == GPIOG))


它的作用就是判断PERIPH是否是GPIOA~GPIOG中的一个,这里的“\”表示下一行代码与该行是连续的。从中可以很容易地找到需要的GPIO端口,并将它作为被调用的GPIO_Init()函数的第一个实参写入。
对于GPIO_Init()函数的第二个实参,因为它对应的形参是一个GPIO_InitTypeDef结构体类型的变量,因此,需要先定义一个该结构体类型的变量: 


GPIO_InitTypeDef  GPIO_InitStructure;


然后,需要对该结构体类型变量的各成员分别进行赋值,在GPIO_InitStructure的后面输入“.”,可以看到,GPIO_InitStructure的3个成员变量会自动显示出来,如图522所示。


图522GPIO_InitStructure
的3个成员变量


这样,就可以很容易地写出GPIO_InitStructure的3个成员变量,再分别对它们进行赋值。
然后,用前面的方法,单击进入IS_GPIO_ALL_PERIPH的定义,程序会跳转到stm32f10x_gpio.h头文件中的如下代码处: 


#define IS_GPIO_MODE(MODE) (((MODE) == GPIO_Mode_AIN) || \

((MODE) == GPIO_Mode_IN_FLOATING) || \

((MODE) == GPIO_Mode_IPD) || \

((MODE) == GPIO_Mode_IPU) || \

((MODE) == GPIO_Mode_Out_OD) ||\

((MODE) == GPIO_Mode_Out_PP) || \

((MODE) == GPIO_Mode_AF_OD) ||\

((MODE) == GPIO_Mode_AF_PP))


与刚才IS_GPIO_ALL_PERIPH(PERIPH)的情况相似,从中可以很容易地找到需要配置的GPIO端口的工作模式,并将其赋值给GPIO_InitStructure的成员变量GPIO_Mode,如下所示: 


GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;


在这段代码的前面,实际上就是GPIOMode_TypeDef的定义,如图523所示。


图523GPIOMode_TypeDef及IS_GPIO_MODE(MODE)的定义


再单击进入IS_GPIO_ALL_PERIPH的定义,程序会跳转到stm32f10x_gpio.h头文件中的相关代码处,如图524所示。


图524所有GPIO引脚及IS_GPIO_ALL_PERIPH(PIN)的定义


在图524中,在IS_GPIO_ALL_PERIPH(PIN)定义的前面,就是对所有GPIO端口引脚的定义GPIO_Pin_0~GPIO_Pin_15,以及一个对全部16个GPIO端口引脚都选中的定义,从中可以很容易地找到需要的GPIO端口引脚,并将其赋值给GPIO_InitStructure的成员变量GPIO_Pin,如下所示: 


GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;


IS_GPIO_ALL_PERIPH(PIN)的定义如下: 


#define IS_GPIO_PIN(PIN) ((((PIN) & (uint16_t)0x00) == 0x00) && ((PIN) != (uint16_t)0x00))


它实际上就是验证PIN是否来自前面定义的16个GPIO端口引脚中的一个或多个。因为16个引脚中的每一个都对应16位二进制数的其中1位,16个引脚刚好对应16位,所以,这16个引脚中的每一个是否被选中与其他引脚是否被选中彼此之间是相互独立的。如果想一次选中多个引脚,则可以用“|”操作符将它们“捆绑”起来。例如,这里如果还想将GPIOA的第6和第7个引脚PA6和PA7也配置为与PA5相同的工作模式,那么只需将相关的代码修改为: 


GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;


而不必对PA6和PA7都像上面PA5那样赋值一次。
最后,单击进入IS_GPIO_SPEED的定义,如图525所示。


图525GPIOSpeed_TypeDef及IS_GPIO_SPEED(SPEED)的定义


在图525中,在IS_GPIO_SPEED(SPEED)的前面,就是对GPIOSpeed_TypeDef的定义,从中也可以很容易地找到我们需要的端口引脚的输出速度,这里选择50MHz,并将其对应的枚举成员赋值给GPIO_InitStructure的成员变量GPIO_Speed,如下所示: 


GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;


通过上面这3个例子,可以看出,ST官方固件库中很多关于取值的相关定义就在对该值进行相关有效性验证的前面。而且,ST官方固件库提供的代码有很好的可读性,基本上从被定义的某值所对应的宏的名字,就可以明白它所表示的含义,而当选取某值时,只需要选取它所对应的宏就可以了,其他工作ST官方固件库都已经为我们做好了,这样既减轻了大家编程的工作量,又提高了代码的可读性。
在确定了对GPIO_InitStructure的3个成员变量的赋值之后,再将“&GPIO_InitStructure”作为被调用的GPIO_Init()函数的第二个实参写入,这样,这段代码就全部写完了。很显然,通过这种方式来调用GPIO_Init()函数并选取它的相关实参,可以使编程更加高效和便捷。
一般情况下,在ST官方提供的每个库函数定义中的起始部分,在相关局部变量的定义之后,都会通过assert_param()函数来对该库函数的形参进行相关的有效性验证,而通过它就可以很容易地获得对该函数的相关实参的选取。这个对库函数的使用技巧非常重要,在后面会再次提到它,到时大家可以结合实际应用来体会它的具体作用。
5.3.2GPIO_SetBits()函数和GPIO_ResetBits()函数
下面介绍与GPIO端口相关的其他重要函数。首先来看GPIO_SetBits()和GPIO_ResetBits(),它们的声明分别如下所示: 


void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);


对于这两个函数,相信大家在经过了刚才对GPIO_Init()函数的学习之后,再结合这两个函数的名字及其形参,不难猜出它们要实现的功能。没错,它们的作用分别是将端口GPIOx的引脚GPIO_Pin上的输出电平设置为高电平和低电平,或者说,将该电平对应的二进制数据置1和清0。它们的定义或具体实现过程分别如下所示: 


void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

{

assert_param(IS_GPIO_ALL_PERIPH(GPIOx));

assert_param(IS_GPIO_PIN(GPIO_Pin));

GPIOx->BSRR = GPIO_Pin;

}

void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

{

assert_param(IS_GPIO_ALL_PERIPH(GPIOx));

assert_param(IS_GPIO_PIN(GPIO_Pin));

GPIOx->BRR = GPIO_Pin;

}


可以看出,可以分别通过设置GPIOx_BSR和GPIOx_BRR寄存器来实现其相应的功能。
如果想要将PA5引脚上的输出电平置1或清0,则可以在通过调用5.3.2节中介绍的GPIO_Init()函数对其进行相关的初始化操作后,再通过调用本节介绍的这两个函数来实现上述的要求,如下所示: 


GPIO_SetBits(GPIOA, GPIO_Pin_5);

GPIO_ResetBits(GPIOA, GPIO_Pin_5);


5.3.3GPIO_Write()函数和GPIO_WriteBit()函数
在5.2节中曾经讲到,对GPIOx_BSR和GPIOx_BRR 寄存器的设置实际上最终的结果是设置GPIOx_ODR寄存器。在stm32f10x_gpio.c源文件中,当然也定义了直接对GPIOx_ODR寄存器进行写操作的相关库函数——GPIO_Write()函数,它的声明如下: 


void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);


显然,只能一次性对端口GPIOx上的输出电平数据赋值PortVal,其具体实现过程如下所示: 


void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal)

{

assert_param(IS_GPIO_ALL_PERIPH(GPIOx));

GPIOx->ODR = PortVal;

}


还有一个名字与之类似的函数GPIO_WriteBit(),声明如下: 


void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);


该函数的功能实际上是GPIO_SetBits()和GPIO_ResetBits()这两个函数的结合,即对端口GPIOx的引脚GPIO_Pin上的输出电平数据赋值BitVal。
5.3.4GPIO_ReadInputDataBit()函数、GPIO_ReadInputData()
函数、GPIO_ReadOutputDataBit()函数和
GPIO_ReadOutputData()函数
还有4个分别对GPIOx_IDR和GPIOx_ODR寄存器进行读操作的库函数,它们的声明分别如下: 


uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);

uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);


这些函数的作用分别是: 返回读取的端口GPIOx的引脚GPIO_Pin上的输入电平数据、返回读取的整个GPIOx端口(即端口GPIOx上所有引脚的)的输入电平数据、返回读取的端口GPIOx的引脚GPIO_Pin上的输出电平数据以及返回读取的整个GPIOx端口(即端口GPIOx上所有引脚的)的输出电平数据。
5.3.5GPIO_DeInit()函数
还有一个与GPIO_Init()函数名字类似的函数GPIO_DeInit(),它的声明如下: 


void GPIO_DeInit(GPIO_TypeDef* GPIOx);


该函数的作用是将端口GPIOx的所有相关寄存器的值都初始化到芯片上电复位后的默认状态。这个函数用得不是很多,在这里给大家介绍它,是想告诉大家,在ST官方固件库中,一般名字以“_DeInit”结尾的函数,都是初始化到默认状态的相关函数。
这样就将与GPIO端口相关的常用库函数全部都介绍完了。这些函数实际上都是通过对底层的相关寄存器进行操作来实现相应的功能的。
目前,对大家的要求是只需掌握怎样使用它们就可以了。如果以后想进行更深层次的开发,可以尝试着学习这些库函数的具体实现,这样可以加深对这些库函数的理解。此外,以后可能需要自己编写相关芯片的外设驱动函数等,学习ST官方库函数的具体实现过程,是很有帮助的。
最后需要说明的是,在5.2节介绍GPIO端口相关的寄存器的内容,是为了让大家对本节介绍的GPIO端口相关的库函数有更深刻的理解。
当用STM32进行实际项目开发时,通过ST官方提供的库函数就可以实现许多功能,但如果只会简单的调用,而不理解其底层操作的原理,那相当于只学会了皮毛,而没有掌握其内在的实质——单片机的应用开发实际上就是通过对它的底层寄存器进行相关的操作来实现其相应的功能,这样以后在较复杂的项目开发过程中很难做到举一反三,因为STM32有许多功能无法只靠ST官方提供的库函数来实现。因此,只有在理解了STM32底层寄存器工作原理的基础上,再去调用ST官方提供的库函数,才能在实际项目开发的过程中取得事半功倍的效果。
5.4GPIO端口的应用实例
通过前面几节的学习,相信大家应该对STM32的GPIO端口的工作原理、相关寄存器以及相关库函数都有了一定程度的理解。本节将在前面学习的基础上,并在本书相关的硬件开发平台的支持下,应用STM32的GPIO端口实现两个典型的案例。
首先,需要说明的是,硬件开发平台——天信通STM32F107开发板的主控芯片STM32F107VCT6只有5个GPIO端口,即GPIOA~GPIOE,每个GPIO端口上都有16个端口位,一共有80个GPIO端口位。
5.4.1流水灯


视频


单片机I/O端口的一个典型应用就是通过输出高低电平来实现控制LED(Light Emitting Diode,发光二极管)的亮灭,并可以进一步实现流水灯。如果大家学过51单片机,对此应该不会感到陌生。本节应用STM32的GPIO端口来实现流水灯,并以这个典型实例作为本书所有应用实例的开始。
1. 实例描述
本实例通过控制开发板的相关GPIO端口引脚不断地输出高低电平来使得开发板中的4个LED依次亮,在每一个LED亮时,其他LED灭,从而实现流水灯的效果。
2. 硬件电路
本实例相关的硬件电路如图526所示。
在图526中,发光二极管D2、D3、D4和D5的正极分别通过限流电阻R54、R53、R52和R51连接到3.3V,它们的负极分别连接到开发板的PE9、PE11、PE13和PE14引脚。根据二极管的单向导电性,只有当正极与负极的电压差不小于开启电压时,二极管才能导通。因此,在该电路中,当PE9、PE11、PE13或PE14引脚输出低电平时,与其相对应的发光二极管(即D2、D3、D4或D5)会亮; 而当PE9、PE11、PE13或PE14引脚输出高电平时,与其相对应的发光二极管不会亮。
3. 软件设计
下面进行本应用实例的软件设计。
首先,新建一个名为“流水灯”的文件夹,并在其中用3.4节中介绍的基于固件库新建工程模板的方法新建一个名为LED的工程。在MDK中,在向工程LED的FWLIB子目录下添加文件时,可以只添加stm32f10x_gpio.c和stm32f10x_rcc.c这两个文件,因为本实例只会用到这两个文件中的库函数。在main.c文件中,可以暂时只保留一个空的main()函数。
然后,在“流水灯”文件夹中新建一个名为HARDWARE的文件夹,再在HARDWARE文件夹中新建一个名为LED的文件夹,如图527所示。



图526LED相关的硬件电路





图527HARDWARE文件夹中的LED文件夹



现在,在MDK的菜单栏上单击File→New命令,或者在MDK的工具栏上单击其相关的图标,新建两个文件,然后在菜单栏上单击File→Save命令,或者在工具栏上单击图标,将它们保存在刚才新建的LED文件夹中,并分别将它们命名为led.c和led.h。注意,这一步也可以直接在如图527所示的LED文件夹中通过右击,在弹出的快捷菜单中选择“新建”→“文本文档”命令的方式来进行创建,只需要将原本文件名的扩展名.txt相应地修改为.c和.h即可,这里是为了给大家介绍用MDK新建文件的基本应用。
然后,在MDK中将led.c源文件添加到LED工程的HARDWARE子目录中。并且在LED工程选项中,将led.h头文件的路径添加到LED工程要包含的头文件的路径列表中。如果对以上操作不是很清楚,可以参考3.4节中的相关内容。
现在,LED工程在MDK中的目录结构如图528所示。


图528LED工程的文件夹结构及其各子文件夹内容简介


在图528中,除了给出了LED工程的文件夹结构,还对LED工程的各子文件夹的内容做了简单的介绍,其中的一部分内容曾经在3.1.3节介绍STM32官方固件库时提到过,但那时大家会感觉有些抽象,现在结合实际例程,再次对它们进行讲解,相信大家会对它们有更深刻的理解。
1) USER子文件夹
USER子文件夹中共包含3个文件: main.c、stm32f10x_it.c和system_stm32f10x.c。其中,main.c文件主要用来编写main函数; stm32f10x_it.c文件主要用来定义部分中断服务函数; system_stm32f10x.c文件主要用来定义SystemInit时钟初始化函数。

2) CORE子文件夹
CORE子文件夹用来存放固件库核心文件core_cm3.c和启动文件startup_stm32f10x_cl.s,这两个文件的内容一般不需要进行修改。
3) FWLIB子文件夹
FLWIB子文件夹用来存放工程用到的ST官方固件库中的相关文件,一般是用到哪个就添加哪个(避免工程过于庞大),本实例中,只用到与STM32的GPIO端口相关的stm32f10x_gpio.c文件以及与STM32的时钟相关的stm32f10x_rcc.c文件。一般情况下,所有的工程都需要添加stm32f10x_rcc.c文件,因为在系统初始化以及使用外设的过程中都会调用到时钟相关的库函数。
4) HARDWARE子文件夹
HARDWARE子文件夹用来存放工程相关的硬件外设的驱动文件,它们一般需要由自己进行创建并编写,如本工程中的led.c文件。
现在,开始编写程序代码。
第一步,在led.h头文件中添加如下程序代码: 


#ifndef __LED_H

#define __LED_H

#include "stm32f10x.h"

void Init_LED(void);

#endif


如果大家是用前面介绍的第二种方法新建的led.c和led.h文件,则可以通过在MDK的菜单栏中单击File→Open命令的方法打开led.h文件。
下面简单解释一下这段代码的含义。
首先,文件通过条件编译预处理命令,避免了编译过程中出现led.h头文件被重复定义的错误,如下所示: 


#ifndef __LED_H

#define __LED_H

…

#endif


上面的这段预处理命令被称为是条件编译预处理命令。所谓预处理命令,顾名思义,即在编译之前会预先处理的命令。引入这段条件编译预处理命令的目的是防止led.h头文件被重复定义。大家以后在实际工程中可以看到几乎所有的被包含的头文件的开头都会采用这种形式,实际上ST官方固件库提供的所有头文件的开头也都采用了这种形式。它的具体含义为: 如果未定义宏__LED_H,则定义它,并执行它后面的一直到#endif之前的代码; 反之,不会执行#endif之前的代码。宏__LED_H实际上对应的就是该头文件,通过这种方式,可避免led.h头文件因被多次包含而重复定义所带来的编译错误。
然后,在条件编译预处理命令之间,文件通过文件包含预处理命令包含了stm32f10x.h头文件,如下所示: 


#include "stm32f10x.h"


前面介绍过,这个头文件包含了许多重要的结构体以及宏的定义,此外,它还包含了系统寄存器定义声明以及包装内存操作,它几乎在整个STM32应用程序开发的过程中都会被使用到。可以在其右键快捷菜单中选择“Open document ‘stm32f10x.h’”命令打开该文件,查看其中的相关定义。
此外,文件还声明了LED相关的初始化函数LED_Init(),如下所示,这个函数会在led.c文件中定义。


void LED_Init(void);


至此,led.h文件中的所有程序代码就全部讲解完了。
第二步,在led.c文件中添加如下程序代码: 


#include "led.h"



void LED_Init(void)

{

GPIO_InitTypeDef GPIO_InitStructure; 

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_11 | GPIO_Pin_13 | GPIO_Pin_14;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_Init(GPIOE, &GPIO_InitStructure);

GPIO_SetBits(GPIOE, GPIO_Pin_9 | GPIO_Pin_11 | GPIO_Pin_13 | GPIO_Pin_14 );

}


在该文件中,首先包含了led.h头文件,这相当于同时包含了stm32f10x.h头文件。在该文件中,主要对LED_Init()函数进行定义,该函数的主要功能是初始化LED相关的GPIO端口位,即配置LED相关的GPIO端口位的工作模式。
在4.2节中曾经讲到,对于STM32,使用它的任何一个外设之前都必须首先使能与它相关的时钟。因此,在LED_Init()函数中,首先需要做的就是初始化将要用到的GPIO端口的时钟。另外,所有的通用端口(即GPIO端口)的时钟都来自APB2时钟。
这里可以通过设置相关的寄存器来实现GPIO端口的时钟使能,当然,更简单的方法还是通过调用ST官方提供的库函数来实现。
ST官方提供的时钟相关的库函数,都被定义在stm32f10x_rcc.c源文件中,打开相应的stm32f10x_rcc.h头文件,在该文件的最后,可以看到一系列时钟相关的函数声明,从中可以找到RCC_APB2PeriphClockCmd()函数的声明,如下所示: 


void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);


该函数的功能就是使能挂在APB2高级外设总线上的外设的时钟,单击进入它的定义,如图529所示。


图529RCC_APB2PeriphClockCmd()函数的定义


在对RCC_APB2PeriphClockCmd()函数进行调用时,可以按照5.3节中介绍的方法来选取相应的实参。找到如图529所示的该函数定义中的通过assert_param()对函数形参进行有效性验证的相关代码,然后分别右击其中的IS_RCC_APB2_PERIPH和IS_FUNCTIONAL_STATE,在弹出的快捷菜单中选择Go to Definition of命令。对IS_RCC_APB2_PERIPH进行这样的操作,程序会跳转到stm32f10x_rcc.h文件中的相关代码处,如图530所示。


图530APB2总线上所有外设时钟及IS_RCC_APB2_PERIPH(PERIPH)的定义



在图530中,在IS_RCC_APB2_PERIPH(PERIPH)定义的前面,是对APB2总线上所有外设时钟的定义。对于这些宏定义,基本上通过它们的名字就可以知道它们所对应的外设,从中可以很容易地找到要使能的端口GPIOE的时钟所对应的宏定义RCC_APB2Periph_GPIOE,并将它作为要被调用的RCC_APB2PeriphClockCmd()函数的相应实参,剩下的工作ST官方固件库都已经为我们做好了。
IS_RCC_APB2_PERIPH(PERIPH)的定义如下所示: 


#define IS_RCC_APB2_PERIPH(PERIPH) ((((PERIPH) & 0xFFC00002) == 0x00) && ((PERIPH) != 0x00))


它的作用就是检验PERIPH是否来自前面定义的挂在APB2总线上的外设时钟。可以看到,每一个挂在APB2总线上的外设时钟都被定义为一个32位二进制数的其中一位,与前面GPIO端口引脚的定义相似,它们当中每一个外设时钟是否被选中与其他外设时钟是否被选中彼此之间都是相互独立的。因此,当需要同时使能多个挂在APB2总线上的外设时钟时,也可以像之前对GPIO端口引脚的操作那样,用“|”将它们“捆绑”起来。
这里对APB2总线上所有外设时钟的定义,实际上相当于给出了一种查找某一个外设挂在哪个外设总线上的方法,当要使能一个外设的时钟而不知道它挂在哪个外设总线上的时候,就可以通过这种方法进行查找。大家可以在如图530所示的这段代码的前后分别看到挂在AHB总线和APB1总线上的外设定义。
然后,在IS_FUNCTIONAL_STATE的右键快捷菜单中选择Go to Definition of命令,程序会跳转到stm32f10x.h文件中的相关代码处,如图531所示。


图531FunctionalState及IS_FUNCTIONAL_STATE(STATE)的定义


很显然,在这里应该选择ENABLE作为RCC_APB2PeriphClockCmd()函数的相应实参。
这样,就通过调用RCC_APB2PeriphClockCmd()函数完成了对GPIOE时钟的使能,如下所示: 


RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);


下面继续来看LED_Init()函数中的代码。在初始化了GPIOE的时钟后,下一步需要初始化本应用实例中用到的相关I/O端口位,这可以通过调用GPIO_Init()函数来实现。关于该函数及其具体应用,在5.3节中有过详细的讲解,此处不再赘述。函数中的相关代码如下所示: 


GPIO_InitTypeDef GPIO_InitStructure; 

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_11 | GPIO_Pin_13 | GPIO_Pin_14;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_Init(GPIOE, &GPIO_InitStructure);


关于这段代码,应注意以下几点: 
第一,局部变量GPIO_InitStructure的定义一定要在函数的最开始,要在刚才讲解的RCC_APB2PeriphClockCmd()函数调用语句的前面; 
第二,因为端口引脚PE9、PE11、PE13和PE14都要被配置为推挽输出工作模式,而且它们来自同一个GPIO端口,所以这里可以通过如下代码来简化编码工作: 


GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_11 | GPIO_Pin_13 | GPIO_Pin_14;


第三,GPIO_Init()函数的第二个形参是GPIO_InitTypeDef的指针类型的,而定义的变量GPIO_InitStructure是GPIO_InitTypeDef类型的,因此,在调用该函数时,应先对GPIO_InitStructure取地址,再将其作为GPIO_Init()函数的第二个实参。
最后,再来看看LED_Init()函数中的最后一条语句: 


GPIO_SetBits(GPIOE, GPIO_Pin_9 | GPIO_Pin_11 | GPIO_Pin_13 | GPIO_Pin_14 );


GPIO_SetBits()函数在5.3节中也曾经介绍过,它的作用就是将相关GPIO端口引脚的输出数据置1,因为本应用实例中的所有LED,都是在其相关引脚输出低电平时亮,高电平时不亮,所以,在这里将它们的输出都设置为高电平,即让它们一开始都不亮。
至此,led.c文件中的所有程序代码就全部讲解完了。
第三步,在main.c文件中添加如下程序代码: 


#include "stm32f10x.h"

#include "led.h"



void Delay(u32 count)

{

while(count--);

}



int main(void)

{

LED_Init();

while(1)

{

GPIO_ResetBits(GPIOE, GPIO_Pin_9);

GPIO_SetBits(GPIOE, GPIO_Pin_11 | GPIO_Pin_13 | GPIO_Pin_14 );

Delay(14400*200);



GPIO_ResetBits(GPIOE, GPIO_Pin_11);

GPIO_SetBits(GPIOE, GPIO_Pin_9 | GPIO_Pin_13 | GPIO_Pin_14 );

Delay(14400*200);



GPIO_ResetBits(GPIOE, GPIO_Pin_13);

GPIO_SetBits(GPIOE, GPIO_Pin_9 | GPIO_Pin_11 | GPIO_Pin_14 );

Delay(14400*200);




GPIO_ResetBits(GPIOE, GPIO_Pin_14);

GPIO_SetBits(GPIOE, GPIO_Pin_9 | GPIO_Pin_11 | GPIO_Pin_13 );

Delay(14400*200);

}

}


这段代码相对来说比较简单。
首先,在文件起始处包含了stm32f10x.h和led.h头文件; 其次,定义了一个很简单的延时函数; 最后,在main()函数中,先调用LED_Init()函数对LED相关的GPIO端口引脚进行相关的初始化,然后在while(1)死循环中,让4个LED——D2、D3、D4和D5依次亮起,且在每个LED亮时其他LED不亮,并且通过延时函数让每个LED亮的状态持续一段固定的时间。要想让某个LED亮,就需要使其相关的引脚输出低电平,这里是通过GPIO_ResetBits()函数来实现的,这个函数在5.3节也曾经介绍过,它的作用是将相关的GPIO端口位清0。最后,需要注意的是,延时函数是必不可少的,而且延时的时间也不能过短,因为人眼能够分辨事物变化的最小时间间隔大约为20ms,所以延时的时间不能小于这个数。经测验,延时函数Delay()的实参大约为14400时,对应的延时时间大约为1ms,就以14400×200作为Delay()函数的实参,这样,延时时间近似为200ms。
至此,本应用例程的编程工作就全部完成了。
4. 例程下载验证
将该例程下载到开发板,来验证它是否实现了流水灯的效果。通过JLINK下载方式将其下载到开发板,并按RESET按键对其复位,可以看到,D2、D3、D4和D5以大约200ms的时间间隔循环往复地不停闪烁,实现了流水灯的效果。


视频


5.4.2按键控制LED 
5.4.1节用GPIO端口的输出工作模式实现了流水灯。本节将用GPIO端口的输入工作模式来实现它的另一个典型应用——按键。

1. 实例描述
本应用实例将会通过按键来控制LED的亮灭,开发板共有4个开关按键和4个LED,编程实现用这4个开关按键分别控制这4个LED的亮灭。即对某一个按键按下一次,它所控制的LED的亮灭状态就改变一次。
2. 硬件电路
本实例相关的硬件电路分别如图532和图533所示。


图532LED相关的硬件电路





图533按键相关的硬件电路


如图532所示的LED相关的硬件电路在5.4.1节已经介绍过,此处不再赘述。
在如图533所示的按键相关的硬件电路中,开关按键S1、S3、S4和S5的一端都接DGND,另一端分别接PC6、PC7、PC8和PC9引脚,且分别通过上拉电阻R34、R82、R87和R88接到3.3V高电平,其中电容C4、C14、C15和C27的作用是按键消抖滤波。当按键未被按下时,相关的引脚被拉高到3.3V高电平; 当按键被按下时,相关的引脚被DGND拉低到低电平。因此,通过检测各开关按键相关引脚的输入电平,就可以判断它们是否被按下。
3. 软件设计 
下面进行本应用实例的软件设计。
因为本应用实例是在流水灯应用实例的基础上完成的,所以,为了讲解方便,就直接在流水灯应用例程上进行修改。对复制的流水灯应用例程的工程文件夹及相关工程文件的名称进行修改,将USER文件夹中的LED.uvprojx和LED.uvoptx分别重命名为KEY加相关的后缀名,并将OBJ文件夹中的所有文件删除。然后,进入工程,将工程的目录名由原来的LED改为KEY,最后在工程选项的OUTPUT标签下,将Name of Executable中的内容由原来的LED改为KEY,这样就完成了对整个工程的重命名操作。
下面正式开始对“按键控制LED”应用例程的实现。
首先,按照在流水灯应用例程实现过程中讲解的方法,在工程文件夹的HARDWARE文件夹中新建一个名为KEY的文件夹,并在其中新建2个文件——key.c和key.h,然后将key.c添加到工程KEY的HARDWARE子文件夹中,将key.h文件的路径添加到工程要包含头文件的路径列表中。
然后,开始编写程序代码。
第一步,在key.h文件中,添加如下程序代码: 


#ifndef __KEY_H

#define __KEY_H

#include "stm32f10x.h"

#include "bitband.h"



#define KEY1 GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_6)

#define KEY2 GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_7)

#define KEY3 GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_8) 

#define KEY4 GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_9) 



void Delay(u32 count);

void KEY_Init(void); 

u8 KEY1_Scan(void);

u8 KEY2_Scan(void);

u8 KEY3_Scan(void);

u8 KEY4_Scan(void);

#endif


这段代码定义了4个宏KEY1、KEY2、KEY3和KEY4,它们分别表示4个按键所对应GPIO端口引脚的输入电平,这是通过调用GPIO_ReadInputDataBit()函数来实现的,这个函数在5.3节中曾经简单介绍过,它的功能就是读取某个GPIO端口引脚上的输入数据。
此外,还声明了延时函数Delay()、按键初始化函数KEY_Init()以及4个按键扫描函数KEY1_Scan()、KEY2_Scan()、KEY3_Scan()和KEY4_Scan()。
第二步,在key.c文件中,添加如下程序代码: 


#include "key.h"



void Delay(u32 count)

{

while(count--);

}



void KEY_Init()

{

GPIO_InitTypeDef GPIO_InitStructure;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; 

GPIO_Init(GPIOE, &GPIO_InitStructure);

}



u8 KEY1_Scan()

{

static u8 key_up1=1;

if(key_up1 && KEY1==0)






{

Delay(14400*10);

if(KEY1==0)

{

key_up1=0;

return 1;

}

}

else if(KEY1==1)

key_up1=1;

return 0;

}



u8 KEY2_Scan()

{

static u8 key_up2=1;

if(key_up2 && KEY2==0)

{

Delay(14400*10);

if(KEY2==0)

{

key_up2=0;

return 1;

}

}

else if(KEY2==1)

key_up2=1;

return 0;

}





u8 KEY3_Scan()

{

static u8 key_up3=1;

if(key_up3 && KEY3==0)

{

Delay(14400*10);

if(KEY3==0)

{

key_up3=0;

return 1;

}

}

else if(KEY3==1)

key_up3=1;

return 0;

}



u8 KEY4_Scan()

{

static u8 key_up4=1;





if(key_up4 && KEY4==0)

{

Delay(14400*10);

if(KEY4==0)

{

key_up4=0;

return 1;

}

}

else if(KEY4==1)

key_up4=1;

return 0;

}


这段代码首先定义了延时函数Delay(),因为在后面的4个按键扫描函数中会用到它,然后定义了按键初始化函数KEY_Init(),它与流水灯实验例程中的KEY_Init()函数相似,此处不再赘述。需要注意的是,这里按键相关的GPIO端口引脚的工作模式应配置为输入上拉模式。
下面重点介绍这4个按键扫描函数,它们的实现过程完全相同,就以KEY1_Scan()为例来对它们进行讲解。
众所周知,按键有支持连续按下操作的,比如计算机的键盘; 也有不支持连续按下操作的,比如电视机遥控器的按键,它在被按下一次后必须经过一次弹起才能进行下一次被按下的操作,否则,即使保持一直被按下的状态,也只能实现按下一次的效果。很显然,这里要实现的按键属于第二种,否则,按下一次相关按键,它所控制的LED会不停地在亮灭状态之间快速转换,因为转换时间太短,甚至看不出它的变化,而只会看到它亮,一直到松开按键,而这时按键的亮灭状态几乎完全是随机的,根本无法控制。
因此,在KEY1_Scan()函数中定义了一个static类型的变量key1_up,它被用来标记当检测到按键S1当前处于被按下状态时,它在此之前是否已经处于被按下状态。当它的值为1时,表明它之前未处于被按下状态,即它是从弹起状态被按下的,则此次按下有效; 当它的值为0时,表明它之前已处于被按下状态,则此次按下无效。因为static类型的变量被存储在静态存储区,所以在KEY1_Scan()函数每次被调用完成之后,key1_up对应的内存不会被释放,因此它具有“记忆”功能,用它可以标记在本次检测前按键S1是否处于被按下状态。需要说明的是,在KEY1_Scan()函数中,对key1_up的初始化操作只会被执行一次,当再次调用该函数时,相关语句不会再次被执行: 


static u8 key_up1=1;


在KEY1_Scan()函数中,当检测到按键S1之前处于未被按下的状态且当前它被按下时,首先需要通过延时函数来消除抖动,如下所示: 


if(key_up1 && KEY1==0)

{

Delay(14400*10);





…

}


这是因为在按键被按下的实际过程中,其相关的引脚并不是从高电平直接变为低电平,而是会经历一个按键抖动的过程,如图534所示。


图534按键抖动过程


按键抖动有前沿抖动和后沿抖动,分别对应按键在被按下和松开的过程中按键的抖动过程。一般来说,按键抖动过程持续的时间为5~10ms。这里用延时函数延时10ms来消除按键在按下过程中的前沿抖动。
在消除按键的抖动后,再判断S1是否被按下,如果是,则将key_up的值清0标记S1已处于被按下状态,然后函数返回1,表明S1被按下,如下所示: 


if(KEY4==0)

{

key_up4=0;

return 1;

}


当S1处于未被按下状态时,需将key_up的值置1,以保证当它被按下时能够及时地检测到,如下所示: 


else if(KEY4==1)

key_up4=1;


最后,函数返回0,则表示未检测到S1被按下,如下所示: 


return 0;


其他几个按键扫描函数的实现过程与KEY1_Scan()函数完全相同,此处不再赘述。
至此,key.c文件中的所有程序代码就全部讲解完了。
第三步,在main.c文件中,将原来的代码删除,添加如下程序代码: 


#include "led.h"

#include "key.h"



int main(void)





{

vu8 key=0;

LED_Init();

KEY_Init();

while(1)

{

if(KEY1_Scan())

{

if(GPIO_ReadOutputDataBit(GPIOE,GPIO_Pin_9))

{

GPIO_ResetBits(GPIOE,GPIO_Pin_9);

}

else

{

GPIO_SetBits(GPIOE,GPIO_Pin_9);

}

}



if(KEY2_Scan())

{

if(GPIO_ReadOutputDataBit(GPIOE,GPIO_Pin_11))

{

GPIO_ResetBits(GPIOE,GPIO_Pin_11);

}

else

{

GPIO_SetBits(GPIOE,GPIO_Pin_11);

}

}



if(KEY3_Scan())

{

if(GPIO_ReadOutputDataBit(GPIOE,GPIO_Pin_13))

{

GPIO_ResetBits(GPIOE,GPIO_Pin_13);

}

else

{

GPIO_SetBits(GPIOE,GPIO_Pin_13);

}

}



if(KEY4_Scan())

{

if(GPIO_ReadOutputDataBit(GPIOE,GPIO_Pin_14))

{

GPIO_ResetBits(GPIOE,GPIO_Pin_14);

}

else

{





GPIO_SetBits(GPIOE,GPIO_Pin_14);

}

}

Delay(14400*10);

}

}


这段代码相对来说比较简单。首先定义一个vu8(volatile u8)类型的变量key,表示它可能会被意想不到地改变。然后分别调用LED_Init()和KEY_Init()函数初始化LED和按键相关的GPIO端口引脚。最后在一个while(1)的死循环中,每隔大约10ms,依次调用4个按键扫描函数来检测相关按键是否被按下,如果有,则将相关按键所对应的LED的亮灭状态改变一次。
这里通过调用GPIO_ReadOutputDataBit()函数来判断相关LED当前的亮灭状态,该函数在5.3节曾经介绍过,它的作用是返回相关GPIO端口引脚的输出数据,它与在key.h文件中调用的GPIO_ReadInputDataBit()函数相似,后者是返回相关GPIO端口引脚的输入数据。然后,根据LED当前的亮灭状态,即相关GPIO端口引脚的输出数据,相应地将它当前的亮灭状态改变一次,这当然是通过GPIO_ResetBits()和GPIO_SetBits()函数来实现的。
至此,本应用例程的编程工作就全部完成了。
4. 例程下载验证
将该例程下载到开发板,来验证它是否实现了按键控制LED的效果。通过JLINK下载方式将其下载到开发板,并按RESET按键对其复位,可以看到,分别通过按下一次按键S1、S3、S4和S5,可以分别将它们所对应的LED,即D2、D3、D4和D5的亮灭状态改变一次,我们的例程实现了按键控制LED的效果。
本章小结
本章介绍STM32中最基本同时也是应用最普遍的外设——GPIO端口,它是STM32与外部进行数据交换的通道,同时,它也是应用STM32内置外设的通道。本章介绍了STM32的GPIO端口的知识, 8种工作模式以及它们各自的工作原理; 介绍了与GPIO端口相关的7个寄存器及库函数以及应用; 通过与GPIO端口相关的两个典型的应用实例,在应用中理解GPIO端口的功能,了解固件库在应用程序开发过程中的强大作用。