From da8cc9704ee41bb1ca9cb4800235a320541b79ed Mon Sep 17 00:00:00 2001 From: Armin Date: Wed, 27 May 2020 00:34:09 +0200 Subject: [PATCH] Version 3.0 Button release handler --- .github/workflows/LibraryBuild.yml | 5 +- README.md | 71 ++++--- examples/Callback/Callback.ino | 50 ++++- examples/DebounceTest/DebounceTest.ino | 5 +- examples/EasyButtonExample/ATtinySerialOut.h | 11 +- .../EasyButtonExample/EasyButtonExample.ino | 56 ++--- examples/OneButton/OneButton.ino | 2 +- library.properties | 4 +- src/EasyButtonAtInt01.cpp.h | 200 +++++++++++------- src/EasyButtonAtInt01.h | 67 ++++-- 10 files changed, 292 insertions(+), 179 deletions(-) diff --git a/.github/workflows/LibraryBuild.yml b/.github/workflows/LibraryBuild.yml index d1d3000..26d61f9 100644 --- a/.github/workflows/LibraryBuild.yml +++ b/.github/workflows/LibraryBuild.yml @@ -7,7 +7,7 @@ # This is the name of the workflow, visible on GitHub UI. name: LibraryBuild -on: +on: push: # see: https://help.github.com/en/actions/reference/events-that-trigger-workflows#pull-request-event-pull_request paths: - '**.ino' @@ -51,7 +51,7 @@ jobs: examples-build-properties: EasyButtonExample: -DTX_PIN=PB0 - + - arduino-boards-fqbn: digistump:avr:digispark-pro platform-url: https://raw.githubusercontent.com/ArminJo/DigistumpArduino/master/package_digistump_index.json @@ -75,4 +75,3 @@ jobs: platform-url: ${{ matrix.platform-url }} examples-exclude: ${{ matrix.examples-exclude }} examples-build-properties: ${{ toJson(matrix.examples-build-properties) }} - \ No newline at end of file diff --git a/README.md b/README.md index 043ba73..ea60e42 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # [EasyButton](https://github.com/ArminJo/EasyButtonAtInt01) Available as Arduino library "EasyButtonAtInt01" -### [Version 2.1.0](https://github.com/ArminJo/EasyButtonAtInt01/releases) +### [Version 3.0.0](https://github.com/ArminJo/EasyButtonAtInt01/releases) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Installation instructions](https://www.ardu-badge.com/badge/EasyButtonAtInt01.svg?)](https://www.ardu-badge.com/EasyButtonAtInt01) @@ -16,8 +16,9 @@ Debouncing is merely done by ignoring a button change within the debouncing time So **button state is instantly available** without debouncing delay! - Each button press toggles a state variable, so **no external logic for implementing a toggle button is needed**. - Support for **double press detection** is included. See EasyButtonExample and Callback example. -- Support for **long press detection**, is included. See EasyButtonExample example. -- Support to **measure maximum bouncing period of a button**. See EasyButtonExample example. +- Support for **long press detection**, is included. See Callback example. +- Support to **measure maximum bouncing period of a button**. See DebounceTest example. +- Support for **active high buttons**. ## Table of available pins for the 2 buttons | CPU | Button 0 | Button 1 using INT1 | Button 1 using PCINT, if INT1_PIN is defined !=3 | @@ -64,59 +65,68 @@ void loop() { } ``` -## Usage of callback function -The callback function is is called on every button press with ButtonToggleState as parameter.
+## Usage of callback functions +The button press callback function is is called on every button press with ButtonToggleState as parameter.
**The value at the first call (after first press) is true**.
-The callback function runs in an interrupt service context, which means it should be as short as possible. -But before callback function is called, interrupts are enabled. -This allows the timer interrupt for millis() to work and therfore **delay() and millis() can be used in the callback function**. +The button release callback function is called on every button release with the additional parameter ButtonPressDurationMillis.
+Both callback functions run in an interrupt service context, which means they should be as short as possible. +But before a callback function is called, interrupts are enabled. +This allows the timer interrupt for millis() to work and therfore **delay() and millis() can be used in a callback function**. ``` #define USE_BUTTON_0 // Enable code for button at INT0 (pin2) #include "EasyButtonAtInt01.cpp.h" -// initial value is false, so first call is with true -void printButtonToggleState(bool aButtonToggleState) { +// Initial value is false, so first call is with true +void handleButtonPress(bool aButtonToggleState) { digitalWrite(LED_BUILTIN, aButtonToggleState); } -EasyButton Button0AtPin2(&printButtonToggleState); +EasyButton Button0AtPin2(&handleButtonPress); void setup() {} void loop() {} ``` ## Long press detection -Check it cyclical in your loop. Do not forget, that you will get a callback (if enabled) at the start of the long press. -The blocking call only blocks if button is pressed, otherwise it returns immediately. +the easiest way is to check it in the button release handler. Do not forget, that you will get a press callback (if enabled) at the start of the long press. ``` -void loop() { -... - // default long press period is 400 ms - if (Button1AtPin3.checkForLongPressBlocking()) { - doSomething(); +#define USE_BUTTON_0 // Enable code for button at INT0 (pin2) +#include "EasyButtonAtInt01.cpp.h" + +void handleButtonRelease(bool aButtonToggleState, uint16_t aButtonPressDurationMillis); +EasyButton Button0AtPin2(true, NULL, &handleButtonRelease); // true -> button is connected to INT0 (pin2) + +handleButtonRelease(bool aButtonToggleState, uint16_t aButtonPressDurationMillis) { + if (aButtonPressDurationMillis >= EASY_BUTTON_LONG_PRESS_DEFAULT_MILLIS) { // 400 ms + Serial.print(F("Long press ")); + Serial.print(aButtonPressDurationMillis); + Serial.println(F(" ms detected")); } -... +} + +void setup() {} +void loop() {} } ``` ## Double press detection -Call checkForDoublePress() only from callback function. It will not work as expected called outside the callback function. +Call checkForDoublePress() only from button press callback function. It will not work as expected called outside this callback function. ``` #define USE_BUTTON_0 // Enable code for button at INT0 (pin2) #include "EasyButtonAtInt01.cpp.h" -void printButtonToggleState(bool aButtonToggleState); +void handleButtonPress(bool aButtonToggleState); EasyButton Button0AtPin2(&printButtonToggleState); -// initial value is false, so first call is with true -void printButtonToggleState(bool aButtonToggleState) { - digitalWrite(LED_BUILTIN, aButtonToggleState); - // This function works reliable only if called in callback function +// Initial value is false, so first call is with true +void handleButtonPress(bool aButtonToggleState) { + // This function works reliable only if called early in callback function if (Button0AtPin2.checkForDoublePress()) { Serial.println(F("Button 0 double press (< 400 ms) detected")); } + digitalWrite(LED_BUILTIN, aButtonToggleState); } void setup() {} @@ -134,7 +144,8 @@ If you are using Sloeber as your IDE, you can easily define global symbols at *P ## Class methods ``` EasyButton(bool aIsButtonAtINT0); // Constructor -EasyButton(bool aIsButtonAtINT0, void (*aButtonPressCallback)(bool aButtonToggleState)); // Constructor +EasyButton(bool aIsButtonAtINT0, void (*aButtonPressCallback)(bool aButtonToggleState)); +EasyButton(bool aIsButtonAtINT0, void (*aButtonPressCallback)(bool aButtonToggleState), void (*aButtonReleaseCallback)(bool aButtonToggleState, uint16_t aButtonPressDurationMillis)); void init(); // used by constructors #define EASY_BUTTON_LONG_PRESS_DEFAULT_MILLIS 400 @@ -150,13 +161,19 @@ bool checkForForButtonNotPressedTime(uint16_t aTimeoutMillis); ``` # Revision History +### Version 3.0.0 +- Added button release handler and adapted examples. +- Revoke change for "only one true result per press for checkForLongPressBlocking()". It is superseded by button release handler. +- Support buttons which are active high by defining `BUTTON_IS_ACTIVE_HIGH`. +- Improved detection of maximum bouncing period used in DebounceTest. + ### Version 2.1.0 - Avoid 1 ms delay for `checkForLongPressBlocking()` if button is not pressed. - Only one true result per press for `checkForLongPressBlocking()`. ### Version 2.0.0 - Ported to ATtinyX5 and ATiny167. -- Support also PinChangeInterrupt for button 1 on Pin PA0 to PA7 for ATtiniy87/167. +- Support also PinChangeInterrupt for button 1 on Pin PA0 to PA7 for ATtiny87/167. - Long button press detection support. - Analyzes maximum debouncing period. - Double button press detection support. diff --git a/examples/Callback/Callback.ino b/examples/Callback/Callback.ino index ac386a4..7d3b36b 100644 --- a/examples/Callback/Callback.ino +++ b/examples/Callback/Callback.ino @@ -30,10 +30,11 @@ #define USE_BUTTON_0 // Enable code for 1. button at INT0 (pin2) #include "EasyButtonAtInt01.cpp.h" -void showButtonToggleState(bool aButtonToggleState); // The callback function -EasyButton Button0AtPin2(&showButtonToggleState); // Only 1. button (USE_BUTTON_0) enabled -> button is connected to INT0 (pin2) +void handleButtonPress(bool aButtonToggleState); // The button press callback function +void handleButtonRelease(bool aButtonToggleState, uint16_t aButtonPressDurationMillis); +EasyButton Button0AtPin2(true, &handleButtonPress, &handleButtonRelease); // true -> button is connected to INT0 (pin2) -#define VERSION_EXAMPLE "2.1" +#define VERSION_EXAMPLE "3.0" #if defined(ARDUINO_AVR_DIGISPARK) #define LED_BUILTIN PB1 @@ -43,6 +44,8 @@ EasyButton Button0AtPin2(&showButtonToggleState); // Only 1. button (USE_B #elif ! defined(LED_BUILTIN) #define LED_BUILTIN PB1 // define port of built in LED for your ATtiny #endif +#define BLINK_SHORT_MILLIS 200 +#define BLINK_LONG_MILLIS 600 void setup() { pinMode(LED_BUILTIN, OUTPUT); @@ -54,22 +57,57 @@ void setup() { #endif // Just to know which program is running on my Arduino Serial.println(F("START " __FILE__ "\r\nVersion " VERSION_EXAMPLE " from " __DATE__)); + Serial.println(F("Using library version " VERSION_EASY_BUTTON)); } void loop() { delay(10); } +void blinkLEDBlocking(uint8_t aLedPin, uint16_t aDelay, uint8_t aRepetitions) { + for (uint8_t i = 0; i < aRepetitions; ++i) { + digitalWrite(aLedPin, HIGH); + delay(aDelay); + digitalWrite(aLedPin, LOW); + delay(aDelay); + } +} + /* * The callback function is called at each button press * Initial value is false, so first call is with true */ -void showButtonToggleState(bool aButtonToggleState) { - digitalWrite(LED_BUILTIN, aButtonToggleState); +void handleButtonPress(bool aButtonToggleState) { /* * Double press (< 200 ms) detection by calling checkForForDoublePress() once at button press time. */ if (Button0AtPin2.checkForDoublePress(300)) { - Serial.println(F("Double press (< 300 ms) detected")); + Serial.print(F("Double press ")); + Serial.print(Button0AtPin2.ButtonLastChangeMillis - Button0AtPin2.ButtonReleaseMillis); + Serial.println(F(" ms detected")); + + // let the led blink twice short + blinkLEDBlocking(LED_BUILTIN, BLINK_SHORT_MILLIS, 2); + Button0AtPin2.ButtonToggleState = false; + } + Serial.println(F("Button pressed")); + digitalWrite(LED_BUILTIN, aButtonToggleState); +} + +void handleButtonRelease(bool aButtonToggleState, uint16_t aButtonPressDurationMillis) { + digitalWrite(LED_BUILTIN, aButtonToggleState); + Serial.println(F("Button released")); + + /* + * Simple long press (> 400 ms) detection + */ + if (aButtonPressDurationMillis >= EASY_BUTTON_LONG_PRESS_DEFAULT_MILLIS) { + Serial.print(F("Long press ")); + Serial.print(aButtonPressDurationMillis); + Serial.println(F(" ms detected")); + + // let the led blink long + blinkLEDBlocking(LED_BUILTIN, BLINK_LONG_MILLIS, 2); + Button0AtPin2.ButtonToggleState = false; } } diff --git a/examples/DebounceTest/DebounceTest.ino b/examples/DebounceTest/DebounceTest.ino index b0958a6..283f5a6 100644 --- a/examples/DebounceTest/DebounceTest.ino +++ b/examples/DebounceTest/DebounceTest.ino @@ -28,7 +28,6 @@ //#define USE_ATTACH_INTERRUPT // enable it if you get the error " multiple definition of `__vector_1'" (or `__vector_2') //#define MEASURE_INTERRUPT_TIMING -//#define DO_NOT_REQUIRE_LONG_AND_DOUBLE_PRESS #define ANALYZE_MAX_BOUNCING_PERIOD #define BUTTON_DEBOUNCING_MILLIS 2 @@ -38,7 +37,7 @@ EasyButton Button0AtPin2; // Only 1. button (USE_BUTTON_0) enabled -> button is connected to INT0 -#define VERSION_EXAMPLE "2.0" +#define VERSION_EXAMPLE "3.0" #if defined(ARDUINO_AVR_DIGISPARK) #define LED_BUILTIN PB1 @@ -62,7 +61,9 @@ void setup() { #endif // Just to know which program is running on my Arduino Serial.println(F("START " __FILE__ "\r\nVersion " VERSION_EXAMPLE " from " __DATE__)); + Serial.println(F("Using library version " VERSION_EASY_BUTTON)); Serial.println(F("Button debouncing time is reduced to " STR(BUTTON_DEBOUNCING_MILLIS) " ms")); + Serial.println(F("Please press the button and watch for \"Bouncing, MBP=...\" output at the Serial Monitor")); } void loop() { diff --git a/examples/EasyButtonExample/ATtinySerialOut.h b/examples/EasyButtonExample/ATtinySerialOut.h index f8f3755..4922c11 100644 --- a/examples/EasyButtonExample/ATtinySerialOut.h +++ b/examples/EasyButtonExample/ATtinySerialOut.h @@ -49,12 +49,16 @@ // +----+ // -#ifndef TINY_SERIAL_OUT_H_ -#define TINY_SERIAL_OUT_H_ +#ifndef ATTINY_SERIAL_OUT_H_ +#define ATTINY_SERIAL_OUT_H_ #if defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) || defined(__AVR_ATtiny87__) || defined(__AVR_ATtiny167__) #include +#define VERSION_ATTINY_SERIAL_OUT "1.1.0" +#define VERSION_ATTINY_SERIAL_OUT_MAJOR 1 +#define VERSION_ATTINY_SERIAL_OUT_MINOR 1 + #if (F_CPU != 1000000) && (F_CPU != 8000000) && (F_CPU != 16000000) #error "F_CPU value must be 1000000, 8000000 or 16000000." #endif @@ -179,6 +183,7 @@ class TinySerialOut // virtual functions of Print class size_t write(uint8_t aByte); + operator bool() { return true; } // To support "while (!Serial); // wait for serial port to connect. Needed for Leonardo only #if !defined(TINY_SERIAL_INHERIT_FROM_PRINT) void print(const __FlashStringHelper * aStringPtr); @@ -220,6 +225,6 @@ extern TinySerialOut Serial; #endif // defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) || defined(__AVR_ATtiny87__) || defined(__AVR_ATtiny167__) -#endif /* TINY_SERIAL_OUT_H_ */ +#endif /* ATTINY_SERIAL_OUT_H_ */ #pragma once diff --git a/examples/EasyButtonExample/EasyButtonExample.ino b/examples/EasyButtonExample/EasyButtonExample.ino index 3433389..7bbcfdd 100644 --- a/examples/EasyButtonExample/EasyButtonExample.ino +++ b/examples/EasyButtonExample/EasyButtonExample.ino @@ -28,7 +28,7 @@ //#define USE_ATTACH_INTERRUPT // enable it if you get the error " multiple definition of `__vector_1'" (or `__vector_2') //#define BUTTON_DEBOUNCING_MILLIS 80 // With this you can adapt to the characteristic of your button. Default is 50. -#define ANALYZE_MAX_BOUNCING_PERIOD +//#define ANALYZE_MAX_BOUNCING_PERIOD // Analyze the button actual debounce value #define USE_BUTTON_0 // Enable code for button 0 at INT0. #define USE_BUTTON_1 // Enable code for button 1 at INT1 or PCINT[0:7] @@ -61,12 +61,11 @@ #include "EasyButtonAtInt01.cpp.h" // The callback function for button 1 -void printButtonToggleState(bool aButtonToggleState); +void handleButtonPress(bool aButtonToggleState); +EasyButton Button0AtPin2(true, &handleButtonPress); // true -> button is connected to INT0 +EasyButton Button1AtPin3((bool) false); // false -> button is not connected to INT0 but connected to INT1 or PCINT[0:7]. (bool) to avoid overloaded warning for digispark compiler. -EasyButton Button0AtPin2(true, &printButtonToggleState); // true -> button is connected to INT0 -EasyButton Button1AtPin3((bool)false); // false -> button is not connected to INT0 but connected to INT1 or PCINT[0:7]. (bool) to avoid overloaded warning for digispark compiler. - -#define VERSION_EXAMPLE "2.1" +#define VERSION_EXAMPLE "3.0" long sOldDeltaMillis; @@ -81,7 +80,7 @@ void setup() { #endif // Just to know which program is running on my Arduino Serial.println(F("START " __FILE__ "\r\nVersion " VERSION_EXAMPLE " from " __DATE__)); - + Serial.println(F("Using library version " VERSION_EASY_BUTTON)); } void loop() { @@ -108,51 +107,20 @@ void loop() { Serial.print(Button1AtPin3.ButtonPressDurationMillis); Serial.print(F(" ms")); } - -#if defined(ANALYZE_MAX_BOUNCING_PERIOD) - /* - * Print max bouncing period for the button - */ - if (Button1AtPin3.MaxBouncingPeriodMillisHasJustChanged) { - Button1AtPin3.MaxBouncingPeriodMillisHasJustChanged = false; - Serial.print(F(" MaxBouncingPeriod=")); - Serial.print(Button1AtPin3.MaxBouncingPeriodMillis); - } -#endif Serial.println(); } +} +void handleButtonPress(bool aButtonToggleState) { /* - * Long press (> 400 ms) detection function for button 0 can be called in loop + * This function works reliable only if called early in press callback function */ - if (Button0AtPin2.checkForLongPressBlocking()) { - Serial.println(F("Button 0 long press (> 400 ms) detected")); + if (Button0AtPin2.checkForDoublePress()) { + Serial.println(F("Button 0 double press (< 400 ms) detected")); } -} -void printButtonToggleState(bool aButtonToggleState) { digitalWrite(LED_BUILTIN, aButtonToggleState); Serial.print("Button 0 ToggleState="); - Serial.print(aButtonToggleState); - -#if defined(ANALYZE_MAX_BOUNCING_PERIOD) - /* - * Print max bouncing period for the button - */ - if (Button0AtPin2.MaxBouncingPeriodMillisHasJustChanged) { - Button0AtPin2.MaxBouncingPeriodMillisHasJustChanged = false; - Serial.print(F(" MaxBouncingPeriod=")); - Serial.print(Button0AtPin2.MaxBouncingPeriodMillis); - } -#endif - Serial.println(); - - /* - * This function works reliable only if called in callback function - */ - if (Button0AtPin2.checkForDoublePress()) { - Serial.println(F("Button 0 double press (< 400 ms) detected")); - } + Serial.println(aButtonToggleState); } - diff --git a/examples/OneButton/OneButton.ino b/examples/OneButton/OneButton.ino index b79efa7..f943ea9 100644 --- a/examples/OneButton/OneButton.ino +++ b/examples/OneButton/OneButton.ino @@ -28,7 +28,6 @@ //#define USE_ATTACH_INTERRUPT // enable it if you get the error " multiple definition of `__vector_1'" (or `__vector_2') #define USE_BUTTON_0 // Enable code for 1. button at INT0 / D2 -#define DO_NOT_REQUIRE_LONG_AND_DOUBLE_PRESS #include "EasyButtonAtInt01.cpp.h" @@ -55,6 +54,7 @@ void setup() { #endif // Just to know which program is running on my Arduino Serial.println(F("START " __FILE__ "\r\nVersion " VERSION_EXAMPLE " from " __DATE__)); + Serial.println(F("Using library version " VERSION_EASY_BUTTON)); } void loop() { diff --git a/library.properties b/library.properties index f8d4988..80bdfec 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,9 @@ name=EasyButtonAtInt01 -version=2.1.0 +version=3.0.0 author=Armin Joachimsmeyer maintainer=Armin Joachimsmeyer sentence=Small and easy to use Arduino library for using push buttons at INT0 and / or INT1 pin using interrupts.

