EXTERNAL INTERRUPTS

Man with flag

In a computing context, interrupts are events or conditions that cause the microprocessor to stop what it's doing, work on the event that caused the interrupt and then resume its original task. They are analogous to someone attracting your attention, asking you perform some trivial task then letting you go back to what you were doing. Interrupts are usually dealt with by a dedicated function called an Interrupt Service Routine (ISR).

Interrupts can be external or internal but only external ones are explained here. Further, there are two types of external interrupts; (1) "external" interrupts can be programmed to detect rising edge, falling edge, change and low and (2) "pin change" interrupts that can be set on any pin but can only detect a change. External interrupts are useful because they allow sensors to be dealt with as and when events occur instead of continually polling the sensor to see if it has any data. Polling is wasteful of resources and can be difficult to program without causing unwanted side-effects such as delays. Just because an interrupt is so called, it doesn't mean that it can't be called many times. For example, a program that measures the speed of a fan might be calling the ISR many 10s of times each second or a frequency counter many 1000s of times.

When an external interrupt occurs it is processed by the ISR and this needs to be done quickly, without imposing any delay on the main program. This means that ISRs must be simple and not contain code that itself causes any interrupts; for example doing any I/O. ISRs generally communicate with the main program by means of changing one or more variables that indicate to the main program that the ISR has been triggered; this could be toggling a value, incrementing a counter, etc. Variables that are used in both an ISR and the main program must be declared outside both. An ISR cannot be called with any arguments and cannot return any values; all communication must be done through the shared variables.

Modern compilers are good at optimising out redundant code but may not understand that a variable's value could change unexpectedly and without an explicit assignment statement. In reality, there are only three types of variables that can change: memory-mapped registers for peripheral devices, global variables that are changed by more than one process in a multi-threaded application and global variables that are changed inside an ISR. To avoid problems, all variables that will be modified in an ISR must be declared with the volatile keyword, which is conventionally placed first in the statement. For example;

volatile int Counter;

There's an interesting page on interrupts at StackExchange.

SPECIAL CONSIDERATIONS FOR ESP8266

Unlike the Arduino UNO and Micro, where only two pins can be configured as interrupt pins, any ESP8266 pin can be used with the exception of GPIO16.

Exclamation mark

You also need to add the linker attribute ICACHE_RAM_ATTR to your ISR as shown below:


void ICACHE_RAM_ATTR PulseCount()

{

    Count++;

}

Under normal circumstances you do not need to declare a function ahead of its point of use, though it's good practise. However, when using the ICACHE_RAM_ATTR attribute, you must declare the function or put its source code ahead of its use and include the attribute in both the declaration and definition.

void ICACHE_RAM_ATTR PulseCount();

int Count=0;

void setup()

{

    .

    attachInterrupt(digitalPinToInterrupt(D2),PulseCount,FALLING);

    .

}

void ICACHE_RAM_ATTR PulseCount()

{

    Count++;

}

The ICACHE_RAM_ATTR attribute specifies that the code for the ISR must be located in RAM. A problem arises with ESP8266 running multiple threads (and ESP32 which is dual-core) because reading and writing to flash can only be done using one thread - if you access flash using two threads your ESP may crash. Restricting the ISR to use only RAM avoids this problem. There's a good explanation on StackOverflow.

Incidentally, there's also an ICACHE_FLASH_ATTR attribute to put code into flash memory only.

It appears to be the case that you do not need to use the digitalPinToInterrupt() function to return an interrupt ID for given a GPIO pin number. This because digitalPinToInterrupt() returns the GPIO pin number on ESP8266 12E and ESP32 devices, so you could just use that on its own.


SOME KEY GUIDELINES

ENSURE THAT VARIABLES AND FUNCTION RETURN VALUES MATCH

This applies to all function calls, but particularly with the large numbers that are associated with timing events. If you are writing sketches that use millis() or micros(), you need to ensure that any variables that are given a value by those functions have the same type as the function, in this case, unsigned long.

OVERFLOW

If you're checking for a difference in time you need to remember that millis() and micros() will overflow and go past zero after about 50 days and 70 minutes respectively. If your sketch is expected to run longer than these times, you will need to accommodate the overflow.

DON'T DO ANY I/O IN AN ISR

Do not be tempted to do any kind of I/O in an ISR. printf() and the family of similar functions are generally large and slow and may cause your ISR to miss interrupts and give erroneous results. See stackoverflow.com and opengroup.org for information on other reasons why I/O in an ISR is poor practice.

INTERRUPT AND PIN NUMBERS

Interrupts are usually given an integer identification number, but these do not match the digital pins with which they are associated. For Arduinos and ESP devices, and to avoid confusion, it's good practice to use the digitalPinToInterrupt() function to convert from the digital pin number to the interrupt identifier. You'll need to do this if you wish to port the sketch to a different model of Arduino or ESP.