UCOSIII -- 05阻塞延时与空闲任务

在上一章节中,任务体内的延时使用的是软件延时,即还是让CPU 空等来达到延时的效果。使用RTOS 的很大优势就是榨干CPU 的性能,永远不能让它闲着,任务如果需要延时也就不能再让CPU 空等来实现延时的效果。

1、什么是阻塞延时?

阻塞延时就是当任务需要延时的时候,任务会放弃CPU 的使用权,CPU 可以去干其他的事情,当任务延时时间到,重新获取 CPU 使用权,任务继续运行,这样就充分地利用了CPU 的资源,而不是干等着。

2、什么是空闲任务?

当任务需要延时,进入阻塞状态,那 CPU 又去干什么事情了?如果没有其他任务可以运行,RTOS 通常会为 CPU 创建一个空闲任务,这个时候 CPU 就运行空闲任务。在 μC/OS-III 中,空闲任务是系统在初始化的时候创建的优先级最低的任务,空闲任务主体很简单,只是对一个全局变量进行计数。鉴于空闲任务的这种特性,在实际应用中,当系统进入空闲任务的时候,可在空闲任务中让单片机进入休眠或者低功耗等操作。

μC/OS中的两个系统任务分别是空闲任务和统计任务。

3、实现空闲任务

1、定义空闲任务栈

空闲任务栈在 os_cfg_app.c(os_cfg_app.c 第一次使用需要自行在文件夹 μC/OS-IIISource 中新建并添加到工程的μC/OS-III Source 组)文件中定义。

CPU_STK        OSCfg_IdleTaskStk   [OS_CFG_IDLE_TASK_STK_SIZE];

声明在 os_cfg_app.h 中。

#ifndef OS_CFG_APP_H
#define OS_CFG_APP_H

/*
************************************************************************************************************************
*                                                      常量
************************************************************************************************************************
*/

/* 空闲任务堆栈大小 */
#define  OS_CFG_IDLE_TASK_STK_SIZE       128u

#endif  /* OS_CFG_APP_H */

空闲任务的栈是一个定义好的数组, 大小由OS_CFG_IDLE_TASK_STK_SIZE 这个宏来控制。OS_CFG_IDLE_TASK_STK_SIZE 在os_cfg_app.h 这个头文件定义,大小为128字节。

这里有与普通任务不一样的一点是 空闲任务的栈的起始地址和大小均被定义成一个常量,不能被修改。 在 os_cfg_app.c 文件中进行定义。

/*
************************************************************************************************************************
*                                                      CONSTANTS
************************************************************************************************************************
*/

/* 空闲任务堆栈起始地址 */
CPU_STK      * const  OSCfg_IdleTaskStkBasePtr   = (CPU_STK    *)&OSCfg_IdleTaskStk[0];
/* 空闲任务堆栈大小 */
CPU_STK_SIZE   const  OSCfg_IdleTaskStkSize      = (CPU_STK_SIZE)OS_CFG_IDLE_TASK_STK_SIZE;

此外,变量 OSCfg_IdleTaskStkBasePtr 和 OSCfg_IdleTaskStkSize 同时还在 os.h 中声明,这样就具有全局属性,可以在其他文件里面被使用。

/*
************************************************************************************************************************
************************************************************************************************************************
*                                                   外部声明
************************************************************************************************************************
************************************************************************************************************************
*/

/* 空闲任务堆栈起始地址 */
extern CPU_STK      * const  OSCfg_IdleTaskStkBasePtr;
/* 空闲任务堆栈大小 */
extern CPU_STK_SIZE   const  OSCfg_IdleTaskStkSize;

2、定义空闲任务函数

空闲任务正如其名,空闲,任务体里面只是对全局变量 OSIdleTaskCtr ++ 操作。
空闲任务函数定义在 os_core.c 文件中。

