3、数据采集任务调度:FreeRTOS任务创建、任务优先级设计、任务间通信(队列、信号量)、时间片轮转
好,咱们进入第三章。这一章是气象站软件架构的「心脏」部分——任务调度。说白了,就是让传感器采集、数据处理、显示刷新、通信上传这些活儿,在单片机上井井有条地跑起来。
我刚开始做嵌入式那会儿,总喜欢写一个大循环,把所有事情按顺序排好。后来发现,一旦传感器多了、功能复杂了,这种「裸奔」方式根本扛不住。一个I2C读操作卡住,整个系统就瘫痪了。嗯,这就是为什么我们要用FreeRTOS。
3.1 任务创建:别一股脑全塞进去
在气象站项目里,我习惯把任务拆成四个核心模块:
- 采集任务:读取温湿度、气压、风速风向
- 处理任务:滤波、校准、计算平均值
- 显示任务:刷新OLED或LCD屏幕
- 通信任务:通过LoRa或WiFi上传数据
创建任务的API很简单,就是xTaskCreate()。但这里有个坑——任务栈大小。我见过太多人把栈设成128字节,结果程序跑着跑着就莫名其妙复位了。
// 创建采集任务
xTaskCreate(
vDataCollectionTask, // 任务函数
"Collect", // 任务名
1024, // 栈大小(字)
NULL, // 参数
3, // 优先级
NULL // 任务句柄
);
3.2 任务优先级设计:谁先跑,谁后跑
优先级设计是个艺术活。你想想看,气象站里哪个任务最不能等?
我个人习惯把采集任务设为最高优先级。为什么?因为传感器数据是时效性的。你延迟100ms去读风速,可能一阵风就过去了。显示任务可以慢一点,哪怕卡顿一帧,人眼也看不出来。
| 任务名称 | 优先级 | 理由 |
|---|---|---|
| 采集任务 | 4(最高) | 传感器数据时效性要求高 |
| 通信任务 | 3 | 丢包重传需要及时响应 |
| 处理任务 | 2 | 计算可以等,但不能太久 |
| 显示任务 | 1(最低) | 刷新慢一点没关系 |
3.3 任务间通信:队列和信号量
任务之间怎么传数据?直接访问全局变量?别闹了,那会出大问题的。FreeRTOS提供了两种好用的机制:队列和信号量。
3.3.1 队列:数据搬运工
采集任务读到的温湿度数据,怎么传给处理任务?用队列。我习惯把队列长度设成10,这样即使处理任务偶尔卡一下,数据也不会丢。
// 创建队列
QueueHandle_t xSensorQueue;
xSensorQueue = xQueueCreate(10, sizeof(SensorData_t));
// 采集任务发送数据
SensorData_t data;
data.temperature = 25.6f;
data.humidity = 60.2f;
xQueueSend(xSensorQueue, &data, portMAX_DELAY);
// 处理任务接收数据
SensorData_t receivedData;
if (xQueueReceive(xSensorQueue, &receivedData, pdMS_TO_TICKS(100)) == pdPASS) {
// 处理数据
}
xQueueSend()而不是xQueueSendToFront()。除非你有特殊需求,否则先进先出是最自然的。
3.3.2 信号量:任务同步的开关
信号量更像是一个「门禁」。比如,采集任务完成一次完整的数据采集后,通知处理任务开始工作。这时候用二值信号量最合适。
我记得有一次,我在项目中用了计数信号量来控制多个传感器同时采集。结果发现,信号量计数值没处理好,导致任务一直阻塞。后来我加了个超时机制,问题就解决了。
// 创建二值信号量
SemaphoreHandle_t xDataReady;
xDataReady = xSemaphoreCreateBinary();
// 采集任务:采集完成后释放信号量
xSemaphoreGive(xDataReady);
// 处理任务:等待信号量
if (xSemaphoreTake(xDataReady, pdMS_TO_TICKS(1000)) == pdPASS) {
// 开始处理数据
}
Give一次,否则第一个Take会永远阻塞。我刚开始用的时候,就因为这个bug调试了一下午。
3.4 时间片轮转:让每个任务都有机会
优先级高的任务一直占着CPU怎么办?这时候就需要时间片轮转。FreeRTOS默认是抢占式调度,但同优先级的任务之间,靠时间片来轮流执行。
我建议把显示任务和通信任务设为同一优先级。这样,当显示任务在刷新屏幕时,通信任务也能分到时间片去发送数据。不会出现「显示卡住,通信也卡住」的情况。
// 在FreeRTOSConfig.h中配置时间片
#define configUSE_TIME_SLICING 1 // 启用时间片轮转
#define configTICK_RATE_HZ 1000 // 系统时钟节拍,1ms一个tick
taskYIELD(),让出CPU。
3.5 实战中的调度策略
说了这么多,咱们看看实际的气象站里,任务是怎么调度的。
- 采集任务每1秒唤醒一次,读取所有传感器。读完后,把数据塞进队列,然后释放信号量。
- 处理任务等待信号量。收到信号后,从队列里取出数据,做滑动平均滤波。处理完再发给显示和通信任务。
- 显示任务每500ms刷新一次屏幕。如果没新数据,就显示上一次的值。
- 通信任务每10秒上传一次数据。如果上传失败,重试3次,再失败就丢弃。
这个架构我用了好几年,在多个气象站项目里验证过。说白了,就是让每个任务各司其职,互不干扰。你想想看,如果采集任务和显示任务抢CPU,那传感器数据可能就丢了。
嗯,这一章的内容就到这里。下一章咱们聊聊传感器驱动层设计,包括I2C、SPI、单总线这些协议的封装技巧。到时候我会分享一个我踩过的坑——关于风速传感器读取时序的,保证让你少走弯路。