Archive for 2012/03

.NET CF アプリケーションからデバッグメッセージ出力

C# や Visual Basic で WinCE/WEC のアプリケーションを開発する際に、デバッグメッセージを出力する方法は、いくつかあります:

  • System.Console クラスの Write(), WriteLine() を呼び出す
  • System.Diagnostics.Debug クラスの Write()/WriteIf(), WriteLine()/WriteLineIf() を呼び出す
  • P/Invoke を使って NKDbgPrintfW() を呼び出す

上に挙げた三つの方法について、それらがどう違うのか、以下に述べます。なお、ここでは、.NET CF (.NET Compact Framework) の 3.5 について記します。

■System.Console クラスを使う場合
このクラスのメソッド一覧は、次のリファレンスページに載っています:

 Console Methods
 http://msdn.microsoft.com/en-us/library/system.console_methods(v=vs.90).aspx

System.Console クラスの Write() や WriteLine() を呼び出すと、コンソールウィンドウが開いて、引数に渡した文字列が表示されます。これは、ネイティブコードの場合に、printf() などを使って標準出力へ出力した場合と同じ振る舞いです。クラス名の通り、コンソールデバイスに出力する API というわけです。

■System.Diagnostics.Debug クラスを使う場合
このクラスのメソッド一覧は、次のリファレンスページに載っています:

 Debug Methods
 http://msdn.microsoft.com/en-us/library/system.diagnostics.debug_methods(v=vs.90).aspx

System.Diagnostics.Debug クラスの Write() や WriteLine() を呼び出すと、アプリケーションをデバッガから実行している場合には、デバッガの出力ウィンドウに文字列が出力されます。デバッガから実行していない場合(通常実行の場合)には、何も起きません。この振る舞いは、OutputDebugString() を呼び出した場合と同様です。

このクラスの Write() や WriteLine() の呼び出しが、OutputDebugString() の呼び出しと同様になるのは、System.Diagnostics.DefaultTraceListener クラスの働きによるものです。DefaultTraceListener クラスの説明は、次のリファレンスページに載っています。

 DefaultTraceListener Class
 http://msdn.microsoft.com/en-us/library/system.diagnostics.defaulttracelistener(v=vs.90).aspx

System.Diagnostics.DefaultTraceListener のインスタンスは、System.Diagnostics.Debug クラスの Listeners プロパティのデフォルト要素であり、そのため、Write() や WriteLine() によるデバッグメッセージ出力は、通常は System.Diagnostics.DefaultTraceListener の Write(), WriteLine() に渡されます。そして、System.Diagnostics.DefaultTraceListener は、Write() や WriteLine() に渡された文字列を引数にして OutputDebugString() を呼び出す、というわけです。

ただし、ネイティブコードで OutputDebugString() を直接呼び出した場合とは異なり、アプリケーションをデバッガから実行していないと、System.Diagnostics.Debug の Write() や WriteLine() を呼び出しても、一切出力されません。一方、ネイティブコードから OutputDebugString() を呼び出した場合は、アプリケーションデバッガから実行していなくても、WinCE/WEC のカーネルデバッガを実行していれば、カーネルデバッガの出力ウィンドウに文字列が出力されます。.NET CF のアプリケーションから P/Invoke で直接 OutputDebugString() を呼び出した場合にどうなるかは、確認していませんが、おそらく、ネイティブコードから OutputDebugString() を呼び出した場合と同じになるのではないかと思います。興味のある方は、試してみて下さい。

System.Diagnostics.Debug クラスの使い方については、次のサポートページも参考になるでしょう:

 How to trace and debug in Visual C#
 http://support.microsoft.com/kb/815788/en-us

■NKDbgPrintfW() を P/Invoke で呼び出す場合
さて、ネイティブコードの場合、デバッグメッセージの出力は、DEBUGMSG() マクロを使うのが普通です。C# や Visual Basic では、このマクロは使えませんので、その代わりに、DEBUGMSG() マクロが使っている NKDbgPrintfW() を、P/Invoke で直接呼び出す必要があります。

たとえば、次のページにあるフォーラムの質問でも、そのような回答がなされています:

 DebugMsg macro in C#
 http://us.generation-nt.com/answer/debugmsg-macro-c-help-51715972.html

.NET CF アプリケーションから、P/Invoke で NKDbgPrintfW() を直接呼び出した場合は、アプリケーションをデバッガから実行していない場合でも、WinCE/WEC のカーネルデバッガを実行していれば、カーネルデバッガの出力ウィンドウに文字列が出力されます。.NET CF アプリケーションを、ネイティブコードのアプリケーションやライブラリと一緒にデバッグしていて、追跡したい個所の呼び出しタイミングの関係を知りたい場合には、NKDbgPrintfW() を使うと、デバッグメッセージをカーネルデバッガの出力に渡すことができます。つまり、.NET CF アプリケーションのデバッグメッセージも、ネイティブコードのデバッグメッセージも(さらには、デバイスドライバのデバッグメッセージも)同じ出力に出せますので、それらを同時に追跡したい場合には、便利でしょう。

