Clever GPIO state change handler

I know, it is not modest to name your own solution “clever”, but I simply like it and I think that it can serve well in various cases. The reason for the solution provided below was the need to handle various information coming from digital pins of my ESP8266. This will work just fine on the other boards such as Arduino, Wemos, Raspberry Pi, and so on. This is simply an idea. The code below is prepared for ESP8266 because this is the board I’m using most frequently.

What for?

I’m using my modules to handle various events. Some of them are reported by the change of the state of the digital input. It can change from HIGH to LOW if something was turned off, from LOW to HIGH and back to LOW if the button was pressed and released – there are different cases. The solution provided below is the simplest possible, it will not care what change occurred.

The concept

The common problem I faced with the digital inputs was that they tend to “bounce” if the source comes (for instance) from the mechanical switch. What does it mean? The state doesn’t go from HIGH to LOW in one simple switch but switches a few times before the final state is reached. There are debouncing tutorials out there and my solution is also kind of such a solution.

The concept is to store the information about the state change and the time when this change occurred. The handler function will handle it “later”. And by later I mean that you can configure when it will be handled. Since typical switch bouncing takes tens of milliseconds, you can decide to handle the switch 100 milliseconds after the last change. Sometimes after a longer time – even minutes if you wish.

The “events” are collected at one point, and they are handled in a separate place. This way there is no mess in both of the places.

The code

Let’s take a look at the code one piece at a time. In the beginning, I’m defining what pins I will use. On the example below, these are the “safe” pins on the NodeMCU version of ESP8266 – the digital inputs that are almost always safe to use. I also define six arrays to store information I will use when collecting events and handling them. See the code first:

// Wiring of NodeMCU on ESP8266, inputs that are safe to use
#define D1 5    // GPIO5
#define D2 4    // GPIO4
#define D5 14   // GPIO14
#define D6 12   // GPIO12
#define D7 13   // GPIO13
#define PIN_COUNT 5 // how many pins in use

int pin_array[] = { D1, D2, D5, D6, D7 };               // pin reference
int pin_state[] = { 0, 0, 0, 0, 0 };                    // last state of the pin
int pin_state_change_unhandled[] = { 0, 0, 0, 0, 0 };   // state change is already handled or not?
unsigned long pin_last_change[] { 0, 0, 0, 0, 0 };      // time of last state change
unsigned long pin_handle_after[] { 500, 5000, 100, 1000, 1000 };     // time after which to handle the pin
String pin_name[] = { "D1", "D2", "D5", "D6", "D7" };   // pin names on the NodeMCU board

void setup() {
  Serial.begin(115200);
  initialize_pins();
}

The “pin_array” holds the reference to all pins I will use. The “pin_state” is the last state that was read from the pin. Sometimes it is useful not only to know that the state was changed but also what is the current state. The “pin_state_change_unhandled” stores the information that the last change is waiting to be handled. The “pin_last_change” stores the information about the time of the last state change (in milliseconds). The “pin_handle_after” is the configuration of the handler – it stores the information about the milliseconds that should pass before the event will be handled. Lastly, the “pin_name” stores the information about the names of the pins. You can change them to something more explanatory if you wish.

In the setup function, I simply call “initialize_pins” which looks as follows:

void initialize_pins() {
  // set all pins in use to input type and set initial state
  for (int i = 0; i < PIN_COUNT; i++) {
    pinMode(pin_array[i], INPUT);
    pin_state[i] = digitalRead(pin_array[i]);
    pin_last_change[i] = millis();
  }
}

The function loops through all pins and sets the pin mode to “INPUT”, reads the initial state of the pin, and sets the last state change to the current time in milliseconds. I’m using “millis” which is the time counted by the CPU of the module since the start, but you can use other time sources if you want.

That’s all for the setup, so we can now take a look at the loop. As I stated before, it contains two separate actions – checking of the pin state and handling events:

void loop() {
  read_pin_statuses();
  handle_pin_changes();
} 

The first one:

void read_pin_statuses() {
  byte this_pin_state;
  unsigned long current_millis = millis();
  unsigned long time_elapsed;
  
  // read all pins statuses
  for (int i = 0; i < PIN_COUNT; i++) {
    // read and display pin state
    this_pin_state = digitalRead(pin_array[i]);

    // check if state changed
    if ( this_pin_state != pin_state[i] ) {
      Serial.println("State of " + pin_name[i] + " changed to: " + this_pin_state);
      pin_state[i] = this_pin_state;        // save current state
      pin_last_change[i] = current_millis;  // save time of state change
      pin_state_change_unhandled[i] = 1;    // set change as unhandled
    } 
  }
}

