SNSへはこちら

切符鉄のススメ/マイコンでC++を使おうぜっていう話(後編)

この記事は Micro Mouse Advent Calendar 2019 9日目の記事です。
本記事群は2本立てになっております。この記事は後半です。前半の記事へのリンクはこちら

前回は切符愛が溢れすぎてこちらの記事が収まりきりませんでした。ということで後編ということでやっと技術系の記事を掲載いたします。
若干説明が雑になっていることをお詫び申し上げます

マイコンプログラミングの言語

皆さんはマイコンで制御プログラムを書く時、何の言語を用いていますか?
多分 99% の人は「C言語に決まっているじゃないか」と答えるかなあと思います。

ではこちらからご提案。「その代わりに C++ はいかがですか?」
想定される答えはこちらです:

最後の人には何も言うことがありませんが、残りの方々は一度やってみることをおすすめします。え?メリットですか??

C++を使うメリット

では個人的にメリットの大きいと思った順に述べていきます。なお、C++ の規格としては C++17 を想定しています。ちなみに僕にとって C++17 は中世です。2020年に C++20 が登場しますが、これは僕は明治時代だと思います。早く現代に追いついてくれ。

なお、参考にするべきものはこのリファレンスです。

基本的なアルゴリズムが言語レベルですでに組まれている

これ、めちゃめちゃデカイです。

ソート

汎用データ配列のソートをする際は、C言語で型それぞれに対応する関数を自作しなければなりません。一方 C++ では、

#include <algorithm>
int arr[] = {1, 1, 4, 5, 1, 4};
std::sort(std::begin(arr), std::end(arr)); // 昇順クイックソート

なんとこれだけです。ちなみに std::begin は先頭のイテレータ(ポインタと思ってください)、std::end は最後の次のイテレータを返します。逆順ソートですか?逆イテレータを入れればいいので

#include <algorithm>
int arr[] = {1, 1, 4, 5, 1, 4};
std::sort(std::rbegin(arr), std::rend(arr)); // 降順クイックソート

とでもすればいいでしょう。
自作構造体でもソートを定義できます。以下のコードでは便宜上参照とラムダ式を使っています。

#include <algorithm>

struct human {
    int height;
    int age;
};

int main(){
    human men[] = {
        {150, 20},
        {190, 35},
        {120, 14},
        {80, 5}
    };
    std::sort(std::begin(men), std::end(men), [](const auto& man1, const auto& man2) {
            return man1.age < man2.age; // 年齢でソート
            }); // 5, 14, 20, 35
}

探索

C言語だとループを書かねばなりません。面倒ですよね?じゃあC++では?

#include <cstdio>
#include <algorithm>

struct human {
    int height;
    int age;STL 

    bool operator==(const human& arg) const {
        return arg.height == this->height && arg.age == this->age;
    }
};

int main(){
    human men[] = {
        {150, 20},
        {190, 35},
        {120, 14},
        {80, 5}
    };

    const human target = {80, 5}; // この要素があるか知りたい
    auto* ans = std::find(std::begin(men), std::end(men), target);
    if( ans != std::end(men) ) { // 要素が見つかったら
        printf("Found!\n");
    }else{
        printf("Not Found!\n");
    }
}

こんな感じです(std::cout はマイコンにとって重すぎるので printf で代用しています。このように C の知識も使えるのが C++ のいいところ)。構造体(実はクラス)専用の比較演算子として、メンバ関数が定義されているのが見えますか?記述通り、heightage がどちらも一致した時に true を返すようにしています。

これ以上述べても記事が間延びするだけなのでここで抑えておきます。

STLが強い

STL とは C++ に標準で搭載されているデータ構造のことです。例えば可変長配列の std::vector、キュー構造の std::queue などです。例えばキュー構造 push/pop は簡単にできて、

#include <cstdio>
#include <queue>

int main(){
    std::queue<int> q;
    q.push(8);
    q.push(4);
    q.push(2);
    q.push(1);

    while( not q.empty() ) {
        printf("%d\n", q.front());
        q.pop();
    }
    return 0;
}

です。すごいでしょう。

コンパイル時に計算が完了する(ものもある)

C++ はとにかくコンパイラをいじめる言語で、コンパイルオプションによってはプログラム実行時ではなく、コンパイル時に計算を行おうとします。魔法の言葉は constexpr。ぜひこれを覚えて帰ってください。

#include <cstdio>

constexpr auto bikkuri(auto n) {
    if( n == 0 ) return 1;
    return n * bikkuri(n - 1);
}

int main(){
    printf("%d\n", bikkuri(6)); // 720
    return 0;
}

この状態でコンパイルしてみましょうか。そして生成されたバイナリの中に bikkuri 関数があるか見てみましょう。

$ g++ -O0 -std=c++17 a.cpp
$ nm a.out | grep bikkuri
0000000100000e0b T __Z7bikkurii