なお、P/Invoke で NKDbgPrintfW() を直接呼び出すと、ネイティブコードで DEBUGMSG() マクロを使った場合とは異なり、リリースビルドでもデバッグメッセージが出力されてしまいます。その点は、注意して下さい。デバッグビルドの時にだけデバッグメッセージが出力されるようにするには、System.Diagnostics.ConditionalAttribute を使ってデバッグビルドに対してのみ有効にするか、または、#if … #endif を使って条件コンパイル指定して下さい。ちなみに、System.Diagnostics.Debug クラスの Write() や WriteLine() では、System.Diagnostics.ConditionalAttribute を使っています。

Add comment 2012/03/29 koga

WinCE/WEC のウォッチドッグタイマ機能(補足)

前回のエントリでは、WinCE/WEC のウォッチドッグタイマ機能について、ハードウェアタイマとソフトウェアタイマの二種類があることを述べました。そして、ハードウェアタイマとソフトウェアタイマでは、ソフトウェアタイマの方が、より柔軟であり、以下の点が異なると説明しました:

  1. ソフトウェアタイマは、複数の監視対象を設定できる(タイマを複数作成できる)
  2. ソフトウェアタイマは、タイマ満了時に発火させるアクションとして、デバイスをリセットする以外の動作を設定できる
  3. ソフトウェアタイマの場合は、カーネルではなく、監視対象のアプリケーションやスレッドが、タイマのリフレッシュ動作を行う

今回のエントリでは、上に示した、ハードウェアタイマとソフトウェアタイマの違いのうち、2. について補足します。ウォッチドッグタイマ機能において、ソフトウェアタイマとハードウェアタイマのどちらも、タイマの満了時にデバイスをリセットできますが、リセット動作の詳細は、異なるのです。

■ハードウェアタイマについての注意点
前回のエントリでは、ソフトウェアタイマがタイマ満了時に発火させることのできる三通りのアクションのうち、WDOG_RESET_DEVICE のことを、「OS をリブート(デバイスをリセット)する」動作だと説明しました。この「OS をリブート」というのが、ハードウェアタイマとは違う点です。ハードウェアのウォッチドッグタイマは、プロセッサを強制リセットします。強制リセットする際、OS がどのように動作しているのかは関知しません。

ここで、ハードウェアタイマの場合には、WinCE/WEC のカーネルが(より正確には、ウォッチドッグタイマ用のカーネルスレッドが)タイマのリフレッシュ動作を行います。従って、ハードウェアタイマが満了したということは、カーネルが動作を停止しているということですから、プロセッサを強制リセットするのが妥当です。通常は、その通りです。しかし、何らかの不具合で OS 全体がフリーズしているわけではないのに、カーネルが動作を停止する場合もあるのです。どんな場合でしょうか?

それは、カーネルデバッガを使っている時です。カーネルデバッガを使ってデバッグ動作している状態では、ブレークポイントにヒットすると、OS 全体の動作が停止します。OS 全体の動作が停止した状態であっても、ハードウェアタイマは動作し続けますから、ハードウェアタイマの満了時間が過ぎると、その時点で、ハードウェアのウォッチドッグタイマの働きにより、プロセッサが強制リセットされてしまうのです。ソフトウェアタイマの場合には、タイマの動作も停止していますから、そのようなことは、起きません。

カーネルデバッガを使ったデバッグ中に、予期せぬ強制リセットが起きしてしまわないよう、カーネルデバッガを使う時にはハードウェアタイマを動かさないか、または、タイマの満了時間をできるだけ長い値に設定して下さい。

■ソフトウェアタイマによるリセット動作
さて、ソフトウェアタイマのリセット動作は、「OS をリブート」する点がハードウェアタイマと違うと上で述べました。これについて、説明します。

まず、ハードウェアタイマのリセット動作では、プロセッサのリセット信号線を使って、ハードウェアによるリセットを行います。つまり、ハードウェアのウォッチドッグタイマが、プロセッサに対してリセット信号を出力する(プロセッサのリセット入力信号線に供給する電圧の論理値を、リセット用の値に変化させる)ことによって、プロセッサを強制リセットします。

