之前文章
优化后文章
1.2、 STM32按键的长按和短按(一)
1.3、 STM32按键的长按和连按(二)
项目,要实现长按、短按、还有多按,用按键来控制单片机开关机,关机后进入到睡眠低功耗,所以我将按键配置中断,用中断来唤醒睡眠,加上定时器完成长短按键,话不多说,开始进入正题。
单片机选择STM32F103C8T6最小系统板
轻触按键
在学习单片机时,接触最多的外设除了LED就是按键,所以按键的介绍就只是简单说一下。
按键原理图
根据按键原理图可知,GPIO引脚P0.0(PA0)、P0.1(PA1)、P5.2在没有按键没有按下时连接的是R5、R6、R8三个电阻接地线,所以在按键没有按下时处于低电平,按键K4、K3、K2 == 0;至于二极管D2、D3是按键K3、K4应用于影响按键K2的,不使用可以不用管。
当按键K4按下时,电源VDD接通经过电阻R5到达地线GND,GPIO引脚P0.0可检测到高电平,按键K4 == 1,
按键是高电平响应,代码可以这这样写:
//#include "key.c"
void KEY_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义GPIO结构体
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PA0引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //输出频率
GPIO_Init(GPIOA, &GPIO_InitStructure); //结构体配置完成初始化
}
uint8_t KEY_Stare(void)
{
static uint8_t key_state = 0;//按键状态位
if(KEY_STATA && !key_state) {
printf("key_ON\r\n");//按下按键
key_state = 1;
}
if(!KEY_STATA && key_state) {
printf("KEY_OFF\r\n");//松开按键
key_state = 0;
}
return key_state;
}
//#include "key.h"
#define KEY_STATE !!(GPIOA->IDR & 0x0001)//寄存器读取按键状态位
#define KEY_STATA !!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)//标准库读取
void KEY_Init(void);
uint8_t KEY_Stare(void);
如果读者按键是低电平响应,只需要修改KEY_STATA的取反即可。
//#include "key.c"
void KEY_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义GPIO结构体
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PA0引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //输出频率
GPIO_Init(GPIOA, &GPIO_InitStructure); //结构体配置完成初始化
}
uint8_t KEY_Stare(void)
{
static uint8_t key_state = 0;//按键状态位
if(!KEY_STATA && !key_state) {//此处修改即可
printf("key_ON\r\n");//按下按键
key_state = 1;
}
if(KEY_STATA && key_state) {
printf("KEY_OFF\r\n");//松开按键
key_state = 0;
}
return key_state;
}
//#include "key.h"
#define KEY_STATE !!(GPIOA->IDR & 0x0001)//寄存器读取按键状态位
#define KEY_STATA !!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)//标准库读取
void KEY_Init(void);
uint8_t KEY_Stare(void);
定时器
选择STM32F103C8T6的通用定时器TIM2。
这两张图来自江协科技,想了解定时器细节建议去看原视频:
STM32入门教程-2023持续更新中_哔哩哔哩_bilibili
定时器配置
定时器常规配置不详细介绍, 计数器溢出进入中断,Tout(溢出时间) = (ARR+1)(PSC+1)/Tclk(时钟分割)
① 配置GPIO结构体初始化;
② 配置NVIC结构体初始化;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义TIM时基结构体
NVIC_InitTypeDef NVIC_InitStructure; //定义NVIC(嵌套向量中断)结构体
编写TIM中断服务函数
当按键按下后,中断服务函数void EXTI0_IRQHandler(void)中,使能定时器TIM_Cmd(TIM2, ENABLE),定时器开始计数,计数器溢出后,开启中断。
Tout(溢出时间) = (ARR+1)(PSC+1)/Tclk(时钟分割)
进入中断服务函数后,g_Time_Count溢出次数变量开始自增,每自增一次代表10ms。
//#include "time.c"
void TIME_Init(uint16_t psc, uint16_t arr)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义TIM时基结构
NVIC_InitTypeDef NVIC_InitStructure; //定义NVIC(嵌套向量中断)结构体
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启低速总线时钟
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分割
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
TIM_TimeBaseInitStructure.TIM_Period = arr - 1; //自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler = psc - 1; //预分频系数
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除TIM的挂起标志
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //使能指定的TIM中断,允许更新中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置优先级分组:抢占优先级和子优先级
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //中断源
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //子优先级
NVIC_Init(&NVIC_InitStructure);
// TIM_Cmd(TIM2, ENABLE); //使能定时器 -- 这里暂时不要使能
}
uint16_t Time_Count;//TIM溢出次数 -- 10ms Tout(溢出时间) = (ARR+1)(PSC+1)/Tclk(时钟分割)
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) {
//进入中断表示定时器计数(CNT)溢出, 自增(10ms溢出一次, 可通过配置ARR, PSC和Tclk自定义溢出时间)
Time_Count++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
//#include "time.h"
extern uint16_t Time_Count; //外部声明--让key.c中也可以用到全局变量
void TIME_Init(uint16_t psc, uint16_t arr);
按键配置
首先,按键常规配置不详细介绍,加了外部中断双边沿触发,按下、松开都可以触发中断。
① 配置GPIO结构体初始化;
② 配置外部中断EXTI结构体初始化;
③ 配置NVIC结构体初始化;
GPIO_InitTypeDef GPIO_InitStructure; //定义GPIO结构体
EXTI_InitTypeDef EXTI_InitStructure; //定义EXTI(外部中断)结构体
NVIC_InitTypeDef NVIC_InitStructure; //定义NVIC(嵌套向量中断)结构体
其次、编写中断服务函数
进入中断服务函数后,读取宏定义KEY_STATA函数,判断按键是按下还是松开。
1、按下按键触发中断,按键状态位Keystate = 1进入到按下状态,清空定时器计数,使能定时器,开始计数。
2、松开按键又触发中断,按键状态位Keystate = 0进入到松开状态,失能定时器,看定时器计数来判断长短按键。
3.长按,因为长按的响应与短按的响应不同,短按是按下松开后响应,长按是按下后确定关机后才松开,所以长按的响应不能放在松开按键的响应中。
//#include "key.c"
void KEY_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义GPIO结构体
EXTI_InitTypeDef EXTI_InitStructure; //定义EXTI(外部中断)结构体
NVIC_InitTypeDef NVIC_InitStructure; //定义NVIC(嵌套向量中断)结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); //开启时钟
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PA0引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //输出频率
GPIO_Init(GPIOA, &GPIO_InitStructure); //结构体配置完成初始化
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); //选择EXTI的信号源
EXTI_InitStructure.EXTI_Line = EXTI_Line0; //选择EXIT事件线
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //EXTI为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;//双边沿中断
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //使能中断
EXTI_Init(&EXTI_InitStructure);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置优先级分组:抢占优先级和子优先级
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //中断源
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //子优先级
NVIC_Init(&NVIC_InitStructure);
}
void EXTI0_IRQHandler(void)
{
static uint8_t Keystate = 0; //静态按键状态 0 -- 松开 1 -- 按下
if(EXTI_GetITStatus(EXTI_Line0) != RESET) {
if(!KEY_STATA && Keystate) {
Keystate = 0; //松开按键
TIM_Cmd(TIM2, DISABLE); //关闭定时器
if(Time_Count < 3) {
//按键抖动
printf("...press, dither time = %d\n", Time_Count);
}
else if((Time_Count > 3) && (Time_Count < 200)) {
//短按、松开后响应
printf("stort press!\n");
}
else if(Time_Count > 200) {
//长按、松开后响应
printf("long press!\n");
}
}
else if(KEY_STATA && !Keystate){
Keystate = 1; //按下按键
Time_Count = 0; //清空定时器溢出次数
TIM_Cmd(TIM2, ENABLE); //启动定时器
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除中断标志
}
}
//#include "key.h"
#define KEY_STATE !!(GPIOA->IDR & 0x0001) //寄存器读取按键状态
#define KEY_STATA !!GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) //标准库读取按键状态
void KEY_Init(void); //外部声明
定时器中断服务函数更新
前面配置的定时器中断服务函数,可以实现长、短按,但是长按要按下按键默数2s,松开按键才知道是长按,但凡数快一点就成短按了,非常不方便,所以这里我稍作修改。
在按键中断服务函数中,去掉长按,将长按按键放到定时器中断服务函数中。
进入定时器中断服务函数后,Time_Count溢出次数变量开始自增,自增到200及以上,也就是2s以上,就是长按了,然后失能定时器,再清空溢出次数Time_Count = 0。
uint16_t Time_Count;//TIM溢出次数 -- 10ms Tout(溢出时间) = (ARR+1)(PSC+1)/Tclk(时钟分割)
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) {
//进入中断表示定时器计数(CNT)溢出, 自增(10ms溢出一次, 可通过配置ARR, PSC和Tclk自定义溢出时间)
Time_Count++;
if(Time_Count >= 200) {
TIM_Cmd(TIM2, DISABLE); //关闭定时器
Time_Count = 0; //清空溢出次数
printf("long press!\n");
}
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
void EXTI0_IRQHandler(void)
{
static uint8_t Keystate = 0; //静态按键状态 0 -- 松开 1 -- 按下
if(EXTI_GetITStatus(EXTI_Line0) != RESET) {
if(KEY_STATA && Keystate) {
Keystate = 0; //松开按键
TIM_Cmd(TIM2, DISABLE); //关闭定时器
if(Time_Count < 3) {
//按键抖动
printf("...press, dither time = %d\n", Time_Count);
}
else if((Time_Count > 3) && (Time_Count < 200)) {
//短按、松开后响应
printf("stort press!\n");
}
// else if(Time_Count > 200) {
// //长按、松开前响应, 所以不能放在这里
// printf("long press!\n");
// }
}
else if(!KEY_STATA && !Keystate){
Keystate = 1; //按下按键
Time_Count = 0; //清空定时器溢出次数
TIM_Cmd(TIM2, ENABLE); //启动定时器
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除中断标志
}
}
主函数
最后是main.c函数
//#include "main.c"
int main(void)
{
LED_Init();
KEY_Init();
TIME_Init(72, 10000);
Usart_Init();
LED_ON;
printf("KEY + TIM\n");
while (1) {
delay_ms(1000);
}
}
运行效果:
源码:链接:https://pan.baidu.com/s/1y3KtWEV_RaXZ5dUeP13dcw?pwd=r0jy
提取码:r0jy