引言

在 RTOS 驱动的嵌入式系统中,I2C 总线常被多个任务共享(如传感器读取、EEPROM 写入)。当低优先级任务持有 I2C 总线时,高优先级任务因等待总线而被阻塞,若中优先级任务不断抢占 CPU,低优先级任务无法释放总线,高优先级任务将无限期等待——这就是经典的优先级反转。本文通过实测 STM32F407 + FreeRTOS 场景,揭示问题本质,并给出可落地的解决方案。

1. 优先级反转原理与 I2C 场景特殊性

1.1 经典优先级反转模型

  • 任务 A(高优先级,如 3)请求 I2C 总线,但任务 C(低优先级,如 1)已持有总线。
  • 任务 B(中优先级,如 2)就绪后抢占 C,导致 C 无法完成 I2C 传输。
  • A 等待 C 释放总线,但 C 被 B 阻塞,A 的实时性被破坏。

1.2 I2C 仲裁的独特挑战

  • I2C 是半双工同步协议,总线操作必须原子完成(起始位、地址、数据、停止位)。
  • 若在传输中途被任务切换打断,总线状态机可能错乱,导致 ACK 失败或总线挂死。
  • 因此,I2C 驱动通常用临界区或互斥锁保护,但这加剧了优先级反转。

2. 实测环境与复现实验

2.1 硬件与软件配置

  • MCU:STM32F407VET6(168MHz)
  • RTOS:FreeRTOS 10.4.6
  • I2C1:400kHz,外接 BMP280 传感器(读操作 20ms)
  • 三个任务:
    • Task_High:优先级 3,每 100ms 读一次传感器(模拟高实时需求)
    • Task_Mid:优先级 2,纯计算任务(持续 50ms)
    • Task_Low:优先级 1,每 200ms 写一次 EEPROM(I2C 操作 30ms)

2.2 复现代码(无保护)

// i2c_shared.c - 无保护版本
void I2C_ReadBMP280(uint8_t reg, uint8_t *data) {
    // 直接操作 I2C 寄存器,无互斥
    I2C_Start();
    I2C_WriteAddr(0x76 << 1);
    I2C_WriteByte(reg);
    I2C_RepeatedStart();
    I2C_ReadBytes(data, 2);
    I2C_Stop();
}

void Task_Low(void *arg) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(200));
        I2C_WriteEEPROM(0x50, 0x00, 0xAA); // 30ms 操作
    }
}

void Task_High(void *arg) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(100));
        uint8_t data[2];
        I2C_ReadBMP280(0xFA, data); // 20ms 操作
        // 处理数据...
    }
}

void Task_Mid(void *arg) {
    while(1) {
        // 纯计算,无 I2C
        for(volatile int i=0; i<1000000; i++);
        vTaskDelay(10);
    }
}

2.3 实测结果

  • 使用逻辑分析仪抓取 I2C 总线占用时间,记录 Task_High 的响应延迟。
  • 无保护时,Task_High 最大延迟达 85ms(远超理论 20ms),且出现 3 次总线错误(ACK 失败)。
  • 原因:Task_Low 持有总线时被 Task_Mid 抢占,总线操作被撕裂,Task_High 等待时间累积。

3. 解法一:互斥锁 + 优先级继承

3.1 原理

  • 使用 FreeRTOS 互斥量(Mutex)保护 I2C 操作。互斥量内置优先级继承机制:当高优先级任务等待互斥量时,持有者的优先级临时提升到等待者的优先级,从而避免中优先级任务抢占。

3.2 配置步骤

  1. 创建互斥量:xSemaphoreCreateMutex()
  2. 在 I2C 操作前后获取/释放:xSemaphoreTake / xSemaphoreGive
  3. 注意:互斥量必须在任务中使用,不能在中断中。

3.3 代码示例

SemaphoreHandle_t i2c_mutex;

void I2C_Init_Mutex(void) {
    i2c_mutex = xSemaphoreCreateMutex();
}

void I2C_ReadBMP280_Mutex(uint8_t reg, uint8_t *data) {
    xSemaphoreTake(i2c_mutex, portMAX_DELAY);
    // 原子 I2C 操作
    I2C_Start();
    I2C_WriteAddr(0x76 << 1);
    I2C_WriteByte(reg);
    I2C_RepeatedStart();
    I2C_ReadBytes(data, 2);
    I2C_Stop();
    xSemaphoreGive(i2c_mutex);
}