一方、ソフトウェアタイマでは、ウォッチドッグタイマ用のカーネルスレッドが、OS のリブート処理を始動することによって、リセット動作を起こします。具体的には、IOCTL_HAL_REBOOT を引数として KernelIoctl() を呼び出します。興味のある方は、ソースコードをご覧になってみて下さい。ウォッチドッグタイマ用のカーネルスレッドのソースは、前回述べた通り、
 %_WINCEROOT%/PRIVATE/WINCEOS/COREOS/NK/KERNEL/watchdog.c
にあり、WatchDogTimerThrd() が、そのスレッドが実行する手続きです。WatchDogTimerThrd() を見ると、ソフトウェアタイマのどれか一つが満了すると、タイマの状態が WD_STATE_SIGNALED になり、その結果、そのタイマに設定されたアクションが実行されることが分かります。タイマに設定されたアクションの実行は、WDTakeDfltAction() を呼び出すことによって行われますが、WDTakeDfltAction() では、タイマに設定されたアクションが WDOG_RESET_DEVICE の場合、IOCTL_HAL_REBOOT を引数として KernelIoctl() を呼び出す実装になっています。

IOCTL_HAL_REBOOT を引数として KernelIoctl() を呼び出すことによって OS のリブート動作が起きる仕組みについては、2009/07/20 のエントリ(「アプリケーションからリブート(または電源 OFF)~その1(1/2)」)に書いています。もし興味があれば、お読みになってみて下さい。

2009/07/20 のエントリで書いたように、カーネルスレッドが、IOCTL_HAL_REBOOT を引数として KernelIoctl() を呼び出した場合には、レジストリやファイルシステムのフラッシュ動作が実行されたのち、OALIoCtlHalReboot() が呼び出されます。OALIoCtlHalReboot() は、カーネル移植レイヤ(OAL)で実装されており、プロセッサごとに異なるリセット動作を実行します。一般的には、ハードウェアのウォッチドッグタイマの発火時間を短い値にセットしたのち、無限ループを実行し、ハードウェアタイマによってプロセッサを強制リセットさせます。

■ハードウェアタイマについて、もう一度だけ注意
ここで重要なことは、ソフトウェアタイマによるリセット動作では、プロセッサをリセットする前に、レジストリやファイルシステムのフラッシュ動作を実行する、という点です。このことにより、ウォッチドッグタイマが発火してリセット動作が起きた際に、永続記憶域上のレジストリ内容やファイルシステムが、壊れてしまう危険性が抑えられるのです。ソフトウェアタイマが発火する原因になった監視対象のアプリケーションが、ファイル操作を行っていた場合には、ファイル内容が壊れてしまう可能性はあります(ファイルの書き込みを行っている途中でフリーズした場合など)。しかし、ディレクトリが壊れて読めなくなってしまったりするといった致命的なことは、起きないようにするというわけです。

この点も、ハードウェアタイマとは違う点です。通常動作中に OS 全体がフリーズしてしまった場合は、強制リセットが起きるのは仕方ありません。しかし、カーネルデバッガを使ったデバッグ中に、ハードウェアタイマが満了してしまい、予期せぬ強制リセットによってレジストリやファイルシステムが壊れてしまう、というようなことが起きないよう、ハードウェアタイマを使う場合には、ご注意ください。

Add comment 2012/03/27 koga

WinCE/WEC のウォッチドッグタイマ機能

■ウォッチドッグタイマの必要性
組み込み機器、特に、PC のような対話的な操作ができない、ヘッドレスのデバイスなどでは、より高い信頼性や耐障害性が求められます。逆に、タッチパネルディスプレイのような、表示と入力のインタフェースを備えたデバイスであれば、PC と同様に、アプリケーションがフリーズするなど異常が起きた時に、デバイスを操作するユーザが異常に気づき、アプリケーションを終了したり、デバイスをリセットしたりして、すぐに対処できます。

しかし、ユーザが対話的に操作せずに、常時稼働するタイプの組み込み機器の場合には、アプリケーションや OS がフリーズした場合、自動的に異常を復旧する仕組みがないと、機器が異常を起こしたままになってしまいます。その結果、深刻な問題を引き起こすおそれがあります。たとえば、自動販売機がフリーズして動かなくなってしまったり、工場のラインの自動制御システムで、制御装置のアプリケーションがフリーズした場合のことを、考えてみて下さい。

アプリケーションや OS がフリーズした場合に、デバイスをリセットしたり、あるいは、フリーズしたアプリケーションを終了して再始動したりするための仕組みとして、ウォッチドッグタイマがあります。Linux であれば、/dev/watchdog として提供されており、ウォッチドッグタイマを更新する watchdog デーモンも提供されています:

 The Linux Watchdog driver API
 http://kernel.org/doc/Documentation/watchdog/watchdog-api.txt

 watchdog(8)
 http://linux.die.net/man/8/watchdog

