65.9K
CodeProject 正在变化。 阅读更多。
Home

AVRILOS SysTick 定时器增强

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2011 年 11 月 8 日

CDDL

9分钟阅读

viewsIcon

21653

downloadIcon

176

AVRILOS SysTick 模块的错误修复和增强。

 

摘要

在本文中,我将解释 AVRILOS SysTick 模块的一些增强功能,以及一些您会发现有用的新功能。

介绍 

AVRILOS 是一个简单的协作式任务内核,占用空间小。请参阅文章底部的相关链接,了解更多关于 AVRILOS 的信息。 

背景 

AVRILOS 的基本模块之一是 SysTick 定时器。此定时器负责 OS 的计时器维护(例如,触发 ADC 采样、生成延迟等),另一方面,它可以帮助应用程序执行一些计时功能,如延迟或超时。

然而,在我最近的一个项目中,当我修改 ifc_time.h 中的各种滴答常数(在本例中为 c_ALIVEOK_ms)时,我遇到了计时不准确的失败。我注意到 LED Alive LED 的闪烁是不单调的。这非常明显(但并不关键),我希望进一步调查并找出原因。此外,如果我需要修改相关常数来正确定义各种滴答(应用程序、ADC),我觉得有点困难。

我的另一个问题是,我需要另一个 LED 像 Alive LED 一样闪烁。我有一个连接到不同端口的第二个 LED,它在物理上不在盒子里,因此用户可以看到它。问题是操作系统中没有提供这种功能的机制,所以我不得不破解 SysTick 来执行这项任务。因此,引入了一种更合适的方法。

这个特定的应用程序(泵控制)需要长时定时器(小时/天),所以我也在 SysTick 中添加了这个功能。

鉴于上一篇文章 AVRILOS & FPGA 中控制 FPGA 的 R/C 伺服器时存在一个小问题,我将应用程序函数分成了两种模式。一种是每隔应用程序滴答(c_AppInterval_ms)运行一次,另一种是在主循环中连续运行,而不等待这个间隔。因此,您可以实现您想要的任何机制(甚至两者都实现)。
最后,我将展示我在夏天制作的小型泵应用程序,它演示了上述大部分功能。

描述

更改:旧功能增强 - Bug 修复


到目前为止,旧的 SysTick 使用模数检查来查看每个任务是否需要按相对间隔运行。下面是 SysTick 的代码摘录,显示了 LED Alive 的模块检查。

  if ( (v_SysStat & (1 << b_SysTick ) ) != 0 )
  {
        v_SysStat &= ~(1 << b_SysTick );
        v_SysTimer++;
        
        // Wrap around Timer
        // Interval is 1 second
        if (v_SysTimer == c_SYSTIMEMAX) v_SysTimer = 0;
        
        // Toggle AliveLED depending on error state        
        if ( (v_SysTimer % v_ErrLevel) == 0)
        {                        
            f_flashled();            
        }  
   }
  
SysTick 定时器从 0-999ms 运行。然后使用除法余数与周期时间进行检查。在下面的图表中,您可以看到正常情况。


OldSysTickGood.jpg

这种方法有一个优点,即只使用一个变量(实际的 SysTick 定时器)来执行许多具有不同周期的不同任务。虽然这看起来很优雅(或很酷),但它也有一些限制(或 bug...)。

如果各种任务(例如 LED Alive、ADC 等)的周期没有一个最大公约数是 SysTick 定时器持续时间的整数因子,则会生成异常周期,这些周期是系统性的但不是单调的。请参阅下面的图表,了解一个“糟糕”的情况,其中选择的周期与总时长 1000ms 不匹配。

当主 SysTick 计数器回绕时,问题就会暴露出来。在这种情况下,SysTick 计数器的余数(0)在任何情况下都是零。因此,在 SysTick 回绕事件发生时,问题就会出现。


OldSysTickBad.jpg

为了解决这个问题,我不得不为每个任务添加一个变量(因此使用更多 RAM)。然后每个任务都有自己的计数器来测量周期,因此它们是完全独立的和准确的。此外,ifc_time.h 包含也被修改为提供此情况的正确计时定义。

旧代码是

#define c_T0Unit_ticks_per_ms   (MPUCLK_Hz/c_T0DIV)/c_ONESECOND_ms
#define c_T0TimeSlice           256-c_T0Unit_ticks_per_ms           // for 1mS interval
#define c_TimeSlice_Hz          c_T0TimeSlice/(MPUCLK_Hz/c_T0DIV)

