按键抖动的原因:
通常按键所用的开关都是机械弹性开关。当按键触点闭合或者断开时,由于机械触点的弹性特点,一个按键开关在闭合时不会马上就稳定的接通。在断开时也不会一下子就彻底断开。而是在闭合和断开时伴随了一系列抖动。
按键消抖的方法:硬件消抖和软件消抖
硬件消抖:
利用电容的充放电特性对抖动过程中产生的电压毛刺进行平滑处理。
软件消抖:
通过延迟程序过滤,通过延迟来过滤掉抖动时间。
实验设计
4个按键控制4个发光二极管的亮灭状态。
硬件电路:
- 4个按键分别连接GPIOA.0~GPIOA.3
- 4个LED灯分别连接GPIOA.4~GPIOA.7
软件设计(程序设计):
要点注意:
- 配置RCC寄存器组,使用PLL输出72MHz时钟作为系统时钟。
- 配置GPIOA.0~GPIOA.3这4个引脚为上拉输入模式,并配置GPIOA.4~GPIOA.7这4个引脚为推挽输出模式。
- 配置SysTick使用系统时钟,并产生20ms时间间隔。现在结合图5.3.1分析按键扫描方法的流程。每20ms进行一次按键扫描。
- 首先假设初始化状态没有按键按下,GPIO上也不存在抖动干扰电平,则此时的状态是“按键扫描状态0”。进入执行之后,因为扫描不到GPIO的电平变化,因此直接退出该状态,并且不更新状态标志。20ms之后仍然执行重复的过程。
- 其次假设仍然没有按键按下,但GPIO上有干扰抖动电平,则程序首先仍然进入“按键扫描状态0”。但进入执行之后,扫描到了GPIO上的电平变化,更新状态标志至“按键扫描状态1”。20ms之后,进入“按键扫描状态1”,再次检测GPIO电平。因为是抖动电平,其持续的时间不会大于20ms,所以此处并不能再次检测到电平变化,因此确认为抖动电平,更新状态标志至“按键扫描状态0”。这样就避免了将抖动电平误识别为按键按下的情况。
- 最后假设有按键按下。首先仍然是“按键扫描状态0”阶段,此状态阶段是判断按键对应的GPIO电平是否有跳变。此时以假设有按键按下,因此更新状态标志至“按键扫描状态1”。20ms以后,进入“按键扫描状态1”。检测得到GPIO电平还维持上一次的状态,确认为按键按下,更新状态标志为“按键扫描状态2”。再过20ms之后,进入“按键扫描状态2”,此时若还检测到电平维持上一次的状态,则显示表示按键尚未松开,则不更新按键标志。20ms后再次检测,直到按键松开之后才将状态标志更新为“按键扫描状态0”。检测下一次按键操作。
主函数 main.c
#include "stm32f10x_lib.h" //头文件
typedef enum
{
KeyScanState_0=0X00,
KeyScanState_1=0X01,
KeyScanState_2=0X02,
}
KeyScanState_Typedef;
#define KEYPORT GPIOA
#define KEY0PIN GPIO_Pin_0
#define KEY1PIN GPIO_Pin_1
#define KEY2PIN GPIO_Pin_2
#define KEY3PIN GPIO_Pin_3
#define LEDPORT GPIOA
#define LED0PIN GPIO_Pin_4
#define LED1PIN GPIO_Pin_5
#define LED2PIN GPIO_Pin_6
#define LED3PIN GPIO_Pin_7
void RccInitialisation(void);
void GpioInitialisation(void);
void SystickInitialisation(void);
int main(void)
{
vu16 KeyPortStatus=0;
KeyPortStatus_Typedef KeyScanState; //定义按键扫描状态枚举变量
RccInitialisation(); //设置系统时钟
GpioInitialisation(); //设置GPIO端口
SystickInitialisation(); //设置Systick定时器
while(1)
{
if(SysTick_GetFlagStatus(SysTick_FLAG_COUNT)==SET) //查询20ms是否到
{
KeyPortStatus=GPIO_ReadInputData(KEYPORT) & 0x000f; //读取I/O电平
switch(KeyScanState) //进入状态机流程
{
//状态1:判断是否有按键按下
case KeyScanState_0:
{
if(KeyScanStatus! = 0x000f)
{
KeyScanState=KeyScanState_1; //有按键按下,更新状态标志
}
break;
}
//状态2:判断是否抖动
case KeyScanState_1:
{
if(KeyPortStatus)
{
//非抖动,确认按键按下,执行相应操作
if(GPIO_ReadInputDataBit(KEYPORT,KEY0PIN)==0)
{
GPIO_WriteBit(LEDPORT,LED0PIN,(BitAction)(1-GPIO_ReadoutputDataBit(LEDPORT,LED0PIN)));
}
else if(GPIO_ReadInputDataBit(KEYPORT,KEY1PIN)==0)
{
GPIO_WriteBit(LEDPORT,LED1PIN,(BitAction)(1-GPIO_ReadoutputDataBit(LEDPORT,LED1PIN)));
}
else if(GPIO_ReadInputDataBit(KEYPORT,KEY2PIN)==0)
{
GPIO_WriteBit(LEDPORT,LED2PIN,(BitAction)(1-GPIO_ReadoutputDataBit(LEDPORT,LED2PIN)));
}
else if(GPIO_ReadInputDataBit(KEYPORT,KEY3PIN)==0)
{
GPIO_WriteBit(LEDPORT,LED3PIN,(BitAction)(1-GPIO_ReadoutputDataBit(LEDPORT,LED3PIN)));
}
KeyScanState=KeyScanState_2; //更新状态标志
}
else
{
KeyScanState=KeyScanState_0; //抖动,确认按键未按下,更新状态标志
}
break;
}
//状态3:松手检测
case KeyScanState_2:
{
if(KeyPortSrarus==0x000f)
{
KeyScanState = KeyScanState_0; //松手更新状态标志
}
break;
}
}
}
}
}
设置系统各部分时钟 RccInitialisation
void RccInitialisation(void)
{
ErrorStatus HSEStartUpStatus; //定义枚举类型变量 HSEStartUpStatus
RCC_DeInit(); //复位系统时钟设置
RCC_HSEConfig(RCC_HSE_ON); //开启HSE
HSEStatrtUpStatus=RCC_WaitForHSEStartUp(); //等待HSE起振并稳定
if(HSEStatrtUpStatus==SUCCESS) //判断HSE是否起振成功,是则进入if()内部
{
RCC_HCLKConfig(RCC_SYSCLK_Div1); //选择HCLK(AHB)时钟源为SYSCLK分频
RCC_PCLK2Config(RCC_HCLK_Div1); //选择PCLK2时钟源为HCLK(AHB)1分频
RCC_PCLK1Config(RCC_HCLK_Div2); //选择PCLK1时钟源为HCLK(AHB)2分频
FLASH_SetLatency(FLASH_Latency_2); //设置Flash延时周期数为2
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); //使能Flash预取缓存
//选择PLL时钟源为 HSE 1 分频,倍频数为9,则PLL=8MHz *9=72MHz
RCC_PLLConfig(RCC_PLLSource_HSE_Div1,RCC_PLLMul_9);
RCC_PLLCmd(ENABLE); //使能PLL
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY)==RESET); //等待PLL输出稳定
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); //选择SYSCLK时钟源为PLL
while(RCC_GetSYSCLKSource()!=0x08); //等待PLL成为SYSCLK时钟源
}
}
设置各GPIO端口功能 GpioInitialisation
void GPIO_GpioInitialisation(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义GPIO初始化结构体GPIO_InitStructure
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开APB2总线上的GPIOA时钟
//设置GPIOA.0~GPIOA.3为上拉输入
GPIO_InitStructure.GPIO_Pin=KEY0PIN | KEY1PIN | KEY2PIN | KEY3PIN ;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_IPU;
GPIO_Init(KEYPORT,&GPIO_InitStructure);
//设置GPIOA.4~GPIOA.7为推挽输出,最大翻转频率为50MHz
GPIO_InitStructure.GPIO_Pin=LED0PIN | LED1PIN | LED2PIN | LED3PIN ;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_Init(LEDPORT,&GPIO_InitStructure);
}
设置Systick定时器,重装载时间为20ms SystickInitialisation
void SystickInitialisation (void)
{
SysTick_CounterCmd(SysTick_Counter_Disable); //失能Systick定时器
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK); //选择HCLK为SysTick时钟源
SysTick_CounterCmd(SysTick_Counter_Clear); //清除Systick计数器
SysTick_SetReload(72000*20);; //主频为72MHz,配置计数值为72000*20可以得到20ms定时间隔
SysTick_CounterCmd(SysTick_Counter_Enable); //启动Systick定时器
}
注意事项:
- 有的按键扫描程序将按键对应的GPIO引脚设置为浮空输入模式,是因为其外部电路已经加入了上拉电阻。如果没有事先加上上拉电阻,那么必须将相应的GPIO引脚设置为上拉输入模式。
- STM32控制器开发的一个通用原则:在配置某个外设之前一定要保证其时钟是打开状态的,否则配置无效。