前面已经学习了任务、队列、信号量、互斥量、任务通知等内容,也完成了生产者消费者和多任务同步实验。
但是实际STM32项目中,很少只有两个任务。
例如一个采集系统可能同时需要:
采集传感器
处理数据
串口通信
状态指示
故障检测
如果所有功能都塞进一个while(1)中,程序会越来越难维护。
所以这一节不再学习新的FreeRTOS API,而是把之前的知识组合起来,看看一个简单的STM32 FreeRTOS工程应该怎样进行任务划分和系统架构设计。
本节学习目标:学会把一个完整功能拆分成多个任务,并根据任务之间的关系选择队列、任务通知和互斥量。
一、先确定项目功能
假设现在使用STM32F407制作一个简单的数据采集系统。
系统需要完成:
每500ms采集一次传感器
采集完成以后:
处理传感器数据
然后:
通过串口发送数据
同时LED:
每1000ms闪烁一次
用于表示系统正在正常运行。
如果使用裸机程序,可能直接写成:
while(1)
{
ReadSensor();
ProcessData();
SendData();
LED_Toggle();
}
程序小时没有问题。
但是以后再增加:
按键
报警
网络通信
故障检测
这个while(1)就会越来越复杂。
二、使用FreeRTOS划分任务
我们可以把系统拆成:
SensorTask
↓
采集数据
ProcessTask
↓
处理数据
UartTask
↓
发送数据
LedTask
↓
状态指示
整个结构:
SensorTask
↓
Queue
↓
ProcessTask
↓
Queue
↓
UartTask
LedTask独立周期运行
这样每个任务只负责:
一类比较明确的工作。
以后传感器代码出问题:
检查SensorTask
串口发送出问题:
检查UartTask
程序结构会清楚很多。
三、定义数据结构
实际传感器通常不只有一个数据。
例如:
typedef struct
{
float temperature;
float humidity;
}SensorData_t;
表示一次采集得到:
温度
湿度
然后创建两个队列:
QueueHandle_t SensorQueue;
QueueHandle_t UartQueue;
第一个:
SensorQueue
SensorTask → ProcessTask
第二个:
UartQueue
ProcessTask → UartTask
四、创建队列
初始化:
SensorQueue = xQueueCreate(
5,
sizeof(SensorData_t)
);
UartQueue = xQueueCreate(
5,
sizeof(SensorData_t)
);
每个队列最多保存:
5组SensorData_t数据。
然后判断:
if((SensorQueue == NULL) ||
(UartQueue == NULL))
{
printf("Queue Create Failed\r\n");
}
队列创建失败通常需要检查:
FreeRTOS Heap是否足够。
五、SensorTask负责采集
SensorTask:
void SensorTask(void *pvParameters)
{
SensorData_t Data;
while(1)
{
Data.temperature =
ReadTemperature();
Data.humidity =
ReadHumidity();
xQueueSend(
SensorQueue,
&Data,
portMAX_DELAY
);
vTaskDelay(
pdMS_TO_TICKS(500)
);
}
}
这个任务只负责:
读取传感器
↓
把数据发送出去
至于:
数据怎么处理
怎么通过串口发送
SensorTask都不关心。
这就是任务划分以后非常重要的思想:
一个任务尽量只负责自己的主要功能。
六、ProcessTask负责处理
ProcessTask:
void ProcessTask(void *pvParameters)
{
SensorData_t Data;
while(1)
{
if(xQueueReceive(
SensorQueue,
&Data,
portMAX_DELAY
) == pdPASS)
{
Data.temperature += 0.5f;
xQueueSend(
UartQueue,
&Data,
portMAX_DELAY
);
}
}
}
如果没有传感器数据:
ProcessTask
↓
阻塞
SensorTask发送数据以后:
ProcessTask被唤醒
↓
处理数据
↓
发送给UartQueue
这里的:
Data.temperature += 0.5f;
只是模拟数据处理。
实际工程中可以替换成:
滤波
校准
单位转换
数据计算
等操作。
七、UartTask负责发送
UartTask:
void UartTask(void *pvParameters)
{
SensorData_t Data;
while(1)
{
if(xQueueReceive(
UartQueue,
&Data,
portMAX_DELAY
) == pdPASS)
{
printf(
"Temp = %.2f, Hum = %.2f\r\n",
Data.temperature,
Data.humidity
);
}
}
}
如果没有数据:
UartTask阻塞。
只有ProcessTask处理完成:
ProcessTask
↓
UartQueue
↓
UartTask
UartTask才开始发送。
因此CPU不会浪费时间不断查询:
有没有新数据?
八、LedTask独立运行
LED状态任务:
void LedTask(void *pvParameters)
{
while(1)
{
GPIO_ToggleBits(
GPIOF,
GPIO_Pin_9
);
vTaskDelay(
pdMS_TO_TICKS(1000)
);
}
}
这个任务与:
传感器采集
数据处理
串口发送
没有直接关系。
所以它可以:
独立周期运行。
如果系统正常调度:
LED持续闪烁。
如果程序严重异常:
LED停止闪烁
还可以作为一个简单的系统状态观察手段。
九、任务优先级怎么分配
例如:
xTaskCreate(
SensorTask,
"Sensor",
256,
NULL,
3,
NULL
);
xTaskCreate(
ProcessTask,
"Process",
256,
NULL,
2,
NULL
);
xTaskCreate(
UartTask,
"Uart",
256,
NULL,
2,
NULL
);
xTaskCreate(
LedTask,
"LED",
128,
NULL,
1,
NULL
);
这里给SensorTask稍高优先级:
SensorTask 3
ProcessTask 2
UartTask 2
LedTask 1
因为我们希望:
到达采样时间以后
优先完成数据采集。
LED只是状态指示:
晚几毫秒闪烁通常没有影响
所以优先级最低。
但是要注意:
优先级不是越高越好。
任务优先级应该根据:
实时性要求
重要程度
任务之间的依赖关系
合理设置。
十、总结
这一节没有学习新的FreeRTOS函数,而是把前面的知识组合成了一个简单的系统架构。
整个项目:
SensorTask
采集数据
↓
SensorQueue
↓
ProcessTask
处理数据
↓
UartQueue
↓
UartTask
发送数据
同时:
LedTask
↓
独立运行
↓
显示系统状态
这里最重要的并不是代码本身,而是:
先根据功能划分任务
↓
再分析任务之间需要传递什么数据
↓
选择队列、信号量、通知或互斥量
↓
最后确定任务优先级
不要看到FreeRTOS支持多任务,就简单地:
一个功能随便创建一个任务
所有任务全部while(1)
真正合理的FreeRTOS工程应该让每个任务:
职责明确
关系清楚
该阻塞时阻塞
需要数据时再运行
这样随着项目功能增加,程序仍然能够保持比较清晰的结构。