在实时操作系统(RTOS)中,信号量(Semaphore) 是一种关键的同步和互斥机制,用于协调多任务对共享资源的访问或实现任务间的同步。以下是信号量的核心用法和分类:


1. 信号量的类型

(1)二进制信号量(Binary Semaphore)
  • 作用:实现任务间的简单同步或互斥访问(如共享资源保护)。

  • 特性

    • 只有 0(不可用)或 1(可用)两种状态。

    • 常用于任务与任务(T2T)或任务与中断(ISR)间的同步。

(2)计数信号量(Counting Semaphore)
  • 作用:管理多个同类资源(如缓冲池、设备实例)。

  • 特性

    • 初始值为资源的总数。

    • 任务每次获取信号量(take)时,计数器减 1;释放(give)时加 1。

    • 计数器为 0 时,任务需等待资源释放。

(3)互斥信号量(Mutex,互斥锁)
  • 作用:解决优先级反转问题,保护共享资源。

  • 特性

    • 支持优先级继承(Priority Inheritance):低优先级任务持有锁时,会临时提升其优先级。

    • 只能由获取它的任务释放(所有权机制)。


      2. 信号量的核心操作

      (1)创建信号量
    • 不同 RTOS 的 API 不同,例如:

      • FreeRTOSxSemaphoreCreateBinary()xSemaphoreCreateMutex()

      • μC/OSOSSemCreate()OSMutexCreate()

    • (2)获取信号量(Take/Pend)
    • 任务尝试获取信号量:

      // FreeRTOS 示例(阻塞式获取,超时时间设为 portMAX_DELAY 表示无限等待) xSemaphoreTake(xSemaphoreHandle, pdMS_TO_TICKS(100)); // 等待最多 100ms

    • 若信号量不可用,任务可能挂起(进入阻塞态)或直接返回错误。

    • (3)释放信号量(Give/Post)
    • 释放信号量以唤醒等待的任务或恢复资源计数:

      // FreeRTOS 示例
      xSemaphoreGive(xSemaphoreHandle);

    • 中断服务程序(ISR) 中需使用特殊 API(如 xSemaphoreGiveFromISR())。


    • 3. 典型应用场景

      (1)任务同步
    • 示例:中断服务程序(ISR)触发事件后,释放信号量通知任务处理。

      // ISR 中释放信号量
      void ADC_ISR() {
        xSemaphoreGiveFromISR(adcReadySemaphore, pdFALSE);
      }

      // 任务中等待信号量
      void Task_ProcessADC() {
        while (1) {
          xSemaphoreTake(adcReadySemaphore, portMAX_DELAY);
          // 处理 ADC 数据
        }
      }

    • (2)资源互斥访问
    • 示例:多个任务共享 SPI 总线时,通过互斥信号量确保独占访问。

      // 任务 A 使用 SPI
      xSemaphoreTake(spiMutex, portMAX_DELAY);
      spi_send(data);
      xSemaphoreGive(spiMutex);

      // 任务 B 同样需先获取 Mutex

    • (3)流量控制
    • 示例:使用计数信号量限制同时访问某资源的最大任务数(如缓冲池)。

      // 初始化缓冲池有 5 个可用缓冲区
      xCountingSemaphore = xSemaphoreCreateCounting(5, 5);

      // 任务申请缓冲区
      if (xSemaphoreTake(xCountingSemaphore, 100) == pdTRUE) {
        // 获取成功,使用缓冲区
      }


    • 4. 注意事项

    • 优先级反转问题

      • 使用 互斥信号量(Mutex) 而非二进制信号量保护共享资源,避免低优先级任务阻塞高优先级任务。

    • 死锁(Deadlock)

      • 避免任务同时持有多个互斥锁,或确保所有任务按相同顺序获取锁。

    • 中断中的使用

      • 在 ISR 中只能使用非阻塞式信号量操作(如 xSemaphoreGiveFromISR())。

在实时操作系统(RTOS)中,互斥量(Mutex,Mutual Exclusion) 是一种特殊的信号量,专门用于解决多任务访问共享资源时的冲突和优先级反转问题。以下是互斥量的核心用法及关键细节:


1. 互斥量的核心特性

  • 所有权机制

    • 互斥量只能由获取它的任务释放,确保资源访问的“锁”由持有者管理。

    • 其他任务无法释放不属于自己的互斥量。

  • 优先级继承(Priority Inheritance)

    • 当低优先级任务持有互斥量时,若高优先级任务尝试获取该互斥量,系统会临时提升低优先级任务的优先级,使其尽快释放资源,避免高优先级任务被长期阻塞(解决优先级反转问题)。

  • 不可在中断中使用

    • 互斥量的获取和释放操作不能在中断服务程序(ISR) 中执行,因其所有权机制与中断的异步特性冲突。


