G
N
I
D
A
O
L

uCOS-III 学习记录(9)——时基列表

参考内容:《[野火]uCOS-III内核实现与应用开发实战指南——基于STM32》第 13 章。

1 数据类型定义和宏定义

1.1 时基列表相关宏定义和全局变量(os_cfg_app.h/c & os.h)

在 os_cfg_app.h 中,宏定义时基列表的大小,其推荐值为任务数/4,推荐使用质数,不推荐使用偶数。如果算出来的大小是偶数,则需加 1 变成质数。

/* 时基列表大小 */
#define OS_CFG_TICK_WHEEL_SIZE			17u

接下来在 os_cfg_app.c 中,定义两个全局变量,分别是时基列表(数组)和时基列表的大小。注意需要在 os.h 中声明。

OS_TICK_SPOKE		OSCfg_TickWheel[OS_CFG_TICK_WHEEL_SIZE];		/* 时基列表 */
OS_OBJ_QTY	const	OSCfg_TickWheelSize = (OS_OBJ_QTY)OS_CFG_TICK_WHEEL_SIZE;	/* 时基列表大小 */

在 os.h 中定义 32 位的 Tick 计数器,即 SysTick 周期计数器,记录系统启动到现在或者从上一次复位到现在经过了多少个 SysTick 周期。每发起一次 SysTick 中断,该变量就会加一。

OS_EXT	OS_TICK			OSTickCtr;		/* SysTick 周期计数器 */

1.2 时基列表定义(os.h)

在 os.h 中定义时基列表结构体,其组成为:

  • FirstPtr:用于指向 TCB 双向链表的第一个节点。时基列表的每个数组元素都记录着一条 TCB 单向链表的信息,被插入该条链表的 TCB 会按照延时时间做升序排列。
  • NbrEntries:记录 TCB 链表有多少个节点。
  • NbrEntriesMax:记录 TCB 链表最多的时候有多少个节点,在节点增加时会刷新,在删除节点时不刷新。
typedef struct os_tick_spoke 	OS_TICK_SPOKE;					/* 时基列表重命名为大写字母格式 */

struct os_tick_spoke{
	OS_TCB		*FirstPtr;			/* TCB 单向链表的头节点 */
	OS_OBJ_QTY	NbrEntries;			/* 单向链表有多少个节点 */
	OS_OBJ_QTY	NbrEntriesMax;		/* 单向链表最多时有多少个节点,该值会被刷新 */
};

1.3 修改 TCB 定义(os.h)

在 TCB 中新增加了 5 个成员,分别是:

  • TickNextPtr:双向链表中指向下一个 TCB 节点。
  • TickPrevPtr:双向链表中指向上一个 TCB 节点。
  • TickSpokePtr:指向时基列表数组的指针,即用来指示该任务 TCB 属于哪条双向链表。
  • TickCtrMatch:该值表示当前时基计数器的值加上任务要延时的周期。
  • TickRemain:表示任务还需要延时多少个 SysTick 周期。该变量替代了 TaskDelayTicks 的功能,所以可以去掉 TaskDelayTicks。
struct os_tcb{
	CPU_STK			*StkPtr;
	CPU_STK_SIZE	StkSize;
	
	//OS_TICK			TaskDelayTicks;		/* 任务延时周期个数 */
	
	OS_PRIO			Prio;				/* 任务优先级 */
	
	OS_TCB			*NextPtr;			/* 就绪列表双向链表的下一个指针 */
	OS_TCB			*PrevPtr;			/* 就绪列表双向链表的前一个指针 */
	
	OS_TCB			*TickNextPtr;		/* 指向时基列表双向链表的下一个 TCB 节点 */
	OS_TCB			*TickPrevPtr;		/* 指向时基列表双向链表的上一个 TCB 节点 */
	OS_TICK_SPOKE	*TickSpokePtr;		/* 用于回指到链表根部 */
	
	OS_TICK			TickCtrMatch;		/* 该值等于时基计数器 OSTickCtr 的值加上 TickRemain 的值 */
	OS_TICK			TickRemain;			/* 设置任务还需要等待多少个时钟周期 */
};

因此,目前实现的 uCOS 一共有两个列表:就绪列表和时基列表,它们均根据自己的规则建立一个 TCB 双向链表,列表中的数组会存放双向链表的信息。就绪列表的数组下标表示优先级,时基列表的数组下标表示时间周期。

下图展示的是时基列表的结构,它其中一个数组元素存储了一条 TCB 双向链表信息,这是一条有 3 个节点的双向链表,注意每个节点都回指了这个数组元素的地址,第三个节点是漏画了。

