当前位置: 首页 > news >正文

esp32课设记录(一)按键的短按、长按与双击

        课程用的esp32的板子上只有一个按键,引脚几乎都被我用光了,很难再外置按键。怎么控制屏幕的gui呢?这就得充分利用按键了,比如说短按、长按与双击,实现不同的功能。

        咱们先从短按入手讲起。

        通过查看原理图,可见按键按下会接地,因此只需要把gpio口配置为input模式,上拉(确保不按下是高电平)即可,使用ESP-IDF框架提供的函数配置一下。

// 定义按钮引脚,当前使用GPIO9连接按钮(按下时接GND)
#define BUTTON_PIN 9/*** @brief 按钮初始化函数** 配置GPIO为输入模式,使用内部上拉电阻*/
void button_init(void)
{// 配置GPIO参数gpio_config_t io_conf = {.pin_bit_mask = (1ULL << BUTTON_PIN),  // 设置GPIO引脚位掩码.mode = GPIO_MODE_INPUT,               // 设置为输入模式.pull_up_en = GPIO_PULLUP_ENABLE,      // 启用内部上拉电阻.pull_down_en = GPIO_PULLDOWN_DISABLE, // 禁用内部下拉电阻.intr_type = GPIO_INTR_DISABLE,        // 禁用GPIO中断};// 应用GPIO配置gpio_config(&io_conf);
}

        ESP-IDF框架是基于FreeRTOS的。我打算使用轮询,不用外部中断,直接在app_main()的循环里面判断按键是否按下就行,和裸片编程差不多。

        vTaskDelay是FreeRTOS中使当前任务进入阻塞状态一段时间的函数,函数参数是系统节拍数。我们要将10ms转换为系统节拍数,而portTICK_PERIOD_MS表示一个系统时钟节拍(tick)对应的毫秒数,举个例子,假如portTICK_PERIOD_MS=2,意味着2ms一个系统节拍,10ms就是有5个系统节拍。因此10 / portTICK_PERIOD_MS就是将10毫秒转换为系统节拍数,vTaskDelay(10 / portTICK_PERIOD_MS);自然就是延迟10ms的意思。

// 在main.c的app_main()的主循环中
while (1) {// 检测按钮点击if (button_is_clicked()) {// 按钮被点击,执行相应操作}// 短暂延时,避免CPU占用过高vTaskDelay(10 / portTICK_PERIOD_MS);
}

        之后写一下button_is_clicked的逻辑。先记录last_state(上次按钮状态)和last_change_time(上次状态变化时间戳),再获取current_state(当前按钮状态)和now(当前状态变化时间戳)。如果不一样则更新状态与时间戳,此时理论上已经按下了。再次松手后,状态还会变化,此时判定当前时间与上次记录的时间,当大于20ms才返回true。

        为什么要记录last_state(上次按钮状态)和last_change_time(上次状态变化时间戳)呢?若不比较上次按钮状态和本次按钮状态,那我长按按键岂不是每10ms都被判定一次按下。上次状态变化时间戳纯粹为了消抖。

        那为何要释放瞬间才认为按键被触发了?这种设计有以下几个重要优势:

1. 完整性验证

确保用户完成了完整的按下-释放周期,而不是误触或抖动触发

可以区分有意识的点击和意外的触碰

2. 防止重复触发

如果在按下瞬间触发,按住按钮时会因轮询多次而重复触发

释放时触发保证每次物理点击只产生一次软件事件

3. 为长按和短按区分做铺垫

在释放时检测可以测量按钮被按下的持续时间

// 添加消抖延时函数
static void button_debounce_delay(void)
{vTaskDelay(20 / portTICK_PERIOD_MS);
}// 检测按钮单击事件(轮询方式)
bool button_is_clicked(void)
{static bool last_state = false;  // 记录上次按钮状态static int64_t last_change_time = 0;  // 上次状态变化时间// 获取当前按钮状态bool current_state = !gpio_get_level(BUTTON_PIN);int64_t now = esp_timer_get_time() / 1000;  // 当前时间(毫秒)// 如果状态变化太快(小于消抖时间),忽略它if (now - last_change_time < 20) {return false;}// 检测按钮从按下到释放的过程(完整点击)if (last_state && !current_state) {last_state = current_state;last_change_time = now;return true;  // 按钮刚被释放,返回点击事件}// 更新按钮状态if (last_state != current_state) {last_state = current_state;last_change_time = now;}return false;
}

        下面进入长按与双击的代码写作。这需要用到状态机。

        我们先梳理一下逻辑。首先事件肯定是空闲、短按、长按、双击。状态定义为空闲状态、按下状态、释放状态、双击状态。短按长按比较好写,直接记录按下到抬起的时间即可,也就是前面的now - last_change_time是否满足某一阈值。双击只要判断单击后是否在某个阈值内再次点击即可。

        因此,总体逻辑应该是这样的:首先是空闲状态。当按钮按下,进入按下状态当按钮松开后,测量按下的时间,到达阈值认为触发长按事件,直接进入空闲状态防止长按后还触发双击事件。没有到达阈值则进入释放状态。此时等待一段时间,在此时间内按键再次按下,判定触发双击事件,当释放后进入空闲状态。未按下则判定进入单击事件,随后进入空闲状态

        先定义事件与状态。

