FreeRTOS_列表和列表项

目录

1. 什么是列表和列表项?

1.1 列表

1.2 列表项

1.3 迷你列表项

2. 列表和列表项初始化

2.1 列表初始化

2.2 列表项初始化

3. 列表项插入

3.1 列表项插入函数分析

3.2 列表项插入过程图示

3.2.1 插入值为 40 的列表项

3.2.2 插入值为 60 的列表项

3.2.3 插入值为 50 的列表项

4. 列表项末尾插入

4.1 列表项末尾插入函数分析

4.2 列表项末尾插入图示

4.2.1 默认列表

4.2.2 插入值为 50 的列表项

5. 列表项的删除

6. 列表的删除

7. 列表项的插入和删除实验

7.1 程序运行结果分析

7.2 实验程序


        列表和列表项是 FreeRTOS 的一个数据结构,FreeRTOS 大量使用到了列表和列表项,它是 FreeRTOS 的基石。

1. 什么是列表和列表项?

1.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_BYE 设置为1
//开启以后会向这两个地方分别添加一个变量 xListIntegrityValue1 
//和 xListIntegrityValue2,在初始化列表的时候会在这两个变量中
//写入一个特殊的值,默认不开启这个功能。

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

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

//(4)中 xListEnd 列表中最后一个列表项,用来表示列表结束,
//此变量的类型为 MiniListItem_t,这是一个迷你列表项。

列表结构示意图如下所示:

注意!上图并没有列出用于列表完整性检查的成员变量。(也就是上述结构体中标注的 (1) 和 (5),用来检查列表完整性的成员变量 )

1.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 变量就指向这个任务
//的任务控制块,表示 xStateListItem 属于此任务。当任务就绪态以后 xStateListItem 的变量
//pvContainer 就指向就绪列表,表明此列表在就绪列表中。
//举个比较通俗的例子:小王在上二年级,他的父亲是老王。如果把小王比作列表项,那么小王的 pvOwner
//属性值就是老王(属于任务控制块,他是老王的儿子,他始终都属于老王);小王的 pvContainer
//属性值就是二年级(列表归谁所有,目前小王上二年级,所以它属于二年级,当小王上三年级以后
//pvContainer 的属性值就变成三年级了,意思大概就是这样!!!)     

列表项的结构示意图如下:

注意!上图并没有列出用于列表完整性检查的成员变量。(也就是上述结构体中标注的 (1) 和 (7),用来检查列表完整性的成员变量 )

1.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 指向上一个列表项

//之所以使用迷你列表项,是因为有些情况下我们不需要列表项这么全的功能,
//可能只需要其中的某几个成员变量,如果此时用列表项的话会造成内存浪费!

迷你列表项结构示意图如下:

注意!上图并没有列出用于列表完整性检查的成员变量。(也就是上述结构体中标注的 (1),用来检查列表完整性的成员变量 )

2. 列表和列表项初始化

2.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)、初始化列表项 xListEnd 的 pxPrevious 变量,因为此时列表只有一个列表项 xListEnd ,因此 pxPrevious 只能指向自身。

(5)、由于初始化的时候,列表中没有其他的列表项,因此 uxNumberOfItems(列表中列表项的数目) 为0,注意这里没有将 xListEnd计算在内。

(6) 和 (7) 、初始化列表项中用于完整性检查的字段,只有宏 configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 为1的时候才有效。同样根据所选的 MCU 不同其写入的值也不同,可以为 0x5a5a 或者 0x5a5a5a5aUL。STM32 是 32 位系统写入的是 0x5a5a5a5aUL

列表初始化完以后如下所示:

2.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,并且给用于完整性检查的变量赋值。这里需要注意,虽然列表项的成员比列表要多,但是在初始化的时候只是初始化了 pvContainer;这是因为列表项要根据实际的使用情况来初始化,比如说创建任务函数 xTaskCreate() 就会对任务堆栈中的 xStateListItem 和 xEventListItem 这两个列表项中的其他成员变量再做初始化;

3. 列表项插入

3.1 列表项插入函数分析

        列表项的插入操作通过函数 vListInsert() 来完成:

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

参数:

pxList:                         列表项要插入的列表

pxNewListItem:          要插入的列表项