image

2 时基列表的相关函数

2.1 初始化时基列表 OS_TickListInit()(os_tick.c)

该函数用于初始化时基列表数组,将全部成员清零。

/* 初始化时基列表 */
void OS_TickListInit (void)
{
	OS_TICK_SPOKE_IX	i;
	OS_TICK_SPOKE		*p_spoke;
	
	for (i = 0u; i < OSCfg_TickWheelSize; i++)
	{
		p_spoke 				= (OS_TICK_SPOKE *)&OSCfg_TickWheel[i];
		p_spoke->FirstPtr 		= (OS_TCB        *)0;
		p_spoke->NbrEntries 	= (OS_OBJ_QTY     )0u;
		p_spoke->NbrEntriesMax 	= (OS_OBJ_QTY     )0u;
	}
}

需要在 OSInit() 中调用此函数以完成初始化工作。初始化后的时基列表如下所示:

image

2.2 往时基列表插入任务控制块 OS_TickListInsert()(os_tick.c)

该函数的作用是将任务 TCB 插入到时基列表中,即对应的 TCB 双向链表,注意这是一个升序排序的双向链表,按照延时时间的长度升序排序。该函数完成的工作有:

  • 获得该任务要延时的时间 time 后,在 TCB 中记录延时到期时的时间,即将当前时基计数器的值加上任务要延时的时间记录在 TickCtrMatch。
  • 在 TCB 中记录任务要延时的时间。TickRemain 的功能可等价为 TaskDelayTicks 的功能。

我们先停一下,举个例子来理解上面的原理。比如现在时基计数器 OSTickCtr = 10(即从系统启动到现在已经过去了 10 个 SysTick 周期),任务需要延时的时间 time(TickRemain) = 2(即任务需要延时 2 个 SysTick 周期),那么意味着当 OSTickCtr = 12 (即从系统启动到现在已经过去了 12 个 SysTick 周期)时任务延时结束,此处的 12 就是 TickCtrMatch 的值。当时基计数器等于 12 时,捕捉到该任务延时结束(所以这也是 Match 的意思?)。

嗯......看到这,你是不是觉得 OSTickCtr 有点像时间戳的作用了......但其实,OSTickCtr 记录的是“周期的时间戳”,而真正的时间戳记录的可是真实的时间哦!

有意思的是,TickRemain 其实可有可无,它更多的是作为计算剩余时间时的过渡变量,在本节涉及到 TickRemain 的代码中有反应这一点。而在 2.4 节中,你可以试着注释掉涉及到 TickRemain 的代码行,你会发现这对仿真结果没有一点影响。

回忆一下前几章中任务延时的办法:SysTick 发起一次中断,系统就遍历就绪列表中的 TCB,给每个 TCB 中的 TaskDelayTicks 减一。现在我们的做法是:定义了一个全局变量,从系统启动就开始计数。任务结束延时的时期,就是这个全局变量加上延时的周期长度,这个值我们记录在 TCB 中。为什么要换方法呢?你看看这两种办法的区别:前者是我们帮每个 TCB 减一,因此比较麻烦;后者是我们在外面计总的时间,你们这些要延时的 TCB 自己看好时间,到时间后你自己站出来报到。而且更绝的是,我们将这些 TCB 按照延时时间排好顺序,如果前一个 TCB 延时时间未到,那后面的 TCB 看都不用看,肯定没到时间。你看,把复杂的事情简单化,uCOS 绝吧!

  • TickCtrMatch 除以时基列表的数组大小 OSCfg_TickWheelSize 求余,作为下标访问时基列表数组。

继续以上面例子为例来说明。现在,TickCtrMatch = 12,我们定义 OSCfg_TickWheelSize = 17,求余后是 12,因此要访问的是 OSCfg_TickWheel[12]。这里运用了哈希算法。接下来,这个任务将插入到 OSCfg_TickWheel[12] 下的双向升序链表中,剩余的工作就是大家熟悉的插入链表操作了。

  • 如果双向链表为空:则该 TCB 的前向、后向指针域皆为零。
  • 如果双向链表不为空:需要遍历整个链表,访问每一个节点,计算该节点的剩余时间,与待插入节点的剩余时间比较一下:如果被访问节点的大,那就插入到它前面去;否则,就是待插入节点的大了,插入到被访问节点的后面去。如果遍历完了都没找到比待插入节点大的,那就排在最后面去。
  • 最后,新插入的 TCB 的 TickSpokePtr 需要指向对应的数组元素。

