上一节学习了FreeRTOS常见错误与工程规范,知道了任务不阻塞、优先级设置混乱、任务栈不合理等问题都会影响系统稳定性。
当程序已经能够稳定运行以后,还可以进一步考虑:
CPU有没有被浪费?
任务栈是不是给得太大?
Heap是不是占用太多?
任务是不是创建得太多?
任务之间的通信方式是否合适?
这些就属于FreeRTOS的性能优化。
本节学习目标:掌握几个最常用的FreeRTOS优化方向,能够从CPU占用、任务栈、Heap、任务数量和任务通信几个方面检查自己的工程。
一、第一步不是优化,而是先测量
很多人优化程序时会直接:
减小任务栈
提高任务优先级
减少Delay
这种方法并不可靠。
正确思路应该是:
先测量
↓
找到问题
↓
再优化
例如我们前面已经学习过:
uxTaskGetStackHighWaterMark();
可以检查任务栈。
xPortGetFreeHeapSize();
可以检查剩余Heap。
vTaskGetRunTimeStats();
可以观察各任务CPU运行时间。
只有知道:
资源到底消耗在哪里
才能进行有针对性的优化。
二、避免任务无意义占用CPU
假设有一个按键任务:
void KeyTask(void *pvParameters)
{
while(1)
{
KeyScan();
}
}
如果KeyScan()执行非常快,那么CPU可能会不断执行:
扫描
↓
扫描
↓
扫描
↓
扫描
但是按键根本没有必要每时每刻扫描。
可以改成:
void KeyTask(void *pvParameters)
{
while(1)
{
KeyScan();
vTaskDelay(
pdMS_TO_TICKS(20)
);
}
}
这样任务每:
20ms
检查一次按键。
剩余时间:
任务进入Blocked
CPU就可以去运行其他任务。
所以优化FreeRTOS程序非常重要的一点就是:
没有事情做的任务,不要让它一直处于Ready状态。
三、能阻塞等待,就不要循环查询
例如通信任务等待数据:
while(1)
{
if(DataReady == 1)
{
SendData();
DataReady = 0;
}
}
这个任务会一直查询:
DataReady有没有变成1?
大多数CPU时间其实都浪费在:
检查一个没有变化的变量。
如果使用队列:
xQueueReceive(
DataQueue,
&Data,
portMAX_DELAY
);
没有数据时:
任务直接阻塞。
有数据以后:
FreeRTOS自动唤醒任务。
类似的还有:
Semaphore
Task Notification
Event Group
所以在FreeRTOS中应该逐渐形成:
事件没有发生
↓
任务阻塞
事件发生
↓
任务被唤醒
而不是所有任务都不断查询状态。
四、优化任务栈大小
创建任务时:
xTaskCreate(
SensorTask,
"Sensor",
1024,
NULL,
2,
NULL
);
如果SensorTask实际上只需要很少的栈空间,那么:
1024
就可能设置得过大。
STM32F407中StackType_t通常为:
4Byte
因此:
1024个StackType_t
大约就是:
4096Byte
如果创建:
5个任务
每个都随手给很大的任务栈,就会浪费大量SRAM。
可以使用:
uxTaskGetStackHighWaterMark(
SensorTaskHandle
);
观察任务运行过程中最小剩余栈空间。
例如:
任务栈设置512
长期运行以后
High Water Mark = 350
说明当前任务栈可能存在较大的富余。
可以在充分测试的基础上适当减小,但一定要:
保留安全余量。
不要为了省几十Byte RAM,把任务栈压到刚刚够用。
五、优化Heap使用
动态创建:
任务
队列
信号量
互斥量
软件定时器
都会消耗FreeRTOS管理的内存。
可以查看:
xPortGetFreeHeapSize();
以及:
xPortGetMinimumEverFreeHeapSize();
例如:
printf(
"Free Heap = %lu\r\n",
(unsigned long)
xPortGetFreeHeapSize()
);
printf(
"Min Heap = %lu\r\n",
(unsigned long)
xPortGetMinimumEverFreeHeapSize()
);
如果系统运行以后发现:
大量Heap始终没有使用
可以根据实际情况调整配置。
如果Heap非常紧张,则需要检查:
任务栈是不是设置太大
队列长度是不是过长
创建的任务是不是过多
而不是直接:
继续增加configTOTAL_HEAP_SIZE。
六、任务不是越多越好
假设系统需要:
LED1闪烁
LED2闪烁
LED3闪烁
最直接的方法可能是创建:
LED1Task
LED2Task
LED3Task
当然可以运行。
但是每创建一个任务,都需要:
TCB
任务栈
调度资源
如果这些LED逻辑非常简单,有时可以使用:
一个任务
统一管理:
void LedTask(void *pvParameters)
{
while(1)
{
LED_Process();
vTaskDelay(
pdMS_TO_TICKS(10)
);
}
}
所以:
一个功能
并不一定必须对应:
一个任务。
是否需要独立任务,要看它是否具有:
独立执行周期
独立阻塞条件
不同实时性要求
七、选择合适的任务通信方式
FreeRTOS提供很多通信和同步工具:
Queue
Semaphore
Mutex
Event Group
Task Notification
它们并不是:
哪个高级就用哪个。
例如:
需要传递一组数据
↓
Queue
ISR通知任务处理事件
↓
Task Notification
或二值信号量
保护UART、SPI等共享资源
↓
Mutex
等待多个状态位
↓
Event Group
如果只是:
Task1通知Task2一次
却专门创建一个很大的队列,就可能没有必要。
选择最符合需求的机制,程序通常也会更加清晰。
八、优先级优化不是全部调高
假设:
ControlTask 4
SensorTask 3
UartTask 2
LedTask 1
这样的优先级体现了不同任务的实时性要求。
但是如果把所有任务都设置成:
最高优先级
并不会让系统整体变快。
相反,优先级设计不合理可能造成:
低优先级任务长时间得不到运行
任务切换增加
程序逻辑变得难以分析
所以任务优先级优化的核心不是:
提高优先级
而是:
让真正需要快速响应的任务
拥有更合适的优先级。
九、减少不必要的printf
调试时我们经常写:
printf("Task1 Running\r\n");
如果放在:
while(1)
中高频执行,串口输出本身就可能占用大量时间。
例如:
while(1)
{
printf("Sensor Task Running\r\n");
ReadSensor();
vTaskDelay(
pdMS_TO_TICKS(10)
);
}
每10ms打印一次:
1秒约100次
调试时可能没有问题,但正式运行时这些输出往往没有必要。
因此项目完成以后,可以删除或降低:
高频调试打印。
否则有时候你看到的:
系统性能差
其实大量时间都消耗在串口输出上。
十、总结
这一节学习了FreeRTOS几个比较实用的性能优化方向。
首先要记住:
不要凭感觉优化
↓
先测量
↓
再修改
可以通过:
uxTaskGetStackHighWaterMark();
检查任务栈。
通过:
xPortGetFreeHeapSize();
xPortGetMinimumEverFreeHeapSize();
检查Heap。
通过运行时间统计观察:
哪些任务占用了大量CPU。
实际优化时重点检查:
任务是否存在无意义死循环
能不能通过阻塞等待事件
任务栈是否设置过大
Heap是否合理
任务数量是否过多
任务通信方式是否合适
任务优先级是否合理
是否存在大量无意义printf
FreeRTOS性能优化并不是:
让CPU一直高速运行。
更合理的目标应该是:
该运行的任务及时运行
不需要运行的任务及时阻塞
RAM不浪费
CPU不空转
任务之间关系清楚
做到这些以后,一个FreeRTOS工程通常就已经具备了比较好的运行效率和可维护性。