Embedded Development Software Architecture in C: Interrupt Service Routines

1. Interrupt Mechanism of Microcontrollers

The interrupt mechanism refers to the ability of a microcontroller to quickly handle external events (interrupt requests) while executing the main program. When an external event A occurs, the microcontroller pauses the current main program (interrupt response), saves the current state data, and then calls the handler for event A to respond to the event. After processing event A, the main program resumes execution from the saved state.

For example, in a smart temperature control system, the temperature sensor continuously monitors the ambient temperature. Without an interrupt mechanism, the CPU would need to constantly poll the temperature sensor for data, which is very resource-intensive. This method is known as polling. With an interrupt mechanism, when the temperature sensor detects that the temperature exceeds or falls below a set threshold, it sends an interrupt signal to the CPU. Upon receiving the signal, the CPU immediately pauses the current task and jumps to the specific program section that handles temperature anomalies (the interrupt handler). Within the interrupt handler, the CPU performs actions based on predefined rules, such as turning on a fan to cool down or activating a heating device to warm up. After addressing the temperature issue, the CPU returns to the previous task and continues execution. This not only improves the system’s response speed but also allows the CPU to utilize resources more efficiently to handle other important tasks.

Similarly, in a real-time data acquisition system, external devices may send data at irregular intervals. The interrupt mechanism ensures that the CPU captures the signal indicating data arrival promptly and processes it in a timely manner, preventing data loss.

2. Interrupt Vector Table

When an external device triggers an interrupt, the CPU responds to the interrupt and calls the associated program, known as the interrupt service routine (ISR). Standard C does not include interrupts. Many compiler developers have added support for interrupts on standard C, providing new keywords to denote interrupt service routines (ISRs), such as __interrupt and #program interrupt. When a function is defined as an ISR, the compiler automatically adds the necessary code for saving and restoring the interrupt context.

How do external device interrupts correspond to interrupt service routines? The correspondence is defined by the interrupt vector table. The interrupt vector table is a data structure that stores the entry addresses of interrupt service routines. When an interrupt occurs, the CPU needs to quickly locate the corresponding ISR to handle the interrupt event, and the interrupt vector table facilitates this rapid location. The interrupt vector table is an array structure, where each array element corresponds to an interrupt source and stores the entry address of the ISR. This array structure allows for O(1) time complexity access through indexing (interrupt number).

Each external interrupt source has a unique corresponding interrupt vector, which is essentially the entry address of the ISR in memory. You can think of the interrupt vector table as a large phone book, where interrupt sources are like customers to contact, and the entry addresses of ISRs are like the customers’ phone numbers. When a “customer” (interrupt source) makes a request, the CPU looks up the “phone book” (interrupt vector table) to quickly find the corresponding “phone number” (ISR entry address) and accurately jump to the appropriate ISR to handle the interrupt request.

In STM32, the interrupt vector table is typically stored at the starting address of flash memory, and its length is generally 256 interrupt vectors. However, the size of the interrupt vector table is not fixed; it depends on the number of interrupts supported by the STM32 chip. Generally, each interrupt vector occupies 4 bytes of space, so the size of the interrupt vector table equals the number of interrupts multiplied by 4 bytes. For example, if an STM32 chip supports 256 interrupts, its interrupt vector table size would be 256×4 = 1024 bytes.

In C, the interrupt vector table is typically represented as an array of function pointers, where each element corresponds to an interrupt number and stores the address of the corresponding ISR.

typedef void (*isr_func)(void);  // Define interrupt function pointer type// Interrupt vector table definition (example with 256 interrupts)__attribute__((section(".isr_vector"))) const isr_func interrupt_vector_table[256] = {    (isr_func)0x20001000,       // Initial stack pointer (according to hardware requirements)    Reset_Handler,              // Reset interrupt handler    NMI_Handler,                // NMI interrupt handler    // ... other ISR entry points};

3. Interrupt Priority

