前言:
使用Liteos的develop版本,Runstop模式由于没有相关代码,如果想要实现uA级功耗的话,Runstop模式需要自己实现,实现的大概过程在我的另一篇(一)基于STM32L431的Liteos低功耗Runstop模式的实现有体现。
上一篇文章实现Runstop模式存在的问题:
①进入stop模式的时间必须大于至少50个tick,否则唤醒之后系统的任务调度会有问题。
②即使进入stop模式的时间大于50个tick,唤醒之后需要执行的任务并没有立刻执行,而是会有时间间隔,这个时间最长达到了312ms,那么系统的实时性就没有了,而且会增加功耗。
测试下来发现,如果是因为软件定时器唤醒的,软件定时器的回调函数能正常执行,软件定时器因为只需要修改头部节点的值就可以,所以不会有问题;如果是因为任务唤醒的,那么任务的执行会有时间间隔,并且这个间隔时间是不一定的。那么就说明是唤醒之后的任务tick补偿有问题。
正常运行时每次systick中断服务函数中会调用osTaskScan()函数,来扫描有没有要执行的任务,如果有就进行任务切换,关键代码:
g_stTskSortLink.usCursor = (g_stTskSortLink.usCursor + 1) % OS_TSK_SORTLINK_LEN;
pstListObject = g_stTskSortLink.pstSortLink + g_stTskSortLink.usCursor;
if (pstListObject->pstNext == pstListObject)
{
return;
}
for (pstTaskCB = LOS_DL_LIST_ENTRY((pstListObject)->pstNext, LOS_TASK_CB, stTimerList);&pstTaskCB->stTimerList != (pstListObject);) /*lint !e413*/
{
usTempStatus = pstTaskCB->usTaskStatus;
if (UWROLLNUM(pstTaskCB->uwIdxRollNum) > 0)
{
UWROLLNUMDEC(pstTaskCB->uwIdxRollNum);
break;
}
LOS_ListDelete(&pstTaskCB->stTimerList);
}
可以看到里面有一个关键的变量 g_stTskSortLink.usCursor每次中断都会加1,OS_TSK_SORTLINK_LEN值为32,而且使用osTaskNextSwitchTimeGet()函数来获取最近一个任务延时到来时间时,也用到了g_stTskSortLink.usCursor的值,但是我在之前任务任务的tick补偿的时候并没有关注这个值。在调用LOS_TaskDelay()函数进行任务延时的时候会调用osTaskAdd2TimerList()函数来把任务添加到延时链表中,在这个函数中也用到了g_stTskSortLink.usCursor的值,那么g_stTskSortLink.usCursor的值应该是导致唤醒后任务执行不及时的原因。
分析源码后发现,Liteos是通过一个环状的队列来判断任务延时是否到来的。具体实现如下图(以OS_TSK_SORTLINK_LEN等于10为例),假设当前g_stTskSortLink.usCursor的值为2,一个任务需要延时120ms,每个tick是5ms,也就是一共延时24个tick。每次systick中断g_stTskSortLink.usCursor值加1,那么24个tick过去之后,g_stTskSortLink.usCursor应该在6的位置。因此任务添加到时间链表中的位置应该是6。
g_stTskSortLink.usCursor当前的值为2,执行一轮再到2的时候过去了10个tick,也就是50ms。而对于任务来说,插入位置6之后任务控制块中UWROLLNUM(pstTaskCB->uwIdxRollNum)的值应该是2,因为每一轮是50ms,过两轮就是100ms,g_stTskSortLink.usCursor的值从2变为6需要经过4个tick,就是20ms,加起来就是任务需要延时的总时间120ms。
理解了这个之后就能对任务进行正确的Tick补偿。osIdxRollNumDec()函数要变为:
LITE_OS_SEC_TEXT_MINOR VOID osIdxRollNumDec(UINT32 Tick)
{
LOS_TASK_CB *pstTaskCB;
LOS_DL_LIST *pstListObject;
UINT32 uwIndex =0;
UINT16 uwRollTicks = Tick%OS_TSK_SORTLINK_LEN;
for (uwIndex = 0; uwIndex < OS_TSK_SORTLINK_LEN; uwIndex++)
{
UINT32 uwTaskTick = Tick;
pstListObject = g_stTskSortLink.pstSortLink + (g_stTskSortLink.usCursor + uwIndex)%OS_TSK_SORTLINK_LEN;
if (pstListObject->pstNext != pstListObject)
{
pstTaskCB = LOS_DL_LIST_ENTRY((pstListObject)->pstNext, LOS_TASK_CB, stTimerList);
uwTaskTick /= OS_TSK_SORTLINK_LEN;
if(uwTaskTick > UWROLLNUM(pstTaskCB->uwIdxRollNum))
{
uwTaskTick = UWROLLNUM(pstTaskCB->uwIdxRollNum);
}
UWROLLNUMDECMULT(pstTaskCB->uwIdxRollNum,uwTaskTick);
}
}
g_stTskSortLink.usCursor = (g_stTskSortLink.usCursor + uwRollTicks)%OS_TSK_SORTLINK_LEN;
}
任务延时补偿的函数没有问题之后还需要注意的是,通过osStopTicksGet()函数获取到的进入stop模式的tick数需要减去1。之所以这么做是因为进入stop模式前会把systick关掉,唤醒之后打开,然后再产生一次systick中断的时候,在中断服务函数中会执行osTaskScan()和osSwtmrScan()函数,这样才能进行正常的任务切换,否则就错过了要执行的位置,那么就需要等下一轮才能执行,系统也就出了问题。还拿上面的图片举例,g_stTskSortLink.usCursor还是2,任务延时插入后位置是6,如果直接补偿24个tick后g_stTskSortLink.usCursor变为6,唤醒之后的下一次systick中断后,g_stTskSortLink.usCursor的值变为7,任务也就得不到执行,需要下一轮了,这中间就需要耽误45ms。
我移植Liteos的时候LOSCFG_BASE_CORE_TICK_PER_SECOND配置的是200,也就是每个tick5ms。在前一篇文章中提到过唤醒之后初始化时钟和外设等需要的时间是1.91ms,如果项目需求对tick的误差不敏感的话,这部分的误差就可以不算进去,那么就不需要获取lptim定时器的计数值,lptim只用来在需要的时间唤醒MCU。如果在乎这段时间的话,就还可以使用前一篇文章中提到的获取lptim计数值后进行补偿,不过就要注意tick补偿之后g_stTskSortLink.usCursor的位置。
测试:
系统因为软件定时器超时需要唤醒:
进入stop之前获取到tick数0x578(1400*5ms = 7s),那么唤醒时间(补偿tick)为0x577(1399),g_stTskSortLink.usCursor值为1。系统中OS_TSK_SORTLINK_LEN值为32。
唤醒之后补偿tick后,g_stTskSortLink.usCursor值为0x18。(1399%32 = 23 23+1 = 24(0x18))
唤醒之后的下一个systick中断,g_stTskSortLink.usCursor在osTaskScan()函数中已经加1(0x19),此时软件定时器的头部节点计数值uwCount为1,减1后为0,执行osSwTmrTimeoutHandle函数。
系统因为任务延时到达需要唤醒:
下一个要执行的任务为test_task1,进入stop之前获取到tick数0xC8(200*5ms = 1s),那么唤醒时间(补偿tick)为0xC7(199),g_stTskSortLink.usCursor值为0x09。
唤醒之后补偿tick后,g_stTskSortLink.usCursor值为0x10。(199%32 = 7 7+9 = 16(0x10))
唤醒之后的下一个systick中断,g_stTskSortLink.usCursor在osTaskScan()函数中加1(0x11),此时可以看到要操作的任务控制块就是test_task1,任务的状态为0x20(OS_TASK_STATUS_DELAY),任务的延时值UWROLLNUM(pstTaskCB->uwIdxRollNum)为0。对于OS_TSK_SORTLINK_LEN值为32来说,pstTaskCB->uwIdxRollNum的值每一个表示32个tick,每个tick5ms就是160ms。
任务的tick补偿修改好后,通过osStopTicksGet()函数获取到的tick数只要大于2个tick就可以再次进入stop模式,之所以有限制是因为任务调度和唤醒后的初始化都需要时间开销。
唤醒后任务执行情况:
从唤醒到任务得到执行的时间基本都不到2ms: