目录
(一)Demo流程
(二)工程配置
(三)代码部分
(四)外部中断(EXTI)
首先,板子上有4个按键,两颗灯,一个beep,所以设计一个demo如下:
1、按下KEY0,LED0输出翻转;
2、按下KEY1,LED1输出翻转;
3、按下KEY2,LED0和LED1输出翻转;
4、按下WK_UP,蜂鸣器输出翻转;
相关部分电路schematic如下:
简单分析一下电路:两个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:
接下来,给每个引脚配置初始化如下:
首先对于两个灯为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
但是,总所周知,轮询查询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触发且下拉):
接下来配置NVIC。设置为2bit抢占优先级 & 2bit次优先级;抢占优先级:谁大可以立即抢占小的中断;次优先级:当抢占优先级一样时,优先执行次优先级大的,但是不能抢占同抢占级的,只能排队;当抢占优先级和次优先级都一样时,则FCFS(First Come First Serve);设置EXTI0,EXTI2,EXTI3,EXTI4的抢占优先级为1,2,1,1,次优先级为0,0,2,1(注意0为最高优先级,3为最低优先级),主要是为了观察同时发生中断时,高抢占优先级的中断能否如理论般正常抢占低抢占优先级的中断,还有就是抢占优先级相同时,次优先级高的是否先执行;如下图所示:
还有一点,设置外部中断的抢占优先级时不能设置为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