#define c_SYSTIMEMAX            c_SysTickPeriod_ms
#define c_ALIVEOK_ticks         (INT16U) (c_TimeSlice_Hz/c_ALIVEOK_ms)    /* Alive LED When Ok */
#define c_ALIVESER_ticks        (INT16U) (c_TimeSlice_Hz/c_ALIVESER_ms)   /* Alive LED When Serial Error */


而新代码是

#define c_T0Unit_ticks_per_ms      (MPUCLK_Hz/(c_T0DIV*c_ONESECOND_ms))
#define c_T0TimeSlice              (256-c_T0Unit_ticks_per_ms)
#define c_TimeSlice_Hz             (MPUCLK_Hz/(c_T0DIV * c_T0TimeSlice))

#define c_SYSTIMEMAX               c_ONESECOND_ms
#define c_ALIVEOK_ticks            (INT16U) (c_TimeSlice_Hz*c_ALIVEOK_ms/c_ONESECOND_ms)    /* Alive LED When Ok */
#define c_ALIVESER_ticks           (INT16U) (c_TimeSlice_Hz*c_ALIVESER_ms/c_ONESECOND_ms)   /* Alive LED When Serial Error */
 

旧代码的另一个问题是 c_TimeSlice_Hz 没有被正确定义(注意新代码中的相关括号),因此在许多情况下计时定义都是错误的。
这很有趣,因为我从未更改过这些参数,也从未见过这个问题。实际上,我是在查看我的 LED Alive 周期时发现这个问题的。然后我注意到我无法以确定性的方式更改闪烁周期(定义错误),我还看到了由于公约数不一致而导致的故障。

以牺牲少量额外变量为代价,问题得以解决。请参阅下面的 SysTick.c 代码片段

        if ( v_SysAlive > v_ErrLevel)
        {        
            v_SysAlive = 0;
            f_flashled();            
        }
        
        if ( v_SysApp > c_APP_ticks )
        {
            v_SysApp = 0;
            v_SysStat |= (1 << b_AppTick );        
        }


因此,每个单独的任务独立使用自己的变量。       

更改:新功能 - 长时定时器

在我提供的示例应用程序中,我需要可以计算小时或天的定时器。我现有的 SysTick 定时器模块没有此功能。为了支持此功能,我添加了许多定时器(ifc_time.h 中定义的计数器数量)。

长时定时器变量的新名称是:v_SwLongTimer_mS[]。它是一个数组(ifc_time.h 定义了您需要的计时器数量)。它倒计时(就像 SysTick 中定义的软件定时器一样),直到它们达到零(计数完成)。计数器倒计时每 1ms(1 个 SysTick 周期)执行一次。因此,我们可以测量 40 亿毫秒,这是一个非常长的时间。这些定时器用于超时,正如在第一个 AVRILOS 文章中所见,它们可以轻松实现此类操作。

当然,为了获得更准确的计时,您应该使用 AVR 的 32KHz 晶体振荡器,该振荡器用于实时时钟。然而,在我的硬件中,我没有使用额外的定时器,所以这是另一种方法。

考虑到晶体振荡器频率可能无法精确除以秒(取决于预分频器值),您会随着时间的推移累积少量误差。对于泵应用程序来说,这并不是一个大问题。否则,您可以减小初始倒计时值以提供准确的计时。

处理长时软件定时器的代码如下所示

       for(v_idx=0;v_idx<c_MAXSWLTIMERS; v_idx++)

        {
            if (v_SwLongTimer_mS[v_idx] != 0)

            {
                v_SwLongTimer_mS[v_idx]--;
            }

        }

 
每个非零计数器都会递减直到零。应用程序检查计数器是否清零以确定过期。

更改:新功能 - AliveHook

然后我遇到了另一个问题!我需要将 Alive LED 的状态反映到连接到另一个 I/O 的不同 LED 上。第二个 LED 在盒子外部可见。然而,我不得不破解 Systick 来提供这个功能。由于我想要一个更永久的解决方案,我决定进行一个弱函数调用到一个钩子函数

void f_flashled(void)

{

    INT8U v_temp;

    v_temp = (c_PORTALIVE);
    v_temp ^= 1 << b_LedAlive; 
    (c_PORTALIVE) = v_temp;   

    f_HookAlive( ((v_temp) & (1 << b_LedAlive)) );

}


我在 flash LED 函数中调用 f_HookAlive。参数表示 Alive LED 是开启还是关闭,因此不需要维护切换代码。这已经在 f_flashled 中执行了。然后,在我的应用程序中,我这样做

// copy led alive to external LED
void f_HookAlive(INT8U v_ledstate)

