Many of the abilities that modern digital electronics enable rely on the fact that common processors can perform millions or billions of operations every second. This blindingly fast speed can make it appear that a device is handling many operations at once when in fact, it is rapidly following a sequential set of instructions, often with multiple repetitions. Thus, from our perception of events at such a time scale, it is indistinguishable from instantaneous.
Today’s computers and smartphone processors also take advantage of multiple processing cores to further boost computational performance. But there are many applications that rely on slower, less expensive, single processing core microcontrollers (MCUs). For single core MCUs, processing speed alone may not deliver an adequate level of responsiveness. In such cases, the end user may feel that a device is sluggish. For example, there might be a large time lag between a button being pressed and an output LED response that results in a subpar user experience.

One option to combat this seemingly slow responsiveness is to poll (check) an input as frequently as possible. However, there is a good chance that many clock cycles will be wasted on checking the status of I/O pins that change state relatively infrequently, for instance, as compared to the number of times the button is checked. Furthermore, if the code that checks the status of a button is in a software loop that only breaks once the button is pressed, then the microcontroller will do nothing but check the button until then! Polling, while simple to implement, is a rather poor solution in cases where there’s one single-core processor and more than one task to accomplish. A majority of microcontrollers, including the lower cost ones, typically implement interrupts. As the name implies, interrupts temporarily suspend the normal operations of the main loop of code in response to some event. Depending on the MCU, there are multiple functions that use interrupts, including internal counters, external I/O pins that are equipped with special hardware, onboard analog-to-digital converters, EEPROMs and communications ports. In addition to interrupts that originate from hardware, some MCUs offer software-based interrupts. Software interrupts can do many functions as well, for instance, they can be used monitor internal registers and trigger an interrupt if certain events occur, such as an attempt by the arithmetic logic unit to divide by zero or if calculation results in an overflow.
In general, when an interrupt is triggered, the microcontroller finishes executing the current instruction. (There are special interrupts that have the ability to get critical tasks serviced immediately.) During an interrupt, the current status of the registers and the program counter must be stored in a special memory area called the stack. Pushing the current status of program execution onto the stack allows the MCU to resume normal program execution after the interrupt has been serviced. The behavior of the stack (last in, first out data structure) allows for nested (multiple) interrupts to be handled in order. Some microcontrollers allow for some level prioritization of interrupts relative to each other, however, sometimes it is preferable to simply disable interrupts while servicing the first interrupt to allow for controlled behavior.
There’s an organizing force behind the interrupts called an interrupt vector table that the MCU uses. Once the current status of the microcontroller’s registers is stored on the stack, the interrupt vector table dictates which instructions execute next. The interrupt vector table is a lookup table (sort of like an address book) that contains a memory address for the beginning of each interrupt service routine (ISR). An ISR is nothing more than a special function that is only called when a particular interrupt is triggered. Depending on the interrupt triggered, a different ISR can be invoked. For example, the microcontroller can behave a certain way if an internal timer interrupt is triggered and have a completely different behavior if an external I/O pin interrupt is triggered. In both cases, a device’s apparent responsiveness will seem much snappier than if you only had a polling method to rely on.
A further note on ISRs: You should take special care to minimize the amount of code within an interrupt service routine using only what is absolutely necessary to accomplish the needed interrupt. For example, it is not a good idea to print debugging messages from inside an interrupt service routine. Once the interrupt service routine has completed execution normal operations can resume by popping the previously stored register data off the stack and resetting the program counter to the value it would have been had the interrupt not been invoked.