在看门狗定时器存在的情况下进行编程

6

我是嵌入式系统编程的新手,虽然在学习期间已经学过相关课程,但实际编程仍有些距离。

问题在于:我需要在没有操作系统的情况下,为NXP LPC2103微控制器(基于ARM 7)编写一个小型系统。该系统具有看门狗定时器,需要定期更新。该系统带有GPRS调制解调器和嵌入式TCP/IP堆栈,初始化这些需要的时间比看门狗超时的时间长。当我调用初始化函数时,系统会重置。

我向一位更有经验的同事请教,他建议我需要从主函数中退出并重新进入相同的初始化函数,同时持续不断地喂看门狗计时器,直到函数执行完成。这个想法听起来不错,但我也想听听其他人的经验。此外,一些参考资料(书籍或网站)也可能很有用,因为我找不到任何特定的资料。

我不想从初始化函数中调用看门狗计时器,我认为这样做不好。


1
你好。我无法精确回答你的问题,但这里有一个关于实时和嵌入式系统设计/编程的优秀网站:http://www.eventhelix.com/RealtimeMantra/。 - Joan Rieu
1
通常,重置看门狗定时器(倒计时)被称为“抚摸”或“轻敲”看门狗。当看门狗时间耗尽时,它会“咬人”(因为它是一只狗,而狗可以咬人),并且通常会重置嵌入式系统上的硬件。 - nategoose
2
@nategoose - 在我们公司,我们“踢”看门狗。 - AShelly
@AShelly:可怜的看门狗。 - nategoose
5个回答

7

我不希望从初始化函数中调用看门狗定时器,我认为这样做并不好。

对于长时间运行的操作,您可能想要执行其他工作,这可能有些过度,但我通常使用的一种通用技术是让长时间运行的函数接受一个回调函数指针,该指针将被定期调用。 我通常使用的模式是具有类似以下样式的回调原型:

int (callback_t*)(void* progress, void* context);

长时间运行的函数将定期调用回调函数,并提供一些信息以指示其进度(如何表示进度及其含义取决于特定函数的详细信息),同时还会提供一个上下文值,该值是原始调用者在回调指针旁传递的(同样 - 该参数的含义及其如何解释完全取决于回调)。通常情况下,回调函数的返回值可能用于指示“长时间运行的东西”应取消或以其他方式更改行为。
这样,您的初始化函数可以使用带有上下文值的回调指针,并定期调用它。显然,在您的情况下,这些回调必须足够频繁地发生,以使看门狗保持满意。
int watchdog_callback( void* progress, void* context)
{
    kick_the_watchdog();

    return 0;  // zero means 'keep going...'
}


void init_modem( callback_t pCallback, void* callback_context)
{
    // do some stuff

    pCallback( 0, callback_context);

    // do some other stuff

    pCallback( 1, callback_context);


    while (waiting_for_modem()) {
         // do work...

         pCallback( 2, callback_context);
    }    
}

这种模式的一个好处是它可以用于不同的情况——您可能有一个函数读取或写入大量数据。回调模式可以用于让某些东西显示进度。

请注意,如果您发现有其他长时间运行的函数,则相同的watchdog_callback()函数可用于允许它们处理防止看门狗重新启动。然而,如果您发现自己经常需要依赖特定的看门狗来完成此类任务,那么您可能需要考虑如何交互您的任务,要么将它们分解得更细,要么使用更复杂的看门狗方案,该方案由其自己的任务管理看门狗,其他任务与之交互以间接保持看门狗计时器的状态。


+1 这样的模式基本上是一种在纯C中模拟C++ OOP好处的方法。这种抽象的另一个好处是,您可以使用组合将多个回调处理程序附加到此函数,而无需更改任何代码。例如,您的回调函数本身可以调用其他函数指针列表,其中一个会启动看门狗,另一个会更新显示进度条等。 - vgru

6

通常,我采用两种方法来处理这种情况。

状态机初始化

第一种方法与您的同事建议的类似:在主循环中调用一个名为状态机的初始化程序,然后停止调用初始化程序并开始调用主程序。

这是一个简单而干净的功能,但在启动低频振荡器等特定长时间进程时可能有些棘手。

时限中断服务例程看门狗处理

如果您有一个“systick”或等效中断(例如每1毫秒触发一次的中断),则还有另一种选择。在这种情况下,您可以考虑在每50个中断调用之后喂狗(例如),但将喂狗的次数限制为等于允许的最大初始程序完成时间。然后通常需要(如果您像我建议的那样拥有窗口看门狗)在初始化结束时有一个短的同步循环,以确保在达到最小窗口时间之前不会喂狗,但这很容易实现。

这是一个相当干净的解决方案(因为它不会使初始化程序变成一个不必要的状态机),并解决了初始化程序挂起的问题。但是,非常重要的是要强制执行ISR中看门狗调用的限制。

讨论

两者都有优点和缺点,但对于不同的要求,具有不同的方法是很有用的。我倾向于采用后一种解决方案,例如拥有低频振荡器(可能需要一段时间才能启动),因为这避免了使初始化程序过于复杂,而初始化程序本身就已经足够复杂了!

我相信其他人也会提供其他替代想法...


“软件辅助”看门狗方法的一个好处是,如果有一个32位的current_time值,那么可以有一个reset_time值,并且每当(unsigned)(reset_time-current_time)超过某个最大值时就进行重置。代码可以尝试将reset_time保持非常接近current_time,但在执行耗时操作之前将其设置得足够远,以避免意外重置。 - supercat

