Skip to content

Instantly share code, notes, and snippets.

@mikee47
Forked from Siapran/keys.h
Last active January 21, 2021 09:53
Show Gist options
  • Save mikee47/1b056a8462facdd000a0a83e77cba173 to your computer and use it in GitHub Desktop.
Save mikee47/1b056a8462facdd000a0a83e77cba173 to your computer and use it in GitHub Desktop.
#pragma once
#include "oled_gui.h"
template <typename T>
T clamped_sub(T lhs, T rhs)
{
return lhs > rhs ? lhs - rhs : 0;
}
template <const unsigned long VALID_MS,
const unsigned long HOLD_MS,
const unsigned long REPEAT_MS>
struct DebouncedKey
{
static constexpr uint32_t validTicks = OneShotFastMs::checkTime<VALID_MS>();
static constexpr uint32_t holdTicks = OneShotFastMs::checkTime<HOLD_MS>();
static constexpr uint32_t repeatTicks = OneShotFastMs::checkTime<REPEAT_MS>();
// comparing DURATIONS with < is allowed
// comparing TIMESTAMPS is not. use - and overflow math
// assume `now` is always measured at or after `last_change`
// returns true on debounced change
bool add_sample(bool sample)
{
switch(state) {
case State::Idle:
if(sample) {
timer.reset<VALID_MS>();
state = State::Debounce1;
}
break;
case State::Debounce1:
if (timer.expired()) {
if(sample) {
// Changing Idle -> Active
state = State::Active;
return true;
} else {
state = State::Idle;
}
} else if(!sample) {
timer.reset<VALID_MS>();
}
break;
case State::Active:
if(!sample) {
timer.reset<VALID_MS>();
state = State::Debounce2;
}
break;
case State::Debounce2:
if (timer.expired()) {
if(!sample) {
// Changing from Active -> Idle
state = State::Idle;
return true;
} else {
state = State::Active;
}
} else if(sample) {
timer.reset();
}
break;
}
// No change
return false;
}
bool has_changed()
{
switch(state) {
case State::Debounce1:
return add_sample(true);
case State::Debounce2:
return add_sample(false);
default:
return false;
}
}
bool is_set()
{
has_changed();
return state == State::Active;
}
bool is_held()
{
return is_set() && (timer.elapsedTicks() >= validTicks + holdTicks);
}
unsigned long count_repeats()
{
if(is_set()) {
return clamped_sub(timer.elapsedTicks(), validTicks + holdTicks) / repeatTicks;
}
return 0;
}
unsigned long consume_repeats()
{
unsigned long counts = count_repeats(now);
unsigned long consumed = clamped_sub(counts, repeats);
repeats = counts;
return consumed;
}
enum class State {
Idle,
Debounce1, // Debouncing Idle->Active
Active,
Debounce2, // Debouncing Active->Idle
};
OneShotFastMs timer;
State state{State::Idle};
unsigned long repeats = 0;
};
struct Key
{
const size_t pin;
char const* const name;
void (*action)(void);
DebouncedKey<DEBOUNCE_KEY_VALID_MS,
DEBOUNCE_KEY_HOLD_MS,
DEBOUNCE_KEY_REPEAT_MS>
debounced{};
};
Key keys[] = {{PIN_KEY_UP, "KEY_UP", upAction},
{PIN_KEY_DOWN, "KEY_DOWN", downAction},
{PIN_KEY_ENTER, "KEY_ENTER", enterAction},
{PIN_KEY_BACK, "KEY_BACK", backAction}};
void init_keys()
{
for (auto& key : keys)
{
attachInterrupt(
key.pin, InterruptDelegate([&key] {
if (key.debounced.add_sample(digitalRead(key.pin) == LOW))
{
if (key.debounced.is_set())
{
key.action();
}
}
}),
CHANGE);
pinMode(key.pin, INPUT_PULLUP);
}
}
void update_input()
{
for (auto& key : keys)
{
if (key.debounced.has_changed())
{
if (key.debounced.is_set())
{
key.action();
}
}
auto consumed = key.debounced.consume_repeats();
for (int i = 0; i < consumed; ++i)
{
key.action();
}
}
}
@slaff
Copy link

slaff commented Jan 21, 2021

@mikee47 - It would be great if we have a sample demonstrating how to press a button for a desired time and handle all debouncing logic as suggested here. This way new users can start directly testing and using the code in their projects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment