之前学过机械按键,可参考:
中断的意义:高效处理紧急程序,不会一直占用CPU资源。
中断分为硬中断和软中断,其分类依据是实现机制,而不是触发机制,比如CPU硬中断,它是由CPU这个硬件实现的中断机制,但它的触发可以通过外部硬件,也可以通过软件的 INT 指令。
类似地,软中断是由软件实现的中断,是纯粹由软件实现的一种类似中断的机制,实际上是模仿硬件,在内存中存储着一组软中断的标志位,然后由内核的一个守护线程不断轮询这些标志位,如果有哪个标志位有效,则再去执行这个软中断对应的中断处理程序。
原理图
触摸按键也是一种按键,我们先看其电路连接:
四个触摸按键。
TTP224N-BSB是一个触摸按键芯片,四个输出,四个输入,具体查看芯片数据手册。
触摸按键的输出端连接到了PE0~PE3。PE0~PE3是EXTI0~EXTI3。
触摸按键的原理跟触摸屏有点像,手指按上去的时候,会引起电容改变,从而引起电路改变,芯片通过检测这种细微变化来识别是否按下了按键。
在以上TTP224N-BSB芯片中,TOG和OD引脚是浮空的,即默认值。其中,AHLB、VDD和LPMB引脚被连接到了电源上;SM、VSS和MOT0接地。
这些引脚什么意思呢?
以下附TTP224N-BSB芯片的关键特性:
******************************************************************************************************
TTP224是一款使用电容式感应原理设计的触摸IC,其稳定的感应方式可以应用到各种不同电子类产品,面板介质可以是完全绝源的材料,专为取代传统的机械结构开关或普通按键而设 计。提供4个触摸输入端口及4个直接输出端口。
工作电压 2.4V~5.5V
可以由外部Option选择是否启用内部稳压电路功能
工作电流@VDD=3V无负载时:
低功耗模式下典型值2.5uA
快速模式下典型值9uA
@VDD=3V时,在快速模式下KEY最快响应时间为100mS,低功耗模式下为200mS.
各KEY灵敏度可以由外部电容进行调节(0~50pF).
提供LPMB端口选择快速模式或低功耗模式.
提供直接输出模式,触发模式,开漏输出, CMOS高电平有效或低电平有效输出, 经由 TOG/AHLB/OD端口选择.
提供两个无二极管保护的输出端口TPQ0D,TPQ2D仅限于低电平有效.
提供MOT1, MOT0端口选择最大输出时间:120秒/64秒/16秒/无穷大
上电后约有0.5秒的系统稳定时间,在此期间内不要触摸Touch PAD,且触摸功能无效
有自动校准功能,当无按键被触摸时,系统重新校准周期约为4.0秒
******************************************************************************************************
以上电路中,出去电源和地,还有另外6个引脚:
TOG——0——直接模式,直接模式还是触发模式有何区别?直接模式就是按下是一种状态,不按是一种状态,固定的,比如按下是高电平,不按就是低电平;触发模式就是按一下就变一下,比如一开始是0,按一下就变成了1,再按一下就变成了0……如此循环。
OD——1——CMOS 输出
AHLB——1——低电平有效,这里的高电平有效还是低电平有效不好理解,指的是,当按下时,输出什么电平,低电平有效,那么当按下按键就输出低电平,高电平有效,那么当按下按键就输出高电平;
LPMB——1——快速模式
SM——0——单键模式
MOT0——0——最长输出时间16秒
******************************************************************************************************
更多细节参考:按键模块TTP224N-BSB - 百度文库
要想写程序,得弄明白按键按下时,会有什么变化,才能触发外部中断。
根据上面的各引脚电平状态可知,按下时是低电平,那么不按时就是高电平,所以就是按下按键时,输出就从高电平变成了低电平,可以通过低电平/下降沿来触发中断。
这个芯片说是输出时低电平有效,我还想着没法输出高电平。
后来看到输出口是推挽输出,我还在想,又没接上拉电阻,哪来的高电平,难道是要把对应的GPIO口设置成上拉?
后来过了一晚,早上起床突然想到,推挽不就是既能输出高电平又能输出低电平吗?
我竟然还在想哪来的高电平,真是一时没反应过来。
低电平有效,应该是指,默认是高电平的,按下按键时,输出的就是低电平。
外部中断
一直搞不太明白,EXTI不就是外部中断吗?可明明除了系统异常,其他都属于外部中断呀?可是在所有的外部中断里,为什么有几个单独的EXTI中断呢?这个外部,到底是指什么?是否可以理解成,除了系统异常,其他都是外设中断,但是外设中断又分为内部外设和外部外设,而EXTI就是外部外设,需要通过IO口来触发。换个角度来说,其实,EXTI属于外部中断,相对于内部而言的,内部的比如RCC、定时器、DMA等,这些都是芯片设计层面的中断。而如果想通过外部的信号来触发中断,这时就要用到EXTI。
STM32小中大容量的19个外部中断:
注,以下这段内容来自正点原子:
外部中断,即触发信号从外部来,既然从外部来,那么肯定就要经过GPIO,然后经过AFIO,之后到达EXTI,即外部中断控制器,之后进入NVIC,即中断总控制器,最终通知CPU进行中断处理。
整个流程如下:
EXTI模块:
这里的输入线就是EXTI线。
脉冲发生器通常输出到具体的外设。
先弄清楚一个概念:
中断和事件的区别:
STM32之中断与事件---中断与事件的区别_无痕幽雨的博客-CSDN博客_stm32中断和事件
一句话总结:
具体查阅参考手册。此处仅贴上外部中断映像图:
每个外部中断只能选择一个引脚作为中断线。比如选择了PE0作为外部中断0的中断线,那么其他端口PA/PB/PC/PD/PF/PG就不能配置EXT0了。
记住:
中断0对应引脚0,中断1对应引脚1,中断2对应引脚2,……
MX初始化
先配置继电器(这里是因为板上有四个触摸按键,但只有3个LED灯,所以就借用这里的D6来作为一个LED使用,也能感受下直接驱动和使用中断驱动的区别):
配置接继电器的PG13引脚:
配置PE0~PE3为外部中断(下降沿触发):
注意,因为默认是高电平,所以必须选择下降沿触发,否则无法实现长按功能。如果根据高电平来检测长按,因为默认就是高电平,所以无法判断。
初始化对应中断:
关于MX中断的配置,有必要说明下:
stm32的优先级配置
STM32(Cortex-M3)中有两个优先级的概念:抢占式优先级和响应优先级,也把响应优先级称作“亚优先级”或“副优先级”或“从优先级”,每个中断源都需要被指定这两种优先级。
- 高抢占优先级的中断可以打断低抢占优先级的中断(抢占式优先级可以打断)
- 相同抢占优先级,高响应优先级无法打断低响应优先级的中断(响应优先级无法打断)
- 相同抢占优先级,两个中断同时触发时,优先执行高响应优先级的中断
- 若所有优先级都相同,谁先触发执行对应中断
- 抢占优先级和响应优先级都相同时,自然优先级高的先执行,自然优先级就是向量表中的优先级
- 优先级数字越低代表优先级越高
抢占式优先级和响应优先级通过中断优先级组进行分配
中断优先级组在stm32中一般可分为0-4共5组,分组配置在寄存器SCB->AIRCR中:由此可知,STM32的响应优先级可以是0位。
- 组0就是4位都用来设置成响应优先级,2^4=16位都是响应优先级
- 组1分为2^1两个抢占优先级,在这两个抢占优先级里面还分别有2^3八个响应优先级
- 组2分为2^2四个抢占优先级,在这四个抢占优先级里面还分别有2^2四个响应优先级
- 组3分为2^3八个抢占优先级,在这八个抢占优先级里面还分别有2^1两个响应优先级
- 组4分为2^4十六个都是抢占优先级
在MX中,就是这样的原理。组数字和优先级的数字是不同的,组数字是一种分组,而优先级数字是有优先级意义的,一定要注意。
接下来,要实现一个功能。
点击一下按键,就能亮灯;
长按按键,灯就会闪烁;
这里的思路是这样的:按下按键,就能通过下降沿触发外部中断,中断服务函数中,点亮相应的灯,并输出相应的串口数据;LED的驱动已经写过了;要增加一个继电器的灯的驱动。
点击可以通过下降沿触发中断;那么长按怎么解决呢?通过低电平触发吗?但是已经设置成了下降沿触发,怎么还能用低电平触发呢?
下降沿能触发中断,但是中断里可以通过检测多长时间的低电平后再执行相关代码。
搭建框架
先根据我自己的思路去搭建框架。
初始化后生成代码,之前的LED灯和串口的代码不变。
生成代码后先编译一遍看有没有问题。根据初始化可知,3个LED灯是亮的,继电器的灯是灭的,按键能触发中断,但是此时不会产生什么影响。
GPIO的初始化代码有增加:
void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; /* GPIO Ports Clock Enable */ __HAL_RCC_GPIOE_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOG_CLK_ENABLE(); /*Configure GPIO pin Output Level */ HAL_GPIO_WritePin(GPIOE, LED1_Pin|LED2_Pin|LED3_Pin, GPIO_PIN_SET); /*Configure GPIO pin Output Level */ HAL_GPIO_WritePin(Relay_GPIO_Port, Relay_Pin, GPIO_PIN_RESET); /*Configure GPIO pins : PEPin PEPin PEPin PEPin */ GPIO_InitStruct.Pin = KEY2_Pin|KEY3_Pin|KEY0_Pin|KEY1_Pin; GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); /*Configure GPIO pins : PEPin PEPin PEPin */ GPIO_InitStruct.Pin = LED1_Pin|LED2_Pin|LED3_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); /*Configure GPIO pin : PtPin */ GPIO_InitStruct.Pin = Relay_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(Relay_GPIO_Port, &GPIO_InitStruct); /* EXTI interrupt init*/ HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn); HAL_NVIC_SetPriority(EXTI1_IRQn, 1, 0); HAL_NVIC_EnableIRQ(EXTI1_IRQn); HAL_NVIC_SetPriority(EXTI2_IRQn, 1, 0); HAL_NVIC_EnableIRQ(EXTI2_IRQn); HAL_NVIC_SetPriority(EXTI3_IRQn, 1, 0); HAL_NVIC_EnableIRQ(EXTI3_IRQn); }
跟上面的配置是一致的。每次配置MX后类似的内容都是一样的,后续不再赘述。
开始搭建框架
先写继电器的代码,建立两个文件,relay.c和relay.h
relay.h
#ifndef _RELAY_H_ #define _RELAY_H_ //确定要实现的led功能 typedef struct { //打开继电器 void (*relayOpen)(void); //关闭继电器 void (*relayClose)(void); //转换继电器状态 void (*relaySwitch)(void); } relay_t; //将结构体声明出去 extern relay_t relayObj; #endif
relay.c
#include "myapplication.h" static void RelayOpen(void); static void RelayClose(void); static void RelaySwitch(void); relay_t relayObj = { RelayOpen, RelayClose, RelaySwitch }; static void RelayOpen(void) { HAL_GPIO_WritePin(Relay_GPIO_Port, Relay_Pin, GPIO_PIN_SET); } static void RelayClose(void) { HAL_GPIO_WritePin(Relay_GPIO_Port, Relay_Pin, GPIO_PIN_RESET); } static void RelaySwitch(void) { HAL_GPIO_TogglePin(Relay_GPIO_Port, Relay_Pin); }
这里只是操作GPIO口,和LED的操作是一模一样的。
外部中断代码
因为配置了外部中断,所以在中断文件stm32f1xx_it.c中,会有相应的中断服务函数:
/** * @brief This function handles EXTI line0 interrupt. */ void EXTI0_IRQHandler(void) { /* USER CODE BEGIN EXTI0_IRQn 0 */ /* USER CODE END EXTI0_IRQn 0 */ HAL_GPIO_EXTI_IRQHandler(KEY0_Pin); /* USER CODE BEGIN EXTI0_IRQn 1 */ /* USER CODE END EXTI0_IRQn 1 */ } /** * @brief This function handles EXTI line1 interrupt. */ void EXTI1_IRQHandler(void) { /* USER CODE BEGIN EXTI1_IRQn 0 */ /* USER CODE END EXTI1_IRQn 0 */ HAL_GPIO_EXTI_IRQHandler(KEY1_Pin); /* USER CODE BEGIN EXTI1_IRQn 1 */ /* USER CODE END EXTI1_IRQn 1 */ } /** * @brief This function handles EXTI line2 interrupt. */ void EXTI2_IRQHandler(void) { /* USER CODE BEGIN EXTI2_IRQn 0 */ /* USER CODE END EXTI2_IRQn 0 */ HAL_GPIO_EXTI_IRQHandler(KEY2_Pin); /* USER CODE BEGIN EXTI2_IRQn 1 */ /* USER CODE END EXTI2_IRQn 1 */ } /** * @brief This function handles EXTI line3 interrupt. */ void EXTI3_IRQHandler(void) { /* USER CODE BEGIN EXTI3_IRQn 0 */ /* USER CODE END EXTI3_IRQn 0 */ HAL_GPIO_EXTI_IRQHandler(KEY3_Pin); /* USER CODE BEGIN EXTI3_IRQn 1 */ /* USER CODE END EXTI3_IRQn 1 */ }
根据调用的函数继续往下走,跳转到了stm32f1xx_hal_gpio.c中:
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin) { /* EXTI line interrupt detected */ if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin); HAL_GPIO_EXTI_Callback(GPIO_Pin); } } /** * @brief EXTI line detection callbacks. * @param GPIO_Pin: Specifies the pins connected EXTI line * @retval None */ __weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { /* Prevent unused argument(s) compilation warning */ UNUSED(GPIO_Pin); /* NOTE: This function Should not be modified, when the callback is needed, the HAL_GPIO_EXTI_Callback could be implemented in the user file */ }
上面的中断处理函数又调用了下面的那个回调函数。
同理,我们要重写这个函数。
//重写外部中断函数 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { switch(GPIO_Pin) { case KEY0_Pin : printf("first key is running.\n\r"); led_operater_middle.ledMiddle(LED1, LedExtinguish); break; case KEY1_Pin : printf("second key is running.\n\r"); led_operater_middle.ledMiddle(LED2, LedExtinguish); break; case KEY2_Pin : printf("third key is running.\n\r"); led_operater_middle.ledMiddle(LED3, LedExtinguish); break; case KEY3_Pin : printf("forth key is running.\n\r"); relayObj.relayOpen(); break; default: printf("key fault!please click right key.\n\r"); } }
这能够完成点击触发中断的功能。
那么,按键长按怎么解决呢?
长按可以在触发中断后的处理函数中解决,下降沿之后,判断是短暂的低电平,还是有个持续的低电平,如果是短暂的低电平,那么就是单击;如果是连续的低电平,那么就认为是长按。判断是否是高低电平,有个读引脚状态的函数HAL_GPIO_ReadPin(……);
时间关系,此处暂时不写了,后面有空再补充吧。
注意
外部中断函数其实在GPIO的库文件中就有定义。
处理函数是各EXTI各自的。根据中断向量表来的。
/** * @brief This function handles EXTI line0 interrupt. */ void EXTI0_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(KEY1_Pin); } /** * @brief This function handles EXTI line1 interrupt. */ void EXTI1_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(KEY2_Pin); } /** * @brief This function handles EXTI line2 interrupt. */ void EXTI2_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(KEY3_Pin); } /** * @brief This function handles EXTI line3 interrupt. */ void EXTI3_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(KEY4_Pin); }
但是所有的EXTI共用回调函数,即各EXTI都是调用同一个回调函数。
所以,需要在重写的回调函数中判断下到底是来自于哪个端口。
注意,跳转到中断处理函数是CPU的自动行为,但是具体如何处理,就需要用户自定义了。
EXTI映射引脚设置完成后,别忘了在NVIC中使能,要不然不生效。
别只设置了引脚复用,然后每在NVIC使能,还傻傻的不知道哪里出了问题。
中断失效的一种情况
在写中断函数时,发现之前配置好的中断不生效了。
找了半天,才发现,main函数中没有加while死循环。
关于main函数中的while死循环。
主函数一定要有while(1)吗?
是的。
这是各种单片机自身决定的。有的单片机程序内如果没有循环程序,那它会从头执行到最后,然后返回最开始继续执行。而有的单片机在执行一次之后,就会停止。有的则是执行完后可能会进入无序运行状态,程序就跑飞了。main函数不能终止,要一直循环。
所以,在写或者移植单片机程序的时候,最好是加上while循环,即便程序只运行一次,也要在最后加上while(1);目的是为了让程序一直保持在我们需要运行的情况下,使其一直指向这个语句而不会出现误操作。在程序末尾加一句“while(1);”,程序显示完全正常。如果不加,程序会不稳定,有时候会在main()里面循环,有时候会乱码。在我们写的C语言后转换成汇编,再观察单片机的代码区,你会发现没有写程序的部分例如全1或者全0区域,程序运行到这里,就会有可能造成意料不到的结果。
比如出现HardFault。
更多参考:按键的长按和短按 - 知乎
清除中断标志位
遇到一个问题,是因为在中断里判断了如果锁定了,则不执行中断里的任何程序。
结果,导致程序回不到主程序了。
为什么?
因为如果不执行中断,那么中断内的业务函数虽然没执行,但是清除中断标志位也没执行。
不清除中断标志位会发生什么?
中断标志位不清除, 结果是完成中断处理程序后, 它就继续再进中断, 根本不会回到主程序。
这里的清除中断标志位的一行代码被我注释掉了,所以并没有清除中断标志位。这样的话会发生什么呢?按下KEY2时,完成中断处理程序后仍然进入中断,中断服务函数里的内容会被一直重复执行,直到松开KEY2。这样的话并不能控制LED0的翻转了,所以中断结束之后一定要清除中断标志位。
拨码开关的常用逻辑
定义一个之前的状态,一个当前的状态,之后判断,如果两个状态不相等,就执行某些操作。
void lock_task(void) { static uint32_t s_PreStatus = 0xffffffff; //之前的锁状态 uint32_t CurStatus; //当前的锁状态 if(s_PreStatus != CurStatus) { s_PreStatus = CurStatus; //执行某种操作 } }
关于中断中要做的事
通常,我们会在中断里设置相应的标志位,然后在主程序中去根据标志位做各种各样的判断。为什么要这么做呢?
一来是因为中断最好快进快出,不要在里面执行复杂的业务逻辑,要不然容易导致时序错误;
二是因为中断发生时,会跳转去执行中断,之后再回来执行主程序,这样,中断里就一定可以将标志位设置好,回到主程序时就可以根据标志位做出正确的判断。
在嵌入式编程中,经常会使用标志位,设置某种标志位,然后根据标志位不同的值,执行不同的动作,也就是调用相应的函数。
补充:编码器开关
一种根据旋转方向,输出严格时序脉冲的电子元器件。
最近用到编码器旋转开关,不会判断是顺时针旋转还是逆时针旋转。
旋转编码器有三个引脚(不带按键),分为A端、G接地端和B端,A端和B端可以分别接单片机引脚。看资料说是,旋转时,AB端分别会输出时序严格的波形,根据二者的波形关系来判断旋转方向,所以可以在中断中判断另一端的电平高低,以此得出往哪边转,结果我在AB两端的中断里都做了中断判断,结果没有实现功能。
后来继续查资料,发现自己理解错了。
旋转编码开关,你不懂,编程方法和结构原理吗?拆给你看!_哔哩哔哩_bilibili
原来要以一端为基准,只用触发其中一端的中断,在中断中,判断另一端是高电平还是低电平,以此得出是顺时针还是逆时针。
举个例子,设置A端为下降沿触发(也可以上升沿),之后,在中断里,判断B端此时是高电平还是低电平,如果是高电平,则是顺时针旋转,如果是低电平,则为逆时针旋转。(此处只是示例,具体电平高低和方向的对应关系还需要看对应的手册中波形是如何)
也就是,根据AB两端的波形关系来判断是顺时针还是逆时针。
比如日本ALPS阿尔卑斯EC12E2420301贯通轴编码器
其波形输出如下:
CW即顺时针旋转(Clock Wise)的方向
与CW反方向旋转时为CCW (Counter Clock Wise)。
以A为参考点,CW时,B是这样的波形,那么CCW时,B的相位就会偏移。
顺时针时
逆时针时
其实就是CW旋转方向中,通常增量型为A相比B相先进行相位输出;
在CCW旋转方向中,通常增量型为B相比A相先进行相位输出。
今天遇到个问题,就是编码器转一次,不断生效,后面经排查发现不是编码器的问题。
我在主循环中不断判断编码器转动的标志位,以此来执行一些动作。
我转了编码器之后,这个动作不断执行,我以为是编码器的问题,结果是因为我执行完一次动作之后,没有将编码器旋转的标志位给清除,导致一直都是处于判断有效状态。
所以,需要在执行完相应的动作后,将编码器旋转标志位清除。所以,常常需要注意标志位或者状态位的置位和复位操作。
消抖
旋转编码器的软件消抖
编码器在使用时,会在旋转过程中出现抖动,导致效果很差,要么是转一下就生效好多次,要么就是往右转然后又往左转就好像没转一样,等等问题。
我们想要的理想结果是,往左转就是左转触发一次,往右转就是往右转触发一次,因此,消抖十分重要。
可以直接参考这篇文章:
江协科技STM32——旋转编码器计次(软件消抖)_stm32 软件消抖-CSDN博客
参照的思路是第三种:增加判断正、反转的条件,读取一个周期内的电平变化再进行判断。首先将最小系统板的PB0引脚与A相连接,触发方式选择上升/下降沿触发,用A相的输出信号来触发中断,然后在A相下降沿触发第一次中断后读取B相电平,紧接着A相上升沿触发第二次中断后读取B相电平,结合两次读取到的电平来判断是正转还是反转,这种检测方法和第二种方法的原理相同,即从A相的下降沿触发到上升沿触发期间,若B相电平发生了变化,则判定编码器转动,反之未转动,波形抖动时B相的电平保持不变,能够实现消抖。
本人参考所实现的程序如下:
void EXTI9_5_IRQHandler(void) { static uint8_t firstLevel, irqCount; if(EXTI_GetITStatus(EXTI_Line6) != RESET)//判断中断是否发生 { //一端下降沿触发第一次中断 if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) == 0 && irqCount == 0) { //计数值加一,表示已经触发了第一次中断 irqCount++; //读取另外一端的电平,若为高电平则firstLevel置1,反之保持0 firstLevel = 0; if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) == 1) { firstLevel = 1; } } //A相上升沿触发第二次中断 if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) == 1 && irqCount == 1) { irqCount = 0;//计数清零 if(firstLevel == 1 && GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) == 0) { knobState = KNOB_RIGHT;;//正转 } if(firstLevel == 0 && GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) == 1) { knobState = KNOB_LEFT;//反转 } } //清除 LINE 上的中断标志位 EXTI_ClearITPendingBit(EXTI_Line6); } }
要注意:此时编码器是双边沿触发。结合两次边沿的判断,得出正确的周期,消除误判。
机械按键的软件消抖
按键的消抖相对简单,就是通过延时,但是不能直接用delay来延时,而是要用定时器来实现延时。
思路就是:当按键的外部中断触发后,在中断触发函数中先不着急给按键赋予按下状态,而是置个标志位,表示当前已经按下了按键。
同时,在10ms定时器里,根据该标志位来进行判断,是否处于按下状态的电平,如果是,再给按键赋予按下状态。
经验证,可以解决抖动的问题。
开关机可以使用阻塞方式来实现按键功能。
因为开关机时,用户默认就是不会使用任何其他功能,只用开机或者关机即可。
主循环里检测到按键时才会阻塞,也就是说,想要开关机的时候才会延时,正常都是不会阻塞的,任务执行过程中的中断按键需要不阻塞,但是开关机可以阻塞实现,简单些。
//按键扫描 void KeyScanTask(void) { if(nrf_gpio_pin_read(POWER_KEY) == 0) { /* 延时3秒 */ for(uint32_t i = 0; i < 300; i++) { nrf_delay_ms(10); if(nrf_gpio_pin_read(POWER_KEY) != 0) { return; } } if(nrf_gpio_pin_read(POWER_KEY) == 0) { TogglePowerHolding(); while(0 == nrf_gpio_pin_read(POWER_KEY)); } } }
最后一个while循环,是为了让用户在没有释放按键之前,不执行任何其他动作。