FreeRTOS学习笔记——FreeRTOS 列表和列表项

要想看懂FreeRTOS 源码并学习其原理,有一个东西绝对跑不了,那就是FreeRTOS 的列表和列表项。列表和列表项是FreeRTOS 的一个数据结构,FreeRTOS 大量使用到了列表和列表项,它是FreeRTOS 的基石。要想深入学习并理解FreeRTOS,那么列表和列表项就必须首先掌握,否则后面根本就没法进行。本章我们就来学习一下FreeRTOS 的列表和列表项,包括对列表和列表项的操作,本章分为如下几部分:
7.1 什么是列表和列表项
7.2 列表和列表项的初始化
7.3 列表项的插入
7.4 列表项末尾插入
7.5 列表项的删除
7.6 列表项的遍历

7.7 列表项的插入和删除实验

7.1 什么是列表和列表项?
7.1.1 列表
列表是FreeRTOS 中的一个数据结构,概念上和链表有点类似,列表被用来跟踪FreeRTOS中的任务。与列表相关的全部东西都在文件list.c 和list.h 中。在list.h 中定义了一个叫List_t 的结构体,如下:


(1) 和(5) 、这两个都是用来检查列表完整性的, 需要将宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 设置为1,开启以后会向这两个地方分别添加一个变量xListIntegrityValue1 和xListIntegrityValue2,在初始化列表的时候会这两个变量中写入一个特殊的值,默认不开启这个功能。以后我们在学习列表的时候不讨论这个功能!
(2)、uxNumberOfItems 用来记录列表中列表项的数量。
(3)、pxIndex 用来记录当前列表项索引号,用于遍历列表。
(4)、列表中最后一个列表项,用来表示列表结束,此变量类型为MiniListItem_t,这是一个迷你列表项,关于列表项稍后讲解。列表结构示意图如图7.1.1.1 所示:


注意!图7.1.1.1 中并未列出用于列表完整性检查的成员变量。
7.1.2 列表项
列表项就是存放在列表中的项目,FreeRTOS 提供了两种列表项:列表项和迷你列表项。这两个都在文件list.h 中有定义,先来看一下列表项,定义如下:



(1)和(7)、用法和列表一样,用来检查列表项完整性的。以后我们在学习列表项的时候不讨论这个功能!
(2)、xItemValue 为列表项值。
(3)、pxNext 指向下一个列表项。
(4)、pxPrevious 指向前一个列表项,和pxNext 配合起来实现类似双向链表的功能。
(5)、pvOwner 记录此链表项归谁拥有,通常是任务控制块。
(6)、pvContainer 用来记录此列表项归哪个列表。注意和pvOwner 的区别,在前面讲解任务控制块TCB_t 的时候说了在TCB_t 中有两个变量xStateListItem 和xEventListItem,这两个变量的类型就是ListItem_t,也就是说这两个成员变量都是列表项。以xStateListItem 为例,当创建一个任务以后xStateListItem 的pvOwner 变量就指向这个任务的任务控制块,表示xSateListItem属于此任务。当任务就绪态以后xStateListItem 的变量pvContainer 就指向就绪列表,表明此列表项在就绪列表中。举个通俗一点的例子:小王在上二年级,他的父亲是老王。如果把小王比作列表项,那么小王的pvOwner 属性值就是老王,小王的pvContainer 属性值就是二年级。列表项结构示意图如图7.1.2.1 所示:

注意!图7.1.2.1 中并未列出用于列表项完整性检查的成员变量!
7.1.3 迷你列表项
上面我们我们说了列表项,现在来看一下迷你列表项,迷你列表项在文件list.h 中有定义,如下:


(1)、用于检查迷你列表项的完整性。

(2)、xItemValue 记录列表列表项值。
(3)、pxNext 指向下一个列表项。
(4)、pxPrevious 指向上一个列表项。
可以看出迷你列表项只是比列表项少了几个成员变量,迷你列表项有的成员变量列表项都有的,没感觉有什么本质区别啊?那为什么要弄个迷你列表项出来呢?那是因为有些情况下我们不需要列表项这么全的功能,可能只需要其中的某几个成员变量,如果此时用列表项的话会造成内存浪费!比如上面列表结构体List_t 中表示最后一个列表项的成员变量xListEnd 就是MiniListItem_t 类型的。迷你列表项结构示意图如图7.1.3.1 所示:


注意!图7.1.3.1 中并未列出用于迷你列表项完整性检查的成员变量!
7.2 列表和列表项初始化
7.2.1 列表初始化
新创建或者定义的列表需要对其做初始化处理,列表的初始化其实就是初始化列表结构体List_t 中的各个成员变量,列表的初始化通过使函数vListInitialise()来完成,此函数在list.c 中有定义,函数如下:


(1)、xListEnd 用来表示列表的末尾,而pxIndex 表示列表项的索引号,此时列表只有一个列表项,那就是xListEnd,所以pxIndex 指向xListEnd。
(2)、xListEnd 的列表项值初始化为portMAX_DELAY, portMAX_DELAY 是个宏,在文件portmacro.h 中有定义。根据所使用的MCU 的不同,portMAX_DELAY 值也不相同,可以为0xffff或者0xffffffffUL,本教程中为0xffffffffUL。
(3)、初始化列表项xListEnd 的pxNext 变量,因为此时列表只有一个列表项xListEnd,因此pxNext 只能指向自身。

(4)、同(3)一样,初始化xListEnd 的pxPrevious 变量,指向xListEnd 自身。
(5)、由于此时没有其他的列表项,因此uxNumberOfItems 为0,注意,这里没有算xListEnd。
(6) 和(7) 、初始化列表项中用于完整性检查字段, 只有宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 为1 的时候才有效。同样的根据所选的MCU 不同其写入的值也不同,可以为0x5a5a 或者0x5a5a5a5aUL。STM32 是32 位系统写入的是0x5a5a5a5aUL,列表初始化完以后如图7.2.1.1 所示:


注意,图7.2.1.1 为了好分析,将xListEnd 中的各个成员变量都写了出来!
7.2.2 列表项初始化
同列表一样,列表项在使用的时候也需要初始化,列表项初始化由函数vListInitialiseItem()来完成,函数如下:


列表项的初始化很简单,只是将列表项成员变量pvContainer 初始化为NULL,并且给用于完整性检查的变量赋值。有朋友可能会问,列表项的成员变量比列表要多,怎么初始化函数就这么短?其他的成员变量什么时候初始化呢?这是因为列表项要根据实际使用情况来初始化,比如任务创建函数xTaskCreate()就会对任务堆栈中的xStateListItem 和xEventListItem 这两个列表项中的其他成员变量在做初始化,任务创建过程后面会详细讲解。
7.3 列表项插入
7.3.1 列表项插入函数分析

列表项的插入操作通过函数vListInsert()来完成,函数原型如下:


参数:
pxList: 列表项要插入的列表。
pxNewListItem: 要插入的列表项。
返回值:

函数vListInsert()的参数pxList 决定了列表项要插入到哪个列表中,pxNewListItem 决定了要插入的列表项,但是这个列表项具体插入到什么地方呢?要插入的位置由列表项中成员变量xItemValue 来决定。列表项的插入根据xItemValue 的值按照升序的方式排列!接下来我们来具体看一下函数vListInsert()的整个运行过程,函数代码如下:


(1)、获取要插入的列表项值,即列表项成员变量xItemValue 的值,因为要根据这个值来确定列表项要插入的位置。

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

