STM32复习笔记(二):GPIO

目录

(一)Demo流程

(二)工程配置

(三)代码部分

(四)外部中断(EXTI)


(一)Demo流程

首先,板子上有4个按键,两颗灯,一个beep,所以设计一个demo如下:

1、按下KEY0,LED0输出翻转;

2、按下KEY1,LED1输出翻转;

3、按下KEY2,LED0和LED1输出翻转;

4、按下WK_UP,蜂鸣器输出翻转;

相关部分电路schematic如下:

STM32复习笔记(二):GPIO_第1张图片

STM32复习笔记(二):GPIO_第2张图片

 STM32复习笔记(二):GPIO_第3张图片

STM32复习笔记(二):GPIO_第4张图片此外,WK_UP接到PA0;

简单分析一下电路:两个LED为0点亮;按键三个为0有效,一个为1有效;蜂鸣器处有一个BJT放大电路,BEEP给1就导通,给0就截止。


(二)工程配置

按老样子配置好SYS,RCC以及时钟频率之后,开始配置引脚;首先找到PF9和PF10,配置为输出,并修改label为LED0和LED1;然后找到PE4、PE3、PE2、PA0,配置为输入,并修改label为KEY0、KEY1、KEY2、WK_UP;最后找到PF8,配置为输出,并修改label为BEEP:

STM32复习笔记(二):GPIO_第5张图片

接下来,给每个引脚配置初始化如下:

STM32复习笔记(二):GPIO_第6张图片

首先对于两个灯为0点亮,所以首先给1且上拉,先不点亮,待需要时再点亮;然后对于beep,因为给1为响,所以先给0,而电路图中已经默认帮我下拉了,所以我直接不需要下拉;最后对于按键,因为WK_UP是1有效,所以默认下拉,检测到1则表明按下,而KEY0~2是0有效,所以默认上拉,检测到0则表明按下;然后设置好相关路径等配置直接generate code即可。


(三)代码部分

首先进入到main.h,就会发现刚刚给引脚起的label名都被define在里面了:

接下来新建文件,勾选下图第一个选项,直接帮你创建相关头文件,无需勾选第二个,后面再在cmake中手动加入:

接下来,需要在cmake中使用include_directories()包含头文件路径,以及使用file()包含源文件路径(此外,如果再用cubemx生成代码的话,CmakeLists.txt文件是会被重新覆盖掉的,所以需要写入CmakeLists_template.txt中,就不会被覆盖):

再点击右键,选择重新加载cmake即可:

经过漫长的代码编写之后......终于写完了:

keyled.h:

#ifndef DEMO_GPIO_KEYLED_H
#define DEMO_GPIO_KEYLED_H
#ifdef __cplusplus
extern "C" {
#endif

#include "main.h"

//表示4个按键的枚举类型
typedef enum {
    KEY_NONE = 0,   //没有按键按下
    KEY0,
    KEY1,
    KEY2,
    WK_UP
}KEYS;

#define KEY_WAIT_ALWAYS 0   //作为函数ScanPressedKey()的一种参数,表示一直等待按键输入

#ifdef  LED0_Pin     //LED0
#define LED0_ON()       HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET)
#define LED0_OFF()      HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET)
#define LED0_TOGGLE()   HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin)
#endif

#ifdef  LED1_Pin     //LED1
#define LED1_ON()       HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET)
#define LED1_OFF()      HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET)
#define LED1_TOGGLE()   HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin)
#endif

#ifdef  BEEP_Pin     //Beep
#define BEEP_ON()       HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_SET)
#define BEEP_OFF()      HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_RESET)
#define BEEP_TOGGLE()   HAL_GPIO_TogglePin(BEEP_GPIO_Port, BEEP_Pin)
#endif


KEYS ScanPressedKey(uint32_t timeout);


#ifdef __cplusplus
}
#endif
#endif //DEMO_GPIO_KEYLED_H

keyled.cpp:

#include "keyled.h"

//轮询方式扫米奥4个按键,并返回按键值
//轮询方式扫描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(true)
    {
#ifdef	KEY0_Pin		 //如果定义了KEY0,就可以检测KEY0
        keyState = HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin); //低输入有效
		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	KEY1_Pin		 //如果定义了KEY1,就可以检测KEY1
        keyState = HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin); //低输入有效
        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	KEY2_Pin		 //如果定义了KEY2,就可以检测KEY2
        keyState = HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin); //低输入有效
        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	WK_UP_Pin		 //如果定义了WK_UP,就可以检测WK_UP
        keyState = HAL_GPIO_ReadPin(WK_UP_GPIO_Port, WK_UP_Pin); //PE4=KeyLeft,低输入有效
        if (keyState == GPIO_PIN_SET)//注意这里默认是下拉
        {
            HAL_Delay(btnDelay);  //前抖动期
            keyState = HAL_GPIO_ReadPin(WK_UP_GPIO_Port, WK_UP_Pin); //再采样
            if (keyState == GPIO_PIN_SET)
                return	WK_UP;
        }
#endif

        if (timeout != KEY_WAIT_ALWAYS)  //没有按键按下时,会计算超时,timeout时退出
        {
            if ((HAL_GetTick() - tickstart) > timeout)
                break;
        }
    }

    return	key;
}

主函数:

