From cd01ce9f2b2a8b0c88a81123bb911e8d313ef323 Mon Sep 17 00:00:00 2001 From: Michael Adams Date: Sun, 21 Jun 2026 14:22:13 +1200 Subject: [PATCH 1/2] Fix millis rollover --- src/Button.cpp | 8 ++++++-- test/test_button/test_main.cpp | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Button.cpp b/src/Button.cpp index 2722edf..7f79e3f 100644 --- a/src/Button.cpp +++ b/src/Button.cpp @@ -6,6 +6,7 @@ #include "Button.h" #include +#include Button::Button(uint8_t pin, uint16_t debounce_ms) : _pin(pin), _delay(debounce_ms), _state(HIGH), _ignore_until(0), _has_changed(false) @@ -23,8 +24,11 @@ void Button::begin() bool Button::read() { - // ignore pin changes until after this delay time - if (_ignore_until > millis()) + // ignore pin changes until after this delay time. The subtraction is done + // in modular (wraparound-safe) arithmetic so debouncing keeps working across + // the ~49-day millis() rollover: a negative signed difference means we have + // not yet reached _ignore_until. + if ((int32_t)((uint32_t)millis() - _ignore_until) < 0) { // ignore any changes during this period } diff --git a/test/test_button/test_main.cpp b/test/test_button/test_main.cpp index eaa5258..72ebb3e 100644 --- a/test/test_button/test_main.cpp +++ b/test/test_button/test_main.cpp @@ -117,6 +117,35 @@ void test_custom_debounce_time(void) TEST_ASSERT_TRUE(button.released()); } +// Debouncing stays correct across the 32-bit millis() rollover (~49 days). +void test_debounce_survives_millis_wraparound(void) +{ + Button button(2, 100); + + // Prime with a normal press so _ignore_until tracks recent activity, the way + // it always does in real operation (never far behind millis()). + _mock_millis = 0x7FFFFFFF; + _mock_pin_state = LOW; + TEST_ASSERT_TRUE(button.pressed()); + + // Release at the very top of the range: the debounce deadline (millis + 100) + // overflows and wraps to a tiny value (~99). + _mock_millis = 0xFFFFFFFF; + _mock_pin_state = HIGH; + TEST_ASSERT_TRUE(button.released()); + + // A bounce at the same (large) millis is still inside the debounce window, + // even though the deadline has wrapped to a small number. The old absolute + // comparison (deadline > millis) would wrongly treat the window as expired + // and let this bounce through. + _mock_pin_state = LOW; + TEST_ASSERT_FALSE(button.pressed()); + + // Once millis() wraps past the (wrapped) deadline, a genuine press registers. + _mock_millis = 0x70; + TEST_ASSERT_TRUE(button.pressed()); +} + int main(int, char **) { UNITY_BEGIN(); @@ -126,5 +155,6 @@ int main(int, char **) RUN_TEST(test_debounce_ignores_bounce); RUN_TEST(test_toggled_on_each_change); RUN_TEST(test_custom_debounce_time); + RUN_TEST(test_debounce_survives_millis_wraparound); return UNITY_END(); } From e8660c38117ff2eb244babab2c254f9a9df3888e Mon Sep 17 00:00:00 2001 From: Michael Adams Date: Sun, 21 Jun 2026 14:36:51 +1200 Subject: [PATCH 2/2] Add hardware test --- extras/hardware_test/README.md | 13 ++++++++++ extras/hardware_test/platformio.ini | 23 +++++++++++++++++ extras/hardware_test/src/main.cpp | 40 +++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 extras/hardware_test/README.md create mode 100644 extras/hardware_test/platformio.ini create mode 100644 extras/hardware_test/src/main.cpp diff --git a/extras/hardware_test/README.md b/extras/hardware_test/README.md new file mode 100644 index 0000000..aa12e03 --- /dev/null +++ b/extras/hardware_test/README.md @@ -0,0 +1,13 @@ +# Test on Real Hardware + +This is a tiny example to test the current library code on a real device. + +Test with: +``` +$ pio run --target upload && pio device monitor +... +Button hardware test - press the button on pin 2 +pressed +released +pressed +``` diff --git a/extras/hardware_test/platformio.ini b/extras/hardware_test/platformio.ini new file mode 100644 index 0000000..9c77384 --- /dev/null +++ b/extras/hardware_test/platformio.ini @@ -0,0 +1,23 @@ +; Hardware smoke-test for the Button library. +; +; Builds the demo sketch in src/main.cpp against the LOCAL library source (this +; working tree - i.e. your unreleased changes), not a published release, and +; uploads it to a real board. +; +; lib_extra_dirs points the Library Dependency Finder at the directory that +; CONTAINS this library (the parent folder), so PlatformIO reads the source in +; place - no copying and no symlinks. Editing ../../src/Button.* and re-running +; upload immediately tests the new code. +; +; pio run -d extras/hardware_test -e uno -t upload +; pio device monitor -b 9600 +; +; (Run from the repo root. Or `cd extras/hardware_test` and drop the `-d`.) + +[env:uno] +platform = atmelavr +board = uno +framework = arduino +lib_extra_dirs = ${PROJECT_DIR}/../../.. +lib_deps = Button +monitor_speed = 9600 diff --git a/extras/hardware_test/src/main.cpp b/extras/hardware_test/src/main.cpp new file mode 100644 index 0000000..acb6e77 --- /dev/null +++ b/extras/hardware_test/src/main.cpp @@ -0,0 +1,40 @@ +/* + Hardware smoke-test for Button. + + Wire a button between pin 2 and GND (the internal pull-up is enabled, so no + external resistor is needed). The onboard LED mirrors the debounced state - + lit while the button is held - and each edge is reported over Serial: + + "pressed" on a debounced press + "released" on a debounced release + + Upload + monitor: + pio run -d extras/hardware_test -e uno -t upload + pio device monitor -b 9600 +*/ + +#include +#include + +const uint8_t BUTTON_PIN = 2; +Button button(BUTTON_PIN); + +void setup() +{ + button.begin(); + pinMode(LED_BUILTIN, OUTPUT); + Serial.begin(9600); + Serial.println(F("Button hardware test - press the button on pin 2")); +} + +void loop() +{ + if (button.pressed()) + Serial.println(F("pressed")); + + if (button.released()) + Serial.println(F("released")); + + // Mirror the debounced state on the LED (PRESSED == LOW). + digitalWrite(LED_BUILTIN, button.read() == Button::PRESSED ? HIGH : LOW); +}