Zephyr RTOS と自動テストで捗るファームウェア開発の話

この記事はAkerun Advent Calendar 13日目の記事です。

今回はファームウェアエンジニア いとう が担当です。

前回からの流れで

前回 の記事でZephyr RTOSのビルドに使える開発環境を作成しました。これをもう少し活用できないでしょうか?ってことで この環境をそのまま CI/CDの実行環境として使ってみます。

TL;DR

できたこと

  • ✅ GitLabと連動した自動ビルド&自動テスト
  • クラウド上で高速かつ単体でも結合でもできるテスト環境

f:id:photosynth-inc:20191213194908p:plain

GitLab CIで自動ビルド

まずは、GitLab CIで自動ビルドをやってみます。GitLab CIには任意のDockerイメージをCIの実行環境として指定できます。ここに前回作成したイメージを指定すると、GitLab runner 側(CI実行サーバー)に各種ツールをインストールしておかなくてもDockerだけでビルドができてしまいます。

Zephyrならではの悩みどころとしては west を使って複数のGitリポジトリでプロジェクトを構成している場合、せっかくGitLab runnerがクローンしてくれた環境を無視して改めてwest init + west updateが必要となってしまいます。これについては今の所解決策は見当たらず、愚直に新しいディレクトリを作成してその中でwest init + west updateをやってます。そのためにssh系のクレデンシャルも必要となりました。ちょうどGitLabにはCIで実行する環境変数を権限のないメンバーから見えなくする機能があるのでこれを使っています。

.gitlab-ci.yml抜粋

image:
  name: odorenotokorono.gitlabnourl.com:5555/hogehoge/dev-env-docker:0.1.1
  entrypoint: [""]

stages:
  - lint
  - build
  - test
  - deploy

before_script:
  - eval $(ssh-agent -s)
  - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
  - mkdir -p ~/.ssh
  - chmod 700 ~/.ssh
  - ssh-keyscan odorenotokorono.gitlabnourl.com >> ~/.ssh/known_hosts
  - chmod 644 ~/.ssh/known_hosts
  - mkdir -p .ccache
  - export CCACHE_BASEDIR=${PWD}
  - export CCACHE_DIR=${PWD}/.ccache
  - mkdir -p .pip
  - export PIPCACHE_DIR=${PWD}/.pip

cache:
  paths:
    - .pip
    - .ccache

variables:
  WEST_DIR: __west__
  BUILD_TYPE: Debug

.extract_west: &extract_west |-
  west init -m ${CI_REPOSITORY_URL} --mr ${CI_COMMIT_SHA} ${WEST_DIR}
  cd ${WEST_DIR}
  west update
  pip3 --cache-dir=${PIPCACHE_DIR} install -r ./zephyr/scripts/requirements.txt
  source ./zephyr/zephyr-env.sh

.branch_param: &branch_param
  BUILD_ARTIFACT_NAME: "${CI_PROJECT_NAME}-${BUILD_BOARD}-${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}"

.tag_param: &tag_param
  BUILD_ARTIFACT_NAME: "${CI_PROJECT_NAME}-${BUILD_BOARD}-${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}"

