在FreeRTOS中存在着大量的基础数据结构列表和列表项的操作,要想读懂FreeRTOS的源代码,就必须弄懂列表和列表项的操作。
一、C语言链表简介
链表就好比一个圆形的晾衣架,晾衣架上有很多钩子,钩子首尾相连。链表也是,链表由节点组成,节点与节点之间首尾相连。晾衣架的钩子本身不能代表很多东西,但是钩子本身可以挂很多东西。同样,链表也类似,链表的节点本身不可能存储太多东西,但是节点跟晾衣架的钩子一样,可以挂很多数据。
链表分为单向链表和双向链表。
1、单向链表
单向链表如下图所示,该链表中有n个节点,前一个节点都有一个箭头指向后一个节点,首尾相连,组成一个圈。
节点本身必须包含一个节点指针,用于指向后一个节点,除了节点指针之外,节点本身还可以携带一些私有信息。通常做法是节点里面只包含一个用于指向下一个节点的指针,要通过链表存储的数据内嵌一个节点即可,这些要存储的数据通过这个内嵌的节点即可挂接到链表中。
2、双向链表
双向链表与单向链表的区别就是节点中有两个节点指针,分别指向前后两个节点,其他完全一样。
3、链表与数组对比
链表 | 数组 |
---|---|
通过节点把离散的数据连接成一个表 ,通过对节点的插入和删除实现对数据的存取 | 通过开辟一段连续的内存存储数据 |
是一个圈,没有首尾之分,但会规定一个根节点 | 有起始地址和结束地址 |
数组的每个成员对应链表中的节点。
二、FreeRTOS中链表的实现
FreeRTOS中与链表相关的操作均在list.h和list.c这两个文件中实现,list.h 第一次使用需要在 include 文件夹下面新建然后添加到工程freertos/source 这个组文件, list.c 第一次使用需要在 freertos 文件夹下面新建然后添加到工程 freertos/source 这个组文件。
1、实现链表节点
1.1 定义链表节点数据结构
链表节点的数据结构在list.h中定义,如下图:
(1)辅助值 用于帮助节点做顺序排列。该辅助值的数据类型TickType_t, 在 FreeRTOS 中,凡是涉及到数据类型的地方,FreeRTOS 都会将标准的 C 数据类型用 typedef 重新取一个类型名,这些经过重定义的数据类型放在 portmacro.h(portmacro.h 第一次使用需要在 include 文件夹下面新建然后添加到工程 freertos/source 这个组文件)这个头文件。TickType_t 具 体 表 示 16 位 还 是 32 位 , 由configUSE_16_BIT_TICKS 这个宏决定, 当该宏定义为 1 时, TickType_t 为 16 位,否则为32 位。该宏在FreeRTOSConfig.h(FreeRTOSConfig.h 第一次使用需要在 include 文件夹下面新建然后添加到工程 freertos/source 这个组文件) 中默认定义为 0。
(4)指向该节点的拥有者,即该节点内嵌在哪个数据结构中。
(5)指向该节点所在链表,通常指向链表的根节点
1.2 链表节点初始化
链表节点初始化在list.c中完成。
链表节点ListItem_t总共有五个成员,初始化时只需将pvContainer初始化为空即可,表示该节点还没有插入任何节点。
2 实现链表根节点
2.1 定义链表根节点数据结构
列表根节点的数据结构在list.h中实现。
typedef struct xLIST
{
UBaseType_t uxNumberOfItems; /* 链表节点计数器 (1)*/
ListItem_t * pxIndex; /* 链表节点索引指针 (2)*/
MiniListItem_t xListEnd; /* 链表最后一个节点 (3)*/
} List_t;
(1):用于记录该链表下节点个数,除根节点外
(2):用于遍历节点
(3):链表最后一个节点。链表首尾相连,即链表的最后一个节点也是链表的第一个节点,我们称之为生产者。该生产者的数据类型定义在list.h中,如下:
struct xMINI_LIST_ITEM
{
TickType_t xItemValue; /* 辅助值,用于帮助节点做升序排列 */
struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */
struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */
};
typedef struct xMINI_LIST_ITEM MiniListItem_t; /* 精简节点数据类型重定义 */
2.2 链表根节点初始化
链表根节点初始化在list.c中实现。
void vListInitialise( List_t * const pxList )
{
/* 将链表索引指针指向最后一个节点 */(1)
pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );
/* 将链表最后一个节点的辅助排序的值设置为最大,确保该节点就是链表的最后节点 */(2)
pxList->xListEnd.xItemValue = portMAX_DELAY;
/* 将最后一个节点的 pxNext 和 pxPrevious 指针均指向节点自身,表示链表为空 */(3)
pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );
pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
/* 初始化链表节点计数器的值为 0,表示链表为空 */(4)
pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
}
2.3 将节点插入到链表的尾部
链表根节点初始化在list.c中实现。将节点插入到链表的尾部(也可理解为头部)就是讲一个新节点插入到一个空链表中。
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t * const pxIndex = pxList->pxIndex;
pxNewListItem->pxNext = pxIndex; (1)
pxNewListItem->pxPrevious = pxIndex->pxPrevious; (2)
pxIndex->pxPrevious->pxNext = pxNewListItem; (3)
pxIndex->pxPrevious = pxNewListItem; (4)
/* 记住该节点所在的链表 */
pxNewListItem->pvContainer = ( void * ) pxList; (5)
/* 链表节点计数器++ */
( pxList->uxNumberOfItems )++; (6)
}
2.4 将节点按照升序排列插入到链表
将节点按照升序排列插入到链表,如果两个节点的值相同,则新节点在旧节点后面插入。
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t *pxIterator;
/* 获取节点的排序辅助值 */
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; (1)
/* 寻找节点要插入的位置 */ (2)
if ( xValueOfInsertion == portMAX_DELAY )
{
pxIterator = pxList->xListEnd.pxPrevious;
}
else
{
for ( pxIterator = ( ListItem_t * ) &( pxList->xListEnd );
pxIterator->pxNext->xItemValue <= xValueOfInsertion;
pxIterator = pxIterator->pxNext )
{
/* 没有事情可做,不断迭代只为了找到节点要插入的位置 */
}
}
/* 根据升序排列,将节点插入 */ (3)
pxNewListItem->pxNext = pxIterator->pxNext;
pxNewListItem->pxNext->pxPrevious = pxNewListItem;
pxNewListItem->pxPrevious = pxIterator;
pxIterator->pxNext = pxNewListItem;
/* 记住该节点所在的链表 */
pxNewListItem->pvContainer = ( void * ) pxList;
/* 链表节点计数器++ */
( pxList->uxNumberOfItems )++;
}
2.4 将节点从链表中删除
假设将一个有三个节点的链表中的中间节点删除。
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;
/*调整链表的节点索引指针 */
if ( pxList->pxIndex == pxItemToRemove )
{
pxList->pxIndex = pxItemToRemove->pxPrevious;
}
/* 初始化该节点所在的链表为空,表示节点还没有插入任何链表 */
pxItemToRemove->pvContainer = NULL;
/* 链表节点计数器-- */
( pxList->uxNumberOfItems )--;
/* 返回链表中剩余节点的个数 */
return pxList->uxNumberOfItems;
}
三、链表节点插入试验
我们新建了一个根节点和三个普通节点,然后将这三个普通节点按照节点的排序辅助值做升序排列插入到链表中。
/*
*************************************************************************
* 包含的头文件
*************************************************************************
*/
#include "list.h"
/*
*************************************************************************
* 全局变量
*************************************************************************
*/
/* 定义链表根节点 */
struct xLIST List_Test; (1)
/* 定义3个普通节点 */
struct xLIST_ITEM List_Item1; (2)
struct xLIST_ITEM List_Item2;
struct xLIST_ITEM List_Item3;
/*
************************************************************************
* main 函数
************************************************************************
int main(void)
{
/* 链表根节点初始化 */
vListInitialise( &List_Test ); (3)
/* 节点 1 初始化 */
vListInitialiseItem( &List_Item1 ); (4)
List_Item1.xItemValue = 1;
/* 节点 2 初始化 */
vListInitialiseItem( &List_Item2 );
List_Item2.xItemValue = 2;
/* 节点 3 初始化 */
vListInitialiseItem( &List_Item3 );
List_Item3.xItemValue = 3;
/* 将节点插入链表,按照升序排列 */ (5)
vListInsert( &List_Test, &List_Item2 );
vListInsert( &List_Test, &List_Item1 );
vListInsert( &List_Test, &List_Item3 );
while(1)
{
}
}
程序编译之后,点击调试按钮,全速运行,然后把List_Test、List_Item1、List_Item2、List_Item3这四个全局变量添加到watch窗口进行观察。
参考:[野火®]《FreeRTOS 内核实现与应用开发实战—基于STM32》