Overview
By and large, a software application is a collection of machine instructions that occur in a defined sequence. The sequence of instructions is determined by the sourced code we generate. Operation A is followed by operation B which is followed by operation C and so on.
In most embedded systems, there are events that occur that do not follow this predictable sequence of operations. They are events that could happen at truly random intervals. These events are considered to be asynchronous to the normal operation of the system. We are going to call these events interrupts. The purpose of an interrupt is to alert the microprocessor that some important event has occurred and the operating state of the application should change to accommodate this event. The event interrupts the normal application flow in order to properly respond to the interrupt.
Interrupts allow a microprocessor to respond to events in a timely manner. Let’s take the example of a user pressing a push button. In our hypothetical example, the main application would perform board initialization and then enter an infinite loop that does some data operations, prints a message, and then checks to see if a push button has been pressed. The figure below provides a visual representation of this system. It represents two passes through the while loop.
As long as the button is pressed while the application is currently examining the button, the button press is detected. But what happens if the button is pressed and then released while the microprocessor is executing the instructions necessary to print the message? The application would essentially ignore the fact the button was pushed.
Interrupts are going to allow us to change how we examine the button. We want to be able to examine the button as soon as it is pressed. In order to do that, we need to temporarily stop the execution of our main program and begin executing code that is dedicated to examining the button. The figure below illustrates the desired behavior.
Interrupt Service Routine
When an interrupt occurs, a dedicated section of code is executed in response to the interrupt. This dedicated piece of code is called the Interrupt Service Routine or ISR. The ISR runs at a higher priority than the main application. This means that once the ISR begins to execute, it will run until completion and then return to the main application.
The purpose of the ISR is to acknowledge that the interrupt has occurred, queue any data the peripheral has received, and then possibly inform the main application that the event has occurred. Lets break down the responsibilities of the ISR in more detail.
Queue Data
Often times an interrupt indicates that new data has arrived. The ISR will read the data from the interrupt source and place the data into SRAM. Since SRAM is accessible by both the ISR and the main application, information is passed between the two using global variables.
It is best practice to have the ISR add data into a data queue of some kind. Examining the data and determining the appropriate response is not the responsibility of the ISR. Examining the data is deferred to the main application. The reason for only queuing the data is that we want our ISRs to be as short and fast as possible. The reason for short, fast ISRs is that most systems have multiple interrupt sources. If all of the interrupts are of the same priority, a slow ISR will block the other ISRs from executing for a significant period of time. Deferring a bulk of the computationally intensive instructions to the main application increases the probability that all interrupts will be received by the system in a timely manner.
Alert the Main Application
When new data arrives and is placed in SRAM, we need a mechanism to inform the main application that new data has been been placed in SRAM. The most basic approach is to have the ISR set a dedicated global variable to Boolean true.
The main application is written in a way that it only examines the data in SRAM if the dedicated global variable is set to true. If the global variable is set to true, the main application sets the global variable to false and begins to process the data placed in the queue by the ISR.
Acknowledge the Interrupt
When an interrupt occurs it normally sets a bit in an interrupt status register. The ISR needs to clear this bit in the status register so that processor resumes execution of the main application. The ISR should clear the status bit prior to exiting the ISR. If you fail to clear the status bit, the ISR will continually be called and the main application will never resume.
ISR Do’s
- Interrupt service routines should be as minimal as possible
- If the interrupt indicates that data has been received, add the data to a queue in SRAM
- Indicate events to the application using global variables.
- Clear the interrupt prior to returning to the main application
ISR Don’t’s
- Do not poll on any status bits in an ISR
- Do not use any computationally intensive loops
- Do not modify SRAM other than the dedicated queues and global variables that are being used to communicate with the main application
Interrupt Service Routine Execution
When an interrupt occurs, how does the interrupt service routine get executed? One common misconception is that the main application calls the interrupt service routine just like it would call any other function. That, however, is not the case. When an interrupt is detected, the microprocessor enters into a special interrupt mode called Handler mode. The microprocessor will save all of the general purpose registers, any status registers, and the program counter to either a reserved portion of SRAM or a dedicated hardware resource. This collection of registers fully describes the current computational state of an application. Saving these registers is called a context save. The context save allows the processor to halt the execution of an application and resume the application at a later time.
Once the context save is finished, the hardware will automatically modify the program counter (PC) and load it with the pre-defined address of an interrupt service routine. The ISR executes and carries out the instructions in the ISR. When the ISR exits, the microprocessor does a context restore by restoring the general purpose registers, PC, and status registers with the values each register held immediately before the interrupt. The final step the microprocessor carries out is to exit Handler Mode. At this point, the PC contains the address of the next instruction in the application and the application continues on as normal. Its important to realize that the ISR does not return to the main routine using the value in the link register. The microprocessor ‘returns’ to the main application by restoring the value of the PC when the microprocessor exits Handler Mode.
Polled Vs Vectored Interrupts
When an interrupt is detected and hardware has saved the context of the application, how does it know which interrupt service vector to execute? There are two common approaches to determining how to execute the correct interrupt service routine.
The first approach is called Polled Interrupts. If a microprocessor uses polled interrupts, the microprocessor will branch to a predefined location that contains a master ISR. Once inside the master ISR, the source of the interrupt is determined by examining interrupt status registers. The interrupt status registers will contain a bit mask that indicates which interrupts are currently active. Once the active interrupt has been determined, the master ISR will then make a function call to a function that acts as the dedicated ISR for the specified device.
The second approach to executing an ISR is called a vectored interrupt handler. A processor that implements a vectored interrupt handler has dedicated memory addresses for each peripheral device. Each of those memory address locations contains the address of the ISR associated with the specified peripheral. When an interrupt occurs, the microprocessor hardware detects which device caused the interrupt and will load the PC with the address specified in the vector table. Vectored interrupts normally result in a more complex interrupt mechanism, but it provides superior performance since software is not responsible for determining the active interrupt source.
ARM Cortex-M Interrupt Priority
Most embedded systems will have multiple interrupts active. So what happens when the ISR of one peripheral device is being executed and another interrupt occurs? The answer to that depends on the priority of each ISR. The ARM architecture allows us to set a priority level for different interrupts. The lower the interrupt number, the higher priority. If an ISR is already running and a higher priority interrupt occurs, the lower priority ISR is interrupted and the higher priority ISR begins to execute. When the higher priority ISR finishes, the remainder of the lower ISR will then complete.
If the second interrupt is of the same priority or lower priority, the active ISR will complete and then the second interrupt’s ISR is executed. Notice that the second ISR is not executed immediately, but it does in fact get executed.
If multiple lower/same priority interrupts occur while a higher priority ISR is active, only one of the interrupts from the lower/same priority source will be acknowledged. The ISR status registers do not keep track of how many times a specific interrupt has occurred.