diff --git a/CMakeLists.txt b/CMakeLists.txt index f6f151a..cfbc679 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,6 +191,7 @@ target_sources( src/hal/adc.cpp src/modules/protocol.cpp src/modules/buttons.cpp + src/modules/debouncer.cpp src/modules/finda.cpp src/modules/leds.cpp ) diff --git a/src/modules/buttons.cpp b/src/modules/buttons.cpp index 94a0b6f..923a6a4 100644 --- a/src/modules/buttons.cpp +++ b/src/modules/buttons.cpp @@ -3,43 +3,9 @@ namespace modules { namespace buttons { -uint16_t Buttons::tmpTiming = 0; +Buttons buttons; -// original idea from: https://www.eeweb.com/debouncing-push-buttons-using-a-state-machine-approach -void Button::Step(uint16_t time, bool press) { - switch (f.state) { - case State::Waiting: - if (press) { - f.state = State::Detected; - timeLastChange = time; - f.tmp = press; - } - break; - case State::Detected: - if (f.tmp == press) { - if (time - timeLastChange > debounce) { - f.state = State::WaitForRelease; - } - } else { - f.state = State::Waiting; - } - break; - case State::WaitForRelease: - if (!press) { - f.state = State::Update; - } - break; - case State::Update: - f.state = State::Waiting; - timeLastChange = time; - f.tmp = false; - break; - default: - f.state = State::Waiting; - timeLastChange = time; - f.tmp = false; - } -} +uint16_t Buttons::tmpTiming = 0; int8_t Buttons::Sample(uint16_t rawADC) { // decode 3 buttons' levels from one ADC diff --git a/src/modules/buttons.h b/src/modules/buttons.h index 6f9fc53..8985d68 100644 --- a/src/modules/buttons.h +++ b/src/modules/buttons.h @@ -2,6 +2,7 @@ #include #include "../hal/adc.h" +#include "debouncer.h" /// Buttons are built on top of the raw ADC API /// This layer should contain debouncing of buttons and their logical interpretation @@ -9,43 +10,13 @@ namespace modules { namespace buttons { -struct Button { +struct Button : public debounce::Debouncer { inline constexpr Button() - : timeLastChange(0) {} - - /// @returns true if button is currently considered as pressed - inline bool Pressed() const { return f.state == State::WaitForRelease; } - - /// State machine stepping routine - void Step(uint16_t time, bool press); + : debounce::Debouncer(debounce) {} private: /// time interval for debouncing @@TODO specify units constexpr static const uint16_t debounce = 100; - - /// States of the debouncing automaton - /// Intentionally not modeled as an enum class - /// as it would impose additional casts which do not play well with the struct Flags - /// and would make the code less readable - enum State { Waiting = 0, - Detected, - WaitForRelease, - Update }; - - /// The sole purpose of this data struct is to save RAM by compressing several flags into one byte on the AVR - struct Flags { - uint8_t state : 2; ///< state of the button - uint8_t tmp : 1; ///< temporary state of button before the debouncing state machine finishes - inline constexpr Flags() - : state(State::Waiting) - , tmp(false) {} - }; - - /// Flags and state of the debouncing automaton - Flags f; - - /// Timestamp of the last change of ADC state for this button - uint16_t timeLastChange; }; class Buttons { @@ -71,5 +42,7 @@ private: static int8_t Sample(uint16_t rawADC); }; +extern Buttons buttons; + } // namespace buttons } // namespace modules diff --git a/src/modules/debouncer.cpp b/src/modules/debouncer.cpp new file mode 100644 index 0000000..eaffd0a --- /dev/null +++ b/src/modules/debouncer.cpp @@ -0,0 +1,43 @@ +#include "debouncer.h" + +namespace modules { +namespace debounce { + +// original idea from: https://www.eeweb.com/debouncing-push-buttons-using-a-state-machine-approach +void Debouncer::Step(uint16_t time, bool press) { + switch (f.state) { + case State::Waiting: + if (press) { + f.state = State::Detected; + timeLastChange = time; + f.tmp = press; + } + break; + case State::Detected: + if (f.tmp == press) { + if (time - timeLastChange > debounceTimeout) { + f.state = State::WaitForRelease; + } + } else { + f.state = State::Waiting; + } + break; + case State::WaitForRelease: + if (!press) { + f.state = State::Update; + } + break; + case State::Update: + f.state = State::Waiting; + timeLastChange = time; + f.tmp = false; + break; + default: + f.state = State::Waiting; + timeLastChange = time; + f.tmp = false; + } +} + +} // namespace debounce +} // namespace modules diff --git a/src/modules/debouncer.h b/src/modules/debouncer.h new file mode 100644 index 0000000..3388d12 --- /dev/null +++ b/src/modules/debouncer.h @@ -0,0 +1,53 @@ +/// A generic debouncing algorithm + +#pragma once +#include + +namespace modules { +namespace debounce { + +/// Implements debouncing on 2-state logic variables (true/false, high/low, on/off, pressed/unpressed) +/// Intentionally not modelled as a template to avoid code bloat +class Debouncer { +public: + inline constexpr Debouncer(uint8_t debounceTimeout) + : timeLastChange(0) + , debounceTimeout(debounceTimeout) {} + + /// @returns true if debounced value is currently considered as pressed + inline bool Pressed() const { return f.state == State::WaitForRelease; } + + /// State machine stepping routine + void Step(uint16_t time, bool press); + +private: + /// States of the debouncing automaton + /// Intentionally not modeled as an enum class + /// as it would impose additional casts which do not play well with the struct Flags + /// and would make the code less readable + enum State { + Waiting = 0, + Detected, + WaitForRelease, + Update + }; + + /// The sole purpose of this data struct is to save RAM by compressing several flags into one byte on the AVR + struct Flags { + uint8_t state : 2; ///< state of the debounced variable + uint8_t tmp : 1; ///< temporary state of variable before the debouncing state machine finishes + inline constexpr Flags() + : state(State::Waiting) + , tmp(false) {} + }; + + /// Flags and state of the debouncing automaton + Flags f; + + /// Timestamp of the last change of raw input state for this variable + uint16_t timeLastChange; + uint8_t debounceTimeout; +}; + +} // namespace debounce +} // namespace modules diff --git a/src/modules/finda.cpp b/src/modules/finda.cpp index 7b659fc..c42a6c3 100644 --- a/src/modules/finda.cpp +++ b/src/modules/finda.cpp @@ -6,13 +6,8 @@ namespace finda { FINDA finda; -uint8_t FINDA::Status() const { - // we can read ADC directly - return hal::adc::ReadADC(1) > 512; -} - -void FINDA::Step() { - // in this implementation FINDA doesn't need any stepping +void FINDA::Step(uint16_t time) { + debounce::Debouncer::Step(time, hal::adc::ReadADC(1) > adcDecisionLevel); } } // namespace finda diff --git a/src/modules/finda.h b/src/modules/finda.h index 74e9734..b54a4f7 100644 --- a/src/modules/finda.h +++ b/src/modules/finda.h @@ -1,17 +1,22 @@ #pragma once #include +#include "debouncer.h" namespace modules { namespace finda { -enum { On, - Off }; - -class FINDA { +class FINDA : protected debounce::Debouncer { public: - inline FINDA() = default; - uint8_t Status() const; - void Step(); + inline FINDA() + : debounce::Debouncer(debounce) {}; + void Step(uint16_t time); + using debounce::Debouncer::Pressed; + +private: + /// time interval for debouncing @@TODO specify units + constexpr static const uint16_t debounce = 100; + /// ADC decision level when a FINDA is considered pressed/not pressed + constexpr static const uint16_t adcDecisionLevel = 512; }; extern FINDA finda; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index bc08872..d755b48 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -2,9 +2,8 @@ add_custom_target(tests) # this had a reason on the Buddy FW, but on the AVR we cannot get close to having 16bit ints and -# pointers so the unit tests may be compiled 64bit afterall -set(CMAKE_C_FLAGS -m32) -set(CMAKE_CXX_FLAGS -m32) +# pointers so the unit tests may be compiled 64bit afterall set(CMAKE_C_FLAGS -m32) +# set(CMAKE_CXX_FLAGS -m32) # include catch_discover_tests function from Catch2 include(${Catch2_SOURCE_DIR}/contrib/Catch.cmake) diff --git a/tests/unit/modules/buttons/CMakeLists.txt b/tests/unit/modules/buttons/CMakeLists.txt index 1d2e9cc..925431f 100644 --- a/tests/unit/modules/buttons/CMakeLists.txt +++ b/tests/unit/modules/buttons/CMakeLists.txt @@ -1,5 +1,8 @@ # define the test executable -add_executable(buttons_tests ../../../../src/modules/buttons.cpp stub_adc.cpp test_buttons.cpp) +add_executable( + buttons_tests ../../../../src/modules/buttons.cpp ../../../../src/modules/debouncer.cpp + stub_adc.cpp test_buttons.cpp + ) # define required search paths target_include_directories(