admin 管理员组文章数量: 1184232
用全局变量搞同步总翻车?FreeRTOS队列教你秒变“任务协调大师”!
你有没有过这样的“代码崩溃瞬间”?在FreeRTOS里写多任务,想用全局变量控制串口打印,逻辑明明捋了八遍——“没人用串口我再上”,结果运行起来全乱套:前一秒还在显示“TaskA处理完成”,后一秒突然蹦出“skB开始工作”,半截文字像被吞了似的,活像打字机突然断了电?
别怀疑自己的代码能力,不是你写得烂,是少了个“任务交通指挥官”!今天咱们要聊的FreeRTOS队列,就是专门治“同步混乱”“资源争抢”的神器——不仅能让任务们“排队干活不打架”,还能轻松实现同步与互斥,看完这篇,你再也不用为“打印翻车”“资源抢崩”头疼!
一、先搞懂:为啥全局变量不靠谱?
在聊队列之前,得先说说咱们踩过的坑——全局变量同步。比如之前用flagUARTUsed控制串口:
void TaskGeneralFunction(void *param) {
while (1) {
if (!flagUARTUsed) { // 觉得“没人用串口”
flagUARTUsed = 1; // 声称“我要用了”
printf("%s\r\n", (char *)param); // 打印内容
flagUARTUsed = 0; // 用完释放
vTaskDelay(1); // 怕独占,主动延时
}
}
}
看着没问题吧?但FreeRTOS是“多任务抢占式”的——刚执行完if (!flagUARTUsed),系统突然切到另一个任务,两个任务同时认为“串口没人用”,都去抢着打印,结果就是文字“叠罗汉”,半截子内容满天飞。
这就像两个同事抢一台打印机,一个刚放好纸,另一个就把文件塞进去,出来的纸肯定是乱的。而队列,就是给打印机装了个“取号机”——按顺序来,绝不插队。
二、队列是啥?一句话讲明白
队列本质就是个“传送带”,或者说“快递柜”:
- 任务A(生产者)把数据“放”到传送带上,按“先放先拿”的规矩(先进先出,FIFO);
- 任务B(消费者)从传送带“拿”数据,拿的永远是最早放上去的那一个;
- 你还得告诉队列两个关键信息:“能放多少个快递”(队列长度)、“每个快递多大”(数据大小)——就像快递柜要标“能放10个件,每个件不超过50cm”。
举个具体例子:
- 任务A放数据“10”进队列,此时队列里只有“10”,头和尾都是它;
- 任务B再放数据“20”进队列,队列就变成“10(头)→20(尾)”;
- 任务C来拿数据,先拿走“10”,队列剩下“20(头)”;
- 任务C再拿,就拿走“20”,队列变空。
简单吧?而且队列还支持“插队”(强制写头部),但一般别用——就像食堂排队有人插塞,容易乱套。
三、队列传数据:FreeRTOS选了个“懒人友好”的方式
用队列传数据,有两种常见思路:“拷贝”和“引用”。FreeRTOS直接选了“拷贝”,理由很实在——省心!
啥是“拷贝”?就是把你要传的变量值,完整“复制粘贴”到队列里。比如你在任务A里定义个局部变量x=10,传给队列时,队列会把“10”存一份自己的副本,哪怕任务A的x被回收了(比如函数退出),队列里的“10”还在。
这就像你把手机里的照片复制到U盘——手机里的照片删了,U盘里的还能用,不用操心“原文件没了怎么办”。
反观“引用”(传变量地址),就麻烦多了:如果原变量被回收,地址就成了“无效地址”,队列再读就会“读脏数据”;而且如果系统有内存保护,还得确保两个任务都能访问这个地址,不然直接报错。
所以除非数据特别大(比如几KB的数组),否则优先用FreeRTOS的“拷贝”方式——不用自己管内存,少踩很多坑。
四、队列的“阻塞访问”:任务不用“傻等”
你可能会问:如果任务去读队列,队列是空的咋办?总不能让任务一直循环查“有数据吗?有吗?有吗?”吧?这也太浪费CPU了。
队列的“阻塞访问”就是解决这个问题的——让任务“先歇会儿,有数据再叫你”。具体规则超简单:
- 读队列时没数据:任务会进入“阻塞状态”(相当于去“休息室”待着),你可以设个“等多久”(比如等10个Tick)。期间如果有数据进队列,系统立刻叫醒任务(进入就绪状态);要是等超时了,也叫醒任务,别在休息室耗着。
就像你点外卖,没到的时候不用一直盯着手机(不占CPU),外卖到了(有数据)就去取,等太久(超时)就换家店。 - 写队列时队列满了:同理,任务也会进入阻塞状态,等队列有空位了再写数据,不用一直试“能写吗?能吗?”。
- 多个任务等同一个队列:如果好几个任务都在等队列的数据,系统会优先叫醒“优先级最高”的任务;如果优先级一样,就叫醒“等最久”的那个——公平又高效。
五、实战第一步:创建队列(两种方式,按需选)
要用电,得先插插座;要用队列,得先“创建”它。FreeRTOS给了两种创建方式:动态分配和静态分配,咱们一个个说。
1. 动态创建:“点外卖式”,不用自己准备内存
动态创建就像点外卖——不用自己准备餐具(内存),系统帮你分配好,用完还能回收。核心函数是xQueueCreate,原型长这样:
QueueHandle_t xQueueCreate(
UBaseType_t uxQueueLength, // 队列长度:能放多少个数据
UBaseType_t uxItemSize // 每个数据的大小:单位是字节
);
返回值很关键:如果返回非NULL,说明队列创建成功(拿到“队列句柄”,以后操作队列全靠它);如果返回NULL,就是内存不够了(系统“没餐具了”)。
举个例子,创建一个“能放2个int类型数据”的队列:
// 先定义一个“队列句柄”,相当于队列的“身份证”
static QueueHandle_t xQueueCalcHandle;
// 创建队列:长度2,每个数据是int(4字节)
xQueueCalcHandle = xQueueCreate(2, sizeof(int));
// 检查是否创建成功
if (xQueueCalcHandle == NULL) {
printf("队列创建失败!内存不够啦~\n");
}
2. 静态创建:“自带饭盒式”,内存自己管
静态创建就像去食堂打饭——得自己带饭盒(提前分配好内存),系统不帮你管内存,但胜在稳定(不会因为内存不足创建失败)。核心函数是xQueueCreateStatic,原型:
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength, // 队列长度
UBaseType_t uxItemSize, // 每个数据大小
uint8_t *pucQueueStorageBuffer, // 存储数据的缓冲区(“饭盒”)
StaticQueue_t *pxQueueBuffer // 存储队列结构的缓冲区(“装饭盒的袋子”)
);
参数要注意:pucQueueStorageBuffer的大小必须≥“队列长度×每个数据大小”,不然数据存不下;pxQueueBuffer必须指向一个StaticQueue_t结构体,用来存队列的“管理信息”。
举个例子,创建一个“能放10个uint32_t类型数据”的静态队列:
// 定义队列参数:长度10,每个数据是uint32_t(4字节)
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof(uint32_t)
// 准备“饭盒”:存数据的缓冲区,大小=10×4=40字节
uint8_t ucQueueStorage[QUEUE_LENGTH * ITEM_SIZE];
// 准备“装饭盒的袋子”:存队列结构
StaticQueue_t xQueueBuffer;
// 在任务里创建静态队列
void vATask(void *pvParameters) {
QueueHandle_t xQueue1;
// 传入参数:长度、数据大小、缓冲区、队列结构
xQueue1 = xQueueCreateStatic(QUEUE_LENGTH, ITEM_SIZE, ucQueueStorage, &xQueueBuffer);
// 检查是否成功:只要pxQueueBuffer不为NULL,一般都能成功
if (xQueue1 == NULL) {
printf("队列创建失败!袋子没准备好~\n");
}
}
小补充:复位和删除队列
- 复位队列:如果想把队列恢复到“刚创建时的空状态”,用
xQueueReset(队列句柄),比如xQueueReset(xQueueCalcHandle),肯定能成功(返回pdPASS)。 - 删除队列:只能删“动态创建”的队列(静态队列内存自己管,系统不删),用
vQueueDelete(队列句柄),比如vQueueDelete(xQueueCalcHandle),删完后句柄就无效了,别再用了。
六、实战第二步:写队列(把数据放进“传送带”)
创建好队列,就可以往里面“放数据”了。核心函数有两个:
xQueueSendToBack:把数据放到队列“尾部”,按FIFO规矩来(推荐用,符合直觉);xQueueSendToFront:把数据放到队列“头部”(插队,特殊场景用,比如紧急数据);
注意:这两个函数不能在中断里用!中断里要用带FromISR后缀的版本(比如xQueueSendToBackFromISR),咱们后面再聊中断。
函数原型和参数(以xQueueSendToBack为例)
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue, // 队列句柄:要写哪个队列
const void *pvItemToQueue, // 数据指针:要传的数据(地址)
TickType_t xTicksToWait // 阻塞时间:队列满了等多久(Tick数)
);
参数解释:
pvItemToQueue:你要传的数据的地址,队列会从这个地址“拷贝”数据进去(比如你要传sum,就传&sum);xTicksToWait:如果队列满了,任务会阻塞多久。设0就是“不等,立刻返回”;设portMAX_DELAY就是“一直等,直到有位置”(别在低优先级任务用,容易卡死)。
返回值:pdPASS表示写成功;errQUEUE_FULL表示队列满了,等超时了还没位置。
例子:任务A往队列写累加值
比如任务A负责计算累加值sum,算完就往队列里写:
static volatile int sum = 0; // 累加值
static QueueHandle_t xQueueCalcHandle; // 队列句柄(提前创建好)
void app_task1(void* pvParameters) {
for (;;) {
sum++; // 计算累加值
// 往队列尾部写sum,没位置就一直等(portMAX_DELAY)
xQueueSendToBack(xQueueCalcHandle, &sum, portMAX_DELAY);
sum = 1; // 重置sum,准备下一次计算
}
}
七、实战第三步:读队列(从“传送带”拿数据)
写了数据,就得有任务来“读”。核心函数是xQueueReceive,专门从队列头部拿数据,拿完后数据会从队列里删掉(给下一个数据腾位置)。
函数原型和参数
BaseType_t xQueueReceive(
QueueHandle_t xQueue, // 队列句柄:要读哪个队列
void *const pvBuffer, // 缓冲区指针:把读的数据存到这里
TickType_t xTicksToWait // 阻塞时间:队列空了等多久(Tick数)
);
参数解释:
pvBuffer:你要存数据的变量地址(比如int val,就传&val),队列会把数据“拷贝”到这个变量里;xTicksToWait:和写队列一样,0是“不等”,portMAX_DELAY是“一直等”。
返回值:pdPASS表示读成功;errQUEUE_EMPTY表示队列空了,等超时了还没数据。
例子:任务B从队列读数据并处理
任务B负责读队列里的累加值,没数据就歇着,有数据就处理(比如打印):
void app_task2(void* pvParameters) {
int val; // 存读出来的数据
for (;;) {
// 从队列读数据,存到val里,没数据就一直等
xQueueReceive(xQueueCalcHandle, &val, portMAX_DELAY);
// 处理数据(比如打印)
printf("拿到的累加值是:%d\n", val);
}
}
八、用队列实现“同步”:任务们“默契配合”
同步的意思是“任务A做完某件事,再通知任务B开始做”。比如任务A算完累加值,再让任务B打印——用队列就能轻松实现,就是咱们上面的例子:
- 队列刚创建时是空的,任务B读队列会进入阻塞状态(不占CPU);
- 任务A算完
sum,往队列里写数据,此时系统会立刻叫醒任务B(因为任务B在等队列数据); - 任务B从阻塞变就绪,然后抢占CPU,读数据并打印;
- 打印完,任务B再去读队列,此时队列又空了,再次阻塞——完美配合,不浪费资源。
这就像你煮完饭(任务A),喊家人来吃饭(写队列),家人听到再过来(任务B读队列),不用你一直喊“饭好了吗”,也不用家人一直等。
九、用队列实现“互斥”:资源“一人用,其他人等”
互斥的意思是“某个资源(比如串口、打印机),同一时间只能有一个任务用”。用队列也能实现,核心思路是“给资源配一把‘钥匙’,拿钥匙才能用,用完还钥匙”。
步骤:用队列实现串口互斥
-
初始化“钥匙”:创建一个长度为1的队列(相当于“钥匙柜”,只能放1把钥匙),并往队列里写一个任意数据(相当于“放入钥匙”);
QueueHandle_t xQueueUARTHandle; // 串口队列句柄 int InitUARTLock(void) { // 创建长度1、数据大小为int的队列(钥匙柜) xQueueUARTHandle = xQueueCreate(1, sizeof(int)); if (xQueueUARTHandle == NULL) { return -1; // 创建失败 } int val = 0; // 任意数据(钥匙) // 把钥匙放进队列 xQueueSend(xQueueUARTHandle, &val, portMAX_DELAY); return 0; // 初始化成功 } -
拿钥匙(用串口前):任务要用水口时,先从队列里读数据(拿钥匙)。因为队列只有1个数据,第一个任务拿走后,队列就空了,其他任务再读会阻塞(拿不到钥匙,只能等);
void GetUARTLock(void) { int val; // 读队列拿钥匙,拿不到就一直等 xQueueReceive(xQueueUARTHandle, &val, portMAX_DELAY); } -
还钥匙(用完串口后):任务用完串口,往队列里写一个数据(还钥匙)。此时队列有数据了,阻塞的任务就能拿到钥匙,开始用串口;
void PutUARTLock(void) { int val = 0; // 写队列还钥匙 xQueueSend(xQueueUARTHandle, &val, portMAX_DELAY); } -
任务用串口:每个要用串口的任务,都要先“拿钥匙”,再用串口,最后“还钥匙”;
void TaskGeneralFunction(void *param) { while (1) { GetUARTLock(); // 拿钥匙,没钥匙就等 printf("%s\r\n", (char *)param); // 用串口打印 PutUARTLock(); // 还钥匙,让其他任务用 vTaskDelay(1); // 主动延时,给其他任务机会 } }
这样一来,串口就像有了“门禁”,同一时间只有一个任务能拿到“钥匙”,再也不会出现“打印翻车”的情况——比全局变量靠谱100倍!
十、总结:队列是FreeRTOS的“万能通信工具”
今天咱们把队列的“底细”摸透了:从解决全局变量的坑,到队列的基本概念、阻塞访问,再到创建、写、读队列,最后用队列实现同步与互斥——其实核心就是“按顺序传数据,让任务们有秩序地干活”。
下次再遇到多任务同步、资源争抢的问题,别再死磕全局变量了,掏出队列这个“神器”,保准让你的代码从“乱糟糟”变“井井有条”。
下一篇咱们再聊“队列集”——多个队列怎么协同工作,让任务通信更灵活。要是这篇里有哪个点没搞懂,回头多敲几遍代码,实践出真知,FreeRTOS没那么难!
版权声明:本文标题:用全局变量搞同步总翻车?FreeRTOS队列教你秒变“任务协调大师”! 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1766240231a3447138.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论