一、信号量(Semaphore)

信号量本质上是基于队列实现的特殊结构,是一种轻量级的同步与资源管理机制。

1.特点

无数据传递:仅通过计数值(uxMessagesWaiting)表示资源状态,不存储实际数据。

操作简化:通过 xSemaphoreGive()(释放)和 xSemaphoreTake()(获取)操作计数值,实现任务同步或资源管理。

2.操作

Give(释放):计数值递增,唤醒等待任务。
Take(获取):计数值递减,若为 0 则阻塞任务。

3.变体类型

(1)二进制信号量

计数值为 0 或 1

  • 中断服务例程(ISR)通知任务:硬件中断触发后,释放信号量唤醒处理任务。
  • 单向同步:任务 A 完成后通知任务 B 开始执行。

(2)计数信号量

计数值可大于 1

  • 资源池管理:管理 N 个缓冲区的使用情况(如 Take 获取缓冲区,Give 释放)。
  • 限流控制:限制同时访问某资源的任务数。

(3)互斥信号量

具有优先级继承特性,低优先级任务持有互斥量时,临时提升其优先级,解决优先级反转问题。

  • 保护共享资源:如全局变量、外设(确保单任务独占访问)。
  • 临界区保护:防止多任务同时修改关键数据结构。

二、信号量与队列

在 FreeRTOS 中,信号量和队列的结构设计差异源于它们的核心功能目标不同。

队列用于任务间传输实际数据(如消息、传感器数据等)。
信号量用于同步或资源计数(无数据传输,仅通过计数值操作)。

虽然信号量在底层基于队列实现,但通过特殊化配置(如队列项大小为 0)和功能裁剪,二者的结构存在显著差异。

1.队列的结构

typedef struct QueueDefinition {
    int8_t *pcHead;             // 队列存储区的起始地址
    int8_t *pcTail;             // 队列存储区的结束地址(辅助计算)
    int8_t *pcWriteTo;          // 下一个写入位置(队尾)
    int8_t *pcReadFrom;         // 下一个读取位置(队头)

    UBaseType_t uxMessagesWaiting; // 当前队列中的消息数量
    UBaseType_t uxLength;          // 队列最大容量(总项数)
    UBaseType_t uxItemSize;        // 每个队列项的大小(字节)

    // 阻塞任务列表:队列满时等待发送的任务 / 队列空时等待接收的任务
    List_t xTasksWaitingToSend;   
    List_t xTasksWaitingToReceive;
} Queue_t;
  1. 数据缓冲区:通过 pcHead 和 pcTail 管理一块连续内存,存储实际传输的数据。
  2. 数据操作:通过 pcWriteTo 和 pcReadFrom 实现环形缓冲区的读写。
  3. 双阻塞列表:分离管理因队列满或空而阻塞的任务(发送者和接收者)。

2.信号量的结构

// 信号量复用 Queue_t 结构体,但以下字段行为特殊化:
typedef struct QueueDefinition Semaphore_t; 


xQueueCreateCountingSemaphore(uxMaxCount, uxInitialCount, 0 /* uxItemSize */);
// 创建信号量时,队列项大小被设为 0(无数据存储)
//队列的 uxLength 字段被解释为信号量的最大计数值(uxMaxCount)
//队列的 uxMessagesWaiting 字段被用作信号量的当前计数值(uxInitialCount)

  1. 无数据存储:队列项大小 uxItemSize 固定为 0,pcHead 和 pcTail 无实际意义。
  2. 仅操作计数值:通过 uxMessagesWaiting 字段表示信号量的计数值。
  3. 单阻塞列表:仅保留 xTasksWaitingToReceive(等待获取信号量的任务阻塞),释放信号量无需等待,本质为唤醒任务。

三、信号量应用实例

1. 二进制信号量:中断与任务同步

当硬件外设(如按键)触发中断时,需要通过信号量通知任务处理事件。二进制信号量适合此类单向事件通知,确保中断服务程序(ISR)快速释放信号量,任务异步响应。