/* 空闲任务 */
void  OS_IdleTask (void  *p_arg)
{
	/* 自己给自己赋值是为了防止编译器警告 */
	p_arg = p_arg; 
	
	/* 空闲任务什么都不做,只对全局变量OSIdleTaskCtr ++ 操作 */
	for(;;)
	{
		OSIdleTaskCtr++;
	}
}

全局变量 OSIdleTaskCtr 称为空闲任务计数变量。在 os.h 中定义。

OS_EXT    OS_IDLE_CTR    OSIdleTaskCtr;//空闲任务计数变量

其中的 OS_IDLE_CTR 是在 os_type.h 中重新定义的数据类型。

typedef   CPU_INT32U      OS_IDLE_CTR;

3、定义空闲任务控制块

在这里插入图片描述

OS_EXT    OS_TCB         OSIdleTaskTCB;

需要特别注意的是:这里的 OS_TCB 中多了一个成员 TaskDelayTicks,它的作用在后面的 实现阻塞延时 中会被提到。

typedef  struct  os_tcb              OS_TCB;
struct os_tcb
{
	CPU_STK         *StkPtr;
	CPU_STK_SIZE    StkSize;
	
	/* 任务延时周期个数 */
	OS_TICK         TaskDelayTicks;
};

OS_TICK是在os_type.h中重定义的变量。

typedef   CPU_INT32U      OS_TICK;

4、空闲任务初始化 – 包括空闲任务创建函数

该函数在 os_core.c 中定义。

/* 空闲任务初始化 */
void  OS_IdleTaskInit(OS_ERR  *p_err)
{	
	/* 初始化空闲任务计数器 ,我们知道,这个是预先在os.h 中定义好的全局变量。初始化时让其初值为 0 */
	OSIdleTaskCtr = (OS_IDLE_CTR)0;
	
	/* 创建空闲任务 -- 把栈,TCB,任务函数联系在一起 */
	OSTaskCreate( (OS_TCB     *)&OSIdleTaskTCB,            // 空闲任务控制块
			      (OS_TASK_PTR )OS_IdleTask,               // 空闲任务主体
			      (void       *)0,                         // 形参设置为0,即 NULL
			      (CPU_STK    *)OSCfg_IdleTaskStkBasePtr,  // 空闲任务堆栈的基地址
			      (CPU_STK_SIZE)OSCfg_IdleTaskStkSize,     // 空闲任务堆栈的大小
			      (OS_ERR     *)p_err );                   // 错误的返回码
}

5、在 OSInit 函数中调用空闲任务初始化函数 OS_IdleTaskInit

在前面,普通任务的任务创建函数是在 main 函数中被调用的。这里是 OSInit 中被调用。这么做的目的是表明在系统还没有启动之前空闲任务就已经创建好。

OSInit 函数也在 os_core.c 中定义。

/* RTOS初始化
** 初始化全局变量
*/
void OSInit (OS_ERR *p_err)
{
	/* 配置OS初始状态为停止态 */
	OSRunning =  OS_STATE_OS_STOPPED;
	
	/* 初始化两个全局TCB,这两个TCB用于任务切换 */
	OSTCBCurPtr = (OS_TCB *)0;
	OSTCBHighRdyPtr = (OS_TCB *)0;
	
	/* 初始化就绪列表 */
	OS_RdyListInit();
	
	/* 初始化空闲任务 */
	OS_IdleTaskInit(p_err);
	if (*p_err != OS_ERR_NONE) 
	{
        return;
    }
}

6、实现阻塞延时

阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离 CPU 使用权,然后进入阻塞状态,直到延时结束,任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间,CPU 可以去执行其他的任务,如果其他的任务也在延时状态,那么 CPU 就将运行空闲任务。

阻塞延时函数在 os_time.c 中定义并实现。

/* 阻塞延时 */
void  OSTimeDly(OS_TICK dly)
{
	/* 设置延时时间 */
	OSTCBCurPtr->TaskDelayTicks = dly;
	
	/* 进行任务调度 */
	OSSched();	
}

