STM32中断服务函数与主循环协作机制设计实践

在嵌入式系统开发中,中断机制是应对异步事件的重要方式。以 STM32 系列微控制器为例,其 NVIC 模块支持多种外设中断,灵活性较高。但如果中断设计不合理,容易导致系统逻辑紊乱、响应变慢,甚至出现死锁。本文将结合中断最小化的实践经验,介绍如何通过事件标志、任务调度等方式,合理安排中断与主循环之间的配合。同时,结合接口分层、回调函数、弱定义函数等常用手法,构建一套清晰、可维护、易扩展的中断处理架构。


一、中断最小化原则:只做标记,不做处理

原则:中断服务函数(ISR)应快速完成,禁止执行复杂逻辑或阻塞操作,仅设置事件标志。

示例:按键外部中断

volatile uint8_t key_pressed = 0;

void EXTI0_IRQHandler(void)
{
    if (EXTI->PR & EXTI_PR_PR0) {
        EXTI->PR = EXTI_PR_PR0;    // 清除中断标志
        key_pressed = 1;           // 设置标志位
    }
}

说明:ISR 只设置一个标志位,避免引入业务逻辑或与其他模块的耦合。


二、事件标志:中断与主逻辑的桥梁

中断设置标志,主循环轮询处理。这种方式适用于裸机系统或轻量任务系统。

示例:主循环处理事件

void key_process(void)
{
    // 实际按键处理逻辑
}

int main(void)
{
    while (1) {
        if (key_pressed) {
            key_pressed = 0;
            key_process();
        }
    }
}

优点:中断代码简单,主循环集中逻辑处理,方便调试与维护。


三、任务调度思想:可扩展的事件处理模型

当事件数量增多时,多个标志变量与处理函数容易造成代码混乱。推荐采用任务表机制实现统一调度。

示例:事件调度框架

typedef struct {
    volatile uint8_t *flag;
    void (*handler)(void);
} task_t;

volatile uint8_t key_flag = 0;
volatile uint8_t uart_flag = 0;

void key_handler(void) { /* 按键逻辑 */ }
void uart_handler(void) { /* 串口逻辑 */ }

task_t task_table[] = {
    { &key_flag,  key_handler },
    { &uart_flag, uart_handler }
};

#define TASK_NUM (sizeof(task_table) / sizeof(task_table[0]))

int main(void)
{
    while (1) {
        for (int i = 0; i < TASK_NUM; i++) {
            if (*(task_table[i].flag)) {
                *(task_table[i].flag) = 0;
                task_table[i].handler();
            }
        }
    }
}

优势:易扩展、结构清晰,增加任务只需新增 flag 和 handler。


四、接口抽象:模块解耦的基础

通过接口抽象设计,使得中断来源与响应逻辑之间解耦,提高复用性和可维护性。

示例:抽象外部输入事件接口

typedef struct {
    void (*on_key_press)(void);
    void (*on_uart_rx)(uint8_t);
} event_cb_t;

event_cb_t g_event = {0};

在 ISR 或调度函数中调用:

void key_handler(void)
{
    if (g_event.on_key_press) {
        g_event.on_key_press();
    }
}

用户注册:

void my_key_callback(void)
{
    // 应用层按键响应
}

void app_init(void)
{
    g_event.on_key_press = my_key_callback;
}

说明:业务逻辑从驱动层剥离,应用只关注回调注册与处理。


五、回调机制:灵活事件通知方式

与接口抽象配合,回调机制适合中断源较多、响应逻辑不固定的场景,如串口接收、多路 GPIO 输入等。

示例:串口接收回调机制

驱动层:

typedef void (*uart_rx_cb_t)(uint8_t);
static uart_rx_cb_t rx_cb = NULL;

void uart_register_callback(uart_rx_cb_t cb)
{
    rx_cb = cb;
}

void USART1_IRQHandler(void)
{
    if (USART1->SR & USART_SR_RXNE) {
        uint8_t data = USART1->DR;
        if (rx_cb) {
            rx_cb(data);
        }
    }
}

用户层:

void my_uart_rx_handler(uint8_t data)
{
    // 应用层处理接收到的数据
}

void app_init(void)
{
    uart_register_callback(my_uart_rx_handler);
}

特点:多个模块可分别注册各自回调,驱动层无需修改,扩展性好。


六、弱函数重载:默认实现 + 用户替换

通过 __weak 提供默认行为,支持用户重载自定义逻辑,是模块库常用的扩展机制。

示例:系统错误处理钩子

默认定义:

__weak void system_fault_handler(int code)
{
    // 默认处理:重启或挂起
}

在其他模块中调用:

if (error_code != 0) {
    system_fault_handler(error_code);
}

用户实现自定义:

void system_fault_handler(int code)
{
    log_error("System fault: %d", code);
    save_state();
    reset_device();
}

优点:库模块提供默认行为,用户按需重载,互不干扰。


七、综合应用:按键+LED中断响应架构设计

1. 中断设置事件标志

volatile uint8_t key_event_flag = 0;

void EXTI0_IRQHandler(void)
{
    if (EXTI->PR & EXTI_PR_PR0) {
        EXTI->PR = EXTI_PR_PR0;
        key_event_flag = 1;
    }
}

2. 调度函数中回调通知

void key_event_handler(void)
{
    if (g_event.on_key_press) {
        g_event.on_key_press();
    }
}

3. 任务表调度

task_t task_table[] = {
    { &key_event_flag, key_event_handler },
};

4. 用户层注册响应

void led_toggle_on_key(void)
{
    toggle_led(LED1);
}

void app_init(void)
{
    g_event.on_key_press = led_toggle_on_key;
}

架构总结:

  • ISR 中断最小化 → 设置标志
  • 主循环调度任务 → 调用 handler
  • handler 调用接口 → 应用注册回调
  • 回调可重载 → 用户实现逻辑

八、总结

构建可靠且可维护的中断响应机制,应重点遵循以下原则:

原则

描述

中断最小化

ISR 不做业务逻辑,只设标志

事件驱动

主循环轮询事件标志处理

任务调度

使用任务表统一管理各事件

接口解耦

抽象事件接口,分离实现

回调机制

应用注册响应函数,灵活扩展

弱函数重载

提供默认处理行为,支持用户重写

通过这些结构化设计手段,中断响应机制不再混乱,实现模块之间解耦、逻辑清晰、扩展灵活,适用于 STM32 等主流平台的中小型控制类项目。

原文链接:,转发请注明来源!