FreeRTOS 列表和列表项

news2024/11/27 16:38:08

文章目录

  • 一、什么是列表和列表项?
  • 二、列表和列表项初始化
  • 三、列表项插入
  • 四、列表项末尾插入
  • 五、列表项的删除
  • 六、列表的遍历
  • 七、列表项的插入和删除实验


一、什么是列表和列表项?

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

typedef struct xLIST
{
	listFIRST_LIST_INTEGRITY_CHECK_VALUE (1)
	configLIST_VOLATILE UBaseType_t uxNumberOfItems; (2)
	ListItem_t * configLIST_VOLATILE pxIndex; (3)
	MiniListItem_t xListEnd; (4)
	listSECOND_LIST_INTEGRITY_CHECK_VALUE (5)
} List_t;

(1) 和 (5) 、这两个都是用来检查列表完整性的 , 需要将宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 设置为 1,开启以后会向这两个地方分别添加一个变量 xListIntegrityValue1 和 xListIntegrityValue2,在初始化列表的时候会这两个变量中写入一个特殊的值,默认不开启这个功能。以后我们在学习列表的时候不讨论这个功能!

(2)、uxNumberOfItems 用来记录列表中列表项的数量。

(3)、pxIndex 用来记录当前列表项索引号,用于遍历列表。

(4)、列表中最后一个列表项,用来表示列表结束,此变量类型为 MiniListItem_t,这是一个迷你列表项,关于列表项稍后讲解。
列表结构示意图如下图所示:
在这里插入图片描述
图中并未列出用于列表完整性检查的成员变量。

2. 列表项
列表项就是存放在列表中的项目,FreeRTOS 提供了两种列表项:列表项和迷你列表项。这两个都在文件 list.h 中有定义,先来看一下列表项,定义如下:

struct xLIST_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)
	void * pvOwner; (5)
	void * configLIST_VOLATILE pvContainer; (6)
	listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE (7)
};
typedef struct xLIST_ITEM ListItem_t;

(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 就指向就绪列表,表明此列表项在就绪列表中,列表项结构示意图如下图所示:

在这里插入图片描述
图中并未列出用于列表项完整性检查的成员变量!

3. 迷你列表项
上面我们我们说了列表项,现在来看一下迷你列表项,迷你列表项在文件 list.h 中有定义,如下:

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)、xItemValue 记录列表列表项值。
(3)、pxNext 指向下一个列表项。
(4)、pxPrevious 指向上一个列表项。

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

迷你列表项结构示意图如下图所示:
在这里插入图片描述
图中并未列出用于迷你列表项完整性检查的成员变量!

二、列表和列表项初始化

1. 列表初始化
新创建或者定义的列表需要对其做初始化处理,列表的初始化其实就是初始化列表结构体List_t 中的各个成员变量,列表的初始化通过使函数 vListInitialise()来完成,此函数在 list.c 中有定义,函数如下:

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)
}

(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,列表初始化完以后如下图所示:
在这里插入图片描述
为了好分析,将 xListEnd 中的各个成员变量都写了出来!

2. 列表项初始化
同列表一样,列表项在使用的时候也需要初始化,列表项初始化由函数 vListInitialiseItem()来完成,函数如下:

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

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

三、列表项插入

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

void vListInsert( List_t * const pxList, 
				ListItem_t * const pxNewListItem )

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

返回值:无

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

void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
	ListItem_t *pxIterator;
	const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; (1)
	listTEST_LIST_INTEGRITY( pxList ); (2)
	listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );
	if( xValueOfInsertion == portMAX_DELAY ) (3)
	{
		pxIterator = pxList->xListEnd.pxPrevious; (4)
	}
	else
	{
		for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->\ (5)
		pxNext->xItemValue <=xValueOfInsertion; pxIterator = pxIterator->pxNext ) 
		{
			//空循环,什么也不做!
		}
	}
	pxNewListItem->pxNext = pxIterator->pxNext; (6)
	pxNewListItem->pxNext->pxPrevious = pxNewListItem;
	pxNewListItem->pxPrevious = pxIterator;
	pxIterator->pxNext = pxNewListItem;
	pxNewListItem->pvContainer = ( void * ) pxList; (7)
	( pxList->uxNumberOfItems )++; (8)
}