// 定义信号量句柄
SemaphoreHandle_t xButtonSemaphore = NULL;

// 按键中断服务程序(ISR)
void Button_ISR() {
	//用于标记是否有更高优先级的任务因信号量释放而被唤醒
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 释放二进制信号量,若唤醒的任务优先级高于当前任务的优先级,则xHigherPriorityTaskWoken会被自动设为 pdTRUE
    xSemaphoreGiveFromISR(xButtonSemaphore, &xHigherPriorityTaskWoken);
    // 根据 xHigherPriorityTaskWoken 的值决定是否触发任务切换,若为pdTRUE则立即触发上下文切换,让更高优先级的任务抢占执行
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken); 
}

// 任务:处理按键事件
void ButtonTask(void *pvParameters) {
    while (1) {
        // 等待信号量(阻塞直到中断释放)
        if (xSemaphoreTake(xButtonSemaphore, portMAX_DELAY) == pdTRUE) {
            printf("按键按下,执行处理逻辑(如去抖动、状态切换)\n");
        }
    }
}

// 初始化
void main() {
    xButtonSemaphore = xSemaphoreCreateBinary(); // 创建二进制信号量
    xTaskCreate(ButtonTask, "ButtonTask", 128, NULL, 2, NULL);
    // 注册按键中断(硬件相关代码略)
    vTaskStartScheduler();
}

2. 计数信号量:资源池管理

管理一个包含 3 个缓冲区的资源池,任务需要申请缓冲区进行数据存储,使用完毕后释放。计数信号量的计数值表示当前可用缓冲区数量。

SemaphoreHandle_t xBufferSemaphore = NULL;
uint8_t bufferPool[3][64]; // 3个缓冲区,每个64字节