继续以上面例子说明。这个任务已经插入到 OSCfg_TickWheel[12] 下的双向升序链表中了,那么该任务 TCB 的 TickSpokePtr 将指向 OSCfg_TickWheel[12],以说明自己在时基列表中的第 12 个位置。

/* 往时基列表插入一个任务 TCB,根据延时时间的大小升序排列 */
void OS_TickListInsert (OS_TCB *p_tcb, OS_TICK time)
{
	OS_TICK_SPOKE_IX	spoke;
	OS_TICK_SPOKE		*p_spoke;
	OS_TCB				*p_tcb0;
	OS_TCB				*p_tcb1;
	
	/* TickCtrMatch 等于当前时基计数器的值加上任务要延时的时间 */
	p_tcb->TickCtrMatch = OSTickCtr + time;	
	
	/* TickRemain 表示任务还需要多少个 SysTick 周期,每来一个周期就会减一 */
	p_tcb->TickRemain   = time;		
	
	/* (哈希算法)求余得到的 spoke 作为时基列表 OSCfg_TickWheel[] 的索引 */
	spoke 	= (OS_TICK_SPOKE_IX)(p_tcb->TickCtrMatch % OSCfg_TickWheelSize);
	
	/* 获取该双向链表的根指针 */
	p_spoke = &OSCfg_TickWheel[spoke];
	
	/* 若双向链表为空 */
	if (p_spoke->NbrEntries == (OS_OBJ_QTY)0u)
	{
		p_tcb->TickNextPtr  = (OS_TCB   *)0;
		p_tcb->TickPrevPtr  = (OS_TCB   *)0;
		p_spoke->FirstPtr   =  p_tcb;
		p_spoke->NbrEntries = (OS_OBJ_QTY)1u;
	}
	else /* 若双向链表不为空 */
	{
		p_tcb1 = p_spoke->FirstPtr;		/* 先获取第一个节点 */	
		/* 开始遍历双向链表 */
		while (p_tcb1 != (OS_TCB *)0)
		{
			p_tcb1->TickRemain = p_tcb1->TickCtrMatch - OSTickCtr;	/* 计算被访问节点的剩余时间 */
			if (p_tcb->TickRemain > p_tcb1->TickRemain)	/* (1) 如果新节点的剩余时间大于被访问节点的剩余时间 */
			{
				if (p_tcb1->NextPtr != (OS_TCB *)0)		/* (1.1) 如果被访问节点不是最后一个节点 */
				{
					p_tcb1 = p_tcb1->NextPtr;			/* 访问下一个节点,继续查找 */
				}
				else		/* (1.2) 否则,意味着整个链表已经遍历完毕,新节点的剩余时间最大,在最后一个节点插入新节点 */
				{
					p_tcb->TickNextPtr  = (OS_TCB *)0;
					p_tcb->TickPrevPtr  =  p_tcb1;
					p_tcb1->TickNextPtr =  p_tcb;
					p_tcb1 = (OS_TCB *)0;		/* 插入操作完成后清零,下一次将跳出循环 */
				}
			}
			else /* (2) 否则,被访问节点的剩余时间小于等于新节点的剩余时间,则插入到该节点前面 */
			{
				if (p_tcb1->TickPrevPtr == (OS_TCB *)0) /* (2.1) 如果被访问节点是第一个节点 */
				{
					p_tcb->TickPrevPtr  = (OS_TCB *)0;
					p_tcb->TickNextPtr  = p_tcb1;
					p_tcb1->TickPrevPtr = p_tcb;
					p_spoke->FirstPtr   = p_tcb;
				}
				else		/* (2.2) 否则,是在两个节点之间插入新节点 */
				{
					p_tcb0              = p_tcb1->TickPrevPtr;
					p_tcb->TickPrevPtr  = p_tcb0;
					p_tcb->TickNextPtr  = p_tcb1;
					p_tcb0->TickNextPtr = p_tcb;
					p_tcb1->TickPrevPtr = p_tcb;
				}
				p_tcb1 = (OS_TCB *)0;		/* 插入操作完成后清零,下一次将跳出循环 */
			}
		}
		p_spoke->NbrEntries++;		/* 节点数加一 */
	}
	
	if (p_spoke->NbrEntriesMax < p_spoke->NbrEntries)	/* 刷新最大值 */
	{
		p_spoke->NbrEntriesMax = p_spoke->NbrEntries;
	}
	
	/* 任务 TCB 中的 TickSpokePtr 回指根节点 */
	p_tcb->TickSpokePtr = p_spoke;	
}

