WOBのソフトウェアを開発していて必要となった知見をまとめました。
Arduino IDEによるビルド生成物の格納場所の変更
preference.txtファイルを探す。Arduino IDEのメニューからファイル→環境設定の一番下あたりに表示されている。
IDEを終了し、preference.txtに以下の行を追加する。
build.path=build
イコールの後ろ側の名前はフォルダ名で、この場合は
(WOBインストールフォルダ)\arduino-1.0.1\build\
以下にビルド生成物が置かれる。このフォルダはあらかじめ用意しておかなくてはならない。
Cygwin環境下におけるコマンド実行
環境変数PATHに以下のパスを追加しておく。
(WOBインストールフォルダ)\arduino-1.0.1\hardware\tools\avr\bin
筆者は~/.bashrcで以下のように記述している。
PATH=$PATH:上記のパス export PATH
LSSファイルを得る
# avr-objdump -h -S file.elf > file.lss
MAPファイルを得る
# avr-ld -M file.elf > file.map
しかしこの方法は「(本当は入っているのに).textセクションに入りきらない」という謎のエラーを出す場合があることが分かった。単純にFlashとRAMのサイズを調べるだけなら
# avr-size -C file.elf
を使ったほうがよい。
new/delete演算子
AVR-GCCでは、C++のnew/delete演算子は使えない(!)ことが判明。
enum型のサイズ
enum型はその処理系のint型と同じサイズを占有する。これはAVR-GCCに限ったことではないらしい。AVR-GCCでは2バイトとなる。コンパイルオプションなどで制御できるものではない。8ビットに収まればいいのに!
例外処理(Exceptions)
Arduino IDEでは例外処理をサポートしないようにするスイッチ-fno-exceptionsを付けてコンパイルしているため例外処理は使えない。IDEを用いる以上、この手のコンパイルスイッチをいじる手立てはない。
スケッチファイル(.ino)以外での’Serial’などの使い方
main() の外、すなわちスケッチファイル以外の.cppファイルなどで定義されたクラス・関数の内部でSerial.print()などを使い たい場合がある。特にデバッグ中などである。Serial.print()とそのまま書くだけではスコープの範囲外となって使えない。そこでヘッダファイ ルなどに以下の追加をする。
#include "HardwareSerial.h" extern Serial;
これでSerial.print()などがそのまま使えるようになる。
C++エラー対処
- Undefined References to `vtable for __cxxabiv1::__class_type_info’
- これはリンク時に出力される。RTTI(Runtime type information)がないというエラー。Cソースをgccで、c++ソースをg++でコンパイルしていたため。g++で統一したら出なくなった。
- foo.cpp:37: warning: ‘Foo::bar’ will be initialized after
- メンバ変数の宣言順とコンストラクタ初期化子の順番が違うと怒られる
- 数分間だんまりの後 sig_send: wait for sig_complete event failed…
- 調査すると、デストラクタが呼ばれない場合がある。「参照」を返す関数をやめ、ポインタを返すようにしたところ解決。
- ISO C++ forbids declaration of ‘class_name’ with no type
- 「宣 言をしていないクラスを使うのは禁止」という意味のエラー。宣言のあるヘッダファイルの内容が正しく認識されていないために起こった。ヘッダ ファイル先頭のインクルードガードに使うマクロ名がどこか(C++内部かArduino内部か)で衝突していたらしい。今回の場合#ifndef SREG_Hとして使っていたSREG_Hがそうだった。これを#ifndef ___SREG_Hとした(アンダスコア3つを前につけた)ところ解決。非常に分かりにくかった。
- (警告)only initialized variables can be placed into program memory area
- C++においてPROGMEMを使ってフラッシュに文字列など定数を格納した時に出る。avr-g++コンパイラのバグらしい。一応回避方法はここでの議論中に書いてあるが、Arduinoのライブラリ内での警告には対処できない。警告は出ても動作に支障はないので放置することに。
- ‘parent-class’ is not an accessible base of ‘sub-class’
- サブクラスの宣言で、親クラスを継承するときに’public’を入れ忘れた。 class sub_class : public parent_class { … };
C++参照型変数の初期化
Memory クラスの仕様定義のため、MemoryConfクラスを参照型メンバ変数として持たせたが、これの初期化が分からなくてはまった。コン ストラクタMemory()内で代入を行うとエラーとなる。いわゆる「メンバ変数の初期化」を行う必要があることがわかった。すなわち
class Memory { public: Memory(MemoryConf& memconf)) : conf(memconf) { ... }; ... protected: MemoryConf& conf; };
上記の「: conf(memconf)」の部分である。これでめでたくメンバ変数confはmemconfと同値の参照変数として扱うことができるようになった。ポインタを使う場合よりもバイナリは短く、ソースは読みやすくなる。
ROM化ジャンプテーブルは失敗
DeM のオペコードによる多方向分岐は通常はジャンプテーブルで効率化できる。実際そのようなコーディングで動作はしたのだが、ジャンプテーブルの 格納先がRAMになってしまうのが(RAMの節約という観点において)欠点だ。これをROM上に置いてみたのだがうまく動作しなかった。
RAM上ジャンプテーブル
ResultID (DeM::*DeM::opX[])(void) = { &DeM::_op0X, &DeM::_op1X, … }; // RAM上
に対する分岐
result = (this->*opX[_opch])();
上記はうまく動作する。しかしROM上ジャンプテーブルでは
ResultID (DeM::*DeM::opX[])(void) PROGMEM = { &DeM::_op0X, &DeM::_op1X, … }; // ROM上
に対する分岐
uint16_t funcaddr = pgm_read_word(&DeM::opX[_opch]); ResultID (*func)(void) = reinterpret_cast<ResultID (*)()>(funcaddr); result = (*func)();
だとジャンプはするのだが戻り値がおかしくなる。(当然コンパイルは通る) コンパイラのバグかもしれず、RAMの節約は他で行うことにしてあまり深く追求しないことにした。 ただ、コンパイラが吐いたアセンブラコードを見て面白いことが分かった。コンパイラはジャンプテーブルの1つの要素 (関数へのポインタ)を4バイトでコー ディングする。アドレスが16ビットしかないのにもかかわらずである。 AVRの上位機種では16ビットを超えるROMアドレスを持つものもあるためだろうか、ここの効率化をさぼっている 節がある。ディスアセンブルして調べてみると、テーブル引きするときはきちんとインデクスを4倍してアドレスオフセットを計算しているので、 前述の不具合はこのテーブル引きが悪いわけではなさそうに見えるのだが…。 もうひとつ。4バイト単位のジャンプテーブルを2バイトに縮めてコンパクトにしようとreinterpret_cast<int>して みたのだが、それはできんとコンパイラに怒られた。 その後改めてswitch-caseを使った分岐と比べてみたところ、速度が多少犠牲になるものの、そちらのほうがコードサイズの小ささとRAM使用量ゼロの観点からジャンプテーブル方式をあきらめるに至った。
その後調べたところ、GCCにおいてはswitch-caseのcaseの数が5つ以上(かつcaseの比較数値も連続)になると自動的にジャンプテーブル化をしてくれることが判明した。最適化技術はすごいと感心することしきりである。巨大なswitch-caseになってもこれで安心。(参考:ジャンプテーブルについて – Maeの(Mae向きな)日記)
inlineメンバ関数の継承はできない?
親クラスにprotectedなinline関数があり、それを継承した子クラスから利用しようとすると「未定義の関数」としてエラーになる。 inlineをはずすとコンパイルが通る。これは言語の仕様なのか、コンパイラのバグなのか?ネット上で検索してもそれに関連する問題は見つからなかっ た。
Emacs小技
- *.hファイルを開くとき自動的にc++モードにする
- ファイルの先頭に // -*- C++ -*- という記述を入れる。
- Windowsショートカットをシンボリックリンクとして扱う
- win32-symlinks.elをロードして (setq w32-symlinks-handle-shortcuts t) (require ‘w32-symlinks) を.emacs中で実行する。ただし、Cygwin上でのリンク作成には別の工夫が必要。
Cygwin小技
- Windowsショートカット型のシンボリックリンクを張る
- CYGWIN=winsymlinks ln -s Path Name