GPIO作为嵌入式开发学者学习单片机的开端,很多学者在学习GPIO的知识时也许只是对着书本或者教学视频粗略的过了一遍,而在后续的开发当中对于该模块也是复制粘贴的过程。但是我们如果去仔细的研究GPIO的配置程序,可能会发现并不是那么好理解,并且在配置程序中牵扯到大量C语言的核心知识,这些内容也是相对较难的。本文旨在介绍GPIO的相关介绍以及详细解释STM32单片机开发时GPIO的配置程序,这样对于新手来说可以更好的理解GPIO的使用和编程方法。
- GPIO简介
GPIO(通用输入输出口),是MCU与外部电路和设备连接的基本外设,也就是常说的端口或者管脚。可以配置为8种输入和输出模式,输出模式下可控制端口输出高低电平,用来驱动LED、控制蜂鸣器等。输入模式下可读取端口的高低电平或电压,用来读取按键输入、模拟通信协议接收数据等。
GPIO的基本机构
如上图所示为GPIO的整体框图,每个GPIO有16个引脚。在每个GPIO模块内主要包含了寄存器和驱动器,寄存器是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写操作,以完成输出电平和读取电平的功能,寄存器的每一位对应一个引脚。对输出寄存器置1.对应的引脚就会输出高电平,置0就会输出低电平。对于输入寄存器读取为1,表明对应的端口是高电平否则为低电平。驱动器是用来增加信号的驱动能力的。寄存器负责存储数据,如果要进行点灯操作就需要驱动器来负责增大驱动能力。
通过查阅参考开发手册,我们可以看到GPIO中每一位的具体电路结构。如下图所示:
具体工作原理这里不做描述。
二、GPIO模式
通过配置GPIO的端口配置寄存器,端口可以配置为以下8种模式:
输入浮空 ─ 输入上拉 ─ 输入下拉 ─ 模拟输入 ─ 开漏输出 ─ 推挽式输出 ─ 推挽式复用功能 ─ 开漏复用功能
三、LED和按键的介绍
LED:发光二极管。正向导通,反向截至。长脚为正极,短脚为负极。
硬件电路图如下图所示:
当PA0输出低电平时,LED两端就会产生电压差,就会形成正向导通的电流,此时二极管导通,LED点亮。这里的电阻为限流电阻,主要作用为一方面可以防止LED因为电流过大而烧毁,另一方面可以调整LED的亮度。在做实验时这个限流电阻一般可以省去,但是在设计电路时,这个电阻要加上。
在硬件设计中,LED的点亮还可以使PA0输出高电平,二极管的负极就接GND,但是我们单片机当中使用做多的是低电平驱动的方法。因为很多单片机或者芯片,都使用了高电平弱驱动,低电平强驱动的规则。
按键:在简单的单片机开发中,我们最常用的按键是两脚和四角的按键,这里对按键的结构原理不做描述。在本实验中我们使用四脚按键对LED进行控制。按键接法硬件电路图如下所示:
在这里当按键按下时,PA0被直接下拉到GND,此时读取PA0就是低电平,当按键松手时,PA0被悬空,引脚电压不确定,所以在这种接法下,必须要求PA0是上拉输入的模式,否则就会出现引脚电压不确定的错误现象。如果PA0是上拉输入模式,引脚悬空就是高电平,在这种方式下按下按键,引脚为低电平,松手为高电平。
另一种就是外接上拉电阻的方法,如下图所示:
这个上拉电阻就可以想象为一个弹簧,当按键松手时,引脚由于上拉作用自然保持为高电平。当按键按下时,引脚直接拉到GND,也就是有一股无穷大的力量把引脚往下拉,弹簧阻挡不了无穷大的力,此时引脚为低电平,这种情况下引脚不会出现悬空状态。所以此时PA0可以配置为浮空输入或者上拉输入。上拉输入就相当于内外两个上拉电阻共同作用对应高电平,这样就会使得引脚对应高电平更稳一点,而强制拉到低时损耗也就会大一些。一般也不建议使用。一般的单片机由于不一定具有下拉输入的模式,所以不建议使用下拉电阻的接法。
四、实验部分:
1.LED的闪烁 :编写程序,实现让连接在STM32单片机上的1个管脚上的发光二级管闪烁。
实验具体步骤:操作STM32的GPIO总共需要三个步骤,
第一步使用RCC开启GPIO的时钟;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
第二步使用GPIO_Init函数初始化GPIO;这个函数的作用是用结构体的参数来初始化GPIO口。我们需要先定义一个结构体变量,然后再给结构体赋值,最后调用这个函数,这个函数内部就会自动读取结构体的值,然后自动把外设的各个参数配置好。
调用GPIO_Init(GPIOA,);
点击函数跳转定义,第一个参数选择GPIOA,第二个参数是一个GPIO_InitTypeDef结构体,先把结构体类型复制下来,在GPIO_Init上面粘贴,起个名字叫GPIO_InitStructure,即
GPIO_InitTypeDef GPIO_InitStructure;
这里的这个结构体实际上也是一种局部变量。复制结构体的名字,用.把结构体成员都引出来,
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
最后把GPIO初始化结构体的地址放到GPIO_Init的第二个参数
GPIO_Init(GPIOA,&GPIO_InitStructure);
这样初始化就完成了,当初始化函数GPIO_Init执行完这个引脚GPIO_Pin_0就自动被配置为推挽输出、50MHz的速度了。它内部的主要执行逻辑就是读取结构体的参数,执行判断和运算,最后写入到GPIO配置寄存器。
第三步使用输出或者输入函数控制GPIO口。
GPIO_SetBits(GPIOA,GPIO_Pin_0);
Delay_ms(500);
GPIO_ResetBits(GPIOA,GPIO_Pin_0);
Delay_ms(500);
GPIO_SetBits(GPIOA,GPIO_Pin_0);
整个程序如下:
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
/*GPIO的初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
while(1)
{
GPIO_SetBits(GPIOA,GPIO_Pin_0);
Delay_ms(500);
GPIO_ResetBits(GPIOA,GPIO_Pin_0);
Delay_ms(500);
GPIO_SetBits(GPIOA,GPIO_Pin_0);
}
}
- 按键控制LED的翻转
按键是一种常见的输入设备,按下导通,松手断开。由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动。
按键在按下的瞬间信号由高电平变为低电平,就会来回的抖几下,这个抖动会比较快,通常在5-10ms,虽然时间很短但是单片机是高速运行的一种设备,5-10ms时间还是很长的,所以就要对这个抖动进行过滤,否则就会出现按键按了一下,单片机却反应了很多次的现象。在按键松手的时候也会有一段时间的抖动,这个在程序当中也同样需要过滤。过滤的方法就是加一段延时,把抖动时间耗过去就可以了。
按键控制LED翻转程序如下所示:
Key.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
if (GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_3) == 0)
{
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_3) == 0);
Delay_ms(20);
KeyNum = 1;
}
return KeyNum;
}
LED.c
#include "stm32f10x.h" // Device header
void LED_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
/*GPIO的初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
void LED1_Turn(void)
{
if (GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_0) == 0)
{
GPIO_SetBits(GPIOA,GPIO_Pin_0);
}
else
{
GPIO_ResetBits(GPIOA,GPIO_Pin_0);
}
}
main.c
#include "stm32f10x.h" // Device header
#include "LED.h"
#include "Key.h"
#include "Delay.h"
uint8_t KeyNum;
int main(void)
{
/*开启时钟*/
LED_Init();
Key_Init();
while(1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
LED1_Turn();
}
}
}
在这里我们可能会发现GPIO的配置也就不过如此,但是你真的懂这段程序的写法吗?
我们不妨回忆一下C语言的知识:
C语言中的宏定义#define
宏定义主要用于用字符串代替一个数字,便于开发者理解。如在程序中用1代表高电平,0代表低电平。但是如果用数字1代表上拉输入,2代表下拉输入等。这个时候用数字表示就显得特别麻烦且不容易理解。此时我们就可以用宏定义将这些数据参数映射到一个字符串上就比较容易理解了。其次宏定义可以提取程序中经常出现的参数,方便开发人员快速修改。这个也是我们开发最为常见的,比如在开发过程中有10多个GPIO_Pin_0,而这个Pin是需要经常去修改的,如果一个一个去改就会显得特别麻烦,影响开发效率。我们就可以使用一个字符串来代替个GPIO_Pin_0,在需要修改的时候,只需要修改一下定义即可。
宏定义使用如下:
定义宏定义:#define ABC 123 这里指的是使用字符串ABC代替123这个参数
引用宏定义:int a = ABC; 这里就相当于int a = 123
在单片机开发程序中如:GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;点击跳转
#define GPIO_Pin_3 ((uint16_t)0x0008) /*!< Pin 3 selected */
此时GPIO_Pin_3替换的是0x0008这个数据。0x0008很不容易理解,所以就用宏定义改一下名字就叫GPIO_Pin_3。
C语言typedef
它的用途就和宏定义的差不多,它是将一个比较长的变量类型换个名字,便于使用
定义:typedef unsigned char uint8_t;
引用:uint8_t a;
这里将新名字定义在右边。uint8_t a就等效于unsigned char a;
注意宏定义任何名字都可以换,而typedef只能给变量类型换名字。
如在STM32程序中:uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
点击跳转:typedef unsigned char uint8_t;
C语言结构体
关键字:struct,要理解库函数的运作逻辑,理解结构体是很重要的。
结构体也是一种数据类型,比如char,short,int等这些我们可以称为基本数据类型,数组就是一大堆基本数据类型的集合,如int a[10];就是10个int数据类型的集合。对于数组组合只能是相同的数据,那么如果我们想要组合不同数据类型的该怎么做?那么这里就体现出C语言结构体的作用了。结构体也是一种组合数据类型,它的作用就是组合不同的数据类型。
如:struct{char x;int y; float z;} c;
这句程序的意思就是定义一个结构体变量,名字叫c,其中包含了char型的x,int的y,float型的z三个子项,接下来就是要引用了。
c.x = ‘A’;
c.y = 66;
我们在C语言编译器当中对它进行编译,如下图所示:
接下来就进一步说明结构体的用法,对于上面的结构体它的名字太长了,如果还想再定义一个结构体就得复制一长串
struct{char x;int y; float z;} c;
struct{char x;int y; float z;} d;
那么我们为了改变这种写法,typedef的作用就表现出来了:
typedef struct{char x;int y; float z;} StructName_t;//给struct{char x;int y; float z;} 命名一个新的名字StructName_t,当有这个新名字之后,以后我们再定义这个结构体,就可以用StructName_t来换掉原来那一长串。如下图所示:
这里的 struct{char x;int y; float z;},是一个结构体的数据类型,typedef将结构体换了个名字,叫StructName_t。
StructName_t c;
StructName_t d;
这个就是结构体的数据类型,然后后面跟的就是结构体变量的名字。直接使用结构体变量的名字用.引出结构体成员的数据,这样就可以进行数据的写入和读取了。对于数据而言,一个就是定义数据和另一个就是引用数据。
那么现在我们来看结构图在库函数中的用法:
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitTypeDef GPIO_InitStructure;这句代码的意思就是定义了一个结构体类型的数据,数据名称叫 GPIO_InitStructure,GPIO_InitTypeDef是结构体类型名称。点击跳转
struct包括花括号里面的就是结构体数据类型,用typedef将这个类型换一个新名字叫GPIO_InitTypeDef,然后再引用结构体成员。
在引用结构体成员时,也可以使用结构体指针的引用方式。如:pStructName-> ‘A’;
这样是因为结构体作为一种组合数据类型,在函数之间的数据传递中通常用的是地址传递而不是值传递。使用地址传递,子函数得到的就是结构体的首地址,这时就可以用->运算符快速的引用结构体成员。
在STM32程序中GPIO_InitTypeDef GPIO_InitStructure;这个结构体变量GPIO_InitStructure在传递给GPIO_Init函数时,传递的是结构体的地址。
对应GPIO_Init函数中,GPIO_InitTypeDef* GPIO_InitStruct就是用结构体指针来接收的,里面再引用结构体成员时,就直接使用->这个符号来引用。
那么现在我们就可以发现这个结构体就是将数据打包的过程。首先将参数写到结构体的这三个变量里,然后打包,将结构体传递到函数里,在函数里面,再把结构体拆包,读取变量。
这样做的原因就是当函数需要多个参数时,参数数量较多的情况下不方便管理,所以这里就使用了结构体的传参方式。
那么以上就是所有的内容了,看完之后你理解了吗?还认为配置GPIO的程序的简单吗?