組み込み機器向けのプロセッサは、ウォッチドッグタイマを内蔵しているものが多く、それを利用するのが一般的です。ハードウェアのウォッチドッグタイマ機能がない場合には、OS のカーネルで、ソフトウェアによるウォッチドッグタイマ機能が提供されることもあります。Linux の場合、/dev/watchdog のウォッチドッグタイマ機能は、ハードウェアで実装されることもあれば、/dev/watchdog はデフォルト実装のカーネル内部のソフトウェアタイマとして提供し、プロセッサ内蔵のウォッチドッグタイマに対しては、別途ブートローダで設定する、という仕組みで提供されることもあるようです。PC 用の Linux の場合、/dev/watchdog は、BIOS の機能を使ったハードウェアタイマとして実装されているようです。

■ハードウェアタイマとソフトウェアタイマ
ウォッチドッグタイマ機能の、ハードウェアによる実装(ハードウェアタイマ)とソフトウェアによる実装(ソフトウェアタイマ)の違いは、何でしょう?

答えは、OS がフリーズした場合に動作できるかどうかです。ハードウェアタイマの場合は、OS が動作しているかどうかに関係なく、独立したハードウェアの機能として動作しますから、たとえ OS 全体がフリーズしていても、設定した通りに動作します。つまり、設定したタイマ満了時間以内にタイマを更新(リフレッシュ)しなければ、タイマが満了した時点で、プロセッサが強制リセットされます。一方、ソフトウェアタイマの場合は、OS 全体がフリーズすると、タイマの動作も停止してしまいますから、タイマ満了時間が過ぎても、何も起きません。つまり、ソフトウェア実装のウォッチドッグタイマ機能は、OS がフリーズした場合には、役に立たないのです。

■ソフトウェアタイマの長所
OS 全体がフリーズした場合(カーネルのスケジューラも停止した場合)には機能しない、という点は、ソフトウェアタイマの短所です。一方、長所もあります。ハードウェアタイマは、一つしか満了時間を設定できませんが、ソフトウェアタイマには、ハードウェアの制約がありませんので、複数のタイマを設定できるように実装することが可能です。

ウォッチドッグタイマを使う目的は、OS や、特定のアプリケーション(プロセス)、あるいはスレッドが、正常に動作し続けているかどうかを監視することにあります。ウォッチドッグタイマが満了した場合、何らかの障害が起きて、それらの監視対象が動作していないということを意味します。そのため、ウォッチドッグタイマが満了した場合は、OS をリブートするなどの、異常からの復旧処理を発火させる、というわけです。

さて、ソフトウェアタイマによって、複数のウォッチドッグタイマを設定できるということは、監視対象を複数にできる、ということです。つまり、複数のウォッチドッグタイマを設定できるなら、常時稼働していなければならないアプリケーション(プロセス)やスレッドが複数ある場合に、それらに対して個別にウォッチドッグタイマを設定し、どれか一つでも動作していないことが検出されたら、復旧処理を発火できます。WinCE/WEC では、それが可能です。さらに、ウォッチドッグタイマの満了時に発火させる復旧処理として、OS をリブートする以外の動作を設定できます。

