1

I'm working on an Arduino project in pure C with multiple buttons. Each button has a different action associated with it, and I want to handle button presses using interrupts.

Currently, the typical approach is to create one ISR per button, like this:


#define NUM_BUTTONS 4

int currentAction[NUM_BUTTONS];
int buttonPins[NUM_BUTTONS] = {2, 3, 4, 5};

void handleButton(int index, int action) {
    // Do something with the button
}

static void buttonHandler0() { handleButton(0, currentAction[0]); }
static void buttonHandler1() { handleButton(1, currentAction[1]); }
static void buttonHandler2() { handleButton(2, currentAction[2]); }
static void buttonHandler3() { handleButton(3, currentAction[3]); }

static void (*buttonISRs[NUM_BUTTONS])() = {
    buttonHandler0, buttonHandler1, buttonHandler2, buttonHandler3
};

void setup() {
    for (int i = 0; i < NUM_BUTTONS; i++) {
        pinMode(buttonPins[i], INPUT);
        enableInterrupt(buttonPins[i], buttonISRs[i], RISING);
    }
}

Problem:

  • This requires manually writing N wrapper functions, which is cumbersome if NUM_BUTTONS grows.

  • `enableInterrupt` only allows a pointer to a function with no parameters, so I cannot directly pass the button index or action.

What I want:

  • A scalable solution in C/Arduino that allows N buttons, each with a separate action.

  • Avoid writing one hardcoded ISR per button.

  • Keep it compatible with `enableInterrupt` or Arduino-style interrupts.

EDIT

Here is a brief explanation of the project and the relevant part of my I/O manager. The game shows a random sequence of 4 digits (1–4) on the LCD, and the player must press the corresponding buttons in the correct order and within a time limit. Each button B[i] turns on LED L[i]. If the sequence is completed on time, the score increases and the next round becomes faster; if the player makes a mistake or runs out of time, the game ends and the red LED lights up. The system uses interrupts for handling button presses, debouncing logic, and a queue of inputs collected during each round.

Below is an extract of my I/O manager, specifically the part that handles debouncing, interrupt handlers, and the array of per-button ISRs:

#define BOUNCING_TIME 100
#define MAX_QUEUE (NUM_BUTTONS + 50)

// Queue
static volatile int inputQueue[MAX_QUEUE];
static volatile int queueLast = 0;

// Debounce
static unsigned long lastButtonPressedTimestamps[NUM_BUTTONS];
static ButtonAction currentAction[NUM_BUTTONS];

// Generated handlers
static void genericButtonHandler0() { buttonHandler(0, currentAction[0]); }
static void genericButtonHandler1() { buttonHandler(1, currentAction[1]); }
static void genericButtonHandler2() { buttonHandler(2, currentAction[2]); }
static void genericButtonHandler3() { buttonHandler(3, currentAction[3]); }

static void (*buttonISRs[NUM_BUTTONS])() = {
    genericButtonHandler0,
    genericButtonHandler1,
    genericButtonHandler2,
    genericButtonHandler3
};

// Queue management
void clearQueue() { queueLast = 0; }
int inputQueueLen() { return queueLast; }

int inputQueueGet(int index) {
    return (index >= 0 && index < queueLast) ? inputQueue[index] : -1;
}

// Button handling
void addActionToButton(int buttonIndex, ButtonAction action) {
    if (buttonIndex < 0 || buttonIndex >= NUM_BUTTONS) return;

    disableInterrupt(pair[buttonIndex].buttonPin);
    enableInterrupt(
        pair[buttonIndex].buttonPin,
        buttonISRs[buttonIndex],
        RISING
    );
}

void disableAllButtons() {
    for (int i = 0; i < NUM_BUTTONS; i++) {
        disableInterrupt(pair[i].buttonPin);
    }
}

