个人名片:
🎓作者简介:嵌入式领域优质创作者
🌐个人主页:妄北y📞个人QQ:2061314755
💌个人邮箱:[mailto:2061314755@qq.com]
📱个人微信:Vir2025WBY🖥️个人公众号:科技妄北
🖋️本文为妄北y原创佳作,独家首发于CSDN🎊🎊🎊
💡座右铭:改造世界固然伟大,但改造自我更为可贵。
专栏导航:
妄北y系列专栏导航:
物联网嵌入式开发项目:大学期间的毕业设计,课程设计,大创项目,各种竞赛项目,全面覆盖了需求分析、方案设计、实施与调试、成果展示以及总结反思等关键环节。📚💼💡
QT基础入门学习:对QT的基础图形化页面设计进行了一个简单的学习与认识,利用QT的基础知识进行了翻金币小游戏的制作。🛠️🔧💭
Linux基础编程:初步认识什么是Linux,为什么学Linux,安装环境,进行基础命令的学习,入门级的shell编程。🍻🎉🖥️
深耕Linux应用开发:分享Linux的基本概念、命令行操作、文件系统、用户和权限管理等,网络编程相关知识,TCP/IP 协议、套接字(Socket)编程等,可以实现网络通信功能。常见开源库的二次开发,如libcurl、OpenSSL、json-c、freetype等💐📝💡
Linux驱动开发:Linux驱动开发是Linux系统不可或缺的组成部分,它专注于编写特殊的程序——驱动程序。这些程序承载着硬件设备的详细信息,并扮演着操作系统与硬件间沟通的桥梁角色。驱动开发的核心使命在于确保硬件设备在Linux系统上顺畅运作,同时实现与操作系统的无缝集成,为用户带来流畅稳定的体验。🚀🔧💻
Linux项目开发:Linux基础知识的实践,做项目是最锻炼能力的一个学习方法,这里我们会学习到一些简单基础的项目开发与应用,而且都是毕业设计级别的哦。🤸🌱🚀
非常期待与您一同在这个广阔的互联网天地里,携手探索知识的海洋,互相学习,共同进步。🌐💫🌱 熠熠星光,照亮我们的成长之路
✨✨ 欢迎订阅本专栏,对专栏内容任何问题都可以随时联系博主,共同书写属于我们的精彩篇章!✨✨
文章介绍:
📚本篇文章将深入剖析FreeRTOS学习的精髓与奥秘,与您一同分享相关知识!🎉🎉🎉
若您觉得文章尚可入目,期待您能慷慨地送上点赞、收藏与分享的三连支持!您的每一份鼓励,都是我创作路上源源不断的动力。让我们携手并进,共同奔跑,期待在顶峰相见的那一天,共庆辉煌!🚀🚀🚀
🙏衷心感谢大家的点赞👍、收藏⭐和评论✍️,您的支持是我前进的动力!
目录:
目录
一、为什么要学列表和列表项:
要深入理解 FreeRTOS 的源码及其工作原理,有一个关键部分是必不可少的,那就是 FreeRTOS 的列表和列表项。它们是 FreeRTOS 的基本数据结构,广泛用于系统的各个方面,构成了 FreeRTOS 的核心基础。掌握列表和列表项对于理解 FreeRTOS 的内部机制至关重要。
1.1 列表(List)
- 定义:列表是一个有序的集合,用于存储和管理列表项。列表结构为List_t,在文件list.h中有如下定义。
- 结构:列表通常包含一个头节点(List Head)和一个尾节点(List Tail),以及一些用于管理列表状态的变量,如列表项的数量等。
- 用途:列表在 FreeRTOS 中用于多种目的,例如任务调度、事件管理、信号量管理等。
typedef struct xLIST {
listFIRST_LIST_INTEGRITY_CHECK_VALUE // 校验列表完整性,确保列表未被损坏
configLIST_VOLATILE UBaseType_t uxNumberOfltems; // 列表项的数量
ListItem_t configLIST_VOLATILE *pxIndex; // 当前列表项目的索引
MiniListItem_t xListEnd; // 列表的末尾,表示此列表的变量类型
listSECOND_LIST_INTEGRITY_CHECK_VALUE // 第二次校验列表完整性
} List_t;
1.2 列表项(List Item)
- 定义:列表项是列表中的单个元素,可以代表一个任务、一个事件或其他任何需要管理的对象。列表项就是存放在列表中的项目,FreeRTOS提供了两种列表项:列表项和迷你列表项。这两个都在文件1ist.h中有定义。
- 结构:列表项包含指向前后列表项的指针,以及其他一些用于标识和管理该项的变量。
- 用途:列表项在 FreeRTOS 中用于表示任务控制块(TCB)、事件等,它们通过列表项在列表中的位置来决定其优先级或其他属性。
好的,以下是将注释翻译为中文后的代码:
```c
struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*< 如果 configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 被设置为 1,则设置为已知值。 */
configLIST_VOLATILE TickType_t xItemValue; /*< 被列出的值。在大多数情况下,这用于按降序排序列表。 */
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*< 指向列表中下一个 ListItem_t 的指针。 */
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*< 指向列表中上一个 ListItem_t 的指针。 */
void * pvOwner; /*< 指向包含该列表项的对象(通常是 TCB)的指针。因此在包含列表项的对象和列表项本身之间存在双向链接。 */
void * configLIST_VOLATILE pvContainer; /*< 指向放置该列表项的列表的指针(如果有的话)。 */
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /*< 如果 configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 被设置为 1,则设置为已知值。 */
};
typedef struct xLIST_ITEM ListItem_t; /* 出于某种原因,lint 希望这是两个单独的定义。 */
```
这样可以帮助中文读者更容易理解每个字段的用途。
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE
和listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE
用于完整性检查。xItemValue
是列表项的值。pxNext
指向下一个列表项。pxPrevious
指向前一个列表项,和pxNext
一起实现双向链表的功能。pvOwner
指向包含该列表项的对象(通常是任务控制块)。pvContainer
指向包含该列表项的列表。
pvContainer与pvOwner区别
pvContainer
用于记录该列表项属于哪个列表。需要注意的是,pvOwner
和 pvContainer
有不同的含义。在任务控制块(TCB)中,有两个变量 xStateListItem
和 xEventListItem
,它们的类型都是 ListItem_t
,即它们都是列表项。以 xStateListItem
为例,当创建一个任务后,xStateListItem
的 pvOwner
变量会指向该任务的任务控制块,表示 xStateListItem
属于这个任务。当任务进入就绪态时,xStateListItem
的 pvContainer
变量会指向就绪列表,表明此列表项位于就绪列表中。
用一个更通俗的例子来说明:假设小王在上二年级,他的父亲是老王。如果把小王比作列表项,那么小王的 pvOwner
属性值就是老王(即小王的所有者),而小王的 pvContainer
属性值就是二年级(即小王所在的位置或集合)。
简而言之:
pvOwner
指向拥有该列表项的对象(通常是任务控制块)。pvContainer
指向该列表项所在的列表。
1.3 迷你列表项:
迷你列表项在文件list.h中有定义:
// 定义一个小型链表项结构体
typedef struct xMINI_LIST_ITEM {
// 链表的完整性检查值
// 用于验证链表结构的完整性
uint32_t listFIRST; // (1)
// 列表项的值,类型为 TickType
TickType_t xItemValue; // (2)
// 指向下一个链表项的指针
struct xMINI_LIST_ITEM *pxNext; // (3)
// 指向前一个链表项的指针
struct xMINI_LIST_ITEM *pxPrevious; // (4)
} MiniListItem_t;
(1)listFIRST:链表完整性检查值,用于确保链表结构的完整性。
(2)xItemValue:链表项存储的值,类型为 TickType_t。
(3)pxNext:指向链表中下一个项的指针。
(4)pxPrevious:指向链表中前一个项的指针。
迷你列表项与标准列表项相比,只减少了几个成员变量。这使得迷你列表项在功能上与标准列表项相似,但在某些情况下,它提供了一种更轻量的选择。
引入迷你列表项的原因主要是为了在特定场景下减少内存占用。当我们只需要列表项的部分功能时,使用迷你列表项可以避免不必要的内存浪费。例如,在某些数据结构中,如列表结构体 `List_t` 中,最后一个列表项的成员变量 `xListEnd` 可以使用迷你列表项类型。这种方式不仅节省了内存,还提高了系统的效率。
1.4 关键函数和操作
- 插入和删除:FreeRTOS 提供了插入和删除列表项的函数,这些操作确保列表的有序性和一致性。
- 遍历:可以通过遍历列表来访问其中的每一个列表项,这在任务调度和事件处理中非常常见。
- 初始化和清理:列表和列表项在使用前需要进行初始化,使用完毕后需要进行清理,以避免内存泄漏或其他问题。
二、列表和列表项初始化
2.1 列表初始化
在创建或定义新的列表时,需要对其进行初始化。列表的初始化实际上是对列表结构体 List_t
中的各个成员变量进行设置。这个初始化过程可以通过调用函数 vListInitialise()
来完成,该函数在 list.c
文件中有具体的实现。
void vListInitialise( List_t * const pxList )
{
/* 列表结构体包含一个列表项,用于标记列表的结束。为了初始化列表,将列表结束项作为唯一的列表入口插入。 */
pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); /*lint !e826 !e740 为了节省内存,迷你列表结构用于作为列表的结束项。这个检查是有效的。 */
/* 列表结束项的值设置为列表中可能的最大值,以确保它始终位于列表的末尾。 */
pxList->xListEnd.xItemValue = portMAX_DELAY;
/* 列表结束项的下一个和上一个指针指向自身,以便我们知道列表是否为空。 */
pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); /*lint !e826 !e740 为了节省内存,迷你列表结构用于作为列表的结束项。这个检查是有效的。 */
pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );/*lint !e826 !e740 为了节省内存,迷你列表结构用于作为列表的结束项。这个检查是有效的。 */
pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
/* 如果 configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 被设置为 1,则为列表写入已知值。 */
listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList );
listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
}
pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );
将列表的索引指针
pxIndex
指向列表结束项xListEnd
,这意味着在初始化时,列表是空的,只有结束项。pxList->xListEnd.xItemValue = portMAX_DELAY;
将结束项的值设置为
portMAX_DELAY
,这是一个足够大的值,确保它在列表中始终位于最后。这种设计有助于在进行列表排序或检索时确保逻辑正确。pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
将结束项的
pxNext
和pxPrevious
指针都指向自身。这意味着在空列表状态下,pxNext
和pxPrevious
都指向同一项,方便后续的操作判定列表是否为空。pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
将当前列表项的数量初始化为0,表明列表此时是空的。
listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList ); listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
如果定义了数据完整性检查宏
configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES
,则写入已知值用于后续的完整性验证。这可以帮助在程序运行期间检测任何意外的内存损坏或错误。
2.2 列表项初始化:
同列表一样,列表项在使用的时候也需要初始化,列表项初始化由函数vListInitialiseItem()
来完成,函数如下:
void vListInitialiseItem( ListItem_t * const pxItem )
{
/* 确保列表项没有被记录为在列表中。 */
pxItem->pvContainer = NULL;
/* 如果 configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 被设置为 1,则为列表项写入已知值。 */
listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
}
列表项的初始化过程相对简单,仅需将成员变量 pvContainer
设置为 NULL
,并为完整性检查的变量赋值。有些朋友可能会疑惑,既然列表项的成员变量比列表要多,为什么初始化函数这么简短?其实,其他成员变量的初始化是根据具体的使用情况进行的。例如,在任务创建函数 xTaskCreate()
中,会对任务控制块中的 xStateListItem
和 xEventListItem
这两个列表项的其他成员变量进行初始化。关于任务创建过程的详细内容,我们将在后面的章节中深入探讨。
2.3 列表项插入
函数 vListInsert()
的目的通常是将一个新的列表项插入到指定的列表中。
void vListInsert(List_t * const pxList, ListItem_t * const pxNewListItem);
参数:
pxList
: 指向要插入列表项的目标列表的指针。
pxNewListItem
: 指向要插入的新的列表项的指针。
返回值:
无
void vListInsert(List_t* const pxList, ListItem_t* const pxNewListItem)
{
ListItem_t* pxIterator;
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;
// 仅在定义了 configASSERT() 时有效,这些测试可以捕捉到内存中被覆盖的列表数据结构。
// 它们无法捕捉因 FreeRTOS 的不正确配置或使用而导致的数据错误。
listTEST_LIST_INTEGRITY(pxList);
listTEST_LIST_ITEM_INTEGRITY(pxNewListItem);
// 将新的列表项插入到列表中,按 xItemValue 的顺序排序。
//
// 如果列表中已经包含具有相同项值的列表项,则新的列表项应放在其后。
// 这确保了存储在就绪列表中的任务控制块(TCB)能够共享 CPU。
// 但是,如果 xItemValue 与后标记相同,则下面的迭代循环将不会结束。
// 因此,首先检查值,并在必要时稍微修改算法。
if (xValueOfInsertion == portMAX_DELAY)
{
// 如果插入值是最大延迟值,则设置迭代器为列表的最后一个有效节点。
pxIterator = pxList->xListEnd.pxPrevious;
}
else
{
/* *** 注意 ***********************************************************
如果您发现应用程序在此处崩溃,则可能的原因如下。
此外,请参见 http://www.freertos.org/FAQHelp.html 获取更多提示,并确保定义了 configASSERT()!
http://www.freertos.org/a00110.html#configASSERT
1) 堆栈溢出 -
请参见 http://www.freertos.org/Stacks-and-stack-overflow-checking.html
2) 不正确的中断优先级分配,尤其是在 Cortex-M 部件上,数值较高的优先级值表示较低的实际中断优先级,这可能会导致困惑。
请参见 http://www.freertos.org/RTOS-Cortex-M3-M4.html 和 configMAX_SYSCALL_INTERRUPT_PRIORITY 的定义
http://www.freertos.org/a00110.html
3) 在临界区内调用 API 函数,或者在调度程序暂停时调用 API 函数,或者从中断中调用未以 "FromISR" 结尾的 API 函数。
4) 在队列或信号量初始化之前,或在调度程序启动之前使用它们(在 vTaskStartScheduler() 调用之前是否有中断触发?)。
**********************************************************************/
// 遍历列表,找到合适的插入位置
for (pxIterator = (ListItem_t*)&(pxList->xListEnd);
pxIterator->pxNext->xItemValue <= xValueOfInsertion;
pxIterator = pxIterator->pxNext) /*lint !e826 !e740 使用迷你列表结构作为列表尾部以节省 RAM。这是经过检查并且有效的。 */
{
// 这里没有任何操作,只是迭代到所需的插入位置。
}
}
// 将新的列表项插入到确定的位置
pxNewListItem->pxNext = pxIterator->pxNext;
pxNewListItem->pxNext->pxPrevious = pxNewListItem;
pxNewListItem->pxPrevious = pxIterator;
pxIterator->pxNext = pxNewListItem;
// 记住该列表项所在的列表。这将允许后续快速删除该项。
pxNewListItem->pvContainer = (void*)pxList;
// 增加列表中的项数
(pxList->uxNumberOfItems)++;
}
vListInsert
函数的功能是将一个新的列表项插入到一个已排序的双向链表中。
1. 参数定义:
List_t * const pxList
: 指向要插入的新项的目标链表。ListItem_t * const pxNewListItem
: 要插入的新项。
2. 数据完整性检查:
- 使用
listTEST_LIST_INTEGRITY
和listTEST_LIST_ITEM_INTEGRITY
函数检查链表和新项的完整性。这在开发中用于捕捉潜在的内存错误。
3. 插入逻辑:
- 获取要插入项的值
xValueOfInsertion
。 - 判断该值是否为
portMAX_DELAY
:- 如果是,设置迭代器
pxIterator
为列表的最后一个有效节点。这是因为在这种情况下,新项应该在最后插入。
- 如果是,设置迭代器
- 如果不是,则进入一个循环,查找合适的插入位置:
- 从列表尾部开始迭代,查找第一个
xItemValue
大于xValueOfInsertion
的位置。在找到合适位置之前,迭代器只需向后遍历链表。
- 从列表尾部开始迭代,查找第一个
4.插入新项:
- 将新项
pxNewListItem
插入到找到的位置,调整相邻节点的指针以保持链表的双向链接。 - 设置新项的
pvContainer
指针,以便将来可以轻松找到它所在的列表。
5. 更新列表项计数:
- 最后,增加链表中的项数
uxNumberOfItems
,以反映新项的插入。
6. 插入图解:
(1)在一个空的列表List中插入一个列表值为40的列表项ListItem1,插入完成以后如图所示:
在插入操作完成后,观察一下列表 List
和列表项 ListItem1
中各个成员变量的变化:
- 列表
List
中的uxNumberOfItems
变为 1,表明当前列表中包含一个列表项。 - 列表项
ListItem1
的pvContainer
成员被设置为List
,这表明该列表项属于列表List
。
通过图可以看到,列表的结构呈现为环形,即形成了一个环形链表的布局。
(2)接着再插入一个值为60的列表项ListItem2,插入完成以后如图所示:
a. 比较项值: ListItem2
的值会与 ListItem1
进行比较。如果 ListItem2
的值大于 ListItem1
的值,那么它就会按照顺序被插入在 ListItem1
的后面。
b. 更新指针:
ListItem2
的pxNext
指针会指向xListEnd
,而pxPrevious
指针会指向ListItem1
。ListItem1
的pxNext
指针会被更新为指向ListItem2
,确保链表的双向链接保持一致。
c. 更新列表状态:
- 列表
List
的uxNumberOfItems
再次加一,变为 2,表明此时列表中有两个列表项。
(3)在上面的列表中再插入一个值为50的列表项ListItem3,插入完成以后如图所示:
按照升序排列的方式,ListItem3应该放到ListIteml和ListItem2中间。
2.4 列表项末尾插入
vListInsertEnd
函数的作用是将新列表项 pxNewListItem
添加到指定列表 pxList
的末尾。这意味着不论当前列表中已有多少项,新的项都会被插入到最后一个有效节点之后,确保列表的顺序和结构不受影响。
void vListInsertEnd(List_t * const pxList, ListItem_t * const pxNewListItem);
参数:
pxList:
- 类型:
List_t * const
- 描述: 指向要插入新列表项的目标列表。这个列表应该已经被初始化并准备好接收新的列表项。
pxNewListItem:
- 类型:
ListItem_t * const
- 描述: 指向要插入的列表项。这是一个新的节点,它将被添加到
pxList
的末尾。
返回值:
空
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
// 获取当前列表的索引指针,通常指向列表的结束节点(xListEnd)
ListItem_t * const pxIndex = pxList->pxIndex;
/*
* 下面的完整性检查仅在定义了 configASSERT() 时有效,这些测试可以捕获
* 列表数据结构在内存中被覆盖的情况。它们不会捕获由于错误配置或
* 使用 FreeRTOS 导致的数据错误。
*/
listTEST_LIST_INTEGRITY( pxList ); // 检查列表的完整性
listTEST_LIST_ITEM_INTEGRITY( pxNewListItem ); // 检查新列表项的完整性
/*
* 将新列表项插入到 pxList 中,而不是对列表进行排序,
* 使新列表项成为最后一个被调用 listGET_OWNER_OF_NEXT_ENTRY() 移除的项。
*/
pxNewListItem->pxNext = pxIndex; // 新项的下一个指针指向当前索引(结束节点)
pxNewListItem->pxPrevious = pxIndex->pxPrevious; // 新项的前一个指针指向当前索引的前一个项
/*
* 仅在决策覆盖测试期间使用的覆盖测试延迟。
*/
mtCOVERAGE_TEST_DELAY(); // 模拟测试覆盖率的延迟
// 更新当前索引(结束节点)之前的项的下一个指针,使其指向新插入的项
pxIndex->pxPrevious->pxNext = pxNewListItem;
// 更新当前索引的前一个项指针,使其指向新插入的项
pxIndex->pxPrevious = pxNewListItem;
// 记录新列表项所属的列表
pxNewListItem->pvContainer = ( void * ) pxList;
// 列表中的项数加一
( pxList->uxNumberOfItems )++;
}
vListInsertEnd
函数的主要目的是将一个新的列表项 pxNewListItem
插入到指定的列表 pxList
的末尾。这是一个双向链表的操作,操作完成后,新的列表项将成为链表中的最后一项。
1. 获取索引指针:pxIndex
通常指向链表的结束节点(如 xListEnd
),此节点不包含有效数据,只作为链表的终止标记。
ListItem_t * const pxIndex = pxList->pxIndex;
2. 完整性检查:这两个宏用于检查列表及新列表项的完整性。这些检查有助于捕获潜在的内存损坏或不一致性,确保在插入操作前数据结构是有效的。
listTEST_LIST_INTEGRITY( pxList );
listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );
3. 设置新项的指针:设置新项的 pxNext
指针指向当前索引(结束节点),并将其 pxPrevious
指针指向当前索引的前一个项。这一步是插入操作的关键,确保新项能够正确链接到链表。
pxNewListItem->pxNext = pxIndex;
pxNewListItem->pxPrevious = pxIndex->pxPrevious;
4. 覆盖测试延迟:该行代码用于测试覆盖率,通常在单元测试中使用。它可以帮助开发人员确保代码的不同路径得到测试。
mtCOVERAGE_TEST_DELAY();
5. 更新指针:这里,首先更新 pxIndex
(结束节点)前一个项的 pxNext
指针,使其指向新插入的项。然后,更新 pxIndex
的 pxPrevious
指针,使其指向新的列表项。这样,链表的双向链接得以保持。
pxIndex->pxPrevious->pxNext = pxNewListItem;
pxIndex->pxPrevious = pxNewListItem;
6. 记录列表项所属的列表:此行代码存储新插入项所属的列表,使得该项可以在后续操作中快速访问其所属列表。
pxNewListItem->pvContainer = ( void * ) pxList;
7. 更新项计数:最后,增加列表中的项数,以反映新项的插入。这是确保列表状态一致的重要步骤。
( pxList->uxNumberOfItems )++;
8.插入图示:
(1)在插入列表项之前我们先准备一个默认列表,如图所示:
(2)在上面的列表中插入一个值为50的列表项ListItem3,插入完成以后如图:
列表List的pxIndex指向列表项ListIteml,因此调用函数VListInsertEnd()插入ListItem3的话就会在ListIteml的前面插入。
2.5 列表项的删除
有列表项的插入,那么必然有列表项的删除,列表项的删除通过函数uxListRemove()来完成,函数原型如下:
UBaseType_t uxListRemove(ListItem_t * const pxItemToRemove);
参数:
pxItemToRemove:
- 类型:
ListItem_t * const
- 描述: 指向要删除的列表项。调用者应确保该项是有效的并且已存在于列表中。
返回值:
- 返回类型:
UBaseType_t
- 描述: 函数返回删除列表项后列表中剩余的项数。这使得调用者能够了解列表的当前状态,并在需要时进行相应的操作。
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;
}
else
{
// 用于标记覆盖测试的代码
mtCOVERAGE_TEST_MARKER();
}
// 将该项的容器指针清空,以防止意外访问
pxItemToRemove->pvContainer = NULL;
// 减少列表项计数
( pxList->uxNumberOfItems )--;
// 返回剩余的项数
return pxList->uxNumberOfItems;
}
1. 获取列表:通过 pxItemToRemove
的 pvContainer
成员获取当前项所在的链表。这是一个重要的步骤,因为后续的操作需要知道要修改哪个列表。
List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer;
2. 更新指针:
这两行代码实现了链表的重链接:
将被删除项的前驱项的 pxNext 指针指向被删除项的后续项。
将被删除项的后续项的 pxPrevious 指针指向被删除项的前驱项。
通过这两步,成功地将被删除项从链表中移除,同时保持链表的完整性。
pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
3. 覆盖测试延迟:
mtCOVERAGE_TEST_DELAY();
4. 更新索引:
这里检查当前列表的索引是否指向被删除项。如果是,更新索引为指向被删除项的前驱项。这确保了即使删除了索引项,索引也始终指向有效的列表项。
mtCOVERAGE_TEST_MARKER()
是用来标记不同的代码路径,以便在测试中进行覆盖率分析。
if( pxList->pxIndex == pxItemToRemove )
{
pxList->pxIndex = pxItemToRemove->pxPrevious;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
5. 清空容器指针:
将被删除项的容器指针设置为 NULL
,以防止后续意外访问或错误操作。这是一个良好的防护措施,确保被删除项不会被误用。
pxItemToRemove->pvContainer = NULL;
6. 减少项计数:更新链表中项的数量,表示已成功删除一个列表项。这是确保链表状态一致的重要步骤。
( pxList->uxNumberOfItems )--;
7. 返回值:返回当前链表中剩余的项数。这对调用者来说是有用的信息,能够了解链表的当前状态。
return pxList->uxNumberOfItems;
2.6 列表的遍历:
在介绍 List_t
结构体时提到,其中的成员变量 pxIndex
用于遍历列表。FreeRTOS 提供了一个用于遍历列表的宏,称为 listGET_OWNER_OF_NEXT_ENTRY
。每次调用这个宏时,pxIndex
变量将指向列表中的下一个项,并返回该项的 pxOwner
变量的值。这一宏的具体定义位于 list.h
文件中。
这个宏使得遍历操作变得简单高效,能够在循环中逐个访问列表中的每个元素,便于执行相应的处理。
/*
* 访问函数,用于获取列表中下一个条目的所有者。
*
* 列表成员 pxIndex 用于遍历列表。调用 listGET_OWNER_OF_NEXT_ENTRY 会将
* pxIndex 增加到列表中的下一个项,并返回该条目的 pxOwner 参数。
* 因此,通过多次调用此函数,可以遍历列表中的每个项。
*
* 列表项的 pxOwner 参数是指向拥有该列表项的对象的指针。
* 在调度器中,这通常是一个任务控制块(TCB)。
* pxOwner 参数有效地在列表项和其所有者之间创建了一个双向链接。
*
* @param pxTCB pxTCB 被设置为下一个列表项所有者的地址。
* @param pxList 要从中返回下一个项所有者的列表。
*
* \page listGET_OWNER_OF_NEXT_ENTRY listGET_OWNER_OF_NEXT_ENTRY
* \ingroup LinkedList
*/
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \
{ \
List_t * const pxConstList = ( pxList ); \
/* 将索引增加到下一个项并返回该项,确保不会返回列表末尾的标记。 */ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
/* 检查当前索引是否指向列表末尾的标记,如果是则继续前进到下一个项。 */ \
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
} \
/* 将下一个项的所有者指针赋值给 pxTCB。 */ \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}
1. 初始化常量指针:将传入的列表指针 pxList
赋值给一个常量指针 pxConstList
,以避免意外修改。
List_t * const pxConstList = ( pxList );
2. 增加索引:将 pxIndex
移动到下一个列表项。
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
3. 检查末尾标记:检查当前索引是否指向列表的末尾标记 xListEnd
,如果是,则继续移动到下一个项,以跳过末尾标记。
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )
4.获取所有者:将当前项的 pvOwner
值赋给 pxTCB
,即获取到下一个项的所有者。
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;
该宏 listGET_OWNER_OF_NEXT_ENTRY
提供了一种简便的方法来遍历 FreeRTOS 中的链表。通过递增 pxIndex
,用户可以轻松获取到列表中的每个项的所有者,同时确保不会访问到列表的末尾。
三、列表项的插入和删除
3.1 设计目的:
学习如何使用 FreeRTOS 的列表和列表项操作函数,并通过实践验证理论分析的正确性。
-
start task:
- 负责创建其他两个任务(task1task 和 task2task)。
- 启动时输出创建任务的信息。
-
task1task:
- 控制 LED0(假设为硬件上 LED 的标识)闪烁,以指示系统正在运行。
- 可设定一定的延迟,使 LED 闪烁周期可见。
-
task2task:
- 用于执行列表和列表项的操作。
- 调用 FreeRTOS 提供的列表相关 API 函数,并通过串口输出信息,帮助观察这些 API 的运行过程。
3.2 程序分析:
3.2.1 任务设置:
任务优先级、堆栈大小和任务句柄等设置如下:
//任务优先级
#define START_TASK_PRIO 1
//任务堆栈大小
#define START_STK_SIZE 128
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
//任务优先级
#define TASK1_TASK_PRIO 2
//任务堆栈大小
#define TASK1_STK_SIZE 128
//任务句柄
TaskHandle_t Task1Task_Handler;
//任务函数
void task1_task(void *pvParameters);
//任务优先级
#define LIST_TASK_PRIO 3
//任务堆栈大小
#define LIST_STK_SIZE 128
//任务句柄
TaskHandle_t ListTask_Handler;
//任务函数
void list_task(void *pvParameters);
3.2.2 列表和列表项的定义:
//定义一个测试用的列表和3个列表项
List_t TestList; //测试用列表
ListItem_t ListItem1; //测试用列表项1
ListItem_t ListItem2; //测试用列表项2
ListItem_t ListItem3; //测试用列表项3
3.2.3 main()函数:
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
LCD_Init(); //初始化LCD
POINT_COLOR = RED;
LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");
LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 7-1");
LCD_ShowString(30,50,200,16,16,"list and listItem");
LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,90,200,16,16,"2016/11/25");
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
3.2.4 任务函数:
任务函数start_task()和task1_task()都比较简单,这里为了缩减篇幅就不列出来了,重点看一下任务函数list_task(),函数如下:
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建TASK1任务
xTaskCreate((TaskFunction_t )task1_task,
(const char* )"task1_task",
(uint16_t )TASK1_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK1_TASK_PRIO,
(TaskHandle_t* )&Task1Task_Handler);
//创建LIST任务
xTaskCreate((TaskFunction_t )list_task,
(const char* )"list_task",
(uint16_t )LIST_STK_SIZE,
(void* )NULL,
(UBaseType_t )LIST_TASK_PRIO,
(TaskHandle_t* )&ListTask_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//task1任务函数
void task1_task(void *pvParameters)
{
while(1)
{
LED0=!LED0;
vTaskDelay(500); //延时500ms,也就是500个时钟节拍
}
}
//list任务函数
void list_task(void *pvParameters)
{
//第一步:初始化列表和列表项
vListInitialise(&TestList);
vListInitialiseItem(&ListItem1);
vListInitialiseItem(&ListItem2);
vListInitialiseItem(&ListItem3);
ListItem1.xItemValue=40; //ListItem1列表项值为40
ListItem2.xItemValue=60; //ListItem2列表项值为60
ListItem3.xItemValue=50; //ListItem3列表项值为50
//第二步:打印列表和其他列表项的地址
printf("/*******************列表和列表项地址*******************/\r\n");
printf("项目 地址 \r\n");
printf("TestList %#x \r\n",(int)&TestList);
printf("TestList->pxIndex %#x \r\n",(int)TestList.pxIndex);
printf("TestList->xListEnd %#x \r\n",(int)(&TestList.xListEnd));
printf("ListItem1 %#x \r\n",(int)&ListItem1);
printf("ListItem2 %#x \r\n",(int)&ListItem2);
printf("ListItem3 %#x \r\n",(int)&ListItem3);
printf("/************************结束**************************/\r\n");
printf("按下KEY_UP键继续!\r\n\r\n\r\n");
while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP键按下
//第三步:向列表TestList添加列表项ListItem1,并通过串口打印所有
//列表项中成员变量pxNext和pxPrevious的值,通过这两个值观察列表
//项在列表中的连接情况。
vListInsert(&TestList,&ListItem1); //插入列表项ListItem1
printf("/******************添加列表项ListItem1*****************/\r\n");
printf("项目 地址 \r\n");
printf("TestList->xListEnd->pxNext %#x \r\n",(int)(TestList.xListEnd.pxNext));
printf("ListItem1->pxNext %#x \r\n",(int)(ListItem1.pxNext));
printf("/*******************前后向连接分割线********************/\r\n");
printf("TestList->xListEnd->pxPrevious %#x \r\n",(int)(TestList.xListEnd.pxPrevious));
printf("ListItem1->pxPrevious %#x \r\n",(int)(ListItem1.pxPrevious));
printf("/************************结束**************************/\r\n");
printf("按下KEY_UP键继续!\r\n\r\n\r\n");
while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP键按下
//第四步:向列表TestList添加列表项ListItem2,并通过串口打印所有
//列表项中成员变量pxNext和pxPrevious的值,通过这两个值观察列表
//项在列表中的连接情况。
vListInsert(&TestList,&ListItem2); //插入列表项ListItem2
printf("/******************添加列表项ListItem2*****************/\r\n");
printf("项目 地址 \r\n");
printf("TestList->xListEnd->pxNext %#x \r\n",(int)(TestList.xListEnd.pxNext));
printf("ListItem1->pxNext %#x \r\n",(int)(ListItem1.pxNext));
printf("ListItem2->pxNext %#x \r\n",(int)(ListItem2.pxNext));
printf("/*******************前后向连接分割线********************/\r\n");
printf("TestList->xListEnd->pxPrevious %#x \r\n",(int)(TestList.xListEnd.pxPrevious));
printf("ListItem1->pxPrevious %#x \r\n",(int)(ListItem1.pxPrevious));
printf("ListItem2->pxPrevious %#x \r\n",(int)(ListItem2.pxPrevious));
printf("/************************结束**************************/\r\n");
printf("按下KEY_UP键继续!\r\n\r\n\r\n");
while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP键按下
//第五步:向列表TestList添加列表项ListItem3,并通过串口打印所有
//列表项中成员变量pxNext和pxPrevious的值,通过这两个值观察列表
//项在列表中的连接情况。
vListInsert(&TestList,&ListItem3); //插入列表项ListItem3
printf("/******************添加列表项ListItem3*****************/\r\n");
printf("项目 地址 \r\n");
printf("TestList->xListEnd->pxNext %#x \r\n",(int)(TestList.xListEnd.pxNext));
printf("ListItem1->pxNext %#x \r\n",(int)(ListItem1.pxNext));
printf("ListItem3->pxNext %#x \r\n",(int)(ListItem3.pxNext));
printf("ListItem2->pxNext %#x \r\n",(int)(ListItem2.pxNext));
printf("/*******************前后向连接分割线********************/\r\n");
printf("TestList->xListEnd->pxPrevious %#x \r\n",(int)(TestList.xListEnd.pxPrevious));
printf("ListItem1->pxPrevious %#x \r\n",(int)(ListItem1.pxPrevious));
printf("ListItem3->pxPrevious %#x \r\n",(int)(ListItem3.pxPrevious));
printf("ListItem2->pxPrevious %#x \r\n",(int)(ListItem2.pxPrevious));
printf("/************************结束**************************/\r\n");
printf("按下KEY_UP键继续!\r\n\r\n\r\n");
while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP键按下
//第六步:删除ListItem2,并通过串口打印所有列表项中成员变量pxNext和
//pxPrevious的值,通过这两个值观察列表项在列表中的连接情况。
uxListRemove(&ListItem2); //删除ListItem2
printf("/******************删除列表项ListItem2*****************/\r\n");
printf("项目 地址 \r\n");
printf("TestList->xListEnd->pxNext %#x \r\n",(int)(TestList.xListEnd.pxNext));
printf("ListItem1->pxNext %#x \r\n",(int)(ListItem1.pxNext));
printf("ListItem3->pxNext %#x \r\n",(int)(ListItem3.pxNext));
printf("/*******************前后向连接分割线********************/\r\n");
printf("TestList->xListEnd->pxPrevious %#x \r\n",(int)(TestList.xListEnd.pxPrevious));
printf("ListItem1->pxPrevious %#x \r\n",(int)(ListItem1.pxPrevious));
printf("ListItem3->pxPrevious %#x \r\n",(int)(ListItem3.pxPrevious));
printf("/************************结束**************************/\r\n");
printf("按下KEY_UP键继续!\r\n\r\n\r\n");
while(KEY_Scan(0)!=WKUP_PRES) delay_ms(10); //等待KEY_UP键按下
//第七步:删除ListItem2,并通过串口打印所有列表项中成员变量pxNext和
//pxPrevious的值,通过这两个值观察列表项在列表中的连接情况。
TestList.pxIndex=TestList.pxIndex->pxNext; //pxIndex向后移一项,这样pxIndex就会指向ListItem1。
vListInsertEnd(&TestList,&ListItem2); //列表末尾添加列表项ListItem2
printf("/***************在末尾添加列表项ListItem2***************/\r\n");
printf("项目 地址 \r\n");
printf("TestList->pxIndex %#x \r\n",(int)TestList.pxIndex);
printf("TestList->xListEnd->pxNext %#x \r\n",(int)(TestList.xListEnd.pxNext));
printf("ListItem2->pxNext %#x \r\n",(int)(ListItem2.pxNext));
printf("ListItem1->pxNext %#x \r\n",(int)(ListItem1.pxNext));
printf("ListItem3->pxNext %#x \r\n",(int)(ListItem3.pxNext));
printf("/*******************前后向连接分割线********************/\r\n");
printf("TestList->xListEnd->pxPrevious %#x \r\n",(int)(TestList.xListEnd.pxPrevious));
printf("ListItem2->pxPrevious %#x \r\n",(int)(ListItem2.pxPrevious));
printf("ListItem1->pxPrevious %#x \r\n",(int)(ListItem1.pxPrevious));
printf("ListItem3->pxPrevious %#x \r\n",(int)(ListItem3.pxPrevious));
printf("/************************结束**************************/\r\n\r\n\r\n");
while(1)
{
LED1=!LED1;
vTaskDelay(1000); //延时1s,也就是1000个时钟节拍
}
}
任务函数 list_task()
旨在通过调用与 FreeRTOS 列表和列表项相关的 API 函数,对列表及其项执行各种操作。每次调用 API 函数后,这个任务都会通过串口输出当前列表的状态和项之间的连接信息。
这种输出将使我们能够直观地观察在插入、删除或在列表末尾添加项时,列表结构的变化情况。通过这些信息,我们可以清晰地理解和分析列表操作的结果和影响。
1. start_task()
功能:
- 负责创建
task1_task
和list_task
,并在创建后删除自身。
代码分析:
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); // 进入临界区,防止任务调度
// 创建TASK1任务
xTaskCreate((TaskFunction_t )task1_task,
(const char* )"task1_task",
(uint16_t )TASK1_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK1_TASK_PRIO,
(TaskHandle_t* )&Task1Task_Handler);
// 创建LIST任务
xTaskCreate((TaskFunction_t )list_task,
(const char* )"list_task",
(uint16_t )LIST_STK_SIZE,
(void* )NULL,
(UBaseType_t )LIST_TASK_PRIO,
(TaskHandle_t* )&ListTask_Handler);
vTaskDelete(StartTask_Handler); // 删除开始任务
taskEXIT_CRITICAL(); // 退出临界区
}
- 临界区: 使用
taskENTER_CRITICAL()
和taskEXIT_CRITICAL()
确保在创建任务时不会被其他任务打断,确保任务创建的原子性。 - 任务创建: 使用
xTaskCreate
创建两个任务,分别是task1_task
(LED 控制任务)和list_task
(列表操作任务)。 - 任务删除:
vTaskDelete(StartTask_Handler)
删除start_task
自身,因为它的主要功能是创建其他任务。
2. task1_task()
功能
- 负责控制 LED0 的闪烁。
代码分析
void task1_task(void *pvParameters)
{
while(1)
{
LED0=!LED0; // 切换 LED0 状态
vTaskDelay(500); // 延时500ms
}
}
- LED 控制: 每 500 毫秒切换一次 LED0 的状态,表现为 LED 的闪烁。
- 任务延时:
vTaskDelay(500)
将任务延时 500ms,意味着该任务不会在这段时间内执行其他操作,允许其他任务运行。
3. list_task()
功能
- 实现了对 FreeRTOS 列表的管理,包含列表的初始化、插入、删除等操作,同时通过串口输出列表的状态。
代码分析
(1)列表和项的初始化:初始化列表和列表项,准备进行后续操作。
vListInitialise(&TestList);
vListInitialiseItem(&ListItem1);
vListInitialiseItem(&ListItem2);
vListInitialiseItem(&ListItem3);
(2)设置列表项的值
ListItem1.xItemValue=40; // 设置列表项值
ListItem2.xItemValue=60;
ListItem3.xItemValue=50;
(3)打印列表和项的地址
printf("/*******************列表和列表项地址*******************/\r\n");
// 打印地址
(4)插入列表项并打印状态:每次插入后,调用 vListInsert()
函数,并打印当前列表的状态,包括 pxNext
和 pxPrevious
指针,帮助观察列表的连接情况。
vListInsert(&TestList,&ListItem1);
printf("/******************添加列表项ListItem1*****************/\r\n");
(5)删除列表项:使用 uxListRemove()
删除 ListItem2
,并在删除后打印列表状态,以确认连接关系的改变。
uxListRemove(&ListItem2);
(6)末尾插入:使用 vListInsertEnd()
将 ListItem2
插入到列表末尾,并打印状态以观察变化。
vListInsertEnd(&TestList,&ListItem2);
(7)LED控制:在列表操作完成后,控制 LED1 的闪烁,作为任务的结束部分。
任务函数 list_task()
的主要功能是利用与列表和列表项相关的 API 函数对列表进行各种操作,同时通过串口输出每次调用 API 函数后列表和列表项的连接信息。通过这些输出,我们可以直观地观察到在执行插入、删除和在列表末尾添加项时,列表的结构变化情况。这样,可以帮助我们更好地理解和分析列表操作的影响。
四、程序运行结果分析:
4.1 第一步和第二步
第一步和第二步的主要目的是初始化列表及其项,并通过串口输出列表和列表项的地址。这一过程是开发板复位后默认执行的,以下是串口调试助手所显示的信息:
由于这些列表和列表项的地址的前六位都为 0x200000,只有最低两位不同,因此我们可以用这最低两位来表示它们的地址。然而,请注意,不同的硬件平台或编译环境下,列表和列表项的地址可能会有所不同,建议以实际实验结果为准。
根据图的分析,我们可以总结出以下信息:
列表 TestList 的地址为 b4。
列表项 ListItem1、ListItem2 和 ListItem3 的地址分别为 c8、dc 和 f0。
列表 TestList 的 xListEnd 地址为 bc。
TestList 的 pxIndex 指向地址 bc,而这个地址正是迷你列表项 xListEnd 的地址。这一观察与我们在分析列表初始化函数 vListInitialise() 时得到的结果相符。
4.2 第三步
按一下KEY_UP键,执行第三步,第三步是向列表TestList中插入列表项ListIteml,列表项ListIteml的成员变量xItem Value的值为40。第三步执行完以后串口调试助手输出如图所示:
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 指向 ListItem1。
4. ListItem1的 pxPrevious 指向地址 bc,而bc是xListEnd的地址,说明 ListItem1 的 pxPrevious 指向 xListEnd。
4.3 第四步:
按一下KEY_UP键,执行第四步,第四步是向列表TestList中插入列表项ListItem2,列表
项ListItem2的成员变量xItem Value的值为60。第四步执行完以后串口调试助手输出如图所示:
1. xListEnd 的 pxNext 指向 ListItem1。
2. ListItem1 的 pxNext 指向 ListItem2。
3. ListItem2 的 pxNext 指向 xListEnd。
对于列表项的 pxPrevious,分析过程与上述类似,因此在后续步骤中,我们将只关注 pxNext 成员变量。可以用简易示意图表示这一关系,如图所示:
4.4 第五步:
按下 KEY UP 键,执行第五步,即将列表项 ListItem3 插入到 TestList 中。此时,`ListItem3` 的成员变量 xItemValue 值为 50。在完成第四步后,串口调试助手会输出如图 7.7.2.6 所示的信息。
分析图可以得出一下信息:
1. xListEnd的pxNext指向ListIteml。
2. ListItem1的pxNext指向ListItem3。
3. ListItem3的pxNext指向ListItem2。
4. ListItem2的pxNext指向xListEnd。
通过这几步可以看出列表项的插入是根据xItem Value的值做升序排列的,这个和我们分析函数vListInsert()得到的结果一样,说明我们的分析是对的!
4.5 第六步和第七步:
这两步是观察函数uxListRemove()和vListInsertEnd()的运行过程的,分析过程和前五步一
样。这里就不做分析了,大家自行根据串口调试助手输出的信息做分析。
📝大佬觉得本文有所裨益,不妨轻点一下👍给予鼓励吧!
❤️❤️❤️本人虽努力,但能力尚浅,若有不足之处,恳请各位大佬不吝赐教,您的批评指正将是我进步的动力!😊😊😊
💖💖💖若您认为此篇文章对您有所帮助,烦请点赞👍并收藏🌟,您的支持是我前行的最大动力!
🚀🚀🚀任务在默默中完成,价值在悄然间提升。让我们携手共进,一起加油,迎接更美好的未来!🌈🌈🌈