返回值:

        函数 vListInsert() 的参数 pxList 决定了列表要插入到哪个列表中,pxNewListItem 决定了要插入的列表项但是这个列表项具体插入到什么地方是由列表项中成员变量 xItemValue 来决定的。列表项的插入根据 xItemValue 的值按照升序的方式排列!

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)、经过上面的查找,我们已经找到列表项的插入点了,所以从本行开始的接下来四行代码就是将列表项插入到列表中,插入的过程和数据结构中的双向链表的插入类似。(首先在链表中找到插入位置,双向链表的插入先考虑后继指针的处理,最后再考虑前驱指针的处理)

(7)、列表项已经插入到列表中了,那么列表项的成员变量 pvContainer 也该记录此列表项属于哪个列表了。

(8)、列表的成员变量 uxNumberOfItems 加一,表示又添加了一个列表项。

3.2 列表项插入过程图示

        本节我们会向一个空的列表中插入三个列表项,这三个列表项的值分别为 40 , 60 和 50。

3.2.1 插入值为 40 的列表项

        在一个空的列表 List 中插入一个列表值为 40 的列表项 ListItem1 ,插入完成以后如下图所示:

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

3.2.2 插入值为 60 的列表项

        在上面的基础上,紧接着再插入一个值为 60 的列表项 ListItem2 ,插入完成以后如下图所示:

        在讲解函数 vListInsert() 的时候说过了列表项是按照升序的方式插入的,所以 ListItem2 肯定是插入到 ListItem1 的后面、xListEnd 的前面。同样的,列表 List 的 uxNumberOfItems 再次加一变为 2 了,说明此时列表中有两个列表项。这里需要注意,列表是环形的,类似于双向列表,通过前驱指针和后继指针将整个列表组成一个环形。

3.2.3 插入值为 50 的列表项

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

        按照升序排列的方式,ListItem3 应该放到 ListItem1 和 ListItem2 中间。列表最终形成一个环形

4. 列表项末尾插入

4.1 列表项末尾插入函数分析

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

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

参数:

pxList:                        列表项要插入的列表

pxNewListItem:          要插入的列表项

返回值:

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 所指向的列表项的前面。

 //这四句代码就类似于双向列表中的插入操作
 //首先要明确,列表是一个环形的
 pxNewListItem->pxNext = pxIndex;   //因为要插入是在列表的末尾插入,pxIndex类似于索引指针
 //pxIndex用于遍历整个列表,所以pxIndex指向头指针,所以这句代码的意思是要出入的列表项的后继指针指向头结点
                    
 pxNewListItem->pxPrevious = pxIndex->pxPrevious; 
 //还没有在列表末尾插入列表项时,头结点的前驱指针指向尾结点
 //在末尾插入尾列表后,头指针的前驱指针要指向新插入的这个末尾列表项
 //所以该代码的意思就是新插入列表项的前驱指针指向原本头结点指向的前驱指针
 //通俗的说就是最后一个结点的前驱指针指向倒数第二个结点

 //通过上述两句代码就能实现新插入结点的前驱和后继指针的指向

 pxIndex->pxPrevious->pxNext = pxNewListItem; 
 //该代码的意思是头结点的前驱指针的后继指针指向新插入的结点
 //也就是倒数第二个结点的后继指针指向倒数第一个结点

 pxIndex->pxPrevious = pxNewListItem; 
 //头结点的前驱指针指向新插入的结点,使得整个列表形成一个环形

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

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

4.2 列表项末尾插入图示

        跟函数 vListInsert() 一样,vListInsertEnd() 的插入过程如下图所示:

4.2.1 默认列表

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

注意:列表中的 pxIndex 所指向的列表项,这里为 ListItem1 ,不再是 xListEnd 了。

4.2.2 插入值为 50 的列表项

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

        列表 List 的 pxIndex 指向列表项 ListItem1 ,因此调用函数 xListInsertEnd() 插入 ListItem3 的话就会在 ListItem1 的前面插入。(因为pxIndex指向头结点,所以 ListItem1 对应的就是头列表,整个列表是环形的,所以在尾部插入就是在头结点的前面插入)

5. 列表项的删除

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

UBaseType_t uxListRemove(ListItem_t *const pxItemToRemove)

参数:

pxItemToRemove:                        要删除的列表项

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

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

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)、返回新列表的当前列表项数目。

6. 列表的删除

        列表 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)、进入 if 判断语句,如果到了列表末尾的话就跳过 xListEnd,pxIndex 再一次重新指向处于列表头的列表项,这样就完成了一次对列表的遍历。

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

