この記事は EtherCATについて語る Advent Calendar 2019 の8日目です。
昨日は@nonNoiseさんの EtherCAT開発方法 概要編 でした。
今日はオープンソースのEtherCATマスターであるSOEMをOS無しのマイコンに移植する上でのポイントをまとめます。(本件の位置づけは上記の記事を読むと分かりやすいです。)
SOEMとは?
SOEMはオープンソースのEtherCATマスターである。Windows、MacOS、Linuxに対応するほか、いくつかのRTOSにも対応しているっぽい。ビルドシステムとしてCMakeを用いる。
移植対象のマイコン
今回は、Arduino Due + Ethernet Shield2を移植対象とする。SOEMはビルドシステムにCMakeを用いているが、マイコン用のクロスビルド環境の構築が面倒なので今回はCMakeは使わないことにする。すでに移植したソースを以下に示す。(注意:あくまで実験目的のものである。)
SOEMのソースのディレクトリ構造
- soem: SOEM本体のソース
- osal: OS抽象化層のソース。タイマやスレッドなどOSが提供する機能の抽象化。
- oshw: ハードウェア抽象化層のソース。エンディアン変換やNICデバイスの抽象化。
- test: アプリケーションのサンプルコード。
各々について移植のポイントを以下にまとめる。
(1) soemの移植
(1.1) メモリリソースの節約
オリジナルのSOEMはワンチップマイコンで動作させるにはRAM使用量が大きすぎる。そのためターゲットのメモリ容量に合わせて以下の定数を小さくしてRAM使用量を減らす。
- EC_MAXODLIST: CoEのオブジェクトディクショナリの数の上限 (※)
- EC_MAXOELIST: CoEのオブジェクトエントリーの数の上限 (※)
- EC_MAXSLAVE: スレーブの最大接続数
- EC_MAXBUF: イーサネットフレームバッファの数
※ CoE = CANopen over EtherCAT プロトコル
(1.2) 時計
オリジナルのSOEMは時刻を使用する。ただし、ワンチップマイコンのシステムではリアルタイムクロックを持たないことや持っていても十分な精度を持たないこともある。今回はマイコン起動時からの経過時間をタイマでカウントして代用することにした。(たぶんこれによって何か不都合があると思われるが未調査。)
具体的には、ecx_configdc関数の処理を適宜修正する。
(1.3) LANポートの冗長構成の省略
オリジナルのSOEMは冗長構成のために2個のLANポートに対応している。しかし1個のLANポートしかないシステムの場合、無駄にRAMを占有されてしまう。そこでLANポートの冗長構成に関係する以下の変数と関数を削除する。
- ecx_redportt構造体
- ecx_init_redundant関数
- ec_init_redundant関数
(2) osalの移植
(2.1) タイマと時計
タイマを抽象化する下記の関数をマイコンに合わせて実装する。
- osal_usleep関数: usec単位で指定した時間だけ処理を止める。
- osal_timer_start関数: usec単位でタイムアウト時間を指定してタイマを開始する。
- osal_timer_is_expired関数: タイマがタイムアウトしたかどうかを返す。
また、時計を抽象化する下記の関数も実装する。ただし(1.2)で述べたように、今回はマイコン起動時からの経過時間で代用することにする。
- osal_current_time関数: 現在の時刻を取得する。
- osal_time_diff関数: 二つの時刻の差を返す。
(2.2) スレッド
タイマを抽象化する下記の関数をマイコンに合わせて実装する。ただし、SOEM自身はスレッド機能を使用していない。アプリケーション側でスレッドを使用しないなら未実装でも問題ない。
- osal_thread_create関数: スレッドを生成する
- osal_thread_create_rt関数: 優先度の高いスレッドを生成する
(3) oshwの移植
(3.1) エンディアン (バイトオーダ)
Ethernetプロトコルはビッグエンディアンだが、CPUはビッグエンディアンのものもあればリトルエンディアンのものもある。そこでバイトオーダの違いを吸収するため以下の関数を実装する。
(3.2) ネットワークインターフェースの検索
利用可能なネットワークインターフェースを検索する下記の関数を実装する。ただし、マイコンシステムではネットワークインターフェースは既知のものに固定されることがほとんどであり、アプリケーション側でこの機能が不要なら未実装でも問題ない。
- oshw_find_adapters関数: 利用可能なネットワークインターフェースを列挙する。
- oshw_free_adapters関数: ネットワークインターフェースの列挙に使用したメモリを解放する。
(3.3) NICデバイスドライバ (ここが一番の難所!)
単一のNICデバイスに対応 | 複数のNICデバイスに対応 | 機能 |
---|---|---|
ec_setupnic | ecx_setupnic | NICデバイスを初期化する |
ec_closenic | ecx_closenic | NICデバイスを閉じる |
ec_setbufstat | ecx_setbufstat | 受信バッファの状態を設定する |
ec_getindex | ecx_getindex | 新しいフレームバッファを取得する |
ec_outframe ec_outframe_red |
ecx_outframe ecx_outframe_red |
フレームを送信する (ノンブロッキング) |
ec_waitinframe | ecx_waitinframe | フレームを受信する (ブロッキング) |
ec_srconfirm | ecx_srconfirm | フレームを送受信する (ブロッキング) |
ec_setupheader | 同左 | Ethernetヘッダを生成する |
ここが一番の難所であるが、以下に要点をまとめる。
(3.3.1) クリティカルセクション
クリティカルセクションを保護するため、OS環境であればミューテックス等を用いる。OS無しのマイコンシステムでも割り込みを使用するなら割り込み禁止/許可の処理が必要である。
(3.3.2) 冗長構成の省略
(1.3)で述べたように、LANポートの冗長構成を省略するのであれば、2個目のLANポートに関係する変数redportとredstateを削除し、これらの変数が使われている冗長構成の処理も削除する。
(3.3.3) ソケットのハンドル
WindowsやMac、LinuxなどのOS環境ではネットワーク通信はソケットを基本としており、生のイーサネットフレームの送受信にも「RAWソケット」を使用する。これをハンドルする変数がsockhandleである。しかし、マイコンシステムでとくにソケットをハンドルする必要がなければこの変数は使わなくてよい。
(3.3.4) Ethernetコントローラの制御処理の実装
ここが最も重要な部分である。Ethernetコントローラの初期化処理、終了処理、フレーム送信処理、フレーム受信処理を移植対象のハードウェアに応じて実装し、必要な箇所から呼び出す。
処理の内容 | 呼び出し箇所(nicdrv.c) | 今回の実装例(SOEM.cpp) |
---|---|---|
初期化処理 | ecx_setupnic関数 | hal_ethernet_open関数 |
終了処理 | ecx_closenic関数 | hal_ethernet_close関数 |
フレーム送信処理 | ecx_outframe関数 | hal_ethernet_send関数 |
フレーム受信処理 | ecx_recvpkt関数 | hal_ethernet_recv関数 |
例えば、今回の例ではEthernetコントローラはEthernet Shield2基板上のW5500を用いるので、初期化処理は下記のようになる。
#include <Ethernet2.h> #include <utility/w5500.h> int hal_ethernet_open(void) { w5500.init(); w5500.writeSnMR(sock, SnMR::MACRAW); w5500.execCmdSn(sock, Sock_OPEN); return 0; }
(4) testの移植
アプリケーションのサンプルコードなので、適宜マイコンシステムの環境に合わせて移植して動作を確認する。特にslaveinfoとsimple_testで基本的な動作が確認できる。