再举个例子。我们现在 OSTickCtr = 10,OSCfg_TickWheelSize = 12,有以下三个任务,情况分别是:

  • 任务 1:需要延时 1 个周期。因此,TickRemain = 1,TickCtrMatch = 11,需要插入的数组下标是 11 % 12 = 11。
  • 任务 2:需要延时 13 个周期。因此,TickRemain = 13,TickCtrMatch = 23,需要插入的数组下标是 23 % 12 = 11。
  • 任务 3:需要延时 25 个周期。因此,TickRemain = 25,TickCtrMatch = 35,需要插入的数组下标是 35 % 12 = 11。

那么插入时基列表后的情况如下图:

image

注意,这三个任务 TCB 都在同一条链表中,那只是数字凑巧而已。假如我把任务 2 的延时改为 14 个周期,那么插入的数组下标是 0,会插到别的链表中。

2.3 在时基列表删除一个指定的任务控制块 OS_TickListRemove()

该函数是在时基列表中删除一个指定的任务 TCB,完成的工作有:

  • 根据 TCB 的 TickSpokePtr,获取对应的时基列表数组元素,进而获取链表的头指针。
  • 若头指针为空,则说明链表不存在,直接退出。
  • 若不为空,说明链表存在,删除指定节点。
  • 删除完后,将该 TCB 的前向、后向指针域、剩余时间全部清零。节点数减一。
/* 往时基列表删除一个指定的任务 TCB */
void OS_TickListRemove (OS_TCB *p_tcb)
{
	OS_TICK_SPOKE		*p_spoke;
	OS_TCB				*p_tcb1;
	OS_TCB				*p_tcb2;
	
	/* 获取任务 TCB 所在链表的根指针 */
	p_spoke = p_tcb->TickSpokePtr;
	
	if (p_spoke != (OS_TICK_SPOKE *)0)		/* 若链表存在 */
	{
		if (p_tcb == p_spoke->FirstPtr)		/* (1) 如果被删除的节点是第一个节点 */
		{
			p_tcb1 = p_tcb->TickNextPtr;
			p_spoke->FirstPtr = p_tcb1;
			if (p_tcb1 != (OS_TCB *)0)	/* 若删除后链表非空,则新的头节点的前向指针域设为 0,否则不用操作 */
			{
				p_tcb1->TickPrevPtr = (OS_TCB *)0;
			}
		}
		else								/* (2) 如果被删除的节点不是第一个节点 */
		{
			p_tcb1 = p_tcb->TickPrevPtr;
			p_tcb2 = p_tcb->TickNextPtr;
			p_tcb1->TickNextPtr = p_tcb2;
			if (p_tcb2 != (OS_TCB *)0)	/* 若删除的不是最后一个节点,则新的尾节点的前向指针域设为 0,否则不用操作 */
			{
				p_tcb2->TickPrevPtr = p_tcb1;
			}
		}
		
		p_tcb->TickNextPtr 	= (OS_TCB *)0;
		p_tcb->TickPrevPtr 	= (OS_TCB *)0;
		p_tcb->TickSpokePtr = (OS_TICK_SPOKE *)0;
		p_tcb->TickRemain 	= (OS_TICK)0u;	/* 剩余时间清零 */
		p_tcb->TickCtrMatch = (OS_TICK)0u;
		
		p_spoke->NbrEntries--;		/* 节点数减一 */
	}
}

2.4 检查任务延时是否到期 OS_TickListUpdate()(os_tick.c)

该函数的作用是:更新时基计数器,扫描时基列表中的任务延时是否到期。完成的工作是:

  • 时基计数器加一。
  • 由当前时基计数器的值求余,获得时基列表的数组下标,进而获得对应的双向链表。
  • 开始遍历链表,访问每一个节点,若发现有节点延时未到,那么后面的节点就不用访问了,因为链表是升序排序的,后面的节点延时肯定未到;若发现有节点延时到了,那么将该任务设置为就绪态,然后继续访问下一个节点。

发现了吗?访问时基列表的哪个数组元素,是跟当前时基计数器的值是息息相关的,通过哈希算法来访问对应的数组元素。不过,这样做的目的似乎不是很明确(?)。