(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 加一,表示又添加了一个列表项。

2. 列表项插入过程图示
上一小节我们分析了列表项插入函数 vListInsert(),本小节我们就通过图片来演示一下这个插入过程,本小节我们会向一个空的列表中插入三个列表项,这三个列表项的值分别为 40,60和 50。

(1)插入值为 40 的列表项
在一个空的列表 List 中插入一个列表值为 40 的列表项 ListItem1,插入完成以后如下图所示:
在这里插入图片描述
注意观察插入完成以后列表 List 和列表项 ListItem1 中各个成员变量之间的变化,比如列表 List 中的 uxNumberOfItems 变为了 1,表示现在列表中有一个列表项。列表项 ListItem1 中的
pvContainer 变成了 List,表示此列表项属于列表 List。通过上图可以看出,列表是一个环形的,即环形列表!

(2)插入值为 60 的列表项
接着再插入一个值为 60 的列表项 ListItem2,插入完成以后如下图所示:
在这里插入图片描述
上面再讲解函数 vListInsert()的时候说过了列表项是按照升序的方式插入的,所以 ListItem2肯定是插入到 ListItem1 的后面、xListEnd 的前面。同样的,列表 List 的 uxNumberOfItems 再次加一变为 2 了,说明此时列表中有两个列表项。

(3)插入值为 50 的列表项
在上面的列表中再插入一个值为 50 的列表项 ListItem3,插入完成以后如下图所示:
在这里插入图片描述
按照升序排列的方式,ListItem3 应该放到 ListItem1 和 ListItem2 中间,大家最好通过对照这三幅图片来阅读函数 vListInsert()的源码,这样就会对函数有一个直观的认识。

四、列表项末尾插入

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

void vListInsertEnd( List_t * const pxList, 
					ListItem_t * const pxNewListItem )

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

返回值:无

函数 vListInsertEnd()源码如下:

void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
	ListItem_t * const pxIndex = pxList->pxIndex;
	listTEST_LIST_INTEGRITY( pxList ); (1)
	listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );
	pxNewListItem->pxNext = pxIndex; (2)
	pxNewListItem->pxPrevious = pxIndex->pxPrevious;
	mtCOVERAGE_TEST_DELAY();
	pxIndex->pxPrevious->pxNext = pxNewListItem;
	pxIndex->pxPrevious = pxNewListItem;
	pxNewListItem->pvContainer = ( void * ) pxList; (3)
	( pxList->uxNumberOfItems )++; (4)
}

(1)、与下面的一行代码完成对列表和列表项的完整性检查。

(2)、从本行开始到(3)之间的代码就是将要插入的列表项插入到列表末尾。使用函数vListInsert()向列表中插入一个列表项的时候这个列表项的位置是通过列表项的值,也就是列表项成员变量 xItemValue 来确定。vListInsertEnd()是往列表的末尾添加列表项的,我们知道列表中的 xListEnd 成员变量表示列表末尾的,那么函数 vListInsertEnd()插入一个列表项是不是就是插到 xListEnd 的前面或后面啊?这个是不一定的,这里所谓的末尾要根据列表的成员变量
pxIndex 来确定的!前面说了列表中的 pxIndex 成员变量是用来遍历列表的,pxIndex 所指向的列表项就是要遍历的开始列表项,也就是说 pxIndex 所指向的列表项就代表列表头!由于是个环形列表,所以新的列表项就应该插入到 pxIndex 所指向的列表项的前面。

(3)、标记新的列表项 pxNewListItem 属于列表 pxList。

(4)、记录列表中的列表项数目的变量加一,更新列表项数目。

2. 列表项末尾插入图示
跟函数 vListInsert()一样,我们也用图片来看一下函数 vListInsertEnd()的插入过程。
(1)默认列表
在插入列表项之前我们先准备一个默认列表,如下图所示:
在这里插入图片描述
注意图中列表的 pxIndex 所指向的列表项,这里为 ListItem1,不再是 xListEnd 了。

(2)插入值为 50 的列表项
在上面的列表中插入一个值为 50 的列表项 ListItem3,插入完成以后如下图所示:
在这里插入图片描述

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

五、列表项的删除

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

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )

参数:
pxItemToRemove: 要删除的列表项。
返回值: 返回删除列表项以后的列表剩余列表项数目。

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

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
	List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer; (1)
	pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; (2)
	pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
	mtCOVERAGE_TEST_DELAY();
	if( pxList->pxIndex == pxItemToRemove )
	{
		pxList->pxIndex = pxItemToRemove->pxPrevious; (3)
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
	pxItemToRemove->pvContainer = NULL; (4)
	( pxList->uxNumberOfItems )--;
	return pxList->uxNumberOfItems; (5)
}