In embedded systems, there are generally multiple interrupt sources, which may issue interrupt requests simultaneously. When this occurs, how does the CPU decide which interrupt to handle first? This is where the concept of interrupt priority comes into play. Interrupt priority can be understood as assigning different levels of urgency to different interrupt sources.

Interrupt priorities can be classified into single-level and multi-level interrupt priorities. In a single-level interrupt system, all interrupt sources are at the same priority level, and the interrupt mask only needs one bit to control. When multiple interrupt sources request an interrupt simultaneously, the CPU responds to these requests in a predetermined fixed order. For example, in a simple embedded system with only button interrupts and timer interrupts, if the button interrupt is set to be higher priority than the timer interrupt, when both interrupts occur simultaneously, the CPU will first respond to the button interrupt, handle the button ISR, and then respond to the timer interrupt.

In a multi-level interrupt system, interrupt sources are divided into multiple different priority levels. When multiple interrupt sources issue interrupt requests simultaneously, the CPU will first respond to the highest priority interrupt. Moreover, higher priority interrupts can interrupt lower priority interrupt handlers, leading to interrupt nesting. For example, in an industrial automation control system, there may be multiple interrupt sources such as emergency fault alarm interrupts, data acquisition interrupts, and communication interrupts. We can set the emergency fault alarm interrupt to the highest priority, the data acquisition interrupt to a secondary priority, and the communication interrupt to a lower priority. When the system is running, if all three interrupts occur simultaneously, the CPU will immediately respond to the emergency fault alarm interrupt, enter the corresponding ISR for processing. If a data acquisition interrupt occurs during this process, it will not interrupt the current ISR because its priority is lower than that of the emergency fault alarm interrupt. However, if a higher priority interrupt (let’s say an even more urgent safety protection interrupt) occurs while handling the emergency fault alarm interrupt, the current ISR will be interrupted, and the CPU will execute the safety protection ISR, returning to the emergency fault alarm ISR after handling the safety protection interrupt, and finally processing the data acquisition and communication interrupts.

4. Multi-Interrupt Service Design Considerations

1. Setting Interrupt Priorities

In a multi-interrupt service system, properly setting interrupt priorities is key to stable system operation. Different interrupt sources have varying degrees of urgency and importance, and interrupt priorities are used to clarify the order of these interrupt responses. When multiple interrupt requests occur simultaneously, the CPU processes them in the order of the predefined priorities.

For example, in an industrial control system involving temperature, pressure, flow, and other sensor data collection, there may also be emergency alarm signal inputs. In this case, the interrupt priority for the emergency alarm signal should be set to the highest, as immediate response and action are required to avoid potentially serious consequences; while the interrupt priorities for temperature and pressure sensor data collection can be set relatively lower. This way, when both the alarm signal and sensor data collection interrupts occur simultaneously, the CPU can prioritize handling the alarm interrupt to ensure system safety.

Typically, interrupt priorities can be divided into fixed priority and dynamic priority modes. In fixed priority mode, interrupt priorities are determined during system initialization and remain unchanged during operation. This mode is simple to implement and highly stable but lacks flexibility. Dynamic priority mode allows for real-time adjustment of interrupt priorities based on system operating conditions, better adapting to complex and variable application scenarios, but it is more complex to implement and consumes more system resources. In practical applications, a balance must be struck based on specific needs.

2. Optimizing Interrupt Handling Processes

First, simplify the ISR code by moving non-critical, time-consuming operations to the main program or background tasks, allowing the ISR to handle only the most core and urgent tasks. For example, when handling a serial port receive interrupt, the ISR only needs to store the received data in a buffer, while subsequent data parsing and processing can be done in the main program. Second, use interrupt masking and enabling mechanisms wisely; temporarily mask lower priority interrupts during critical code execution to prevent interruptions, ensuring that the interrupt task can execute quickly and completely. After completing the interrupt operation, promptly enable the masked interrupts. Third, fully utilize hardware features, such as using DMA (Direct Memory Access) technology to reduce CPU involvement during data transfer, allowing the CPU to focus on other tasks, thereby improving overall system efficiency.

3. Managing Shared Resources

