引言

在嵌入式实时系统中,中断服务程序(ISR)负责快速响应硬件事件,而任务则处理复杂的业务逻辑。两者之间的数据传递若处理不当,轻则数据丢失,重则系统崩溃。信号量(Semaphore)作为RTOS的核心同步机制,常被用于ISR向任务发送“数据就绪”信号。然而,看似简单的用法背后隐藏着诸多边界条件,如中断嵌套、信号量计数溢出、优先级反转等,稍有不慎便埋下隐患。本文以FreeRTOS + STM32为例,深入分析这些边界条件,并提供一套可靠的设计模式。

信号量基础与ISR专用API

信号量分为二值信号量和计数信号量。在ISR中,我们通常使用计数信号量来累积事件次数,或使用二值信号量作为事件标志。FreeRTOS为ISR提供了专用API,如xSemaphoreGiveFromISR,其原型如下:

BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);

关键点:pxHigherPriorityTaskWoken用于指示是否有更高优先级任务被唤醒,需在退出ISR时检查并执行上下文切换。

边界条件分析

1. 中断嵌套与优先级

当系统支持中断嵌套时,高优先级中断可能打断低优先级ISR。若低优先级ISR中调用xSemaphoreGiveFromISR,而高优先级ISR也操作同一信号量,则需确保信号量操作是原子性的。FreeRTOS内部通过临界区保护,但若中断优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY,则不能调用任何FreeRTOS API。因此,必须合理设置中断优先级,确保所有调用FreeRTOS API的中断优先级不高于该宏定义值。

  • 建议:将调用FreeRTOS API的中断优先级设置为低于configMAX_SYSCALL_INTERRUPT_PRIORITY
  • 对于非FreeRTOS管理的中断,避免直接操作信号量,可改用无锁环形缓冲区。

2. 信号量溢出

计数信号量有最大计数值限制。若ISR频繁触发,而任务处理不及时,信号量计数可能达到上限,导致后续xSemaphoreGiveFromISR失败(返回pdFALSE)。这会造成事件丢失。

  • 解决:使用队列代替信号量,队列可缓存数据,且长度可配置。
  • 若必须用信号量,需监控计数并动态调整任务优先级或处理速率。

3. 优先级反转

当任务等待信号量时,若一个低优先级任务持有该信号量(通过xSemaphoreTake),而高优先级任务被阻塞,则发生优先级反转。在ISR场景中,通常信号量只由ISR给出,任务获取,因此反转风险较低,但若任务间共享信号量,则需使用互斥量(Mutex)并启用优先级继承。

  • 注意:ISR中不能调用xSemaphoreTake,只能Give

4. 上下文切换时机

xSemaphoreGiveFromISR不会立即进行上下文切换,而是通过pxHigherPriorityTaskWoken标志延迟切换。若在ISR中多次调用,需在ISR末尾统一处理。

BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
// 其他操作
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

配置步骤(以STM32 + FreeRTOS为例)

  1. 在CubeMX中启用FreeRTOS,并创建计数信号量(如xSemaphoreCreateCounting(10, 0))。
  2. 配置中断优先级,确保ISR优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY(默认5)。
  3. 在ISR中调用xSemaphoreGiveFromISR,并传递pxHigherPriorityTaskWoken
  4. 在任务中调用xSemaphoreTake获取信号量,并处理数据。
  5. 在中断服务函数末尾调用portYIELD_FROM_ISR

完整代码示例

// 信号量句柄
SemaphoreHandle_t xDataSemaphore;

// 任务:数据处理
void vDataTask(void *pvParameters) {
    while(1) {
        if(xSemaphoreTake(xDataSemaphore, portMAX_DELAY) == pdTRUE) {
            // 从缓冲区读取数据并处理
            processData();
        }
    }
}

// UART中断服务程序
void USART1_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 检查中断标志
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        // 读取数据到缓冲区(例如环形缓冲区)
        uint8_t data = USART_ReceiveData(USART1);
        buffer_push(data);
        // 给出信号量
        xSemaphoreGiveFromISR(xDataSemaphore, &xHigherPriorityTaskWoken);
    }
    // 退出中断时切换上下文
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

int main(void) {
    // 初始化硬件
    // 创建信号量,最大计数10,初始0
    xDataSemaphore = xSemaphoreCreateCounting(10, 0);
    // 创建任务
    xTaskCreate(vDataTask, "DataTask", 128, NULL, 2, NULL);
    // 启动调度器
    vTaskStartScheduler();
    while(1);
}

注意事项

  • 确保信号量创建成功,否则系统崩溃。
  • 在ISR中避免使用阻塞操作,如vTaskDelay
  • 若数据量较大,建议使用队列(xQueueSendFromISR),队列可缓存数据,且支持多元素。
  • 监控信号量计数,若频繁溢出,需优化任务处理速度或增大计数上限。
  • 对于时间敏感型应用,考虑使用任务通知(Task Notification)替代信号量,其开销更低。

总结

基于信号量的ISR到任务数据流传递是RTOS应用中的经典模式,但边界条件不容忽视。通过合理配置中断优先级、选择合适同步原语、处理信号量溢出,并正确使用上下文切换机制,可以构建高效且健壮的嵌入式系统。在实际开发中,建议结合具体场景进行压力测试,验证极端情况下的行为。