(1)、要删除一个列表项我们得先知道这个列表项处于哪个列表中,直接读取列表项中的成员变量 pvContainer 就可以得到此列表项处于哪个列表中。

(2)、与下面一行完成列表项的删除,其实就是将要删除的列表项的前后两个列表项“连接”在一起。

(3)、如果列表的 pxIndex 正好指向要删除的列表项,那么在删除列表项以后要重新给pxIndex 找个“对象”啊,这个新的对象就是被删除的列表项的前一个列表项。

(4)、被删除列表项的成员变量 pvContainer 清零。

(5)、返回新列表的当前列表项数目。

六、列表的遍历

介绍列表结构体的时候说过列表 List_t 中的成员变量 pxIndex 是用来遍历列表的,FreeRTOS提供了一个函数来完成列表的遍历,这个函数是 listGET_OWNER_OF_NEXT_ENTRY()。每调用一次这个函数列表的 pxIndex 变量就会指向下一个列表项,并且返回这个列表项的 pxOwner变量值。这个函数本质上是一个宏,这个宏在文件 list.h 中如下定义:

#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。
此函数用于从多个同优先级的就绪任务中查找下一个要运行的任务。

七、列表项的插入和删除实验

1、实验目的
学习使用 FreeRTOS 列表和列表项相应的操作函数的使用,观察这些操作函数的运行结果和我们理论分析的是否一致。

2、实验设计
本实验设计 3 个任务:start_task、task1_task 和 list_task,这三个任务的任务功能如下:
start_task:用来创建其他 2 个任务。
task1_task:应用任务 1,控制 LED0 闪烁,用来提示系统正在运行。
task2_task: 列表和列表项操作任务,调用列表和列表项相关的 API 函数,并且通过串口输出相应的信息来观察这些 API 函数的运行过程。实验需要用到 KEY_UP 按键,用于控制任务的运行。

3、实验程序与分析

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

#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 个列表项
List_t TestList; //测试用列表
ListItem_t ListItem1; //测试用列表项 1
ListItem_t ListItem2; //测试用列表项 2
ListItem_t ListItem3; //测试用列表项 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(); //开启任务调度
}

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

//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()通过调用与列表和列表项相关的 API 函数来对列表和列表项做相应的操作,并且通过串口打印出每调用一个 API 函数以后列表和列表项的连接信息,通过这些信息我们可以直观的判断出列表项在插入、删除和末尾插入的时候这个列表的变化情况。

4. 程序运行结果分析
编译并下载实验代码到开发板中,打开串口调试助手,然后按照任务函数 list_task()中的步骤一步步的测试分析。

● 第一步和第二步
第一步和第二步是用来初始化列表和列表项的,并且通过串口输出列表和列表项的地址,这一步是开发板复位后默认运行的,串口调试助手信息如下所示:
在这里插入图片描述
由于这些列表和列表项地址前六位都为 0X200000,只有最低 2 位不同,所以我们就用最低2 位代表这些列表和列表项的地址。注意!列表和列表项的地址在不同的硬件平台或编译软件
上是不同的,以自己的实际实验结果为准!简单的分析一下上图可以得到如下信息:

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。第三步执行完以后串口调试助手输出如下图所示:
在这里插入图片描述
分析上图可以得出一下信息:
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。

用简易示意图表示这一步的话如下图所示:
在这里插入图片描述
●第四步
按一下 KEY_UP 键,执行第四步,第四步是向列表 TestList 中插入列表项 ListItem2,列表项 ListItem2 的成员变量 xItemValue 的值为 60。第四步执行完以后串口调试助手输出如下图所示:
在这里插入图片描述
分析上图可以得出一下信息:
1、xListEnd 的 pxNext 指向 ListItem1。
2、ListItem1 的 pxNext 指向 ListItem2。
3、ListItem2 的 pxNext 指向 xListEnd。
4、列表项的 pxPrevious 分析过程类似,后面的步骤中就不做分析了,只看 pxNext 成员变量。用简易示意图表示这一步的话如下图所示:
在这里插入图片描述
●第五步
按一下 KEY_UP 键,执行第五步,第五步是向列表 TestList 中插入列表项 ListItem3,列表项 ListItem3 的成员变量 xItemValue 的值为 50。第四步执行完以后串口调试助手输出如下图所示:
在这里插入图片描述
分析上图可以得出一下信息:
1、xListEnd 的 pxNext 指向 ListItem1。
2、ListItem1 的 pxNext 指向 ListItem3。
3、ListItem3 的 pxNext 指向 ListItem2。
4、ListItem2 的 pxNext 指向 xListEnd。
用简易示意图表示这一步的话如下图所示:
在这里插入图片描述
通过这几步可以看出列表项的插入是根据 xItemValue 的值做升序排列的,这个和我们分析函数 vListInsert()得到的结果一样,说明我们的分析是对的!

