EtherCATマスターSOEMをマイコンに移植する

この記事は EtherCATについて語る Advent Calendar 2019 の8日目です。

昨日は@nonNoiseさんの EtherCAT開発方法 概要編 でした。

今日はオープンソースのEtherCATマスターであるSOEMをOS無しのマイコンに移植する上でのポイントをまとめます。(本件の位置づけは上記の記事を読むと分かりやすいです。)

SOEMとは?

SOEMはオープンソースのEtherCATマスターである。WindowsMacOSLinuxに対応するほか、いくつかのRTOSにも対応しているっぽい。ビルドシステムとしてCMakeを用いる。

github.com

移植対象のマイコン

f:id:licheng:20191207215436p:plain:w640

今回は、Arduino Due + Ethernet Shield2を移植対象とする。SOEMはビルドシステムにCMakeを用いているが、マイコン用のクロスビルド環境の構築が面倒なので今回はCMakeは使わないことにする。すでに移植したソースを以下に示す。(注意:あくまで実験目的のものである。)

github.com

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関数: 優先度の高いスレッドを生成する
(2.3) デバッグ出力

デバッグ出力のための EC_PRINT関数を実装する。マイコンシステムの場合、デバッグ用のUART(シリアルポート)に出力するのが一般的である。
EC_PRINT関数はマクロで定義されており、EC_DEBUGマクロが定義されているときのみ有効となる。EC_PRINT関数の実体はprintf関数と同等の可変長引数を取る関数として実装する。printf関数が使える環境であれば #define EC_PRINT printf とすればよい。

(3) oshwの移植

(3.1) エンディアン (バイトオーダ)

Ethernetプロトコルはビッグエンディアンだが、CPUはビッグエンディアンのものもあればリトルエンディアンのものもある。そこでバイトオーダの違いを吸収するため以下の関数を実装する。

  • oshw_htons関数: 16ビット整数をCPUのバイトオーダからビッグエンディアンに変換する。
  • oshw_ntohs関数: 16ビット整数をビッグエンディアンからCPUのバイトオーダに変換する。
(3.2) ネットワークインターフェースの検索

利用可能なネットワークインターフェースを検索する下記の関数を実装する。ただし、マイコンシステムではネットワークインターフェースは既知のものに固定されることがほとんどであり、アプリケーション側でこの機能が不要なら未実装でも問題ない。

  • oshw_find_adapters関数: 利用可能なネットワークインターフェースを列挙する。
  • oshw_free_adapters関数: ネットワークインターフェースの列挙に使用したメモリを解放する。
(3.3) NICデバイスドライバ (ここが一番の難所!)

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) ソケットのハンドル

WindowsMacLinuxなどの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で基本的な動作が確認できる。

デモ

最後に、今回取り上げたArduino Due版SOEMをマスターとするロボットのデモを下記に示す。

github.com