設計情報
MWX ライブラリ内で用いる C++ 言語について、その仕様、制限事項、本書記載留意事項、設計メモを記載します。
MWXライブラリでアプリケーション記述する場合は、このページを読み飛ばしても差し支えありません。
このページはライブラリの動作の理解や改造などのためライブラリソースコードを参照する場面を想定しています。ライブラリを利用するのと比べより高度なC++言語に関する知識を前提となります。
設計方針について
アプリケーションのループ記述では、一般によく用いられる API 体系に近い記述を出来ることを目的とするが、TWELITEの特性に合わせた実装とする。
TWENET はイベントドリブンによるコード記述であり、これを扱えるようにクラス化を行う。上記クラス化によりアプリケーションのふるまいをカプセル化できるようにする。
イベントドリブンとループの記述は共存できる形とする。
代表的なペリフェラルはクラス化して手続きを簡素化する。可能な限りループ記述でアクセスできるようにする。
当社で販売する MONOSTICK/PAL といったボードを利用する手続きをクラス化し手続きを簡素化する。(例えば外部のウォッチドッグタイマーの利用を自動化する)
アプリケーションクラスやボードクラスは、ポリモーフィズムの考え方を導入し、統一した手続きによって利用できるようにする。(例えば、いくつかの振る舞いをするアプリケーションクラスを始動時にロードするような場合、また TWENET C ライブラリの接続部のコードを都度定義しなくてよいようにするため)。
C++の機能については、特に制限を設けず利用する。例えば、無線パケットを取り扱うにあたり煩雑なパケット構築、分解といった代表的な手続きを簡略化する手段を提供する。
演算子
->
を極力使用しないようにし、原則として参照型による API とする。
限られた時間で実装を進めているため、細かい点まで網羅した設計ではありませんが、設計・実装等でお気づきの点がありましたら、当社サポートにご連絡ください。
C++ コンパイラについて
バージョン
gcc version 4.7.4
C++言語規格
C++11 (コンパイラ対応状況は一般の情報を参考にしてください)
C++ の制限事項
※ 当社で把握しているものについての記載です。
new, new[]
演算子でのメモリ確保は行えますが、確保したメモリを破棄することはできません。C++ライブラリで動的メモリ確保をするものは殆どが事実上利用不可能です。一度だけ生成してそれ以降破棄しないオブジェクトに使用しています。グローバルオブジェクトのコンストラクタが呼び出されません。 参考:必要な場合は、初期化関数(
setup()
) でnew ((void*)&obj_global) class_foo();
のように初期化することでコンストラクタの呼び出しを含めた初期化を行えます。例外
exception
が使用できません。仮想関数
virtual
が使用できません。
設計メモ
本節ではMWXライブラリのコードを参照する際に理解の助けとなる情報を記載します。
現状の実装
限られた時間で実装を進めているため、詳細部分の整備が十分でない場合があります。例えば const に対する考慮は多くのクラスで十分なされていません。
名前空間
名前空間について、以下の方針としています。
定義は原則として共通の名前空間
mwx
に配置する。名前空間の識別子なしで利用できるようにしたいが、一部の定義は識別子を必須としたい。
クラス名については比較的長い命名とし、ユーザが利用するものは別名定義とする。
クラス・関数・定数は一部の例外を除きmwx
名(正確にはinline namespace L1
で囲んだmwx::L1
)の名前空間内に定義しています。inline namespace
を指定しているのは、mwx::
の指定を必須とする定義と、必須としない定義を共存させるためです。
殆どの定義はusing namespace
により名前空間名を指定しなくても良いようになっています。これらの指定はライブラリ内のusing_mwx_def.hpp
で行っています。
例外的に比較的短い名前についてはmwx::crlf, mwx::flush
のように指定します。これらはinline namespace
のmwx::L2
の名前空間に配置されています。using namespace mwx::L2;
を指定することで名前空間名の指定なしで利用できるようになります。
また、いくつかのクラス名はusing
指定をしています。
MWXライブラリ内で利用するstd::make_pair
をusing
指定しています。
CRTP(奇妙に再帰したテンプレートパターン)
仮想関数 (virtual), 実行時型情報(RTTI) が利用できない、かつ利用できるようにしたとしても、パフォーマンス面で難があるため、これに代わる設計手法として CRTP (Curiously recurring template pattern : 奇妙に再帰したテンプレートパターン)を用いています。CRTPは、継承元の親クラスから子クラスのメソッドを呼び出すためのテンプレートパターンです。
以下の例では Base
を継承した Derived
クラスに interface()
というインタフェースを実装する例です。Base
からはDerived::print()
メソッドを呼び出しています。
MWXライブラリで利用されている主要クラスは以下です。
イベント処理の基本部分
mwx::appdefs_crtp
ステートマシン
public mwx::processev_crtp
ストリーム
mwx::stream
CRTP での仮想化
CRTPクラスは、継承元のクラスはインスタンスごとに違います。このため、親クラスにキャストして、同じ仲間として取り扱うといったこともできませんし、仮想関数(virtual
)やRTTI(実行時型情報)を用いたような高度なポリモーフィズムも使うことが出来ません。
以下は上述のCRTPの例を、仮想関数で実装した例です。CRTPではBase* b[2]
のように同じ配列にインスタンスをまとめて管理することは、そのままではできません。
MWXライブラリでは、CRTP のクラスインスタンスを格納するための専用クラスを定義し、このクラスに同様のインタフェースを定義することで解決しています。以下にコード例を挙げます。
VBase
クラスのメンバ変数 p_inst
は、Base <T>
型のオブジェクトへのポインタを格納し、pf_intrface
は Base<T>::s_intrface
へのメンバ関数ポインタです。 Base<T>::s_intrface
は、自身のオブジェクトインスタンスを引数として渡され、T
型にstatic_cast
することでT::intrface
メソッドを呼び出します。
VBase
への格納は、ここでは =
演算子のオーバーロードによって実装しています(ソース例は後述)。
上記の例ではb[0].intrface()
の呼び出しを行う際には、VBase::pf_intrface
関数ポインタを参照しBase<Derived1>::s_intrface()
が呼び出されることになります。さらにDerived1::intrface()
の呼び出しを行うことになります。この部分はコンパイラによるinline展開が期待できます。
VBase
型から元のDerived1
やDerived2
への変換を行うことも、強制的なキャストにより可能ですが、void*
で格納されたポインタの型を直接知る方法はありません。完全に安全な方法はないものの、以下のようにクラスごとに一意のID(TYPE_ID
)を設けて、キャスト実行時(get()
メソッド)にIDのチェックを行うようにしています。違う型を指定して get()
メソッドを呼び出したときは、エラーメッセージを表示するといった対処になります。
Base<T>
型としてのポインタが格納されるとT
型に正しく変換できない可能性(T
が多重継承している場合など)あるため、<type_trails>
のis_base_of
によりBase<T>
型の派生であることをコンパイル時に static_assert
による判定を行っています。
new, new[] 演算子
TWELITEモジュールのマイコンには十分なメモリもなければ、高度なメモリ管理もありません。しかしマイコンのメモリマップの末尾からスタックエリアまでの領域はヒープ領域として、必要に応じて確保できる領域があります。以下にメモリマップの概要を図示します。APPがアプリケーションコードで確保されたRAM領域、HEAPはヒープ領域、STACKはスタック領域です。
たとえdelete
できなくてもnew
演算子が有用である場面も想定されます。そのため、以下のようにnew, new[]
演算子を定義しています。pvHear_Alloc()
は半導体ライブラリで提供されているメモリ確保の関数で、u32HeapStart, u32HeapEnd
も同様です。0xdeadbeef
はダミーアドレスです。beefがdeadなのは変だとかいう指摘はしないでください。
例外も使えないため失敗したときの対処はありません。また、メモリ容量を意識せず確保を続けた場合、スタック領域と干渉する可能性もあります。
システム(MAC層など)が確保するメモリは約2.5KBです。
コンテナクラス
MWXライブラリでは、マイコンのリソースが小さい点、メモリーの動的確保ができない点を考慮し標準ライブラリで提供されるコンテナクラスの利用はせず、シンプルなコンテナクラスを2種類定義しています。コンテナクラスにはイテレータやbegin(), end()
メソッドを定義しているため、範囲for文やSTLのアルゴリズムの一部を利用できます。
クラス名
概要
smplbuf
配列クラスで、最大領域 (capacity) と最大領域範囲内で都度サイズを指定できる利用領域(size)を管理します。
また本クラスは stream インタフェースを実装しているため、<< 演算子を用いてデータを書き込むことができます。
smplque
FIFOキューを実装しています。キューのサイズはテンプレートのパラメータで決定します。割り込み禁止を用いキューを操作するためのテンプレート引数もあります。
コンテナクラスのメモリについて
コンテナクラスではメモリの確保方法をtemplate
引数のパラメータとして指定します。
クラス名
内容
alloc_attach
すでに確保済みのバッファメモリを指定する。
Cライブラリ向けに確保したメモリ領域を管理したいとき、同じバッファ領域の分断領域として処理したい時などに使用します。
alloc_static
クラス内に静的配列として確保する。
事前にサイズが決まっていたり、一時利用の領域として使用します。
alloc_heap
ヒープ領域に確保する。 システムのヒープに確保後は破棄できませんが、初期化時にアプリケーションの設定などに従い領域を確保するといった使い方に向いています。
可変数引数
MWXライブラリでは、バイト列やビット列の操作、printf
相当の処理を行う処理に可変数引数を用いています。下記の例は指定のビット位置に1をセットする処理です。
この処理では template のパラメータパック (typename...
の部分) で、再帰的な処理を行い引数の展開を行っています。上記の例ではconstexpr
の指定があるため、コンパイル時に計算が行われマクロ定義やb2
のようなconst
値の指定と同等の結果になります。また変数を引数として動的に計算する関数としても振る舞うこともできます。
以下の例では、expand_bytes
関数により、受信パケットのデータ列からローカル変数に値を格納しています。パラメータパックを用いた場合各引数の型を把握できるため、下記のように、受信パケットのバイト列から、サイズや異なる型にあった値を格納することができます。
イテレータ
イテレータはポインタの抽象化で、例えばメモリの連続性のないようなデータ構造においても、あたかもポインタを使ったようにデータ構造にアクセスできる効果があります。
C++のSTLでは、begin()
メソッドで得られるコンテナの先頭を示すイテレータと、end()
メソッドで得られるコンテナの末尾の「次」を示すイテレータの組み合わせが良く用いられます。
コンテナの末尾の「次」をend()
としているのは、以下のような記述を想定しているためです。MWXライブラリでもこれに倣ってコンテナの実装を行っています。
イテレータを標準ライブラリの仕様に適合させることで、範囲for文が利用できたり、標準ライブラリのアルゴリズムを利用できるようになります。
(MWXライブラリではC++標準ライブラリに対する適合度や互換性についての検証は行っていません。動作確認の上利用ください)
以下の例では、通常のポインタでは連続的なアクセスができないFIFOキューのイテレータ、さらに、FIFOキューの構造体の特定メンバー(例ではX軸)のみを抽出するイテレータを利用する例です。
以下は smplque
クラスのイテレータの実装の抜粋です。このイテレータでは、キューオブジェクトの実体と、インデックスにより管理しています。キューのメモリが不連続になる(末尾の次は先頭を指す必要があるリングバッファ構造)部分はsmplque::operator []
で解決しています。オブジェクトのアドレスが一致することとインデックスが一致すればイテレータは同じものを指していることになります。
この実装部分には <iterator>
が要求する typedef なども含まれ、より多くのSTLのアルゴリズムが適用できるようになります。
構造体を格納したコンテナ中の、特定構造体メンバーだけアクセスするイテレータは少々煩雑です。構造体のメンバーにアクセスするメンバー関数を予め定義しておきます。このメンバー関数をパラメータ(R& (T::*get)()
)としたテンプレートを定義します。Iter
はコンテナクラスのイテレータ型です。
値にアクセスするoperator *
この上述のメンバー関数を呼び出しています。(*_p
はaxis_xyzt
構造体で、(*_p.*get)()
は、T::*get
に&axis_xyzt::get_x
を指定した場合_p->get_x()
を呼び出します)
_axis_xyzt_iter_gen
クラスはbegin(), end()
のみを実装し、上記のイテレータを生成します。これで範囲for文やアルゴリズムが利用できるようになります。
このクラス名は非常に長くなりソースコード中に記述するのは困難です。このクラスを生成するためのジェネレータ関数を用意します。下記の例では末尾の行の get_axis_x()
です。このジェネレータ関数を用いることで冒頭のようなauto&& vx = get_axis_x(que);
といった簡潔な記述になります。
また、この軸だけを抽出するイテレータは、配列型のsmplbuf
クラスでも同様に利用できます。
割り込み・イベント・状態ハンドラの実装
ユーザ定義クラスによりアプリケーション動作を記述するため、代表的なハンドラは必須メソッドとして定義が必要ですが、それ以外に多数ある割り込みハンドラ、イベントハンドラ、ステートマシンの状態ハンドラをすべて定義するのは煩雑です。ユーザが定義したものだけ定義され、それのみのコードが実行されるのが理想です。
MWXライブラリでは、数の多い DIO割り込みハンドラ(TWELITEハード上は単一の割り込みですが、利用しやすさのためDIO一つずつにハンドラを割り当てることにしました)などを、テンプレートによる空のハンドラーとして定義した上、ユーザ定義のメンバー関数をそのテンプレートの特殊化することにより定義する手法を採用しました。
実際のユーザ記述コードは、マクロ化やヘッダファイルのインクルードを行うことで、簡素化されていますが、上記は解説のために必要なコードを含めています。
TWENETからの割り込みハンドラからmy_app_def::cbTweNet_u8HwInt()
が呼び出されます。cppファイル中では、int_dio_handler<12>
のみが特殊化されて記載された内容でインスタンス化されます。12番以外はhppファイル中のテンプレートからインスタンス化されます。結局以下のように展開されることになります。
最終的に、コンパイラの最適化により12番以外のコードは無意味と判断されコード中から消えてしまうことが期待できます(ただし、上記のように最適化されることを保証するものではありません)。
つまりユーザコード上では12番の割り込み時の振る舞いを定義したいときはint_dio_handler<12>
を記述するだけで良い、ということになります(注:DIO割り込みを有効にするには attachInterrupt()
を呼び出す必要があります)。登録しないハンドラはコンパイル時の最適化により低コストな呼び出しで済むことが期待できます。
ユーザが関数を定義したときにこれを有効にし、定義しない場合は別の関数を呼び出す手法として、リンク時の解決方法があります。下記のように__attribute__((weak))
の指定します。ユーザコードで wakeup()
関数が定義された場合は、ユーザーコードを関数をリンクし、未定義の場合は中身が空の関数をリンクします。
上記ハンドラの実装においてはweak指定したメンバー変数を明示的に生成する必要があり、またinline化による最適化が行いにくいため使用しませんが、wakeup()
といったいくつかのTWENETからのコールバック関数の受け皿としてweak指定の関数を定義しています。
Streamクラス
ストリームクラスは、主にUART(シリアルポート)の入出力に用います。MWXライブラリでは、出力用の手続きを主に定義しています。一部入力用の定義もあります。
ここでは派生クラスが必要とする実装について解説します。
上記は1文字書き出すwrite()
メソッドの実装です。親クラスのstream<serial_jen>
からはキャストを実行するget_Drived()
メソッドを用いて、serial_jen::write()
メソッドにアクセスしています。
必要に応じて write(), read(), flush(), available()
といったメソッドを定義します。
書式出力にはMarco Paland氏によるprintfライブラリを利用しています。MWXライブラリから利用するための実装が必要になります。下記の例で派生クラスのserial_jen
で必要なことは1バイト出力のための vOutput()
メソッドを定義することと、vOutput()
がstaticメソッドであるため出力のための補助情報を親クラスのpvOutputContext
に保存することです。
get_pfcOutput()
により、派生クラスで定義したvOutput()
関数を指定し、そのパラメータとしてpvOutputContext
が渡されます。上記の例では<<
演算子がint型で呼び出されたときserial_jen::vOutput()
とUART用に設定済みのTWE_tsFILE*
をfctprintf()
関数に渡しています。
Wire, SPIのワーカーオブジェクト
Wire
クラスでは、2線デバイスとの送信・受信時に、通信開始から終了までを管理する必要があります。ワーカーオブジェクトを利用する記述について内容を記述します。
periph_twowire::writer
クラスの抜粋です。streamインタフェースを実装するために mwx::stream<writer>
を継承しています。steamインタフェースを利用するために write()
と vOutput()
メソッドの実装を行っています。
コンストラクタでは2線シリアルの通信開始を、デストラクタで通信終了のメソッドを呼び出しています。また、operator bool()
演算子では、2線シリアルのデバイスの通信開始に成功した場合 true
を返すようになっています。
get_writer()
メソッドによりオブジェクトwrt
を生成します。この時にオブジェクトのコピーは通常発生しません。C++コンパイラのRVO(Return Value Optimization)という最適化により、writer
はwrt
に直接生成されるためコピーは発生せず、コンストラクタで実行されているバスの初期化を多重に行ったりすることはありません。ただしRVOはC++の仕様では保証されておらず、念のためMWXライブラリ中ではコピー、代入演算子の削除、moveコンストラクタを定義しています(moveコンストラクタが評価される可能性はないと考えられますが)。
if節の中の wrt
は、まずコンストラクタにより初期化され同時に通信開始します。通信開始でエラーがなければ、条件判定時のbool演算子がtrue
を返し、if節スコープ内の処理が行われます。スコープを脱出するとデストラクタにより、2線シリアルバスの利用終了処理を行います。通信の相手先がない場合は false
が戻り、wrt
オブジェクトは破棄されます。
Wire, SPI特有の定義としてoperator << (int)
の定義をオーバーライドしています。ストリームのデフォルトの振る舞いは、数値を文字列に変換して出力するのですが、WireやSPIで数値文字列をバスに書き込むことは稀で、反対に設定値など数値型のリテラルをそのまま入力したいことが多いのですが、数値型リテラルは多くの場合int型として評価されるため、この振舞を変更します。
ここではint型の値については8bitに切り詰めて、その値を出力しています。
最終更新