此函数用于从多个优先级的就绪任务中查找下一个要运行的任务。

7. 列表项的插入和删除实验

        本实验设计 3 个任务:start_task、task1_task 和 list_task,这三个任务的任务功能如下:

start_task:用来创建其他 2 个任务。

task1_task:应用任务 1 ,控制 LED0 闪烁,用来提示系统正在运行。

list_task:列表和列表项操作任务,调用列表和列表项相关 API 函数,并且通过串口输出相应的信息来观察这些 API 函数的运行过程。

7.1 程序运行结果分析

第一步和第二步是用来初始化列表和列表项的,并且通过串口输出列表和列表项的地址,这一步是开发板复位后默认运行的:

//第一步:初始化列表和列表项
    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");
    //注:%x表示打印十六进制数
    //    %#x表示在打印的十六进制数前面加上 0x 
    
    while(KEY_Scan(0)!=4)
        delay_ms(10);    //等待KEY_UP按键按下

        列表 TestList 的地址为 0x200000b8, 列表 TestList 的索引指针 pxIndex 指向地址 0x200000c0, 列表 TestList 的最后一个列表项地址为 0x200000c0。初始化时列表中还没有列表项,所以索引指针 pxIndex 和 xListEnd 指向的地址一致,都表示指向自己。列表项 ListItem1、ListItem2、ListItem3 的初始化地址分别为 0x200000cc、0x200000e0、0x200000f4。

 第三步向列表中添加列表项 ListItem1:

//第三步:向列表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)!=4)
    {
        delay_ms(10);    //等待KEY_UP按键按下
    }

 

        ListItem1.xItemValue=40;        // ListItem1列表项值为40
        ListItem2.xItemValue=60;        // ListItem2列表项值为60
        ListItem3.xItemValue=50;        // ListItem3列表项值为50

        列表项是按照列表项的值递增有序排列的;

        当插入列表项 ListItem1 以后,列表中存在一个列表项 ListItem1 ,因为列表是环形的,所以会形成列表和列表项 1 的双向循环;此时列表最后一个列表项 xListEnd 的后继指针 pxNext 一定是指向列表项 1,地址也就是 0x200000cc,列表项 1 的后继指针 pxNext 一定是指向列表的最后一个列表项 xListEnd,地址也就是 0x200000cc。列表最后一个列表项 xListEnd 的前驱指针一定是指向列表项 1,其地址为 0x200000cc,列表项 1 的前驱指针一定是指向列表的最后一个列表项 pxPrevious,其地址为 0x200000c0.

第四步向列表中添加列表项 ListItem2:

//第四步:向列表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)!=4)
        delay_ms(10);					//等待KEY_UP键按下

        ListItem1.xItemValue=40;        // ListItem1列表项值为40
        ListItem2.xItemValue=60;        // ListItem2列表项值为60
        ListItem3.xItemValue=50;        // ListItem3列表项值为50

        列表项是按照列表项的值递增有序排列的;

        当插入列表项 ListItem2 以后,此时列表中存在两个列表项 ListItem1 和 ListItem2;此时列表的最后一个列表项和列表项 1 以及列表项 2 形成环形结构。

        列表的最后一个列表项 xListEnd 的后继指针依然指向列表项 1 ,其地址为 0x200000cc,列表项 1 的后继指针指向列表项 2 ,其地址为 0x200000e0,列表 2 的后继指针应该指向列表的最后一个列表项 xListEnd ,其地址为 0x200000cc;列表的最后一个列表项 xListEnd 的前驱指针应该指向列表项 2 ,其地址为 0x200000c0,列表项 1 的前驱指针应该指向列表的最后一个列表项 xListEnd ,其地址为 0x200000c0,列表项 2 的前驱指针应该指向列表项 1 ,其地址为 0x200000cc。

第五步向列表中添加列表项 ListItem3:

//第五步:向列表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)!=4) 
        delay_ms(10);					//等待KEY_UP键按下

        ListItem1.xItemValue=40;        // ListItem1列表项值为40
        ListItem2.xItemValue=60;        // ListItem2列表项值为60
        ListItem3.xItemValue=50;        // ListItem3列表项值为50

        列表项是按照列表项的值递增有序排列的;注意此时列表项的顺序应该是 xListEnd、ListItem1、ListItem3、ListItem2;

        当插入列表项 ListItem3 以后,此时列表中存在三个列表项 ListItem1、ListItem2 以及 ListItem3 ;此时列表的最后一个列表项和列表项 1 、列表项 2 以及 列表项 3 形成环形结构。

        列表的最后一个列表项的后继指针依然指向列表项 1,其地址为 0x200000cc,列表项 1 的后继指针应该指向列表项 3 ,其地址为 0x200000f4 ,列表项 3 的后继指针应该指向列表项 2,其地址为 0x200000e0,列表项 2 的后继指针应该指向 xListEnd,其地址为 0x200000c0;列表最后一个列表项的前驱指针应该指向列表项 2 ,其地址为 0x200000e0,列表项 3 的前驱指针应该指向列表项 1 ,其地址为 0x200000cc,列表项 2 的前驱指针应该指向列表项 3 ,其地址为 0x200000f4;

第六步删除 ListItem2:

//第六步:删除ListItem2,并通过串口打印所有列表项中成员变量pxNext和
    //pxPrevious的值,通过这两个值观察列表项在列表中的连接情况。
    uxListRemove(&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)!=4)
        delay_ms(10);					//等待KEY_UP键按下

        ListItem1.xItemValue=40;        // ListItem1列表项值为40
        ListItem2.xItemValue=60;        // ListItem2列表项值为60
        ListItem3.xItemValue=50;        // ListItem3列表项值为50

        列表项是按照列表项的值递增有序排列的;注意此时列表项的顺序应该是 xListEnd、ListItem1、ListItem3;

        此时,列表的最后一个列表项 xListEnd 的后继指针应该指向列表项 listItem1 ,其地址为 0x200000cc,列表项 1 的后继指针应该指向列表项 3,其地址为 0x200000f4,列表项 3 的后继指针应该指向 xListEnd ,其地址为 0x200000c0; 列表的最后一个列表项 xListEnd 的前驱指针应该指向列表项 listItem3 ,其地址为 0x200000f4,列表项 1 的前驱指针应该指向 xListEnd,其地址为 0x200000c0,列表项 3 的前驱指针应该指向列表项 1,其地址为 0x200000cc;

第七步在末尾添加列表项 ListItem2:

//第七步:在末尾添加列表项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");

        ListItem1.xItemValue=40;        // ListItem1列表项值为40
        ListItem2.xItemValue=60;        // ListItem2列表项值为60
        ListItem3.xItemValue=50;        // ListItem3列表项值为50

        列表项是按照列表项的值递增有序排列的;注意此时列表项的顺序应该是 xListEnd、ListItem2、ListItem1、ListItem3;

        在还没有在进行该项操作之前,也就是还未在末尾添加列表项 ListItem2 的时候,列表 TestList 的索引指针 pxIndex 指向列表项 1;列表的索引指针 pxIndex 还是指向列表项 1,其地址为 0x200000cc,列表的最后一个列表项的后继指针应该指向列表项 2 ,其地址也就是 0x200000e0,列表项 2 的后继指针应该指向列表项 1,其地址为 0x20000cc,列表项 1 的后继指针应该指向列表项 3 ,其地址为0x200000f4,列表项 3 的后继指针应该指向 xListEnd,其地址为 0x200000e0;

        列表的最后一个列表项的前驱指针应该指向列表项 3 ,其地址为 0x200000f4,列表项 2 的前驱指针应该指向 xListEnd,其地址为 0x200000e0,列表项 1 的前驱指针应该指向列表项 2 ,其地址为 0x20000e0,列表项 3 的前驱指针应该指向列表项 1 ,其地址为 0x200000cc。

7.2 实验程序

#include "stm32f4xx.h"  
#include "FreeRTOS.h" //这里注意必须先引用FreeRTOS的头文件,然后再引用task.h
#include "task.h"     //存在一个先后的关系
#include "LED.h"
#include "LCD.h"
#include "key.h"
#include "usart.h"
#include "delay.h"

//任务优先级
#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.c 和 List.h 中定义
List_t TestList;        //测试用列表
ListItem_t ListItem1;   //测试用列表项1
ListItem_t ListItem2;   //测试用列表项2
ListItem_t ListItem3;   //测试用列表项3

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);  //设置系统中断优先级分组4
    delay_init(168);
    uart_init(115200);
    LED_Init();
    LCD_Init();
    KEY_Init();
    
    POINT_COLOR=RED;
    LCD_ShowString(30,10,200,16,16,"ATK STM32F407");
    LCD_ShowString(30,30,200,16,16,"FreeRTOS Test");
    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,"2023/06/24");
    
    //创建开始任务
    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();          //开启任务调度
}