/* 更新时基计数器,扫描时基列表中的任务延时是否到期 */
void OS_TickListUpdate (void)
{
	OS_TICK_SPOKE_IX	spoke;
	OS_TICK_SPOKE		*p_spoke;
	OS_TCB				*p_tcb;
	OS_TCB				*p_tcb_next;
	CPU_BOOLEAN			done;
	
	CPU_SR_ALLOC();
	OS_CRITICAL_ENTER();
	
	/* 时基计数器加一 */
	OSTickCtr++;
	
	/* (哈希算法)求余得到的 spoke 作为时基列表 OSCfg_TickWheel[] 的索引 */
	spoke 	= (OS_TICK_SPOKE_IX)(OSTickCtr % OSCfg_TickWheelSize);
	
	/* 获取该双向链表的根指针 */
	p_spoke = &OSCfg_TickWheel[spoke];
	
	/* 获取该双向链表的第一个节点 */	
	p_tcb   = p_spoke->FirstPtr;	
	done    = DEF_FALSE;
	
	while (done == DEF_FALSE)
	{
		if (p_tcb != (OS_TCB *)0)	/* 若节点存在 */
		{
			p_tcb_next = p_tcb->TickNextPtr;
			p_tcb->TickRemain = p_tcb->TickCtrMatch - OSTickCtr;	/* 计算节点的剩余时间 */
			
			if (OSTickCtr == p_tcb->TickCtrMatch)	/* 如果节点的延时时间已到 */
			{
				OS_TaskRdy (p_tcb);					/* 则让任务就绪 */
			}
			else	/* 否则,节点的延时时间未到 */
			{
				done = DEF_TRUE;	/* 则退出循环,因为链表节点按延时时间升序排列,该节点的延时时间未到,后面的节点肯定未到 */
			}
			p_tcb = p_tcb_next;
		}
		else
		{
			done = DEF_TRUE;
		}
	}
	
	OS_CRITICAL_EXIT();
}

举个例子。我们定义 OSCfg_TickWheelSize = 12,在 OSTickCtr = 7 时,插入了以下三个任务,情况分别是:

  • 任务 1:需要延时 16 个周期。因此,TickRemain = 16,TickCtrMatch = 28,需要插入的数组下标是 11 % 12 = 11。
  • 任务 2:需要延时 28 个周期。因此,TickRemain = 28,TickCtrMatch = 35,需要插入的数组下标是 23 % 12 = 11。
  • 任务 3:需要延时 40 个周期。因此,TickRemain = 40,TickCtrMatch = 47,需要插入的数组下标是 35 % 12 = 11。

时基列表的情况如下图:

image

当 SysTick 中断来临时,OSTickCtr = 8,对 OSCfg_TickWheelSize(等于 12)求余等于 8,则对 OSCfg_TickWheel[8] 下面的链表进行扫描,从图中可以得知,8 这个索引下没有节点,则直接退出。这三个 TCB 是在 OSCfg_TickWheel[11] 下的链表,根本不用扫描,因为时间只是刚刚过了 1 个时钟周期而已,它们只是刚刚插入时基列表,远远没有达到它们需要的延时时间。

2.5 时基列表运行全景

当我们把时基列表的相关代码都实现出来以后,很有意思的事情发生了:我们假定 OSCfg_TickWheelSize = 17,即时基列表的大小为 17,那么:

  • 在系统刚启动时 OSTickCtr = 0;
  • SysTick 中断发生后 OSTickCtr = 1,只检查 OSCfg_TickWheel[1];
  • SysTick 中断又发生后 OSTickCtr = 2,只检查 OSCfg_TickWheel[2];
  • SysTick 中断又发生后 OSTickCtr = 3,只检查 OSCfg_TickWheel[3];
  • ......
  • SysTick 中断又发生后 OSTickCtr = 16,只检查 OSCfg_TickWheel[16];
  • SysTick 中断又发生后 OSTickCtr = 17,只检查 OSCfg_TickWheel[0];
  • SysTick 中断又发生后 OSTickCtr = 18,只检查 OSCfg_TickWheel[1];
  • ......
  • SysTick 中断又发生后 OSTickCtr = 33,只检查 OSCfg_TickWheel[16];
  • SysTick 中断又发生后 OSTickCtr = 34,只检查 OSCfg_TickWheel[0];
  • SysTick 中断又发生后 OSTickCtr = 35,只检查 OSCfg_TickWheel[1];
  • .....