Just connect buttons between ground and pin 2 or 3 of your Arduino - that's it

No call of begin() or update() function needed, no polling function to call. No blocking debouncing.
-paragraph=INT0 and INT1 are connected to Pin 2 / 3 on most Arduinos (ATmega328), to PB6 / PA3 on ATtiny167 and on ATtinyX5 we have only INT0 at PB2.

In you main program define an EasyButton and use ButtonStateIsActive or ButtonToggleState to determine your action.
Or use a callback function which will be called once on every button press.

Usage:
#define USE_BUTTON_0
#include "EasyButtonAtInt01.cpp.h"
EasyButton Button0AtPin2;

void setup() {}
void loop() {
...
digitalWrite(LED_BUILTIN, Button0AtPin2.ButtonToggleState);
...
}

Functions for long and double press detection are included.

New: Revisited long and double press functions. +paragraph=INT0 and INT1 are connected to Pin 2 / 3 on most Arduinos (ATmega328), to PB6 / PA3 on ATtiny167 and on ATtinyX5 we have only INT0 at PB2.

In you main program define an EasyButton and use ButtonStateIsActive or ButtonToggleState to determine your action.
Or use a callback function which will be called once on every button press or release.

Usage:
#define USE_BUTTON_0
#include "EasyButtonAtInt01.cpp.h"
EasyButton Button0AtPin2;