■WinCE/WEC のウォッチドッグタイマ機能
WinCE/WEC のウォッチドッグタイマ機能には、ハードウェアタイマとソフトウェアタイマがあり、ソフトウェアタイマは、カーネルの API を使って利用できます。ソフトウェア実装のウォッチドッグタイマについては、リファレンスの次のページをご覧下さい:

 CreateWatchDogTimer (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee482966(v=winembedded.60).aspx

 CreateWatchDogTimer (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee482966.aspx

上のページで説明されている CreateWatchDogTimer() を使って、ソフトウェア実装のウォッチドッグタイマを設定できます。この関数では、タイマの満了時間に加え、タイマ満了時に発火させるアクションを指定できます。指定できるアクションは、次の三つです:

  • WDOG_KILL_PROCESS    監視対象のプロセスを強制終了する
  • WDOG_NO_DFLT_ACTION  何もしない
  • WDOG_RESET_DEVICE    OS をリブート(デバイスをリセット)する

CreateWatchDogTimer() は、戻り値として、ウォッチドッグタイマのハンドルを返します。このハンドルは、non signaled な状態ですが、タイマが満了した時点で、signaled な状態にセットされます。タイマ満了時に発火させるアクションとして、WDOG_NO_DFLT_ACTION を指定すると、ウォッチドッグタイマのハンドルに対して WaitForSingleObject() を呼び出すことにより、タイマが満了したことを検出できます。アプリケーション内で、特定のスレッドが動作しなくなったことを検出したいなどの場合には、WDOG_NO_DFLT_ACTION を使うのが便利でしょう。

CreateWatchDogTimer() で生成したウォッチドッグタイマを更新(リフレッシュ)するには、RefreshWatchDogTimer() を呼び出します。ウォッチドッグタイマを生成した後、StartWatchDogTimer() でタイマを始動したら、CreateWatchDogTimer() の dwPeriod 引数で指定したタイムアウト時間よりも短い間隔で、RefreshWatchDogTimer() を繰り返し呼び出さなければいけません。そうしないと、タイマが満了してしまいます。

次に、ハードウェアタイマについて見てみましょう。

ハードウェアタイマに対しては、API は提供されておらず、実装用のインタフェースが、カーネル移植レイヤ(OAL)で定義されています。ハードウェアがウォッチドッグタイマをサポートしていない場合は、OAL の初期化関数である OEMInit() の中で、ウォッチドッグタイマのインタフェース設定を変更せず、デフォルトのままにします。デフォルトでは、ハードウェアタイマ機能は、使用されません。ハードウェアタイマを有効にする場合は、OAL において、ハードウェアタイマの初期化処理とリフレッシュ処理を実装し、ウォッチドッグタイマのインタフェースを設定します。具体的には、OEMGLOBAL 構造体の dwWatchDogPeriod と pfnRefreshWatchDog を、OEMInit() で設定し、pfnRefreshWatchDog に設定した関数で、リフレッシュ処理(および、必要ならハードウェアタイマの初期化処理も)を実行します。 dwWatchDogPeriod は、デフォルトでは 0 に設定されており、この値が 0 の場合は、ハードウェアタイマが存在しないものとして扱われます。なお、pfnRefreshWatchDog のデフォルト値は、NULL ではなく、何もしない関数を指す関数ポインタです。

 dwOEMWatchDogPeriod (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee479342(v=winembedded.60).aspx

 pfnOEMRefreshWatchDog (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee479294(v=winembedded.60).aspx

 OEMGLOBAL (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee478176(v=winembedded.60).aspx

上は、WinCE 6.0 のリファレンスです。WEC 7 のリファレンスは、以下にあります。

 dwWatchDogPeriod (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee479342.aspx

 OEMRefreshWatchDog (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee479294.aspx

 OEMGLOBAL (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee478176.aspx

ハードウェアタイマに対しては、OAL で設定したリフレッシュ間隔(dwWatchDogPeriod)以内の間隔で、WinCE/WEC カーネルが定期的に pfnRefreshWatchDog の関数を呼び出すことにより、リフレッシュ動作を行います。一方、ソフトウェアタイマの場合は、CreateWatchDogTimer() を呼び出したアプリケーション自身が、リフレッシュ動作を行います(あるいは、監視対象のアプリケーションと、監視するアプリケーションの二つに役割を分割し、監視する役割のアプリケーションが CreateWatchDogTimer() を呼び出し、監視対象のアプリケーションが RefreshWatchDogTimer() を呼び出す、というやり方も考えられます)。

■ウォッチドッグタイマ機能の実装を見てみる
上で述べた、WinCE/WEC のウォッチドッグタイマ機能について、どのように実装されているのか、カーネルのソースコードを見てみるのも、面白いでしょう。カーネルが提供するウォッチドッグタイマ機能のソースコードは、
 %_WINCEROOT%/PRIVATE/WINCEOS/COREOS/NK/KERNEL/watchdog.c
にあります。wachdog.c で実装されている WatchDogTimerThrd() が、ウォッチドッグタイマのカーネルスレッドが実行する手続きです。

ウォッチドッグタイマのカーネルスレッドは、ハードウェアタイマが存在する場合か、または、CreateWatchDogTimer() が呼び出された場合に生成されます(*1)。ハードウェアタイマが存在する場合、つまり、OEMInit() の中で、OAL によって OEMGLOBAL 構造体の dwWatchDogPeriod と pfnRefreshWatchDog が設定された場合は、カーネルのスケジューラが初期化時に呼び出す InitWatchDog() の中で、ウォッチドッグタイマのカーネルスレッドを生成・始動します。ハードウェアタイマが存在しない場合には、CreateWatchDogTimer() が最初に呼び出された時に、ウォッチドッグタイマのカーネルスレッドが生成・始動されます(*1)

(2012-04-19 追記)
*1: これは、WinCE 6.0 の場合です。WEC 7 のカーネルでは、ハードウェアタイマが存在するか否かに関わらず、
InitWatchDog() において、ウォッチドッグタイマのカーネルスレッドが生成されます。

ウォッチドッグタイマのカーネルスレッドは、ハードウェアタイマとソフトウェアタイマの両方を管理します。WatchDogTimerThrd() の実装を見ると分かりますが、システムが終了するまで実行を続けるループの中で、ハードウェアタイマと全てのソフトウェアタイマのタイマ満了時刻に従って待ち動作を行い、適切なタイミングで、各タイマをリフレッシュします。ハードウェアタイマもソフトウェアタイマも存在しない状態では、待ち時間を無限に設定して待ち動作を行います。この待ち動作は、CreateWatchDogTimer() や StartWatchDogTimer(), StopWatchDogTimer() を呼び出した際に解除され、ウォッチドッグタイマのカーネルスレッドが、待ち時間を再調整します。

ハードウェアタイマについては、たとえば、WinCE 6.0 のデバイスエミュレータであれば、以下のソースコードを見て下さい:
 %_WINCEROOT%/PLATFORM/DEVICEEMULATOR/src\oal/oallib/watchdog.c
 %_WINCEROOT%/PLATFORM/DEVICEEMULATOR/src\oal/oallib/timer.c
 %_WINCEROOT%/PLATFORM/DEVICEEMULATOR/src\oal/oallib/init.c

上のソースファイルのうち、watchdog.c にある SMDKInitWatchDogTimer() と RefreshWatchdogTimer() が、ハードウェアタイマ用の初期設定と、ハードウェアタイマのリフレッシュ処理を行う関数です。SMDKInitWatchDogTimer() は、timer.c にある OALTimerInit() を経由して、init.c にある OEMInit() から呼び出されます。つまり、OEMInit() の中で、(SMDKInitWatchDogTimer() によって)OEMGLOBAL 構造体の dwWatchDogPeriod と pfnRefreshWatchDog が設定されます。RefreshWatchdogTimer() は、最初に呼び出された時は、プロセッサ(S3C2410)内蔵のウォッチドッグタイマを初期化し、それ以降の呼び出しでは、ウォッチドッグタイマをリフレッシュ(タイマのカウンタをリセット)します。

なお、SMDKInitWatchDogTimer() を見ると、dwWatchDogPeriod と pfnRefreshWatchDog という変数に対して代入を行っていますが、これらの名前は、OEMGLOBAL 型の大域変数のメンバを指すように #define されています。興味のある方は、
 %_WINCEROOT%/PUBLIC/COMMON/OAK/INC/bcoemglobal.h
をご覧になってみて下さい。

Add comment 2012/03/19 koga

WinCE/WEC でシリアルコンソール

Windows Embedded CE/Compact には、PC 用の Windows と同様のコマンドプロセッサ(cmd.exe)が付属しているのは、皆さんご存じの通りです。今回は、このコマンドプロセッサをシリアルケーブル経由で PC から操作する手順について述べます。

さて、Platform Builder のカタログ項目ビューで依存項目を見ると、コマンドプロセッサには、コンソールウィンドウと Telnet サーバーが依存していることが分かります。コンソールウィンドウは、「コマンドプロンプト」のウィンドウですが、コンソールアプリケーションから printf() などで標準出力に出力した場合も、コンソールウィンドウが開いて出力内容が表示されます。PC 用の Windows でも、同様にコンソールウィンドウが開きますよね。つまり、コンソールウィンドウは、標準出力と結びつく(同様に、標準入力とも結びつく)コンソールデバイスとして機能するというわけです。Telnet サーバについては、後で述べます。

■コンソールの出力先に対するレジストリ設定
コマンドプロセッサを、シリアルケーブル経由で PC から操作するには、コンソールの出力先の設定を変更します。この設定は、リファレンスの次のページで説明されています:

 Command Processor Registry Settings (Windows Embedded CE 6.0)
 http://msdn.microsoft.com/en-US/library/ee506287(v=winembedded.60).aspx

 Command Processor Registry Settings (Windows Embedded Compact 7)
 http://msdn.microsoft.com/en-us/library/ee506287.aspx

上のページで説明されているように、[HKEY_LOCAL_MACHINE\Drivers\Console] キー直下の OutputTo の値により、コンソールの出力先を、コンソールウィンドウ以外に設定できるのです。たとえば、2番のシリアルポート(COM2)であれば、次のような設定になります。


[HKEY_LOCAL_MACHINE\Drivers\Console]
    "OutputTo"=dword:2
    "COMSpeed"=dword:1C200

上の例は、COM2 をコンソールの出力先とし、COM2 のボーレートを 115200 としています。

上記のように設定することで、シリアルケーブルで接続した PC から、ターミナルソフトを使ってコマンドプロセッサを操作できます。このように設定しておくと、コマンドプロセッサ(cmd.exe)を起動すると、コンソールウィンドウは開かず、代わりに、シリアルケーブルで接続した PC のターミナルソフトのウィンドウに、コマンドプロセッサのプロンプトが出力されます。そして、コマンドプロセッサのプロンプトに対して、コマンド名やプログラム名をタイプ入力すれば、Windows Embedded CE/Compact 上のコンソールウィンドウの場合と同様、コマンドやプログラムを実行できます。

なお、コンソール出力をデフォルト(0)以外に設定すると、「コマンドプロンプト」を起動しても、ウィンドウが開かず、コンソール出力に設定したデバイスにプロンプトが出力されます。この点は、注意して下さい。

■シリアルコンソールに関する注意点
シリアルケーブル経由でコマンドプロセッサを使う設定については、たとえば、次の Blog エントリでも紹介されています:

 Command line (Console) over serial
 http://ce4all.blogspot.com/2007/06/command-line-console-over-serial.html

このエントリには、いくつか読者コメントが付いており、最後のコメントにある「入力がエコーバックされない」という質問には、回答が付いていないままです。実は、エコーバックされないのが通常動作です。コマンドプロセッサ(cmd.exe)のソースを見ると分かりますが、cmd.exe 自身は、コンソールに対して入力をエコーバックしません。バッチファイル(.bat, .cmd)の中で、echo コマンドによるエコーバックの ON/OFF 切り替えは出来ますが、コンソールに対しては、echo コマンドの実行有無とは関係無く、エコーバック動作は行いません。

cmd.exe のソースコードは、WinCE/WEC のソースツリーの、以下の場所にあります:

 %_WINCEROOT%/private/winceos/UTILS/cmd2/

コンソールに対する入力処理は、cmd.cxx にある cmd_GetInput() で実装されていますが、バッチファイル(.bat, .cmd)の実行ではなく、コンソールからの入力、つまり標準入力に対する入力処理では、エコーバックは行いません。そもそも、コマンド入力を _fgetts() で取得していますので、タイプ入力の1文字ごとにエコーバックする仕組み自体がないのです。従って、シリアルコンソールで入力のエコーバックを実現するには、自前でエコーバック処理を実装する必要があります。

■タイプ入力に対するエコーバック動作
コマンドプロセッサのプロンプトに対してエコーバックする処理は、コマンドプロセッサが標準入出力を行う先の、「コンソールデバイス」が担当する役割です。たとえば、コンソールウィンドウは、コマンドプロセッサがデフォルトで使用するコンソールデバイスであり、コンソールウィンドウにおけるタイプ入力のエコーバック処理は、コンソールウィンドウが行います。

もう一つの例は、Telnet サーバです。以下のページでも言及されていますが、Telnet サーバは、自身をコンソールデバイスとして登録したうえで、コマンドプロセッサ(cmd.exe)を起動します。

 http://msdn.microsoft.com/en-us/library/aa446909.aspx (Implementing a Network Service on Windows CE)

そして、Telnet クライアントでのタイプ入力を受け取り、それを、コンソールデバイスのインタフェースを介してコマンドプロセッサへ渡し、コマンドの実行を依頼します。コマンドプロセッサがコマンドを実行し、標準出力へ出力を行うと、それが(入力とは逆の向きに)コンソールインタフェースを介して Telnet サーバに渡され、Telnet クライアントに送信される、というわけです。なお、Telnet の場合には、Telnet クライアントでのタイプ入力のエコーバック処理は、Telnet クライアント自身が行うことになるはずです。

コンソールウィンドウのソースコードは、開示されていないため、コンソールデバイスとしての振る舞いの詳細を見ることができません。一方、Telnet サーバの方は、
 %_WINCEROOT%/public/servers/sdk/samples/telnetd/
にソースコードが収録されていますので、興味のある方は、ご覧になってみて下さい。telndev.cpp において、Stream Interface Driver のインタフェース(TEL_Init(), TEL_Deinit(), TEL_Open(), TEL_Close(), TEL_Read(), TEL_Write(), TEL_Seek(), TEL_IOControl(), TEL_PowerUp(), TEL_PowerDown() 関数)を実装しており、TEL_Read() と TEL_Write() で、コンソール入出力の処理を行います。

Telnet サーバが cmd.exe を起動する処理は、telnetd.cpp にある TelnetLaunchCmd() ですが、ここでは、cmd.exe を起動する前に、SetStdioPathW() を呼び出して標準入出力を変更します。この時、コンソールデバイスとして、自分自身を実体に割り当てたデバイス(”TEL<インデックス>:” というデバイス名)を設定することにより、cmd.exe の標準入出力先として自身を設定します。Telnet サーバは、Windows の流儀でいう「サービス」、つまり、ユーザモードのデバイスドライバとして動作しますから、このような動作が可能なのです。

■エコーバック動作の実現方策
では、シリアルコンソールの場合に、タイプ入力のエコーバック動作を実現するには、どうすればよいのでしょうか?

方策は、二つ考えられます。一つは、Telnet サーバのように、自前でコンソールデバイス機能を実装し、コマンドプロセッサとシリアルポートの間に介在することにより、シリアルポートからの入力を、一文字ごとにエコーバックする、という方策です。

もう一つは、シリアルポートに対して直接入出力を行い、シリアルポートから文字列が入力されたら、入力された文字列をコマンド名として、都度コマンドプロセッサ(cmd.exe)を起動する、という方策です。

二つの方策のうち、後者の方が、より簡単に実装できます。cmd.exe にコマンドを実行させるには、/Q と /C オプション付きで cmd.exe を実行すればよいのです。つまり、
 ”/Q /C <コマンド名>[ コマンド引数]”
という文字列を、CreateProcess() の第二引数として渡し、第一引数に “cmd.exe” を渡して呼び出せば、cmd.exe がコマンドを実行し、その結果が、レジストリで設定したコマンドプロセッサのコンソール、つまりシリアルポートへ出力されます。その際、CreateProcess() が返したプロセスハンドルに対して WaitForSingleObject() を呼び出せば、コマンド実行の完了を待つことが出来ます。

ただし、二番目の方策については、次の点を注意して下さい:

・コマンド実行のたびに、都度 cmd.exe を起動するので、シリアルコンソールから cd コマンドが実行されても、その結果が保持されない。
 (cd コマンドに対応するとしたら、移動先に指定されたディレクトリを、cmd.exe を起動するプログラム自身が記録することによって、履歴動作を実現する必要がある。)

・cmd.exe を実行している間は、シリアルポートを閉じて、cmd.exe がシリアルコンソールを使用できるようにしなければならない。

二番目の注意点ですが、これは、シリアルポートのデバイスドライバの制約によるものです。シリアルポートは、一つの実体に対し、デバイスを一つしかオープンできないのです。たとえば、COM1 であれば、”COM1:” を指定して CreateFile() を呼び出すことにより、シリアルポートのデバイスをオープンします。この時、”COM1:” のデバイスをオープンしたままの状態で、同じシリアルポート(”COM1:”)に対して CreateFile() を呼び出すと、エラーとなり、オープンできません。この制約は、シリアルポートのデバイスドライバの、MDD レイヤの実装によるものです。

シリアルポートのデバイスドライバの MDD レイヤのソースコードは、
 %_WINCEROOT%/public/COMMON/oak/drivers/serial/com_mdd2/
に収録されています。このディレクトリにある mdd.c で実装されている COM_Open() を見ると、既にオープン済みのシリアルポートに対して呼び出された場合は、ERROR_INVALID_ACCESS をエラーコードとしてセットして、NULL を返すことが分かります。

ところで、シリアルコンソールにバックスペース(’\b’)が入力された場合、エコーバック処理では、入力を一文字消さなければいけません。単純に ‘\b’ をエコーバックしただけでは、PC のターミナルソフトでは、カーソルが一文字戻るだけで、文字が消えないのではないかと思います。カーソルを戻すだけでなく、直前にあった文字を消したい場合には、’\b’ を単純にエコーバックする代わりに、”\b \b” という文字列を出力すればよいでしょう。つまり、カーソルを一文字分戻した後、空白文字を出力することにより、直前にあった文字を消し、再度 ‘\b’ を出力してカーソルを戻す、というわけです。

■シリアルコンソールを利用した管理機能の実現
上で述べた、シリアルコンソールのタイプ入力に対するエコーバック動作の二つの実現方策のうち、後者の方は、シリアルコンソールを使った管理機能を実装する際には、却って向いているかも知れません。前者の方策の場合、OS (WinCE/WEC) の起動完了直後にコマンドプロセッサを起動するように設定すれば、シリアルコンソールにコマンドプロセッサのプロンプトが出力されて、Linux などと同じ感覚で使うことが可能です。しかし、そのようにしてしまうと、全てのプログラムをコマンドプロセッサから実行できてしまいます。

シェルをカスタマイズして、特定の操作しかできないようにしたデバイスや、あるいは、ヘッドレスのデバイスの場合には、限られたコマンドだけをシリアルコンソールから実行できるように制限したり、また、ログイン動作を実装してセキュリティを確保する必要があるでしょう。そのような場合は、単純にコマンドプロセッサをシリアルコンソールへ割り当てるのではなく、コマンドプロセッサを呼び出す「ラッパー」/「ドライバ」プログラムを割り当てて、そのプログラムが、ログイン動作や、シリアルコンソールから実行可能なプログラムの名前だけを受け付けて実行する、という仕組みにするのが良いと思います。

2 comments 2012/03/15 koga


Categories

Links

Posts by Authors

Recent Posts

Calendar

2012年3月
« 2月   4月 »
 123
45678910
11121314151617
18192021222324
25262728293031

Posts by Month

Posts by Category

Meta