μC/OS-II——软件定时器

uCOS-ii 软件定时器

最近学习嵌入式操作系统,见过了很多RTOS之后,最本质的东西也就那点东西。无论是FreeRTOS还是μC/OS-II-III、鸿蒙、RT_Thread等等,内核层面的实现机制大同小异。想从最基本的底层原理学习一个OS的设计思想和实现原理。μC/OS-II是最好的学习对象。
μC/OS-II 嵌入式操作系统属于微内核的RTOS,1992年由美国人推出。To date, μC/OS-III已经出现了。无论各种RTOS时怎么变,一些设计思想和实现方法都不会变的。我一直认为OS是一种很有技术和智慧的软件产品,OS主要功能是管理硬件、提供服务。实现机制无非就是一些数据结构和算法。
本文只对μC/OS-II 的软件定时器功能进行讲解,因为有项目用到,后续会对每个部分的功能进行学习。主要通过博客记录一些自己学习的历程和经验,在CSDN分享自己的学习经验和历程。

功能介绍

软件定时器属于OS提供的一种服务和功能,其作用就是定时,到特定的时间时触发相应的操作(一般以回调函数的方式实现)。
μC/OS-II 参考说明如下:

Timers are down counters that perform an when the counter reaches zero. The user action provides the action through a function (or simply ). A callback is a user-declared function that will be called when the timer expires.
Timers are useful in protocol stacks (re-transmission timers, for example), and can also be used to poll I/O devices at predefined intervals.

总觉得英文原版对于理解会更加切意一点,建议大家多去读英文技术手册原版。

μC/OS-II——软件定时器_第1张图片

定时器的状态切换主要靠内核API实现,这些函数在 os_tmr.c 中可以找的到。

实现原理

Timer属于内核对象,内核以 OS_TMR 表示一个软件定时器,又可叫做定时器控制块。其数据结构如下:

// An highlighted block
// 定时器控制块
typedef  struct  os_tmr {
    INT8U            OSTmrType;                       /* Should be set to OS_TMR_TYPE                  */
    OS_TMR_CALLBACK  OSTmrCallback;                   /* Function to call when timer expires                      */
    void            *OSTmrCallbackArg;                /* Argument to pass to function when timer expires  */
    void            *OSTmrNext;                       /* Double link list pointers                                     									*/
    void            *OSTmrPrev;
    INT32U           OSTmrMatch;                      /* Timer expires when OSTmrTime == OSTmrMatch    */
    INT32U           OSTmrDly;                        /* Delay time before periodic update starts              */
    INT32U           OSTmrPeriod;                     /* Period to repeat timer                                        */
#if OS_TMR_CFG_NAME_EN > 0u
    INT8U           *OSTmrName;                       /* Name to give the timer                                        */
#endif
    INT8U            OSTmrOpt;                        /* Options (see OS_TMR_OPT_xxx)                                  */
    INT8U            OSTmrState;                      /* Indicates the state of the timer:  */
                                                      /* OS_TMR_STATE_UNUSED                               
                                                      /* OS_TMR_STATE_RUNNING                                      */
                                    				  /* OS_TMR_STATE_STOPPED                    */
}OS_TMR;

一个定时器大体由 3 部分组成:定时时间,回调函数和属性。当定时时间到了的话,就进行一次回调函数的处理,定时器属性说明定时器是周期性的定时还是只做一次定时。如果用户使能了 OS_TMR_EN,ucosii 会在内部创建一个定时器任务,负责处理各个定时器。这个任务一般应该由硬件定时器的中断函数中调用 OSTmrSignal()去激活。所以从本质上说 os_tmr.c 中的定时器是由一个硬件定时器分化出来的。默认情况下是由 SysTick 中断里通过 OSTimeTickHook()去激活定时器任务的。
μC/OS-II——软件定时器_第2张图片

μC/OS-II 的软件定时器算法分析

μC/OS-II使用三种数据结构来管理软件定时器,也就是两个结构体数组,一个结构体指针

// μC/OS-II 软件// 以数组的形式静态分配定时器控制块所需的 RAM 空间,并存储所有已建立的定时器控制块。定时器中实现了 3 类链表的维护:
OS_EXT OS_TMR OSTmrTbl[OS_TMR_CFG_MAX]; /* Table containing pool of timers /
OS_EXT OS_TMR * OSTmrFreeList; /
Pointer to free list of timers */
OS_EXT OS_TMR_WHEEL OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE];

