简易按钮库开发记

在进行嵌入式系统开发时,按钮是最常见的输入设备之一。为了简化按钮的控制和管理,创建了一个简单的按钮库。最初,这个库只提供了基本的功能,但随着项目需求的增加,开始添加更多的功能,比如支持中断和实现按钮的多功能性。在这个过程中,逐渐理解了为什么市面上的按钮库会如此复杂。

在项目开发中,需要一个能够即时反馈用户操作的按钮,同时在某些情况下,按钮需要能够响应中断信号,或者根据用户的按压方式执行不同的功能。为了满足这些需求,开始着手开发一个更加完善的按钮库。

开发工具和硬件

在开发过程中,使用了以下工具和硬件:

  • VS Code 编辑器
  • PlatformIO 插件
  • TTGO T-Display v1 开发板

虽然可以使用不同的硬件,但需要根据硬件修改 platformio.iniconfig.hpp 中的引脚配置。

如何使用

首先,需要在项目中添加 htcw_button 库的依赖,并在代码中包含 。然后,可以在代码中实例化按钮对象,并为它们配置回调函数。

// 配置按钮 using button_1_t = button_ex; using button_2_t = button; static button_1_t button_1; static button_2_t button_2;

在上面的代码中,声明了两个按钮。第一个是一个扩展按钮,第二个是一个常规按钮。这个项目是为TTGO T-Display配置的,所以引脚定义为35和0,开路高。它们都有10毫秒的消抖时间,并且扩展按钮配置为中断驱动。

回调函数

接下来,需要为按钮配置回调函数。例如,可以为 button_1 配置点击和长按回调,为 button_2 配置一个回调。

button_1.on_click([](int clicks, void* state){ Serial.print("1 - on click: "); Serial.println(clicks); }); button_1.on_long_click([](void* state){ Serial.println("1 - on long click"); }); button_2.callback([](bool pressed, void* state){ Serial.print("2- "); Serial.println(pressed? "pressed" : "released"); });

在这里使用了简单的lambda表达式来将信息输出到串行端口。注意,出于性能原因,它们不能捕获任何值。使用 state 参数来传递值。

按钮更新

为了让回调函数能够触发,需要在任何希望按钮工作的循环中调用按钮的 update() 方法,就像在 loop() 方法中所做的那样:

// 更新所有按钮对象 button_1.update(); button_2.update();

当按钮被操作时,会在串行端口看到相应的消息。

代码解析

现在,让来探索一下这些按钮的内部工作原理。

首先,来看基础按钮。它主要由几个重要的部分组成。首先是初始化:

bool initialize() { if (m_pressed == -1) { m_last_change_ms = 0; if (open_high) { pinMode(pin, INPUT_PULLUP); } else { pinMode(pin, INPUT_PULLDOWN); } m_pressed = raw_pressed(); } return m_pressed != -1; }

在这里,检查按钮是否未初始化(m_pressed == -1),然后初始化成员变量,并根据按钮是开路高还是开路低设置引脚模式。最后,将 pressed 设置为按钮的当前值——按下或未按下。如果初始化成功,它将返回true,在这个例子中它总是会成功。

接下来是 update() 方法,它处理点击事件:

void update() { bool pressed = raw_pressed(); if (pressed != m_pressed) { uint32_t ms = millis(); if (ms - m_last_change_ms >= debounce_ms) { if (m_callback != nullptr) { m_callback(pressed, m_state); } m_pressed = pressed; m_last_change_ms = ms; } } }

在这里,获取按钮的底层值(raw_pressed()),如果它与上次记录的值不同,并且已经过去了 debounce_ms 毫秒,那么如果配置了回调函数,就会触发回调。注意,可以不使用回调,只使用 update()pressed()。一旦回调被触发,按下的值和上次更新时间就会被更新。

这个按钮要复杂得多。在之前的按钮中,update 例程收集按钮点击并报告它们。在这个按钮中,它们是两个独立的例程。此外,这个按钮保持了一个带有关联时间戳的按钮状态变化缓冲区。当注册按钮点击时,这个缓冲区会被填充,当报告按钮事件时,它会被清空。最后,当报告时,使用一个状态机来解析存储的按钮事件,并产生相应的回调。

中断处理

首先,这个按钮可以通过中断信号,这意味着每当按钮被按下或释放时,MCU的CPU会停止它正在做的任何其他事情,并处理按钮事件变化。这意味着一个启用中断的按钮会收集按下事件,即使 update() 不能被调用,比如在刷新电子纸显示屏的过程中。

update() 仍然必须被调用以实际触发事件。不能在中断例程中触发回调,因为回调中的代码可能不适合中断,这意味着回调必须在整个应用程序的生命周期内都在RAM中。

状态机

使用状态机允许对按钮进行复杂的分析,比如计算释放之间的时间,以允许在一个事件上触发多次点击,或者计算按下和释放之间的时间以进行长按,以及为以后扩展其他类型的点击提供空间。

中断例程

这里是中断例程,它收集按钮的按下和释放以及相关的时间戳:

#ifdef ESP32 IRAM_ATTR #endif static void process_change(void* instance) { type* this_ptr = (type*)instance; uint32_t ms = millis(); bool pressed = this_ptr->raw_pressed(); if (pressed != this_ptr->m_pressed) { if (ms - this_ptr->m_last_change_ms >= debounce_ms) { if (!this_ptr->m_events.full()) { this_ptr->m_events.put({ms, pressed}); this_ptr->m_pressed = pressed; this_ptr->m_last_change_ms = ms; } } } }

可以看到,一旦忽略了 this_ptr 的东西,这与旧按钮的 update() 例程非常相似,除了将事件放入 m_events 成员。指针的东西只是因为这是一个静态成员,所以必须将类实例作为通用状态参数 "instance" 传递,然后从那里重新构建类成员访问。

事件处理

这是相当复杂的部分——事件处理:

void update() { if (!initialize()) { return; } if (!use_interrupt) { process_change(this); } if (m_pressed == 1) { return; } if (m_last_change_ms != 0 && !m_events.empty() && millis() - m_last_change_ms >= double_click_ms) { event_entry_t ev; uint32_t press_ms = 0; int state = 0; int clicks = 0; int longp = 0; int done = 0; while (!done) { switch (state) { case 0: if (!m_events.get(&ev)) { done = true; break; } if (ev.state == 1) { state = 1; break; } else { while (ev.state != 1) { if (!m_events.get(&ev)) { done = true; break; } state = 1; } break; } case 1: ++clicks; press_ms = ev.ms; while (ev.state != 0) { if (!m_events.get(&ev)) { done = true; break; } state = 2; } break; case 2: longp = !!(m_on_long_click && ev.ms - press_ms >= long_click_ms); if (!m_events.get(&ev)) { if (m_on_click) { if (clicks > longp) { m_on_click(clicks - longp, m_on_click_state); } } if (longp) { m_on_long_click(m_on_long_click_state); } done = true; break; } state = 1; break; } } } }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485