stm32--第一章GPIO
0 简介
首先STM32 的IO 口可以由软件配置成如下8 种模式:
1、输入浮空
2、输入上拉
3、输入下拉
4、模拟输入
5、开漏输出
6、推挽输出
7、推挽式复用功能
8、开漏复用功能
STM32的每个 IO 端口都有 7 个寄存器来控制。他们分别是:配置模式的 2 个 32 位的端口配置寄存器 CRL 和 CRH。 2 个 32 位的数据寄存器 IDR 和 ODR 。1 个 32 位的置位/复位寄存器BSRR ;一个 16 位的复位寄存器 BRR ;1 个 32 位的 锁存 寄存器 LCKR 。如果想要了解每个寄存器的详细使用方法,可以参考 《 STM32 中文参考手册 V10 》 P105~P129。
操作GPIO的步骤为:
- 1 使能 IO 口时钟。调用函数为 RCC_APB2PeriphClockCmd 。
- 2 初始化 IO 参数,即配置端口。调用函数 GPIO_Init();
- 3 操作 IO 。操作 IO 的方法就是上面我们讲解的方法。
1 端口模式配置
1.1 寄存器CRL和CRH
CRL和 CRH 控制着每个 IO 口的模式及输出速率。
下图是CRL寄存器的具体位说明:
从上图可以得出: STM32 的 CRL 控制着每组 IO 端口( A~G )的低 8 位的模式。每个 IO 端口的位占用 CRL 的 4 个位,高两位为 CNF ,低两位为 MODE 。
具体寄存器的值对应的模式如下图所示:
这里我们可以记住几个常用的配置,比如 0X0 表示模拟输入模式( ADC 用)、 0X3 表示推挽输出模式(做输出口用,50M 速率)、 0X8 表 示上 下拉输入模式(做输入口用)、 0XB 表示复用输出(使用 IO 口的第二功能, 50M 速率)。
CRH的作用和 CRL 完全一样,只是 CRL 控制的是低 8 位输出口,而 CRH 控制的是高 8位输出口。这里我们对 CRH 就不做详细介绍了。
1.2 操作CRL和CRL——GPIO_Init()
GPIO 相关的函数和定义分布在固件库文件 stm32f10x_gpio.c 和头文件stm32f10x_gpio.h 文件中。
1.2.1 函数介绍
在固件库开发中,操作寄存器 CRH 和 CRL 来 配置 IO 口的模式和速度是通过 GPIO 初始化函数完成:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
这个函数有两个参数:
- 第一个参数用来指定GPIO,取值范围为GPIOA~GPIOG。
- 第二个是 GPIO_InitTypeDef 类型的指针,这个结构体的定义如下:
typedef struct
{
uint16_t GPIO_Pin;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode_TypeDef GPIO_Mode;
}GPIO_InitTypeDef;
1.2.2 参数说明和例子
具体的用法一般是(例子):
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED0 ---->PB.5 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 速度 50 MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); 根据设定参数配置 GPIO
上面代码的意思是设置GPIOB 的第 5 个端口为推 挽输出模式,同时速度为 50M。
从上面初始化代码可以看出,结构体 GPIO_InitStructure 的第一个成员变量GPIO_Pin 用来设置是要初始化哪个或者哪些 IO 口;
第二个成员变量 GPIO_Mode 是用来设置对应 IO 端口的输出输入模式,这些模式是上面我们讲解的 8 个模式, 在 MDK 中是通过一个枚举类型定义的:
typedef enum
{
GPIO_Mode_AIN = 模拟输入
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;
第三个参数是IO 口速度设置, 有三个可选值,在 MDK 中同样是通过枚举类型定义:
typedef enum
{
GPIO_Speed_10MHz = 1,
GPIO_Speed_2MHz,
GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;
2 端口数据读取
2.1 IDR-端口输入数据寄存器
IDR是一个端口输入数据寄存器,只用了低 16 位。该寄存器为只读寄存器,并且只能以16 位的形式读出。该寄存器各位的描述如下图 :
要想知道某个IO 口的 电平状态,你只要读这个寄存器,再看某个位的状态就可以了。使用起来是比较简单的。
2.2 操作IDR(读IDR)——GPIO_ReadInputDataBit()函数
在固件库中操作 IDR 寄存器读取 IO 端口数据是通过 GPIO_ReadInputDataBit 函数实现的:
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
比如我要读GPIOA.5 的 电平 状态, 那么方法是:
GPIO_ReadInputDataBit(GPIOA , GPIO_Pin _5);
返回值是1( Bit_SET 或者 0( Bit_RESET)。
3 向端口写数据
3.1 ODR-端口数据输出寄存器
ODR是一个端口输出数据寄存器,也只用了低 16 位。该寄存器为可读写,从该寄存器读出来的数据可以用于判断当前 IO 口的输出状态。而向该寄存器写数据,则可以控制某个 IO 口的输出电平。该寄存器的各位描述如下图所示:
3.2 操作ODR——GPIO_Write函数
在固件库中设置 ODR 寄存器的值来控制 IO 口的输出状态是通过函数 GPIO_Write来实现的:
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
该函数一般用来往 一次性 一个 GPIO 的多个端口设值 。
4 端口置位与复位
4.1 BSRR和BRR寄存器
BSRR寄存器是端口位设置 清除 寄存器。 该寄存器和 ODR 寄存器具有类似的作用,都可以用来设置 GPIO 端口的输出位是 1 还是 0 。下面我们看看该寄存器的描述如下图:
也就是说,向BSRR的高16位写1,可以使对应的端口输出1。向BSRR的低16位写1,可以使对应的端口输出0。该寄存器往相应位写0 是无影响的,所以我们要设置某些位,我们不用管其他位的值。
例如你要设置 GPIOA 的第 1 个端口值为 1 ,那么你只需要往寄存器 BSRR 的 低 16 位 对应位 写 1 即可:
GPIOA->BSRR = 1<<1;
如果你要设置GPIOA 的第 1 个端口值为 0 你只需要往寄存器高 16 位对应为写 1 即可:
GPIOA->BSRR=1<<(16 +1);
BRR 寄存器 是端口位清除寄存器。该寄存器的作用跟 BSRR 的高 16 位雷同 ,这里就不做详细讲解。
4.2 端口置位与复位相关函数
在 STM32 固件库中 通过 BSRR 和 BRR 寄存器设置 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 端口的输入和输出状态。 比如我们要设置 GPIOB.5 输出 1 ,那么方法为:
GPIO_SetBits( GPIOB , GPIO_Pin _5);
反之如果要设置GPIOB.5 输出位 0 ,方法为:
GPIO_ResetBits(GPIOB , GPIO_Pin _5);
5 案例一——跑马灯
硬件
将端口电平拉低即可电量LED。
软件部分
跑马灯实验我们主要用到的固件库文件是:
stm32f10x_gpio.c /stm32f10x_gpio.h
stm32f10x_rcc.c/stm32f10x_rcc.h
misc.c/ misc.h
stm32f10x_usart stm32f10x_us art.h
其中stm32f10x_rcc.h 头文件在每个实验中都要引入,因为系统时钟配置函数以及相关的外设时钟使能函数都在这个其源文件stm32f10x_rcc.c 中。 stm32f10x_usart.h 和 misc.h 头文件在我们SYSTEM 文件夹中都需要使用到,所以每个实验都会引用。
注:自己编写的是main.c文件和HARDWARE文件夹下的文件以及SYSTEM文件夹下的文件。其中SYSTEM文件夹可以用现有模板,因为所有工程项目使用他们的时候都是一样的,里面实现了一些比如延时函数等的常用功能。根据自己编写的文件中用到的模块,在FWLIB中放入相应的固件库即可。
工程如下图所示:
代码如下:
//led.c
#include "led.h"
// 初始化PB5和PE5位输出口,并使能这两个时钟
//LED IO初始化
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_5); //PB.5 输出高
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_SetBits(GPIOE,GPIO_Pin_5); //PE.5 输出高
}
// led.h
#ifndef __LED_H
#define __LED_H
#include "sys.h"
#define LED0 PBout(5)// PB5
#define LED1 PEout(5)// PE5
void LED_Init(void);//³õʼ»¯
#endif
// main.c
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
int main(void)
{
delay_init(); //延时函数初始化
LED_Init(); //初始化与LED连接的GPIO端口
while(1)
{
LED0=0;
LED1=1;
delay_ms(300); //延时300ms
LED0=1;
LED1=0;
delay_ms(300); //延时300ms
}
}
/**
******下面注释的代码是通过调用库函数来实现IO控制的方法******
int main(void)
{
delay_init();
LED_Init();
while(1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_5); //LED0对应引脚GPIOB.5拉低,亮,等同于LED0=0;
GPIO_SetBits(GPIOE,GPIO_Pin_5); //LED1对应引脚GPIOE.5拉高,灭,等同于LED1=1;
delay_ms(300);
GPIO_SetBits(GPIOB,GPIO_Pin_5); //LED0对应引脚GPIOB.5拉高,灭,等同于LED0=1;
GPIO_ResetBits(GPIOE,GPIO_Pin_5); //LED1对应引脚GPIOE.5拉低,亮,等同于LED1=0;
delay_ms(300);
}
}
6 案例二——按键实验
在这个案例中,我们将利用板载的 4 个按键来控制板载的两个 LED 的亮灭和蜂鸣器的开关。
目的:上面已经熟悉了向端口写,这里是为了熟悉读端口状态。
6.1 实验简介
我们将通过 ALIENTEK 战舰 STM32 开发板上载有的 4 个按钮( WK_UP 、 KEY0 、KEY1 和 KEY2 ),来控制板上的 2 个 LED DS0 和 DS1 和蜂鸣器 ,其中 WK_UP 控制 蜂鸣器,按一次叫,再按一次停; KEY 2 控制 DS 0 按一次亮,再按一次灭; KEY1 控制 DS1 ,效果同KEY2 。KEY 0 则 同时控制 DS0 和 DS1 ,按一次,他们的状态就翻转一次。
6.2 硬件
LED的连接在上个案例,下面是按键和蜂鸣器的连接。其中端口PB8输出高电平,则蜂鸣器响。我们是读按键端口,写LED和蜂鸣器端口。
这里需要注意的是KEY0 、 KEY1 和 KEY2 是低电平有效的,而 WK_UP 是高电平有效的,并且外部都没有上下拉电阻,所以,需要在 STM32 内部设置上下拉。
在上图中解释了上拉输入、下拉输入和浮空输入。GPIO1是上拉输入,默认是高电平,只有在外部输入是低电平时才有效。GPIO2是下拉输入,默认是低电平,只有在外部输入是高电平时才有效。
6.3 软件设计
只需自己编写led.c,beep.c以及key.c和main.c即可
自定义led.c、beep.c 、以及key.c。这三个自定义文件都是只调用GPIO固件库,所以FWLib和上面案例一样。
1 key.c和key.h
// key.c
#include "stm32f10x.h"
#include "key.h"
#include "sys.h"
#include "delay.h"
//按键初始化函数
void KEY_Init(void) //IO初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);//使能GPIOA和GPIOE
//因为按键模块使用了两个GPIO,在key.c中只需使能他自己的即可,LED和蜂鸣器的使能在他们自己的.c文件中。
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;//KEY0-KEY2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIOE2,3,4
//初始化WK_UP-->GPIOA.0 设置成下拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0设置为输入,下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0
}
//按键处理函数
//返回按键值
//mode:0,不支持连续按;1,支持连续按;
//0,没有任何按键按下
//1,KEY0按下
//2,KEY1按下
//3,KEY2按下
//4,KEY3按下,WK_UP
//注意此函数有响应优先级 ,KEY0>KEY1>KEY2>KEY3
u8 KEY_Scan(u8 mode)
{
static u8 key_up=1;//按键松开标志
if(mode)key_up=1; //支持连续按
if(key_up&&(KEY0==0||KEY1==0||KEY2==0||WK_UP==1))
{
delay_ms(10);//去抖动
key_up=0;
if(KEY0==0)return KEY0_PRES;
else if(KEY1==0)return KEY1_PRES;
else if(KEY2==0)return KEY2_PRES;
else if(WK_UP==1)return WKUP_PRES;
}else if(KEY0==1&&KEY1==1&&KEY2==1&&WK_UP==0)key_up=1;
return 0;// 无按键按下
}
KEY_Scan函数,则是用来扫描这 4 个 IO 口是否有按键按下。 KEY_Scan 函数, 支持两
种扫描方式,通过 mode 参数来设置。当mode 为 0 的时候, KEY_Scan 函数将不支持连续按, 扫描某个按键,该按键按下之后必须要松开,才能第二次触发,否则不会再响应这个按键,这样的好处就是可以防止按一次多次触发,而坏处就是在需要长按的时候比较不合适。当mode 为 1 的时候, KEY_Scan 函数将支持连续按,如果某个按键一直按下,则 会一直返回这个按键的键值,这样可以方便的实现长按检测。有了mode 这个参数,大家就可以根据自己的需要,选择不同的方式。这里要提醒大家,因为该函数里面有 static 变量,所以该函数不是一个可重入函数,在有 OS 的情况下,这个大家要留意下。 同时还有一点要注意的就是,该函数的按键扫描是有优先级的,最优先的是 KEY0第二优先的是 KEY1 接着 KEY2 最后是 WK_UP 按键 。该函数有返回值,如果有按键按下,则返回非 0 值,如果没有或者按键不正确,则返回 0 。
#ifndef __KEY_H
#define __KEY_H
#include "sys.h"
//注释掉的是另外一种方式-位带操作,但是还是使用库函数简单易懂
//#define KEY0 PEin(4) //PE4
//#define KEY1 PEin(3) //PE3
//#define KEY2 PEin(2) //PE2
//#define WK_UP PAin(0) //PA0 WK_UP
#define KEY0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)//读取按键0
#define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)//读取按键1
#define KEY2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2)//读取按键2
#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键3(WK_UP)
#define KEY0_PRES 1 //KEY0按下
#define KEY1_PRES 2 //KEY1按下
#define KEY2_PRES 3 //KEY2按下
#define WKUP_PRES 4 //KEY_UP按下(即WK_UP/KEY_UP)
void KEY_Init(void);//IO初始化
u8 KEY_Scan(u8); //扫描按键函数
#endif
2 beep.c和beep.h
这个简单,同LED的驱动
//beep.c
//初始化PB8为输出口,并使能这个口的时钟
//蜂鸣器初始化
void BEEP_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能控制蜂鸣器的端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //BEEP-->PB.8 端口配置
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_8);//输出0,关闭蜂鸣器输出。
}
// beep.h
#ifndef __BEEP_H
#define __BEEP_H
#include "sys.h"
#define BEEP PBout(8) // BEEP,蜂鸣器接口
void BEEP_Init(void); // 初始化
#endif
3 main.c
#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "beep.h"
int main(void)
{
vu8 key=0;
delay_init(); //延时函数初始化
LED_Init(); //LED端口初始化
KEY_Init(); //初始化与按键连接的硬件接口
BEEP_Init(); //初始化蜂鸣器端口
LED0=0; //先点亮红灯
while(1)
{
key=KEY_Scan(0); //得到按键值
if(key)
{
switch(key)
{
case WKUP_PRES: //控制蜂鸣器
BEEP=!BEEP;
break;
case KEY2_PRES: //控制LED0反转
LED0=!LED0;
break;
case KEY1_PRES: //控制LED1反转
LED1=!LED1;
break;
case KEY0_PRES: //同时控制LED0和LED1反转
LED0=!LED0;
LED1=!LED1;
break;
}
}else delay_ms(10);
}
}