ありますねえ。では最適化レベルを上げましょう。

$ g++ -Os -std=c++17 a.cpp
$ nm a.out | grep bikkuri
$

という訳で、なんと bikkuri 関数がなくなってしまいました。逆アセンブルしてみると、計算結果の 720 がレジスタに直接代入されるようなコードになっています。つまり実行時の計算時間がゼロになったということです。これは非常に高速化に貢献してくれるわけです!!!

これを悪用すれば、この記事のようにコンパイル時、コンパイラさんに速度テーブルを作らせることも可能です。

そもそもマイコンのペリフェラル管理はクラスが向いている

マイコンのペリフェラルはそれが果たす機能で別れていて、GPIO なら GPIO、タイマならタイマが担っています。これは実チップ上での配置も同じです。
一方オブジェクト指向で用いる構造は、そのような機能ブロックと作りが似ています。つまり、この部分は積極的に C++ のクラス化をすべきなのです。

例えば GPIO。gpio_init(GPIOA, 5) なんてC言語で自前で書かなくても、予めクラスの設計図を作っておけば、ペリフェラルとピン番号を一括で管理できてバグを減らせるでしょう。

こんなファイルを用意します。

そして main 関数でこう書くわけです。

#include "gpio.hpp"

int main() {
    Gpio led(GPIOA, 5, GpioConfig::PinMode::GPIO_OUTPUT);
    led = 0; // 取り敢えず消灯

    while(1) {
        led ^= 1; // XORっぽく反転する仕様にした
        delay_msec(1000); // こんなディレイ関数があるとして
}

これだけでLチカできます。他の関数で使いたい場合はグローバル変数にすればいいですし、とにかく管理が楽です。

メリットまとめ

C++ はマイコンプログラミングに対して以下のようなメリットがあります。

  • 基本的なアルゴリズム・データ構造が言語規格レベルで規定されている
  • コンパイル時に計算が完了するものがあるため、うまくやると大きくプログラムを高速化出来る
  • クラス化が便利。特にマイコンペリフェラルの設定は積極的にクラス化をすると管理が非常に楽になる。

C++プロジェクトの作り方 for STM32CubeIDE

「それじゃあどうやってマイコン用プログラムを生成するのさ」という人がいると思いますので、簡単に説明します。

1. プロジェクト作成時に C++ を選択する

プロジェクト作成ウィザードに C か C++ かを選ぶところがあると思うので、C++ を選択します。

なお、すでに作成したプロジェクトを C++ にも対応させたい場合は、左ペインの Project Explorer にある対象のプロジェクトを右クリックすると、Convert to C++ というのが見えるので、それを押せばすぐに変換できます。
ちなみに C と C++ は混在可能なので、変換したからと言ってなにかまずいことが起きるわけではありません。

ビルド時には .c ファイルは C、.cpp ファイルは C++ として見てくれるので、そのへんの心配はいりません。

2. C++17 を使えと脅迫する

この記事にあるように、CubeIDE に搭載されている gcc は C++17 対応版です。でも何故か IDE 側の選択肢として C++14 までしかありません。あり得ないですね。なので無理矢理 C++17 を使うように仕向けます。

Project Explorer で対象のプロジェクトを右クリックして、Properties を選びます。
出てきたウィンドウの左ペインで C/C++ Build -> Settings と行き、右側で Tool Settings タブを選びます。
そうしたら MCU G++ Compiler -> General と選択していき、Language Standard を GCC default にしてやってください。デフォでは「C++14 使うよ」と IDE さんは主張しているのですが、「うるせぇ黙れ」と言わんばかりにその主張を握りつぶします。
つづいてその下の Miscellaneous に飛び、ここで -std=c++17 を追加しましょう。

折角なので最適化レベルを上げましょう。ちょっと上の Optimization に飛び、 -Os にします。

3. (任意) .ioc ファイルを吹き飛ばす

これはやらなくてもいいですが、やった方が気持ちいいはずです。プロジェクトのルートにある .ioc ファイルは単なるおせっかいでしか無いので削除します。これで CubeMX 的なコードジェネレーション機能の息の根を止めることに成功しました。

4. main.c を main.cpp に

main 関数から C++ のコードを呼び出せるようにリネームします。これで使う準備は万端です。快適な C++ ライフを。

切符鉄の話をしすぎた気がしますが、たまにはこういうのもいいですよね。まあぶっちゃけC++の記事書いているよりも切符鉄の方が何杯も楽しかったけど。

実際記事書いてて疲れましたね。元気が出たら打ち間違いとか修正島〜〜す
それでは私の記事は以上。ありがとうございました。

明日 12/10 は DC マウス制作2年目にして全日本大会の迷路を結構いいタイムで完走したという快挙を果たしたパワハラパワプロくんの記事です。