int main(void)
{
  HAL_Init();

  SystemClock_Config();

  MX_GPIO_Init();

  while (1)
  {
      KEYS curkey = ScanPressedKey(KEY_WAIT_ALWAYS);    //一直等待按键输入
      switch (curkey) {
          case KEY0:
              LED0_TOGGLE();
              break;
          case KEY1:
              LED1_TOGGLE();
              break;
          case KEY2:
              LED0_TOGGLE();
              LED1_TOGGLE();
              break;
          case WK_UP:
              BEEP_TOGGLE();
              break;
      }
      HAL_Delay(200);   //跳过后抖动
  }
}

以上,实现了轮询检测按键,从而控制LED及Beep。

工程链接:https://pan.baidu.com/s/11xCxqty3KRJD6cx0S65jWQ 
提取码:0xFF


(四)外部中断(EXTI)

但是,总所周知,轮询查询GPIO口是非常非常浪费cpu资源的,所以可以利用外部中断(EXTI,External Interrupt)来检测按键输入;设计一个demo如下:

1、按下KEY0,触发EXTI4,LED0输出翻转;

2、按下KEY1,触发EXTI3,LED1输出翻转;

3、按下WK_UP,触发EXTI0,LED0和LED1输出翻转;

4、按下KEY2,产生EXTI0软中断(SWIT),模拟按下WK_UP;

配置如下(因为KEY0~2为0有效,所以设置为falling edge触发且上拉;而WK_UP为1有效,所以设置为raising edge触发且下拉):

STM32复习笔记(二):GPIO_第7张图片

接下来配置NVIC。设置为2bit抢占优先级 & 2bit次优先级;抢占优先级:谁大可以立即抢占小的中断;次优先级:当抢占优先级一样时,优先执行次优先级大的,但是不能抢占同抢占级的,只能排队;当抢占优先级和次优先级都一样时,则FCFS(First Come First Serve);设置EXTI0,EXTI2,EXTI3,EXTI4的抢占优先级为1,2,1,1,次优先级为0,0,2,1(注意0为最高优先级,3为最低优先级),主要是为了观察同时发生中断时,高抢占优先级的中断能否如理论般正常抢占低抢占优先级的中断,还有就是抢占优先级相同时,次优先级高的是否先执行;如下图所示:

STM32复习笔记(二):GPIO_第8张图片

还有一点,设置外部中断的抢占优先级时不能设置为0,因为外部中断的回调函数中会用到HAL_Delay()函数来延时消抖,该函数实际上用的是SysTick嘀嗒计时器的中断,其抢占优先级为0,如果外部中断的抢占优先级也设置为0,那么SysTick嘀嗒计时器的中断就无法抢占外部中断(相同抢占优先级),这将会导致HAL_Delay()函数死循环,系统卡死。

接下来生成代码,可以观察到在stm32f4xx_it.c中,cubemx已经生成了外部中断的函数,切记函数名不能改,因为在启动的汇编文件.s中已经将它们定义好了,要保持二者一致(除非去改汇编,但是没必要):

观察每个EXTI_IRQHandler()函数,发现它们都调用了HAL_GPIO_EXTI_IRQHandler()函数,跳到定义会发现,该函数首先判断是否为中断触发,然后传入中断触发源,再调用HAL_GPIO_EXTI_Callback()外部中断回调函数(callback means 回调):

继续跟进代码可以看到,回调函数是一个__weak修饰的函数,而__weak是一个宏定义,表示为属性:__attribute__((weak)),也就是弱函数;弱函数需要用户自己重新实现,编译时编译器就会自动编译重新实现的函数而忽略弱函数,如果没有重新实现,则自动编译原来的弱函数;其中的UNUSER()是为了避免gcc编译警告:

重新实现的回调函数如下(随便写在哪个文件都行,反正编译器会找得到,不过最好写在相关文件中):

编译下载到板子中会发现结果有一点点不如预期,比如说按下WK_UP时,两个LED会翻转两次,这很明显就是触发了两次中断,但是代码里不是用了1s这么长的延时来消抖么?为什么还会有问题?问题就出在cubemx生成的代码中;观察下图可以发现,HAL_GPIO_EXTI_IRQHandler()函数先判断是否为中断,然后清除标志位,再调用中断回调函数,一般的中断流程这样处理没有问题,主要是为了硬件能及时响应下一次中断;但是对于检测按键输入EXTI就有问题了,因为按键的抖动会导致产生不止一次的外部中断,而先清除了第一次的中断标志位,再执行回调时,后面还有几个抖动的相同外部中断又来了,同样会产生中断标志位,而此时系统正在中断的回调中延时消抖,执行完第一次回调函数之后,cpu出来又发现还有一个中断标志位,将会再进行一次同样的外部中断。

当然理解了原理修改起来就不难,只需要将两行函数互换,当检测到外部中断时,立马执行中断回调,不在管外界还有多少个相同的外部中断均不理会,只有当回调函数执行完毕后,再清除中断标志,这样就避免了多次中断。如下图:

值得注意的是,当重新用cubemx生成代码后,这两行又默认变回原来的位置了,还要手动修改,,,这也是不算bug的bug吧。。。

完~


工程链接:https://pan.baidu.com/s/1Svj7bh_sRzLUYvGjNr4Q3A 
提取码:0xFF

以上均为个人学习心得,如有错误,请不吝赐教~

THE END

你可能感兴趣的:(STM32复习笔记,stm32,笔记,嵌入式硬件)