基于STM32F103的FreeRTOS系列(七)·任务创建·列表的使用超详细解析

目录

[1. 列表和列表项](#1. 列表和列表项)

[1.1 列表和列表项简介](#1.1 列表和列表项简介)

[1.1.1 列表](#1.1.1 列表)

[1.1.2 列表项](#1.1.2 列表项)

[1.1.3 迷你列表项](#1.1.3 迷你列表项)

[1.1.4 列表与列表项关系图](#1.1.4 列表与列表项关系图)

[1.2 列表初始化](#1.2 列表初始化)

[1.3 列表项的初始化](#1.3 列表项的初始化)

[1.4 列表项的插入函数](#1.4 列表项的插入函数)

[1.5 列表项的末尾插入](#1.5 列表项的末尾插入)

[1.6 列表项的删除](#1.6 列表项的删除)

[1.7 列表的遍历](#1.7 列表的遍历)


1. 列表和列表项

1.1 列表和列表项简介

1.1.1 列表

是 FreeRTOS 中的一个数据结构,概念上和链表有点类似,列表被用来跟踪 FreeRTOS中的任务。每个列表项有前驱结点指针prev,同时又有后继结点指针next,这样,双向循环链表的增删改查非常方便,动态改变,节省内存!

C语言菜鸟入门·数据结构·链表超详细解析-CSDN博客

cs 复制代码
typedef struct xLIST
{
    	listFIRST_LIST_INTEGRITY_CHECK_VALUE			//①校验值
    	volatile UBaseType_t uxNumberOfItems;			//②列表中的列表项数量
   		ListItem_t * configLIST_VOLATILE pxIndex		//③用于遍历列表项的指针
    	MiniListItem_t xListEnd							//④末尾列表项
    	listSECOND_LIST_INTEGRITY_CHECK_VALUE			//⑤校验值
} List_t;

在上述结构体中, 包含了两个宏,这两个宏是确定的已知常量,FreeRTOS 通过检查这两个常量的值,来判断列表的数据在程序运行过程中,是否遭到破坏(数据的完整性) ,该功能一般用于调试。因为默认是不开启的,所以我们并没有用到这两个宏(如上①和⑤)。

成员 uxNumberOfItems,用于记录列表中列表项的个数(不包含 xListEnd)。

注意:xListEnd 就是排在某位的。我们的列表里面有很多列表项,可以挂载很多列表项,但是 xListEnd 这个列表项总是排在最底,也就是最末尾的位置。记住这个特性就行了。

成员 pxIndex 用于指向列表中的某个列表项,一般用于遍历列表中的所有列表项。

成员变量 xListEnd 是一个迷你列表项,排在最末尾,我们也称它为末尾列表项。


1.1.2 列表项

就是存放在列表中的项目 。意思它就是列表的子集。每一个列表项关联着一个任务,在列表项里面有一个成员变量,用来存放任务控制块,描述这个任务的相关属性信息。

cs 复制代码
struct xLIST_ITEM
{
    	listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE			/* ①用于检测列表项的数据完整性,我们并没有使用 */
    	configLIST_VOLATILE TickType_t xItemValue			/* ②列表项的值 */
     	struct xLIST_ITEM * configLIST_VOLATILE pxNext		/* ③下一个列表项 */
  		struct xLIST_ITEM * configLIST_VOLATILE pxPrevious	/* ④上一个列表项 */
    	void * pvOwner										/* ⑤列表项的拥有者 */
    	struct xLIST * configLIST_VOLATILE pxContainer; 	/* ⑥列表项所在列表 */
   		listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE			/* ⑦用于检测列表项的数据完整性,我们并没有使用 */
};
typedef struct xLIST_ITEM ListItem_t; 	

同列表该子集目录下的①和⑦用于检测列表项的数据完整性,我们并没有使用,一般情况下不用去管它;

②是代表列表项的值;

③和④通过指向下一个列表项和上一个列表项可以实现类似于双向链表的作用;

⑤记录列表项归谁拥有的;创建了这个列表,他就会指向这个这个任务的任务控制块,表示任务控制块的任务节点ListItem_t xStateListItem;归任务所有。

⑥记录列表项归哪一个列表。指向了就绪列表。


1.1.3 迷你列表项

有些情况下,不需要列表项那么全的功能,可以使用迷你列表项,省内存。迷你列表项仅用于标记列表的末尾和挂载其他插入列表中的列表项,挂载如何理解:列表初始化的时候,只有一个迷你列表项,当插入新的列表项时,就把新的列表项对应的两只手(前驱指针和后继指针)牵上迷你列表项即可!迷你列表项没有存储数据,不会存储实际的任务数据信息,因此不需要成员变量 pxOwner 和 pxContainer,以节省内存开销!

struct xMINI_LIST_ITEM
{
    listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE (1)
    configLIST_VOLATILE TickType_t xItemValue; (2)
    struct xLIST_ITEM * configLIST_VOLATILE pxNext; (3)
    struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; (4)
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

(1)不使用;

(2)代表列表项的值;

(3)指向下一个列表项;

(4)指向上一个列表项。


1.1.4 列表与列表项关系图

1.2 列表初始化

初始化列表就是为列表的结构体成员赋初值,此时列表只有一个末尾列表项。

void vListInitialise( List_t * const pxList )
{
    pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); (1)
    pxList->xListEnd.xItemValue = portMAX_DELAY; (2)
    pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); (3)
    pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd ); (4)
    pxList->uxNumberOfItems = ( UBaseType_t ) 0U; (5)
    listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList ); (6)
    listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList ); (7)
}

下面我们来逐步分析:

pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );

xListEnd 用来表示列表的末尾,而 pxIndex 表示列表项的索引号,此时列表只有一个列表项,那就是 xListEnd,所以 pxIndex 指向 xListEnd。


pxList->xListEnd.xItemValue = portMAX_DELAY; 

xListEnd 的列表项值初始化为 portMAX_DELAY。

需要注意: portMAX_DELAY 是个宏,在文件portmacro.h 中有定义。根据所使用的 MCU 的不同, portMAX_DELAY 值也不相同,可以为 0xffff或者 0xffffffffUL,本教程中为 0xffffffffUL。


pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );

初始化列表项 xListEnd 的 pxNext 变量,因为此时列表只有一个列表项 xListEnd,因此 下一个指定的列表项pxNext 只能指向自身。


pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );

同理,初始化 xListEnd 的 pxPrevious 变量,指向 xListEnd 自身。


pxList->uxNumberOfItems = ( UBaseType_t ) 0U;

初始化时,列表中的列表项数量为0,不包含末尾列表项xListEnd


    listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList ); 
    listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList ); 

用于检测列表数据完整性的校验值,只有当指定的宏定义为1,才会使用到,平常情况下我们并没有使用到。

1.3 列表项的初始化

cs 复制代码
void vListInitialiseItem( ListItem_t * const pxItem )
{
    //初始化 pvContainer 为 NULL
    pxItem->pvContainer = NULL; 
    //初始化用于完整性检查的变量,如果开启了这个功能的话。
    listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
    listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
}

列表项的初始化很简单,只是将列表项成员变量 pvContainer 初始化为 NULL,并且给用于完整性检查的变量赋值。

在这里我们有个疑问:列表项的成员变量比列表要多,怎么初始化函数就这么短?其他的成员变量什么时候初始化呢?

这是因为列表项要根据实际使用情况来初始化,比如任务创建函数 xTaskCreate()就会对任务堆栈中的 xStateListItem 和 xEventListItem 这两个列表项中的其他成员变量在做初始化

1.4 列表项的插入函数

|------|---------------|-----------|
| void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem ) |||
| 参数: | pxList | 列表项要插入的函数 |
| 参数: | pxNewListItem | 要插入的列表项 |
| 返回值: | 无 | |

cs 复制代码
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
    ListItem_t *pxIterator;
    const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; //获取要插入的列表项值,即列表项成员变量 xItemValue 的值,因为要根据这个值来确定列表项要插入的位置

    listTEST_LIST_INTEGRITY( pxList ); //用来检查列表和列表项的完整性的。其实就是检查列表和列表项中用于完整性检查的变量值是否被改变。这些变量的值在列表和列表项初始化的时候就被写入了,这两行代码需要实现函数 configASSERT()

    listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );

    //要插入列表项,第一步就是要获取该列表项要插入到什么位置!如果要插入的列表项的值等于 portMAX_DELAY,也就是说列表项值为最大值, 这种情况最好办了,要插入的位置就是列表最末尾了
    if( xValueOfInsertion == portMAX_DELAY ) 
    {
        //获取要插入点
        pxIterator = pxList->xListEnd.pxPrevious; //注意!列表中的 xListEnd 用来表示列表末尾,在初始化列表的时候xListEnd的列表值也是portMAX_DELAY, 此时要插入的列表项的列表值也是portMAX_DELAY。这两个的顺序该怎么放啊?通过这行代码可以看出要插入的列表项会被放到 xListEnd 前面
    }
    else
    {
        //要插入的列表项的值如果不等于 portMAX_DELAY 那么就需要在列表中一个一个的找自己的位置,这个 for 循环就是找位置的过程,当找到合适列表项的位置的时候就会跳出。由于这个 for 循环是用来寻找列表项插入点的,所以 for 循环体里面没有任何东西。 这个查找过程是按照升序的方式查找列表项插入点的
        for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->\ 
        pxNext->xItemValue <=xValueOfInsertion; pxIterator = pxIterator->pxNext )
        {
        //空循环,什么也不做!
        }
    }
    //将列表项插入到列表中, 插入过程和数据结构中双向链表的插入类似
    pxNewListItem->pxNext = pxIterator->pxNext; 
    pxNewListItem->pxNext->pxPrevious = pxNewListItem;
    pxNewListItem->pxPrevious = pxIterator;
    pxIterator->pxNext = pxNewListItem;
    pxNewListItem->pvContainer = ( void * ) pxList; //列表项已经插入到列表中了,那么列表项的成员变量 pvContainer 也该记录此列表项属于哪个列表的了
    ( pxList->uxNumberOfItems )++; // 列表的成员变量 uxNumberOfItems 加一,表示又添加了一个列表项
}