In a multi-interrupt environment, when multiple ISRs need to access the same resource, improper handling can easily lead to conflicts, resulting in data errors or system failures.

For example, if two ISRs need to read and write to a global variable, without effective management measures, one ISR may read the variable value and, before completing the write operation, another ISR may also read and write to that variable, leading to data inconsistency.

To avoid resource conflicts, mutexes and semaphores can be used. Before an ISR accesses a shared resource, it first acquires a mutex to ensure that only one ISR can access that resource at a time; after access, it promptly releases the mutex. Semaphores can also be used to avoid resource conflicts by assigning a semaphore to the shared resource. When the semaphore value is 0, it indicates that the resource is occupied, and other ISRs must wait; when the semaphore value is greater than 0, the ISR can acquire the semaphore and access the resource.

5. Multi-Interrupt Service Implementation Example

1. Hardware Environment

Using the widely used STM32 development board as the hardware platform, which is based on the ARM Cortex-M core. This development board has multiple GPIO (General Purpose Input/Output) ports, such as PA, PB, PC, etc., which can be used to connect various external devices like buttons, sensors, and LEDs to generate hardware interrupt signals. For example, a button can be connected to a GPIO pin, and when the button is pressed, the GPIO pin level changes, triggering an interrupt.

The development board also has multiple timers, such as TIM1 and TIM2, which can generate periodic interrupts, commonly used for timed tasks. For instance, a timer can be used to periodically collect sensor data or control the operation time of a motor.

Additionally, the USART serial communication interface enables serial communication with external devices, where both receiving and sending data can trigger interrupts, facilitating real-time data processing. For example, when a host computer sends commands to the development board via serial port, the USART receive interrupt will be triggered, allowing the development board to respond promptly to the host’s commands.

2. Code Implementation

The initialization code primarily configures the interrupt controller and related hardware devices, preparing for subsequent interrupt handling.

#include "stm32f10x.h"  // Include STM32F10x series header files// Interrupt priority group configurationvoid NVIC_Configuration(void) {    NVIC_InitTypeDef NVIC_InitStructure;    // Configure interrupt priority group to 2 bits for preemption priority, 2 bits for response priority    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);     // Configure button interrupt    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;     // Set preemption priority to 1    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;     // Set response priority to 0    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;     // Enable interrupt channel    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;     // Initialize interrupt controller    NVIC_Init(&NVIC_InitStructure);     // Configure timer interrupt    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;     // Set preemption priority to 2    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;     // Set response priority to 0    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;     // Enable interrupt channel    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;     // Initialize interrupt controller    NVIC_Init(&NVIC_InitStructure); }// Button GPIO and interrupt line initializationvoid EXTI_Configuration(void) {    EXTI_InitTypeDef EXTI_InitStructure;    GPIO_InitTypeDef GPIO_InitStructure;    // Enable GPIOA clock    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);     // Configure PA0 as floating input    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;     // Initialize GPIOA    GPIO_Init(GPIOA, &GPIO_InitStructure);     // Enable AFIO clock    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);     // Connect PA0 to EXTI0    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);     // Configure EXTI0 as interrupt mode, triggered on falling edge    EXTI_InitStructure.EXTI_Line = EXTI_Line0;     EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;     EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;     // Enable EXTI0    EXTI_InitStructure.EXTI_LineCmd = ENABLE;     // Initialize EXTI    EXTI_Init(&EXTI_InitStructure); }// Timer TIM2 initializationvoid TIM_Configuration(void) {    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;    // Enable TIM2 clock    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);     // Basic timer configuration, prescaler set to 71, count period set to 9999, achieving 1s timing    TIM_TimeBaseStructure.TIM_Period = 9999;     TIM_TimeBaseStructure.TIM_Prescaler = 71;     TIM_TimeBaseStructure.TIM_ClockDivision = 0;     TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;     // Initialize TIM2    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);     // Enable TIM2 update interrupt    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);     // Start TIM2    TIM_Cmd(TIM2, ENABLE); }// System initialization function, calls the above initialization functionsvoid System_Init(void) {    NVIC_Configuration();    EXTI_Configuration();    TIM_Configuration();}

