イーサネット機能をPIC32マイコンに追加する

令和 3年 3月

    W5500ドライバをコルーチン化(非プリエンプティブ動作環境)に変更する

 これまでの動作検証により、W5500によるEthernet接続動作が確認できました。
ただ、このままでは私の環境には使用できません。標準仕様のW5500ドライバは動作環境としてシングルタスクかプリエンプティブ動作するマルチタスクOSが必要です。組み込み装置でシングルタスクは現実的ではないので何らかのマルチタスク環境が前提になります。
私は自作の簡易OSの元でW5500を動作させます。この簡易OSではマルチタスクの実現方法としてコルーチン(非プリエンプティブ方式)のみを使用します。
WindowsやLinuxが採用しているプリエンプティブ方式は採用していません。このため、プログラムの変更が必要になります。

    マルチタスクの実現方法について
プリエンプティブ方式を採用しない理由は幾つかあるのですが、プリエンプティブ方式はワンチップマイコンが内蔵できるRAM容量に対して余裕がないことが多いのです。
プリエンプティブ方式ではタスクが使用するスタックメモリ領域を各タスク毎に用意する必要があります。
32bitマイコンを前提にした場合、タスク毎に必要なスタック量は少なく切り詰める努力をしても1kB程度は必要でしょう。
MIPSを例に取ると割り込みによるレジスタの退避領域だけでも256byte以上が必要です。これにタスク内で使用する一時変数の使用量を加算すると1kBというのは非常に心許ない数値です。
タスクの必要数を2桁とした場合には、マルチタスクを実現するためには120kB以上のRAMが必要です。
 対して、非プリエンプティブ方式ではスタック領域は全てのタスクで共有するため、それ程の容量は必要ありません。
この点がワンチップマイコンを多用する組み込み装置で非プリエンプティブ方式を採用するメリットになります。
反面、非プリエンプティブ方式のデメリットは、
 ・タスクの切り替えをプログラム内で明示的に記述する必要がある。
 ・WindowsやLinuxなどの汎用OSからプログラムを流用する場合には、上記のタスク切り替え部分を書き直しする必要が出てくる。


 1.標準ドライバのオプション機能について検証する

 標準のW5500ドライバのHelpを参照すると非プリエンプティブ環境でも使えそうなオプションがあります。
socket()関数のoption SF_IO_NONBLOCKがそれです。Helpを参考にするとnonblock io modeと書かれています。
ネットでnonblock io modeについて調べてみると、send()やrecv()などの関数内でIO動作が終了するまで関数内に留まるのがblock io modeだそうで、今回はnonが付いているので留まらないことになります。
 しかし、ソースコードを眺めてみると,あまり実用的ではないことが分かります。
このoptionを有効にした場合、例えばconnect()の動作は接続要求を送信した段階で関数を抜けて終了します。
このoptionが無効であった場合はその後、接続が確定するかタイムアウトを起こすまで関数内に留まります。つまり、結果画確定するまで待っています。nonblock io modeでは別途確認処理の追加が必要になります。これでは結果としてプログラムの書き換えが必要で、あまり意味がありません。
他の関数についても同様で処理を実行開始した段階で関数を抜けてしまい結果を確認していません。
これでは結局、書き直した方が良いと思われることから、このoptionの使用は諦めます。


 2.標準ドライバの仕様について詳しく検証する

 書き直しはコルーチン化が目的ですので、プログラムの流れを追ってICチップ内での処理に時間のかかるところや通信が行われる箇所を見つけ、そこで一旦処理を中断するコードを追加します。
この作業の中で幾つか気になった点や注意すべき点について記録しておきます。

 ・send関数の処理手順
 send関数は直感とは異なる手順でデータ処理を行っています。
   1. 最初に過去にデータを送信したかを調べ、送信していればその結果を確認し、エラーであればエラーを返す。
   2. 先の送信がOKであった場合、今回の送信要求処理を実行し、処理開始後に送信処理を行ったバイト数を返す。

 これには幾つかの注意点があります。
