前言
废话不多说,直接看主要要探究的问题:
一、临界段代码保护
1.什么是临界段?
图里面说,临界区的代码是不能被打断的,它运行时不能被中断打断,也不能由于非阻塞任务延时而切换到其他任务去。
比如说IIC进行通信时,软件模拟IIC会有一个4-8us的延时,说明通信时序非常重要,这里一旦被打断就会有影响。
如果我写项目,任务一需要利用IIC通信测量MPU6050加速度值,任务二需要每0.5秒上报一次数据,那我的任务一会被任务二的调度而打断,即使事后仍能回到打断处,但是对于时序要求极高的IIC通信而言,肯定会造成出错。
那应该怎样才能使临界区程序不被打断呢?
2.临界段代码不被中断打断的方法
对临界区代码的保护本质上就是关中断、开中断
在第三节中断管理中,谈到了开关中断的函数:
这与临界段保护的开关中断函数不一样,但是本质是一样的。
3.看一个实例
其实实例很简单,而且之前也提到过,就是写开始任务时,要创建任务,必须先进入临界区,在退出临界区。
taskENTER_CRITICAL() 宏用于进入临界区,它会将任务优先级设置为最高,禁止所有中断,保证在临界区的代码不会被其他任务或中断打断。
taskEXIT_CRITICAL() 宏用于退出临界区,它会将任务优先级恢复到原来的值,并允许中断,让其他任务或中断可以继续执行。
为什么这么做呢?第二节我写过,比如如果不这样做,你创建一个task1和task2,在刚创建好task1并且还没创建好task2时,task1就会因为优先级高而先被执行了,万一task1不分享时间片,那岂不是task2根本就不会被运行?为了防止这种任务提前抢占当前任务的情况,那就得用临界区代码保护功能,使得task1和2全被初始化后,在按照优先级老老实实运行。
二、任务调度器的挂起和恢复
1.前言
当创建好任务后,总是需要开启任务调度器来进行任务的切换:
vTaskStartScheduler();
那么,任务调度器同样也会有挂起和恢复的状态。
2.任务调度器挂起和恢复的API函数
首先要知道中断与任务(调度)切换的区别:
①触发方式:中断是由外部事件触发的,如硬件设备的输入、定时器溢出等;而任务切换是由操作系统内部的调度器触发的,它根据一定的调度策略来决定切换到哪个任务。
②执行环境:中断是在中断上下文中执行的,它会暂停当前运行的任务,保存中断现场,执行中断服务程序,最后恢复现场返回到原来的任务。而任务切换是在任务上下文中执行的,它会保存当前任务的上下文,切换到另一个任务的上下文继续执行。
③调度开销:中断的调度开销比任务切换要小,因为中断服务程序通常很短,只需要保存和恢复现场即可;而任务切换需要保存和恢复多个寄存器和堆栈,因此开销相对较大。
④调度优先级:中断的优先级通常比任务的优先级高,因为中断需要尽快响应外部事件;而任务的优先级则由操作系统内部的调度器决定,根据任务的重要性和紧急程度来分配优先级。
通俗地说,我创建了两个task,均是每隔0.5s闪烁一次led,同时,我又初始化了一个外部中断+按键,按下会使得task1挂起。那么,两个task之间的调度属于任务调度,而外部中断、定时器溢出中断、串口接收中断等等均属于中断,优先级更高。
挂起任务调度器, 调用此函数不需要关闭中断
这句话的意思是说:它仅仅是防止了任务之间的资源争夺,中断照样可以直接响应。挂起调度器的方式,适用于临界区位于任务与任务之间;既不用去延时中断,又可以做到临界区的安全。
它是怎么保护临界区程序的呢?很简单,把任务调度器ban了,使得可能占据程序资源的任务根本不会来抢占了。但是也有缺点,就是外部中断照样有可能打断临界区程序。
这就要看你的临界区程序具体的形式和所处的位置来决定采用开关中断或者ban任务调度器。
;临界区代码保护和任务调度器挂起的总结图:
三、列表与列表项
前面各种地方都在说列表,什么就绪任务列表、任务挂起列表等等,那列表到达是个什么东西呢?这个概念是非常重要的,如果只看重外部实现,那浅浅了解,但是要深入去看freertos内核源码是怎么实现的,列表是重中之重!对于理解freertos的运行机制很有帮助。
1.列表和列表项的定义
(1)列表:列表是 FreeRTOS 中的一个数据结构,概念上和链表有点类似,列表被用来跟踪 FreeRTOS中的任务。数据结构上说,链表(Linked List)是一种常见的线性数据结构,用于存储一系列数据元素。链表中的每个元素由一个节点(Node)表示,每个节点包含一个数据元素和指向下一个节点的指针。通过指针,可以将各个节点连接起来,形成一个链式结构。
(2)列表项:列表项就是存放在列表中的项目。
(3)示意图:
列表项就是列表中的节点。它是一个环形的列表,是一个双向的环形链表。
举个例子:
三个人组成了一个环形的链表结构,我们知道链表是用指针指向节点的,这里比如小明是一个节点,它的身体就是数据元素,而他的右手就是一个指向下一位节点小黑的指针,他的左手就是指向上一位节点小红的指针。
假设我这时突然要在小明和小红之间插进来一个小美,那就让小美的左右手跟小明、小红拉上即可,而要删除一个节点,同理。
所以这种列表结构为什么适合freertos呢?
(1)列表的特点:列表项间的地址非连续的,是人为的连接到一起的。列表项的数目是由后期添加的个数决定的,随时可以改变。我可以随意添加和删除其中的节点,每个节点表示一个任务,包含了任务的状态、优先级、堆栈等信息。通过遍历链表,可以快速访问所有的任务,并根据调度算法选择下一个要执行的任务。
(2)动态分配内存:链表可以根据需要动态分配内存,可以灵活地增加或删除任务,不需要事先知道任务列表的大小。
(3)高效的插入和删除:链表的插入和删除操作只需要修改指针,时间复杂度为 O(1)。
(4)空间利用率高:链表只需要存储任务的指针和状态信息,不需要像数组那样预留空间,因此空间利用率更高。
2.列表相关结构体
list.c就是freertos源码中对于列表的配置。
①列表结构体
这里进行解释:
(1)在该结构体中, 包含了两个宏,
listFIRST_LIST_INTEGRITY_CHECK_VALUE
listSECOND_LIST_INTEGRITY_CHECK_VALUE
这两个宏是确定的已知常量, FreeRTOS通过检查这两个常量的值,
来判断列表的数据在程序运行过程中,是否遭到破坏 ,该功能一般用于调试, 默认是不开启的,其实不重要,可以不看。
(2)
volatile UBaseType_t uxNumberOfItems;
成员uxNumberOfItems,用于记录列表中列表项的个数(不包含 xListEnd)
(3)成员 pxIndex 用于指向列表中的某个列表项,一般用于遍历列表中的所有列表项
ListItem_t * configLIST_VOLATILE pxIndex;
(4)成员变量 xListEnd 是一个迷你列表项,排在最末尾
MiniListItem_t xListEnd;
另外,看这个结构图:
列表项是列表中用于存放数据的地方,在 list.h 文件中,有列表项的相关结构体定义。每一个列表项代表一个任务。
②列表项结构体
而列表项的结构成员如下:
1、成员变量 xItemValue 为列表项的值,这个值多用于按升序对列表中的列表项进行排序
2、成员变量 pxNext 和 pxPrevious 分别用于指向列表中列表项的下一个列表项和上一个列表项
3、成员变量 pxOwner 用于指向包含列表项的对象(通常是任务控制块)
4、成员变量 pxContainer 用于指向列表项所在列表,有了这个变量,就可以知道任务目前属于哪个状态的了。
③迷你列表项结构体
3.列表和列表项的关系
列表的初始状态:**在 FreeRTOS 中,列表的初始状态是空的,即列表中没有任何列表项。此时,列表的头部和尾部都是指向同一个特殊的标记 xListEnd 的指针,并且列表项数量为 0。**如图:
插入两个列表项:假设我们要向列表中插入两个列表项 A 和 B。插入操作一般包括以下几个步骤:
(1) 创建列表项:首先需要创建两个列表项 A 和 B,分别用 ListItem_t 结构体类型定义,可以使用 pvPortMalloc() 函数在堆上动态分配内存。
(2) 初始化列表项:对于每个列表项,需要设置其 xItemValue 值、pvOwner 指针、pxNext 指针和 pxPrevious 指针等信息。其中,xItemValue 可以理解为列表项的优先级,pvOwner 指向该列表项的拥有者,pxNext 和 pxPrevious 指向该列表项的下一个和上一个列表项。
(3) 插入列表项:将列表项 A 插入列表的头部,将列表项 B 插入列表的尾部。插入操作需要修改前后列表项的 pxNext 和 pxPrevious 指针,将其指向新的列表项。同时,需要修改列表的头部和尾部指针,将其指向插入后的新的头部和尾部。
(4) 更新列表项数量:每次插入或删除列表项后,需要更新列表中的列表项数量 uxNumberOfItems。
插入两个列表项后,列表的状态如下:
+------------------------------------------------+
| xListEnd |
|------------------------------------------------|
| pxPrevious = &B | | pxPrevious = &A |
|------------------------------------------------|
| pxNext = &A | | pxNext = &B |
|------------------------------------------------|
| xItemValue = 0 | | xItemValue = 1 |
|------------------------------------------------|
| pvOwner | | pvOwner |
+------------------------------------------------+|v+--------+| List |+--------+pxIndex = &AuxNumberOfItems = 2
其中,xListEnd 表示列表的结尾,A 和 B 分别为列表项,List 表示列表,pxIndex 表示用于遍历列表项的指针,uxNumberOfItems 表示列表中的列表项数量,pxNext 和 pxPrevious 分别表示列表项的下一个和上一个列表项。
4.列表相关API函数
①初始化列表
图中说的很清楚,就是对列表结构体的配置。初始化时,列表中只有 xListEnd,因此 pxIndex 指向 xListEnd。xListEnd 的值初始化为最大值,用于列表项升序排序时,排在最后。初始化时,列表中只有 xListEnd,因此上一个和下一个列表项都为 xListEnd 本身。初始化时,列表中的列表项数量为 0(不包含 xListEnd)。
②初始化列表项
函数参数为指向要初始化的列表项的指针 pxItem,函数没有返回值。
函数的作用是将列表项的 pxNext 和 pxPrevious 指针都设置为 NULL,将 xItemValue 和 pvOwner 值都设置为 0,表示将列表项初始化为空。
该函数在插入新的列表项时经常使用,因为在插入列表项之前需要先将其初始化为空。例如,可以使用以下代码初始化列表项:
ListItem_t xItem;
vListInitialiseItem( &xItem );
这样就可以将 xItem 的 pxNext 和 pxPrevious 指针都设置为 NULL,将 xItemValue 和 pvOwner 值都设置为 0,表示将列表项初始化为空。然后可以将 xItem 插入到列表中,如通过调用 vListInsert() 函数将其插入到列表的头部或尾部。
③列表项插入函数(升序插入)
升序插入是指按照列表项的 xItemValue 值进行排序,将新的列表项插入到正确的位置。**列表项数值越大,插入的顺序就越靠后。**升序插入可以通过调用 vListInsert() 函数实现,例如:
ListItem_t xItemA, xItemB;
xItemA.xItemValue = 1;
xItemB.xItemValue = 2;
vListInitialiseItem( &xItemA );
vListInsert( &xList, &xItemA );
vListInsert( &xList, &xItemB );
在此例子中,先创建了两个列表项 xItemA 和 xItemB,分别设置其 xItemValue 值为 1 和 2,并使用 vListInitialiseItem() 函数初始化这两个列表项。然后,将 xItemA 插入到列表中,接着将 xItemB 插入到列表中。由于 xItemB 的 xItemValue 值比 xItemA 大,因此会将 xItemB 插入到 xItemA 的后面,最终列表中的顺序为 A->B。
④列表项插入函数(末尾插入)
注意:函数vListInsertEnd(),是将待插入的列表项插入到列表 pxIndex 指针指向的列表项前面;看的就是pxIndex 指针,其他不要看。现在详细地进行讲解:
(1)它的参数为指向要插入列表的指针 pxList和待插入列表项。
(2)例一:
首先,index指针指向末尾列表项,这时插入值为30的列表项2,
那么画一个层次图:
要插入的列表项是在index指针指向的列表项的前一个地方。
(3)例二:
这时index指针指向值为40的列表项1,那么:
⑤移除列表项函数
函数的作用是从列表中删除指定的列表项,并返回删除的列表项数量。该函数是通过将要删除的列表项的 pxPrevious 指针的 pxNext 指向要删除的列表项的 pxNext 指针,将要删除的列表项的 pxNext 指针的 pxPrevious 指向要删除的列表项的 pxPrevious 指针来实现的。
5.列表项的插入和删除实战例程
目的:
这里其实只用看核心任务代码就行:
List_t TestList; /* 定义测试列表 */
ListItem_t ListItem1; /* 定义测试列表项1 */
ListItem_t ListItem2; /* 定义测试列表项2 */
ListItem_t ListItem3; /* 定义测试列表项3 *//* 任务二,列表项的插入和删除实验 */
void task2( void * pvParameters )
{vListInitialise(&TestList); /* 初始化列表 */vListInitialiseItem(&ListItem1); /* 初始化列表项1 */vListInitialiseItem(&ListItem2); /* 初始化列表项2 */vListInitialiseItem(&ListItem3); /* 初始化列表项3 */ListItem1.xItemValue = 40;ListItem2.xItemValue = 60;ListItem3.xItemValue = 50;/* 第二步:打印列表和其他列表项的地址 */printf("/**************第二步:打印列表和列表项的地址**************/\r\n");printf("项目\t\t\t地址\r\n");printf("TestList\t\t0x%p\t\r\n", &TestList);printf("TestList->pxIndex\t0x%p\t\r\n", TestList.pxIndex);printf("TestList->xListEnd\t0x%p\t\r\n", (&TestList.xListEnd));printf("ListItem1\t\t0x%p\t\r\n", &ListItem1);printf("ListItem2\t\t0x%p\t\r\n", &ListItem2);printf("ListItem3\t\t0x%p\t\r\n", &ListItem3);printf("/**************************结束***************************/\r\n");printf("\r\n/*****************第三步:列表项1插入列表******************/\r\n");vListInsert((List_t* )&TestList, /* 列表 */(ListItem_t*)&ListItem1); /* 列表项 */printf("项目\t\t\t\t地址\r\n");printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem1.pxNext));printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem1.pxPrevious));printf("/**************************结束***************************/\r\n");/* 第四步:列表项2插入列表 */printf("\r\n/*****************第四步:列表项2插入列表******************/\r\n");vListInsert((List_t* )&TestList, /* 列表 */(ListItem_t*)&ListItem2); /* 列表项 */printf("项目\t\t\t\t地址\r\n");printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem1.pxNext));printf("ListItem2->pxNext\t\t0x%p\r\n", (ListItem2.pxNext));printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem1.pxPrevious));printf("ListItem2->pxPrevious\t\t0x%p\r\n", (ListItem2.pxPrevious));printf("/**************************结束***************************/\r\n");/* 第五步:列表项3插入列表 */printf("\r\n/*****************第五步:列表项3插入列表******************/\r\n");vListInsert((List_t* )&TestList, /* 列表 */(ListItem_t*)&ListItem3); /* 列表项 */printf("项目\t\t\t\t地址\r\n");printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem1.pxNext));printf("ListItem2->pxNext\t\t0x%p\r\n", (ListItem2.pxNext));printf("ListItem3->pxNext\t\t0x%p\r\n", (ListItem3.pxNext));printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem1.pxPrevious));printf("ListItem2->pxPrevious\t\t0x%p\r\n", (ListItem2.pxPrevious));printf("ListItem3->pxPrevious\t\t0x%p\r\n", (ListItem3.pxPrevious));printf("/**************************结束***************************/\r\n");/* 第六步:移除列表项2 */printf("\r\n/*******************第六步:移除列表项2********************/\r\n");uxListRemove((ListItem_t* )&ListItem2); /* 移除列表项 */printf("项目\t\t\t\t地址\r\n");printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem1.pxNext));printf("ListItem3->pxNext\t\t0x%p\r\n", (ListItem3.pxNext));printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem1.pxPrevious));printf("ListItem3->pxPrevious\t\t0x%p\r\n", (ListItem3.pxPrevious));printf("/**************************结束***************************/\r\n");/* 第七步:列表末尾添加列表项2 */printf("\r\n/****************第七步:列表末尾添加列表项2****************/\r\n");TestList.pxIndex = &ListItem1;vListInsertEnd((List_t* )&TestList, /* 列表 */(ListItem_t* )&ListItem2); /* 列表项 */printf("项目\t\t\t\t地址\r\n");printf("TestList->pxIndex\t\t0x%p\r\n", TestList.pxIndex);printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem1.pxNext));printf("ListItem2->pxNext\t\t0x%p\r\n", (ListItem2.pxNext));printf("ListItem3->pxNext\t\t0x%p\r\n", (ListItem3.pxNext));printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem1.pxPrevious));printf("ListItem2->pxPrevious\t\t0x%p\r\n", (ListItem2.pxPrevious));printf("ListItem3->pxPrevious\t\t0x%p\r\n", (ListItem3.pxPrevious));printf("/************************实验结束***************************/\r\n");while(1){vTaskDelay(1000);}
}
这是一个使用 FreeRTOS 列表实现列表项的插入和删除的示例任务。该任务通过初始化列表、列表项和设置列表项的值,然后将列表项插入到列表中,并打印插入列表项前后的列表和列表项的地址,最后从列表中删除指定的列表项。
具体操作如下:
初始化列表和列表项:使用 vListInitialise() 和 vListInitialiseItem() 函数分别初始化列表和列表项。
打印列表和其他列表项的地址:使用 printf() 函数打印列表和其他列表项的地址,以便后面插入列表项时进行对比。
列表项1插入列表:使用 vListInsert() 函数将列表项1插入到列表中,打印插入列表项前后的列表和列表项的地址,以便对比。
列表项2插入列表:使用 vListInsert() 函数将列表项2插入到列表中,打印插入列表项前后的列表和列表项的地址,以便对比。
列表项3插入列表:使用 vListInsert() 函数将列表项3插入到列表中,打印插入列表项前后的列表和列表项的地址,以便对比。
移除列表项2:使用 uxListRemove() 函数将列表项2从列表中移除,打印移除列表项后的列表和列表项的地址,以便对比。
列表末尾添加列表项2:使用 vListInsertEnd() 函数将列表项2插入到列表的末尾,打印插入列表项前后的列表和列表项的地址,以便对比。
看运行结果:
首先是第二步:注意这时列表里面内容为空,只有末尾列表项。
然后第三步升序插入列表项1:
就像手牵手一样,列表项1的next指向末尾列表项,末尾列表项的previous指向前一个即列表项1.后续的思路都差不多。这里我不把结果截图出来了。
最后总结一下列表和列表项的作用:
列表和列表项的主要作用如下:
任务调度:FreeRTOS 中的任务调度器使用列表和列表项来实现任务的调度。每个任务都有一个列表项,任务调度器根据列表项的优先级和状态来决定下一个要执行的任务。
事件通知:FreeRTOS 中的事件通知机制也使用列表和列表项来实现。每个事件都有一个列表项,当事件发生时,可以将该事件对应的列表项插入到一个等待列表中,等待被唤醒。
存储数据:列表也可以用于存储数据,例如 FreeRTOS 中的消息队列就是使用列表来存储消息的。