//开始任务任务函数
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");
    //注:%x表示打印十六进制数
    //    %#x表示在打印的十六进制数前面加上 0x 
    
    while(KEY_Scan(0)!=4)
        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)!=4)
    {
        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)!=4)
        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)!=4) 
        delay_ms(10);					//等待KEY_UP键按下
    
    //第六步:删除ListItem2,并通过串口打印所有列表项中成员变量pxNext和
    //pxPrevious的值,通过这两个值观察列表项在列表中的连接情况。
    uxListRemove(&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)!=4)
        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个时钟节拍	
	}
}



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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【二】构造函数和原型

ES6&#xff08;ECMAScript 6.0&#xff09;之前js没有引入类的概念 在ES6之前&#xff0c;对象不是基于类创建的&#xff0c;而是用一种称为构建函数的特殊函数来定义对象和它们的特征 ES6之前创建对象可以通过以下三种方式创建对象&#xff1a; 对象字面量&#xff1a; v…

【Spring AOP】面向切面编程,面向切面编程是面向对象编程的孪生兄弟嘛?且听我细细道来! ! !

前言: 大家好,我是良辰丫,面向切面编程和面向对象编程是两种几乎不同的编程方式,并不是所谓的孪生兄弟,但是我们可以说面向切面编程是面向对象编程的一种补充和完善,到底是什么意思呢?请跟随良辰的步伐往下瞧! ! !&#x1f48c;&#x1f48c;&#x1f48c; &#x1f9d1;个人主…

TypeScript ~ 掌握基本类型 ①

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; TypeScript ~ TS &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &…

Redis原理 - IO详解

原文首更地址&#xff0c;阅读效果更佳&#xff01; Redis原理 - IO详解 | CoderMast编程桅杆https://www.codermast.com/database/redis/redis-IO.html 用户空间与内核空间 任何Linux 系统的发行版&#xff0c;其系统内核都是 Linux 。我们的应用都需要通过 Linux 内核与硬…

怎么给PDF添加图片水印?其实很简单,看这篇就会了!

许多人都意识到版权问题的重要性&#xff0c;尽管在日常生活中我们可能很少遇到&#xff0c;但在办公和学习中却经常涉及到此类问题。例如&#xff0c;我们辛辛苦苦制作的PDF文件&#xff0c;如何确保不被他人盗用呢?这就涉及到如何为PDF添加图片水印的问题&#xff0c;相当于…

经典基于外观的SLAM框架-RTABMAP(RGBD视觉输入方案)

经典基于外观的SLAM框架-RTABMAP 文章目录 经典基于外观的SLAM框架-RTABMAP1. RTABMAP整体框架2.RTABMAP的内存管理机制3. 视觉里程计4. 局部地图5. 回环检测与图优化6. 代码工程实践 1. RTABMAP整体框架 RTABMAP是采用优化算法的方式求解SLAM问题的SLAM框架&#xff0c;本赛题…

【python 第三方库安装换源】

换源&#xff1a; pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/其他国内第三方库的下载源地址&#xff1a; 阿里云&#xff1a;http://mirrors.aliyun.com/pypi/simple/ 科技大学&#xff1a;https://pypi.mirrors.ustc.edu.cn/simple/ 豆瓣&a…

Vue实例知识点分享

文章目录 导文下面是创建 Vue 实例的基本步骤 常用的 Vue 实例方法和属性总结 导文 Vue的实例是用来创建 Vue 应用程序的对象。通过实例化 Vue 构造函数&#xff0c;我们可以创建一个具有响应式数据、计算属性、方法和生命周期钩子等特性的 Vue 实例。 下面是创建 Vue 实例的基…

python技术分享

文章目录 python介绍应用领域环境搭建基础知识编程工具变量基本数据类型容器数据类型程序结构运算符函数类 技巧总结python内存管理python常用技术python的缺陷优化python的编码规范提升性能总结 python介绍 弱类型的语言 声明一个变量&#xff0c;直接赋值即可&#xff0c;简…

Android强大的原生调试工具adb的常用命令