(3)、要插入列表项,第一步就是要获取该列表项要插入到什么位置!如果要插入的列表项的值等于portMAX_DELAY,也就是说列表项值为最大值,这种情况最好办了,要插入的位置就是列表最末尾了。
(4)、获取要插入点,注意!列表中的xListEnd 用来表示列表末尾,在初始化列表的时候xListEnd 的列表值也是portMAX_DELAY,此时要插入的列表项的列表值也是portMAX_DELAY。这两个的顺序该怎么放啊?通过这行代码可以看出要插入的列表项会被放到xListEnd 前面。
(5)、要插入的列表项的值如果不等于portMAX_DELAY 那么就需要在列表中一个一个的找自己的位置,这个for 循环就是找位置的过程,当找到合适列表项的位置的时候就会跳出。由于这个for 循环是用来寻找列表项插入点的,所以for 循环体里面没有任何东西。这个查找过程是按照升序的方式查找列表项插入点的。
(6)、经过上面的查找,我们已经找到列表项的插入点了,从本行开始接下来的四行代码就是将列表项插入到列表中,插入过程和数据结构中双向链表的插入类似。像FreeRTOS 这种RTOS 系统和一些协议栈都会大量用到数据结构的知识,所以建议大家没事的时候多看看数据结构方面的书籍,否则的话看源码会很吃力的。
(7)、列表项已经插入到列表中了,那么列表项的成员变量pvContainer 也该记录此列表项属于哪个列表的了。
(8)、列表的成员变量uxNumberOfItems 加一,表示又添加了一个列表项。
7.3.2 列表项插入过程图示
上一小节我们分析了列表项插入函数vListInsert(),本小节我们就通过图片来演示一下这个插入过程,本小节我们会向一个空的列表中插入三个列表项,这三个列表项的值分别为40,60和50。
1、插入值为40 的列表项
在一个空的列表List 中插入一个列表值为40 的列表项ListItem1,插入完成以后如图7.3.2.1所示:


注意观察插入完成以后列表List 和列表项ListItem1 中各个成员变量之间的变化,比如列表List 中的uxNumberOfItems 变为了1,表示现在列表中有一个列表项。列表项ListItem1 中的pvContainer 变成了List,表示此列表项属于列表List。通过图7.3.2.1 可以看出,列表是一个环形的,即环形列表!

2、插入值为60 的列表项

接着再插入一个值为60 的列表项ListItem2,插入完成以后如图7.3.2.2 所示:


上面再讲解函数vListInsert()的时候说过了列表项是按照升序的方式插入的,所以ListItem2肯定是插入到ListItem1 的后面、xListEnd 的前面。同样的,列表List 的uxNumberOfItems 再次加一变为2 了,说明此时列表中有两个列表项。
3、插入值为50 的列表项

在上面的列表中再插入一个值为50 的列表项ListItem3,插入完成以后如图7.3.2.3 所示:


按照升序排列的方式,ListItem3 应该放到ListItem1 和ListItem2 中间,大家最好通过对照这三幅图片来阅读函数vListInsert()的源码,这样就会对函数有一个直观的认识。
7.4 列表项末尾插入
7.4.1 列表项末尾插入函数分析

列表末尾插入列表项的操作通过函数vListInsertEnd ()来完成,函数原型如下:


参数:
pxList: 列表项要插入的列表。

pxNewListItem: 要插入的列表项。

返回值:

函数vListInsertEnd()源码如下:


(1)、与下面的一行代码完成对列表和列表项的完整性检查。
(2)、从本行开始到(3)之间的代码就是将要插入的列表项插入到列表末尾。使用函数vListInsert()向列表中插入一个列表项的时候这个列表项的位置是通过列表项的值,也就是列表项成员变量xItemValue 来确定。vListInsertEnd()是往列表的末尾添加列表项的,我们知道列表中的xListEnd 成员变量表示列表末尾的,那么函数vListInsertEnd()插入一个列表项是不是就是插到xListEnd 的前面或后面啊?这个是不一定的,这里所谓的末尾要根据列表的成员变量pxIndex 来确定的!前面说了列表中的pxIndex 成员变量是用来遍历列表的,pxIndex 所指向的列表项就是要遍历的开始列表项,也就是说pxIndex 所指向的列表项就代表列表头!由于是个环形列表,所以新的列表项就应该插入到pxIndex 所指向的列表项的前面。
(3)、标记新的列表项pxNewListItem 属于列表pxList。
(4)、记录列表中的列表项数目的变量加一,更新列表项数目。
7.4.2 列表项末尾插入图示
跟函数vListInsert()一样,我们也用图片来看一下函数vListInsertEnd()的插入过程。
1、默认列表

在插入列表项之前我们先准备一个默认列表,如图7.4.2.1 所示:


注意图7.4.2.1 中列表的pxIndex 所指向的列表项,这里为ListItem1,不再是xListEnd 了。
3、插入值为50 的列表项

在上面的列表中插入一个值为50 的列表项ListItem3,插入完成以后如图7.4.2.2 所示:


列表List 的pxIndex 指向列表项ListItem1,因此调用函数vListInsertEnd()插入ListItem3 的话就会在ListItem1 的前面插入。

7.5 列表项的删除
有列表项的插入,那么必然有列表项的删除,列表项的删除通过函数uxListRemove()来完成,函数原型如下:


参数:
pxItemToRemove: 要删除的列表项。
返回值: 返回删除列表项以后的列表剩余列表项数目。
注意,列表项的删除只是将指定的列表项从列表中删除掉,并不会将这个列表项的内存给释放掉!如果这个列表项是动态分配内存的话。函数uxListRemove()的源码如下:


(1)、要删除一个列表项我们得先知道这个列表项处于哪个列表中,直接读取列表项中的成员变量pvContainer 就可以得到此列表项处于哪个列表中。
(2)、与下面一行完成列表项的删除,其实就是将要删除的列表项的前后两个列表项“连接”在一起。
(3)、如果列表的pxIndex 正好指向要删除的列表项,那么在删除列表项以后要重新给
pxIndex 找个“对象”啊,这个新的对象就是被删除的列表项的前一个列表项。
(4)、被删除列表项的成员变量pvContainer 清零。
(5)、返回新列表的当前列表项数目。
7.6 列表的遍历
介绍列表结构体的时候说过列表List_t 中的成员变量pxIndex 是用来遍历列表的,FreeRTOS提供了一个函数来完成列表的遍历,这个函数是listGET_OWNER_OF_NEXT_ENTRY()。每调用一次这个函数列表的pxIndex 变量就会指向下一个列表项,并且返回这个列表项的pxOwner变量值。这个函数本质上是一个宏,这个宏在文件list.h 中如下定义:



(1)、pxTCB 用来保存pxIndex 所指向的列表项的pvOwner 变量值,也就是这个列表项属于谁的?通常是一个任务的任务控制块。pxList 表示要遍历的列表。
(2)、列表的pxIndex 变量指向下一个列表项。
(3)、如果pxIndex 指向了列表的xListEnd 成员变量,表示到了列表末尾。
(4)、如果到了列表末尾的话就跳过xListEnd,pxIndex 再一次重新指向处于列表头的列表项,这样就完成了一次对列表的遍历。
(5)、将pxIndex 所指向的新列表项的pvOwner 赋值给pxTCB。此函数用于从多个同优先级的就绪任务中查找下一个要运行的任务。
7.7 列表项的插入和删除实验
上面我们通过分析源码的方式了解了FreeRTOS 的列表和列表项的操作过程,但是实际上究竟是不是这样的、我们的分析有没有错误?最好的检验方法就是写一段测试代码测试一下,观察在对列表和列表项操作的时候其变化情况和我们分析的是否一致。
7.7.1 实验程序设计
1、实验目的
学习使用FreeRTOS 列表和列表项相应的操作函数的使用,观察这些操作函数的运行结果和我们理论分析的是否一致。
2、实验设计
本实验设计3 个任务:start_task、task1_task 和list_task,这三个任务的任务功能如下:start_task:用来创建其他2 个任务。task1_task:应用任务1,控制LED0 闪烁,用来提示系统正在运行。task2_task: 列表和列表项操作任务,调用列表和列表项相关的API 函数,并且通过串口输出相应的信息来观察这些API 函数的运行过程。实验需要用到KEY_UP 按键,用于控制任务的运行。
3、实验工程

FreeRTOS 实验7-1 FreeRTOS 列表项的插入和删除实验。

点击下载代码

4、实验程序与分析
● 任务设置

实验中任务优先级、堆栈大小和任务句柄等的设置如下:


● 列表和列表项的定义


● main()函数


● 任务函数
任务函数start_task()和task1_task()都比较简单,这里为了缩减篇幅就不列出来了,重点看一下任务函数list_task(),函数如下:




