STM32实战总结:HAL之触摸按键

之前学过机械按键,可参考:

51单片机外设篇:按键_路溪非溪的博客-CSDN博客

外设篇:按键和CPU的中断系统_路溪非溪的博客-CSDN博客

中断的意义:高效处理紧急程序,不会一直占用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个外部中断:

注,以下这段内容来自正点原子:

第1讲 什么是中断?_哔哩哔哩_bilibili

外部中断,即触发信号从外部来,既然从外部来,那么肯定就要经过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循环,是为了让用户在没有释放按键之前,不执行任何其他动作。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值