The above function is looping through all configured pins and checks if the state has changed. On the change, it is storing the current state, the time of the change, and the information that the state change is waiting to be handled. No matter if the pin already waits to be handled – on each change, the time is set to the current time and the unhandled flag is set. So, now the last piece, the handler itself:

void handle_pin_changes() {
  unsigned long current_millis = millis();
  
  for (int i = 0; i < PIN_COUNT; i++) {
    if (pin_state_change_unhandled[i] == 1) {
      // check if millis counter reached limit and returned to zero (occurs about every 50 days)
      if (pin_last_change[i] > current_millis) {
        pin_last_change[i] = 0;
      }

      // check if pin should be handled
      if ( (current_millis - pin_last_change[i]) > pin_handle_after[i] ) {
        Serial.println("Time to handle for " + pin_name[i] + " reached, handling...");
        pin_state_change_unhandled[i] = 0;
        // your handle script here...
      }
      
    }
  }
}

Again, the function is looping through all pins and checking if there is an unhandled flag set to one of them. Once there is an unhandled event, it checks how much time passed and if the time is right to handle the event.

There is one mysterious thing though: the lines in which I check if the pin last change time is larger than the current time. Why do so? The current CPU time is stored as unsigned long, so the value gets reset in about 50 days. Once this occurs, the handler is not working properly for the “old” values. Thus the time reset I’m performing here. Some modules are rebooted every once in a while, so this never happens, but some of them are running for months without reboot and I have to take care of this.

If the time is right to handle the event, the unhandled flag is removed and the handling code is executed. In the example above, there is no code, but in most cases, I simply call the proper function to take care of the state change.

The above covers the basic digital pin state change event handling. The only thing that remains is to provide the whole code in a single piece if you want to test it on your own:

// Wiring of NodeMCU on ESP8266, inputs that are safe to use
#define D1 5    // GPIO5
#define D2 4    // GPIO4
#define D5 14   // GPIO14
#define D6 12   // GPIO12
#define D7 13   // GPIO13
#define PIN_COUNT 5 // how many pins in use

int pin_array[] = { D1, D2, D5, D6, D7 };               // pin reference
int pin_state[] = { 0, 0, 0, 0, 0 };                    // last state of the pin
int pin_state_change_unhandled[] = { 0, 0, 0, 0, 0 };   // state change is already handled or not?
unsigned long pin_last_change[] { 0, 0, 0, 0, 0 };      // time of last state change
unsigned long pin_handle_after[] { 500, 5000, 100, 1000, 1000 };     // time after which to handle the pin
String pin_name[] = { "D1", "D2", "D5", "D6", "D7" };   // pin names on the NodeMCU board

void setup() {
  Serial.begin(115200);
  initialize_pins();
}

void loop() {
  read_pin_statuses();
  handle_pin_changes();
}

void initialize_pins() {
  // set all pins in use to input type and set initial state
  for (int i = 0; i < PIN_COUNT; i++) {
    pinMode(pin_array[i], INPUT);
    pin_state[i] = digitalRead(pin_array[i]);
    pin_last_change[i] = millis();
  }
}

void read_pin_statuses() {
  byte this_pin_state;
  unsigned long current_millis = millis();
  unsigned long time_elapsed;
  
  // read all pins statuses
  for (int i = 0; i < PIN_COUNT; i++) {
    // read and display pin state
    this_pin_state = digitalRead(pin_array[i]);

    // check if state changed
    if ( this_pin_state != pin_state[i] ) {
      Serial.println("State of " + pin_name[i] + " changed to: " + this_pin_state);
      pin_state[i] = this_pin_state;        // save current state
      pin_last_change[i] = current_millis;  // save time of state change
      pin_state_change_unhandled[i] = 1;    // set change as unhandled
    } 
  }
}

void handle_pin_changes() {
  unsigned long current_millis = millis();
  
  for (int i = 0; i < PIN_COUNT; i++) {
    if (pin_state_change_unhandled[i] == 1) {
      // check if millis counter reached limit and returned to zero (occurs about every 50 days)
      if (pin_last_change[i] > current_millis) {
        pin_last_change[i] = 0;
      }

      // check if pin should be handled
      if ( (current_millis - pin_last_change[i]) > pin_handle_after[i] ) {
        Serial.println("Time to handle for " + pin_name[i] + " reached, handling...");
        pin_state_change_unhandled[i] = 0;
        // your handle script here...
      }
      
    }
  }
}