65.9K
CodeProject 正在变化。 阅读更多。
Home

两个按钮的故事: 为 Arduino 构建物联网按钮库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (3投票s)

2022年12月10日

MIT

5分钟阅读

viewsIcon

11736

downloadIcon

102

构建一个按钮库看似简单,实则不然。

TTGO Button

引言

起初,我只需要一个能报告按下和释放的简单按钮,而我查看过的按钮库似乎都有些“杀鸡焉用牛刀”,于是我创建了一个简单的按钮库来处理这个非常简单的功能。

最终,我希望按钮具有更多功能,比如能够连接到中断,以及能够根据按下方式使按钮具有多功能,因此我开始制作第一个按钮的更全功能版本。

哦,天哪,我开始意识到我见过的按钮库为什么会如此复杂。管理多功能按键时序的代码很快就会变得棘手,并且处理中断也需要显著增加复杂性。尽管如此,我还是认为我可以用一种比我见过的更好的代码按钮处理方法做得更好。

我扩展的按钮仍然相对简单,并且我可能会在以后添加更多功能,但到目前为止,它已经满足了我现有项目以及未来的需求。对于需要即时反馈且不需要额外功能的场景,普通按钮仍然很有用。

必备组件

您需要安装了 PlatformIOVS Code

您需要一个 TTGO T-Display v1

您可以使用不同的硬件,但您需要相应地修改您的platformio.ini文件和config.hpp中的引脚配置。

使用这个烂摊子

一旦您将库依赖项添加到了htcw_button并将在您的项目中包含了<htcw_button.hpp>,您就可以在arduino命名空间下使用button<>button_ex<>了。

首先要做的是实例化模板,然后用这些具体类型来实例化*它们*。

// configure the buttons
using button_1_t = button_ex<PIN_BUTTON_1, 10, true, true>;
using button_2_t = button<PIN_BUTTON_2, 10, true>;

static button_1_t button_1;
static button_2_t button_2;

您可以看到我们声明了两个按钮。第一个是扩展按钮,第二个是普通按钮。该项目配置为 TTGO,因此引脚定义为 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参数来传递值。

为了使回调生效,您需要在任何想要它工作的循环中调用按钮的 pump 函数,就像我们在loop()函数中做的那样。

// pump all our objects
button_1.update();
button_2.update();

当按钮被操作时,您将在串口上看到相应的消息转储。

编写这个混乱的程序

嗯,这很简单!背后是怎么回事?

button<>

我们先来研究一下简单的按钮。

只有几个关键部分,我们会一一介绍。

首先,初始化

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,在本例中它总是成功的。

现在,pump 函数,用于处理点击

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() 来使用它,而无需回调。一旦触发了回调,`pressed` 值和上次更新时间就会被更新。

button_ex<>

这个按钮要复杂得多。在之前的按钮中,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) {
                    // pressed
                    state = 1;
                    break;
                } else {
                    // released
                    while(ev.state!=1) {
                        if(!m_events.get(&ev)) {
                            done = true;
                            break;
                        }
                        // pressed
                        state = 1;
                    }
                    break;
                }
            case 1: // press state
                ++clicks;
                press_ms = ev.ms;
                while(ev.state!=0) {
                    if(!m_events.get(&ev)) {
                        done = true;
                        break;
                    }
                    state = 2;
                }
                break;
            case 2: // release state
                longp = !!(m_on_long_click && 
                    ev.ms-press_ms>=long_click_ms);
                if(!m_events.get(&ev)) {
                    // flush the clicks
                    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;
            }
        }
    }
}

真是个难题。这花了我一段时间。基本上,在释放时,我们启动一个状态机来处理我们之前捕获的所有事件。我们只在距离上次按钮释放已经过了 double_click_ms 时间后才这样做,这样我们就有时间收集多次点击。然后我们从状态零开始,处理第一个事件,然后根据事件移动到状态 1(按下)或状态 2(释放)。从那里,我们基本上来回切换,增加点击次数(按下)或触发事件(释放)。

除了我们上面介绍的内容之外,没有太多其他的了。祝您编码愉快!

历史

  • 2022 年 12 月 9 日 - 初始提交
© . All rights reserved.