C语言嵌入式开发系统软件架构篇:中断服务程序

单片机的中断机制


中断机制是指单片机在执行主程序时,发生外部事件A,请求单片机迅速处理(中断请求),单片机暂停当前的主程序(中断响应),保存当前断点数据,然后调用事件A的处理程序进行事件响应,事件A处理完成后,主程序恢复断点处的数据,并继续执行主程序。
例如在智能温控系统中,系统中的温度传感器会实时监测环境温度。如果没有中断机制,CPU 就需要不断地查询温度传感器的数据,非常耗费 CPU 的时间和资源,这种方式被称为轮询。而有了中断机制,当温度传感器检测到温度超过或低于设定的阈值时,就会向 CPU 发送一个中断信号,CPU 收到信号后,会立即暂停当前正在执行的任务,迅速跳转到专门处理温度异常的程序部分(中断处理程序)。在中断处理程序内部,CPU 会根据预设的规则进行相应的操作,比如启动风扇降温或者开启加热设备升温。处理完温度问题后,CPU 再回到之前的任务上继续执行。这样一来,不仅提高了系统的响应速度,还能让 CPU 更高效地利用资源,去处理其他重要的任务。
再比如在一个实时数据采集系统中,外部设备可能会不定时地发送数据过来。中断机制能够确保 CPU 在第一时间捕获到这些数据到达的信号,并及时进行处理,避免数据丢失。

中断向量表

外部设备触发中断后,CPU会响应中断,并调用与中断相关的程序,该程序称为中断服务程序。标准 C 中不包含中断。许多编译开发商在标准 C 上增加了对中断的支持,提供新的关键字用于标示中断服务程序(ISR),类似于__interrupt、#program interrupt 等。当一个函数被定义为 ISR 的时候,编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。
外部设备的中断和中断服务程序如何对应呢?对应关系就是中断向量表。中断向量表是一个存储着中断服务程序入口地址的数据结构。当系统发生中断时,CPU 需要迅速找到对应的中断服务程序来处理这个中断事件,而中断向量表就是帮助 CPU 实现这一快速定位的数据结构。中断向量表是一种‌数组结构‌,每个数组元素对应一个中断源,存储该中断服务程序(ISR)的入口地址。这种数组结构能够通过索引(中断号)实现O(1)时间复杂度的快速访问。
每一个外部中断源都有一个唯一对应的中断向量,这个向量其实就是该中断服务程序在内存中的入口地址。可以把中断向量表想象成一个大型的电话簿,中断源就像是一个个需要联系的客户,而中断服务程序的入口地址则是客户的电话号码。当有 “客户”(中断源)发出请求时,CPU 就会通过查找 “电话簿”(中断向量表),快速找到对应的 “电话号码”(中断服务程序入口地址),从而准确无误地跳转到相应的中断服务程序去处理这个中断请求。​
在 STM32 中,中断向量表通常存储在闪存(Flash)的起始地址处,结构长度一般是256个中断向量。但中断向量表的大小并非固定不变,它取决于 STM32 芯片所支持的中断数量。一般来说,每个中断向量占用 4 个字节的空间,因此,中断向量表的大小等于中断数量乘以 4 字节。例如,如果一款 STM32 芯片支持 256 个中断,那么它的中断向量表大小就是 256×4 = 1024 字节。
中断向量表在C语言中通常表现为‌函数指针数组‌,每个元素对应一个中断号,存储对应的中断服务函数(ISR)地址。

typedef void (*isr_func)(void); // 定义中断函数指针类型

// 中断向量表定义(以256个中断为例)
__attribute__((section(".isr_vector")))
const isr_func interrupt_vector_table[256] = {
(isr_func)0x20001000, // 初始堆栈指针(根据硬件要求)
Reset_Handler, // 复位中断处理函数
NMI_Handler, // NMI中断处理函数
// ...其他中断服务函数入口
};

中断优先级