这样循环往复的检查每一个数组元素。这时我们插入一个需要延时的 TCB 会怎样呢?假设在 OSTickCtr = 4 时插入一个 TCB,延时时长为 3,那么:

  • 在系统刚启动时 OSTickCtr = 0;
  • SysTick 中断发生后 OSTickCtr = 1,只检查 OSCfg_TickWheel[1],发现没有任务;
  • SysTick 中断又发生后 OSTickCtr = 2,只检查 OSCfg_TickWheel[2],发现没有任务;
  • SysTick 中断又发生后 OSTickCtr = 3,只检查 OSCfg_TickWheel[3],发现没有任务;
  • SysTick 中断又发生后 OSTickCtr = 4,只检查 OSCfg_TickWheel[4],发现没有任务;接着任务发起阻塞,延时时间为 3,计算得出在 OSTickCtr = 7 时延时到期(TickCtrMatch = 7),因此在 OSCfg_TickWheel[7] 下插入 TCB;
  • SysTick 中断又发生后 OSTickCtr = 5,只检查 OSCfg_TickWheel[5],发现没有任务;
  • SysTick 中断又发生后 OSTickCtr = 6,只检查 OSCfg_TickWheel[6],发现没有任务;
  • SysTick 中断又发生后 OSTickCtr = 7,只检查 OSCfg_TickWheel[7],发现有任务,而且 TickCtrMatch == OSTickCtr,延时到期,移出时基列表;
  • ......

我们再假设在 OSTickCtr = 25 时插入一个 TCB,延时时长为 19,(OSCfg_TickWheelSize = 17)那么:

  • 在系统刚启动时 OSTickCtr = 0;
  • SysTick 中断发生后 OSTickCtr = 1,只检查 OSCfg_TickWheel[1],发现没有任务;
  • SysTick 中断又发生后 OSTickCtr = 2,只检查 OSCfg_TickWheel[2],发现没有任务;
  • SysTick 中断又发生后 OSTickCtr = 3,只检查 OSCfg_TickWheel[3],发现没有任务;
  • ......
  • SysTick 中断又发生后 OSTickCtr = 25,只检查 OSCfg_TickWheel[8],发现没有任务;接着任务发起阻塞,延时时间为 19,计算得出在 OSTickCtr = 44 时延时到期(TickCtrMatch = 44),因此在 OSCfg_TickWheel[10] 下插入 TCB;
  • SysTick 中断又发生后 OSTickCtr = 26,只检查 OSCfg_TickWheel[9],发现没有任务;
  • SysTick 中断又发生后 OSTickCtr = 27,只检查 OSCfg_TickWheel[10],发现有任务,但是未到期(TickCtrMatch != OSTickCtr),继续位于时基列表中;
  • ......
  • SysTick 中断又发生后 OSTickCtr = 43,只检查 OSCfg_TickWheel[9],发现没有任务;
  • SysTick 中断又发生后 OSTickCtr = 44,只检查 OSCfg_TickWheel[10],发现有任务,而且 TickCtrMatch == OSTickCtr,延时到期,移出时基列表;
  • ......

希望这样写有助于大家理解整个时基列表的运行机制。

3 添加和修改相应代码

3.1 将任务设置为就绪态 OS_TaskRdy()(os_core.c)

这是一个新加入的函数,位于 os_core.c 中,作用是将任务设置为就绪态,具体操作是:

  • 从时基列表中删除该 TCB。
  • 在就绪列表中加入该 TCB。
/* 任务就绪 */
void OS_TaskRdy (OS_TCB *p_tcb)
{
	OS_TickListRemove (p_tcb);		/* 从时基列表中删除 */
	OS_RdyListInsert  (p_tcb);		/* 插入就绪列表     */
}

3.2 阻塞延时函数 OSTimeDly()(os_time.c)

该函数需要修改,作用是将任务设置为阻塞态,具体操作是:

  • 从时基列表中加入该 TCB。
  • 在就绪列表中删除该 TCB。
  • 执行任务调度。

注意:

  • 以上工作位于临界段内。

我们发现,阻塞延时函数和上面函数的操作正好是相反的。

/* 阻塞延时 */
void OSTimeDly (OS_TICK dly)
{
	CPU_SR_ALLOC();
	OS_CRITICAL_ENTER();	
	
	/* 插入时基列表 */
	OS_TickListInsert (OSTCBCurPtr, dly);
	
	/* 从就绪列表移除 */
	OS_RdyListRemove (OSTCBCurPtr);
	
	OS_CRITICAL_EXIT();		
	
	/* 任务切换 */
	OSSched();
}

3.3 SysTick 发起中断调用 OSTimeTick()(os_time.c)

SysTick每发起一次中断,就会调用本函数,完成两件事情:

  • 更新时基列表。让时基计数器加一,以及检查就绪列表中有无任务延时结束。如果有,调用 OS_TaskRdy() 进行处理。如果没有,什么都不用做。
  • 然后执行任务调度。
void OSTimeTick (void)
{
	/* 更新时基列表 */
	OS_TickListUpdate();
	
	/* 任务调度 */
	OSSched();  
}

