FreeRTOS学习(三十七)——常见错误与工程规范

上一节学习了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工程真正稳定,并不是因为使用了很多:

复制代码
队列

信号量

事件组

任务通知

而是:

复制代码
每个任务职责明确

任务该阻塞时能够阻塞

共享资源正确保护

中断和任务边界清楚

系统资源能够被监控

把这些基本规范做好以后,程序规模逐渐增加时才不容易出现难以定位的问题。

相关推荐
小席是个热心肠2 小时前
Redis的自我学习
数据库·redis·学习
zyf1044162 小时前
暑期实践日志 Day33:完成全部字幕添加,工作基本收尾
学习·计算机网络·剪辑·暑期实践·课题任务
Chris _data3 小时前
WPF 上位机开发学习笔记 - 第四天
笔记·学习·wpf
HY小宝F3 小时前
树莓派 FFmpeg 实战笔记(二):命令行、源码编译与硬件编码的真相
学习·职场和发展·ffmpeg
2601_967264284 小时前
Jetpack Compose 实践指南:从入门到进阶
java·学习
金字塔頂の蝸牛4 小时前
商务日语口语实用手册
笔记·学习笔记·日语·外语
小雪崩5 小时前
嵌入式学习 day25:哈希表及排序与查找
linux·c语言·数据结构·学习·排序算法
yiqiefeimeng5 小时前
C语言指针难倒90%人?买房比喻让你瞬间开窍
c语言·学习·编程·指针·比喻
GHL2842710905 小时前
Skill学习
学习·ai