組込 1 年目のエンジニアが開発環境を整備した話


Web から組込の世界に来てそろそろ 1 年になろうとしています。前回の 組込の開発でも CI/CD を構築してみた の続編として、その後のいくつかの取り組みについて紹介していきます。

コードフォーマッタの導入

ClangFormat は VSCode の C/C++ Extension とも連携できて良いですね。設定はこんな感じです。

.clang-format
BasedOnStyle: LLVM

AccessModifierOffset: -4
AllowShortIfStatementsOnASingleLine: false
BreakBeforeBraces: Attach
ColumnLimit: 0
Cpp11BracedListStyle: false
IncludeCategories:
  - Regex:           '^<.*\.h>'
    Priority:        1
  - Regex:           '^<.*'
    Priority:        2
  - Regex:           '.*'
    Priority:        3
IndentCaseLabels: false
IndentWidth: 4
PointerAlignment: Left
SpaceAfterCStyleCast: true
TabWidth: 4
UseTab: Never

導入の注意点としては #include がソートされるので、マナーの悪いヘッダがいると挙動が変わってしまう点です。そのような場合は、途中に空行を入れれば塊ごとにソートされるので、特別に対応が必要なら順番を調整できます。段階的に導入したりしていくと良いと思います。

CI では、フォーマットに従っていないソースがあったら落とすようにしています。最初は警告のみにして、覚悟がキマったらエラーにするようにします。

log_error() {
    echo -e "\033[1;41m ERROR \033[00m $*" 1>&2
}

