設計情報

MWX ライブラリ内で用いる C++ 言語について、その仕様、制限事項、本書記載留意事項、設計メモを記載します。

このページはライブラリの動作の理解や改造などのためライブラリソースコードを参照する場面を想定しています。ライブラリを利用するのと比べより高度な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 namespacemwx::L2の名前空間に配置されています。using namespace mwx::L2;を指定することで名前空間名の指定なしで利用できるようになります。

また、いくつかのクラス名はusing指定をしています。

MWXライブラリ内で利用するstd::make_pairusing指定しています。

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_intrfaceBase<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 型から元のDerived1Derived2への変換を行うことも、強制的なキャストにより可能ですが、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 *この上述のメンバー関数を呼び出しています。(*_paxis_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)という最適化により、writerwrtに直接生成されるためコピーは発生せず、コンストラクタで実行されているバスの初期化を多重に行ったりすることはありません。ただしRVOはC++の仕様では保証されておらず、念のためMWXライブラリ中ではコピー、代入演算子の削除、moveコンストラクタを定義しています(moveコンストラクタが評価される可能性はないと考えられますが)。

if節の中の wrtは、まずコンストラクタにより初期化され同時に通信開始します。通信開始でエラーがなければ、条件判定時のbool演算子がtrueを返し、if節スコープ内の処理が行われます。スコープを脱出するとデストラクタにより、2線シリアルバスの利用終了処理を行います。通信の相手先がない場合は falseが戻り、wrtオブジェクトは破棄されます。

Wire, SPI特有の定義としてoperator << (int)の定義をオーバーライドしています。ストリームのデフォルトの振る舞いは、数値を文字列に変換して出力するのですが、WireやSPIで数値文字列をバスに書き込むことは稀で、反対に設定値など数値型のリテラルをそのまま入力したいことが多いのですが、数値型リテラルは多くの場合int型として評価されるため、この振舞を変更します。

ここではint型の値については8bitに切り詰めて、その値を出力しています。

最終更新