4 时基列表的应用

4.1 主函数 main(app.c)

app.c 不用改动。

#include "ARMCM3.h"
#include "os.h"

#define  TASK1_STK_SIZE       128
#define  TASK2_STK_SIZE       128
#define  TASK3_STK_SIZE       128

static   CPU_STK   Task1Stk[TASK1_STK_SIZE];
static   CPU_STK   Task2Stk[TASK2_STK_SIZE];
static   CPU_STK   Task3Stk[TASK3_STK_SIZE];

static   OS_TCB    Task1TCB;
static   OS_TCB    Task2TCB;
static   OS_TCB    Task3TCB;

uint32_t flag1;
uint32_t flag2;
uint32_t flag3;

void Task1 (void *p_arg);
void Task2 (void *p_arg);
void Task3 (void *p_arg);

int main (void)
{
	OS_ERR err;
	
	/* 初始化相关的全局变量,创建空闲任务 */
	OSInit(&err);
	
	/* CPU 初始化:初始化时间戳 */
	CPU_Init();
	
	/* 关中断,因为此时 OS 未启动,若开启中断,那么 SysTick 将会引发中断 */
	CPU_IntDis();
	
	/* 初始化 SysTick,配置 SysTick 为 10ms 中断一次,Tick = 10ms */
	OS_CPU_SysTickInit(10);
	
	/* 创建任务 */
	OSTaskCreate ((OS_TCB*)      &Task1TCB, 
	              (OS_TASK_PTR)  Task1, 
	              (void *)       0,
				  (OS_PRIO)		 1,
	              (CPU_STK*)     &Task1Stk[0],
	              (CPU_STK_SIZE) TASK1_STK_SIZE,
	              (OS_ERR *)     &err);

	OSTaskCreate ((OS_TCB*)      &Task2TCB, 
	              (OS_TASK_PTR)  Task2, 
	              (void *)       0,
				  (OS_PRIO)		 2,
	              (CPU_STK*)     &Task2Stk[0],
	              (CPU_STK_SIZE) TASK2_STK_SIZE,
	              (OS_ERR *)     &err);
				  
	OSTaskCreate ((OS_TCB*)      &Task3TCB, 
	              (OS_TASK_PTR)  Task3, 
	              (void *)       0,
				  (OS_PRIO)		 3,
	              (CPU_STK*)     &Task3Stk[0],
	              (CPU_STK_SIZE) TASK3_STK_SIZE,
	              (OS_ERR *)     &err);
	
	/* 启动OS,将不再返回 */				
	OSStart(&err);
}

void Task1 (void *p_arg)
{
	for (;;)
	{
		flag1 = 1;
		OSTimeDly (2);	
		flag1 = 0;
		OSTimeDly (2);
	}
}

void Task2 (void *p_arg)
{
	for (;;)
	{
		flag2 = 1;
		OSTimeDly (2);		
		flag2 = 0;
		OSTimeDly (2);
	}
}

void Task3 (void *p_arg)
{
	for (;;)
	{
		flag3 = 1;
		OSTimeDly (2);		
		flag3 = 0;
		OSTimeDly (2);
	}
}

4.2 运行过程

4.2.1 在主函数中

  • 系统初始化:初始化各种全局变量,初始化优先级表,初始化就绪列表,初始化时基列表,初始化空闲任务(包括初始化空闲任务栈和空闲任务 TCB)。
  • CPU 初始化:暂为空。
  • 关中断:因为此时 OS 未启动,若开启中断,那么 SysTick 将会引发中断,打断初始化流程。
  • 初始化 SysTick:配置 SysTick 为 10ms 中断一次,Tick = 10ms。
  • 创建任务:包括创建任务栈和任务 TCB,以及将 TCB 插入到就绪列表中,在优先级表对应位置置位。
  • 启动系统:先找到最高优先级,然后开始运行最高优先级对应的任务(最高优先级为 1,即为 Task1),启动第一次任务切换(此时将完成最后的初始化流程,即有关 PendSV 的中断优先级配置,接着触发 PendSV 异常,发起任务切换),将 CPU 占有权交给任务 Task1。

4.2.2 在 Task1 中

  • flag1 = 1。
  • 执行到阻塞函数 OSTimeDly:将 Task1 的 TCB 插入到时基列表中(TickCtrMatch = 2),将就绪列表中的 TCB 移除(同时在优先级表中的相应位置,即优先级 1 的位置清零),然后启动任务调度。
  • 执行任务调度 OSSched:任务调度器先找到最高优先级,然后再找到最高优先级的任务。 TCB。如果发现该任务就是当前任务,则不进行任务切换。在本案例中发现最高优先级为 2,对应任务是 Task2,不是当前任务,则发起任务切换(发起 PendSV 异常)。
  • PendSV 异常处理程序:保存 Task1 的状态,加载 Task2 的状态,更新全局变量的值。

