/*
Button-bounce timing + bounce counting (UNO)
------------------------------------------------------------
Goal:
- Time the *bounce span* (first edge -> last edge) using micros()
- Count bounces (edges after the first) on each press and release
- Interrupt on D2 (CHANGE) feeds an FSM in loop()
- Avoid "false open/close" paths: all chatter is bounce
- Use settle windows (15 ms each) to decide when bouncing has ended
- Build report text with snprintf into buffers (no inline Serial prints)
- When the FSM transitions back to OPEN, print the entire report for
that full sequence (OPEN -> OTWUP -> CLOSED -> OTWDOWN -> OPEN)
Wiring:
- Pushbutton between D2 and GND (INPUT_PULLUP)
- LEDs:
D5 = RED (OPEN)
D6 = YELLOW (OTWUP / OTWDOWN)
D7 = GREEN (CLOSED)
Timing:
- UP_SETTLE_US, DOWN_SETTLE_US: quiet time after the LAST edge
- Reported bounce_us does NOT include the settle tail:
bounce_us = lastEdgeUs - firstEdgeUs
(Your ~2 ms switch should show ~2000 us here.)
*/
# include <Arduino.h>
# include <stdarg.h>
// ---------------- pins ----------------
const byte BTN = 2; // INT0 on UNO (PD2)
const byte LED_R = 5;
const byte LED_Y = 6;
const byte LED_G = 7;
// -------------- timing ----------------
const unsigned long UP_SETTLE_US = 15000UL; // 15 ms after last edge
const unsigned long DOWN_SETTLE_US = 15000UL; // 15 ms after last edge
// -------------- states ----------------
enum { OPEN, OTWUP, CLOSED, OTWDOWN };
// -------- ISR -> loop ring buffer -----
// 128 events costs ~640 bytes RAM (timestamps + levels), reasonable on UNO.
constexpr int EVT_BUF_SZ = 128;
volatile unsigned long evt_t[EVT_BUF_SZ];
volatile byte evt_level[EVT_BUF_SZ];
volatile byte evt_head = 0; // next write index
volatile byte evt_tail = 0; // next read index
volatile unsigned int evt_overflow = 0;
// ---------- reporting buffers ----------
constexpr byte REP_MAX_LINES = 5;
constexpr unsigned int REP_LINE_LEN = 96;
char rep[REP_MAX_LINES][REP_LINE_LEN];
byte repN = 0;
static void repClear() {
repN = 0;
for (byte i = 0; i < REP_MAX_LINES; i++) rep[i][0] = '\0';
}
static void repAdd(const char *fmt, ...) {
if (repN >= REP_MAX_LINES) return;
va_list ap;
va_start(ap, fmt);
vsnprintf(rep[repN], REP_LINE_LEN, fmt, ap);
va_end(ap);
repN++;
}
static void repPrintAll() {
for (byte i = 0; i < repN; i++) {
Serial.println(rep[i]);
}
Serial.println("");
}
// ------------- small helpers ----------
static void setLEDs(bool r, bool y, bool g) {
digitalWrite(LED_R, r ? HIGH : LOW);
digitalWrite(LED_Y, y ? HIGH : LOW);
digitalWrite(LED_G, g ? HIGH : LOW);
}
static void setLEDsForState(byte s) {
switch (s) {
case OPEN: setLEDs(true, false, false); break; // red
case OTWUP: setLEDs(false, true, false); break; // yellow
case CLOSED: setLEDs(false, false, true ); break; // green
case OTWDOWN: setLEDs(false, true, false); break; // yellow
}
}
static inline byte next_idx(byte i) { return (byte)((i + 1) & (EVT_BUF_SZ - 1)); }
static_assert((EVT_BUF_SZ & (EVT_BUF_SZ - 1)) == 0, "EVT_BUF_SZ must be a power of two");
// Fast read for D2 on UNO (PD2). INPUT_PULLUP => HIGH=open, LOW=pressed.
static inline byte readBtnFast() {
return (PIND & _BV(PD2)) ? HIGH : LOW;
}
// -------------- ISR -------------------
void isrButtonChange() {
unsigned long t = micros();
byte level = readBtnFast();
byte h = evt_head;
byte nh = next_idx(h);
if (nh == evt_tail) {
evt_overflow++;
} else {
evt_t[h] = t;
evt_level[h] = level;
evt_head = nh;
}
}
static bool popEvent(unsigned long &t, byte &level) {
noInterrupts();
if (evt_head == evt_tail) {
interrupts();
return false;
}
byte idx = evt_tail;
t = evt_t[idx];
level = evt_level[idx];
evt_tail = next_idx(idx);
interrupts();
return true;
}
static unsigned int takeOverflowCount() {
noInterrupts();
unsigned int ovf = evt_overflow;
evt_overflow = 0;
interrupts();
return ovf;
}
// -------- FSM bookkeeping -------------
byte state = OPEN;
// Current episode info (valid in OTWUP/OTWDOWN)
unsigned long episodeStartUs = 0; // first edge timestamp
unsigned long lastEdgeUs = 0; // most recent edge timestamp
unsigned long settleUs = 0; // settle window for this episode
unsigned int bounceCount = 0; // edges after the first edge
// Optional: for context in the report
unsigned long seqStartUs = 0; // when the sequence began (first press edge)
unsigned int seqPressNum = 0; // sequence counter
static void startEpisode(unsigned long tUs) {
episodeStartUs = tUs;
lastEdgeUs = tUs;
bounceCount = 0;
}
static bool episodeSettled(unsigned long nowUs) {
return nowUs - lastEdgeUs >= settleUs;
}
void setup() {
pinMode(BTN, INPUT_PULLUP);
pinMode(LED_R, OUTPUT);
pinMode(LED_Y, OUTPUT);
pinMode(LED_G, OUTPUT);
Serial.begin(115200);
delay(20);
byte pinNow = (byte)digitalRead(BTN);
state = (pinNow == HIGH) ? OPEN : CLOSED;
setLEDsForState(state);
repClear();
repAdd("event\tbounce_us\tquiet_us\tbounces\toverflow");
attachInterrupt(digitalPinToInterrupt(BTN), isrButtonChange, CHANGE);
}
void loop() {
unsigned long t;
byte level;
// Drain all pending events quickly (no Serial printing here).
while (popEvent(t, level)) {
switch (state) {
case OPEN:
// Start of a press episode: first contact conducts => level LOW.
if (level == LOW) {
seqPressNum++;
seqStartUs = t;
repClear();
repAdd("seq\t%u\tstart_us\t%lu", seqPressNum, seqStartUs);
repAdd("event\tbounce_us\tbounces\toverflow");
state = OTWUP;
setLEDsForState(state);
startEpisode(t);
}
break;
case OTWUP:
// Any change is bounce. Count edges after the first.
if (t != episodeStartUs) bounceCount++;
lastEdgeUs = t;
break;
case CLOSED:
// Start of a release episode: first release opens => level HIGH.
if (level == HIGH) {
state = OTWDOWN;
setLEDsForState(state);
startEpisode(t);
}
break;
case OTWDOWN:
if (t != episodeStartUs) bounceCount++;
lastEdgeUs = t;
break;
}
}
// Check settle (quiet tail reached) and finalize episode(s).
unsigned long nowUs = micros();
if (state == OTWUP && nowUs - lastEdgeUs >= UP_SETTLE_US) {
// Press episode done -> declare CLOSED
unsigned long bounceUs = lastEdgeUs - episodeStartUs;
unsigned long quietUs = nowUs - lastEdgeUs;
unsigned int ovf = takeOverflowCount();
repAdd("PRESS\t%lu\t%u\t%u", bounceUs, bounceCount, ovf);
state = CLOSED;
setLEDsForState(state);
// Note: keep report buffers; we'll print when we return to OPEN.
}
else if (state == OTWDOWN && nowUs - lastEdgeUs >= DOWN_SETTLE_US) {
// Release episode done -> declare OPEN
unsigned long bounceUs = lastEdgeUs - episodeStartUs;
unsigned long quietUs = nowUs - lastEdgeUs;
unsigned int ovf = takeOverflowCount();
repAdd("RELEASE\t%lu\t%u\t%u", bounceUs, bounceCount, ovf);
state = OPEN;
setLEDsForState(state);
// Sequence complete: print the whole report now.
repAdd("seq_end_us\t%lu\telapsed_us\t%lu", nowUs, nowUs - seqStartUs);
repPrintAll();
}
// No delay(): we want to drain the ring buffer ASAP to avoid overflow.
}CLOSED
<->
OPEN