.generic_artifact: &generic_artifact
  name: "${BUILD_ARTIFACT_NAME}"
  paths:
    - ./*.hex
    - ./*.exe
    - ./debug_files

.generic_build: &generic_build
  stage: build
  script:
    - *extract_west
    - source ./app/VERSION
    - >-
      if [ -z "${EXTRAVERSION}" ]; then
        export VERSION_STRING=${VERSION_MAJOR}.${VERSION_MINOR}.${PATCHLEVEL};
      else
        export VERSION_STRING=${VERSION_MAJOR}.${VERSION_MINOR}.${PATCHLEVEL}-${EXTRAVERSION};
      fi
    - >-
      if [ -z "$CI_COMMIT_TAG" ]; then
        export BUILD_APP_NAME=${CI_PROJECT_NAME}-${BUILD_BOARD}-${CI_COMMIT_REF_SLUG}-${BUILD_TYPE}-${CI_COMMIT_SHORT_SHA};
      else
        export BUILD_APP_NAME=${CI_PROJECT_NAME}-${BUILD_BOARD}-v${VERSION_STRING}-${BUILD_TYPE};
      fi
    - echo ${VERSION_STRING}
    - west build -b ${BUILD_BOARD} -d build_${BUILD_BOARD} ./app -- -D CMAKE_BUILD_TYPE=${BUILD_TYPE}
    - cd ${CI_PROJECT_DIR}
    - mv ${WEST_DIR}/build_${BUILD_BOARD}/zephyr/${BUILD_KERNEL_NAME}.hex ./${BUILD_APP_NAME}.hex || true
    - mv ${WEST_DIR}/build_${BUILD_BOARD}/zephyr/${BUILD_KERNEL_NAME}.exe ./${BUILD_APP_NAME}.exe || true
    - mkdir debug_files
    - mv ${WEST_DIR}/build_${BUILD_BOARD}/zephyr/${BUILD_KERNEL_NAME}* ./debug_files/
    - mv ${WEST_DIR}/build_${BUILD_BOARD}/zephyr/.config ./debug_files/
  artifacts:
    <<: *generic_artifact
  tags:
    - docker

.foo_model: &foo_model
  BUILD_BOARD: foo_model
  BUILD_KERNEL_NAME: foo_model

build-firmware: 
  <<: *generic_build
  artifacts:
    <<: *generic_artifact
    expire_in: 1 week
  variables:
    <<: *foo_model
    <<: *branch_param
  only:
    - branches

build-firmware-release: 
  <<: *generic_build
  artifacts:
    <<: *generic_artifact
    expire_in: 1 week
  variables:
    <<: *foo_model
    <<: *branch_param
    BUILD_TYPE: Release
  only:
    - branches

少し長めになってますがポイントは3つ

  • キャッシュ

Zephyrのビルドは標準でcchaceによる、コンパイラキャッシュが有効となっています。これにより同じコンパイラオプションであれば再ビルドしても爆速でコンパイルが完了します(クリーンビルドでも)。ですがGitLab-CIのDocker環境は毎回フレッシュな環境になってしまうのでこのcchaceが効きません。このようなキャッシュデータを引き継ぐ機能をGitlab-CIは提供しています。下記抜粋中のcache:\n paths:のところで指定しているディレクトリは別のビルドジョブの時も引き継がれます。設定さえしておけばこのキャッシュはCI実行マシンをまたいで共有できます。

before_script:
  - export CCACHE_BASEDIR=${PWD}
  - export CCACHE_DIR=${PWD}/.ccache
  - export PIPCACHE_DIR=${PWD}/.pip

cache:
  paths:
    - .pip
    - .ccache

同様にpipのダウンロードデータもキャッシュ化してビルドジョブごとに使い回しています。

west使っている部分を抜き出すと次の通り、通常のターミナルで行う処理と変わりません。Release/Debugビルド両方でビルドできるように変数で切り替えを行なっています。ここでビルドに必要なpipパッケージのインストールも行なっています。

##中略
.extract_west: &extract_west |-
  west init -m ${CI_REPOSITORY_URL} --mr ${CI_COMMIT_SHA} ${WEST_DIR}
  cd ${WEST_DIR}
  west update
  pip3 --cache-dir=${PIPCACHE_DIR} install -r ./zephyr/scripts/requirements.txt
  source ./zephyr/zephyr-env.sh
##中略
.generic_build: &generic_build
  stage: build
  script:
##中略
    west build -b ${BUILD_BOARD} -d build_${BUILD_BOARD} ./app -- -D CMAKE_BUILD_TYPE=${BUILD_TYPE}
##中略
  • ビルド結果の保存

さて、ビルドもできたのでビルドでできたファイルをGitLabサーバーに保存しておきましょう。*.elfはもちろん、デバッグ時に必要となりそうなファイルをごそっと取っておくとあとできっと役に立ちます。GitLab-CIではこのようなファイルはアーティファクトと呼ばれています。

##中略

.branch_param: &branch_param
  BUILD_ARTIFACT_NAME: "${CI_PROJECT_NAME}-${BUILD_BOARD}-${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}"

##中略
    - mv ${WEST_DIR}/build_${BUILD_BOARD}/zephyr/${BUILD_KERNEL_NAME}.hex ./${BUILD_APP_NAME}.hex || true
    - mv ${WEST_DIR}/build_${BUILD_BOARD}/zephyr/${BUILD_KERNEL_NAME}.exe ./${BUILD_APP_NAME}.exe || true
    - mkdir debug_files
    - mv ${WEST_DIR}/build_${BUILD_BOARD}/zephyr/${BUILD_KERNEL_NAME}* ./debug_files/
    - mv ${WEST_DIR}/build_${BUILD_BOARD}/zephyr/.config ./debug_files/

##中略
.generic_artifact: &generic_artifact
  name: "${BUILD_ARTIFACT_NAME}"
  paths:
    - ./*.hex
    - ./*.exe
    - ./debug_files

小さなtipsですが生成されるファイルが毎回同じだと作業中に取りちがえる可能性があるため、ブランチの名前とコミットのハッシュの一部をファイル名にしてアーティファクト指定しています。アーティファクトは生存期間も指定できるの少し長めに設定しておけば、いつの間にかバグが入り込んだ時の「どこからバグが入り込んだかチェック」が少しは捗ります。

GitLab CIで自動テスト

自動でビルドができれば次は自動テストをやってみましょう。変更をプッシュするたびにテストが行われてどこも壊れていないことが保証されれば怖いもの無しです。Zephyr にはテストフレームワークが同梱されています。これを使うしかないでしょう。内容の紹介は省きますが普通に使えるやつです。

unit-test:
  stage: test
  script:
    - *extract_west
    - ./zephyr/scripts/sanitycheck -v -i --ninja -T ./app/test
  tags:
    - docker

さて、組み込みで自動テストはどのようにすればいいのでしょうか?私の知っている範囲で組み込みの自動テストは次の3種類にざっくり分類されると思います。

それぞれメリット・デメリットがあると思います。しっかりしたプロジェクトでは複数のテスト種類を実施しているのではないでしょうか?(しっかりしたプロジェクトを寡聞にして知りませんが・・・)Zephyrのテストフレームワークはいずれにも対応しているようです。今回はクラウド上で動作しているGitLab-CIで実施する自動テストなので実機でのテストはなしにするとして、実行スピード重視でホスト環境での単体テストにすべきか?組み合わせテストができるけど遅いエミュレーターを使うのが正解でしょうか・・・?

救世主 native_posix !

先ほどの悩みは native_posix で一気に解決できました。

docs.zephyrproject.org

これはZephyrをホスト側OS(今回はLinux)のアプリケーションとしてビルドしてしまい実行できるようにするBOARDです。ビルドする際にボードの指定に-b native_posixとしてやれば zephyr.elfの代わりに zephyr.exe が生成されます。

  • 実行はネイティブ!
    • ./zephyr.exe で実行できるし速い
  • いろんなカーネルコールも呼べる!
    • マルチスレッドもOK
  • 各種ペリフェラルでテスト可能!
    • 割り込み
    • ハードウェアタイマー
    • Bluetooth!
    • FlashROM
    • その他諸々

これは素晴らしいですね。FlashROMの動作を確かめたところ同一階層にあるファイルに読み書きでき、実機で使うのと同じAPI(NVS)で読み書きができました。経験上ファームウェアのバージョンアップ時に不揮発データのデータ追加などを行うとマイグレーション時に不具合でがちなのでこの辺を重点的にテストできれば安心感が増します。

これをZephyrのテスト環境として使うのは ***/hogehoge/testcase.yamlの対応基板のホワイトリストnative_posixを指定するだけでOKです。unit_testと違いprintk()k_sleep(ms)呼び放題でテストを組めますし実行は速いです。