typedef  struct  os_tmr_wheel {
    OS_TMR          *OSTmrFirst;                      /* Pointer to first timer in linked list  */
    INT16U           OSTmrEntries;
} OS_TMR_WHEEL;

宏 OS_TMR_CFG_WHEEL_SIZE 定义了 OSTmr-WheelTbl[]数组的大小,同时这个值也
是定时器分组的依据。按照定时器到时值与 OS_TMR_CFG_WHEEL_SIZE 相除的余数进行分组:不同余数的定时器放在不同分组中;相同余数的定时器处在同一组中,由双向链表连接。这样,余数值为 0~OS_TMR_CFG_WHEEL_SIZE-1 的不同定时器控制块,正好分别对应了数组元素 OSTmr-WheelTbl[0]~OSTmrWheelTbl[OS_TMR_CFGWHEEL_SIZE-1]的不同分组。每次时钟节拍到来时,时钟数 OSTmrTime 值加 1,然后也进行求余操作,只有余数相同的那组定时器才有可能到时,所以只对该组定时器进行判断。这种方法比循环判断所有定时器更高效。随着时钟数的累加,处理的分组也由 0~OS_TMR_CFG_WHE EL_SIZE-1循环。
信号量唤醒定时器管理任务,计算出当前所要处理的分组后,程序遍历该分组中的所有
控制块,将当前 OSTmr-Time 值与定时器控制块中的到时值相比较。若相等(即到时),则调用该定时器到时回调函数;若不相等,则判断该组中下一个定时器控制块。如此操作,直到该分组链表的结尾。定时器管理任务的流程如下图所示。 OS_TMR_CFG_WHEEL_SIZE 的取值推荐为 2 的 N 次方,以便采用移位操作计算余数,缩短处理时间。

μC/OS-II——软件定时器_第3张图片
仔细理解上图表示的意思, OSTmrWheelTbl[index]表示已经存在的定时器,free指向空闲定时器可用于生成定时器,OSTmrTbl[SIZE]是整个定时器池。

代码实现

// 定时器管理任务
static  void  OSTmr_Task (void *p_arg)
{
	// 0. 开始 临时变量的定义
	INT8U err;
	
	for(; ; )
	{
		// 1.OSTmrSemSignal 触发释放信号量
		OSSemPend(OSTmrSemSignal, 0, &err);
		// 2.系统调度上锁
		OSSchedLock();
		// 3.系统节拍计数器加1
		OSTmrTime++;
		// 4.确定本次到时时要处理的分组
		spoke = OSTmrTime % OS_TMR_CFG_WHEEL_SIZE ;
		// 5.选定分组
		OS_TMR_WHEEL *pspoke = &OSTmrWheelTbl[spoke];
		// 6.获得分组要处理的第一个定时器
		OS_TMR *ptmr = pspoke->OSTmrFirst;
		
		while (ptmr != (OS_TMR *) 0)	//指针有效
		{
			OS_TMR *ptmr_next = ptmr->OSTmrNext;	// 获得下一个定时器
			
			if( OSTmrTime == ptmr->OSTmrMatch)	// 判断此定时器是否到时
			{
				// 移除此定时器
				OSTmr_Unlink(ptmr);
				if (ptmr->OSTmrOpt == OS_TMR_OPT_PERIODIC)	//重新执行
				{
					OSTmr_Link(ptmr, OS_TMR_LINK_PERIODIC);
				}
				else
				{
					ptmr->OSTmrState = OS_TMR_STATE_COMPLETED
				}
				
				// 执行到时回调函数
				OS_TMR_CALLBACK  pfnct = ptmr->OSTmrCallback;
				if (pfnct != (OS_TMR_CALLBACK)0)
				{
					(*pfnct)((void *)ptmr, ptmr->OSTmrCallbackArg);
				}
			}
			ptmr = ptmr_next;		// 
		}
		OSSchedUnlock();
	}
}

内核是如何管理定时器控制块的?双向链表。对应着就存在链表的几种基本的操作:插入、删除、初始化。链表的插入和删除,前项指针和后向指针的移动,总让人混乱。