上图片来理解一下:

首先,我们现规定一个列表,此时列表项的数值uxNumberOfItems=0;

初始化列表项 xListEnd 的 pxNext 变量,因为此时列表只有一个列表项 xListEnd,因此 下一个指定的列表项pxNext 只能指向自身,初始化 xListEnd 的 pxPrevious 变量,指向 xListEnd 自身:

然后我们在该列表内插入一个列表项,值为40,此时可以看到此时uxNumberOfItems=1,代表有一个列表项,此时pxNext和pxPrevious指向下一个列表项,而下一个列表项的pxNext和pxPrevious指向xListEnd,形成类似于环形链表的结构,并且此时pvContainer=List代表此列表项ListItem1是List的列表项:

然后我们在该列表内在插入一个列表项,值为60:

我们将上图简化:

可以配合这样图,列表类似于链表,每个列表都有指向下一个结点的pxNext,也都有指向上一个结点的pxPrevious,这样形成了类似于双向链表的结构:

然后我们在该列表内在插入一个列表项,值为50,因为列表是按照升序排列的方式,那么50就需要在40和60之间:

这里可以去看一下数据结构的双向链表,更容易理解。

1.5 列表项的末尾插入

|------|---------------|-----------|
| void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem ) |||
| 参数: | pxList | 列表项要插入的函数 |
| 参数: | pxNewListItem | 要插入的列表项 |
| 返回值: | 无 | |