任务函数list_task()通过调用与列表和列表项相关的API 函数来对列表和列表项做相应的操作,并且通过串口打印出每调用一个API 函数以后列表和列表项的连接信息,通过这些信息我们可以直观的判断出列表项在插入、删除和末尾插入的时候这个列表的变化情况。
7.7.2 程序运行结果分析
编译并下载实验代码到开发板中,打开串口调试助手,然后按照任务函数list_task()中的步骤一步步的测试分析。
● 第一步和第二步
第一步和第二步是用来初始化列表和列表项的,并且通过串口输出列表和列表项的地址,这一步是开发板复位后默认运行的,串口调试助手信息如下所示:


由于这些列表和列表项地址前六位都为0X200000,只有最低2 位不同,所以我们就用最低2 位代表这些列表和列表项的地址。注意!列表和列表项的地址在不同的硬件平台或编译软件上是不同的,以自己的实际实验结果为准!简单的分析一下图7.7.2.1 可以得到如下信息:
1、列表TestList 地址为b4。
2、列表项ListItem1、ListItem2 和ListItem3 的地址分别为c8、dc 和f0。
3、列表TestList 的xListEnd 地址为bc。
4、列表TestList 的pxIndex 指向地址bc,而这个地址正是迷你列表项xListEnd,说明pxIndex指向xListEnd,这个和我们分析列表初始化函数vListInitialise()的时候得到的结果是一致的。
● 第三步
按一下KEY_UP 键,执行第三步,第三步是向列表TestList 中插入列表项ListItem1,列表项ListItem1 的成员变量xItemValue 的值为40。第三步执行完以后串口调试助手输出如图7.7.2.2所示:


分析图7.7.2.2 可以得出一下信息:
1、xListEnd 的pxNext 指向地址c8,而c8 是ListItem1 的地址,说明xListEnd 的pxNext 指向ListItem1。
2、列表项ListItem1 的pxNext 指向地址bc,而bc 是xListEnd 的地址,说明ListItem1 的pxNext 指向xListEnd。
3、xListEnd 的pxPrevious 指向地址c8,而c8 是ListItem1 的地址,说明xListEnd 的pxPrevious指向ListItem2。
4、ListItem1 的pxPrevious 指向地址bc,bc 是xListEnd 的地址,说明ListItem1 的pxPrevious指向xListEnd。

用简易示意图表示这一步的话如图7.7.2.3 所示:


●第四步
按一下KEY_UP 键,执行第四步,第四步是向列表TestList 中插入列表项ListItem2,列表项ListItem2 的成员变量xItemValue 的值为60。第四步执行完以后串口调试助手输出如图7.7.2.4所示:


分析图7.7.2.4 可以得出一下信息:
1、xListEnd 的pxNext 指向ListItem1。
2、ListItem1 的pxNext 指向ListItem2。
3、ListItem2 的pxNext 指向xListEnd。
4、列表项的pxPrevious 分析过程类似,后面的步骤中就不做分析了,只看pxNext 成员变量。用简易示意图表示这一步的话如图7.7.2.5 所示:


●第五步
按一下KEY_UP 键,执行第五步,第五步是向列表TestList 中插入列表项ListItem3,列表项ListItem3 的成员变量xItemValue 的值为50。第四步执行完以后串口调试助手输出如图7.7.2.6所示:


分析图7.7.2.6 可以得出一下信息:
1、xListEnd 的pxNext 指向ListItem1。
2、ListItem1 的pxNext 指向ListItem3。
3、ListItem3 的pxNext 指向ListItem2。
4、ListItem2 的pxNext 指向xListEnd。

用简易示意图表示这一步的话如图7.7.2.7 所示:


通过这几步可以看出列表项的插入是根据xItemValue 的值做升序排列的,这个和我们分析函数vListInsert()得到的结果一样,说明我们的分析是对的!
●第六步和第七步
这两步是观察函数uxListRemove()和vListInsertEnd()的运行过程的,分析过程和前五步一样。这里就不做分析了,大家自行根据串口调试助手输出的信息做分析。


  • 9
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值