uint8_t buttonsPressed;  // Updated by the ISRs to indicate which buttons are pressed. (Not debounced.)
uint64_t pTime;

// Set the pin that rose.
void ProcessPinRise(uint8_t pin)
{
  buttonsPressed |= 1 << pin;
}

void IRAM_ATTR IsrPin2Rise()
{
  ProcessPinRise(2);
}

void IRAM_ATTR IsrPin4Rise()
{
  ProcessPinRise(4);
}

void IRAM_ATTR IsrPin5Rise()
{
  ProcessPinRise(5);
}

struct Button
{
  int pin;
  void (*isr)(void);
};

const Button Buttons[] =
{
  { 2, IsrPin2Rise },
  { 4, IsrPin4Rise },
  { 5, IsrPin5Rise }
};

void setup()
{
  Serial.begin(115200);
  for (const auto& Button : Buttons)
  {
    pinMode(Button.pin, INPUT);
    attachInterrupt(digitalPinToInterrupt(Button.pin), Button.isr, RISING);
  }
}

void loop()
{
  uint64_t tick = millis();

  // Make a local copy because the ISRs will update the global variable asynchronously on each press or bounce.
  uint8_t localButtonsPressed = buttonsPressed;

  if (localButtonsPressed)
  {
    DiscernButtons(localButtonsPressed);
    buttonsPressed &= ~localButtonsPressed;
  }
  if (tick - pTime >= 1000)
  {
    pTime = millis();
    Serial.println("Tick.");
  }
}

void DiscernButtons(uint8_t localButtonsPressed)
{
  Serial.print("Pressed: ");
  uint8_t button = 0;
  while (localButtonsPressed)
  {
    int pressed = localButtonsPressed & 1;
    if (pressed)
    {
      Serial.print(button);
      Serial.print(" ");
    }
    localButtonsPressed >>= 1;
    button++;
  }
  Serial.println();
}