ウォッチドッグタイマ(WDT)は組み込みプログラムならば必須の機能で、一定時間内にクリアし続けないとシステムリセットするタイマである。
プログラムが暴走してしまい、わけの分からない所を走り続けたり、無限ループに陥ってしまったり、いわゆるロックアップ状態になってしまった時に、すべてをご破算にして最初からやり直す機会を与えてくれる心強い味方である。
WDTは例えてみるならば止めることのできない時限爆弾であり、時間切れになる前にタイマをクリアしてやらないと爆発(システムリセット)する。
だから時間切れになる前に定期的にクリアし続けてやる必要があり、この行為によりプログラムが暴走やロックアップしてないことを間接的に証明している訳だ。
ちなみに、LPC810のユーザーズマニュアル(UM10601)では、このWDTのクリアのことをfeedと言う単語で表している。ペットにエサをやったり、機械に燃料を供給するという意味である。
英語圏ではそう表すのが一般的なのだろうか。しかしここでは「クリア」という言い方を使う。
それから、最近のマイコンは皆そうなのかもしれないが、LPC810のWDTはウィンドウ形式のウォッチドッグタイマ(WWDT)である。これは、WDTのクリア期間に制限を設ける機能であり、詳しくは後で説明する。
LPC810のWDTだが、操作してみて想像以上にトラブってしまった。
ここでは、その紆余曲折も踏まえてLPC810でのWDTの使用方法について、まとめておきたいと思う。
WDTの初期化処理をWdt_ini()、クリア処理をWdt_clr()とすると、以下のような感じになると思う。
int main(void) { Wdt_ini(); /* WDT初期化:ここからロックアップ監視 */ setup(); /* その他の初期化処理 */ for (;;) { loop(); /* メイン処理 */ Wdt_clr(); /* WDTクリア:ここまで来たらOK */ } return 0; }
setup()はWDT以外の初期化全般を行う関数を示す。setup内にWDTの初期化処理を入れても良いと思うが、まず最初にWDTを動作させることを強調したかったのでこのように示した。
まず最初にWDTを有効にした理由は、特にマイコンの初期化処理では、外部信号やハードウェアの状態待ち(安定待ち)で待機し続ける状況が多く、ある程度待ち続けても安定しない場合は、システムリセットしてもう一度最初からやり直したいと思ったからだ。
WDTの満了時間(時間切れと見なしシステムリセットする時間)にもよるが、必要ならばsetup内部でもWDTをクリアしても良いだろう。必要なのは「これ以上時間がかかったらシステム異常と見なす」時間を最初にしっかりと決めておくことだ。MHzのオーダーで動くマイコンなのだから、満了時間は2秒〜ぐらいで十分だろう。
loop()は、本プログラムのメイン処理関数を示す。この中でロックアップしていたり、暴走してあらぬ所を走り続けて、このメインループに戻って来れなくなったらクリアできずシステムリセットする。
本来ならばLPCXpresso付属のWDTライブラリについて説明したいところだが、はっきり言ってお勧めできない。自分が見ているライブラリはLPCXpresso Ver.6.1.0のものであり、これ以外の版では状況が違っているのかもしれないが、少なくともこの版のWDTライブラリは使えない。
lpc800_driver_libで用意されているWDTライブラリは、lpc8xx_clkconfig.c内のWDT_CLK_Setupと、lpc8xx_wdt.cで定義されている関数群である。
はっきり言うが、これらの関数はLPCXpressoで提供されているサンプルコードNXP_LPC8xx_SampleCodeBundle用のものであり、参考にはなるが、わざわざライブラリとして使う必要は無い。
ファイル名 関数名 説明 lpc8xx_clkconfig.c WDT_CLK_Setup WDT用オシレータの周波数を最小値(9.375kHz)に設定し、WWDTユニットにクロックの供給を開始する。周波数の選択ができないため、使い勝手が非常に悪い。 lpc8xx_wdt.c WDT_IRQHandler WDT警告割り込みハンドラだが、はっきり言ってサンプルコードである。使う必要ない。
警告割り込みについては後で説明する。 lpc8xx_wdt.c WDTInit WDTユニットの初期化関数という位置付けだが、はっきり言ってサンプルコードの初期化関数である。使う必要ない。 lpc8xx_wdt.c WDTFeed WDTクリア関数。このファイル内では唯一使える関数だが、単純な関数だし、この関数のためだけに本WDTライブラリを使う必要は無い。
はっきり言って現ライブラリ(LPCXpresso Ver.6.1.0付属)を全否定してしまったが、汎用性無く使えないのだから仕方がない。
ライブラリに頼らないで、自分なりの代替関数を用意した方が良いと思う。以下、この点を踏まえてLPC810のWDT機能について説明していく。
まずは、ウィンドウウォッチドッグタイマ(WWDT)ユニットに供給するソースクロックの設定を行う必要がある。
システムクロックの項でも説明したが、LPC810は12MHzの内蔵オシレータとは独立したWDT用のオシレータも持っている。
WDT用オシレータは動作クロックを9.375kHz〜2.3MHzの範囲で指定可能であり、このソースクロックの1クロック毎にウォッチドッグ「タイマ」のカウントをカウントダウンしていくのだ。そしてカウントアップした時に爆発(システムリセット)する。
WDT用として機能に応じたカウンタが全部で四つ用意されている。
これらはLPC810のレジスタであり、LPC8xx.hをインクルードすることによりアクセス可能になる。
それぞれの意味と関係を以下に示す。このうち、実際にカウントダウンするカウンタはLPC_WWDT->TVのみであり、その他のものは指標を示すリファレンスカウンタである。
レジスタ 設定範囲 初期値 説明 LPC_WWDT->TV ー 0xFF WDTカウンタ。ダウンカウンタであり、カウンタが負になるとWDT満了である。読み出し専用のため、直接設定することはできない。 LPC_WWDT->TC 0xFF〜0xFFFFFF 0xFF 満了時間(リセットするまでの時間)カウンタ LPC_WWDT->WINDOW 0〜0xFFFFFF 0xFFFFFF ウィンドウ(クリア可能時間)カウンタ LPC_WWDT->WARNINT 0〜0x3FF 0 WDT警告割り込みの発生時間カウンタ
各カウンタはWDT用オシレータの周波数をベースとした値を設定する必要がある。ただし、図のようにWWDTユニット内で、プリスケーラがソースクロックをさらに四分周している点に注意が必要である。
耳慣れない言葉もあると思うので、以下説明しておこう。
TVの値を直接設定することはできず、WDTクリアすることによりTCの値がTVへロードされる。
TVはダウンカウンタであり、四分周された1クロック毎に-1減分される。カウンタが負になった時(より厳密に言うと0からさらに-1した時)が時間切れとなる。
クリアはWWDTユニットのFEEDレジスタを使う。言葉で説明するよりも実際にプログラムを示した方が早いだろう。
/* WDTのクリア */ LPC_WWDT->FEED = 0xAA; LPC_WWDT->FEED = 0x55;
このようにFEEDレジスタに、0xAA・0x55をこの順に書くことで、WDTカウンタTVにTCの値がロードされて、もう一度最初からカウントダウンし始める。
最近のマイコンには良くついている機能だが、LPC810のWDTもウィンドウ付きである。
ウィンドウとは、クリア期間にも制限を設けたい時に使用する。すなわち、クリア後に一定時間経過してから次のクリアを受け付けるようにしたい場合である。
WDTカウンタTVが、ウィンドウカウンタWINDOW以下になっていた時のみWDTクリアを受け付け、もし上回っていたら満了と同じ扱いをさせる。
だから、以下のような悪意のある無限ループでもシステムリセットさせることが可能なのだ。
for (;;) { /* クリアしながらロックアップ */ LPC_WWDT->FEED = 0xAA; LPC_WWDT->FEED = 0x55; }
WINDOWカウンタは初期値として最大値0xFFFFFFが設定されており、常にクリア可能となっている。例えば、この値が0x2FFFだった場合は、TVが0x3000以上の時にWDTクリアするとシステムリセットする。
なお、これは個人的見解なのだが、このウィンドウ機能というのはどうなのだろうか。
今までの経験上、あまりこのウィンドウ機能が必要になったケースに出会ったことがなく、逆にウィンドウ設定したために、後々煩わしくなることが、ままあった。
「全然クリアできない」状況に比較して「早すぎるクリア」をシステム異常と見なすのは、やり過ぎの様な気もする。
恐らく、余程時間的にカッチリとシステムを動かしたいようなケースでしか、使う機会は無いだろう。
暴走検出してシステムリセットするのは良いが、システムリセットする前に何らかの終息処理をしておきたいケースも考えられる。
例えば、外部リソースを解放しておきたいとか、デバッグのためにスタックダンプを取っておきたいような場合だ。
警告割り込みは、そのような用途で使用できる。いわば死ぬ前の遺言のようなものだ。
WDTカウンタTVが警告割り込みカウンタWARNINTと一致すると、警告割り込みが発生し、警告割り込みハンドラWDT_IRQHandlerが呼び出される。終息処理は、このハンドラの中で行うことになる。
しかし、何かおかしな状況に陥ってしまったためにハンドラが呼び出されたのだから、まともなシステムサービスが使用できるとは考えられず、ハンドラの中でできることは非常に限られているだろう。
また、警告ハンドラが呼び出されたとしても、その後システムが持ち直すようなケースも考えられるので、外部リソースを解放するようなケースでは注意が必要である。その場合は、ハンドラ内部で無限ループして強制的にWDTリセットを生じさせてしまった方が良いかもしれない。
警告割り込み内での処理に関しては、より深く掘り下げて述べたいこともあるのだが、話しがWDTから外れてしまうので、別のトピックで説明したいと思う。
なお、WARNINTに現在のTVより小さい値を設定すると、いきなり警告割り込みが発生してしまうようだ。TVの初期値は0xFFであるため、WARNINTにこれ以下の値を設定する場合は注意が必要である。この点を踏まえたWARNINTへの設定方法については、この後で説明しよう。
WDT用オシレータの周波数はLPC810のSYSCON(System configuration)ユニットのWDTOSCCTRLレジスタへの設定値で決まる。
既にシステムクロックの項でも説明しているが、見返す手間を省き、ここでも触れておこう。
WDTOSCCTRLレジスタのビット構成を以下に示す。DIVSEL、及びFREQSELの設定値に基づいた周波数となる。
SYSCON - WDTOSCCTRLレジスタ
※ユーザーズマニュアル(UM10601) - 4.6.6 Watchdog oscillator control registerより
ビット 記号 説明 b4〜0 DIVSEL 出力時の分周値を指定する。
分周値は以下の式で示される。
分周値 = (DIVSEL + 1) × 2
以下に実際の値を示す。
00000: 2 = (0 + 1) × 2
00001: 4 = (1 + 1) × 2
〜
11111: 64 = (31 + 1) × 2 b8〜5 FREQSEL WDT用オシレータの周波数を指定する。
0000(0x0)〜1111(0xF)の16種類の周波数が選択可能である(別表)。 b9〜 — 0以外設定してはならない。
FREQSEL 周波数 FREQSEL 周波数 FREQSEL 周波数 FREQSEL 周波数 0x0 0 0x4 1.75MHz 0x8 3.00MHz 0xC 4.00MHz 0x1 600KHz 0x5 2.10MHz 0x9 3.25MHz 0xD 4.20MHz 0x2 1.05MHz 0x6 2.40MHz 0xA 3.50MHz 0xE 4.40MHz 0x3 1.40MHz 0x7 2.70MHz 0xB 3.75MHz 0xF 4.60MHz
なお、システムクロックとしてWDT用オシレータを選択していた場合は、既に指定の周波数で動作しているのであらためて設定する必要はない。
それ以外の場合は、WDT用オシレータも設定する必要がある。
enum { WWDT_OSCVAL = 0x03F, /* オシレータを9.375kHzで動作 */ SYS_WDTOSC_PD = 0x1<<6, /* WDT用オシレータ電源供給ビット */ SYS_AHB_CLK_WWDT = 0x1<<17 /* WWDTユニットクロック供給ビット */ }; LPC_SYSCON->WDTOSCCTRL = WWDT_OSCVAL; LPC_SYSCON->PDRUNCFG &= ~SYS_WDTOSC_PD; LPC_SYSCON->SYSAHBCLKCTRL |= SYS_AHB_CLK_WWDT;
リセット直後はWWDTユニットにクロックも電源も供給されてないので、それをしてやる必要がある。PDRUNCFGは1の設定でパワーダウンとなるので、0を書いてやる必要がある。
先にも述べたが、WWDTユニット内では、このクロックをさらに四分周したものを使用している。TCなどの各カウンタには、その周波数をベースにしたカウンタ値を設定する必要がある。
WDT用オシレータでいくつかの周波数を選択し、その時に各カウンタで最大値を設定した時の時間を表にまとめた(TCに関しては最小値も示している)。
1カウント時間は四分周後の時間を示している。これはカウントダウンする間隔になる。例えば、ソースクロックを最小値の9.375kHzとした場合、約430μs毎にカウントダウンされる。この場合、TCに5,000を設定すると約2秒でカウントアップする。
なお、カウンタに1,023(=0x3FF)を設定した場合、1,024回目にカウントアップするので、カウンタ値に対応する時間を式で示すと以下のようになる。
(カウンタ値+1) ÷ (WDT用オシレータの周波数 ÷ 4)
動作モードの選択はWWDTユニットのMODレジスタで行う(LPC_WWDT->MOD)。
本レジスタはビット単位で意味を持ち、ユーザーズマニュアルに基づいた各ビットの機能を以下にまとめる。
特に、ユーザーズマニュアルに書いてない重要な点や、恐らくマニュアルが間違っていると思われる点についてもまとめておこう。
これらのビットの初期値は全て0である。「変更不可」なビットは、一度1を設定すると動作中は0にできなくなるビットだ。
WWDT - MODレジスタ
※ユーザーズマニュアル(UM10601:Rev.1.5 - 6 March 2014) - 12.6.1 Watchdog mode register
ビット 記号 変更不可 説明 b0 WDEN ◎ WDT機能のマスタービット。本ビットを1にすることでWDT機能が有効となる。ただし、ここを1にしただけではWDTカウンタは動作開始しない。この後に明示的なWDTクリアが必要。 b1 WDRESET ◎ タイマ満了時の動作選択ビット。タイマ満了時にシステムリセットする(=1)か、割り込み発生(=0)にするか選択できる。割り込みハンドラは警告割り込みハンドラが使われる。 b2 WDTOF タイマ満了通知ビット。WDTタイマが満了した場合に1がセットされる。0を書いて明示的にクリアする必要あり。 b3 WDINT 警告割り込み発生ビット。警告割り込み発生時に1にセットされる。こちらも明示的にクリアする必要あり。ユーザーズマニュアルの記述とは異なりクリアするためには1を書く必要あり。 b4 WDPROTECT ◎ TCカウンタのプロテクトビット。1を書くことで、TVの値がWARNINT、及びWINDOWの値を下回っている時のみTCが変更可能となる。が、動かない。 b5 LOCK ◎ WDT機能のロックビット。1を書くことでWDT用オシレータを停止しWDT機能自体を止める。が、ビット位置が間違っている。 b6〜 ー (予約)
WDRESET、WDTOFに関してはウィンドウ期間外のWDTクリアでもWDT満了と同じ扱いになる。
なお、WDTの動作を開始するにはWDENに1を設定する必要があるが、それだけでは動作開始しない。その後に明示的なWDTクリア(ユーザーズマニュアルの言い方に則ればfeed操作)が必要なのだ。
enum { WWDT_TC_CNT = 0x2FFF, WWDT_WARN_CNT = 0xFF } enum { WWDT_WDEN = 0x1<<0, WWDT_WDRESET = 0x1<<1 }; LPC_WWDT->TC = WWDT_TC_CNT; LPC_WWDT->MOD = WWDT_WDEN | WWDT_WDRESET; /* 有効にした後に明示的なクリア動作が必要 */ LPC_WWDT->FEED = 0xAA; LPC_WWDT->FEED = 0x55; /* WDTカウンタ(TV)ロード後にWARNINTを設定しないと割り込み発生の危険あり */ LPC_WWDT->WARNINT = WWDT_WARN_CNT;
この場合のクリア動作はウィンドウ期間外とは扱われないようなので、いきなりリセットしてしまう恐れはない。
それから、上ではWDTクリア後にWARNINTの設定を行っているが、これはTVの初期値が0xFFであるため、クリア前にWARNINTの設定を行うと、いきなり警告割り込みが発生してしまうからだ。
WDTクリアを行うことによりTCの設定値がTVにロードされるので、その後にWARNINTの設定を行えば、そのような予期せぬ割り込みに見舞われることはない。
なお、上記のようにWDENを1にしてデバッグする時には注意が必要である。デバッガはWDT用オシレータの動作までは制御できないようであり、プログラムをブレークすることでWDTカウンタが満了してしまう場合がある。これは非常に煩わしいので、デバッグ版と製品版とでMODレジスタの設定を以下のように切り替える必要があるだろう。
/*** WDT動作モードの指定 ***/ enum { // WWDT_MODE = WWDT_WDRESET WWDT_MODE = WWDT_WDEN | WWDT_WDRESET } LPC_WWDT->MOD = WWDT_MODE;
ちなみにWDRESETを0にすることで、暴走検出時にリセットせず単なる割り込みにすることも可能である。
警告割り込みを使わず、こちらを使用して重要な終息処理を行ったあとに自らリセットする(もしくは不具合発生時の状態を温存するため、そのままロックアップしておく)といった手段も取れるだろう。
なお、割り込み要因のWDTOFとWDINTであるが、それぞれクリアする手順が異なる。ユーザーズマニュアルでは両ビットとも0書きでクリアできる記述があるが、WDINTをクリアするには1を書く必要がある。
よくよく調べてみると、WWDTのサンプルプログラム(lpc8xx_wdt.c)でもそのようなコードになっている。
/* LPCXpresso付属のサンプルプログラムから抜粋 */ #define WDTOF (0x1<<2) #define WDINT (0x1<<3) void WDT_IRQHandler(void) { /* 〜略 */ LPC_WWDT->MOD |= (WDINT | WDTOF); /* clear the interrupt flag */ LPC_WWDT->MOD &= ~(WDINT | WDTOF); /* clear the time-out flag */ }
どうやらマニュアルの方が間違っているように思うのだが、Rev.1.5でも直ってないようである。(以下引用。下線は筆者。)
WDINT The Watchdog interrupt flag is set when the Watchdog counter reaches the value specified by WARNINT. This flag is cleared when any reset occurs, and is cleared by software by writing a 0 to this bit.
それから、WDPROTECTビットとLOCKビットだが、これははっきり言ってユーザーズマニュアルもおかしいし、実際の動作もおかしい。
以下のようなサンプルプログラムを作って動作確認してみた。なお、WDT_CLK_Setupはライブラリ内のクロック設定関数である。
WDT_CLK_Setup(); /* 一番遅い9.375kHzで初期化 */ LPC_WWDT->TC = 0x2FFF; /* 約5秒 */ LPC_WWDT->WINDOW = LPC_WWDT->TC; /* 常にクリア可 */ LPC_WWDT->WARNINT = 0; NVIC_EnableIRQ(WDT_IRQn); /* ユーザーズマニュアル(UM10601)によるとb4はプロテクトのはず */ LPC_WWDT->MOD = (0x1 << 0) | (0x1 << 4); LPC_WWDT->FEED = 0xAA; LPC_WWDT->FEED = 0x55; LPC_WWDT->TC = 0x3FFF; /* WARNINTが0なので変更は不可のはず */ for (;;) { ; }
ユーザーズマニュアルによればTCを変更可能なのは、WDTカウンタがWINDOW、及びWARNINTよりも下回っている時のみのはずだ。(以下引用。下線は筆者。)
UM10601 - 12.5.3.2 Changing the WWDT reload value(p.174)
If bit 4 is set in the WWDT MOD register, the watchdog time-out value(TC) can be changed only after the counter is below the value of WDWARNINT and WDWINDOW.
WARNINTを0にしているので、この値を下回ることは有り得ないため、TCの書き換えはできないはずである。 デバッガでこのプログラムを動作させ、一分ぐらい放置してからブレークした時の様子が以下の通りである。
TCは16,383(0x3FFF)に書き替わっており、MODは17(0x11)なのでb0とb4のみが1で、WDTOF(b2)もWDTINT(b3)も1になってない。
WDT用オシレータとして9.375kHzを選択したので、四分周後の周波数は約2.344kHz、TCが0x2FFFの場合は約5秒、0x3FFFでも約7秒で満了である。一分以上放置したにも関わらず何も動作してない。
そもそもTVが初期値255(0xFF)のままである。WDTが動いてないのだろう。
これは、どちらかというと隣のLOCKビット(b5)の動きだ。
7秒ほど放置すると、以下のようにWDTOFとWDTINTが1になった状態で割り込み(WDT_IRQHandler)が発生する。
TCが16,383(0x3FFF)の場合は約7秒後ぐらいに満了となるため、書き替わった後のTCに基づいた動作であることが分かる。(TVがTCと同じ値になっているのは、カウンタが満了しTCの値が自動的にリロードされたのだろう。)
実動作ではb4の方がLOCKビットのように動いている。
実動作ではb4にもb5にもWDPROTECTの動作を認められなかった。
うーん、使い方が悪かったのだろうか。しかしLOCKビットの位置がずれているのは間違いないだろう。どちらにしろ、これらのビット機能は使わないのが正解だ。実際、使わなくともWDT機能としては十分だ。
なお、ここまでの内容をベースにWDT機能を使ったサンプルプログラムを作ってみた。
GitHubで公開(https://github.com/mitsware/lpc8xxBasic)しておいたので、興味のある方は参考にして頂きたい。
このサンプルプログラムでは、以前「ライブラリ使用上の注意点」でも触れた問題についても対応している。