STM32 HAL库教程(二):GPIO输入输出操作详解

HAL库STM32常用外设教程(二)—— GPIO输入\输出


文章目录

  • HAL库STM32常用外设教程(二)—— GPIO输入\输出
  • 前言
  • 一、GPIO功能概述
  • 二、GPIO的HAl库驱动
  • 三、GPIO使用示例
  • 1.示例功能
  • 四、代码讲解
  • 五、总结

  • 前言

    所用工具:
    1、STM32F407ZGT6
    2、STM32CubeMx软件
    3、keil5
    内容简述:
    通篇文章将涉及以下内容,如有错误,欢迎指出
    GPIO的8个工作模式

    1、GPIO功能概述
    2、GPIO的HAL库驱动
    3、GPIO使用示例
    (1)CubeMx配置
    (2)GPIO驱动程序


    一、GPIO功能概述

    STM32F407ZG有8个16引脚的GPIO端口,从PA到PH,还有一个12引脚的PI端口,这些IO端口都连接在APB1总线上,最高时钟频率168MHz,GPIO引脚能承受5V电压,作为GPIO引脚使用时,我们可以输入或输出数字信号。
    一个端子的16个GPIO引脚的功能可以单独设置,每个引脚的输入\输出数据可以单独读取或输出,一个GPIO引脚的内部结构如下图显示,内部有一个双向保护二极管(电路电压较大时可以保护电路),有可配置的是否使用的上拉和下拉电阻。每个GPIO都可配置为多个工作模式。

    图1-1
    GPIO的八个工作模式:
    (1)浮空输入(input floating),作为GPIO的输入引脚,不使用上拉或下拉电阻。
    (2)输入上拉(input pull-up),作为GPIO的输入引脚,使用内部上拉电阻,引脚外部无输入时读取的引脚输入电平为高电平。
    (3)输入下拉(input pull-down),作为GPIO的输入引脚,使用内部下拉电阻,引脚外部无输入时读取的引脚输入电平为低电平。
    (4)模拟(analog)输入,作为GPIO模拟引脚,用于ADC输入引脚或DAC输出引脚。
    (5)开漏输出(output open-drain),如果不使用上拉或下拉电阻,开漏输出1时引脚为高阻态(相当于开路、不会输出任何电平),输出0时引脚为低电平,它们不能提供高电平,只能提供低电平或悬空状态,需要外接上拉电阻,才能实现输出高电平。
    (6)推挽输出(output push-pull),如果不使用上拉或下拉电阻,推挽输出1时引脚为高电平,输出0时引脚为低电平。若需要增强引脚输出驱动能力,就可以使用上拉,例如,需要GPIO引脚输出高电平点LED时。
    (7)推挽 复用输出(alternate function push-pull),当GPIO为复用IO时的推挽输出模式,一般用于外设功能,如I2C的SCL、SDA。
    (8)开漏 复用输出(alternate function push-pull),当GPIO为复用IO时的开漏输出模式,一般用于外设功能,如TX1、MOSI、MISO.SCK.SS。
    注:
    ①所有未进行任何配置的GPIO引脚,在系统复位后处于输入浮空模式。
    ②推挽输出与开漏输出的区别
    推完输出是一种主动驱动型输出。当输出为高电平时,推完输出器件会提供正电压来驱动负载;而当输出为低电平时,它会提供零电压或接近零电压来驱动负载。这种输出方式可以提供较高的驱动能力和较快的切换速度,适合直接连接到负载或需要驱动高电平信号的应用。
    开漏输出是一种被动驱动型输出。在高电平状态下,开漏输出器件不提供电压,相当于断开了输出端与电源之间的连接;而在低电平状态下,它会将输出端拉低,与地(GND)连接。为了使输出得到正确的电平,通常需要使用上拉电阻将输出端连接到正电源。这种输出方式可以实现多个设备的共享以及电平适配,适合需要进行电平转换、总线通信或多路输出的应用。
    总体而言,推完输出可以提供两个电平的输出,具有较高的驱动能力和切换速度,而开漏输出只提供低电平输出,需要使用上拉电阻实现正电平。选择哪种输出方式取决于具体应用需求和连接环境。

    二、GPIO的HAl库驱动

    GPIO引脚的操作主要包括初始化、读取引脚输入和设置引脚输出,相关的HAL库1驱动程序定义在文件stm32f4xx_hal_gpio.h中,主要操作函数如下表所示。
    表2-1 GPIO操作相关函数

    函数名 功能描述
    HAL_GPIO_Init() GPIO引脚初始化
    HAL_GPIO_DeInit() GPIO引脚反初始化,恢复为复位后的状态
    HAL_GPIO_WritePin() 使引脚输出0或1
    HAL_GPIO_ReadPin() 读取引脚的输入电平
    HAL_GPIO_TogglePin() 翻转引脚的输入
    HAL_GPIO_LockPin() 锁定引脚配置,而不是锁定引脚的输入或输出状态

    三、GPIO使用示例

    1.示例功能

    开发板有1个LED、4个按键和一个有源蜂鸣器,它们都是通过GPIO引脚控制,电路图如下:图上标识了连接的MCU引脚。
    (1)LED电路,是由外接+3.3V 电源驱动的。当GPIO引脚输出为0时,LED点亮,输出为1时,LED熄灭。因此,与LED连接的引脚PF9和PF10要设置为推挽输出。
    (2)对于KeyUp键,它的外端接的是+3.3V。在按键按下时,输入PA0引脚的是高电平,所以引脚PAO应该设置为输入下拉。在按键未按下时,输入是0。
    (3)另外3个连接在PE2、PE3、PE4上的按键,外端接地。按键按下时,输入低电平,所
    以使用输入上拉。
    (4)蜂鸣器的控制端接PF8,应设置为推挽输出。当PF8输出为1时,蜂鸣器响,输出为
    0时,蜂鸣器不响。
    注:此处通过三极管状态判断,该三极管是NPN型三极管,该三极管导通条件为b、e之间的PN结一定要正偏(即b端的电压要高于e端),所以b端输出为1时三极管导通,蜂鸣器一段接24V,一端接地,才能导通。


    图3-1
    图3-3

    图3-4

    示例流程如下:
    按下KEY0 键时,使LED0的输出翻转。
    按下KEY1 键时,蜂鸣器输出翻转。
    按下KEY2 键时,使LED1的输出翻转。
    按下KEYUp 键时,使LED1和LED2的输出都翻转。

    根据按键、LED和峰鸣器的电路,整理出MCU连接的GPIO引脚的输入/输出配置,如表2-2所示,根据表6-2的配置在CubeMX里进行设置。

    表3-1 与按键、LED、蜂鸣器连接的MCU引脚的配置

    用户标签 引脚名称 引脚功能 GPIO模式 上拉或下拉
    LED0 PF9 GPIO_Output 推挽输出
    LED1 PF10 GPIO_Output 推挽输出
    KEY_UP PA0 GPIO_Intput 输入 下拉
    KEY0 PE2 GPIO_Intput 输入 上拉
    KEY1 PE3 GPIO_Intput 输入 上拉
    KEY2 PE4 GPIO_Intput 输入 上拉
    Buzzer PF8 GPIO_Output 推挽输出

    在CubeMx里,我们选择STM32F407ZG新建一个项目,做如下一些初始设置。
    (1)在SYS组件中,设置Debug接口为Serial Wrie。
    (2)在RCC组件,设置HSE为Crystal/Ceramic Resonator.
    (3)在时钟树上,设置HSE频率为8MHz(开发板实际晶振的频率)。主锁相环选择HSE作为时钟源,设置HCLK频率为168MHz,由软件自动配置时钟树。
    (4)根据表 进行GPIO引脚设置。
    (5)设置保存项目,导出工程。

    图3-5 新建CubeMx工程


    图3-6 在GPIO引脚视图上设置引脚功能,在GPIO组件配置界面对引脚进行更多配置

    图3-7文件保存设置

    图3-8文件保存设置

    图3-9 导出工程

    四、代码讲解

    在CubeMX生成的工程后 自己又创建一个独立的文件夹(slave_module),并重新定义了一个头文件和源文件(keyled.c和keyled.h)自定义头文件和源文件放在这个文件夹中。这样,当重新生成工程时,CubeMX只会更新与配置相关的文件,而不会触及到自定义文件。
    注:别忘了添加头文件的路径
    图4-1 添加独立文件

    main.h

    代码如下:
    该处的宏定义优势主要有三个
    (1) 通过使用条件编译,你可以选择是否启用或禁用某些特定的功能。例如,如果没有定义KEY2_Pin,这部分按键检测的代码就不会被编译进去(在keyled.c里面从#ifdef KEY2_Pin到 #endif之间的程序),从而减小了程序的体积,如果定义了 ,则启用了相关的按键检测功能。
    (2)可维护性: 使用宏定义可以提高代码的可维护性。如果你想禁用或启用按键检测,只需在程序的其他地方定义或注释掉 KEY2_Pin 宏,而不必修改检测按键的实际代码。这使得修改程序行为变得更加灵活和容易。
    (3)平台移植性: 如果你的代码需要在不同的平台或不同的项目中使用,通过定义或取消定义宏,可以使相同的代码适应不同的硬件配置或项目需求,提高了代码的可移植性。

    图4-2 引脚宏定义

    #define KEY2_Pin GPIO_PIN_2
    #define KEY2_GPIO_Port GPIOE
    #define KEY1_Pin GPIO_PIN_3
    #define KEY1_GPIO_Port GPIOE
    #define KEY0_Pin GPIO_PIN_4
    #define KEY0_GPIO_Port GPIOE
    #define Buzzer_Pin GPIO_PIN_8
    #define Buzzer_GPIO_Port GPIOF
    #define LED0_Pin GPIO_PIN_9
    #define LED0_GPIO_Port GPIOF
    #define LED1_Pin GPIO_PIN_10
    #define LED1_GPIO_Port GPIOF
    #define WK_UP_Pin GPIO_PIN_0
    #define WK_UP_GPIO_Port GPIOA
    

    main.c
    ①添加头文件
    图4-3 添加头文件

    #include	"keyled.h"
    

    ②在while循环里面不断检测哪一个按键被按下
    图4-4 添加main.c代码

    		 KEYS curKey=ScanPressedKey(KEY_WAIT_ALWAYS);
    
    	  switch(curKey)
    	  {
    	  case KEY0:
    		  LED0_Toggle();
    		  break;
    
    	  case KEY2:
    		  LED1_Toggle();
    		  break;
    
    	  case KEY_UP:
    		  LED0_Toggle();
    		  LED1_Toggle();
    		  break;
    
    	  case KEY1:
    		  Buzzer_Toggle();
    	  }
    
    

    整个keyled.h

    代码如下:

    #ifndef KEYLED_H_
    #define KEYLED_H_
    
    #include	"main.h"   //在main.h中定义了Keys和LEDs的Labels宏
    
    //表示4个按键的枚举类型
    typedef enum {
    	KEY_NONE=0,		//没有按键被按下
    	KEY0,		//KeyLeft
    	KEY2,		//KeyRight
    	KEY_UP,			//KeyUp
    	KEY1		//KeyDown
    }KEYS;
    
    #define		KEY_WAIT_ALWAYS		0	//作为函数ScanKeys()的一种参数,表示一直等待按键输入
    //轮询方式扫描按键,timeout=KEY_WAIT_ALWAYS表示一直扫描,否是等待时间timeout, 延时单位ms
    KEYS  ScanPressedKey(uint32_t timeout);
    
    
    #ifdef	LED0_Pin		//LED1的控制
    	#define	 LED0_Toggle()	HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin)    //输出翻转
    
    	#define	 LED0_ON()		HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET)  //输出0,亮
    
    	#define	 LED0_OFF()		HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET)  //输出1,灭
    #endif
    
    
    #ifdef	LED1_Pin	//LED2的控制
    	#define	 LED1_Toggle()	HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin)    //输出翻转
    
    	#define	 LED1_ON()		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET)  //输出0,亮
    
    	#define	 LED1_OFF()		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET)  //输出1,灭
    #endif
    
    
    #ifdef	Buzzer_Pin		//蜂鸣器的控制
    	#define	 Buzzer_Toggle()	HAL_GPIO_TogglePin(Buzzer_GPIO_Port, Buzzer_Pin)  //输出翻转
    
    	#define	 Buzzer_ON()		HAL_GPIO_WritePin(Buzzer_GPIO_Port, Buzzer_Pin, GPIO_PIN_RESET)  //输出0,蜂鸣器响
    
    	#define	 Buzzer_OFF()		HAL_GPIO_WritePin(Buzzer_GPIO_Port, Buzzer_Pin, GPIO_PIN_SET)  //输出1蜂鸣器不响
    #endif
    
    
    #endif /* KEYLED_H_ */
    
    
    

    整个keyled.c

    代码如下:

    #include	"keyled.h"
    
    //轮询方式扫描4个按键,返回按键值
    //timeout单位ms,若timeout=0表示一直扫描,直到有键按下
    KEYS ScanPressedKey(uint32_t timeout)
    {
    	KEYS  key=KEY_NONE;
    	uint32_t  tickstart = HAL_GetTick();  //当前计数值
    	const uint32_t  btnDelay=20;	//按键按下阶段的抖动,延时再采样时间
    	GPIO_PinState keyState;
    
    	while(1)
    	{
    #ifdef	KEY0_Pin		 //如果定义了KeyLeft,就可以检测KeyLeft
    		keyState=HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin); //PE4=KeyLeft,低输入有效
    		if (keyState==GPIO_PIN_RESET)
    		{
    			HAL_Delay(btnDelay);  //前抖动期
    			keyState=HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin); //再采样
    			if (keyState ==GPIO_PIN_RESET)
    				return	KEY0;
    		}
    #endif
    
    #ifdef	KEY2_Pin 	//如果定义了KeyRight,就可以检测KeyRight
    		keyState=HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin); //PE2=KeyRight,低输入有效
    		if (keyState==GPIO_PIN_RESET)
    		{
    			HAL_Delay(btnDelay); //前抖动期
    			keyState=HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin);//再采样
    			if (keyState ==GPIO_PIN_RESET)
    				return	KEY2;
    		}
    #endif
    
    #ifdef	KEY1_Pin		//如果定义了KeyDown,就可以检测KeyDown
    		keyState=HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin); //PE3=KeyDown,低输入有效
    		if (keyState==GPIO_PIN_RESET)
    		{
    			HAL_Delay(btnDelay); //前抖动期
    			keyState=HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin);//再采样
    			if (keyState ==GPIO_PIN_RESET)
    				return	KEY1;
    		}
    #endif
    
    #ifdef	WK_UP_Pin		//如果定义了KeyUp,就可以检测KeyUp
    		keyState=HAL_GPIO_ReadPin(WK_UP_GPIO_Port, WK_UP_Pin); //PA0=KeyUp,高输入有效
    		if (keyState==GPIO_PIN_SET)
    		{
    			HAL_Delay(btnDelay); //10ms 抖动期
    			keyState=HAL_GPIO_ReadPin(WK_UP_GPIO_Port, WK_UP_Pin);//再采样
    			if (keyState ==GPIO_PIN_SET)
    				return	KEY_UP;
    		}
    #endif
    
    		if (timeout != KEY_WAIT_ALWAYS)  //没有按键按下时,会计算超时,timeout时退出
    		{
    			if ((HAL_GetTick() - tickstart) > timeout)
    				break;
    		}
    	}
    
    	return	key;
    }
    

    程序中定义了表示按键的枚举类型KEYS,函数ScanPressedKey(uint32_t timeout)用于检测案件输入,参数timeout是等待时间,如果timeout为KEY_WAIT_ALWAYS,就表示无限等待时间,即timeout=KEY_WAIT_ALWAYS,只有当按键按下时才能跳出ScanPressedKey函数。


    五、总结

    用上述的方法轮询的去查按键按下的状态不是处理按键不是一个好方法,之所以采用这种方法,是因为方便学习者能够更加深入的了解GPIO的用法。需要注意的是,这种方式的坏处是当按键按下时,其状态可能被触发很多次,例如,原本LED0处于熄灭状态,当按下KEY0时,LED0还是处于熄灭状态,之所以产生这种情况,并不是因为该程序没有起作用,可能是在极短的时间内LED0亮后接着熄灭了,可以在翻转电平的地方通过打印具体看到底执行了几次。
    一般来说,还可以通过外部触发中断进行按键检测,在中断回调函数里面开启一个10ms的定时器,在定时器中断服务例程里面进行消抖,然后到中断外部去做进一步的处理时较好的一种方式。

    参考书籍:《STM32Cube高效开发教程(基础篇)》王维波

    “生活不可能像你想象得那么好,但也不会像你想象得那么糟。” ————《傲慢与偏见》

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32 HAL库教程(二):GPIO输入输出操作详解

    发表评论