3.4 实测效果

  • Task_High 最大延迟降至 22ms(接近理论值),无总线错误。
  • 优先级继承有效,但注意:如果中优先级任务长时间运行,继承优先级可能影响其他低优先级任务,需权衡。

4. 解法二:中断代理(ISR + 消息队列)

4.1 原理

  • 将 I2C 操作放入中断上下文,利用中断优先级高于任何任务,避免任务级抢占。
  • 任务通过消息队列发送请求,ISR 执行 I2C 传输,完成后通过信号量通知任务。

4.2 配置步骤

  1. 配置 I2C 事件中断(TXE、RXNE、STOP)
  2. 创建消息队列(存储请求)和信号量(完成标志)
  3. 任务发送请求,等待信号量;ISR 处理传输

4.3 代码示例(简化)

QueueHandle_t i2c_req_queue;
SemaphoreHandle_t i2c_done_sem;

void I2C_ISR(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 处理 I2C 事件...
    if (transfer_complete) {
        xSemaphoreGiveFromISR(i2c_done_sem, &xHigherPriorityTaskWoken);
    }
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void Task_High_ISR(void *arg) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(100));
        uint8_t data[2];
        I2C_Request req = {0x76, 0xFA, data};
        xQueueSend(i2c_req_queue, &req, 0);
        xSemaphoreTake(i2c_done_sem, pdMS_TO_TICKS(50));
        // 处理数据
    }
}

4.4 实测效果

  • Task_High 延迟稳定在 20ms 左右,且不受中优先级任务影响。
  • 缺点:ISR 中不能阻塞,需精心设计状态机;代码复杂度高。

5. 解法三:优先级天花板(Priority Ceiling)

5.1 原理

  • 设置互斥量的天花板优先级为所有可能使用它的任务中的最高优先级。
  • 当任务获取互斥量时,其优先级立即提升到天花板,防止任何中优先级任务抢占。
  • FreeRTOS 中可通过 xSemaphoreCreateMutex 后调用 vSemaphoreSetPriorityCeiling(需开启 configUSE_PRIORITY_CEILING)。

5.2 配置步骤

  1. 在 FreeRTOSConfig.h 中定义 configUSE_PRIORITY_CEILING 1
  2. 创建互斥量后设置天花板:vSemaphoreSetPriorityCeiling(i2c_mutex, 3)(最高任务优先级)

5.3 代码示例

// 初始化时
SemaphoreHandle_t i2c_mutex = xSemaphoreCreateMutex();
vSemaphoreSetPriorityCeiling(i2c_mutex, 3); // 天花板为最高优先级

// 使用同互斥锁版本,无需修改

5.4 实测效果

  • 与优先级继承类似,但更简单,延迟约 21ms。
  • 注意:天花板优先级过高可能导致低优先级任务饥饿,需根据实际任务集调整。

6. 对比与选型建议

| 方法 | 延迟改善 | 复杂度 | 适用场景 | |------|----------|--------|----------| | 无保护 | 85ms | 低 | 不推荐 | | 互斥锁+继承 | 22ms | 中 | 通用,推荐首选 | | 中断代理 | 20ms | 高 | 高实时、复杂协议 | | 优先级天花板 | 21ms | 低 | 任务优先级固定时 |

  • 对于大多数 I2C 应用,互斥锁 + 优先级继承足够,且易维护。
  • 若 I2C 操作频繁且要求极低延迟,可考虑中断代理。
  • 优先级天花板适合任务优先级静态确定的系统。

7. 注意事项

  • 互斥锁中禁止调用阻塞 API(如 vTaskDelay),否则死锁。
  • I2C 操作必须完整放在临界区或互斥锁内,避免总线状态撕裂。
  • 使用中断代理时,确保 ISR 中不调用非 ISR 安全函数。
  • 实测时用逻辑分析仪或 GPIO 翻转测量延迟,避免理论误差。
  • 在低功耗系统中,优先级继承可能阻止低功耗模式,需评估。

结语

优先级反转在 I2C 总线场景中会严重破坏实时性,甚至导致总线错误。通过互斥锁 + 优先级继承、中断代理或优先级天花板,可以有效解决。实测表明,互斥锁方案在复杂度和性能间取得最佳平衡。开发者应根据任务模型和实时需求,选择最合适的解法,并在设计阶段就考虑共享资源的访问策略。