# ライブラリ以外を find して clang-format をかける
find . -path ./lib -prune \
    -o \( -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.hpp' \) \
    -exec clang-format -i {} \;
# diff が発生していたら落とす
if ! git diff --quiet; then
    log_error "Code must be formatted by clang-format"
    exit 1
fi

循環的複雑度をチェックする

lizard は、循環的複雑度 (CCN, cyclomatic complexity number) を計算したり、コードの重複を検出してくれたりするツールです。

CI に組み込むところまではできていませんが、時々見ると重複部分や長すぎる関数を警告してくれます。こちらも、最終的には CI でエラーにしていくところまで整備したいと思っています。

単体テストの強化

C++ を利用

テストや実験用プロジェクトに限って C++ を解禁しました。 extern "C" で混ぜて利用します。

名前空間や class 、オーバーロード(同じ名前で異なる引数)、テンプレートあたりを利用できるようになります。 std::array あたりも利用できます。例外やヒープ領域を利用するような STL (vector など) を利用するには。 かなりの苦労が必要そうなので一旦見送っています。

さらに C++17 の constexpr ラムダ式で関数がその場で作れるのはとても便利です。コールバックがその場で書けるようになることで、処理の続きを近接して記述できるようになるため、可読性が向上します。

auto addOne = [](int n){
    return n + 1;
};
// コンパイル時に関数ポインタにすることができます。
constexpr int (*addOneFp)(int) = addOne;
static_assert(addOneFp(3) == addOne(3), "");

コンパイルオプションとしては -fno-exceptions -fno-use-cxa-atexit -fno-threadsafe-statics あたりがついていれば十分かと思います。

単体テストフレームワークを開発

Unity Test はとりあえずは使えたのですが、割り込みなどを含むもう少し複雑なテストをしようとすると不便を感じてきました。

  • 実行部分と結果を出力する部分は疎になっていた方が良さそう
    • テスト結果のレポートは自分で出力するので結果の情報だけ欲しい
  • テストしたい処理の中に割り込みが含まれると、テストケースが関数 1 つに収まらない
    • テストの開始と終了を自分で呼び出すテストフレームワークが欲しい
  • Assert 失敗したら即 setjmp, longjmp で終わると困るときもある
    • 共通じゃない tearDown 的なやつ (try-finally とか Go の defer 的なやつ) が欲しいときとか

というあたりを解消するために、 ecunit という小さなテストフレームワークを作りました。

ショートカットでテスト実行までできるようにする

実際のところ実行が面倒だと活用されない問題があります。そこで、 VSCode の tasks.json で、 test 用バイナリをビルド、書き込み、実行を一気に行えるようにしました。

.vscode/tasks.json
        {
            "label": "make test flash run",
            "type": "shell",
            "command": "make -j4 test && make flash && python3 ../../../scripts/run-test.py",
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "options": {
                // SDK の examples のプロジェクトと同じ構成ならおおよそこんな感じ
                "cwd": "${workspaceRoot}/ボード/SoftDeviceの名前/armgcc"
            },
            // その他いろいろ
        }

これで Tasks: Run Build Task (デフォルトだと Ctrl+Shift+B) から実行できるようなります。

run-test.py は、 pylink を使ったスクリプトで、特定の文字列がログに出力されるまで実行します。このとき、ログが省略されてしまわないように SEGGER_RTT_CONFIG_DEFAULT_MODE を BLOCK に設定します。

開発者個人用 Makefile

一時的にマクロを変えたいとき用の設定を書く場所です。特定のテストだけを実行したり、特定の部分のみログを出力したりしたいときに利用します。

Makefile
# local.mk があれば include
-include local.mk
local.template.mk
# 個別で変えたくなる設定をまとめておく

## Watchdog Timer
# CFLAGS += -DWDT_ENABLED=0

## Log
# CFLAGS += -DNRF_LOG_ENABLED=0
# CFLAGS += -DNRF_LOG_DEFERRED=0
# CFLAGS += -DNRF_LOG_DEFAULT_LEVEL=4

一時的にコンパイルオプションを変えたくなったら、 local.template.mk をコピーして local.mk を作成します。 また、 local.mk をバージョン管理に含まれないようにします。このようにすることで、一時的に変えた設定を共有してしまうことを防げます。

.gitignore
# Local configuration
local.mk

Natvis の利用

例えばデバッガでポインタを表示したりすると、そのままではアドレスとその値が示す場所の値だけが表示されてしまいます。本当はそこから隣の size_t lenght; 分見たいんだけどなぁというときは、通常は GCC を手で操作したりメモリダンプなどを利用したりすることになります。

Natvis は、 VS Code (元々 Visual Studio の機能らしいですが) で、デバッガ上の見え方をカスタマイズするツールで、データ構造を視覚化することができます。

例えば、メモリ上の区間を表す brange_t があったとして、

typedef struct {
    uint8_t* ptr;
    const uint8_t* end;
} brange_t;

通常では、 *ptr*end ぐらいしか見ることができません (一応気を利かせて uint8_t* を文字列として表示してくれてはいますが)。

下記のように natvis を設定することで、 ptr から始まる長さ end - ptr の配列として認識させることができます。

.vscode/default.natvis
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
    <Type Name="brange_t">
        <DisplayString>{{size = {end - ptr}}}</DisplayString>
        <Expand>
            <Item Name="[size]">end - ptr</Item>
            <ArrayItems>
                <Size>end - ptr</Size>
                <ValuePointer>ptr</ValuePointer>
            </ArrayItems>
        </Expand>
    </Type>
</AutoVisualizer>
.vscode/launch.json
    "configurations": [
        {
            "name": "GDB",
            "type": "cppdbg",
            "request": "launch",

            // その他デバッグ起動用のいろいろ

            // .vscode/default.natvis を利用するように設定
            "visualizerFile": "${workspaceRoot}/.vscode/default.natvis",
        }
    ]

[Visualized View] の表示が新しく追加されています。

16 進数や 2 進数で表記したり、 Linked List や HashMap を表現する方法も想定されていて、データ構造を視覚的に確認することができます。 Git の管理対象にすることで、互いの Natvis の定義を共有することができるようになります。

MkDocs の利用

ドキュメントを Word や Pages ではなく、 MkDocs で管理するようにしました。

さらに CSS/JavaScript を利用することで、視覚的な情報を効率的に表現できます。例えば LED の点灯パターンを解説するドキュメントは、 div タグと data 属性で表現されており、 CSS と JavaScript で見た目を作っています。

## LED Blinking Pattern

本製品は以下の各状況に応じたパターンで LED を点滅させます。

<div class="blink-pattern">
    <div data-duration-ms="900"></div>
    <div data-duration-ms="100"></div>
</div>

<div class="blink-pattern">
    <div data-duration-ms="100"></div>
    <div data-duration-ms="400"></div>
    <div data-duration-ms="100"></div>
    <div data-duration-ms="400"></div>
</div>

まとめ

広く応用できそうな部分を前回より具体的に書いてみました。

個人的には、 GTAC 2014: Move Fast & Don't Break Things の 22:37 あたりで話しているような Pyramid の状態に近づけたいと考えています。 UI テストに相当する部分は、組込で言うところの実際に動かしてテストする部分です。その前段階を厚くしていくことで、ビジネスの期待に素早く答えながら開発を進めていくことができると考えています。

このような取り組みに興味がございましたらぜひ一緒にやっていきましょう

ちなみにタイトルですが、「組込 1 年目」であって「1 年目のエンジニア」ではないです。