4.2.3 在 Task2 中

  • flag2 = 1。
  • 执行到阻塞函数 OSTimeDly:将 Task2 的 TCB 插入到时基列表中(TickCtrMatch = 2),将就绪列表中的 TCB 移除(同时在优先级表中的相应位置,即优先级 2 的位置清零),然后启动任务调度。
  • 执行任务调度 OSSched:任务调度器先找到最高优先级,然后再找到最高优先级的任务 TCB。如果发现该任务就是当前任务,则不进行任务切换。在本案例中发现最高优先级为 3,对应任务是 Task3,不是当前任务,则发起任务切换(发起 PendSV 异常)。
  • PendSV 异常处理程序:保存 Task2 的状态,加载 Task3 的状态,更新全局变量的值。

4.2.4 在 Task3 中

  • flag3 = 1。
  • 执行到阻塞函数 OSTimeDly:将 Task3 的 TCB 插入到时基列表中(TickCtrMatch = 2),将就绪列表中的 TCB 移除(同时在优先级表中的相应位置,即优先级 3 的位置清零),然后启动任务调度。
  • 执行任务调度 OSSched:任务调度器先找到最高优先级,然后再找到最高优先级的任务 TCB。如果发现该任务就是当前任务,则不进行任务切换。在本案例中发现最高优先级为 31,对应任务是 空闲任务,不是当前任务,则发起任务切换(发起 PendSV 异常)。
  • PendSV 异常处理程序:保存 Task3 的状态,加载空闲任务的状态,更新全局变量的值。

注意,此时时基计数器为 0,三个任务的延时时间都是 2 个 SysTick 周期,OSCfg_TickWheelSize = 17,因此均位于 OSCfg_TickWheel[2] 下的双向链表。

4.2.5 在空闲任务中 SysTick 发起中断

  • 执行 OSTimeTick:时基计数器加一(OSTickCtr = 1),检查时基列表(OSCfg_TickWheel[1]),发现各个任务的延时未到期(TickCtrMatch = 2)。最后发起任务调度,发现不用进行任务切换,空闲任务继续运行。
  • 再次发起中断,执行 OSTimeTick:时基计数器加一(OSTickCtr = 2),检查时基列表(OSCfg_TickWheel[2]),发现各个任务的延时已到期(TickCtrMatch = 2),将它们全部置为就绪态。置为就绪态的过程是:在时基列表中删除对应 TCB,在就绪列表中加入对应 TCB(同时将 Task1、Task2、Task3 的优先级在优先级表中的相应位置重新置位)。最后发起任务调度,发现最高优先级为 1,对应的是 Task1,切换到 Task1 运行。

4.2.6 又在 Task1 中

  • 将全局变量 flag1 由 1 变成 0。
  • 执行到阻塞函数 OSTimeDly:将 Task1 的 TCB 插入到时基列表中(TickCtrMatch = 4),将就绪列表中的 TCB 移除(同时在优先级表中的相应位置,即优先级 1 的位置清零),然后启动任务调度。
  • 执行任务调度 OSSched:任务调度器先找到最高优先级,然后再找到最高优先级的任务 TCB。如果发现该任务就是当前任务,则不进行任务切换。在本案例中发现最高优先级为 2,对应任务是 Task2,不是当前任务,则发起任务切换(发起 PendSV 异常)。
  • PendSV 异常处理程序:保存 Task1 的状态,加载 Task2 的状态,更新全局变量的值。

如此反复后,三个任务又一次进入阻塞态,此时时基计数器为 2(OSTickCtr = 2),三个任务的延时时间都是 2 个 SysTick 周期(TickCtrMatch = 4),OSCfg_TickWheelSize = 17,因此均位于 OSCfg_TickWheel[4] 下的双向链表。 接下来的步骤跟上面的类似,在发起第四次 SysTick 中断时(OSTickCtr = 4),检查时基列表(OSCfg_TickWheel[4]),发现三个任务已经延时完毕,于是它们又进入到就绪态中。

4.3 实验现象

实验现象与上一篇记录是一样的,也符合以上分析,如图所示:

image

posted @ 2022-02-13 11:50  漫舞八月(Mount256)  阅读(475)  评论(0编辑  收藏  举报