问题背景:
FreeRTOS在使用OS之后,将OS的基础时钟与其他基础时钟都放在Systick_Handler中,其他基准时钟会不准,OS时钟却依旧很准。
博主只是一个在读研究生,在公司的项目中遇到的问题,期间在CSDN上搜了很多相关资料,但是没有对于这个问题的详细描述与解答,在另外几篇blog中,有人讲述了问题的解决方法,有人已经知道了原因,但是没有人对这个问题的原因给出了详细的解答,组长本着锻炼我的想法让我试试,花了大概10天终于将这个问题琢磨透了。
本文不具体细讲OS相关参数如何配置。
具体现象:
项目2是基于项目1的衍生产品,仅比项目1多一个FOC功能,一个FOC占用了70%的资源,因此开发的时候屏蔽了大部分的周期任务,仅保留了10ms的。
开发的时候断开debug(debug的时候看门狗不会触发复位),配置好运行环境后,上电输出PWM,电机不转,示波器观察波形发现一直在复位,秒复位。
ROOT CAUSE:
问题就处在最开始没有将OS的基准时钟单独放在Systick_Handler中。
void SysTick_Handler(void)
{
WD_Counter;
/* 如何调度器已经开启,运行或者挂起都算开启 */
if(xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
}
}
那么重点来了:为什么将几个基准时钟放一起就不准了呢?明明只是添加了一个“WD_Counter++”就不准了?而且只有OS基准时钟是准的,显得很诡异。(当然,比较了解FreeRTOS的应该已经猜到答案了)
回到最开始,发现WD_Counter所在的Systick_Handler()从本来定义的1ms触发变成了10ms触发(下左图),而且运行电机的时候规律的10ms波形会穿插的1ms与2ms 的波形(下右图),但是通过调全局变量发现 :OS嘀嗒计数值和基于嘀嗒计数值的10ms周期任务都是准的!!!
我们再看看代码,xPortSysTickHandler()就是嘀嗒中断的主要内容,里面调用了xTaskIncrementTick()便是整个嘀嗒中断的精髓了,任务的调度、调度器的挂起与使能、OS基准时钟都在里面了。当然执行这步前还有最关键的一步:vPortRaiseBASEPRI(),关闭优先级不高于5(在FreeRTOS中,中断优先级不高于5就是数字小于5,中断优先级高于一切任务的优先级)。
发现什么诡异的地方了吗?没错,现在目前看来Systick_Handler()像是被约束住了,只有在周期任务和中断来的时候,才会触发。
void xPortSysTickHandler( void )
{
vPortRaiseBASEPRI(); /* 关闭中断 */
{
/* 如果返回值标记了任务切换,即有优先级高的任务 */
if( xTaskIncrementTick() != pdFALSE )
{
/* 设置PendSV中断位 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
vPortClearBASEPRIFromISR(); /* 打开中断 */
}
在花了几天排查了项目模块因素后,发现这个问题就是OS的问题,怎么控制变量去测我们自己开发的代码都找不到原因。期间还怀疑是被资源锁给锁住了,后来在debug中打断点发现,确实每次进入嘀嗒中断的时间就是10ms,并不是被锁住了无法正常赋值。
刚接触FreeRTOS的我已然不知如何下手了,优先级最低的嘀嗒中断被约束了,从1ms变到了10ms?
没错,就是我最前面加粗没标红的10ms周期任务啊。经过测试,发现没有其他中断的情况下,Systick_Handler()的进入时间取决于当前周期任务中周期最短的那个任务。
于是接着排查,用GPT5定时器新建一个1ms中断(这个不依赖于OS是准的),通过如下的方式观察嘀嗒计数器晶振次数变量 (CPU->SYSTICK_CUR.reg)发现,每次向下计数溢出的reload值被改了。晶振40MHZ,1ms就是计数40000=0x9C40次,所以按理来说if语句中判断条件为 cpu_tick_current<0x9C40 应该才是正确的计数,但是只有将值改为当前最小周期整数倍才是准的(这时候测试开的是5ms周期任务,40000*5 = 0x30D40)。
激动的心,颤抖的手,接着往下查。查找了所有可能修改嘀嗒定时器reload值的变量,发现唯一能修改的地方便是
__weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime ),这是FreeRTOS的低功耗功能。简单的说就是
#if configUSE_TICKLESS_IDLE == 1
__weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
{
uint32_t ulReloadValue, ulCompleteTickPeriods, ulCompletedSysTickDecrements, ulSysTickCTRL;
TickType_t xModifiableIdleTime;
/* Make sure the SysTick reload value does not overflow the counter. */
if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks )
{
xExpectedIdleTime = xMaximumPossibleSuppressedTicks;
}
/* Stop the SysTick momentarily. The time the SysTick is stopped for
is accounted for as best it can be, but using the tickless mode will
inevitably result in some tiny drift of the time maintained by the
kernel with respect to calendar time. */
portNVIC_SYSTICK_CTRL_REG &= ~portNVIC_SYSTICK_ENABLE_BIT;
/* Calculate the reload value required to wait xExpectedIdleTime
tick periods. -1 is used because this code will execute part way
through one of the tick periods. */
ulReloadValue = portNVIC_SYSTICK_CURRENT_VALUE_REG + ( ulTimerCountsForOneTick * ( xExpectedIdleTime - 1UL ) );
if( ulReloadValue > ulStoppedTimerCompensation )
{
ulReloadValue -= ulStoppedTimerCompensation;
}
/* Enter a critical section but don't use the taskENTER_CRITICAL()
method as that will mask interrupts that should exit sleep mode. */
__disable_irq();
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
/* If a context switch is pending or a task is waiting for the scheduler
to be unsuspended then abandon the low power entry. */
if( eTaskConfirmSleepModeStatus() == eAbortSleep )
{
/* Restart from whatever is left in the count register to complete
this tick period. */
portNVIC_SYSTICK_LOAD_REG = portNVIC_SYSTICK_CURRENT_VALUE_REG;
/* Restart SysTick. */
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
/* Reset the reload register to the value required for normal tick
periods. */
portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;
/* Re-enable interrupts - see comments above __disable_irq() call
above. */
__enable_irq();
}
else
{
/* Set the new reload value. */
portNVIC_SYSTICK_LOAD_REG = ulReloadValue;
/* Clear the SysTick count flag and set the count value back to
zero. */
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/* Restart SysTick. */
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
/* Sleep until something happens. configPRE_SLEEP_PROCESSING() can
set its parameter to 0 to indicate that its implementation contains
its own wait for interrupt or wait for event instruction, and so wfi
should not be executed again. However, the original expected idle
time variable must remain unmodified, so a copy is taken. */
xModifiableIdleTime = xExpectedIdleTime;
configPRE_SLEEP_PROCESSING( xModifiableIdleTime );
if( xModifiableIdleTime > 0 )
{
__dsb( portSY_FULL_READ_WRITE );
__wfi();
__isb( portSY_FULL_READ_WRITE );
}
configPOST_SLEEP_PROCESSING( xExpectedIdleTime );
/* Stop SysTick. Again, the time the SysTick is stopped for is
accounted for as best it can be, but using the tickless mode will
inevitably result in some tiny drift of the time maintained by the
kernel with respect to calendar time. */
ulSysTickCTRL = portNVIC_SYSTICK_CTRL_REG;
portNVIC_SYSTICK_CTRL_REG = ( ulSysTickCTRL & ~portNVIC_SYSTICK_ENABLE_BIT );
/* Re-enable interrupts - see comments above __disable_irq() call
above. */
__enable_irq();
if( ( ulSysTickCTRL & portNVIC_SYSTICK_COUNT_FLAG_BIT ) != 0 )
{
uint32_t ulCalculatedLoadValue;
/* The tick interrupt has already executed, and the SysTick
count reloaded with ulReloadValue. Reset the
portNVIC_SYSTICK_LOAD_REG with whatever remains of this tick
period. */
ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL ) - ( ulReloadValue - portNVIC_SYSTICK_CURRENT_VALUE_REG );
/* Don't allow a tiny value, or values that have somehow
underflowed because the post sleep hook did something
that took too long. */
if( ( ulCalculatedLoadValue < ulStoppedTimerCompensation ) || ( ulCalculatedLoadValue > ulTimerCountsForOneTick ) )
{
ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL );
}
portNVIC_SYSTICK_LOAD_REG = ulCalculatedLoadValue;
/* The tick interrupt handler will already have pended the tick
processing in the kernel. As the pending tick will be
processed as soon as this function exits, the tick value
maintained by the tick is stepped forward by one less than the
time spent waiting. */
ulCompleteTickPeriods = xExpectedIdleTime - 1UL;
}
else
{
/* Something other than the tick interrupt ended the sleep.
Work out how long the sleep lasted rounded to complete tick
periods (not the ulReload value which accounted for part
ticks). */
ulCompletedSysTickDecrements = ( xExpectedIdleTime * ulTimerCountsForOneTick ) - portNVIC_SYSTICK_CURRENT_VALUE_REG;
/* How many complete tick periods passed while the processor
was waiting? */
ulCompleteTickPeriods = ulCompletedSysTickDecrements / ulTimerCountsForOneTick;
/* The reload value is set to whatever fraction of a single tick
period remains. */
portNVIC_SYSTICK_LOAD_REG = ( ( ulCompleteTickPeriods + 1UL ) * ulTimerCountsForOneTick ) - ulCompletedSysTickDecrements;
}
/* Restart SysTick so it runs from portNVIC_SYSTICK_LOAD_REG
again, then set portNVIC_SYSTICK_LOAD_REG back to its standard
value. The critical section is used to ensure the tick interrupt
can only execute once in the case that the reload register is near
zero. */
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
portENTER_CRITICAL();
{
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
vTaskStepTick( ulCompleteTickPeriods );
portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;
}
portEXIT_CRITICAL();
}
}
#endif /* #if configUSE_TICKLESS_IDLE */
* 上面的代码及其调用的函数可以简要概括如下:
(1) 处于空闲任务其他任务处于阻塞或者挂起才可以进入,且会判断是否满足进入条件(当系统运行于低功耗模式 的时钟节拍数大于宏定义 configEXPECTED_IDLE_TIME_BEFORE_SLEEP (系统默认是2个系统时钟节拍,用户定义必须大于2))。
(2)在停止模式下,所有的I/O引脚都保持它们在运行模式时的状态。
(3)进入即关闭滴答定时器(OS_PreSleepProcessing()中可以选择更多操作),可以记录下系统节拍中断的关闭时间,当系统节拍中断再次开启运行的时候补上这段时间。
(4)当一个中断或唤醒事件导致退出停止模式时, HSI RC振荡器被选为系统时钟。
(5)退出低功耗的停机模式后,需要重新配置使用HSE(OS_PostSleepProcessing()中完成)。
(6)在进入低功耗模式之前获取还有多长时间进行下一个任务。
好,好,好,到这里就真相大白了。是因为在空闲任务进入了低功耗模式,紧接着关闭了滴答定时器,所以里面的功能全部都失效了,这也就是WC_Counter不准的原因,然后在退出低功耗模式的时候,通过前面获取下一次任务或者中断来临的时间点,补上嘀嗒定时器的计数值,所以在我们看来,这个数值和周期任务(周期任务直接取决于嘀嗒计数值)都是准的,也所以在我们看来,嘀嗒中断被周期任务于中断约束了。
第一篇BLOG,尽量缩减了内容,可能字还是有点多红火火恍恍惚惚,我觉得这样能讲的更透。