FreeRTOS学习记录
2024-12-28 16:06:32 # 嵌入式

FreeRTOS就是为了复杂流程而生的。


契机

参加了机赛,自动分拣项目需要一个复杂的流程。代码实在不是很优雅。想到了现在熟视无睹的FreeRTOS,或许能发挥点作用呢,于是潜心学习去了。

资源

我是通过一下几个渠道学习的。之前看过一个csdn的教程,上来就解释一堆宏和函数用法,着实看不懂。
我觉得最舒服的学习方法是:

有了需求之后,结合自己的需求去看官方的文档和推荐的资料
看的不用很细,详略得当地看,应用时再来细细研究
同时可以从别人的项目那里看实际应用例子,充实自己的认知

官方网站
掌握FreeRTOS文档
csdn上的一些项目

关于发行和堆

对于FreeRTOS的移植,因为CubeMX里面已经移植了FreeRTOS了,所以不是很有必要去深究。
但是可以关注的是config.h,来开关一些功能。
值得注意的是CubeMX所采用的是CMSIS OS的版本,对FreeRTOS进行了封装,这就是为什么osDelayvTaskDelay是同一个东西的原因。
至于heap的内存管理,我只知道heap4是最好用的版本。

任务

在死循环里放你想执行的任务,配合各种事件来做出相应,如队列,信号量,事件组等

任务状态

阻塞:等待事件(时间事件也算,如Delay)到达,激活后切成就绪
挂起:用resume来恢复
就绪:根据优先级来判断是否运行
运行:就是运行

时间测量和Delay

设置FreeRTOS的时候,一般会开一个用定时器作为滴答时间测量的,相当于生产时间戳,Delay就是通过这个实现的
每一个滴答间隔之间都有一个滴答中断,来决定下一个滴答运行哪个任务
延时函数一种是vTaskDelay,另一种是vTaskDelayUntil,区别大概是后者更精确吧,这个有待了解

调度方式

时间分片:相同优先级的在相邻滴答周期交替进行
抢占式:FreeRTOS的灵魂

优先级

可以通过函数改变自身或通过句柄他人的优先级

相关函数

任务函数本身
创建函数
删除函数
延时函数
优先级相关函数


队列

作为在堆的一个内核对象,它复制数据到堆里,负责任务间传输数据
问题来了,为啥不用extern呢

发送与接收

结合优先级,写入和读取各有阻塞,可以实现一些有意思的操作。

如读取任务优先级高于写入任务,则可以保证队列里只有一个数据,因为一写入,接受任务就会抢占,读取并删除数据,这时进入阻塞状态,等待写入任务发送数据。
交换优先级可以让队列永远是满的。
如果有两个发送任务优先级相等,而接受任务高于他俩,则会交替发送,一发完就接受。

结合上述知识,队列可以帮助实现流程中的任务间数据转移,或任务“通知”。

多源和指针

可以通过发送结构体(包含id和数据)来让接受任务区分是哪个发送者发出的。
可以通过发送指针,来发送不同类型和长度的数据

队列集

这个可以看成是多源和指针的高级加强版,可以有信号量和队列两种元素在队列集里面。
这有一个很实用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/* 从中接收字符指针的队列句柄。 */
QueueHandle_t xCharPointerQueue;

/* 接收 uint32_t 值的队列句柄。 */
QueueHandle_t xUint32tQueue;

/* 二进制信号量的句柄。 */
SemaphoreHandle_t xBinarySemaphore;

/* 两个队列和二进制信号量所属的队列集。 */
QueueSetHandle_t xQueueSet;

void vAMoreRealisticReceiverTask( void *pvParameters )
{
QueueSetMemberHandle_t xHandle;
char *pcReceivedString;
uint32_t ulRecievedValue;
const TickType_t xDelay100ms = pdMS_TO_TICKS( 100 );

for( ;; )
{
/* 在队列集中阻塞最长100毫秒,以等待队列集中的一个成员包含数据。*/
xHandle = xQueueSelectFromSet( xQueueSet, xDelay100ms);

/* 测试从 xQueueSelectFromSet() 返回的值。如果返回值为空,则对
xQueueSelectFromSet() 的调用超时。如果返回值不为空,则返回值将是集合成员之一的句柄。
QueueSetMemberHandle_t 值可以转换为 QueueHandle_t 或 SemaphoreHandle_t。是否需
要显式转换取决于编译器。 */

if( xHandle == NULL )
{
/* 对 xQueueSelectFromSet() 的调用超时。 */
}
else if( xHandle == ( QueueSetMemberHandle_t ) xCharPointerQueue )
{
/* 对 xQueueSelectFromSet() 的调用返回了接收字符指针的队列句柄。从队列中读取。已
知队列包含数据,因此使用阻塞时间 0。*/
xQueueReceive(xCharPointerQueue, &pcReceivedString, 0 );

/* 这里可以处理接收到的字符指针... */
}
else if( xHandle == ( QueueSetMemberHandle_t ) xUint32tQueue )
{
/* 对 xQueueSelectFromSet() 的调用返回了接收 uint32_t 类型的队列句柄。从队列中
读取。已知队列包含数据,因此使用 0 的阻塞时间。 */
xQueueReceive(xUint32tQueue, &ulRecievedValue, 0 );

/* 接收到的值可以在这里处理... */
}
else if( xHandle == ( QueueSetMemberHandle_t ) xBinarySemaphore )
{
/* 对 xQueueSelectFromSet() 的调用返回了二进制信号量的句柄。现在拿旗语。信号量已知
可用,因此使用 0 的阻塞时间。*/
xSemaphoreTake(xBinarySemaphore, 0 );

/* 获取信号量时需要的任何处理都可以在这里执行... */
}
}
}