static void buttonHandler(int i, ButtonAction action) {
    unsigned long ts = millis();
    if (ts - lastButtonPressedTimestamps[i] > BOUNCING_TIME) {
        lastButtonPressedTimestamps[i] = ts;
        int status = digitalRead(pair[i].buttonPin);

#ifdef __DEBUG__
        Serial.println("Pressed button " + String(i + 1));
#endif

        if (status == HIGH && queueLast < MAX_QUEUE) {
            inputQueue[queueLast] = i + 1;
            queueLast++;
        }
    }
}
New contributor
Cufa Niello is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
2
  • You didn't even mention for what device this is. Commented Nov 14 at 10:04
  • If you need hard-real time response from your switches, such as emergency-stop, limit switches or collision detection, then tying the switches to interrupts may make sense, but for general user input buttons, using an interrupt is over-kill and complicates debouncing, use a timer to poll all the buttons and apply debouncing as suggested by @Lundin Commented 20 hours ago

2 Answers 2

3

I dont know avr specifics much, so hope someone will provide better answer.

Interrupts internally are just table (think array of pointers to functions) that cpu uses to jump to on interrupt, like gpio 1 is index interrupts[GPIOISR+1].

You can register same function for all of them, but then you need to know which one triggered, which would be reading some interrupt mask register, avr specific. Thats more complex and less portable code than declaring functions, and you still need map of registry bit = interrupt number, this doesnt shorten code much, same amount of lines

Its possible to also dynamically generate functions in runtime, but not on avr ( non executable ram), and thats overengineering

You could make macro to define function, to make handler code somewhat easier to modify

DEF_BTN_HDLR(1);  
DEF_BTN_HDLR(2);  

I personally prefer polling for buttons, as polling with some interval around ~50ms automatically does debouncing of switches, there isnt much point for interrupts if you dont have latency requirements. Also, dont arduino has very limited gpio interrupt count? Would you realistically have more than 10 buttons this way?

New contributor
None is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
Sign up to request clarification or add additional context in comments.

Comments

3

Ignoring Arduino, here are some prerequisites for working with GPIO buttons in any generic microcontroller system:

  • During the first few beginner-level classes of embedded systems, the concept of "de-bouncing" buttons is typically addressed.

    Placing interrupts directly on GPIO pins to read buttons is deeply problematic due to the electromechanical bouncing of the switch, but also because of EMI and ESD. During bouncing or EMI, you would get many spurious interrupts which on a tiny MCU like AVR would cause dangerous stack peak usage.

    Therefore it is pretty senseless to discuss such interrupts without a schematic of the hardware in mind - are there hardware low-pass filters or not etc. There are ways to write such interrupts but is more complex. If this answer was the first time ever you heard about de-bouncing, then you should just forget all about GPIO interrupts until you know more about embedded systems and electronics.

  • From the microcontroller point of view, speaking of individual buttons in a higher layer concept. The MCU only cares about port registers and pins. And so the only thing the button driver needs to concern itself with is which ports and pins to read.

  • For the above reasons, the best way to implement button reads is therefore do only read buttons from a cyclic timer interrupt, not from a GPIO interrupt. Then you can implement the simplest form of de-bouncing by comparing the current read with the previous one and - assuming active high polarity - set everything to zero unless you identify two active reads in a row.

An example from an entirely different MCU project I'm working with right now follows. This MCU only got 2 ports of note, port A and B:

static uint32_t pta_prev=0;
static uint32_t ptb_prev=0;

/* These must be volatile since these 2 variables are the results 
   of the interrupt to be shared with the rest of the driver code: */
static volatile uint32_t pta_debounced; 
static volatile uint32_t ptb_debounced; 

// timer interrupt callback function:
static void tim_switches_read (void)
{
  uint32_t pta = PORT->Group[0].IN.reg; // reading hardware register port A
  uint32_t ptb = PORT->Group[1].IN.reg; // reading hardware register port B

  // discard everything until you have active high reads in a row:
  pta_debounced = pta & pta_prev; 
  ptb_debounced = ptb & ptb_prev;

  // remember results until next cyclic interrupt
  pta_prev = pta;
  ptb_prev = ptb;
}

This driver actually doesn't care the slightest on which ports I have actual switches connected - it only reads the port and de-bounces it, then leaves the logic of parsing out switches to higher layer logic, outside any interrupt.

This is the most basic version: more intricate de-bouncing using median filters etc is another possibility, but generally not required unless dealing with high integrity systems or safety functions.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.