4
LPC2103中的看门狗是高度可定制的。您有许多选项来控制它:
您可以在初始化序列完成之前不启用它。
您可以延长饲料之间的时间,使其变得非常长。
问题是您将用看门狗做什么?
如果它用于检查您的软件是否正常运行而不会冻结,我不认为AI的ISR选项会如何帮助您(即使程序被卡住,ISR也可以继续工作)。
有关看门狗选项的详细信息,请参见您的MCU用户手册中的WatchDog Timer(WDT)章节(17)。 http://www.nxp.com/documents/user_manual/UM10161.pdf

更改wd周期是一个可行的选择。Al指出,您必须限制中断例程中对WD的更新次数,以避免未检测到挂起的init例程。 - ziggystar
  1. LPC2103没有最小化的窗口来喂看门狗,因此您可以限制保存处理器资源的时间,但不必这样做。
  2. 您如何检查从ISR挂起的例程?从ISR喂养看门狗有什么帮助?
- landmn
  1. 如果您可以将WD周期延长到足够长的时间,就不需要使用ISR方法。在大多数MCU上,使用ISR来人为地延长WD周期是一种可选方案-这个解决方案是通用的,在给定任务中对于LPC2103可能并不必要。
  2. 您无法检查挂起的例程。但是,您可以将ISR限制为仅重置WD固定次数(如100次),然后让其过期。
- ziggystar

3

监控程序是很好的,但当你的程序或系统不易适配时,就会成为一种烦恼。它们在以下情况下表现最佳(通常):

Watchdog_init();

hardware_init();
subsystem1_init();
subsystem2_init();
subsystem3_init();
...
subsystemN_init();

forever {
   Watchdog_tickle();

   subsystem1_work();
   subsystem2_work();
   subsystem3_work();
   ...
   subsystemN_work();
}

很多时候,您可以设计程序,使其正常工作,通常它非常防错(但并非完全防错)。
但是在像你这样的情况下,这种方法并不适用。您最终需要设计和创建(或可能使用库)一个框架,其中有各种必须满足的条件来控制看门狗何时被喂养。这可能非常棘手。该代码的复杂性本身可能会引入自己的错误。您可能会编写完美的应用程序,除了看门狗框架外,您的项目可能会频繁重置,或者所有代码都可能很糟糕,并且只会不断地喂养看门狗,导致它永远不会重置。
将上述代码更改以处理更复杂的情况的一种好方法是将subsystemX_work函数更改为跟踪状态。这可以通过函数中的静态变量或使用函数指针而不是函数并更改执行的实际函数以反映该子系统的当前状态来完成。每个子系统都成为状态机。
另一种解决长时间故意等待快速咬合看门狗的方法是将长时间运行的函数分成较短的片段。而不是:
slow_device_init();
Watchdog_tickle();

你可以这样做:
slow_device_init_begin();
Watchdog_tickle();
slow_device_init_finish();
Watchdog_tickle();

然后通过执行以下操作来延长看门狗定时器的时间:

slow_device_init_begin();
for ( i = SLOW_DEV_TRIES; i ; i--) {
   Watchdog_tickle();
   if (slow_device_init_done()) {
       break;
   }
}
Watchdog_tickle();

即使如此,它也可能变得越来越复杂。通常你会需要创建一个看门狗委托来检查是否满足条件,并根据这些条件进行操作或不操作看门狗。这开始变得非常复杂。可以通过为每个子系统创建一个对象来实现,该对象具有某些方法/函数来调用以测试子系统的健康状况。健康方法可能非常复杂,甚至随着子系统状态的改变而改变,但应尽可能简单,以便尽可能轻松地验证代码的正确性,并且因为子系统的工作方式发生更改将需要更改如何测量其健康状况。
如果您可以确保某些代码定期运行,则可以为每个子系统只设置一个整数作为该子系统的本地看门狗。一些代码(可能是在定时器中断处理程序中,但不一定)将递减并测试每个子系统的变量。如果任何子系统的变量达到0,则看门狗未被触发。
Watchdog_periodic() {
   for_each subsustem in subsystem_list { // not C, but you get the idea
      if ( 0 > --(subsystem->count_down) ) {
           // Do something that causes a reset. This could be returning and not petting
           // the hardware watchdog, doing a while(1);, or something else
      }
   }
   Watchdog_tickle();
}

每个子系统都可以通过将其计数器设置为正值来不同程度地触发其自己的倒计时。

您还应该注意,尽管它可能利用硬件看门狗进行实际重置,但这实际上只是一个软件看门狗。

您还应该注意,看门狗框架越复杂,出错的机会就越大,同时其他代码中的错误也可能导致其无法正常工作。例如指针错误:

int x;
fscanf(input, "%i", x); // Passed uninitialized x rather than address of x

可能会导致设置某些子系统的倒计时值,这可能会使看门狗在应该咬人时不会咬人。


1

你可能需要重新考虑在代码中服务WD定时器的位置。

通常情况下,WD定时器需要在空闲时间(空闲循环或空闲任务)和最低级别驱动程序(例如,当您正在从/写入GPRS调制解调器或MAC以进行TCP / IP连接等)中进行服务。

如果这不够用,您的固件可能仅在延迟例程中烧毁CPU周期。在此处添加WD定时器服务是可以的,但您可能需要调整延迟计时器以考虑WD服务时间。

如果您的应用程序只是有一些长时间的,耗费CPU的任务,需要执行比WD定时器周期允许的更长时间,您可以考虑将WD定时器间隔稍微延长一点。这可能并不总是可能的,但我喜欢将WD定时器引用保持在固件的上层之外,以使应用程序层尽可能具有可移植性。 WD计时器通常是硬件相关的,因此代码中任何WD计时器引用都很少具有可移植性。 低级别驱动程序通常也很少具有可移植性,因此这通常是服务WD计时器的更好位置。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接