{
    if (v_ledstate == 0) LedYelOn;
    else LedYelOff;    
}

SysTick 模块已经对此函数有一个外部引用,因此链接器将建立连接并调用此函数。

然而,如果未定义钩子会怎样?嗯,小秘密就在 SysTick 中的弱定义

extern void f_HookAlive(INT8U v_ledstate) __attribute__((weak)); 


将函数定义为弱函数意味着该函数可能不存在。链接器将忽略对不存在的弱函数的引用(不调用),一切都会顺利进行。如果您需要该函数,只需在您的程序中某处声明它即可。在这种情况下,链接器将看到连接并调用您的函数。

更改:新功能 - 应用程序定时和连续操作

在前一篇文章中,当应用程序不每隔应用程序间隔 c_AppInterval_ms 运行时,R/C 伺服器的驱动更加平滑。这与 UART 和伺服器控制的组合有关。在伺服器控制运行之前,有很多按键被接受。因此,程序是以批次执行用户输入的。解决方案是将应用程序从 Kernel.c 中的定时触发部分移除。

            if ( ( v_SysStat & (1 << b_AppTick) ) != 0)

            {
                v_SysStat &= ~(1 << b_AppTick);
                //f_Applic();  // Initial placement of application
            }
            f_Applic(); // R/C Servo placement for smoother operation 


然后我有了这个想法,我们可以为标准的空应用程序提供这样的版本,并且程序员可以使用他或她喜欢的任何钩子(甚至两者都使用)。
kernel.c 修改如下

            if ( ( v_SysStat & (1 << b_AppTick) ) != 0)

            {
                v_SysStat &= ~(1 << b_AppTick);
                f_Applic_Tick();                
            }
            f_Applic_Cont(); 


然后 Applic.c 修改如下

// Function called by the main kernel loop (task)
// Should not block
// This process runs on every Application Tick
// Use this for timed event handling
void f_Applic_Tick(void)
{

}



// Function called by the main kernel loop (task)
// Should not block
// This process runs continuously on the main loop
// Use this if you need no wait time on each run
void f_Applic_Cont(void)
{

}

 

现在由程序员根据应用程序使用他需要的任何版本。
这里的目标不是出于应用程序特定的原因修改 Kernel.c,而是仅出于新的设备驱动程序。
 

示例:泵控制

在示例应用程序中,我有以下泵系统
 

BlockDiagram.png
物理布置如下
增压器希望将水压保持在最低限制之上。当达到这个低限制时,它会请求启动水泵。通常,水泵会充满增压器水箱,并在达到高压限制后,增压器会停止水泵。因此,我们保证增压器出口处有最低水压。

这是用于花园的经典自动化。问题是,如果某处有泄漏,水泵可能会工作太长时间,可能会导致水泵烧毁。

目标是允许水泵正常运行,但在异常活动时停止水泵。例如,有两种情况

1. 从水泵到增压器的管道破裂。
2. 增压器后的某个管道破裂。

案例 1

在这种情况下,水泵将无法填充增压器。增压器请求水泵活动以进行填充。泄漏量会很大,整个花园可能会充满水。关键在于,在增压器可能充满的最大时间(加上一些余量)后停止水泵。这是连续运行限制;即水泵的连续运行不得超过限制。
通常,此限制应在几分钟范围内(例如 4 分钟)。

情况 2:

在这种情况下,增压器会频繁填充和排空。一天中的总水泵时间不应超过合理限制,否则可能会发生泄漏。这是每日限制。如果发生任何此类限制条件,系统将停止运行,直到按下开关(或者在我这里,光敏电阻检测到控制箱打开 - 这是通用复位)。例如,在我的情况下,这个限制大约是每天 2 小时的运行时间。

打开外壳会绕过限制和计数器操作。这是通过冻结内部计数器来实现的。代码使用一个非常简单的状态机。它处于 IDLE 状态,直到有水泵激活。然后它跳转到 WORK 状态,此时计数器会增加。然后它可以返回到 IDLE,或者如果违反了一个限制,它会跳转到 BLOCK 状态(水泵关闭)。然后打开外壳会将状态机重置回 IDLE。代码很小,不言自明。

结论  


在本文中,我介绍了一些关于 AVRILOS 定时功能的 bug 修复和增强。     

在我的下一篇文章中,我将介绍 SourceForge 上的 AVRILOS 存储库的 Mercurial (hg) 访问。   

相关链接  


avrilos 文章

历史  

本文 V1.0。

AVRILOS V1.23。

© . All rights reserved.