在嵌入式系统中,一般会存在多个中断源,这些中断源可能会在同一时间发出中断请求 。当这种情况发生时,CPU 该如何决定先处理哪个中断呢?这就需要用到中断优先级的概念。中断优先级可以理解为给不同的中断源分配不同的优先处理级别。
中断优先级分为单级中断优先级和多级中断优先级。在单级中断系统中,所有的中断源处于同一优先级,中断屏蔽字只需要一位来控制。当多个中断源同时申请中断时,CPU 会按照预先设定好的固定顺序依次响应这些中断请求 。比如,在一个简单的嵌入式系统中,只有按键中断和定时器中断两个中断源,且设定按键中断在前,定时器中断在后。当这两个中断同时发生时,CPU 会先响应按键中断,处理完按键中断服务程序后,再去响应定时器中断。​
而在多级中断系统中,中断源被分为多个不同的优先级级别。当多个中断源同时发出中断请求时,CPU 会首先响应优先级最高的中断。并且,高级中断可以打断低级中断处理程序的运行,转而执行高级中断处理程序,这就形成了中断嵌套的情况。例如,在一个工业自动化控制系统中,可能有紧急故障报警中断、数据采集中断和通信中断等多个中断源。我们可以将紧急故障报警中断设置为最高优先级,数据采集中断为次优先级,通信中断为较低优先级。当系统运行时,如果同时发生了这三个中断,CPU 会立即响应紧急故障报警中断,进入相应的中断服务程序进行处理。在处理过程中,如果此时数据采集中断发生,由于其优先级低于紧急故障报警中断,所以不会打断当前的中断服务程序。但如果在处理紧急故障报警中断时,又发生了优先级更高的中断(假设还有一个更紧急的安全保护中断),那么当前的紧急故障报警中断服务程序就会被打断,CPU 转而执行安全保护中断的服务程序,等安全保护中断处理完后,再回到紧急故障报警中断服务程序继续执行,最后再去处理数据采集中断和通信中断。​

多中断服务设计思路

一、中断优先级设置​


在多中断服务系统中,合理设置中断优先级是系统稳定运行的关键。不同的中断源有着不同的紧急程度和重要性,中断优先级就是用来明确这些中断响应顺序的机制。当多个中断请求同时出现时,CPU 会按照预先设定的优先级顺序依次处理。​
例如在一个工业控制系统中,涉及温度、压力、流量等多种传感器数据采集,同时还有紧急报警信号输入。此时,紧急报警信号的中断优先级就应设置得最高,因为一旦发生异常情况,需要系统立即响应并采取措施,以避免可能出现的严重后果;而温度、压力等传感器数据采集的中断优先级则可相对低一些。这样,当报警信号和传感器数据采集中断同时发生时,CPU 能够优先处理报警中断,确保系统安全。​
通常,中断优先级可分为固定优先级和动态优先级两种模式。固定优先级模式下,中断优先级在系统初始化时就已确定,且在运行过程中保持不变,这种模式实现简单、稳定性高,但缺乏灵活性;动态优先级模式则允许根据系统运行状态实时调整中断优先级,能更好地适应复杂多变的应用场景,但实现较为复杂,对系统资源的消耗也相对较大。在实际应用中,需根据具体需求权衡选择。​

二、中断处理流程优化​


一是简化中断服务程序代码,将非关键、耗时较长的操作放到主程序或后台任务中执行,中断服务程序仅负责处理最核心、最紧急的任务。例如,在处理串口接收中断时,中断服务程序只需将接收到的数据存入缓冲区,后续的数据解析和处理工作可在主程序中完成。二是合理运用中断屏蔽和使能机制,在执行关键代码段时,暂时屏蔽其他低优先级中断,防止被打断,确保中断任务能快速、完整地执行;完成中断操作后,再及时使能被屏蔽的中断 。三是充分利用硬件特性,如采用 DMA(直接内存访问)技术,在数据传输过程中减少 CPU 的参与,让 CPU 能够专注于其他任务,从而提高系统整体运行效率。​

