4、队列与信号量:队列的创建与收发、二值信号量与互斥量、计数信号量、优先级反转问题与解决
各位同学,今天我们来聊聊嵌入式实时系统里最核心的两个通信机制——队列和信号量。说实话,这两个东西在无人机飞控里几乎无处不在。我刚开始做飞控那会儿,就因为信号量用错了,导致飞机在空中直接“抽风”,差点炸机。嗯,从那以后我对这块就特别上心。
4.1 队列:任务间的数据管道
队列说白了就是一个先进先出的缓冲区。一个任务往里扔数据,另一个任务从里面取数据。在FreeRTOS里,队列的创建和收发非常直接。
4.1.1 队列的创建
我个人习惯用xQueueCreate()这个API。它需要两个参数:队列长度和每个元素的大小。
// 创建一个能存放10个uint32_t数据的队列
QueueHandle_t xQueue;
xQueue = xQueueCreate(10, sizeof(uint32_t));
if (xQueue == NULL) {
// 创建失败,内存不够了
// 我建议这里直接断言或者记录错误日志
}
这里有个坑——队列元素大小不要设得太大。我在项目中遇到过有人把整个结构体塞进队列,结果内存直接爆了。你想想看,每个元素几百字节,队列长度再设个几十,那内存消耗可不是闹着玩的。
4.1.2 队列的发送与接收
发送用xQueueSend(),接收用xQueueReceive()。这两个函数都支持超时机制。
// 发送端:传感器任务
uint32_t sensor_data = 0x12345678;
if (xQueueSend(xQueue, &sensor_data, pdMS_TO_TICKS(10)) != pdPASS) {
// 发送失败,队列满了
// 我曾经在这里吃过亏,队列满时直接丢弃数据会导致控制链路中断
// 建议根据业务决定是阻塞等待还是丢弃旧数据
}
// 接收端:控制任务
uint32_t received_data;
if (xQueueReceive(xQueue, &received_data, portMAX_DELAY) == pdPASS) {
// 成功收到数据,处理之
process_control_data(received_data);
}
注意看接收端用了portMAX_DELAY,这意味着它会一直等,直到有数据来。这在飞控的主循环里很常见——控制任务就等着传感器数据,数据不来就不干活。
关键点:队列操作一定要检查返回值。我见过太多人直接忽略返回值,结果数据丢了都不知道。在飞控里,丢一个数据可能就意味着姿态解算出错。
4.2 二值信号量与互斥量
这两个东西经常被搞混。我简单说一下区别:二值信号量用于“通知”,互斥量用于“保护共享资源”。
4.2.1 二值信号量
二值信号量就像一个旗子,要么是1(有效),要么是0(无效)。一个任务给信号量,另一个任务等信号量。
// 创建二值信号量
SemaphoreHandle_t xBinarySemaphore;
xBinarySemaphore = xSemaphoreCreateBinary();
// 中断中给出信号量(通知任务)
void ISR_Handler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 任务中等待信号量
void vTask_Handler(void *pvParameters) {
while(1) {
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdPASS) {
// 收到中断通知,处理数据
process_interrupt_data();
}
}
}
这里有个细节——中断里给信号量要用FromISR版本。我刚开始做的时候直接用了xSemaphoreGive(),结果系统直接死机。嗯,这个错误犯过一次就再也不会忘了。
4.2.2 互斥量
互斥量比二值信号量多了一个特性——优先级继承。这个特性专门用来解决优先级反转问题。我们后面会细说。
// 创建互斥量
SemaphoreHandle_t xMutex;
xMutex = xSemaphoreCreateMutex();
// 访问共享资源
void access_shared_data(void) {
if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdPASS) {
// 安全访问共享数据
shared_data.value += 1;
// 记得释放
xSemaphoreGive(xMutex);
} else {
// 获取互斥量超时,做错误处理
log_error("Failed to acquire mutex");
}
}
我的建议:保护共享资源时,优先用互斥量而不是二值信号量。互斥量有优先级继承机制,能避免很多坑。但要注意,互斥量不能在中断中使用——中断里只能用二值信号量或计数信号量。
4.3 计数信号量
计数信号量就像一个计数器。它记录某个资源的可用数量。比如,一个缓冲区有10个空位,每放一个数据就减1,每取一个数据就加1。
// 创建计数信号量,初始值为5,最大值为10
SemaphoreHandle_t xCountingSemaphore;
xCountingSemaphore = xSemaphoreCreateCounting(10, 5);
// 生产者:占用一个资源
void producer_task(void *pvParameters) {
while(1) {
// 生产数据...
xSemaphoreGive(xCountingSemaphore); // 增加可用资源数
}
}
// 消费者:等待资源
void consumer_task(void *pvParameters) {
while(1) {
if (xSemaphoreTake(xCountingSemaphore, pdMS_TO_TICKS(50)) == pdPASS) {
// 有可用资源,消费之
consume_data();
}
}
}
计数信号量在飞控里用得很多。比如,DMA传输完成中断给信号量,主循环等信号量处理数据。多个DMA通道可以同时触发,计数信号量正好能记录有多少个传输完成了。
4.4 优先级反转问题与解决
优先级反转是实时系统里最头疼的问题之一。说白了就是:一个高优先级任务被一个低优先级任务阻塞了,而中间还有一个中优先级任务在抢CPU。
4.4.1 问题场景
假设有三个任务:
- 任务A:高优先级,需要访问共享资源
- 任务B:中优先级,纯计算任务
- 任务C:低优先级,持有共享资源的锁
执行流程是这样的:
- 任务C先运行,拿到了互斥量
- 任务A就绪,抢占CPU,但发现互斥量被C持有,于是阻塞等待
- 任务B就绪,抢占CPU(因为C优先级低,被抢了)
- 任务B一直运行,任务C得不到CPU,无法释放互斥量
- 任务A只能干等,直到任务B运行完
你看,高优先级的任务A,硬生生被中优先级的任务B给拖死了。这就是优先级反转。
我曾经踩过的坑:在飞控里,姿态解算任务(高优先级)和日志记录任务(低优先级)共享了一个数据结构。结果日志任务拿到锁后,被一个中优先级的通信任务抢占了CPU。姿态解算任务等了整整200ms才拿到锁,飞机直接翻了。从那以后,我对共享资源的访问控制就特别谨慎。
4.4.2 解决方案:优先级继承
FreeRTOS的互斥量自带优先级继承机制。当高优先级任务被低优先级任务阻塞时,系统会临时把低优先级任务的优先级提升到和高优先级一样高。这样中优先级任务就抢不过它了。
// 使用互斥量(自带优先级继承)
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
void high_priority_task(void *pvParameters) {
while(1) {
// 尝试获取互斥量
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdPASS) {
// 访问共享资源
access_shared_resource();
xSemaphoreGive(xMutex);
}
}
}
void low_priority_task(void *pvParameters) {
while(1) {
xSemaphoreTake(xMutex, portMAX_DELAY);
// 持有锁期间,如果高优先级任务来等锁
// 系统会自动提升本任务的优先级
access_shared_resource();
xSemaphoreGive(xMutex);
// 释放锁后,优先级恢复原样
}
}
注意,只有互斥量才有优先级继承。二值信号量没有这个机制。所以,如果你用二值信号量来保护共享资源,优先级反转问题依然存在。
4.4.3 其他解决方案
除了优先级继承,还有几种方法:
- 关中断:在临界区关中断,但会影响实时性
- 优先级天花板:把持有锁的任务优先级设到最高
- 避免共享资源:用消息队列传递数据副本,而不是共享指针
我个人最推荐最后一种。在飞控里,我尽量用队列传递数据,而不是共享内存。虽然会多一次拷贝,但避免了所有同步问题。你想想看,一次memcpy才几个微秒,比处理优先级反转带来的bug要划算得多。
总结一下:
- 队列用于任务间数据传递,注意检查返回值
- 二值信号量用于通知,互斥量用于保护共享资源
- 计数信号量管理资源数量,适合多生产者-消费者场景
- 优先级反转是实时系统的头号杀手,用互斥量或避免共享来解决
好了,这一章的内容就到这里。下一章我们会讲软件定时器和任务通知,这两个也是飞控里常用的机制。记得多动手写代码,光看是学不会的。