// 定时器链表的插入
// 定时器下次到时的 OSTmrTime 值=定时器定时值+当前 OSTmrTime 值新的分组=定时器下次到时的 OSTmrTime 值%OS_TMR_CFG_WHEEL_SIZE
static void OSTmr_Link(OS_TMR *ptmr, INT8U type)
{
	/*
		插入说明次定时器需要去定时超时处理
	*/
	OS_TMR			*ptmr1;
	OS_TMR_WHEEL	*pspoke;
	INT16U 		spoke;
	
	ptmr->OSTmrState = OS_TMR_STATE_RUNNING;
	
	if(type == OS_TMR_LINK_PERIODIC)  // 周期定时
	{
		ptmr->OSTmrMatch = ptmr->OSTmrPeriod +  OSTmrTime
	}
	else
	{
		if(ptmr->OSTmrDly == 0u)
		{
			ptmr->OSTmrMatch = ptmr->OSTmrPeriod + OSTmrTime;
		}
		else
		{
			ptmr->OSTmrMatch = ptmr->OSTmrDly    + OSTmrTime;
		}
	}
	
	// 1.获得届满定时器所在的分组
	spoke = ptmr->OSTmrMatch % OS_TMR_CFG_WHEEL_SIZE;
	pspoke =  &OSTmrWheelTbl[spoke];
	
	if(pspoke->OSTmrFirst == (OS_TMR *)0)	// 次分组空
	{
		pspoke->OSTmrFirst   = ptmr;
		ptmr->OSTmrNext      = (OS_TMR *)0;
		pspoke->OSTmrEntries = 1u;
	}
	else	// 链表指针移动
	{
		ptmr1                 = pspoke->OSTmrFirst; 
		pspoke->OSTmrFirst   = ptmr;	
		ptmr->OSTmrNext      = (void *)ptmr1;
		ptmr1->OSTmrPrev     = (void *)ptmr;
		pspoke->OSTmrEntries++;
	}
	
	ptmr->OSTmrPrev = (void *)0;	// ptmr处于当前最新的第一个位置
}

定时器链表的删除


// 定时器链表的删除
static  void  OSTmr_Unlink (OS_TMR *ptmr)
{
	/*
		ptmr1->OSTmrPrev = 
		ptmr1->OSTmrNext = ptmr->prev;
		ptmr2->OSTmrPrev = ptmr->next
		ptmr2->Next = 
	*/
	OS_TMR        *ptmr1;
	OS_TMR        *ptmr2;
	OS_TMR_WHEEL  *pspoke;
	INT16U         spoke;
	
	spoke  = (INT16U)(ptmr->OSTmrMatch % OS_TMR_CFG_WHEEL_SIZE);
	pspoke = &OSTmrWheelTbl[spoke];		// 已经存在的定时器轮子pool中处理

	if(pspoke->OSTmrFirst == ptmr)	// 运气好,第一个就是要删除的
	{
		ptmr1	= ptmr->OSTmrNext;	// 第二个定时器
		pspoke->OSTmrFirst = (OS_TMR *)ptmr1;
		if (ptmr1 != (OS_TMR *)0)
		{
			ptmr1->OSTmrPrev = (void *)0;		// 此时,之前的第二个定时器变为第一个
		}
	}
	else	// 运气不好,ptmr在中间
	{
		// 1.先断开保存
		ptmr1			  = (OS_TMR *)ptmr->OSTmrPrev;
		ptmr2            = (OS_TMR *)ptmr->OSTmrNext;
		// 2.链接
		ptmr1->OSTmrNext = ptmr2;	// 将 N-1 与 N+1 链接
		if(ptmr2 != (OS_TMR *)0)
		{
			ptmr2->OSTmrPrev = (void *) ptmr1;
		}
	}
	
	// 更新 ptmr 的状态信息
	 ptmr->OSTmrState = OS_TMR_STATE_STOPPED;
	 ptmr->OSTmrNext  = (void *)0;
	 ptmr->OSTmrPrev  = (void *)0;
	 pspoke->OSTmrEntries--;
}

总结

OS提供的服务和功能一般以API函数的形式提供给外界用户,其实现的方式一般就是数据结构和算法的方法。问题是如何构造这些合适的、恰当的数据结构,效率高简便的算法是很重要的方面。每一个软件产品和软件模块都应该有它自己的实现方案和设计思想,代码背后的一些东西才是学习需要探索和挖掘的。
最近在工作中一些项目功能和µC/OS-II提供的功能很相似,就想着能不能用OS来实现,顺便重新学习了一下RTOS。把基本原理搞懂,工作、做项目才能安心的使用。

你可能感兴趣的:(ARM,嵌入式,c语言,os,rtos,嵌入式)