#C++ による AVR 組み込み開発について
C++ には組み込み開発の際に役立つ要素がたくさん有ります。最近私が使用している AVR マイコンの開発では C++ をかなりそれっぽく使うことが出来ます。ありがたいことです。
C言語による開発においては、「メイン関数には何を書くか」「モジュール設計はどうするべきか」「コールバックをどうやって行うか」等の作法が存在している感じがします。
C++ではどうなるでしょうか?
ここしばらく悩んで、こんなもんかなと思えるところに一旦辿り着いた気がします。まあ趣味の範囲で一人でうんうん言ってる程度なので、参考にして頂ける要素が一つでもあればといった感じです。プロの方には当たり前かもしれません?
ごく小規模な組み込み開発にしか応用できない部分もありますが、Better C としての C++ をちょっとだけ超えるのが目標です。
#悩んだ結果 まとめ
- クラスの包含関係でプログラムの階層構造を表現する
- ヘッダファイルの依存性を最小限にし、かつメモリ消費量を明らかにするためplacement new を利用する
- コールバックには std::function 相当のライブラリを用いる
#1. クラスの包含関係でプログラムの階層構造を表現する
何か具体的な組み込み機器をイメージして設計を行ってみます。光センサの信号を受けて、スピーカーから「ニャーン」という鳴き声を出す組み込み機器を想定しましょう。そこに書き込むプログラムを考えます。
全部 main 関数に書いても何とかなりそうな感じ(マイコンだって要らないかも)ですが、ちょっと丁寧に分割して考えてみます。
先ずは光センサの信号を扱う部分が有ります。LightSensorHandler
クラスが有って良さそうです。
そしてスピーカに適切な信号を送らねばなりません。SpeakerController
クラスも有ると便利そうです。
さらに、それらをまとめて目的の振る舞いをさせるための ExampleApplication
クラスが欲しくなります。
階層構造は**ExampleApplication
にLightSensorHandler
とSpeakerController
が所有されている、こんな感じになります。これに従って、ExampleApplication
** は次の様なクラスとしてみます。
class LightSensorHandler; // #include "LightSensorHandler.h" はせずに、前方宣言のみにする
class SpeakerController; // こちらも同様
class ExampleApplication
{
public:
ExampleApplication();
void run();
private:
LightSensorHandler *lightSensorHandler; // ポインタを所有する
SpeakerController *speakerController; // こちらも同様
};
上記の ExampleApplication.h
のようにクラスの前方宣言のみ行ってポインタを所有しておけば、他のクラスのヘッダを ExampleAPplication.h
内で #include
する必要が無くなるので、色々スマートに出来ます。
ExampleApplication.h で他のクラスのヘッダを #include すると…
また ExampleApplication.h
をインクルードしただけで、下の階層のクラスも全て #include
されてしまうと、全ての内部的なクラスが見える事になります。これでは後でこれらのクラスを再利用する際に「どうやって使うんだっけ、見えるものは使っていいんだよね?」となって、最初に意図した階層構造が忘れ去られてしまうかもしれません。クラスを正しく使ってもらうための努力は最大限しておきたいです。
#2. ヘッダファイルの依存性を最小限にし、かつメモリ消費を明らかにするためplacement new を利用する
上のコードは幾つか疑問に思われるだろうポイントが有ります。**ポインタだけ所有してどうするのか?**ですとか、**組み込み系で new 演算子を使って良いのか?**といった部分が気になります。
先ず、ポインタの指し示す先にきちんと確保されたメモリ領域と初期化されたインスタンスが必要です。組み込みでない C++ プログラムでは new 演算子を使用するのが自然になりますが、組み込み系だと動的メモリ確保は大変に嫌がられる様です(初期化時に一回ずつだけ new 演算子が呼び出されるのは許容され易いかもしれません?)。
new演算子を使うにしても、AVR 開発環境で利用できるSRAM使用量表示機能では、動的に確保されるSRAM使用量は(スタック変数もですが)表示されないのが気になります。
↓Atmel Studio でコンパイルするときに表示されるこれです。
Program Memory Usage : 19304 bytes 58.9 % Full
Data Memory Usage : 1881 bytes 91.8 % Full
この数字だとギリギリですね…。グローバル変数・スタティック変数としてとして確保するものは Data Memory Usage (動的確保やスタック変数を含まない SRAM 使用量) としてきちんとカウントされ、どのくらい余裕が有るかを見積もることが出来ます。
動的確保する分はどうなっているのかこの数字では分からず、自分で計算しなければなりません。
ExampleApplication
のコンストラクタで、「静的にに確保しておいたメモリの中に、所有するクラスをインスタンス化する」ことが出来ればだいぶ便利になります。そこで、placement new と呼ばれるちょっと変わった new 演算子の出番です。
##グローバル変数としてメモリ領域を確保して、placemenet new を使う
ExampleApplication.cpp
の概要を示します。所有するクラスのサイズに合わせたメモリ領域をグローバル変数として確保しています。
new(g_lightSensorHandlerBuffer) LightSensorHandler
が placement new を使用している部分です。()
内でインスタンスを配置したい領域の先頭のポインタを指定しています。また無名名前空間内に配置することで、外部から参照されるのを防いでいます(これをしないと、外部で extern char g_lightSensorHandlerBuffer[...]
とされてアクセス出来るので…)。
#include "ExampleApplication.h"
#include <stdint-gcc.h>
#include "nonstd.h"
#include "LightSensorHandler.h"
#include "SpeakerController.h"
namespace {
char g_lightSensorHandlerBuffer[sizeof(LightSensorHandler)];
char g_speakerControllerBuffer[sizeof(SpeakerController)];
int someFunction(int x)
{ /*単位変換など、メンバ変数にアクセスする必要の無いちょっとした関数をココに書くのもアリ*/ }
}
ExampleApplication::ExampleApplication() :
lightSensorHandler(new(g_lightSensorHandlerBuffer) LightSensorHandler),
speakerController(new(g_speakerController) SpeakerController)
{
//コールバック関係の登録:後述します
lightSensorHandler->onBrightened = [this] { speakerController->meow(); };
}
#3. コールバックには std::function 相当のライブラリとラムダ式を用いる
std::function は、関数ポインタに類するものを保存できます。AVR C++でのコールバック関数にこれに準ずるものを使用出来ると、色々柔軟に出来そうです。
[](なぜ「準ずる」なのかというと…
AVR 開発環境で使用できるC++機能には幾つか制限が有ります。
その一つが、C++の強力なライブラリであるSTLをPCの環境の様に使用することが出来ない点です。
そのため、std::function もまたそのまま使用することは困難です。
割込み処理からの各クラスの処理の流れは、次の図の様にしたいと思います。
次の様な書き方をすれば、図の通りの処理の流れを実現できます。
#include <avr/interrupt.h>
ISR(PCINT0_vect)
{
LightSensorHandler::onPCINT0Interrupt();
}
#include "nonstd.h"
class LightSensorHandler
{
public:
LightSensorHandler();
nonstd::function<void()> onBrightened;
static nonstd::function<void()> onPCINT1Interrupt;
};
#include "LightSensorHandler.h"
nonstd::function<void()> LightSensorHandler::onPCINT1Interrupt;
LightSensorHandler::LightSensorHandler()
{
onPCINT1Interrpt = [this] { onBrightened(); };
}
[](```C++:ExampleApplication.cpp
ExampleApplication::ExampleApplication() :
`nonstd::function` は、[こちら](https://github.com/winterscar/functional-avr) のライブラリを Arduino.h を使わない AVR 向けに少し修正して使いました。`std::function` 相当の機能を持っています。`this` をキャプチャしたラムダ式を保存することで、特定のインスタンスのメンバ関数を割込みハンドラから呼び出すことがスムーズに行えます。
[](<font color=dimgray><details><summary>nonstd::function のメモリ管理について</summary>
`nonstd::function` が確保するメモリ領域の大きさにはある定数値が設定されています。
それ以上の領域が必要なものを格納しようとすると、コンパイル時にエラーになります。
`nonstd.h` の末尾の方に領域サイズを設定する部分が有るので、メモリを節約したい場合にはコンパイルが通る限りで小さい値に設定するのが良いと思います。
</details></font>)
以上で、前の図に示した様な割込み呼び出しが可能になりました!
#4. 欠点 と まとめ
個人的にはこれで結構満足なのですが、割込みハンドラ `ISR(PCINT0_vect)` が呼ばれてから `meow()` 関数が呼ばれるまでの間に、何度も関数呼び出しを経由しています。処理速度が求められる場合にはこの書き方は厳しいかもしれません。
とはいえ、当初の目的であった**Better C としての C++ をちょっとだけ超える**のは達成できている気がします。C++好きな組み込みプログラマの方は是非、自分流の納得できる書き方を紹介して下さい!
#参考文献(C言語での組み込みソフトウェア設計の本)
<a href="https://www.shoeisha.co.jp/book/detail/9784798147611">組込みソフトウェア開発のための構造化プログラミング(SESSAMEWG2)|翔泳社の本</a>