关于C#:在看门狗定时器存在的情况下编程

Programming in presence of watchdog timer

我是嵌入式系统编程的新手,虽然我在学习期间完成了课程,但实际编程还是有点远。

问题在于:我必须在NXP LPC2103微控制器(基于ARM 7)上编写一个小型系统,而不需要操作系统。它有一个看门狗定时器,需要定期更新。该系统具有嵌入了TCP / IP堆栈的GPRS调制解调器,并且初始化这需要比看门狗需要超时的时间更长的时间。当我调用初始化函数时,系统重置。

我和一位经验更丰富的同事交谈过,他建议我退出并重新进入同一个初始化函数,从主函数开始,我将看门狗定时器咬了很久,直到函数完成执行。这个想法听起来不错,但我还想听听其他一些经历。此外,参考(书籍或网站)也可能有用,因为我找不到任何具体的内容。

我不想从初始化函数调用看门狗定时器,我没有发现这个好。


I wouldn't like to call watchdog timer from the initialization function, I don't find this good.

对于这种情况可能有点过头了,但是我用于长时间运行操作的一般技术是让你可能想要执行其他工作的是让长时间运行的函数接受将定期调用的回调函数指针。我通常使用的模式是有一个回调原型,可能看起来像:

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

长时间运行的函数将定期调用回调,其中一些信息指示它的进度(如何表示该进度表示它取决于特定函数的详细信息)以及原始调用者传入的上下文值以及回调指针(再次 - 该参数的含义以及它的解释方式完全取决于回调)。一般来说,回调函数的返回值可能用于指示"长时间运行的东西"应该取消或以其他方式改变行为。

这样,您的初始化函数可以使用带有上下文值的回调指针,并且只是定期调用它。显然,在你的情况下,那些回调必须经常发生,以保持看门狗的快乐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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()函数来处理阻止看门狗重置的问题。但是,如果您发现自己需要经常为监视程序依赖此类事物,那么您可能需要考虑您的任务如何进行交互并将其分解得更多或使用更加复杂的监视程序方案来管理监视程序通过自己的任务,其他任务与之交互,以保持监视程序计时器间接满意。


一般来说,我采用了两种方法来应对这种情况。

状态机初始化

第一个就像你的同事建议的那样:在作为主循环的一部分调用的状态机中实现初始化例程,然后停止调用初始化例程并开始调用主例程。

这是一个简单而干净的功能,但在涉及特殊的长过程(例如启动低频振荡器)时可能会有点尴尬。

限时ISR看门狗处理

如果你有一个'systick'或等效的中断,还有另一种选择,例如每1毫秒触发一次中断。在这种情况下,您可以考虑每隔50次中断调用(例如)馈送看门狗,但限制看门狗被馈送的次数等于初始化例程完成的最大允许时间。然后通常需要(如果你有,我认为应该是一个窗口看门狗)在初始化结束时有一个短的同步循环,以确保在达到最小窗口时间之前没有馈送看门狗,但这很难实现。

这是一个非常干净的解决方案(因为它不会将初始化例程转换为不必要的状态机)并处理初始化例程挂起的问题。但是,强制执行ISR中看门狗呼叫的限制非常重要。

讨论

两者都有其优点和缺点,但是针对不同的要求采用不同的方法是有用的。我倾向于选择后一种解决方案,其中我有一些低频振荡器(可能需要一段时间才能启动),因为它避免了过度复杂的初始化程序,这可能足够复杂!

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


LPC2103中的看门狗可高度定制。
你有很多选择来控制它:

在初始化序列结束之前,您不能启用它。

您可以将Feed之间的时间段延长很长时间。

问题是你使用看门狗是什么?

如果它用于检查您的软件是否运行良好而且没有冻结,我不会看到AI的ISR选项如何帮助您(即使您的程序被卡住,ISR仍可继续工作)。

有关看门狗选项的详细信息,请参阅MCU用户手册中的看门狗定时器(WDT)章节(17)。
http://www.nxp.com/documents/user_manual/UM10161.pdf


看门狗很棒,但当你的程序或系统不适合它时,后面也会很痛苦。当您的代码看起来(通常)如下时,工作效果最佳:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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函数以跟上状态。这可以通过函数中的静态变量或使用函数指针而不是函数来完成,并更改执行的实际函数以反映该子系统的当前状态。每个子系统都成为状态机。

使用快速咬合监视器进行长时间有意识等待的另一种方法是将长时间运行功能分解为更短的部分。而不是:

1
2
slow_device_init();
Watchdog_tickle();

你可以这样做:

1
2
3
4
slow_device_init_begin();
Watchdog_tickle();
slow_device_init_finish();
Watchdog_tickle();

然后扩展它以通过执行以下操作来扩展监视程序计时器:

1
2
3
4
5
6
7
8
slow_device_init_begin();
for ( i = SLOW_DEV_TRIES; i ; i--) {
   Watchdog_tickle();
   if (slow_device_init_done()) {
       break;
   }
}
Watchdog_tickle();

即使它仍然会变得越来越复杂。通常,您最终必须创建一个监视程序委托,该委托只检查要满足的条件,并根据这些条件对监视程序执行或不接受监视程序。这开始变得非常复杂。它可以通过为每个子系统创建一个具有某些方法/函数的对象来调用子系统的运行状况来实现。健康方法可能非常复杂,甚至可能随着子系统状态的变化而改变,尽管它应该尽可能简单,以便尽可能简单地验证代码是否正确,还因为更改了如何子系统工作将需要改变您测量健康的方式。

如果您可以确保某些代码定期运行,那么您可以为每个充当子系统本地监视程序的子系统提供一个整数。一些代码(可能在定时器中断处理程序中,但不一定)会减少并测试每个子系统的变量。如果任何子系统达到0,则看门狗不会被发痒。

1
2
3
4
5
6
7
8
9
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();
}

然后,通过将count_down设置为正值,每个子系统可以在不同的时间内触发自己的count_down。

您还应该注意到,这实际上只是一个软件看门狗,即使它可以利用硬件看门狗来进行实际复位。

你还应该注意到,看门狗框架越复杂,它就越容易出现错误,而且其他代码中的错误也会导致它不正常地工作。例如指针错误,例如:

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

可能导致设置一些子系统的count_down值,这可能最终导致看门狗不应该咬人。


您可能会重新考虑在WD代码服务的代码中的位置。

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

如果这还不够,您的固件也可能只会在延迟例程中烧掉CPU周期。可以在这里添加WD计时器服务,但您可能需要调整延迟计时器以考虑WD服务时间。

如果您的应用程序只是执行了一些长时间的CPU密集型任务,这些任务需要花费更多时间来执行WD计时器周期允许的时间,您可以考虑使WD计时器间隔更长一些。这可能并不总是可行,但我喜欢将WD定时器参考保留在固件的上层之外,以使应用层尽可能便携。 WD定时器通常依赖于硬件,因此代码中的任何WD定时器引用都很少可移植。低级驱动程序很少是便携式的,因此这通常是维护WD计时器的更好地方。