void setup() {}
void loop() {
...
digitalWrite(LED_BUILTIN, Button0AtPin2.ButtonToggleState);
...
}

Functions for long and double press detection are included.

New: Added button release handler. category=Signal Input/Output url=https://github.com/ArminJo/EasyButtonAtInt01 architectures=avr diff --git a/src/EasyButtonAtInt01.cpp.h b/src/EasyButtonAtInt01.cpp.h index 0d4a281..094823c 100644 --- a/src/EasyButtonAtInt01.cpp.h +++ b/src/EasyButtonAtInt01.cpp.h @@ -12,7 +12,7 @@ * * Usage: * #define USE_BUTTON_0 - * #include "EasyButtonAtInt01.h" + * #include "EasyButtonAtInt01.cpp.h" * EasyButton Button0AtPin2(true); * * Copyright (C) 2018 Armin Joachimsmeyer @@ -41,7 +41,7 @@ * Usage: * #define USE_BUTTON_0 // Enable code for button at INT0 (pin2 on 328P, PB6 on ATtiny167, PB2 on ATtinyX5) * #define USE_BUTTON_1 // Enable code for button at INT1 (pin3 on 328P, PA3 on ATtiny167, PCINT0 / PCx for ATtinyX5) - * #include "EasyButtonAtInt01.h" + * #include "EasyButtonAtInt01.cpp.h" * EasyButton Button0AtPin2(true); // true -> Button is connected to INT0 * EasyButton Button0AtPin3(false, &Button3CallbackHandler); // false -> button is not connected to INT0 => connected to INT1 * ... @@ -51,8 +51,8 @@ */ // For external measurement of code timing -//#define MEASURE_INTERRUPT_TIMING -#if defined(MEASURE_INTERRUPT_TIMING) || defined (LED_FEEDBACK_TEST) +//#define MEASURE_EASY_BUTTON_INTERRUPT_TIMING +#if defined(MEASURE_EASY_BUTTON_INTERRUPT_TIMING) || defined (LED_FEEDBACK_TEST) #include "digitalWriteFast.h" #endif @@ -117,13 +117,34 @@ EasyButton::EasyButton(bool aIsButtonAtINT0 __attribute__((unused)), void (*aBut #endif } +#if ! defined(NO_BUTTON_RELEASE_CALLBACK) +# if defined(USE_BUTTON_0) && defined(USE_BUTTON_1) +EasyButton::EasyButton(bool aIsButtonAtINT0, void (*aButtonPressCallback)(bool aButtonToggleState), + void (*aButtonReleaseCallback)(bool aButtonToggleState, uint16_t aButtonPressDurationMillis)) +# else +EasyButton::EasyButton(bool aIsButtonAtINT0 __attribute__((unused)), void (*aButtonPressCallback)(bool aButtonToggleState), + void (*aButtonReleaseCallback)(bool aButtonToggleState, uint16_t aButtonPressDurationMillis)) +# endif + { + ButtonPressCallback = aButtonPressCallback; + ButtonReleaseCallback = aButtonReleaseCallback; +# if defined(USE_BUTTON_0) && not defined(USE_BUTTON_1) +init(true); // 1. button +# elif defined(USE_BUTTON_1) && not defined(USE_BUTTON_0) +init(false); // 2. button +# else + init(aIsButtonAtINT0); +# endif +} +#endif // NO_BUTTON_RELEASE_CALLBACK + /* - * Sets pin 2 mode to INPUT_PULLUP and enables INT0 Interrupt on any logical change. + * Sets pin mode to INPUT_PULLUP if not defined(BUTTON_IS_ACTIVE_HIGH) and enables INT0 Interrupt on any logical change. */ void EasyButton::init(bool aIsButtonAtINT0) { isButtonAtINT0 = aIsButtonAtINT0; -#if defined(MEASURE_INTERRUPT_TIMING) - pinModeFast(BUTTON_TEST_TIMING_PIN, OUTPUT); +#if defined(MEASURE_EASY_BUTTON_INTERRUPT_TIMING) + pinModeFast(INTERRUPT_TIMING_OUTPUT_PIN, OUTPUT); #endif #if defined(LED_FEEDBACK_TEST) @@ -135,7 +156,9 @@ void EasyButton::init(bool aIsButtonAtINT0) { * Only button 0 requested */ INT0_DDR_PORT &= ~(_BV(INT0_BIT)); // pinModeFast(2, INPUT_PULLUP); - INT0_OUT_PORT |= _BV(INT0_BIT); +# if ! defined(BUTTON_IS_ACTIVE_HIGH) + INT0_OUT_PORT |= _BV(INT0_BIT); // enable pullup +# endif sPointerToButton0ForISR = this; # if defined(USE_ATTACH_INTERRUPT) attachInterrupt(digitalPinToInterrupt(INT0_PIN), &handleINT0Interrupt, CHANGE); @@ -150,7 +173,9 @@ void EasyButton::init(bool aIsButtonAtINT0) { * Only button 1 requested */ INT1_DDR_PORT &= ~(_BV(INT1_BIT)); - INT1_OUT_PORT |= _BV(INT1_BIT); +# if ! defined(BUTTON_IS_ACTIVE_HIGH) + INT1_OUT_PORT |= _BV(INT1_BIT); // enable pullup +# endif sPointerToButton1ForISR = this; # if (! defined(ISC10)) || ((defined(__AVR_ATtiny87__) || defined(__AVR_ATtiny167__)) && INT1_PIN != 3) @@ -186,8 +211,10 @@ void EasyButton::init(bool aIsButtonAtINT0) { /* * Button 0 */ - INT0_DDR_PORT &= ~(_BV(INT0_BIT)); // pinModeFast(2, INPUT_PULLUP); - INT0_OUT_PORT |= _BV(INT0_BIT); + INT0_DDR_PORT &= ~(_BV(INT0_BIT)); // pinModeFast(2, INPUT); +# if ! defined(BUTTON_IS_ACTIVE_HIGH) + INT0_OUT_PORT |= _BV(INT0_BIT); // enable pullup +# endif sPointerToButton0ForISR = this; # if defined(USE_ATTACH_INTERRUPT) attachInterrupt(digitalPinToInterrupt(INT0_PIN), &handleINT0Interrupt, CHANGE); @@ -202,7 +229,9 @@ void EasyButton::init(bool aIsButtonAtINT0) { * Allow PinChangeInterrupt */ INT1_DDR_PORT &= ~(_BV(INT1_BIT)); // pinModeFast(INT1_BIT, INPUT_PULLUP); - INT1_OUT_PORT |= _BV(INT1_BIT); +# if ! defined(BUTTON_IS_ACTIVE_HIGH) + INT1_OUT_PORT |= _BV(INT1_BIT); // enable pullup +# endif sPointerToButton1ForISR = this; # if (! defined(ISC10)) || ((defined(__AVR_ATtiny87__) || defined(__AVR_ATtiny167__)) && INT1_PIN != 3) @@ -241,19 +270,37 @@ void EasyButton::init(bool aIsButtonAtINT0) { } /* - * Negative logic for readButtonState() true means button pin is LOW + * Negative logic for readButtonState() true means button pin is LOW, if button is active low (default) */ bool EasyButton::readButtonState() { #if defined(USE_BUTTON_0) && not defined(USE_BUTTON_1) +# if defined(BUTTON_IS_ACTIVE_HIGH) + return (INT0_IN_PORT & _BV(INT0_BIT)); // = digitalReadFast(2); +# else return !(INT0_IN_PORT & _BV(INT0_BIT)); // = digitalReadFast(2); +# endif + #elif defined(USE_BUTTON_1) && not defined(USE_BUTTON_0) - return !(INT1_IN_PORT & _BV(INT1_BIT)); // = digitalReadFast(3); +# if defined(BUTTON_IS_ACTIVE_HIGH) + return (INT1_IN_PORT & _BV(INT1_BIT)); // = digitalReadFast(3); +# else + return !(INT1_IN_PORT & _BV(INT1_BIT)); // = digitalReadFast(3); +# endif + #elif defined(USE_BUTTON_0) && defined(USE_BUTTON_1) +# if defined(BUTTON_IS_ACTIVE_HIGH) + if (isButtonAtINT0) { + return (INT0_IN_PORT & _BV(INT0_BIT)); // = digitalReadFast(2); + } else { + return (INT1_IN_PORT & _BV(INT1_BIT)); // = digitalReadFast(3); + } +# else if (isButtonAtINT0) { return !(INT0_IN_PORT & _BV(INT0_BIT)); // = digitalReadFast(2); } else { return !(INT1_IN_PORT & _BV(INT1_BIT)); // = digitalReadFast(3); } +# endif #endif } @@ -276,8 +323,8 @@ bool EasyButton::updateButtonState() { noInterrupts(); if (readDebouncedButtonState() != ButtonStateIsActive) { #ifdef TRACE - if (LastChangeWasBouncingToInactive) { - Serial.print(F("Updated button state, last button press was shorter than debouncing period of ")); + if (LastBounceWasChangeToInactive) { + Serial.print(F("Updated button state, assume last button press was shorter than debouncing period of ")); Serial.print(BUTTON_DEBOUNCING_MILLIS); Serial.print(F(" ms")); #ifdef ANALYZE_MAX_BOUNCING_PERIOD @@ -314,6 +361,7 @@ uint16_t EasyButton::updateButtonPressDuration() { /* * Used for long button press recognition, while button is still pressed! + * !!! Consider to use button release callback handler and check the ButtonPressDurationMillis * returns EASY_BUTTON_LONG_PRESS_DETECTED, EASY_BUTTON_LONG_PRESS_STILL_POSSIBLE and EASY_BUTTON_LONG_PRESS_ABORT */ uint8_t EasyButton::checkForLongPress(uint16_t aLongPressThresholdMillis) { @@ -337,32 +385,27 @@ uint8_t EasyButton::checkForLongPress(uint16_t aLongPressThresholdMillis) { /* * Checks for long press of button * Blocks until long press threshold is reached or button was released. + * !!! Consider to use button release callback handler and check the ButtonPressDurationMillis * @return true if long press was detected - only once for each long press */ bool EasyButton::checkForLongPressBlocking(uint16_t aLongPressThresholdMillis) { - if (!ButtonLongPressJustDetected) { - /* - * wait as long as button is pressed shorter than threshold millis. - */ - while (checkForLongPress(aLongPressThresholdMillis) == EASY_BUTTON_LONG_PRESS_STILL_POSSIBLE) { - delay(1); - } - /* - * Here button was not presses or time was greater than threshold. - * ButtonPressDurationMillis was updated by call to checkForLongPress before - */ - if (ButtonPressDurationMillis >= aLongPressThresholdMillis) { - ButtonLongPressJustDetected = true; - return true; - } + /* + * wait as long as button is pressed shorter than threshold millis. + */ + while (checkForLongPress(aLongPressThresholdMillis) == EASY_BUTTON_LONG_PRESS_STILL_POSSIBLE) { + delay(1); } - return false; + /* + * Here button was not pressed or time was greater than threshold. + * ButtonPressDurationMillis was updated by call to checkForLongPress before + */ + return (ButtonPressDurationMillis >= aLongPressThresholdMillis); } /* * Double press detection by computing difference between current (active) timestamp ButtonLastChangeMillis * and last release timestamp ButtonReleaseMillis. - * !!!Works only reliable if called in callback function!!! + * !!!Works only reliable if called early in ButtonPress callback function!!! * @return true if double press detected. */ bool EasyButton::checkForDoublePress(uint16_t aDoublePressDelayMillis) { @@ -398,8 +441,10 @@ void EasyButton::handleINT01Interrupts() { */ #if defined(USE_BUTTON_0) && not defined(USE_BUTTON_1) tCurrentButtonStateIsActive = INT0_IN_PORT & _BV(INT0_BIT); // = digitalReadFast(2); + #elif defined(USE_BUTTON_1) && not defined(USE_BUTTON_0) tCurrentButtonStateIsActive = INT1_IN_PORT & _BV(INT1_BIT); // = digitalReadFast(3); + #elif defined(USE_BUTTON_0) && defined(USE_BUTTON_1) if (isButtonAtINT0) { tCurrentButtonStateIsActive = INT0_IN_PORT & _BV(INT0_BIT); // = digitalReadFast(2); @@ -407,7 +452,10 @@ void EasyButton::handleINT01Interrupts() { tCurrentButtonStateIsActive = INT1_IN_PORT & _BV(INT1_BIT); // = digitalReadFast(3); } #endif +#if ! defined(BUTTON_IS_ACTIVE_HIGH) tCurrentButtonStateIsActive = !tCurrentButtonStateIsActive; // negative logic for tCurrentButtonStateIsActive! true means button pin is LOW +#endif + #ifdef TRACE Serial.print(tCurrentButtonStateIsActive); Serial.print('-'); @@ -417,25 +465,22 @@ void EasyButton::handleINT01Interrupts() { unsigned int tDeltaMillis = tMillis - ButtonLastChangeMillis; // Check for bouncing - state change during debounce period if (tDeltaMillis <= BUTTON_DEBOUNCING_MILLIS) { -#ifdef ANALYZE_MAX_BOUNCING_PERIOD - if (MaxBouncingPeriodMillis < tDeltaMillis) { - MaxBouncingPeriodMillis = tDeltaMillis; - MaxBouncingPeriodMillisHasJustChanged = true; - } -#endif /* * Button is bouncing, signal is ringing - do nothing, ignore and wait for next interrupt */ - if (tCurrentButtonStateIsActive) { - LastChangeWasBouncingToInactive = false; -#ifdef TRACE +#ifdef ANALYZE_MAX_BOUNCING_PERIOD + if (MaxBouncingPeriodMillis < tDeltaMillis) { + MaxBouncingPeriodMillis = tDeltaMillis; Serial.print(F("Bouncing, MBP=")); Serial.println(MaxBouncingPeriodMillis); // Serial.print(F("ms=")); // Serial.print(tMillis); // Serial.print(F(" D=")); // Serial.println(tDeltaMillis); + } #endif + if (tCurrentButtonStateIsActive) { + LastBounceWasChangeToInactive = false; } else { /* * Store, that switch goes inactive during debouncing period. @@ -443,32 +488,22 @@ void EasyButton::handleINT01Interrupts() { * In this case we do not set the ButtonStateIsActive to false because we are in debouncing period. * On the next press this will be detected as a spike, if not considered. */ - LastChangeWasBouncingToInactive = true; -#ifdef TRACE - Serial.print(F("Bouncing, MBP=")); - Serial.println(MaxBouncingPeriodMillis); -#endif + LastBounceWasChangeToInactive = true; } } else { /* * Here we are after debouncing period */ - if (tCurrentButtonStateIsActive == ButtonStateIsActive) { - if (tCurrentButtonStateIsActive && LastChangeWasBouncingToInactive) { - // Very short press detected, which was handled as bounce above -> adjust last button state + /* + * No valid change detected - current is equals last state + */ + if (tCurrentButtonStateIsActive && LastBounceWasChangeToInactive) { + // We assume we had a very short press before (or a strange spike), which was handled as a bounce. -> must adjust last button state ButtonStateIsActive = false; -#ifdef ANALYZE_MAX_BOUNCING_PERIOD - MaxBouncingPeriodMillis = 0; -#endif #ifdef TRACE - Serial.print(F("Preceding short press detected")); -#ifdef ANALYZE_MAX_BOUNCING_PERIOD - Serial.print(F(" reset MBP")); - MaxBouncingPeriodMillis = 0; -#endif - Serial.println(); + Serial.println(F("Preceding short press detected, which was handled as bounce")); #endif } else { @@ -478,7 +513,6 @@ void EasyButton::handleINT01Interrupts() { */ #ifdef TRACE Serial.println(F("Spike")); - #endif } } @@ -489,16 +523,15 @@ void EasyButton::handleINT01Interrupts() { * Valid change detected */ ButtonLastChangeMillis = tMillis; - LastChangeWasBouncingToInactive = false; + LastBounceWasChangeToInactive = false; #ifdef TRACE Serial.println(F("Change")); #endif ButtonStateIsActive = tCurrentButtonStateIsActive; ButtonStateHasJustChanged = true; if (tCurrentButtonStateIsActive) { - ButtonLongPressJustDetected = false; // reset lock flag for long button press detection /* - * Action on button press, no action on release + * Button pressed */ #ifdef LED_FEEDBACK_TEST digitalWriteFast(BUTTON_TEST_FEEDBACK_LED_PIN, HIGH); @@ -521,10 +554,10 @@ void EasyButton::handleINT01Interrupts() { Serial.println(F("Button release during callback processing detected.")); #endif ButtonStateIsActive = false; - ButtonStateHasJustChanged = true; tMillis = millis(); ButtonPressDurationMillis = tMillis - ButtonLastChangeMillis; ButtonLastChangeMillis = tMillis; + ButtonStateHasJustChanged = true; ButtonReleaseMillis = tMillis; } } @@ -534,6 +567,29 @@ void EasyButton::handleINT01Interrupts() { */ ButtonPressDurationMillis = tDeltaMillis; ButtonReleaseMillis = tMillis; +#if ! defined(NO_BUTTON_RELEASE_CALLBACK) + if (ButtonReleaseCallback != NULL) { + /* + * Call callback function. + * interrupts() is required if callback function needs more time to allow millis() to proceed. + */ + interrupts(); + ButtonReleaseCallback(ButtonToggleState, ButtonPressDurationMillis); + /* + * Check button again since it may ba activated while processing callback function + */ + if (readButtonState()) { + // button activated now, so maintain status +# ifdef TRACE + Serial.println(F("Button active after callback processing detected.")); +# endif + ButtonStateIsActive = true; + ButtonLastChangeMillis = millis(); + ButtonStateHasJustChanged = true; + LastBounceWasChangeToInactive = false; + } + } +#endif #ifdef LED_FEEDBACK_TEST digitalWriteFast(BUTTON_TEST_FEEDBACK_LED_PIN, LOW); #endif @@ -565,12 +621,12 @@ void __attribute__ ((weak)) handleINT1Interrupt() { //ISR(INT0_vect, __attribute__ ((weak))) { # if defined(USE_BUTTON_0) ISR(INT0_vect) { -# ifdef MEASURE_INTERRUPT_TIMING - digitalWriteFast(BUTTON_TEST_TIMING_PIN, HIGH); +# ifdef MEASURE_EASY_BUTTON_INTERRUPT_TIMING + digitalWriteFast(INTERRUPT_TIMING_OUTPUT_PIN, HIGH); # endif handleINT0Interrupt(); -# ifdef MEASURE_INTERRUPT_TIMING - digitalWriteFast(BUTTON_TEST_TIMING_PIN, LOW); +# ifdef MEASURE_EASY_BUTTON_INTERRUPT_TIMING + digitalWriteFast(INTERRUPT_TIMING_OUTPUT_PIN, LOW); # endif } # endif @@ -586,12 +642,12 @@ ISR(PCINT2_vect) ISR(INT1_vect) # endif { -# ifdef MEASURE_INTERRUPT_TIMING - digitalWriteFast(BUTTON_TEST_TIMING_PIN, HIGH); +# ifdef MEASURE_EASY_BUTTON_INTERRUPT_TIMING + digitalWriteFast(INTERRUPT_TIMING_OUTPUT_PIN, HIGH); # endif handleINT1Interrupt(); -# ifdef MEASURE_INTERRUPT_TIMING - digitalWriteFast(BUTTON_TEST_TIMING_PIN, LOW); +# ifdef MEASURE_EASY_BUTTON_INTERRUPT_TIMING + digitalWriteFast(INTERRUPT_TIMING_OUTPUT_PIN, LOW); # endif } # endif diff --git a/src/EasyButtonAtInt01.h b/src/EasyButtonAtInt01.h index 26a000f..7db8cf3 100644 --- a/src/EasyButtonAtInt01.h +++ b/src/EasyButtonAtInt01.h @@ -31,7 +31,20 @@ * along with this program. If not, see . */ +#ifndef EASY_BUTTON_AT_INT01_H_ +#define EASY_BUTTON_AT_INT01_H_ + +#define VERSION_EASY_BUTTON "3.0.0" +#define VERSION_EASY_BUTTON_MAJOR 3 +#define VERSION_EASY_BUTTON_MINOR 0 + /* + * Version 3.0.0 - 5/2020 + * - Added button release handler and adapted examples. + * - Revoke change for "only one true result per press for checkForLongPressBlocking()". It is superseded by button release handler. + * - Support buttons which are active high by defining BUTTON_IS_ACTIVE_HIGH. + * - Improved detection of maximum bouncing period used in DebounceTest. + * * Version 2.1.0 - 5/2020 * - Avoid 1 ms delay for checkForLongPressBlocking() if button is not pressed. * - Only one true result per press for checkForLongPressBlocking(). @@ -44,9 +57,6 @@ * - Renamed to EasyButtonAtInt01.cpp.h */ -#ifndef EASY_BUTTON_AT_INT01_H_ -#define EASY_BUTTON_AT_INT01_H_ - #include /* @@ -61,12 +71,18 @@ * ... * */ + +/* + * Define BUTTON_IS_ACTIVE_HIGH if you buttons are active high. + */ +//#define BUTTON_IS_ACTIVE_HIGH /* * Define USE_ATTACH_INTERRUPT to force use of the arduino function attachInterrupt(). * Needed if you get the error " multiple definition of `__vector_1'" (or `__vector_2'), because another library uses the attachInterrupt() function. * For one button it needs additional 160 bytes FLASH, for 2 buttons it needs additional 88 bytes. */ //#define USE_ATTACH_INTERRUPT +// /* * You can define your own value if you have buttons which are worse or better than the one I have. * Since debouncing is not done with blocking wait, reducing this value makes not much sense, except you expect regular short button presses, @@ -75,7 +91,7 @@ * * Test your own new value with the DebounceTest example * - * Analyze the current button debounce value with defining ANALYZE_MAX_BOUNCING_PERIOD and looking at MaxBouncingPeriodMillis. + * Analyze the button actual debounce value with defining ANALYZE_MAX_BOUNCING_PERIOD and looking at MaxBouncingPeriodMillis. * Defining ANALYZE_MAX_BOUNCING_PERIOD computes the maximum bouncing period. * this is the time between first level change and last bouncing level change during BUTTON_DEBOUNCING_MILLIS */ @@ -84,6 +100,11 @@ #define BUTTON_DEBOUNCING_MILLIS 50 // 35 millis measured for my button :-). #endif +/* + * Comment this out to save 2 bytes RAM and ? bytes FLASH + */ +//#define NO_BUTTON_RELEASE_CALLBACK +// /* * Return values for checkForLongPress() */ @@ -109,12 +130,12 @@ #endif // For external measurement of code timing -//#define MEASURE_INTERRUPT_TIMING +//#define MEASURE_EASY_BUTTON_INTERRUPT_TIMING -#if defined(MEASURE_INTERRUPT_TIMING) -# ifndef BUTTON_TEST_TIMING_PIN -#define BUTTON_TEST_TIMING_PIN 6 // use pin 6 -//#define BUTTON_TEST_TIMING_PIN 12 // use pin 12 +#if defined(MEASURE_EASY_BUTTON_INTERRUPT_TIMING) +# ifndef INTERRUPT_TIMING_OUTPUT_PIN +#define INTERRUPT_TIMING_OUTPUT_PIN 6 // use pin 6 +//#define INTERRUPT_TIMING_OUTPUT_PIN 12 // use pin 12 # endif #endif @@ -219,25 +240,32 @@ class EasyButton { EasyButton(void (*aButtonPressCallback)(bool aButtonToggleState)); EasyButton(bool aIsButtonAtINT0); EasyButton(bool aIsButtonAtINT0, void (*aButtonPressCallback)(bool aButtonToggleState)); +#if ! defined(NO_BUTTON_RELEASE_CALLBACK) + EasyButton(bool aIsButtonAtINT0, void (*aButtonPressCallback)(bool aButtonToggleState), + void (*aButtonReleaseCallback)(bool aButtonToggleState, uint16_t aButtonPressDurationMillis)); +#endif void init(bool aIsButtonAtINT0); + /* + * !!! checkForDoublePress() works only reliable if called in button press callback function !!! + */ + bool checkForDoublePress(uint16_t aDoublePressDelayMillis = EASY_BUTTON_DOUBLE_PRESS_DEFAULT_MILLIS); + bool readButtonState(); bool readDebouncedButtonState(); bool updateButtonState(); + uint16_t updateButtonPressDuration(); // Updates the ButtonPressDurationMillis by polling, since this cannot be done by interrupt. - /* - * Updates the ButtonPressDurationMillis by polling, since this cannot be done by interrupt. - */ - uint16_t updateButtonPressDuration(); + bool checkForForButtonNotPressedTime(uint16_t aTimeoutMillis); + + //!!! Consider to use button release callback handler and check the ButtonPressDurationMillis uint8_t checkForLongPress(uint16_t aLongPressThresholdMillis = EASY_BUTTON_LONG_PRESS_DEFAULT_MILLIS); bool checkForLongPressBlocking(uint16_t aLongPressThresholdMillis = EASY_BUTTON_LONG_PRESS_DEFAULT_MILLIS); - bool checkForDoublePress(uint16_t aDoublePressDelayMillis = EASY_BUTTON_DOUBLE_PRESS_DEFAULT_MILLIS); // !!!Works only reliable if called in callback function!!! - bool checkForForButtonNotPressedTime(uint16_t aTimeoutMillis); - void handleINT01Interrupts(); + void handleINT01Interrupts(); // internal use only - bool LastChangeWasBouncingToInactive; // Internal state, reflects actual reading with spikes and bouncing. Negative logic: true / active means button pin is LOW + bool LastBounceWasChangeToInactive; // Internal state, reflects actual reading with spikes and bouncing. Negative logic: true / active means button pin is LOW volatile bool ButtonStateIsActive; // Negative logic: true / active means button pin is LOW. If last press duration < BUTTON_DEBOUNCING_MILLIS it holds wrong value (true instead of false) :-( volatile bool ButtonToggleState; // Toggle is on press, not on release - initial value is false @@ -255,15 +283,16 @@ class EasyButton { volatile unsigned long ButtonLastChangeMillis; // For debouncing volatile unsigned long ButtonReleaseMillis; // for double press recognition - volatile bool ButtonLongPressJustDetected; // Lock flag for long button press detection #if defined(ANALYZE_MAX_BOUNCING_PERIOD) volatile unsigned int MaxBouncingPeriodMillis = 0; // Maximum bouncing period. Time between first level change and last bouncing level change during BUTTON_DEBOUNCING_MILLIS - volatile bool MaxBouncingPeriodMillisHasJustChanged; #endif volatile bool isButtonAtINT0; void (*ButtonPressCallback)(bool aButtonToggleState) = NULL; // If not null, is called on every button press with ButtonToggleState as parameter +#if ! defined(NO_BUTTON_RELEASE_CALLBACK) + void (*ButtonReleaseCallback)(bool aButtonToggleState, uint16_t aButtonPressDurationMillis) = NULL; // If not null, is called on every button release with ButtonPressDurationMillis as parameter +#endif #if defined(USE_BUTTON_0) static EasyButton * sPointerToButton0ForISR;