●第六步和第七步
这两步是观察函数 uxListRemove()和 vListInsertEnd()的运行过程的,分析过程和前五步一样。可自行根据串口调试助手输出的信息做分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/423563.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【九】springboot启动源码 - refreshContext结束后

afterRefresh 钩子函数,默认实现为空 listeners.started 执行实现SpringApplicationRunListener的started回调 通用处理 this.listeners在之前已经通过SPI加载 获取SpringApplicationRunListener的实现类 EventPublishingRunListener 推送ApplicationStartedEvent事件…

红队内网靶场

文章目录开篇介绍靶场介绍靶场下载以及配置Tomcat Get Shell突破DMZ防火墙拿下域内成员机器将内网机器上线到CS使用Adfind侦察子域信息控制子域DCRadmin登录子域进行权限维持(白银票据/ACL)子域bloodhound获取父域信息分析子域Krbtgt密钥创建跨域金票Dcsync父域PTH父域DC准备打…

Activity启动模式

Activity的启动模式 首先activity启动之后是放在任务栈中的&#xff0c;task stack&#xff0c;既然是栈&#xff0c;遵循先进后出原则。有玩家比喻oncreate是入栈&#xff0c;onDestroy是出栈。 同一个APP中&#xff0c;不同的activity可以设置为不同的启动模式。在manifest…

【深一点学习】我用CPU也能跟着沐神实现单发多框检测(SSD),从底层了解目标检测任务的实现过程,需要什么样的方法调用。《动手学深度学习》Yes,沐神,Yes

目标检测近年来已经取得了很重要的进展&#xff0c;主流的算法主要分为两个类型[1611.06612] RefineNet: Multi-Path Refinement Networks for High-Resolution Semantic Segmentation (arxiv.org)&#xff1a;&#xff08;1&#xff09;two-stage方法&#xff0c;如R-CNN系算法…

centos7.2-rh7.2安装docker的问题

目录1. 环境2. 问题排查2.1 failed to start daemon: error initializing graphdriver: overlay2: the backing xfs filesystem2.2 centos7.2内核bridge.ko问题2.3 kernel:unregister_netdevice: waiting for ... to become free. Usage count 12.4 离线安装依赖问题1. 环境 …

AC7811—FOC无感控制代码调试与测试

目录 滑膜观测器调试 开环调试 闭环调试 IF VF DIRECT IF VF DIRECT 磁链观测器 一次/两次预定位 脉冲注入 高频注入 直线or五段曲线 滑膜观测器调试 开环调试 PWM0 两通道接示波器后&#xff0c;屏幕上打印的是开环给定同步转速积分电角度和滑模观测器估计转子电…

【C++】从string开始了解STL

