上一节学习了FreeRTOS移植与调度原理,知道了STM32F407中的SysTick、PendSV和SVC在FreeRTOS调度过程中分别起什么作用。
到这里,FreeRTOS的大部分常用功能我们已经学习过了。
真正开始写项目以后,很多问题往往不是:
不会使用某个API
而是:
API会用,但是使用方式不合理。
例如任务中忘记阻塞、优先级设置过高、在中断中调用普通API、任务栈设置过小等。
这些问题有些甚至不会立即报错,而是在程序运行一段时间以后才出现。
本节学习目标:集中整理FreeRTOS实际工程中最常见的错误,并形成几个基本的编程习惯。
一、任务while(1)中忘记阻塞
这是非常常见的问题。
例如:
void Task1(void *pvParameters)
{
while(1)
{
ReadSensor();
}
}
这个任务:
读取完成
↓
马上再次读取
↓
再次读取
↓
一直运行
如果Task1优先级又比较高,就可能导致低优先级任务长时间得不到CPU。
如果传感器只需要:
100ms采集一次
可以写成:
void Task1(void *pvParameters)
{
while(1)
{
ReadSensor();
vTaskDelay(
pdMS_TO_TICKS(100)
);
}
}
这样Task1完成一次采集以后:
进入Blocked
↓
让出CPU
↓
其他任务运行
所以写完一个任务以后,首先可以检查:
这个任务什么时候会进入阻塞态?
如果答案是:
永远不会
就应该考虑这个任务是否真的需要一直占用CPU。
二、所有任务优先级都设置得很高
有些初学者会认为:
优先级越高
任务运行越快
于是创建任务:
SensorTask 10
UartTask 10
LedTask 10
KeyTask 10
这种设计通常没有必要。
优先级表示的是:
多个任务都能运行时
谁应该先获得CPU。
例如:
紧急控制任务 4
数据采集任务 3
通信任务 2
LED状态任务 1
会更加容易理解。
像LED闪烁:
晚几毫秒通常没有关系。
就没必要给非常高的优先级。
因此:
优先级应该根据实时性要求分配,而不是越高越好。
三、中断中调用普通FreeRTOS API
前面学习中断安全API时已经讲过:
在中断服务函数中不能随意使用普通版本的FreeRTOS API。
例如不要在中断中直接:
xQueueSend();
而应该使用对应的:
xQueueSendFromISR();
例如:
void USART1_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken =
pdFALSE;
xQueueSendFromISR(
UartQueue,
&Data,
&xHigherPriorityTaskWoken
);
portYIELD_FROM_ISR(
xHigherPriorityTaskWoken
);
}
FreeRTOS很多中断版本API都会带:
FromISR
所以看到自己正在写:
IRQHandler
时就应该特别注意:
我调用的这个FreeRTOS函数
能不能在中断中使用?
四、任务栈随便填写
创建任务:
xTaskCreate(
Task1,
"Task1",
128,
NULL,
2,
NULL
);
这里的:
128
不能每个任务都机械地复制。
例如任务中定义:
uint8_t Buffer[500];
就会明显增加任务栈的消耗。
再加上:
函数调用
局部变量
上下文保存
原来的任务栈可能就不够用了。
可以通过:
uxTaskGetStackHighWaterMark();
检查任务历史最小剩余栈。
调试阶段还建议开启:
#define configCHECK_FOR_STACK_OVERFLOW 2
这样比程序出现HardFault以后再猜问题要可靠得多。
五、创建对象以后不检查结果
例如:
SensorQueue =
xQueueCreate(
10,
sizeof(uint32_t)
);
然后直接使用:
xQueueSend(
SensorQueue,
&Data,
0
);
如果队列因为Heap不足创建失败:
SensorQueue = NULL
后面的程序就可能出现问题。
因此建议:
if(SensorQueue == NULL)
{
printf(
"Sensor Queue Create Failed\r\n"
);
while(1)
{
}
}
任务创建也一样:
BaseType_t Result;
Result = xTaskCreate(
Task1,
"Task1",
128,
NULL,
2,
NULL
);
if(Result != pdPASS)
{
printf("Task Create Failed\r\n");
}
不要默认:
调用创建函数
=
一定创建成功。
六、多个任务直接操作同一个外设
假设:
Task1
↓
USART1
Task2
↓
USART1
两个任务都直接:
printf();
如果Task1发送到一半发生任务切换:
Task1:Temperature =
↓
Task2:System OK
↓
Task1:25
最终串口数据就可能混乱。
对于这种共享资源,可以使用:
Mutex
例如:
xSemaphoreTake(
UartMutex,
portMAX_DELAY
);
printf(
"Temperature = %d\r\n",
Temperature
);
xSemaphoreGive(
UartMutex
);
这样同一时刻只有一个任务可以使用串口。
七、拿到互斥量以后忘记释放
例如:
xSemaphoreTake(
UartMutex,
portMAX_DELAY
);
printf("Send Data\r\n");
但是忘记:
xSemaphoreGive(UartMutex);
那么:
Task1获得Mutex
↓
一直没有释放
↓
Task2等待Mutex
↓
一直阻塞
这种问题看起来很像:
Task2突然不运行了。
实际上它只是一直在等待别人没有释放的资源。
因此工程中应该保持:
Take
↓
使用资源
↓
Give
成对出现。
八、临界区里面执行太多代码
例如:
taskENTER_CRITICAL();
ReadSensor();
ProcessData();
printf("Data OK\r\n");
SaveData();
taskEXIT_CRITICAL();
这种写法非常不推荐。
临界区主要用于保护:
必须连续完成的短操作。
临界区时间过长会影响:
中断响应
系统实时性
因此应该尽量做到:
进入临界区
↓
只处理必要代码
↓
尽快退出
不要为了"安全",把一大段程序全部包进临界区。
九、建立几个基本工程习惯
写FreeRTOS工程时,可以逐渐形成下面几个习惯:
任务
↓
必须考虑什么时候阻塞
优先级
↓
根据实时性设置
共享资源
↓
考虑Mutex保护
中断
↓
检查是否应该使用FromISR API
任务创建
队列创建
信号量创建
↓
检查返回值
任务栈
↓
通过High Water Mark验证
临界区
↓
越短越好
这些习惯看起来都比较简单,但是实际项目中很多FreeRTOS问题恰恰就出现在这些地方。
十、总结
这一节集中整理了FreeRTOS工程中最容易出现的几个问题。
最常见的包括:
任务while(1)不阻塞
优先级随意设置
中断中使用普通API
任务栈设置不合理
创建对象不检查结果
共享资源没有保护
Mutex获取后忘记释放
临界区时间过长
以后写完一个任务,可以简单检查:
这个任务什么时候阻塞?
优先级真的需要这么高吗?
有没有和其他任务共享资源?
栈空间够不够?
有没有在中断中错误调用普通API?
FreeRTOS工程真正稳定,并不是因为使用了很多:
队列
信号量
事件组
任务通知
而是:
每个任务职责明确
任务该阻塞时能够阻塞
共享资源正确保护
中断和任务边界清楚
系统资源能够被监控
把这些基本规范做好以后,程序规模逐渐增加时才不容易出现难以定位的问题。