邮箱

这只是仅有单个数据的队列。
但是使用不同的函数让数据不会被读完删除,而是只读,写入覆盖这样。

相关函数

队列创建函数
发送到头或尾的函数
接收函数
返回队列项目数的函数
队列集创建,加入,择取
覆写,只读不删函数


定时器

定时器由一个回调函数和定时器实例(包括定时器命令队列,守护进程任务)组成。
具体机制是:发出定时器命令(如开启,重置等)到命令队列里面,守护进程任务会检查这个队列,并执行相应的回调函数。
守护进程任务的优先级最好设置的高一点,虽然较低也不会影响它执行回调函数,估计是用到了滴答间隔的滴答中断来检查定时器列表。

特性

可以运行和休眠
有单次和周期性定时器
定时器有ID来辨识身份
可以手动改变周期或者重置

想法

底盘任务里判断是否到达指定位置:即定期检查里程误差,就可以用定时器回调和重置的功能

相关函数

创建函数
开始,停止函数
重置或改变周期函数
获取滴答计数函数
设置ID查询ID函数
回调函数


资源管理

原子操作:一种不想被打断的连续操作,就像开大时外面电话响了,你得完事才出去接吧。
专业一点,就是线程安全,访问堆栈上的变量是安全的,但是多个函数都可以访问一个全局/静态存储区里的变量是不安全的(static变量)。这个我了解不是很清楚。

临界区

基本临界区是由调用宏taskENTER_critical()taskEXIT_CRITICAL()包围的代码区域,里面存放原子操作。
vTaskSuspendAll() xTaskResumeAll()挂起的是调度器,从而禁止任务切换。
临界区也有中断版本的,都中断嵌套。

互斥锁

也称为二进制信号量,MUTEXES,这个就像绝命毒师里面的话语权抱枕,拿到它的人才可以说话。
xSemaphoreTake()获得互斥锁后,才会执行后面的代码。互斥锁必须给回去。
这会发生一个问题,叫优先级反转:高优先级的任务在等待持有互斥锁的低优先级任务。

死锁和递归互斥锁,后者解决前者,这个我之后再写。

看门人任务

这个也可以达到保护资源的目的。只有看门人任务可以直接访问资源,其他人需要通过看门人间接访问资源。可以使用队列来通知看门人任务事件到达。

滴答钩子函数

这一节多介绍了滴答钩子函数,调度器在滴答间隔马上执行这个钩子函数。所以必须要快,用ISR版的API函数。

相关函数

临界区的两个宏
挂起所有和恢复所有函数
创建,拿取,返回互斥锁函数
滴答钩子函数


中断和信号量

在中断中使用ISR这种安全的API接口

中断与任务切换

任务切换在这个文档里也有一种说法是上下文切换。
关于pxHigherPriorityTaskWoken这个参数,它决定了中断里面发生的一些事件(如发送队列)使关联的高优先级任务进入就绪状态后————会不会立马运行。设置这个参数还有些技巧,一开始得是pdFALSE,再变成pdTRUE才可以被读到。
这个参数一般在类似xSemaphoreGiveFromISR()的函数中,这个函数在下文会提到。
portYIELD_FROM_ISR的宏和pxHigherPriorityTaskWoken参数是配合使用的,调用这个宏即可进行任务切换。

信号量和中断

延迟中断处理的意思是,因为中断要尽可能短,所以设置一个任务跟在中断屁股后面给它做任务,这就需要结合上一节所说的参数和宏,和信号量Semaphore
核心思想是,先创造一个二进制信号量;然后在中断那里给出,同时唤醒高优先级任务;这个延时处理任务拿到信号量,进行处理,达到“与中断同步”的效果。

关于中断的还有好多

估计一时半会用不到,先不写了。

相关函数

一堆


事件组

特性:可以多个事件组合发生,可以在阻塞状态下等待一个事件或多个

事件组设置和等待

读取

用掩码设置和读取。

1
2
3
4
/*"事件组中事件位的定义。 */
#define mainFIRST_TASK_BIT ( 1UL << 0UL ) /* 事件位0,由任务设置。 */
#define mainSECOND_TASK_BIT ( 1UL << 1UL ) /* 事件位1,由任务设置。 */
#define mainISR_BIT ( 1UL << 2UL ) /* 事件位2,由ISR设置。 */

等待

等待时是阻塞的。
等待有xWaitForAllBitsxClearOnExit的模式,且可以设置超时时间。

同步任务

可以通过事件组来同步多个任务到一个同步点。
没细看,以后写。