第5章 STM32 GPIO 本章讲述STM32 GPIO,包括STM32 GPIO接口概述、STM32 GPIO功能、STM32的GPIO常用库函数、 STM32的GPIO使用流程、STM32 GPIO输出应用实例和STM32 GPIO输入应用实例。 5.1STM32 GPIO接口概述 General Purpose Input Output(通用输入输出,GPIO)接口的功能是让嵌入式处理器能够通过软件灵活地读出或控制单个物理引脚上的高、低电平,实现内核和外部系统之间的信息交换。GPIO是嵌入式处理器使用最多的外设,能够充分利用其通用性和灵活性,是嵌入式开发者必须掌握的重要技能。作为输入时,GPIO可以接收来自外部的开关量信号、脉冲信号等,如来自键盘、拨码开关的信号; 作为输出时,GPIO可以将内部的数据传输给外部设备或模块,如输出到LED、数码管、控制继电器等。另外,理论上讲,当嵌入式处理器上没有足够的外设时,可以通过软件控制GPIO模拟UART、SPI、I2C、FSMC等各种外设的功能。 正是因为GPIO作为外设具有无与伦比的重要性,除特殊功能的引脚外,STM32所有引脚都可以作为GPIO使用。以常见的LQFP144封装的STM32F407ZGT6为例,有112个引脚可以作为双向I/O使用。为便于使用和记忆,STM32将它们分配到不同的“组”中,在每个组中再对其进行编号。具体来讲,每个组称为一个端口,端口号通常以大写字母命名,从A开始,依次简写为PA、PB、PC等。每个端口最多有16个GPIO,软件既可以读写单个GPIO,也可以通过指令一次读写端口中全部16个GPIO。每个端口内部的16个GPIO又被分别标以0~15的编号,从而可以通过PA0、PB5或PC10等方式指代单个GPIO。以STM32F407ZGT6为例,它共有7个端口(PA、PB、PC、PD、PE、PF和PG),每个端口有16个GPIO,共7×16=112个GPIO。 绝大多数嵌入式系统应用都涉及开关量的输入和输出功能,如状态指示、报警输出、继电器闭合和断开、按钮状态读入、开关量报警信息的输入等。这些开关量的输入和控制输出都可以通过GPIO实现。 GPIO的每个位都可以由软件分别配置成以下模式。 (1) 输入浮空。浮空(Floating)就是逻辑器件的输入引脚既不接高电平,也不接低电平。由于逻辑器件的内部结构,当输入引脚浮空时,相当于该引脚接了高电平。一般实际运用时,不建议引脚浮空,易受干扰。 (2) 输入上拉。上拉就是把电压拉高,如拉到VCC。上拉就是将不确定的信号通过一个电阻钳位在高电平。电阻同时起限流作用。弱强只是上拉电阻的阻值不同,没有什么严格区分。 (3) 输入下拉。下拉就是把电压拉低到GND。与上拉原理相似。 (4) 模拟输入。模拟输入是指传统方式的模拟量输入。数字输入是输入数字信号,即0和1的二进制数字信号。 (5) 具有上拉/下拉功能的开漏输出模式。输出端相当于三极管的集电极。要得到高电平状态,需要上拉电阻才行。 该模式适合做电流型的驱动,其吸收电流的能力相对较强(一般在20mA以内)。 (6) 具有上拉/下拉功能的推挽输出模式。可以输出高低电平,连接数字器件。推挽结构一般是指两个三极管分别受两个互补信号的控制,总是在一个三极管导通时另一个截止。 (7) 具有上拉/下拉功能的复用功能推挽模式。复用功能可以理解为GPIO被用作第二功能时的配置情况(即并非作为通用I/O接口使用)。STM32 GPIO 的复用功能推挽 模式中输出使能、输出速度可配置。这种复用模式可工作在开漏及推挽模式,但是输出信号是源于其他外设的,这时的输出数据寄存器 GPIOx_ODR 是无效的; 而且输入可用,通过输入数据寄存器可获取 I/O 接口实际状态,但一般直接用外设的寄存器获取该数据信号。 (8) 具有上拉/下拉功能的复用功能开漏模式。复用功能可以理解为GPIO接口被用作第二功能时的配置情况(即并非作为通用I/O接口使用)。每个I/O接口可以自由编程,而I/O接口寄存器必须按32位字访问(不允许半字或字节访问)。GPIOx_BSRR和GPIOx_BRR寄存器允许对任何GPIO寄存器的读/更改的独立访问,这样,在读和更改访问之间产生中断(IRQ)时不会发生危险。 每个GPIO端口包括4个32位配置寄存器(GPIOx_MODER、GPIOx_OTYPER、GPIOx_OSPEEDR和GPIOx_PUPDR)、两个32位数据寄存器(GPIOx_IDR和GPIOx_ODR)、一个32位置位/复位寄存器(GPIOx_BSRR)、一个32位配置锁存寄存器(GPIOx_LCKR)和两个32位复用功能选择寄存器(GPIOx_AFRH和GPIOx_AFRL)。应用程序通过对这些寄存器的操作实现GPIO的配置和应用。 一个I/O接口的基本结构如图51所示。 图51一个I/O接口的基本结构 STM32的GPIO资源非常丰富,包括26、37、51、80、112个多功能双向5V兼容的快速I/O接口,而且所有I/O接口可以映射到16个外部中断,对于STM32的学习,应该从最基本的GPIO开始。 GPIO的每个位可以由软件分别配置成多种模式。 常用的I/O接口寄存器只有4个: CRL、CRH、IDR、ODR。CRL和CRH寄存器控制每个I/O接口的模式及输出速率。 每个GPIO引脚都可以由软件配置成输出(推挽或开漏)、输入(带或不带上拉或下拉)或复用的外设功能端口。多数GPIO引脚都与数字或模拟的复用外设共用。除了具有模拟输入功能的端口,所有GPIO引脚都有大电流通过能力。 根据数据手册中列出的每个I/O接口的特定硬件特征,GPIO的每个位可以由软件分别配置成多种模式: 输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽式输出、推挽式复用功能、开漏复用功能。 5.1.1输入通道 输入通道包括输入数据寄存器和输入驱动器。在接近I/O引脚处连接了两只保护二极管,假设保护二极管的导通电压降为Vd,则输入输入驱动器的信号电压范围被钳位在 VSS-VdBSRRL=i;}//设置为高电平 #define digitalLo(p,i){p->BSRRH=i;}  //输出低电平 #define digitalToggle(p,i){p->ODR ^=i;} //输出反转状态 /* 定义控制I/O的宏 */ #define LED1_TOGGLEdigitalToggle(LED1_GPIO_PORT,LED1_PIN) #define LED1_OFFdigitalHi(LED1_GPIO_PORT,LED1_PIN) #define LED1_ONdigitalLo(LED1_GPIO_PORT,LED1_PIN) #define LED2_TOGGLEdigitalToggle(LED2_GPIO_PORT,LED2_PIN) #define LED2_OFFdigitalHi(LED2_GPIO_PORT,LED2_PIN) #define LED2_ONdigitalLo(LED2_GPIO_PORT,LED2_PIN) #define LED3_TOGGLEdigitalToggle(LED3_GPIO_PORT,LED3_PIN) #define LED3_OFFdigitalHi(LED3_GPIO_PORT,LED3_PIN) #define LED3_ONdigitalLo(LED3_GPIO_PORT,LED3_PIN) /* 基本混色,后面高级用法使用PWM可混出全彩颜色且效果更好 */ //红 #define LED_RED\ LED1_ON;\ LED2_OFF;\ LED3_OFF //绿 #define LED_GREEN\ LED1_OFF;\ LED2_ON;\ LED3_OFF //蓝 #define LED_BLUE\ LED1_OFF;\ LED2_OFF;\ LED3_ON //黄(红+绿) #define LED_YELLOW\ LED1_ON;\ LED2_ON;\ LED3_OFF //紫(红+蓝) #define LED_PURPLE\ LED1_ON;\ LED2_OFF;\ LED3_ON //青(绿+蓝) #define LED_CYAN \ LED1_OFF;\ LED2_ON;\ LED3_ON //白(红+绿+蓝) #define LED_WHITE\ LED1_ON;\ LED2_ON;\ LED3_ON //黑(全部关闭) #define LED_RGBOFF\ LED1_OFF;\ LED2_OFF;\ LED3_OFF void LED_GPIO_Config(void); #endif /* __LED_H */ 这部分宏控制LED亮灭的操作是直接向BSRR、BRR和ODR这3个寄存器写入控制指令实现的 。对BSRR寄存器写1输出高电平,对BRR寄存器写1输出低电平,对ODR 寄存器某位进行异或操作可反转位的状态。 RGB彩灯可以实现混色。 上述代码中的\是C语言中的续行符语法,表示续行符的下一行与续行符所在的代码是同一行。因为代码中宏定义关键字 #define只对当前行有效,所以使用续行符连接起来。以下的代码是等效的。 #define LED_YELLOW LED1_ON; LED2_ON; LED3_OFF 应用续行符时要注意,在\后面不能有任何字符(包括注释、空格),只能直换行。 2. bsp_led.c源文件 #include "./led/bsp_led.h" /* * @brief初始化控制LED的I/O * @param无 * @retval 无 */ void LED_GPIO_Config(void) { /*定义一个GPIO_InitTypeDef类型的结构体*/ GPIO_InitTypeDef GPIO_InitStructure; /*开启LED相关的GPIO外设时钟*/ RCC_AHB1PeriphClockCmd ( LED1_GPIO_CLK|LED2_GPIO_CLK| LED3_GPIO_CLK, ENABLE); /*选择要控制的GPIO引脚*/ GPIO_InitStructure.GPIO_Pin = LED1_PIN; /*设置引脚模式为输出模式*/ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; /*设置引脚的输出类型为推挽输出*/ GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; /*设置引脚为上拉模式*/ GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; /*设置引脚速率为2MHz */ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; /*调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO*/ GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure); /*选择要控制的GPIO引脚*/ GPIO_InitStructure.GPIO_Pin = LED2_PIN; GPIO_Init(LED2_GPIO_PORT, &GPIO_InitStructure); /*选择要控制的GPIO引脚*/ GPIO_InitStructure.GPIO_Pin = LED3_PIN; GPIO_Init(LED3_GPIO_PORT, &GPIO_InitStructure); /*关闭RGB灯*/ LED_RGBOFF; } 初始化 GPIO 端口时钟时采用了 STM32 库函数,函数执行流程如下。 (1) 使用GPIO_InitTypeDef定义GPIO初始化结构体变量,以便下面用于存储GPIO配置。 (2) 调用库函数 RCC_AHB1PeriphClockCmd()使能 LED 灯的 GPIO 端口时钟,如果是直接向 RCC 寄存器赋值使能时钟的,不如这样直观。该函数有两个输入参数,第1个参数用于指示要配置的时钟,如本例中的 RCC_AHB1Periph_GPIOH和RCC_AHB1Periph_GPIOD,应用时使用|操作同时配置3个LED的时钟; 函数的第2个参数用于设置状态,可输入DISABLE关闭或ENABLE使能时钟。 (3) 向GPIO初始化结构体赋值,把引脚初始化成推挽输出模式,其中的GPIO_Pin使用宏LEDx_PIN赋值,使函数的实现方便移植。 (4) 使用以上初始化结构体的配置,调用GPIO_Init()函数向寄存器写入参数,完成GPIO的初始化,这里的GPIO端口使用 宏LEDx_GPIO_PORT赋值,也是为了程序移植方便。 (5) 使用同样的初始化结构体,只修改控制的引脚和端口,初始化其他LED使用的GPIO引脚。 (6) 使用宏控制RGB灯默认关闭。 编写完LED的控制函数后,就可以在main函数中测试了。 3. main.c文件 #include "stm32f4xx.h" #include "./led/bsp_led.h" void Delay(__IO u32 nCount); /* * @brief主函数 * @param无 * @retval 无 */ int main(void) { /* LED 端口初始化 */ LED_GPIO_Config(); /* 控制LED灯 */ while (1) { LED1( ON ); // 亮 Delay(0xFFFFFF); LED1( OFF );// 灭 LED2( ON ); // 亮 Delay(0xFFFFFF); LED2( OFF );// 灭 LED3( ON ); // 亮 Delay(0xFFFFFF); LED3( OFF );// 灭 /*轮流显示红绿蓝黄紫青白*/ LED_RED; Delay(0xFFFFFF); LED_GREEN; Delay(0xFFFFFF); LED_BLUE; Delay(0xFFFFFF); LED_YELLOW; Delay(0xFFFFFF); LED_PURPLE; Delay(0xFFFFFF); LED_CYAN; Delay(0xFFFFFF); LED_WHITE; Delay(0xFFFFFF); LED_RGBOFF; Delay(0xFFFFFF); } } void Delay(__IO uint32_t nCount) //简单的延时函数 { for(; nCount != 0; nCount--); } 在main函数中,调用定义的LED_GPIO_Config()函数初始化LED的控制引脚,然后直接调用各种控制LED亮灭的宏实现LED的控制。 以上就是一个使用STM32标准软件库开发应用的流程。 把编译好的程序下载到开发板并复位,可以看到RGB彩灯轮流显示不同的颜色。 5.6STM32 GPIO输入应用实例 本节GPIO输入应用实例是使用固件库的按键检测。 5.6.1STM32的GPIO输入应用硬件设计 按键机械触点断开、闭合时,由于触点的弹性作用,按键开关不会马上稳定接通或一下子断开,使用按键时会产生抖动信号,需 图57按键检测电路 要用软件消抖处理滤波,不方便输入检测。本实例开发板连接的按键附带硬件消抖功能,如图57所示。它利用电容充放电的延时消除了波纹,从而简化软件的处理,软件只需要直接检测引脚的电平即可。 从按键检测电路可知,这些按键在没有被按下时,GPIO引脚的输入状态为低电平(按键所在的电路不通,引脚接地),当按键按下时,GPIO引脚的输入状态为高电平(按键所在的电路导通,引脚接到电源)。只要按键检测引脚的输入电平,即可判断按键是否被按下。 若读者使用的开发板按键的连接方式或引脚不一样,只需根据工程修改引脚即可,程序的控制原理相同。 在本实例中,根据图57电路设计一个示例,通过按键控制LED,具体如下。 (1) 按下KEY1,红灯翻转。 (2) 按下KEY2,绿灯翻转。 5.6.2STM32的GPIO输入应用软件设计 为了使工程更加有条理,把LED控制相关的代码独立分开存储,方便以后移植。 在 工程模板上新建bsp_key.c及 bsp_key.h文件,其中的bsp即 Board Support Packet的缩写(板级支持包)。 编程要点如下。 (1) 使能GPIO端口时钟。 (2) 初始化GPIO目标引脚为输入模式(浮空输入)。 (3) 编写简单测试程序,检测按键的状态,实现按键控制LED。 1. bsp_key.h头文件 #ifndef __KEY_H #define__KEY_H #include "stm32f4xx.h" //引脚定义 /*******************************************************/ #define KEY1_PINGPIO_Pin_0 #define KEY1_GPIO_PORTGPIOA #define KEY1_GPIO_CLKRCC_AHB1Periph_GPIOA #define KEY2_PINGPIO_Pin_13 #define KEY2_GPIO_PORTGPIOC #define KEY2_GPIO_CLKRCC_AHB1Periph_GPIOC /*******************************************************/ /* 按键按下标志宏 * 按键按下为高电平,设置 KEY_ON=1, KEY_OFF=0 * 若按键按下为低电平,把宏设置成KEY_ON=0 ,KEY_OFF=1 即可 */ #define KEY_ON1 #define KEY_OFF0 void Key_GPIO_Config(void); uint8_t Key_Scan(GPIO_TypeDef* GPIOx,u16 GPIO_Pin); #endif /* __LED_H */ 2. bsp_key.c源文件 #include "./key/bsp_key.h" /// 不精确的延时 void Key_Delay(__IO u32 nCount) { for(; nCount != 0; nCount--); } /* * @brief配置按键用到的I/O口 * @param无 * @retval 无 */ void Key_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /*开启按键GPIO的时钟*/ RCC_AHB1PeriphClockCmd(KEY1_GPIO_CLK|KEY2_GPIO_CLK,ENABLE); /*选择按键1的引脚*/ GPIO_InitStructure.GPIO_Pin = KEY1_PIN; /*设置引脚为输入模式*/ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; /*设置引脚不上拉也不下拉*/ GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; /*使用上面的结构体初始化按键2*/ GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStructure); /*选择按键2的引脚*/ GPIO_InitStructure.GPIO_Pin = KEY2_PIN; /*使用上面的结构体初始化按键2*/ GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStructure); } 函数执行流程如下。 (1) 使用GPIO_InitTypeDef定义GPIO初始化结构体变量,以便下面用于存储GPIO配置。 (2) 调用库函数RCC_AHB1PeriphClockCmd()使能按键的GPIO端口时钟,调用时使用|操作同时配置两个按键的时钟。 (3) 向GPIO初始化结构体赋值,把引脚初始化成浮空输入模式,其中的GPIO_Pin使用宏KEYx_PIN赋值,使函数的实现方便移植。由于引脚的默认电平受按键电路影响,所以设置成浮空、上拉、下拉模式均没有区别。 (4) 使用以上初始化结构体的配置,调用GPIO_Init()函数向寄存器写入参数,完成GPIO的初始化,这里的GPIO端口使用 宏KEYx_GPIO_PORT赋值,也是为了程序移植方便。 (5) 使用同样的初始化结构体,只修改控制的引脚和端口,初始化其他按键检测时使用的 GPIO 引脚。 /* * @brief检测是否有按键按下 * @param GPIOx:具体的端口, x可以是A~K * @param GPIO_PIN:具体的端口位, 可以是GPIO_PIN_x(x可以是0~15) * @retval按键的状态 * @arg KEY_ON:按键按下 * @arg KEY_OFF:按键未按下 */ uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin) { /*检测是否有按键按下 */ if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON ) { /*等待按键释放 */ while(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON); return KEY_ON; } else return KEY_OFF; } 这里定义了一个Key_Scan()函数,用于扫描按键状态。 GPIO 引脚的输入电平可通过读取 IDR寄存器对应的数据位感知,而 STM32标准库提供了库函数GPIO_ReadInputDataBit()获取位状态,该函数输入GPIO端口及引脚号,函数返回该引脚的电平状态,高电平返回1,低电平返回0。Key_Scan()函数中以GPIO_ReadInputDataBit()函数的返回值与自定义的宏 KEY_ON对比,若检测到按键按下,则使用while循环持续检测按键状态,直到按键释放,按键释放后Key_Scan()函数返回一个KEY_ON值; 若没有检测到按键按下,则函数直接返回KEY_OFF。若按键的硬件没有做消抖处理,需要在Key_Scan()函数中做软件滤波,防止波纹抖动引起误触发。 3. main.c程序 #include "stm32f4xx.h" #include "./led/bsp_led.h" #include "./key/bsp_key.h" /* * @brief主函数 * @param无 * @retval 无 */ int main(void) { /* LED 端口初始化 */ LED_GPIO_Config(); /*初始化按键*/ Key_GPIO_Config(); /* 轮询按键状态,若按键按下则翻转LED */ while(1) { if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON ) { /*LED1翻转*/ LED1_TOGGLE; } if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON ) { /*LED2翻转*/ LED2_TOGGLE; } } } 代码中初始化LED及按键后,在while循环中不断调用Key_Scan()函数,并判断其返回值,若返回值表示按键按下,则翻转LED的状态。 把编译好的程序下载到开发板并复位,按下KEY1和KEY2分别可以控制LED的亮、灭状态。