cs 复制代码
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
    ListItem_t * const pxIndex = pxList->pxIndex;

    //与下面的一行代码完成对列表和列表项的完整性检查
    listTEST_LIST_INTEGRITY( pxList ); 
    listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );

    //将要插入的列表项插入到列表末尾
    pxNewListItem->pxNext = pxIndex; 
    pxNewListItem->pxPrevious = pxIndex->pxPrevious;
    mtCOVERAGE_TEST_DELAY();
    pxIndex->pxPrevious->pxNext = pxNewListItem;
    pxIndex->pxPrevious = pxNewListItem;

    //标记新的列表项 pxNewListItem 属于列表 pxList
    pxNewListItem->pvContainer = ( void * ) pxList; 

    //记录列表中的列表项数目的变量加一,更新列表项数目
    ( pxList->uxNumberOfItems )++; 
}

1.6 列表项的删除

cs 复制代码
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
    //得到此列表项处于哪个列表中
    List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer; 

    //与下面一行完成列表项的删除,其实就是将要删除的列表项的前后两个列表项"连接"在一起
    pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; 
    pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;

    mtCOVERAGE_TEST_DELAY();
    if( pxList->pxIndex == pxItemToRemove )
    {
        pxList->pxIndex = pxItemToRemove->pxPrevious; //如果列表的 pxIndex 正好指向要删除的列表项,那么在删除列表项以后要重新给pxIndex 找个"对象"啊,这个新的对象就是被删除的列表项的前一个列表项
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
    pxItemToRemove->pvContainer = NULL; //被删除列表项的成员变量 pvContainer 清零
    ( pxList->uxNumberOfItems )--;
    return pxList->uxNumberOfItems; //返回新列表的当前列表项数目
}