tests:
  hogehoge.genral:
    platform_whitelist: native_posix
    tags: test_framework

あとはガシガシテストを書いていくだけですね💪!捗る〜

できたこと&やりたいこと

できたこと

  • ✅ GitLabと連動した自動ビルド&自動テスト
  • クラウド上で高速かつ単体でも結合でもできるテスト環境

今後やりたいこと

  • [ ] Raspberry piでGitLab runnerを動かして J-Link経由で実機自動テスト
    • これは時間がなくてやっていないだけでやればできそう
    • プッシュするたびAkerunがガシャガシャなったら楽しそう
  • [ ] Raspberry pi (ARM64) で native_posix を動かしてなおかつBluetoothも動かせればAkerunのテストしたい範囲がほぼほぼカバーできそう
    • GitHubのissueに上がっていたので現状は無理らしい
    • Raspberry piだったら安いしオフィスに転がっているので 10台ぐらい並べて並列自動テストしてみたい
    • Raspberry piだったらI2CやSPIも出ているので、HAL書いてやれば最高の環境じゃないですか?SoC選定前にソフト作れちゃうしデバイス評価もできちゃう

以上です!最後まで読んでいただきありがとうございました〜 👋

・・・

株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 recruit.jobcan.jp

Akerun Proの購入はこちらから akerun.com