やーやーやー。なんとワタクシ、遂に脱8bitしました!ワーワー、ドンドンパチパチ!
さて、組み込みでもRustがアツかったり色々しますね。ただまぁ、Rustってサポートや車載対応等が無いので、政治的ごにょごにょでちょっと使いにくいですよね。
だから、今まで通りC言語………って決めてしまう前に、ちょっと待って、まだ選択肢有るじゃない。
そう、C++ですよ!
新しい言語とか無理
安心してください。C++は昭和の言語です(1983年)。昭和ですよ、ショーワ。世界大戦があった時代の言語です。今は令和ですよ。断言しますが、この記事書いてる現在、令和生まれの若い子って、昭和とかもう知らないですよ。古すぎるといって過言ではありません。
そうじゃない オブジェクト指向とか無理
安心してください。C++はオブジェクト指向言語ではありません。C++は 魔留血原太武言語 組み込み屋向けに嬉しい機能がいっぱいある言語です。
単に組み込みって言ってしまうと、かなり範囲が広いので一概には言えませんが、特にRTOSも使わない様な環境では、実はオブジェクト指向ってしっくりこないんです。staticを理解していない人のコードを見ると、いちいちインスタンス宣言しているので笑ってしまう。
オブジェクト指向なんて、メモリとポインタ使ってなんぼです。リソースも計算時間も無い系な組み込みには向いてません。オブジェクト指向なんて、組み込み屋が使えなくても問題ありません。newとか組み込みでは死語です。vector、string?使わねぇって。
組み込み屋が理解すべきは、C++の 魔境 templateとconstexprと型システムです。
え、潤沢に富豪プログラムができる環境だって?JavaとかPythonとか使えば良いんじゃないかな。
気分はtemplate constexpr!
C言語で、あるペリフェラルのレジスタのビットに書き込みたいとき、どう書くでしょうか?こう?
PIYO->CTRL = 0x10;
FOO->EN = 1 << 15 | 1 << 20 | 1 << 1;
あ、ちなみに、場合によりけりだけど、以下の様にビットフィールドを使うのは駄目です。
FOO->EN_b.EN15=1; FOO->EN_b.EN20=1; FOO->EN_b.EN1=1;
通常、ペリフェラルのレジスタはvolatileだから、上記のコードはread-modify-writeが3回走ります。一括で初期化したい場合には向いてないですね。
C++なら、こんな感じでかけます。
constexpr uint32_t bit(){return 0;}
template<typename... ARGS>
constexpr uint32_t bit(size_t a,ARGS... args){
return (1 << a) | bit(args...);
}
FOO->EN = bit(1,15,20);
1,15,20を引数とするbit関数の呼び出しになっていますが、C言語と違って、最適化ビルドをしなくても、この関数の計算をコンパイル時にさせてしまう機能がC++にはあります。この関数の実行時の計算コストは0で、C言語で直値で書いたのと同じです。恐ろしいですね。
constexprな変数を使ってみよう
constexprは定数式と言います。C言語では定数を#defineで名前を付けていました。C++では、これをconstexprで記述します。
#define FOO_MODE1_CONFIG 1 << 1 | 1 << 21
constexpr uint32_t FOO_MODE1_CONFIG = 1 << 1 | 1 << 21;
もはや理解が追いつかない!と言う程の大した差は無いですね。
組み込み屋にとって、一番の大きな違いは、マクロ特有のバグが入らないことです。
例えば、以下のコードは、#defineとconstexprでは結果が変わります。
FOO->CONFIG = FOO_MODE1_CONFIG + 0x1;
define文はあくまで文字列置換なので、「1 << 1 | 1 << 21 + 0x1」となり、結果的に「1 << 1 | 1 << 22」と同じになりますね。このバグを避ける為に、defineを書くときはヒステリックな程に()で式を囲います。
constexprでは特に()で囲ったりしていませんが、正しく「FOO_MODE1_CONFIGの値と、0x1の加算」となります。
constexprな変数?定数?
ところで、constexprを外すと、普通の変数宣言と同じになります。
constexpr uint32_t FOO_MODE1_CONFIG = 1 << 1 | 1 << 21;
uint32_t FOO_MODE1_CONFIG = 1 << 1 | 1 << 21;
ということは、どこかに変数用の領域が用意されていて、それを読み出しているのだろうと思われるかも知れません。
対象デバイスの命令セットに依存しますが、今回のFOO_MODE1_CONFIGは大きな値なので、命令の直値にはならないでしょう。多分、普通の定数と同じく、関数の後ろに値が配置されるかと思われます。FOO_MODE1_CONFIGが非常に大きな配列やら構造体なら、Flashのどこかに1箇所だけ置かれるかも知れません。
しかし、逆に、FOO_MODE1_CONFIGが1とか100みたいな小さな数であれば、命令の直値に変換されることでしょう。
constexprな変数は、書き換え不可能な変数であり、かつ、直値にもなり得えます。直値として埋め込まれ、誰も実体を必要としなければ、最終的なバイナリに残りません。
マクロと違って安全で、それでいて、マクロと同じく直値にもなり得る。こんな素晴らしい機能があるのに組み込みで使わないなんて信じられません。さぁ、C++を使いましょう。
constexprな関数を使ってみよう
constexprな変数FOO_MODE1_CONFIGには問題があります。
MODEが10個くらいあったらどうするというのだろうか。全部書くのか?
FOO_MODE A_X_1 CONFIGとか、プロパティ増えたらどーすんの!?
これを解決してくれるのがconstexprな関数です。
例えば、FOO_MODEx_CONFIGは「1 << x | 1 << 21」としよう。
constexpr uint32_t FOO_MODE_CONFIG(int x){ return 1 << x | 1 << 21; }
FOO->CONFIG = FOO_MODE_CONFIG(1) + 0x1;
関数なので、全部のモードについて定義をしなくても、勝手に値が求まります。
そして、C++コンパイラはconstexprな関数を定数だけで呼び出している場合、勝手に計算して、定数にしてくれます。
実行時には関数呼び出しが行われませんので、計算コストはタダです。
なお、実行時に値が決まる変数などで呼び出すと、実行時に関数を呼び出します。constexprな関数は、定数としても、関数としても使うことができます。
文法も単純で、ふつーの関数定義にconstexprがついているだけです。非常に簡単ですね。
と、言いたいのですが、constexprな関数は場合によってはちょっと面倒くさくなります。
例えば、を愚直に0+1+2+3....と計算するconstexprな関数はこう書きます。なお、愚直じゃない計算は ですね。
constexpr uint32_t sum(uint32_t x)
{
return
x == 0? 0 :
x == 1? 1 :
2 * (sum(x/2) + sum((x-1)/2)) + (x +1)/2;
}
ちょっと何書いてるのか意味が分からないですね。これ書いた私は、非常に頭が悪いですね。私にもさっぱり分かりません。
こんな頭の悪いクソコードを読み解く必要はなくて、重要なのは、return文しかないという所です。実はconstexprな関数には、return文しか書けませんでした。
でした、って述べた様に、この書き方はC++11っていうちょびっと古いバージョンで、凡骨向けの書き方です。
私の様なポンコツには想像も及ばない世界です。
でも大丈夫。C++14でだいぶポンコツフレンドリーになりました。C++14ではfor文もif文も変数宣言も解禁され、以下の様に書けます。
constexpr uint32_t sum(uint32_t x)
{
uint32_t s = 0;
for(uint32_t i =0 ; i < x; i++)
s += i;
return s;
}
読める、読めるぞぉ!
C++11のconstexprは、どうしても再帰関数の形になって、最適化しないとスタック消費が激しいですが、C++14なら、constexpr用のsumと実行時用のsumを別個に実装なんてしなくても良いのです。ビバC++14!
さて、constexprな関数がどれぐらい便利かは、お使いのコンパイラがどのC++のバージョンにまで対応しているかによります。
constexprが使えるC++のバージョンはC++11、C++14があります。正確には「ISO/IEC 14882:2011」、「ISO/IEC 14882:2014」といいます。要するに2011年規格と、2014年規格ってことです。1998年版はC++98です。2098年?さぁ……?
ARM系なら、基本的には最新のコンパイラはC++14に正式に対応しています。
KailのARM Compiler Version6、IARのコンパイラ、arm-none-eabi-gcc 8辺りは全部対応しています。
本当は更にポンコツ迎合したC++17ってのもあるんだけど、今のところ組み込みで正式に対応してるのはないんじゃないかな。
GCCは先月公開された9.1で正式にC++17に対応しています。組み込みも、後2,3年ぐらいしたらC++17対応してくるかと思いますが。
C++20対応はあと6,7年ぐらいしたら出てくるんじゃないの。
static_assertでテストを簡略化しよう
組み込みって、チープな環境でのテストで嫌になりますよね。テスト走らそうにも、メモリが足りないしね。
しかし、どれだけ面倒だろうが、C言語では、関数の実装が本当に正しいかは、実機やシミュレータで走らせて確認するしかありませんでした。
C++はこれをコンパイル時に確認するstatic_assertを提供してくれます。
#include <cstdint>
#include <cassert>
constexpr uint32_t sum(uint32_t x)
{
uint32_t s = 0;
for(uint32_t i =0 ; i < x; i++)
s += i;
return s;
}
static_assert( sum(0) == 0 ,"sum(0) == 0");
static_assert( sum(1) == 1 ,"sum(1) == 1");
static_assert( sum(10) == 55 ,"sum(10) == 55");
static_assert( sum(1000) == 1000*1001/2, "sum(1000) = 1000*1001/2");
static_assert ( 成立するべき式 , "エラーメッセージ" ) と記述します。C++17ではエラーメッセージの省略ができます。うらやましい。
これをコンパイルすると、例えばこんな感じになります。
test.cpp:13:25: error: static assertion failed: sum(1) == 1
static_assert( sum(1) == 1 ,"sum(1) == 1");
~~~~~~~^~~~
sum(1)が1にならない!と言っていますね。原因は i < x; の部分ですね。正確には、i <= xです。バグってのはこういうしょーもない所に入り込む物です。
このように、実機に持ち込まなくても、コンパイル時にconstexprな関数の動作を確認することができます。言い換えれば、ある入力に対する結果が確定する処理は、コンパイル時にテスト可能です。
組み込みはコンパイル時に動作が決まらないんだよ、糞野郎!という魂の叫びが聞こえますが、果たして本当にそうでしょうか?
無論、組み込みである以上、ペリフェラルの状態に依存することも多いでしょう。しかし、動作の全てがコンパイル時に未定義なのでしょうか?
例えば、UARTから値を受け取って、コマンドを実行する場面を考えましょう。確かに、通信相手とUARTペリフェラル次第で、動きは確定できません。UARTから値を受け取る処理は、実機検証するしかないでしょう。
しかし、「コマンドの実行」まで動作未定義でしょうか?むろん、ペリフェラル依存のコマンドもあるかも知れませんが、コンパイル時に確定する動作もあるのではないでしょうか?
全体としてはコンパイル時に決まらない動作でも、部分的にはコンパイル時に確定するはずです。そう言った内容は、constexprな関数で抜き出して、先にC++コンパイラの上でテストすることができます。
(ペリフェラルの動作をmocで作っちゃえば、static_assertでテストできそうとか思いましたが、実機って思った様に動かないので、やめた方がいいでしょうね。)
template constexprな関数を使おう
先に挙げたbit関数の例は、複数ビットを指定することができます。C++14版も載せておきましょう。
constexpr uint32_t bit(){return 0;}
template<typename... ARGS> constexpr uint32_t bit(size_t a,ARGS... args){
return (1 << a) | bit(args...);
}
template<typename... ARGS>
constexpr uint32_t bit(ARGS... args){
uint32_t v = 0;
for(const auto a: {args...})
v |= 1 << a;
return v;
}
FOO->EN = bit(1,15,20);
さて、templateという謎の機能が出てきました。
テンプレートやスケルトンと言ってなんとなーくイメージが湧いてくれれば良いのですが、これを簡単に語れというのは、ぶっちゃけ無理です。
ざっくり言ってしまえば、関数の設計書で、template<typename... ARGS>のARGSを型を指定することで、実際の関数を作る機能です。
色々探してみて、読みやすいかなーと思った記事を紹介しておきます。以下の記事で理解できなければ、「C++ 関数テンプレート」「C++ テンプレート アンパック」とかで検索してください。
template<数値> constexprな関数を使おう
先ほどは、「型で」templateを指定していましたが、実は型意外にも整数でtemplateを作ることができます。
template<int V>
constexpr int add100(){ return V + 100;}
static_assert( add100<1>() == 101 ,"");
これを使って、bit関数を再定義すると以下の様になります。
template<unsigned int... ARGS>
constexpr uint32_t bit(){
uint32_t v = 0;
for(const auto a: {ARGS...}) v |= 1 << a;
return v;
}
FOO->EN = bit<1,15,20>();
これのメリットは変数を渡せなくなったことです。
このbit関数って、組み込み的に考えたら、高確率で定数ですよね。
しかし、間違えてbit関数に変数を受け入れてしまうと、そこがbit関数呼び出しになってしまう。しかし、コンパイルエラーになる訳じゃないから、そのもったいないを見抜けない。
この書き方だと、bit<変数>とは書けませんから、コンパイル時にミスに気がつきます。なお、constexprな変数=定数は入れることができます。
template constexprな変数を使おう
先ほどの「bit<1,15,20>();」ですが………、面倒くさくないっすか?「()」書くの。だってさー、()っていらないじゃーん。ぶっちゃけ。
そういう場合は、template constexprな変数を使いましょう。
template<unsigned int... ARGS>
constexpr uint32_t __bit(){
uint32_t v = 0;
for(const auto a: {ARGS...}) v |= 1 << a;
return v;
}
template<unsigned int... ARGS>
constexpr uint32_t bit = __bit<ARGS...>();
FOO->EN = bit<1,15,20>;
無駄な「()」が必要なくなりましたね!
気分はstatic!
templateとconstexprを理解した皆さんは、C++がいかにCより組み込み屋にとって便利か、理解できたかと思います。
しかし、まだ、使いこなしているとは言いがたいです。
最後のキーワード、それはstaticです。C言語にも、ファイルスコープを表すstaticがありましたが、ここでのstaticはクラスの中で使うstaticです。
staticはC++のclassやstructを名前空間に変えてしまう、魔法の単語です。
例えば、各ペリフェラルの電力供給管理をするペリフェラルのPOWレジスタがある、という場面を考えてみましょう。各ペリフェラルの電力は、POWレジスタの適当なビットに対応してるとします。例えば、以下の様な情報として記述しておくとしましょうか。
struct DMA
{
static constexpr uint32_t POW = bit<3>;
};
template<unsigned int N> struct PWM
{
static constexpr uint32_t POW = bit<6 + N>;
};
using PWM0 = PWM<0>;
using PWM1 = PWM<1>;
template<unsigned int N> struct UART
{
static constexpr uint32_t POW = bit<12 + N>;
};
using UART0 = UART<0>;
using UART1 = UART<1>;
using UART2 = UART<2>;
using UART3 = UART<3>;
ここで重要なのは、DMA,PWM,UART全ての構造体が、「static constexpr POW」を持つという所です。これを利用すると、POWレジスタを設定する関数は以下の様に作れます。
template<typename... ARGS>
constexpr uint32_t __get_POW(){
uint32_t v = 0;
(void)std::initializer_list<int>{(void( v |= ARGS::POW),0)...};
return v;
}
template<typename... ARGS>
inline void powerOn(){
FOO-> POW = FOO-> POW | __get_POW<ARGS...>();
}
powerOn<PWM0,UART2,UART3>();
「(void)std::initializer_list{(void( v |= ARGS::POW),0)...};」ここがミソですね。swallowと呼ばれるテクニックを使っていますが、それを可能にしているのが、全ての構造体が「static constexpr POW」を持つという条件です。
確かにこの構造体を先に定義するのは面倒ですが、一度定義してしまえば、別プロジェクトでも使い回せます。あくまでconstexprな定数でしかないので、余計なバイナリを生成する心配はありません。
ビットフィールド指定と違って、constexprな関数の__get_POWでビットを計算するので、read-modify-writeも1回しか発生しません。それでいて、ソースコードを後から読んだときでも、PWM0、UART2、UART3の3モジュールの電源を入れてるんだなと、理解しやすいですよね。可読性もバッチリです。
まとめ
どうでしたでしょうか、C++が理解できたでしょうか? うん、知ってる、 わからんよね!!
ぶっちゃけると、C++は深淵を覗けば、何処までも深みにはまります。他人のコードを見ると、魔法みたいなことをしてることが多々あります。
私もC++なんて全然分かりません。最近になってinline namespaceとか知ったぐらいだしね。C++は謎の記号が多くて、検索しにくく、何の文法かも分からんことがあります。C++の全てを理解してる人間なんて、この世にいないんじゃないかとすら思います。理解してるのは、魔人か何かです。
しかし、「Cで書けるんだから、C++なんて価値もないクソ」ではなく、「C++はよく分からないけど、組み込みに便利らしい」と、少しでも歩み寄って頂けたなら本望です。
今回は紹介しませんでしたが、RAIIなど、Cでは使えない組み込みで有用なテクニックが他にも色々あるので、調べてみると良いでしょう。