最初は、エラーか返ってくるのは実際に送信要求したタイミングではなく、その次の送信要求時であること。これは組み込み装置を前提にした場合には、うれしくない処理です。送信終了まで待って結果を返すように変更した方が良い。
 次は、送信要求したバイト数が必ず処理終了しているとは限らず、途中バイト数まででも処理を終了して返ってくる。これも送信のパケットが複数に分かれても、全てのデータを処理終了後に結果を返す方が望ましいと考えます。当然ですが、途中でエラーが起きたときは、その時点でエラーを返して終了します。

 ただ、何故このような仕様にしているかについては考えておいた方が良いので、もう少し検討を続けます。
これらの仕様はSocket IFが考案されたUnixでの実装を元に作られていると思われます。Unixシステムでは、データの入出力はストリームと呼ばれるパスを通して扱われます。ストリームには大量のバッファが用意され、send関数自体はこのバッファにデータを一時的に記録します。そしてバッファ内のデータ量が一定量以上になるか一定以上の時間が経過すれば実際の送信を行います。この仕様では、データをバッファに記録する処理とそのデータを実際に送信した結果を確認するまでには時間的なズレがあるため、このような確認方法が取られていると考えることが出来る。
 次の仕様も、やはりPCのストリームが前提と考えます。データバッファ内にデータの一部のみがストアされた状態でも、無理なく処理を継続しやすいことから、このような仕様としたと考えれば納得できる。

 対して、W5500ではバッファ容量は標準で2kB程で、送信データ量も数十B程度が多数と考えられます。
コルーチン化によって送信処理の終了を待っても時間のロスは殆ど無いので、この点は変更して実装します。
2番目のデータが複数のパケットに分割され一部のみが送信されるケースはW5500ではレアケースとなります。基本的にsend関数の呼び出し毎に送信を行う、つまりパケット送信までを行います。たとえデータが数バイトであってもです。
このためパケットが分割されるのは、送信要求データ量がその時点でのW5500のバッファ容量を上回る程大きい場合に限定されます。
このようなケースでもsend関数内で複数回のパケット送信処理を行ない完全にデータ送信が終了するまで待つようにします。

 ・sendto関数
 sendto関数は、送信が終了するまで関数内に留まっている。これはsocket()関数のoption SF_IO_NONBLOCKの値によらない。
sendto関数でも宛先IPアドレスが存在しないケースではTimeoutを起こすため、非常に長い時間関数内に留まることがあり得る。
W5500の標準設定値ではTimeoutまで1.6秒間(TimeOut 200ms x Retry 8回)掛かり、この時間を一つのタスクが占有するのはどうかと思う。よって、送信開始後一旦処理を中断するようにプログラムを変更する。


 以下の関数の実行時間を測定してみた。これらの関数内にはW5500のコマンドを実行する処理が含まれており、チップ内部での処理に時間を要する可能性がある。実行時間にはSPIの通信時間が含まれるのでSPIの通信速度によって少しの差が出てくることには注意が必要。今回の実測ではPIC32MM CPUを48MHzで、SPI通信は24MHzで動作させた。

 ・socket関数
 W5500のStatusがClosedの状態で約570us。socket関数は内部にclose関数を含むため比較的長い時間が掛かるが、この程度なら処理を分割しなくても良い。

 ・close関数
 close関数は関数を実行する時のW5500のStatus値によって処理時間が変わる可能性がありそうだと思い、StatusがCLOSED, INIT, LISTENの各状態中の実行時間を計ってみたが全て約196usであった。

 ・listen関数
  W5500のStatusがINITのときlisten()関数の実行時間は約240us。これも分割は行わない。


ドライバをコルーチン化することで、アプリケーションもそれにあわせて書き直しが必要になります。とはいえ、W5500ドライバに含まれるアプリケーションプログラムをコルーチン化する程度なら、左程負担は大きくない。むしろ、これだけの少ない投資で数千行を超えるTCP/IPスタックのプログラムをブラックボックスとして扱えるのはW5500のメリットです。

 これらの変更を行ない、動作検証までを終わらせました。


 前へ  次へ