在云栈社区的嵌入式开发讨论中,中断服务程序(ISR)始终是一个绕不开的核心话题。它承担着响应硬件事件的重任,直接决定系统的实时性与可靠性。一个编写不当的 ISR,轻则让系统响应迟缓,重则引发难以排查的随机故障。那么,怎么才能写出合格的 ISR 呢?
ISR 的本质
ISR 与普通函数的区别
很多人刚开始接触嵌入式开发时,会把 ISR 当成一个“特殊的函数”来看。但 ISR 和普通函数的差异,远不止写法上的不同。下表梳理了二者的核心区别:
| 特性 |
普通函数 |
ISR |
| 调用方式 |
显式函数调用 |
触发调用 |
| 执行时机 |
程序流程决定 |
异步、不可预测 |
| 进入/退出 |
简单跳转/返回 |
保存/恢复完整上下文 |
| 执行时间 |
无严格限制 |
必须极短 |
| 栈使用 |
使用当前任务栈 |
可能使用独立中断栈 |
| 可重入性 |
可重入设计 |
通常不可重入,需特别处理 |
| 资源访问 |
常规方式 |
需考虑与主程序/其他ISR的竞争 |
设计原则
ISR 的设计原则可以用一句话概括:快进快出,只做最必要的事。下面这个例子,很直观地展示了初学者最容易犯的错误,以及对应的正确写法。
// ❌ 错误示例:ISR中做太多事情
void UART_IRQHandler(void) {
char buffer[256];
int i;
// 接收数据
for (i = 0; i < 256; i++) {
while (!(USART1->SR & USART_SR_RXNE));
buffer[i] = USART1->DR;
}
// 复杂的数据处理
process_large_data(buffer, 256);
// 发送响应
send_response_over_uart(buffer);
}
// ✅ 正确示例:ISR只做最必要的工作
volatile uint8_t uart_rx_buffer[256];
volatile uint16_t uart_rx_index = 0;
volatile uint8_t uart_data_ready = 0;
void UART_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
uint8_t data = USART1->DR;
if (uart_rx_index < 255) {
uart_rx_buffer[uart_rx_index++] = data;
}
if (data == '\n') {
uart_data_ready = 1;
}
}
}
// 在主循环中处理数据
void main_loop(void) {
while (1) {
if (uart_data_ready) {
process_large_data(uart_rx_buffer, uart_rx_index);
send_response_over_uart(uart_rx_buffer);
uart_rx_index = 0;
uart_data_ready = 0;
}
}
}
对照两段代码,差异一目了然。再强调几个要点:
- 精简高效:ISR 执行时间应尽量短(通常 <10% CPU 周期)
- 避免阻塞:禁止使用
delay() 等阻塞式函数
- 减少复杂度:避免在 ISR 中进行复杂计算、大量数据处理
- 任务分离:ISR 只做数据接收/发送,复杂处理交给主循环或任务
可重入性
什么是可重入性
所谓可重入函数,是指在被调用过程中再次被中断并重新进入,仍能正确执行的函数。这个概念直接关系到 ISR 的安全性——如果你的 ISR 里调用了一个不可重入函数,就相当于埋下了一颗不定时炸弹。
可重入函数的条件
一个函数要做到可重入,必须同时满足以下三条:
- 不使用全局或静态变量
- 不修改自身代码
- 只调用可重入函数
// ❌ 不可重入函数:使用静态变量
int bad_counter(void) {
static int count = 0; // 静态变量,不可重入
count++;
return count;
}
// ✅ 可重入函数:使用参数传递状态
int good_counter(int *count) {
(*count)++; // 使用外部传入的指针
return *count;
}
// ❌ 不可重入:返回指向静态数据的指针
char *bad_format_time(time_t t) {
static char buffer[32]; // 静态缓冲区
strftime(buffer, sizeof(buffer), "%Y-%m-%d", localtime(&t));
return buffer; // 多次调用会覆盖同一缓冲区
}
// ✅ 可重入:调用者提供缓冲区
void good_format_time(time_t t, char *buffer, size_t size) {
strftime(buffer, size, "%Y-%m-%d", localtime(&t));
}
ISR 中的可重入问题
ISR 和主程序共享全局变量是最常见的隐患。当 32 位变量在 8 位或 16 位 MCU 上被读取时,一次读操作可能被拆成多条指令,这期间如果来了中断,读到的值就可能面目全非。
// ❌ 危险示例:ISR和主程序共享全局变量
uint32_t system_tick = 0;
void SysTick_Handler(void) {
system_tick++; // 读-改-写操作,可能被打断
}
uint32_t get_tick(void) {
return system_tick; // 32位读取在8位/16位MCU上不是原子操作
}
// ✅ 安全示例1:使用volatile + 临界区保护
volatile uint32_t system_tick = 0;
void SysTick_Handler(void) {
system_tick++;
}
uint32_t get_tick(void) {
uint32_t tick;
__disable_irq();
tick = system_tick;
__enable_irq();
return tick;
}
// ✅ 安全示例2:使用原子操作(C11/C17)
#include <stdatomic.h>
atomic_uint_fast32_t system_tick = ATOMIC_VAR_INIT(0);
void SysTick_Handler(void) {
atomic_fetch_add(&system_tick, 1);
}
uint32_t get_tick(void) {
return atomic_load(&system_tick);
}
浮点运算的特殊处理
在 ISR 中使用浮点数需要格外小心。某些架构下,浮点寄存器不会自动保存和恢复,ISR 里的浮点运算可能会悄悄破坏主程序的浮点上下文。
// ❌ 危险:在ISR中使用浮点运算(某些架构)
float float_value = 0.0f;
void TIM_IRQHandler(void) {
float_value += 0.1f; // 可能破坏主程序的浮点上下文
}
// ✅ 安全:编译器特定指令保存浮点上下文
#pragma SAVE
#pragma FPU
void TIM_IRQHandler(void) {
float_value += 0.1f;
}
#pragma RESTORE
// ✅ 更好:避免在ISR中使用浮点数
volatile uint32_t integer_counter = 0;
void TIM_IRQHandler(void) {
integer_counter++; // 使用整数,主循环中再转换
}
void main_loop(void) {
while (1) {
float float_value = integer_counter * 0.1f; // 在主循环计算
}
}
临界区保护
什么是临界区
临界区是指访问共享资源的代码段,在同一时间只能有一个执行流进入。如果没有保护,多个执行流同时读写同一份数据,轻则数据错乱,重则系统崩溃。临界区保护到底有哪些常用手段?
临界区保护的常用方法
关中断(最简单但影响大)
关中断是最直接的办法,但也最粗暴——它会暂时屏蔽所有中断,影响系统实时性。
// 基础实现
void critical_section_example(void) {
__disable_irq(); // 关中断
// 临界区代码
shared_data++;
__enable_irq(); // 开中断
}
// ✅ 更安全:保存中断状态
void safe_critical_section(void) {
uint32_t primask = __get_PRIMASK(); // 保存当前中断状态
__disable_irq();
// 临界区代码
shared_data++;
__set_PRIMASK(primask); // 恢复之前的状态
}
// ✅ 封装成宏(推荐)
#define CRITICAL_SECTION_ENTER() \
uint32_t __primask = __get_PRIMASK(); \
__disable_irq()
#define CRITICAL_SECTION_EXIT() \
__set_PRIMASK(__primask)
void usage_example(void) {
CRITICAL_SECTION_ENTER();
shared_data++;
CRITICAL_SECTION_EXIT();
}
使用基元屏蔽(只屏蔽相关中断)
更好的做法是只屏蔽与共享资源相关的中断,把对其他中断的影响降到最低。
// ✅ 更好:只屏蔽相关中断,不影响其他中断
void uart_specific_protection(void) {
NVIC_DisableIRQ(UART1_IRQn); // 只屏蔽UART1中断
// 临界区:只访问UART相关的共享数据
uart_rx_buffer[uart_rx_index++] = data;
NVIC_EnableIRQ(UART1_IRQn); // 恢复UART1中断
}
使用互斥锁(RTOS 环境)
如果系统跑着 RTOS,互斥锁能提供更优雅的同步机制。
// ✅ 在RTOS中使用互斥锁
#include "FreeRTOS.h"
#include "semphr.h"
SemaphoreHandle_t xMutex;
void rtos_critical_section(void) {
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 临界区代码
shared_data++;
xSemaphoreGive(xMutex);
}
}
中断嵌套处理
什么是中断嵌套
中断嵌套是指高优先级中断打断正在执行的低优先级中断。这种机制在实时系统中很常见,但如果优先级配置不当,也会引发优先级反转等问题。
Cortex-M 中的优先级配置
以 STM32F4 为例,Cortex-M 内核通过抢占优先级和子优先级共同决定中断响应顺序。
#include "stm32f4xx.h"
void NVIC_Configuration(void) {
// 设置优先级分组:抢占优先级2位,子优先级2位
// 抢占优先级:决定是否可以打断其他中断
// 子优先级:决定同优先级中断的响应顺序
NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
// 配置高优先级中断(抢占优先级0)
NVIC_SetPriority(EXTI0_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 0, 0));
NVIC_EnableIRQ(EXTI0_IRQn);
// 配置中优先级中断(抢占优先级1)
NVIC_SetPriority(TIM2_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 1, 0));
NVIC_EnableIRQ(TIM2_IRQn);
// 配置低优先级中断(抢占优先级2)
NVIC_SetPriority(UART1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 2, 0));
NVIC_EnableIRQ(UART1_IRQn);
}
常见问题处理
忘清除中断标志
忘记清除中断标志会让 ISR 反复触发,直接耗尽 CPU 资源。
// ❌ 错误:忘清除中断标志,导致ISR反复触发
void Bad_TIM_IRQHandler(void) {
g_counter++;
// 忘记清除TIM中断标志!
}
// ✅ 正确:及时清除中断标志
void Good_TIM_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 先清除标志
g_counter++;
}
}
ISR 中调用非重入函数
malloc()、free() 这类库函数往往不可重入,在 ISR 里调用它们极有可能引发难以追踪的问题。
// ❌ 危险:在ISR中调用malloc()
void Bad_ISR(void) {
char *buffer = malloc(256); // malloc可能不可重入
// 使用buffer
free(buffer);
}
// ✅ 正确:预先分配缓冲区
static char isr_buffer[256]; // 静态缓冲区
void Good_ISR(void) {
// 使用预分配的缓冲区
isr_buffer[0] = data;
}
volatile 使用不当
共享变量不加 volatile 是新手最常见的失误之一。编译器优化可能把你认为会重新读取的变量直接缓存起来,导致主循环永远等不到 ISR 设置的值。
// ❌ 错误:共享变量未用volatile声明
uint8_t flag = 0; // 编译器可能优化掉读取操作
void ISR(void) {
flag = 1;
}
void main_loop(void) {
while (!flag); // 编译器可能优化成死循环!
process_data();
}
// ✅ 正确:使用volatile
volatile uint8_t flag = 0; // 告诉编译器变量可能被意外修改
void ISR(void) {
flag = 1;
}
void main_loop(void) {
while (!flag); // 正确读取
process_data();
}
printf 调试 ISR
在 ISR 里用 printf 打印调试信息,听起来很自然,实际上会引入严重的实时性问题。
// ❌ 错误:在ISR中使用printf
void Bad_ISR(void) {
printf("ISR triggered\r\n"); // printf很慢,可能导致问题
}
// ✅ 正确:使用简单的标志或GPIO
volatile uint8_t isr_triggered = 0;
void Good_ISR(void) {
isr_triggered = 1;
GPIOA->BSRR = GPIO_PIN_0; // 翻转GPIO(最快)
}
总结
ISR 是嵌入式系统中最关键也最容易出错的部分。遵循这些编写规范,可以帮助你避开常见陷阱,写出安全、高效且可靠的中断服务程序。如果你在实践中遇到过其他棘手的中断问题,也不妨回到云栈社区与更多嵌入式开发者一起交流,共同少走弯路。