在嵌入式系统中,多个任务共享硬件资源是常态。想象一下,两个任务同时操作串口发送数据会发生什么?数据混叠、通信失败——这就是典型的资源竞争场景。FreeRTOS通过信号量和互斥量提供了优雅的解决方案,它们就像交通信号灯,协调着任务对共享资源的访问秩序。
信号量的本质是一个计数器。计数型信号量(Counting Semaphore)允许计数值从0到设定最大值,适合管理有限资源池,比如缓冲区块数量。二进制信号量(Binary Semaphore)则是计数型信号量的特例,计数值只有0和1两种状态,常用于任务同步。创建信号量时,xSemaphoreCreateCounting(10, 0)的第一个参数设定最大值,第二个是初始值。
互斥量(Mutex)是特殊的二进制信号量,核心区别在于它实现了优先级继承机制。当高优先级任务因低优先级任务持有锁而阻塞时,低优先级任务会临时继承高优先级,避免"优先级反转"问题。这种机制就像救护车优先通过路口——普通车辆(低优先级任务)获得临时通行权(优先级提升)以快速让出道路。
在数据采集系统中,ADC任务(生产者)和数据处理任务(消费者)的典型场景中,计数型信号量能完美匹配。以下是具体实现步骤:
c复制// 创建最大计数值为5的信号量
SemaphoreHandle_t xDataSemaphore = xSemaphoreCreateCounting(5, 0);
void vADCTask(void *pvParameters) {
while(1) {
uint16_t adcValue = ReadADC();
PushToBuffer(adcValue); // 数据存入缓冲区
xSemaphoreGive(xDataSemaphore); // 计数值+1
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void vProcessTask(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xDataSemaphore, portMAX_DELAY) == pdTRUE) {
uint16_t data = PopFromBuffer(); // 取出数据
ProcessData(data);
}
}
}
这个模型的关键优势在于:
在以太网通信项目中,我使用计数型信号量管理TCP连接池。初始化时创建等于最大连接数的信号量(如xSemaphoreCreateCounting(8, 8)),任务获取连接时Take,释放连接时Give。这种方式比直接操作连接状态数组更安全,因为信号量操作是原子性的,不会出现竞态条件。
当处理按键中断这类异步事件时,二进制信号量是最佳选择。在STM32项目中,我这样实现按键检测:
c复制SemaphoreHandle_t xButtonSem = xSemaphoreCreateBinary();
// 中断服务程序
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xButtonSem, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void vButtonTask(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xButtonSem, portMAX_DELAY) == pdTRUE) {
DebounceAndHandleKey(); // 消抖处理
}
}
}
这种模式的三个关键点:
xSemaphoreGiveFromISR这个特殊版本xHigherPriorityTaskWoken实现及时任务切换在电机控制系统中,需要确保PWM生成任务和电流采样任务严格同步。我创建了两个二进制信号量构成同步屏障:
c复制SemaphoreHandle_t xPWMReady = xSemaphoreCreateBinary();
SemaphoreHandle_t xSampleReady = xSemaphoreCreateBinary();
void vPWMTask(void *pvParameters) {
while(1) {
GeneratePWM();
xSemaphoreGive(xPWMReady); // 通知采样任务
xSemaphoreTake(xSampleReady, portMAX_DELAY); // 等待采样完成
}
}
void vSampleTask(void *pvParameters) {
while(1) {
xSemaphoreTake(xPWMReady, portMAX_DELAY);
ADCSampling();
xSemaphoreGive(xSampleReady);
}
}
这种乒乓操作确保了控制周期的时间精度,实测同步误差小于1μs。
在无人机飞控系统中,我曾遇到这样的优先级反转案例:
使用普通二进制信号量时,系统响应延迟达到不可接受的15ms。改为互斥量后,当控制任务请求锁时:
在复杂协议栈实现中,经常遇到函数嵌套调用需要重复加锁的情况。递归互斥量允许同一任务多次获取锁,例如:
c复制SemaphoreHandle_t xRecursiveMutex = xSemaphoreCreateRecursiveMutex();
void ProcessLayer3() {
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
// 处理逻辑
xSemaphoreGiveRecursive(xRecursiveMutex);
}
void ProcessLayer2() {
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
ProcessLayer3();
xSemaphoreGiveRecursive(xRecursiveMutex);
}
void ProcessLayer1() {
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
ProcessLayer2();
xSemaphoreGiveRecursive(xRecursiveMutex);
}
每个TakeRecursive必须配对GiveRecursive,系统内部维护嵌套计数。我在Modbus TCP协议解析中应用此模式,既保证线程安全,又避免自死锁。
FreeRTOS的信号量实际是基于队列实现的特殊案例。创建信号量时:
uxMaxCountuxMessagesWaiting变量作为计数值这种设计带来两个重要特性:
查看semphr.h源码可以发现,xSemaphoreGive()实质是调用xQueueGenericSend(),而xSemaphoreTake()调用xQueueGenericReceive()。
互斥量的优先级继承机制通过三个关键步骤实现:
xTaskPriorityInherit()函数临时提升持有者优先级xTaskPriorityDisinherit()恢复原优先级taskRECORD_READY_PRIORITY()确保调度器及时响应优先级变化在Cortex-M3内核上,这些操作通常能在20个时钟周期内完成,开销几乎可以忽略。
在资源受限的STM32F103(20KB RAM)项目中,实测不同同步原语的内存消耗:
| 同步类型 | 每个实例占用字节 |
|---|---|
| 计数型信号量 | 56 |
| 二进制信号量 | 56 |
| 互斥量 | 64 |
| 递归互斥量 | 72 |
对于需要大量同步对象的应用,可以考虑复用信号量或采用更轻量化的方案(如关中断)。
在多个商业项目中,我总结出这些典型错误:
FromISR版本有个特别隐蔽的Bug:在RTOS tick中断中调用xSemaphoreGiveFromISR后忘记检查xHigherPriorityTaskWoken,导致高优先级任务延迟调度。这个问题的定位花了我整整两天时间。