三、共享资源管理​


在多中断环境下,当多个中断服务程序都需要访问同一资源时,若处理不当,极易引发冲突,导致数据错误或系统故障。​
例如两个中断服务程序都要对一个全局变量进行读写操作,如果没有有效的管理措施,就可能出现其中一个中断服务程序在读取变量值后、尚未完成写入操作时,另一个中断服务程序也对该变量进行读写,从而造成数据不一致的情况。​
为避免资源冲突,可以使用互斥锁和信号量。在中断服务程序访问共享资源前,先获取互斥锁,确保同一时间只有一个中断服务程序能够访问该资源;访问结束后,及时释放互斥锁。也可以通过信号量来避免资源冲突,将共享资源分配一个信号量,当信号量的值为 0 时,表示资源已被占用,其他中断服务程序需等待;当信号量的值大于 0 时,中断服务程序可获取信号量并访问资源。

多中断服务实现案例

硬件环境

以广泛应用的 STM32 开发板作为硬件平台,它基于 ARM Cortex-M 内核,在这个开发板上,有多个 GPIO(通用输入 / 输出)端口,如 PA、PB、PC 等,这些端口可用于连接各种外部设备,像按键、传感器、LED 灯等,以便产生硬件中断信号。以按键连接为例,可以将按键一端连接到某个 GPIO 引脚,另一端接地,当按键按下时,GPIO 引脚的电平会发生变化,从而触发中断。​
开发板还内置了多个定时器,如 TIM1、TIM2 等,定时器能够产生定时中断,常用于实现定时任务。例如可以利用定时器定时采集传感器数据,或者控制电机的运转时间。​
此外,串口通信接口 USART 实现与外部设备的串口通信,在通信过程中,接收和发送数据都可以触发中断 ,方便数据的实时处理。例如当上位机通过串口发送指令给开发板时,开发板的 USART 接收中断会被触发,从而及时响应上位机的指令。

代码实现

初始化代码的主要作用是对中断控制器以及相关硬件设备进行配置,为后续的中断处理做好准备。

#include "stm32f10x.h" // 包含STM32F10x系列的头文件

// 中断优先级分组配置
void NVIC_Configuration(void) {
NVIC_InitTypeDef NVIC_InitStructure;
// 配置中断优先级分组为2位抢占优先级,2位响应优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

// 配置按键中断
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
// 设置抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
// 设置响应优先级为0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
// 使能中断通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
// 初始化中断控制器
NVIC_Init(&NVIC_InitStructure);

// 配置定时器中断
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
// 设置抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
// 设置响应优先级为0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
// 使能中断通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
// 初始化中断控制器
NVIC_Init(&NVIC_InitStructure);
}

// 按键GPIO和中断线初始化
void EXTI_Configuration(void) {
EXTI_InitTypeDef EXTI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;

// 使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置PA0为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
// 初始化GPIOA
GPIO_Init(GPIOA, &GPIO_InitStructure);

// 使能AFIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 将PA0连接到EXTI0
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

// 配置EXTI0为中断模式,下降沿触发
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
// 使能EXTI0
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
// 初始化EXTI
EXTI_Init(&EXTI_InitStructure);
}

// 定时器TIM2初始化
void TIM_Configuration(void) {
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
// 使能TIM2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

// 定时器基本配置,预分频器设为71,计数周期设为9999,实现1s定时
TIM_TimeBaseStructure.TIM_Period = 9999;
TIM_TimeBaseStructure.TIM_Prescaler = 71;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
// 初始化TIM2
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

// 使能TIM2更新中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 启动TIM2
TIM_Cmd(TIM2, ENABLE);
}

// 系统初始化函数,调用上述初始化函数
void System_Init(void) {
NVIC_Configuration();
EXTI_Configuration();
TIM_Configuration();
}