文章目录 ADB简介常用命令列出链接的设备进入设备的shell环境设备日志安装应用程序卸载应用程序将本地文件复制到调试设备上将设备上的文件拉取到本地启动程序强制停止程序运行截图屏幕录制列出调试设备所有的应用的报名 结语 ADB简介 ADB&#xff08;Android Debug Bridge&am…

【从零开始学习JAVA | 第二十一篇】常见API介绍 System

目录 前言&#xff1a; System&#xff1a; System类中静态方法&#xff1a; 总结&#xff1a; 前言&#xff1a; system 是一个很底层的 API&#xff0c;是一个工具类&#xff0c;提供了一些与系统相关的方法。他在我们写项目的时候提供了一些非常实用的方法&#xff0c;本…

量子机器学习Variational Quantum Classifier (VQC)简介

变分量子分类器&#xff08;Variational Quantum Classifier&#xff0c;简称VQC&#xff09;是一种利用量子计算技术进行分类任务的机器学习算法。它属于量子机器学习算法家族&#xff0c;旨在利用量子计算机的计算能力&#xff0c;潜在地提升经典机器学习方法的性能。 VQC的…

优化--分类树,我从2s优化到0.1s

1.前言 分类树查询功能&#xff0c;在各个业务系统中可以说随处可见&#xff0c;特别是在电商系统中。 但就是这样一个简单的分类树查询功能&#xff0c;我们却优化了5次。 到底是怎么回事呢&#xff1f; 2.背景 我们的网站使用了SpringBoot推荐的模板引擎&#xff1a;Thym…

【Python实战】Python采集情感音频

成年人的世界真不容易啊 总是悲伤大于欢喜 爱情因为懵懂而快乐 却走进了复杂和困惑的婚姻 前言 我最近喜欢去听情感类的节目&#xff0c;比如说&#xff0c;婚姻类&#xff0c;我可能老了吧。我就想着怎么把音乐下载下来了&#xff0c;保存到手机上&#xff0c;方便我们业余时…

Jnpf低代码开发平台

一、写在前面 低代码开发平台&#xff0c;一个号称能在几分钟的时间里开发出一套公司内部都可使用的应用系统开发工具。 很多人或许都隐隐听说过低代码&#xff0c;因为低代码不仅远名国外&#xff0c;国内的腾讯、阿里、华为、网易、百度等科技巨头也纷纷入局&#xff0c;足以…

URL到页面: 探索网页加载的神秘过程

当我们从浏览器的地址栏输入 URL, 按下回车, 再到最后出现需要的网页界面, 这中间究竟发生了什么, 接下来就一步步进行解析. 主要是如下过程: 输入网址DNS 解析客户端发送 HTTP 请求建立 TCP 连接服务器处理请求, 计算响应, 返回响应浏览器渲染页面关闭连接 本篇中只是概述整…

docker 操作手册

名词解释 images&#xff1a;封装了应用程序的镜像 tag&#xff1a;镜像的标记&#xff0c;一个镜像可以创建多个标记 container&#xff1a;装载镜像并运行 常用命令 查看容器 docker ps -a //查看全部镜像 启动容器 docker start mysql //启动mysql容器 停止容器 doc…

Maven(三):Maven的组成详解

文章目录 坐标和依赖坐标详解依赖配置依赖范围传递性依赖依赖调节可选依赖优化排除依赖归类依赖优化依赖 仓库本地仓库远程仓库仓库镜像常用搜索地址 生命周期与插件三套生命周期clean生命周期default生命周期site生命周期 插件 聚合与继承更加灵活的构建常见问题使用jdk来运行…

TuyaOS 开发固件OTA上传固件指南

文章目录 一、产品创建二、TuyaOS设备开发三、固件上传 通过TuyaOS接入涂鸦云的产品全部默认支持固件OTA功能&#xff0c;TuyaOS设备实现固件OTA需要&#xff1a; 自定义产品创建TuyaOS嵌入式开发固件上传固件OTA配置与发布 等步骤实现产品OTA。本文重点讲述TuyaOS开发模式下&…

基于数据驱动 U-Net 模型的大气污染物扩散快速预测,提升计算速度近6000倍

项目背景 当前&#xff0c;常见的大气污染预测模型大多是基于物理机理构建的&#xff0c;比如空气质量预测模型 Calpuff、AERMOD、CMAQ 等。然而&#xff0c;这些模型运算较为复杂&#xff0c;对于输入数据的要求非常高&#xff0c;运算耗时也比较长&#xff0c;适合用于常规固…