2. 何时使用互斥量?

  • 共享资源的互斥访问

    • 保护全局变量、硬件外设(如SPI、I2C总线)、内存池等共享资源,确保同一时间仅一个任务可访问。

  • 避免优先级反转

    • 当高、低优先级任务可能竞争同一资源时,互斥量的优先级继承机制能有效减少高优先级任务的阻塞时间。


3. 互斥量的操作(以FreeRTOS为例)

(1)创建互斥量

SemaphoreHandle_t xMutex = xSemaphoreCreateMutex(); // 创建互斥量

(2)获取互斥量(加锁)

if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
    // 成功获取互斥量,访问共享资源
} else {
    // 超时未获取,处理错误
}

  • 参数说明

    • xMutex:互斥量句柄。

    • 100:超时时间(单位:毫秒),设为portMAX_DELAY表示无限等待。

(3)释放互斥量(解锁)

xSemaphoreGive(xMutex); // 必须由获取互斥量的任务释放


4. 典型应用场景

场景1:保护硬件外设(如UART发送)

SemaphoreHandle_t uartMutex = xSemaphoreCreateMutex();

// 任务A发送数据
void TaskA(void *pvParameters) {
    while (1) {
        xSemaphoreTake(uartMutex, portMAX_DELAY);
        uart_send("Hello from TaskA");
        xSemaphoreGive(uartMutex);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 任务B发送数据
void TaskB(void *pvParameters) {
    while (1) {
        xSemaphoreTake(uartMutex, portMAX_DELAY);
        uart_send("Hello from TaskB");
        xSemaphoreGive(uartMutex);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

  • 效果:确保任务A和任务B的UART发送操作不会互相干扰。

场景2:防止优先级反转
  • 问题:低优先级任务(L)持有资源 → 中优先级任务(M)抢占CPU → 高优先级任务(H)等待资源被阻塞。

  • 解决:当H尝试获取L持有的互斥量时,L的优先级被临时提升至H的优先级,使其尽快释放资源,避免M任务抢占导致H长期阻塞。


5. 注意事项

  1. 避免死锁(Deadlock)

    • 若任务需获取多个互斥量,应按固定顺序获取,防止交叉依赖导致死锁。

    • 示例错误:

      // 任务1:先获取A,再获取B
      // 任务2:先获取B,再获取A → 可能导致互相等待

  2. 严禁在中断中操作互斥量

    • 中断服务程序(ISR)应使用二进制信号量队列进行同步。

  3. 及时释放互斥量

    • 获取互斥量后,必须尽快释放,避免其他任务长期阻塞。

  4. 避免递归获取

    • 若同一任务多次获取同一互斥量,需确保释放次数与获取次数一致(某些RTOS支持递归互斥量)。


6. 互斥量 vs 二进制信号量

特性 互斥量(Mutex) 二进制信号量(Binary Semaphore)
所有权 必须由获取者释放 任何任务或ISR均可释放
优先级继承 支持(避免优先级反转) 不支持
适用场景 保护共享资源 任务同步、简单事件通知
初始状态 通常初始为可用(1) 可初始为0或1(取决于同步需求)

总结

互斥量是RTOS中解决资源竞争和优先级反转的核心工具,适用于:

  • 需要严格互斥访问的共享资源(如硬件外设、全局变量)。

  • 多优先级任务环境,需避免低优先级任务阻塞高优先级任务。

关键原则

  • 谁获取,谁释放:确保任务逻辑清晰,避免错误释放。

  • 短持有时间:尽量减少互斥量的持有时间,提升系统实时性。

  • 优先级继承:合理设计任务优先级,充分发挥互斥量的优势。

在实时操作系统(RTOS)中,信号(Signal) 通常指一种轻量级的任务间通信机制,用于通知任务特定事件的发生。不过,不同 RTOS 对“信号”的定义和实现可能有所差异。例如,某些 RTOS 中直接称为“信号”,而另一些(如 FreeRTOS)则使用 任务通知(Task Notifications) 或 事件标志组(Event Groups) 实现类似功能。以下详细解释其核心用法和典型场景:


1. RTOS 中“信号”的常见实现形式

(1)任务通知(Task Notifications)
  • 特性

    • 每个任务拥有一个独立的“通知值”(32 位数值),可用于传递事件或简单数据。

    • 轻量高效,无需额外内存分配(对比信号量、队列等)。

    • 支持多种操作模式:覆盖、递增、按位操作等。

  • 适用场景

    • 替代二进制信号量或事件标志,实现任务同步。

    • 传递简单数据(如状态码)。

(2)事件标志组(Event Groups)
  • 特性

    • 通过位掩码(bitmask)表示多个事件的状态。

    • 任务可以等待单个或多个事件(位的组合)。

    • 支持事件间的逻辑“与”(AND)或“或”(OR)触发条件。

  • 适用场景

    • 多事件同步(例如,等待“网络连接成功”且“数据接收完成”)。

(3)传统信号机制(如 POSIX 信号)
  • 注意

    • 在嵌入式 RTOS 中较少直接支持 POSIX 信号,因其异步中断模型复杂且资源消耗较大。

    • 通常用更轻量的机制(如任务通知)替代。


2. 核心操作与示例(以 FreeRTOS 任务通知为例)

(1)发送信号(通知任务)

// 向指定任务发送通知,覆盖其原有通知值
xTaskNotify(taskHandle, value, eSetValueWithOverwrite);

// 向指定任务发送通知,按位或(OR)操作更新通知值
xTaskNotify(taskHandle, bitmask, eSetBits);

// 在中断中发送通知(需使用 FromISR 版本)
xTaskNotifyFromISR(taskHandle, value, eSetValueWithOverwrite, pdFALSE);

(2)等待信号(接收通知)

uint32_t receivedValue = 0;

// 阻塞等待通知,无限期等待(返回 pdTRUE 表示成功)
xTaskNotifyWait(0x00, 0xFFFFFFFF, &receivedValue, portMAX_DELAY);

// 非阻塞检查通知(返回 pdTRUE 表示有通知)
xTaskNotifyWait(0x00, 0xFFFFFFFF, &receivedValue, 0);

(3)清除信号状态

// 清除任务通知值的特定位
xTaskNotifyAndClear(taskHandle, bitsToClear);


3. 典型应用场景

场景1:替代二进制信号量(任务同步)

// 任务 A 等待任务 B 完成某项操作
void TaskA(void *pvParam) {
    while (1) {
        // 等待任务 B 的通知
        xTaskNotifyWait(0, 0xFFFFFFFF, NULL, portMAX_DELAY);
        // 执行后续操作
    }
}

void TaskB(void *pvParam) {
    // 完成操作后通知任务 A
    xTaskNotifyGive(taskA_handle);
}

场景2:传递简单状态信息

// 发送方向任务传递错误码
#define ERROR_TIMEOUT  (1 << 0)
#define ERROR_CRC      (1 << 1)

void SenderTask(void *pvParam) {
    if (operation_failed) {
        xTaskNotify(receiverHandle, ERROR_TIMEOUT | ERROR_CRC, eSetBits);
    }
}

// 接收任务处理多错误标志
void ReceiverTask(void *pvParam) {
    uint32_t errorFlags;
    xTaskNotifyWait(0, 0xFFFFFFFF, &errorFlags, portMAX_DELAY);
    if (errorFlags & ERROR_TIMEOUT) { /* 处理超时 */ }
    if (errorFlags & ERROR_CRC)     { /* 处理 CRC 错误 */ }
}

场景3:多事件同步(事件标志组)

EventGroupHandle_t eventGroup = xEventGroupCreate();

// 任务等待网络连接成功(bit 0)且数据接收完成(bit 1)
void NetworkTask(void *pvParam) {
    EventBits_t bits = xEventGroupWaitBits(
        eventGroup,
        (BIT_0 | BIT_1),  // 等待的位
        pdTRUE,            // 等待成功后清除这些位
        pdTRUE,            // 需要所有位同时置位(AND 条件)
        portMAX_DELAY
    );
    // 执行后续操作
}

// 其他任务或中断设置事件位
void DataReceivedISR() {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xEventGroupSetBitsFromISR(eventGroup, BIT_1, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}


4. 信号 vs 信号量的区别

特性 信号(任务通知/事件标志) 信号量(Semaphore)
资源占用 更轻量(无需额外对象) 需显式创建信号量对象
灵活性 可传递数值或位掩码 仅计数或二进制状态
所有权 无所有权概念 互斥信号量有所有权机制
适用场景 任务间轻量通知、多事件同步 资源计数、严格互斥访问

5. 注意事项

  1. 避免滥用

    • 任务通知虽然高效,但一次只能有一个任务等待通知,复杂场景需结合其他机制(如队列)。

  2. 中断安全性

    • 在中断服务程序(ISR)中需使用 FromISR 版本 API,并处理上下文切换(如 portYIELD_FROM_ISR())。

  3. 事件标志组的位管理

    • 合理规划事件位的分配,避免冲突(例如使用宏定义位掩码)。

  4. 清除机制

    • 及时清除已处理的事件位,防止重复触发。

在实时操作系统(RTOS)中,队列(Queue) 是一种核心的进程间通信(IPC)机制,用于在任务之间或任务与中断服务程序(ISR)之间安全传递数据。

1. 队列的核心特性

  • 先进先出(FIFO):数据按发送顺序被接收(除非使用优先级插入)。

  • 数据复制机制:队列存储数据的副本而非指针,避免数据竞争。

  • 阻塞与非阻塞操作

    • 发送时若队列满,任务可选择等待(阻塞)或立即返回。

    • 接收时若队列空,任务可等待数据到达或直接退出。

  • 支持多任务和中断

    • 多个任务可同时向队列发送或接收数据。

    • ISR 中需使用特殊 API(如 xQueueSendFromISR())。


2. 队列的操作(以 FreeRTOS 为例)

(1)创建队列

// 创建队列:可存储 10 个元素,每个元素为 4 字节(例如 int 类型)
QueueHandle_t xQueue = xQueueCreate(10, sizeof(int));

  • 参数

    • 10:队列容量(最大可存储的元素数量)。

    • sizeof(int):单个元素的大小(支持任意数据类型,包括结构体)。

(2)发送数据到队列

int data = 42;

// 阻塞式发送(等待直到队列有空间或超时)
xQueueSend(xQueue, &data, pdMS_TO_TICKS(100));  // 等待最多 100ms

// 非阻塞发送(若队列满,立即返回错误)
xQueueSendToBack(xQueue, &data, 0);  // 插入队尾(等同于 xQueueSend)
xQueueSendToFront(xQueue, &data, 0); // 插入队首(优先被接收)

// 中断中发送(使用 FromISR 版本)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 触发上下文切换(若需要)

(3)从队列接收数据

int received_data;

// 阻塞式接收(等待直到队列有数据或超时)
xQueueReceive(xQueue, &received_data, portMAX_DELAY); // 无限等待

// 非阻塞接收(若队列空,立即返回错误)
xQueueReceive(xQueue, &received_data, 0); 

// 查看队首数据但不移除(Peek)
xQueuePeek(xQueue, &received_data, 0); 


3. 典型应用场景

场景1:生产者-消费者模型

// 生产者任务(生成数据并发送到队列)
void ProducerTask(void *pvParam) {
    int sensor_value;
    while (1) {
        sensor_value = read_sensor();
        xQueueSend(xQueue, &sensor_value, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 消费者任务(从队列接收并处理数据)
void ConsumerTask(void *pvParam) {
    int data;
    while (1) {
        if (xQueueReceive(xQueue, &data, portMAX_DELAY)) {
            process_data(data);
        }
    }
}

场景2:中断与任务的数据传递

// 中断服务程序(ADC 转换完成时发送数据)
void ADC_ISR() {
    static uint16_t adc_value;
    adc_value = read_adc();
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(xQueue, &adc_value, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 若需要切换任务
}

// 任务处理 ADC 数据
void ProcessADCTask(void *pvParam) {
    uint16_t value;
    while (1) {
        xQueueReceive(xQueue, &value, portMAX_DELAY);
        save_to_buffer(value);
    }
}

场景3:多任务间的消息传递

// 定义消息结构体
typedef struct {
    uint8_t cmd;
    uint32_t param;
} Message_t;

// 创建队列(存储 5 条消息)
QueueHandle_t msgQueue = xQueueCreate(5, sizeof(Message_t));

// 发送任务
void CommandTask(void *pvParam) {
    Message_t msg = { .cmd = 0x01, .param = 100 };
    xQueueSend(msgQueue, &msg, 0);
}

// 接收任务
void HandlerTask(void *pvParam) {
    Message_t msg;
    while (1) {
        if (xQueueReceive(msgQueue, &msg, portMAX_DELAY)) {
            execute_command(msg.cmd, msg.param);
        }
    }
}


4. 队列 vs 其他通信机制

机制 队列 任务通知 信号量
数据传输 支持任意类型数据(结构体、数组等) 仅传递 32 位数值或位掩码 无数据传递,仅计数/同步
多接收者 支持多个任务接收同一队列 仅单任务可等待通知 信号量无“接收者”概念
资源开销 较高(需预分配内存) 最低(无需额外对象) 中等
适用场景 结构化数据传递、流式数据传输 轻量级通知、状态更新 资源计数、互斥访问

5. 注意事项

  1. 队列深度与数据大小

    • 队列长度和数据类型的乘积决定内存占用(例如 10 元素 × 100 字节 = 1KB)。

    • 避免传递大型数据(如数组),改用指针传递(需自行管理数据生命周期)。

  2. 超时设置

    • 合理设置阻塞时间(如 portMAX_DELAY 或具体毫秒数),避免任务永久挂起。

  3. 中断安全

    • 在 ISR 中必须使用 FromISR 结尾的 API,且不可阻塞。

  4. 优先级反转

    • 若多个高优先级任务等待同一队列,可能因竞争导致低优先级任务长期阻塞,需合理设计任务优先级。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