在这段初始化代码中,NVIC_Configuration函数主要负责配置中断控制器 NVIC,设置中断优先级分组,并分别为按键中断(EXTI0)和定时器中断(TIM2)配置相应的中断优先级和使能中断通道。EXTI_Configuration函数用于初始化按键的 GPIO 和中断线,使能 GPIOA 时钟,配置 PA0 为浮空输入,将 PA0 连接到 EXTI0,并配置 EXTI0 为下降沿触发的中断模式。TIM_Configuration函数则对定时器 TIM2 进行初始化,使能 TIM2 时钟,设置定时器的预分频器、计数周期、时钟分频和计数模式,使能 TIM2 的更新中断并启动定时器。最后,System_Init函数将这些初始化函数整合起来,方便在主程序中调用,完成整个系统的初始化工作。

中断服务程序代码

中断服务程序是处理中断请求的核心部分,下面是按键中断服务程序和定时器中断服务程序的代码实现:

// 按键中断服务程序
void EXTI0_IRQHandler(void) {
if (EXTI_GetITStatus(EXTI_Line0) != RESET) { // 检查是否是EXTI0中断
// 执行按键按下后的操作,例如控制LED灯状态翻转
GPIO_WriteBit(GPIOC, GPIO_Pin_13, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13)));
// 清除EXTI0中断标志位
EXTI_ClearITPendingBit(EXTI_Line0);
}
}

// 定时器中断服务程序
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { // 检查是否是TIM2更新中断
// 执行定时任务,例如串口发送数据
// 假设已初始化串口USART1
USART_SendData(USART1, 'A');
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
// 清除TIM2更新中断标志位
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}

在按键中断服务程序EXTI0_IRQHandler中,首先通过EXTI_GetITStatus函数检查是否是 EXTI0 中断触发,如果是,则执行按键按下后的操作,这里以控制连接在 PC13 引脚上的 LED 灯状态翻转为例,通过读取当前 LED 灯的状态并取反后写入,实现 LED 灯的状态切换。最后,使用EXTI_ClearITPendingBit函数清除 EXTI0 中断标志位,以便下一次中断能够正常触发。​
在定时器中断服务程序TIM2_IRQHandler中,同样先通过TIM_GetITStatus函数检查是否是 TIM2 的更新中断触发,如果是,则执行定时任务,这里假设已初始化串口 USART1,通过USART_SendData函数向串口发送字符 'A',并等待发送完成标志位USART_FLAG_TXE置位,确保数据发送成功。完成任务后,使用TIM_ClearITPendingBit函数清除 TIM2 的更新中断标志位 。

主程序代码

主程序负责初始化系统,并在主循环中执行其他任务,同时与中断服务程序协同工作。​

int main(void) {​
// 初始化系统​
System_Init(); ​
// 使能GPIOC时钟,用于控制LED灯​
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); ​
GPIO_InitTypeDef GPIO_InitStructure;​
// 配置PC13为推挽输出,用于驱动LED灯​
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; ​
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; ​
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; ​
// 初始化GPIOC​
GPIO_Init(GPIOC, &GPIO_InitStructure); ​
​
while (1) {​
// 主循环中可以执行其他任务
// 这里简单示例为延时一段时间​
for (volatile int i = 0; i < 1000000; i++); ​
}​
}​


在主程序main中,首先调用System_Init函数完成系统的初始化工作,包括中断控制器、按键 GPIO 和中断线以及定时器的初始化。然后使能 GPIOC 时钟,并配置 PC13 为推挽输出模式,用于驱动 LED 灯。在主循环while(1)中,程序可以执行其他任务,这里简单地通过一个延时循环模拟主程序在执行其他操作,实际应用中可以根据需求进行数据处理、状态监测等复杂任务的编写 。在主程序运行过程中,如果有按键按下或定时器定时时间到,会触发相应的中断,CPU 将暂停主程序的执行,转而执行对应的中断服务程序,完成中断处理后再返回主程序继续执行。