TaskDelayTicks 是任务控制块的一个成员,用于记录任务需要延时的时间,单位为 SysTick 的中断周期。比如当SysTick 的中断周期为 10 ms,调用 OSTimeDly(2) 则完成 2*10ms 的延时。

函数 OSSched() 需要重新实现,在 os_core.c 中。

/* 任务切换,实际就是触发PendSV异常,然后在PendSV异常中进行上下文切换 */
void OSSched(void)
{	
	/* 如果当前任务是空闲任务,那么就去尝试执行任务1或者任务2,看看他们的延时时间是否结束
	   如果任务的延时时间均没有到期,那就返回继续执行空闲任务 */
	if( OSTCBCurPtr == &OSIdleTaskTCB )
	{
		if(OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
		{
			OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
		}
		else if(OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
		{
			OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
		}
		else
		{
			return;		/* 任务延时均没有到期则返回,继续执行空闲任务 */
		} 
	}
	else
	{
		/*如果是task1或者task2的话,检查下另外一个任务,如果另外的任务不在延时中,就切换到该任务
        否则,判断下当前任务是否应该进入延时状态,如果是的话,就切换到空闲任务。否则就不进行任何切换 */
		if(OSTCBCurPtr == OSRdyList[0].HeadPtr)
		{
			if(OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
			{
				OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
			}
			else if(OSTCBCurPtr->TaskDelayTicks != 0)
			{
				OSTCBHighRdyPtr = &OSIdleTaskTCB;
			}
			else 
			{
				return;		/* 返回,不进行切换,因为任务一结束延时等待状态 */
			}
		}
		else if(OSTCBCurPtr == OSRdyList[1].HeadPtr)
		{
			if(OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
			{
				OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
			}
			else if(OSTCBCurPtr->TaskDelayTicks != 0)
			{
				OSTCBHighRdyPtr = &OSIdleTaskTCB;
			}
			else 
			{
				return;		/* 返回,不进行切换,因为任务二结束延时等待状态 */
			}
		}
	}
	
	/* 任务切换 */
	OS_TASK_SW();
}

主要操作时通过当前控制块的指针 OSTCBCurPtr (指向当前正在运行的一个任务)和最高优先级控制块的指针 OSTCBHighRdyPtr (指向下一个要运行的任务)来进行控制和判断的。

画图来理解下上述代码。
在这里插入图片描述

7、编写 main 函数

int main(void)
{	
	OS_ERR err;
	
	/* 关闭中断 */
	CPU_IntDis();
	
	/* 配置SysTick 10ms 中断一次 */
	OS_CPU_SysTickInit (10);
	
	/* 初始化相关的全局变量 */
	OSInit(&err);
	
	/* 创建任务 */
	OSTaskCreate ((OS_TCB*)      &Task1TCB, 
	              (OS_TASK_PTR ) Task1, 
	              (void *)       0,
	              (CPU_STK*)     &Task1Stk[0],
	              (CPU_STK_SIZE) TASK1_STK_SIZE,
	              (OS_ERR *)     &err);

	OSTaskCreate ((OS_TCB*)      &Task2TCB, 
	              (OS_TASK_PTR ) Task2, 
	              (void *)       0,
	              (CPU_STK*)     &Task2Stk[0],
	              (CPU_STK_SIZE) TASK2_STK_SIZE,
	              (OS_ERR *)     &err);
				  
	/* 将任务加入到就绪列表 */
	OSRdyList[0].HeadPtr = &Task1TCB;
	OSRdyList[1].HeadPtr = &Task2TCB;
	
	/* 启动OS,将不再返回 */				
	OSStart(&err);
}

需要注意:空闲任务初始化函数在OSInint 中调用,在系统启动之前创建好空闲任务。

8、修改 任务一 和 任务二 函数

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

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

9、这一切最为关键的主线其实还是 时基 SysTick

在 SysTick 中断服务函数中

/* SysTick 中断服务函数 */
void SysTick_Handler(void)
{
	OSTimeTick();
}

而 OSTimeTick();

void  OSTimeTick (void)
{
	unsigned int i;
	
	/* 扫描就绪列表中所有任务的TaskDelayTicks,如果不为0,则减1 */
	for(i=0; i<OS_CFG_PRIO_MAX; i++)
	{
		if(OSRdyList[i].HeadPtr->TaskDelayTicks > 0)
		{
			OSRdyList[i].HeadPtr->TaskDelayTicks --;
		}
	}
	
	/* 任务调度 */
	OSSched();
}

需要注意这个任务调度是非常有必要的,如果不进行任务调度,执行完 for 循环之后就结束了,直到发生下一次的SysTick中断,再来执行 for 循环,以此往复,但是就是不切换回到任务。所以说这个任务调度函数 OSSched() 是非常重要的。

总结

为了方便下次看到能迅速反应过来,我把自己理解的程序运行流程再详细描述如下。

1、程序总是从main函数开始执行,在main函数中配置了 SysTick 以及初始化了 OSInit(&err);以后,启动OS,程序将不再返回。(注意:当 SysTick 初始化完成之后,计时就开始了,只不过在程序一开始的时候 SysTick 中断被关闭了,在 系统启动 OSStart() 函数里面的 OSStartHighRdy() 中才被重新开启。

2、程序首先执行任务一,当执行完 flag1 = 1;开始执行 OSTimeDly(2); 在这个程序中设置了任务一的延时时间以及为了实现阻塞延时做了任务切换到了任务二,然后任务二开始执行,任务一失去 CPU ,同样的, 当任务二执行完 flag2 = 1;开始执行 OSTimeDly(2); 在这个程序中设置了任务二的延时时间以及为了实现阻塞延时做了任务切换到了空闲任务(因为任务一仍然还处在阻塞状态),随着第一次 SysTick 中断产生(10ms时间到),系统执行中断服务程序 OSTimeTick,将每个任务的 TaskDelayTicks 减去 1 ,之后,通过任务调度函数 OSSched() 才切换到空闲任务(因为此时任务一和任务二依然处于阻塞状态)。当第二次 SysTick 中断产生(又一个10ms时间到),系统再次执行中断服务程序 OSTimeTick,将每个任务的 TaskDelayTicks 再减去 1 ,之后通过任务切换发现,任务一的TaskDelayTicks等于0(其实任务二的 TaskDelayTicks 也等于 0 了,但是在程序中的 if 条件语句中优先处理任务一了,通过颠倒顺序,仿真结果验证了,我在最后贴图。)之后,任务一再次往下执行,执行到 flag1 = 0 后,开始执行 OSTimeDly(2); 再次进入延时阻塞状态,才切换到任务二 …

好了,程序过程描述至此。

仿真验证疑问

if( OSTCBCurPtr == &OSIdleTaskTCB )
{
	if(OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
	{
		OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
	}
	else if(OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
	{
		OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
	}
	else
	{
		return;		/* 任务延时均没有到期则返回,继续执行空闲任务 */
	} 
}

在这里插入图片描述
调整顺序

if( OSTCBCurPtr == &OSIdleTaskTCB )
{
	if(OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
	{
		OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
	}
	else if(OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
	{
		OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
	}
	else
	{
		return;		/* 任务延时均没有到期则返回,继续执行空闲任务 */
	} 
}

在这里插入图片描述
需要注意的是,

1、从宏观上来看,任务一和任务二看似是同时运行的,但是实际上任务间还是有先后之分的。

2、在实际使用 μC/OS 时,我们并不会这样使用延时阻塞。我们会插入一个 时基列表 来进行维护。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是一些可能的uCOS-III多任务创建实验思考题及其答案,供您参考: 1. uCOS-III提供了哪些函数和数据类型来创建任务? 答:uCOS-III提供了以下函数和数据类型来创建任务: - OS_TASK_CREATE_EXT()函数:用于创建任务,并指定任务的优先级、栈空间、入口函数等参数。 - OS_PRIO数据类型:表示任务的优先级。 - OS_TCB数据类型:表示任务控制块,用于存储任务的信息。 - OSTaskCreateExtHook函数指针:可以在任务创建时执行一些额外的操作,如设置任务名称、堆栈检查等。 2. 如何理解任务的优先级和时间片轮转? 答:任务的优先级表示任务在系统中的重要程度或执行顺序,优先级越高的任务将优先执行。uCOS-III采用的是优先级抢占式调度,即当一个优先级更高的任务就绪时,将立即抢占当前任务的执行权。时间片轮转是指当多个任务优先级相同时,系统会为每个任务分配一个时间片,并按照轮流执行的方式来切换任务。这样可以保证所有任务都能得到执行,避免优先级低的任务长时间得不到执行。 3. 在多任务环境下,如何保证共享资源的安全性? 答:在多任务环境下,多个任务可能同时访问同一个共享资源,如全局变量、队列、信号量等。为了保证共享资源的安全性,可以使用以下方法: - 禁止中断:在访问共享资源时,可以关闭中断,这样可以防止其他任务干扰当前任务的执行,但会影响系统的实时性。 - 使用信号量:信号量是一种同步机制,可以用于多个任务之间的同步和互斥。当一个任务需要访问共享资源时,可以申请一个信号量,当访问完成后释放信号量,这样可以保证同一时间只有一个任务访问共享资源。 - 使用互斥量:互斥量是一种特殊的信号量,可以用于实现临界区保护。当一个任务进入临界区时,可以申请一个互斥量,在退出临界区时释放互斥量,这样可以保证同一时间只有一个任务进入临界区。 4. 如何处理任务间的通信和同步? 答:在多任务环境下,任务之间需要进行通信和同步,以实现共同的任务目标。可以使用以下方法: - 队列:队列是一种常用的通信机制,可以用于任务间的数据传输。发送任务将数据发送到队列中,接收任务从队列中取出数据。队列可以实现任务间的异步通信,也可以使用信号量来实现同步通信。 - 信号量:信号量是一种同步机制,可以用于任务间的同步和互斥。发送任务将信号量的值减一,接收任务将信号量的值加一。当信号量的值为0时,发送任务会被阻塞,直到有接收任务将信号量的值加一。 - 事件标志组:事件标志组可以用于任务间的事件通知和同步。发送任务将事件标志组的某个标志位设置为1,接收任务等待相应的标志位被设置为1。当某个标志位被设置为1时,接收任务被唤醒并执行相应的操作,可以实现任务间的同步和事件通知。 - 信箱:信箱是一种特殊的队列,可以用于任务间的数据传输和同步。发送任务将数据发送到信箱中,接收任务从信箱中取出数据。当信箱为空时,接收任务会被阻塞,直到有发送任务将数据发送到信箱中。 5. 实验中可能会遇到哪些问题,如何解决? 答:在实验中可能会遇到以下问题: - 堆栈溢出:如果任务的堆栈空间不足,可能会导致堆栈溢出,影响系统的稳定性。可以通过设置堆栈空间大小、堆栈检查等方法来避免堆栈溢出。 - 优先级反转:当一个低优先级任务正在占用一个共享资源时,一个高优先级任务需要访问该共享资源,这时可能会导致优先级反转,影响系统的实时性。可以使用优先级继承、优先级反转解决方案等方法来避免优先级反转。 - 死锁:当多个任务互相等待对方释放资源时,可能会导致死锁,影响系统的稳定性。可以通过避免多个任务同时申请多个资源、使用超时机制等方法来避免死锁。 - 系统资源耗尽:当系统资源(如堆内存、任务控制块等)耗尽时,可能会导致系统不可用。可以通过增加系统资源、优化系统资源的使用等方法来避免系统资源耗尽的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xuechanba

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值