1.7 列表的遍历

每调用一次这个函数列表的 pxIndex 变量就会指向下一个列表项,并且返回这个列表项的 pxOwner变量值。 这个函数本质上是一个宏,这个宏在文件 list.h 中如下定义:

cs 复制代码
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )               \ (1)
{                                                                  \
    List_t * const pxConstList = ( pxList );                       \
    ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;   \ (2)
    if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )\ (3)
    {                                                                         \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ (4)
}                                                             \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;                         \ (5)
}

(1)pxTCB 用来保存 pxIndex 所指向的列表项的 pvOwner 变量值,也就是这个列表项属于谁的?通常是一个任务的任务控制块。 pxList 表示要遍历的列表。

(2)列表的 pxIndex 变量指向下一个列表项。

(3)如果 pxIndex 指向了列表的 xListEnd 成员变量,表示到了列表末尾。

(4)如果到了列表末尾的话就跳过 xListEnd, pxIndex 再一次重新指向处于列表头的列表项,这样就完成了一次对列表的遍历。

(5)将 pxIndex 所指向的新列表项的 pvOwner 赋值给 pxTCB。

FreeRTOS_时光の尘的博客-CSDN博客

相关推荐
零炻大礼包几秒前
久久派搭建风电系统网站(基于mariadb数据库)
数据库·嵌入式硬件·mariadb
深圳恒森宇电子1 小时前
AD15B 适用于单节干电池 升压LED驱动芯片 小电流芯片
嵌入式硬件·元器件
鼹鼠SDN5 小时前
微软RD客户端 手机 平板 远程控制 Windows桌面
windows·科技·智能手机·远程工作·数码
合宙Luat8 小时前
合宙LuatOS开发板Core_Air780EP使用说明
单片机·嵌入式硬件·物联网·硬件工程·lua
手打猪大屁8 小时前
STM32——串口通信(发送/接收数据与中断函数应用)
经验分享·笔记·stm32·单片机·嵌入式硬件
伍心9 小时前
004: VTK读入数据---vtkImageData详细说明
c++·windows·microsoft·mfc·软件工程·visual studio
yyqzjw10 小时前
【STM32】cubemx配置GPIO
stm32·单片机·嵌入式硬件
RT-Thread物联网操作系统10 小时前
睿赛德科技携手先楫共创RISC-V生态|RT-Thread EtherCAT主从站方案大放异彩
大数据·人工智能·科技·物联网·risc-v
weixin_3497909513 小时前
python脚本源码如何使用PyOxidizer编译Windows可执行文件
开发语言·windows·python
嵌入式大圣13 小时前
单片机工程师:创新与挑战之路
单片机·嵌入式硬件