从最开始创建一个LED任务,到现在理解任务调度、队列、信号量、任务通知、内存管理以及FreeRTOS工程架构,这个系列已经走到了最后一篇。
刚开始学习FreeRTOS时,我们面对的可能只是:
xTaskCreate();
然后产生一个疑问:
为什么写几个while(1),
STM32却能让它们一起运行?
现在再回头看,FreeRTOS其实并没有那么神秘。
它做的核心事情可以概括成:
把程序拆成多个任务
↓
根据优先级进行调度
↓
任务之间传递数据和同步
↓
合理管理CPU和内存资源
本节不再增加新的知识点,而是把整个系列重新串起来,形成一套完整的FreeRTOS知识框架。
一、FreeRTOS最核心的东西还是任务
我们最开始学习的是:
xTaskCreate();
例如:
xTaskCreate(
LEDTask,
"LED",
128,
NULL,
2,
&LEDTaskHandle
);
创建以后:
LEDTask
就成为FreeRTOS管理的一个任务。
任务通常写成:
void LEDTask(void *pvParameters)
{
while(1)
{
GPIO_ToggleBits(
GPIOF,
GPIO_Pin_9
);
vTaskDelay(
pdMS_TO_TICKS(500)
);
}
}
这里最重要的并不是:
while(1)
而是:
vTaskDelay();
它让当前任务:
Running
↓
Blocked
然后CPU可以运行其他任务。
所以学习到最后应该建立一个很重要的习惯:
写任务的时候,一定要考虑这个任务什么时候阻塞。
二、任务调度其实就是CPU在不断切换
STM32F407只有一个Cortex-M4内核。
因此:
Task1
Task2
Task3
并不是真的同时执行。
实际过程更接近:
Task1运行
↓
保存Task1现场
↓
Task2运行
↓
保存Task2现场
↓
Task3运行
FreeRTOS根据:
任务优先级
任务状态
系统Tick
决定:
下一刻CPU应该交给谁。
而STM32F407上的:
SysTick
主要提供系统时间节拍。
PendSV
主要负责上下文切换。
SVC
参与启动第一个任务。
这就是我们整个任务调度体系最底层的一条主线。
三、任务之间需要传数据就想到队列
例如:
SensorTask
采集数据
↓
Queue
↓
UartTask
发送数据
核心函数:
xQueueSend();
和:
xQueueReceive();
例如:
xQueueSend(
SensorQueue,
&Data,
portMAX_DELAY
);
消费者:
xQueueReceive(
SensorQueue,
&Data,
portMAX_DELAY
);
队列不仅能够:
传递数据
还能够:
暂时保存数据。
所以以后遇到:
一个任务产生数据
另一个任务处理数据
首先就可以考虑:
Queue。
四、任务之间需要通知就考虑同步机制
不是所有情况都需要传递一大块数据。
有时候只是:
任务A完成了
↓
告诉任务B可以开始了
这种情况下可以考虑:
二值信号量
任务通知
例如:
ADC采集完成
↓
Give
↓
处理任务运行
如果只是一个任务通知另一个任务:
Task Notification
往往更加轻量。
如果需要表示:
多个事件状态
例如:
WiFi连接完成
传感器初始化完成
服务器连接完成
则可以考虑:
Event Group。
所以不要看到同步就只会:
Semaphore。
而应该根据具体需求选择合适的工具。
五、共享资源就想到互斥量
如果两个任务都要使用:
UART
SPI
I2C
例如:
Task1
↘
UART
↗
Task2
就需要考虑:
资源竞争。
这时可以使用:
xSemaphoreCreateMutex();
访问资源:
xSemaphoreTake(
UartMutex,
portMAX_DELAY
);
/* 使用UART */
xSemaphoreGive(
UartMutex
);
互斥量解决的核心问题就是:
这个资源我正在使用
↓
其他任务先等待。
而且FreeRTOS的Mutex还有:
优先级继承
机制,可以降低优先级翻转带来的影响。
六、中断和任务之间要分清边界
STM32项目不可能只有任务。
还有:
UART中断
定时器中断
外部中断
DMA中断
我们学习过一个非常重要的原则:
中断
↓
尽量快速处理
复杂工作
↓
交给任务
例如UART收到数据:
UART IRQ
↓
通知任务
↓
退出中断
↓
UartTask处理数据
在ISR中使用FreeRTOS API时,还要注意:
FromISR
版本。
例如:
xQueueSendFromISR();
而不是直接使用:
xQueueSend();
这也是FreeRTOS实际工程中非常重要的一条边界。
七、FreeRTOS还需要管理RAM
FreeRTOS不是:
任务随便创建多少都可以。
每创建一个任务,都需要:
TCB
+
任务栈
队列、信号量等对象同样需要内存。
所以我们学习了:
heap_1
heap_2
heap_3
heap_4
heap_5
也学习了:
xPortGetFreeHeapSize();
检查剩余Heap。
以及:
uxTaskGetStackHighWaterMark();
检查任务栈。
以后看到:
程序运行一段时间HardFault
任务莫名其妙异常
除了检查代码逻辑,也应该想到:
任务栈是不是不够?
八、一个实际FreeRTOS工程怎么设计
以后拿到一个STM32项目,不要上来就创建十几个任务。
可以先把功能列出来。
例如:
传感器采集
数据处理
电机控制
MQTT通信
状态检测
然后分析:
哪些功能需要独立周期运行?
哪些功能需要等待事件?
哪些功能实时性比较高?
哪些功能需要传递数据?
哪些功能共享同一个外设?
最后再决定:
创建哪些任务
↓
任务优先级是多少
↓
使用Queue还是Notification
↓
哪些资源需要Mutex
所以FreeRTOS工程设计的正确顺序不是:
先创建任务
↓
再想任务干什么
而应该是:
分析功能
↓
划分任务
↓
确定任务关系
↓
选择通信同步方式
↓
设置优先级
↓
运行并测试
九、整个系列最终可以归纳成一张图
学完这个系列以后,可以把FreeRTOS理解成下面这套框架:
FreeRTOS
│
┌──────────┴──────────┐
│ │
任务管理 任务通信
│ │
xTaskCreate() Queue
vTaskDelay() Semaphore
Suspend/Resume Mutex
Priority Event Group
│ Task Notification
│ │
└──────────┬──────────┘
│
系统资源
│
Stack / Heap
│
FreeRTOSConfig
│
调度器
│
SysTick / PendSV / SVC
│
STM32F407
如果以后某个知识点忘记了,不需要把整个FreeRTOS重新学习一遍。
只需要先判断:
这个问题属于任务?
通信?
同步?
内存?
调度?
然后再去查对应API即可。
十、最终总结
从这个系列开始到现在,我们已经依次学习了:
任务创建与延时
优先级与时间片
任务句柄
挂起、恢复和删除
任务状态与优先级修改
Idle Task与系统Tick
队列
二值信号量与计数信号量
互斥量与优先级继承
事件组
任务通知
软件定时器
流缓冲区与消息缓冲区
中断安全API
临界区与调度器锁
heap_1到heap_5
任务栈与栈溢出检测
Hook函数与运行时间统计
Tickless Idle
FreeRTOSConfig
调试与追踪
CMSIS-RTOS
生产者消费者模型
多任务同步
系统架构设计
FreeRTOS移植与调度原理
工程规范
性能优化
真正需要掌握的并不是把这些API全部背下来。
最重要的是建立这样一种思维:
任务什么时候运行?
什么时候应该阻塞?
数据应该怎样传递?
任务应该怎样同步?
共享资源应该怎样保护?
中断应该做多少事情?
RAM到底够不够?
任务优先级是否合理?
当你开始主动考虑这些问题的时候,就已经不再只是:
会调用FreeRTOS函数
而是在真正按照:
RTOS的方式设计嵌入式程序。
最后再回到我们最开始的两个LED任务:
xTaskCreate(
LED1Task,
"LED1",
128,
NULL,
2,
NULL
);
xTaskCreate(
LED2Task,
"LED2",
128,
NULL,
2,
NULL
);
vTaskStartScheduler();
最开始看到这些代码时,我们只是知道:
它能够创建两个任务。
现在再看,背后已经能够想到:
任务栈
↓
TCB
↓
Ready状态
↓
优先级
↓
SysTick
↓
调度器
↓
PendSV
↓
上下文切换
↓
CPU轮流执行两个任务
这就是这个FreeRTOS系列真正需要建立起来的完整认识。
至此,FreeRTOS中文学习系列全部完成。