文章目录1.初识STL1.什么是STL2.STL的版本3.STL的六大组件2.string1.string类模板2.string类的构造函数3.string内部数据访问4.string的遍历5.string类的迭代器6.string的Capacity相关接口7.string的修改相关接口8.其他接口1.初识STL 1.什么是STL STL(standard template liba…

Kotlin在Android开发中的应用与实践

Kotlin在Android开发中的应用与实践 通过本篇文章你将了解到下面内容&#xff1a; 介绍Kotlin语言&#xff1a;简述Kotlin语言的背景、特点和优势&#xff0c;为什么Kotlin在Android开发中变得越来越流行。 Kotlin与Java的对比&#xff1a;对比Kotlin和Java在语法、编程范式、…

规模化敏捷框架:Scrum@Scale

Scrum 敏捷方法有助于团队成员之间更有效地合作&#xff0c;实现共同的业务目标。但是当一个组织想要扩展 Scrum 方法到更多的团队时&#xff0c;应该如何实施&#xff1f;Scrum 仅为单团队开发、交付和运维产品提供了一个框架&#xff0c;而 ScrumScale&#xff08;SS&#xf…

多模态 AI 开发者召集令|4月22日@深圳,等你参加!

四月春暖花开&#xff0c;Jina AI 联合好朋友 OpenMMLab 一起&#xff0c;共同举办第八期「中文社区面对面」活动&#xff0c;将于 4 月 22 日&#xff08;周六&#xff09;下午在 Alpha Bay 深圳智汇港湾孵化器举行。本活动也得到了亚马逊云科技 User Group深圳、Xtreme1 的大…

C++ std::cin

C std::cin相关概念使用1. 一个常见的使用场景&#xff1a;2. 用于静态对象的构造和析构函数中&#xff0c;访问标准输入/输出流是安全的。3. 作为 *while* 语句的条件4. 配合 *get*、*getline* 使用相关概念 istream 一个class&#xff0c;提供输入操作。cin 一个 istream 对…

javaweb游戏代练网站设计与实现

目 录 第1章 绪论 5 1.1 选题的依据及意义 5 1.2 国内外现状研究 6 1.3 研究目的 6 第2章 设计技术与开发环境 7 2.1 相关技术介绍 7 2.1.1 JSP简介 7 2.1.2 B/S模式分析 8 2.1.3 mysql简介 9 2.2 开发环境介绍 9 2.2.1 Myeclipse简介 9 2…

魔镜魔镜告诉我,如何挑选靠谱的硬件方案!

随着传统健身房半胁迫顾客进行数万元的预售、办卡后&#xff0c;分分钟遁地跑路的新闻已经在大家心中留下了深深的烙印&#xff0c;再加上良莠不齐的教练、器材质量、永远无法抢到的健身课、以及办卡之后就被“打入冷宫”……可真是苦了每一个无处安放的运动灵魂啊&#xff01;…

XCODE14生成的APP制作成IPA

1.用XCODE14生成Archives存档包 2.在Finder中显示 3.显示包内容 4.双击进入Products 5.双击进入Applications 6.复制生成的.App文件到Playload文件夹 压缩Playload文件夹成zip 重命名zip为ipa 在弹出 的 提示中 选择 Use .ipa 重命名IPA文件为自己的命名

【vscode】SSH连接远程服务器Host/容器

目录 1. 连接远程服务器 1.1 确保远程服务器Host上ssh打开 1.2 本地主机 1.2.1 安装ssh-client 1.2.2 安装vscode插件 1.2.3 通过密钥完成身份认证 1.2.4 SSH连接配置 2. Python代码跳转 3. C代码跳转 4. Git 历史记录 5. 连接远程服务器上的Docker容器&#xff08;易…

radmin远程控制软件怎么样,有没有替代品

Radmin 是流行的、屡获殊荣的安全远程控制软件&#xff0c;它使您能够在远程计算机上实时工作&#xff0c;就像使用它自己的键盘和鼠标一样。 您可以从多个地方远程访问同一台计算机&#xff0c;是网络和管理类别中流行的远程桌面工具。 Radmin 是外国软件&#xff0c;在国内使…

Unity协程和线程的区别深入理解(附实验展示)

Unity协程和线程的区别附实验展示写在前面协程、进程、线程的概念进程与线程的区别协程与线程的区别实验1&#xff1a;协程中执行普通函数实验2&#xff1a;协程中开启另一个协程实验3&#xff1a;协程中开启WWW请求实验4&#xff1a;一个脚本中多个协程访问临界资源实验5&…

车载以太网基础篇之Eth Driver

车载以太网基础篇之Ethernet Driver前言 首先&#xff0c;请问大家几个小小问题&#xff0c;你清楚&#xff1a; 你知道Eth Driver模块的主要作用是什么吗&#xff1f;EthDriver与以太网控制器&#xff0c;以太网收发器&#xff0c;都有哪些关系呢&#xff1f;Eth Driver的常…

java虚拟机反射机制

&#xff08;1&#xff09;Java虚拟机反射机制的定义&#xff1f; Java反射机制是在运行状态中&#xff0c;对于任意一个类&#xff0c;都能够知道这个类的所有属性和方法;对于任意一个对象&#xff0c;都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功…

苍穹最终一致性使用

适用场景 最终一致模式可以保证跨数据库或跨节点更新时的数据一致。它会以1个更新操作为基准&#xff0c;注册多个其它更新操作&#xff0c;最终保证所有更新都成功&#xff0c;实现分布式事务的弱一致性。可以适用一个更新适用多个场景(跨云、跨库、跨系统) 工作原理 1、在第…