// 任务:申请缓冲区并写入数据
void DataTask(void *pvParameters) {
    while (1) {
        // 尝试获取缓冲区(等待100ms)
        if (xSemaphoreTake(xBufferSemaphore, pdMS_TO_TICKS(100)) {
            //获取当前信号量的计数值。这里用该计数值作为索引,从bufferPool中选择一个缓冲区
            uint8_t *buffer = bufferPool[uxSemaphoreGetCount(xBufferSemaphore)];
            //sprintf:把格式化字符串 “Data from task % d” 和任务参数pvParameters的值写入所选的缓冲区。
            sprintf(buffer, "Data from task %d", (int)pvParameters);
            //printf:将写入缓冲区的数据打印输出。
            printf("写入数据: %s\n", buffer);
             // 模拟处理时间
            vTaskDelay(pdMS_TO_TICKS(500));
            // 释放缓冲区
            xSemaphoreGive(xBufferSemaphore); 
        } else {
            printf("获取缓冲区超时!\n");
        }
    }
}

// 初始化
void main() {
    // 初始计数值为2,说明仅有2个资源可用,另外一个需要显式释放:xSemaphoreGive()
    xBufferSemaphore = xSemaphoreCreateCounting(3, 2);
    xTaskCreate(DataTask, "DataTask1", 128, (void*)1, 2, NULL);
    xTaskCreate(DataTask, "DataTask2", 128, (void*)2, 2, NULL);
    vTaskStartScheduler();
}

3.互斥信号量:保护共享资源

多个任务需要访问共享的 SPI 总线,为避免数据冲突,使用互斥信号量确保同一时间只有一个任务独占总线。

SemaphoreHandle_t xSPIMutex = NULL;

// 任务:通过SPI发送数据
void SPITask(void *pvParameters) {
    while (1) {
        // 获取互斥量(阻塞等待)
        if (xSemaphoreTake(xSPIMutex, portMAX_DELAY) == pdTRUE) {
            printf("任务%d占用SPI总线...\n", (int)pvParameters);
            SPI_SendData(...);  // 实际SPI操作(假设为非阻塞)
            vTaskDelay(pdMS_TO_TICKS(10)); // 模拟SPI传输时间
            xSemaphoreGive(xSPIMutex);      // 释放互斥量
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}


// 初始化
void main() {
    xSPIMutex = xSemaphoreCreateMutex(); // 创建互斥信号量
    xTaskCreate(SPITask, "SPITask1", 128, (void*)1, 3, NULL);
    xTaskCreate(SPITask, "SPITask2", 128, (void*)2, 3, NULL);
    vTaskStartScheduler();
}

四、优先级翻转(Priority Inversion)

1.优先级翻转

优先级翻转是指高优先级任务因等待低优先级任务持有的共享资源而被阻塞,而低优先级任务又被中等优先级任务抢占的现象。此时,高优先级任务的执行被间接“翻转”到低优先级任务的优先级之下。

典型场景:

任务优先级:任务 H(高) > 任务 M(中) > 任务 L(低)。

流程:

  1. 任务 L 获取共享资源(如互斥锁),开始执行。
  2. 任务 H 就绪,尝试获取该资源时被阻塞。
  3. 任务 M 就绪(优先级高于 L),抢占任务 L 的 CPU 使用权。
  4. 任务 L 因被M 抢占,无法释放资源,导致任务 H 无限期等待。

结果:
高优先级任务 H 的执行被低优先级任务 L 和中等优先级任务 M 的组合行为无限延迟。

2.互斥量优先级继承(Priority Inheritance)

FreeRTOS 中的互斥量通过优先级继承(Priority Inheritance) 机制解决优先级翻转。

工作原理:

  1. 优先级继承触发:

    当高优先级任务(H)尝试获取已被低优先级任务(L)持有的互斥量时,系统会临时提升 L 的优先级至与 H 相同。

    例如,若 H 的优先级为 3,L 的优先级为 1,则 L 的优先级会被提升到 3。

  2. 关键路径执行:

    提升优先级后,任务 L 不会被中等优先级任务(M)抢占,从而能够快速释放互斥量。

    任务 L 释放互斥量后,其优先级恢复为原始值(1)。

  3. 高优先级任务恢复:

    任务 H 立即获取互斥量并继续执行,避免被阻塞。

优势:

低优先级任务在持有资源时“暂时继承”高优先级,避免被中等优先级任务抢占。

缩短高优先级任务的等待时间。

3.应用实例

// 定义互斥量
SemaphoreHandle_t xMutex;

void Task_Sensor(void *pvParameters) {
    while (1) {
        // 获取互斥量(阻塞等待)
        if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            // 写入传感器数据到共享缓冲区
            xSemaphoreGive(xMutex); // 释放互斥量
        }
        vTaskDelay(100 / portTICK_PERIOD_MS);
    }
}

void Task_Display(void *pvParameters) {
    while (1) {
        // 获取互斥量(阻塞等待)
        if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            // 从共享缓冲区读取数据并显示
            xSemaphoreGive(xMutex); // 释放互斥量
        }
        vTaskDelay(200 / portTICK_PERIOD_MS);
    }
}

// 主函数中初始化互斥量并创建任务
int main() {
    xMutex = xSemaphoreCreateMutex(); // 创建互斥量
    xTaskCreate(Task_Sensor, "Sensor", 128, NULL, 2, NULL);  // 优先级 2
    xTaskCreate(Task_Display, "Display", 128, NULL, 3, NULL); // 优先级 3(更高)
    vTaskStartScheduler();
    return 0;
}
Logo

「智能机器人开发者大赛」官方平台,致力于为开发者和参赛选手提供赛事技术指导、行业标准解读及团队实战案例解析;聚焦智能机器人开发全栈技术闭环,助力开发者攻克技术瓶颈,促进软硬件集成、场景应用及商业化落地的深度研讨。 加入智能机器人开发者社区iRobot Developer,与全球极客并肩突破技术边界,定义机器人开发的未来范式!

更多推荐