In this initialization code, the NVIC_Configuration function is primarily responsible for configuring the interrupt controller NVIC, setting the interrupt priority groups, and configuring the respective interrupt priorities and enabling interrupt channels for the button interrupt (EXTI0) and timer interrupt (TIM2). The EXTI_Configuration function initializes the button GPIO and interrupt line, enabling the GPIOA clock, configuring PA0 as a floating input, connecting PA0 to EXTI0, and configuring EXTI0 as a falling edge triggered interrupt. The TIM_Configuration function initializes the TIM2 timer, enabling the TIM2 clock, setting the timer’s prescaler, count period, clock division, and count mode, enabling the TIM2 update interrupt, and starting the timer. Finally, the System_Init function integrates these initialization functions for easy calling in the main program, completing the entire system initialization process.

3. Interrupt Service Routine Code

The interrupt service routine is the core part of handling interrupt requests. Below is the code implementation for the button interrupt service routine and the timer interrupt service routine:

// Button interrupt service routinevoid EXTI0_IRQHandler(void) {    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {  // Check if it is EXTI0 interrupt        // Execute actions after button press, e.g., toggle LED state        GPIO_WriteBit(GPIOC, GPIO_Pin_13, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13)));         // Clear EXTI0 interrupt flag        EXTI_ClearITPendingBit(EXTI_Line0);     }}// Timer interrupt service routinevoid TIM2_IRQHandler(void) {    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {  // Check if it is TIM2 update interrupt        // Execute timed task, e.g., send data via serial port        // Assume USART1 has been initialized        USART_SendData(USART1, 'A');         while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);         // Clear TIM2 update interrupt flag        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);     }}

In the button interrupt service routine EXTI0_IRQHandler, the first step is to check whether the EXTI0 interrupt was triggered using the EXTI_GetITStatus function. If it was, the actions following the button press are executed, such as toggling the state of the LED connected to pin PC13 by reading the current state and writing the inverted value back. Finally, the EXTI_ClearITPendingBit function is used to clear the EXTI0 interrupt flag to allow for the next interrupt to trigger normally.

In the timer interrupt service routine TIM2_IRQHandler, the same approach is taken to check whether the TIM2 update interrupt was triggered using the TIM_GetITStatus function. If it was, the timed task is executed, such as sending the character ‘A’ via the initialized USART1 serial port using the USART_SendData function, and waiting for the transmission complete flag USART_FLAG_TXE to be set to ensure successful data transmission. After completing the task, the TIM_ClearITPendingBit function is used to clear the TIM2 update interrupt flag.

4. Main Program Code

The main program is responsible for initializing the system and executing other tasks in the main loop while working in conjunction with the interrupt service routines.

int main(void) {    // Initialize system    System_Init();     // Enable GPIOC clock for controlling LED    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);     GPIO_InitTypeDef GPIO_InitStructure;    // Configure PC13 as push-pull output for driving LED    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;     // Initialize GPIOC    GPIO_Init(GPIOC, &GPIO_InitStructure);     while (1) {        // Other tasks can be executed in the main loop        // Here, a simple example is to delay for a period        for (volatile int i = 0; i < 1000000; i++);     }}

In the main program main, the System_Init function is first called to complete the system initialization, including the interrupt controller, button GPIO, interrupt line, and timer initialization. Then, the GPIOC clock is enabled, and PC13 is configured as a push-pull output mode to drive the LED. In the main loop while(1), the program can execute other tasks, here simply simulated by a delay loop. In actual applications, more complex tasks such as data processing and status monitoring can be implemented as needed. During the execution of the main program, if a button is pressed or the timer reaches its timing interval, the corresponding interrupt will be triggered, causing the CPU to pause the execution of the main program and execute the corresponding ISR, completing the interrupt handling before returning to continue executing the main program.

Embedded Development Software Architecture in C: Interrupt Service Routines

Leave a Comment