// 按钮事件类型
typedef enum
{BUTTON_EVENT_NONE,        // 无事件BUTTON_EVENT_SHORT_PRESS, // 短按BUTTON_EVENT_LONG_PRESS,  // 长按BUTTON_EVENT_DOUBLE_PRESS // 双击
} button_event_t;
// 按钮状态类型
typedef enum
{BUTTON_STATE_IDLE,        // 空闲状态BUTTON_STATE_PRESS,       // 按下状态BUTTON_STATE_RELEASE,     // 释放状态BUTTON_STATE_DOUBLE_CLICK // 双击状态
} button_state_t;

        编写获取按钮事件的函数,直接输出按钮事件类型。

//true - 表示之前的事件已被处理完毕,可以检测新事件
//false - 表示有一个事件已被检测到但尚未处理
bool event_processed = true;/*** @brief 获取按钮事件** @return 按钮事件类型*/
button_event_t button_get_event(void)
{static bool last_pressed = false;bool current_pressed = !gpio_get_level(BUTTON_PIN);int64_t current_time = esp_timer_get_time() / 1000;// 如果上一个事件尚未处理,则继续返回该事件if (!event_processed){return last_event;}// 状态机逻辑switch (button_state){case BUTTON_STATE_IDLE:if (current_pressed && !last_pressed){// 按钮从释放状态变为按下状态press_time = current_time;button_state = BUTTON_STATE_PRESS;}break;case BUTTON_STATE_PRESS:if (!current_pressed && last_pressed){// 按钮从按下状态变为释放状态release_time = current_time;// 检查是长按还是可能的短按/双击if (release_time - press_time >= BUTTON_LONG_PRESS_TIME){// 长按事件last_event = BUTTON_EVENT_LONG_PRESS;event_processed = false;button_state = BUTTON_STATE_IDLE; // 长按后直接回到IDLE状态}else{// 可能是短按或双击的第一次点击button_state = BUTTON_STATE_RELEASE;}}else if (current_pressed && (current_time - press_time >= BUTTON_LONG_PRESS_TIME)){// 长按事件(按住未释放但已达到时间阈值)last_event = BUTTON_EVENT_LONG_PRESS;event_processed = false;button_state = BUTTON_STATE_IDLE;}break;case BUTTON_STATE_RELEASE:if (current_pressed && !last_pressed){// 可能是双击的第二次按下if (current_time - release_time <= BUTTON_DOUBLE_CLICK_TIME){button_state = BUTTON_STATE_DOUBLE_CLICK;}}else if (current_time - release_time > BUTTON_DOUBLE_CLICK_TIME){// 超过双击时间窗口,确认为短按last_event = BUTTON_EVENT_SHORT_PRESS;event_processed = false;button_state = BUTTON_STATE_IDLE;}break;case BUTTON_STATE_DOUBLE_CLICK:if (!current_pressed && last_pressed){// 双击的第二次释放,确认为双击last_event = BUTTON_EVENT_DOUBLE_PRESS;event_processed = false;button_state = BUTTON_STATE_IDLE;}break;}// 更新上一次按钮状态last_pressed = current_pressed;// 返回事件if (!event_processed){return last_event;}return BUTTON_EVENT_NONE;
}

        一样在while循环里面写逻辑,直接获取button_get_event的事件,根据事件来执行相应操作。

        // 获取按钮事件button_event_t event = button_get_event();// 处理按钮事件if (event != BUTTON_EVENT_NONE){// 标记事件已处理event_processed = true;switch (event){case BUTTON_EVENT_SHORT_PRESS:// 短按break;case BUTTON_EVENT_LONG_PRESS:// 长按break;case BUTTON_EVENT_DOUBLE_PRESS:// 双击break;case BUTTON_EVENT_NONE:// 这种情况不应该发生// 但为了满足编译器的要求,添加此casebreak;}}// 短暂延时vTaskDelay(10 / portTICK_PERIOD_MS);

        结束!烧录代码,能够实现短按、长按与双击的检测。

相关文章:

  • 区间带边权并查集,XY4060泄露的测试点
  • pycharm连接github(详细步骤)
  • 如何利用 Java 爬虫获得某书笔记详情:实战指南
  • 面向GIS的Android studio移动开发(二)--在地图上绘制电子围栏
  • Spring AI Alibaba集成阿里云百炼大模型
  • 【已经解决诸多问题】Mamba安装
  • 延时双删-争议与我的思路-001
  • Neo4j数据库
  • 有哪些GIF图片转换的开源工具
  • 07 负载均衡
  • Linux的MySQL头文件和找不到头文件问题解决
  • windows多版本Python共存(大合集)
  • 方案精读:104页DeepSeek金融银行核算流程场景部署建设方案【附全文阅读】
  • LeetCode 155. 最小栈:Java 双栈解法详解
  • LWIP的Socket接口
  • SmartETL函数式组件的设计与应用
  • 【时时三省】(C语言基础)数组习题
  • 前端三剑客之HTML
  • LLM大语言模型系列1-token
  • 【AWS入门】Amazon SageMaker简介
  • 加快推进科技服务业高质量发展,九部门联合发文
  • 国家统计局:4月全国城镇调查失业率为5.1%,比上月下降0.1个百分点
  • 纽约市长称墨西哥海军帆船撞桥事故已致2人死亡
  • 墨西哥海军一载两百余人帆船撞上纽约布鲁克林大桥,多人落水
  • 价格周报|本周猪价继续下探,机构预计今年猪价中枢有支撑
  • 出走的苏敏阿姨一路走到了戛纳,这块红毯因她而多元