はじめに
色々な環境のOTA(Firmware Update)を試しています。
今回はZephyr OSで使われているmcubootのOTA。
取りあえず付属のサンプルをFRDM-k64Fで動かしてみただけで、実はオンラインアップデートにもなってませんが、記録を残しておきます。
【今回の環境】
mcubootについて
mcubootの仕組みは以下に説明があります。
https://mcuboot.com/mcuboot/design.html
自分の理解のために一部ですが和訳しました。
Design document of mcuboot
上記にかなり細かく説明が書かれていますが、この説明だけではわからない点もあり、以下は実機で確認した内容も含みます。
全体的な仕組み
mcubootは、Firmware格納場所として2つのPartitionを想定します。通常はPrimary PartitionのFirmwareが、署名検証後に起動されます。更新用FirmwareがSecondary Partitionに格納されると、mcubootはSecondaryの署名を検証後、Scratch Partitionを使ってPrimary/SecondaryのFirmwareを交換し、PrimaryのFirmwareを起動します。交換後のFirmware起動が正常に行われなかった場合(交換後FirmwareによりImage OKがセットされなかった場合)、mcubootは次のブート時にPrimary/SecondaryのFirmwareを再交換し、Primaryを以前のFirmwareに戻します。
なお、上記図中のFlashのアドレスはFRDM-K64F用のサンプルで使われているもので、環境によって調整が必要です。
また、今回動かしたサンプルでは、Secondary Partitionへの書込みをFirmwareの機能では行わず、Primary Partitionと同様に直接書込みしてます。
Firmwareへの署名
Firmwareへの署名にはimgtool.py
を使います。署名後は、署名関連の情報がTLV(Type Length Value)形式でFirmwareの後ろに付与されます。今回動かしたサンプルでは以下の情報が付与されました。
項目 | 用途 | 長さ |
---|---|---|
TLV header | MAGIC NumberとTLV領域長 | 4byte |
SHA256 | Firmwareのハッシュ値 | Tag 2byte, Length 2byte, Value 32byte |
KEYHASH | 署名鍵に対応する公開鍵のハッシュ値 | Tag 2byte, Length 2byte, Value 32byte |
ECDSA256 | Firmwareの署名値 | Tag 2byte, Length 2byte, Value 72byte |
Image Trailer
Primary/Secondary Partitionの末尾はImage Trailerと呼ばれ、mcubootが各Partitionの状態を管理するための領域として使われます。主な記録内容は以下の通り。
項目 | 用途 |
---|---|
Swap status | Firmware交換が中断した場合に備えて進行状況を記録 |
Image OK | Firmwareが正しく起動できたことを示すフラグ |
MAGIC | Firmwareが格納されていることを示すMAGIC Number |
Swap Type
Boot時にFirmware交換や戻しを行うかどうかは、Image Trailerの情報から以下のように判定されます(ドキュメントにはもっと細かく書かれていますが、要約すると恐らく以下のような感じ)。
判定順序 | 判定条件 | Swap Type | Swap Typeの意味 | Boot動作 |
---|---|---|---|---|
1 | SecondaryのMAGICが正しく設定済 | BOOT_SWAP_TYPE_TEST | Secondaryに新たなFirmwaerがあるので要交換 | Secondaryの署名を確認後、Primary/SecondaryのFirmwareを交換し、Primaryからの起動を試みる |
2 | PrimaryのMAGICが設定済だが、Image OKが未設定 | BOOT_SWAP_TYPE_REVERT | Firmware交換を行ったが正しく起動できなかったので戻しが必要 | Primary/SecondaryのFirmwareを交換して元に戻したう上で、Primaryからの起動を試みる |
3 | それ以外 | BOOT_SWAP_TYPE_NONE | 通常状態 | Primaryからの起動を試みる |
Firmware更新方式のバリエーション
Mode | 内容 |
---|---|
Swapping Mode | 通常の方式。Secondary Partitionに交換用Firmwareが存在する場合、ブートローダは次のブート時にSecondaryの署名を確認後、Primary/SecondaryのFirmwareを交換し、Primaryからの起動を試みる。 |
Overwriting Mode | Firmwareの戻しを行わない方式。Secondary Partitionに交換用Firmwareが存在する場合、ブートローダは次のブート時にSecondaryの署名を確認後、PrimaryをSecondaryの内容で上書きし、Primaryからの起動を試みる。MCUBOOT_OVERWRITE_ONLY オプションで指定する。 |
Direct-xip Mode | Firmwareの交換や上書きを行わず、Primary/Secondaryのうちバージョンの新しい方を直接起動する。 |
RAM-load Mode | Firmwareの交換や上書きを行わず、Primary/Secondaryのうちバージョンの新しい方を、RAMにコピーしてから起動する。 |
実験準備(Zephyr環境の準備)
Zephyrのドキュメントに沿って用意していきます。
Getting Started Guide
1. Select and Update OS
開発環境のパッケージを最新化。
sudo apt update
sudo apt upgrade
2. Install dependencies
Zephyr OSのビルドに必要なパッケージを追加します。
sudo apt install --no-install-recommends git cmake ninja-build gperf \
ccache dfu-util device-tree-compiler wget \
python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file \
make gcc gcc-multilib g++-multilib libsdl2-dev
CMakeのバージョンチェック。3.13.1以上ならOK。
$ cmake --version
cmake version 3.16.3
3. Get Zephyr and install Python dependencies
Zephyrの管理ツールwestをインストール。
pip3 install --user -U west
echo 'export PATH=~/.local/bin:"$PATH"' >> ~/.bashrc
source ~/.bashrc
Zephyrのソースコードをダウンロード。
west init ~/zephyrproject
cd ~/zephyrproject
west update
ZephyrアプリをビルドするためのCMake環境を準備。
west zephyr-export
追加で必要なツールをインストール。
pip3 install --user -r ~/zephyrproject/zephyr/scripts/requirements.txt
4. Install a Toolchain
Zephyr SDK installerをダウンロード。
cd ~
wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.11.4/zephyr-sdk-0.11.4-setup.run
Zephyr SDKをインストール。
Toolchainも一緒にインストールされます。
chmod +x zephyr-sdk-0.11.4-setup.run
./zephyr-sdk-0.11.4-setup.run -- -d ~/zephyr-sdk-0.11.4
udev ruleをインストール。
sudo cp ~/zephyr-sdk-0.11.4/sysroots/x86_64-pokysdk-linux/usr/share/openocd/contrib/60-openocd.rules /etc/udev/rules.d
sudo udevadm control --reload
5. Build the Blinky Sample
まずはZephyrのサンプルを動かしてみます。ボードとしてfrdm_k64f
を指定してビルド。
cd ~/zephyrproject/zephyr
west build -p auto -b frdm_k64f samples/basic/blinky
6. Flash the Sample
フラッシュに書き込むと、FRDM-K64Fの緑のLEDが点滅しました。
west flash
ディレクトリ構成
以上で、Zephyr環境の準備ができました。
今回使うディレクトリは以下の構成になっています。
~/
├ zephyrproject
│ ├ bootloader
│ │ └ mcuboot
│ │ └ samples
│ │ └ zephyr
│ └ zephyr
│ └ samples
│ └ basic
│ └ blinky
└ zephyr-sdk-0.11.4
└ arm-zephyr-eabi
mcuboot sampleのビルドとテスト
上記の通り、mcubootはzephyrproject/bootloader/mcuboot
に、Zephyr用のサンプルはその下のsamples/zephyr
にあります。
このサンプルの使い方は、同じディレクトリのMakefile
の冒頭コメントに記載されてます。
ビルドとテスト
Makefileのコメントを参考に以下コマンドでビルドします。
なお、Makefile中で環境変数ZEPHYR_BASE
を参照しているので、あらかじめZephyrの場所を定義しておきます。
export ZEPHYR_BASE=~/zephyrproject/zephyr
make BOARD=frdm_k64f all
ビルドが終わると、以下3つのファイルが作られました。
ファイル | サイズ | 内容 |
---|---|---|
mcuboot.bin | 0xB774 | ブートローダ |
signed-hello1.bin | 0x3C70 | 最初のFirmware |
signed-hello2.bin | 0x60000 | 交換用Firmware(Image Trailer付き) |
交換用Firmwareは、本来であれば、現行FirmwareによってSecondary Partitionに書き込まれた後、現行FirmwareがImage TrailerのMAGIC領域にMagic Numberを書き込むことによって交換されますが、このサンプルでは交換用FirmwareとMAGIC領域の書込みもコマンドで直接行います。そのため、signed-hello2.bin
は--pad
オプション付きで署名されており、Image Trailerを含むSecondary Partition全体を表現する大きなサイズ(0x60000 byte)になっています。
次に、まずブートローダを書き込んでみます。
make flash_boot
シリアルには以下のメッセージ。まだPrimary Partitionが空なのでエラーになります。
*** Booting Zephyr OS build zephyr-v2.4.0-1531-gf004410aa1a7 ***
[00:00:00.005,000] <inf> mcuboot: Starting bootloader
[00:00:00.006,000] <inf> mcuboot: Primary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.006,000] <inf> mcuboot: Scratch: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.006,000] <inf> mcuboot: Boot source: primary slot
[00:00:00.010,000] <inf> mcuboot: Swap type: none
[00:00:00.010,000] <err> mcuboot: Unable to find bootable image
次に、最初のFirmwareをPrimary Partitionに書き込みます。
make flash_hello1
書込んだFirmwareが起動されました。
*** Booting Zephyr OS build zephyr-v2.4.0-1531-gf004410aa1a7 ***
[00:00:00.005,000] <inf> mcuboot: Starting bootloader
[00:00:00.006,000] <inf> mcuboot: Primary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.006,000] <inf> mcuboot: Scratch: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.006,000] <inf> mcuboot: Boot source: primary slot
[00:00:00.010,000] <inf> mcuboot: Swap type: none
*** Booting Zephyr OS build zephyr-v2.4.0-1531-gf004410aa1a7 ***
Hello World from hello1 on frdm_k64f!
続いて、交換用FirmwareをSecondary Partitionに書き込みます。前述の通りサイズが大きいので、書込みには時間がかかります。
make flash_hello2
Swap TypeがBOOT_SWAP_TYPE_TEST
に変わり、後から書き込んだFirmware(hello2)が起動されました。
*** Booting Zephyr OS build zephyr-v2.4.0-1531-gf004410aa1a7 ***
[00:00:00.005,000] <inf> mcuboot: Starting bootloader
[00:00:00.006,000] <inf> mcuboot: Primary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.006,000] <inf> mcuboot: Scratch: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.006,000] <inf> mcuboot: Boot source: primary slot
[00:00:00.010,000] <inf> mcuboot: Swap type: test
*** Booting Zephyr OS build zephyr-v2.4.0-1531-gf004410aa1a7 ***
Hello World from hello2 on frdm_k64f!
しかし、このFirmwareにはImage OK
をセットする処理は入っていないため、リブートするとSwap TypeはBOOT_SWAP_TYPE_REVERT
となり、Firmwareの戻しが行われます。
*** Booting Zephyr OS build zephyr-v2.4.0-1531-gf004410aa1a7 ***
[00:00:00.005,000] <inf> mcuboot: Starting bootloader
[00:00:00.006,000] <inf> mcuboot: Primary image: magic=good, swap_type=0x2, copy_done=0x1, image_ok=0x3
[00:00:00.006,000] <inf> mcuboot: Scratch: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.006,000] <inf> mcuboot: Boot source: none
[00:00:00.006,000] <inf> mcuboot: Swap type: revert
*** Booting Zephyr OS build zephyr-v2.4.0-1531-gf004410aa1a7 ***
Hello World from hello1 on frdm_k64f!
最初のFirmware(hello1)に戻っていることがわかります。もう一度リブートすると、Swap Typeが通常状態のBOOT_SWAP_TYPE_NONE
に戻っていることがわかります。
*** Booting Zephyr OS build zephyr-v2.4.0-1531-gf004410aa1a7 ***
[00:00:00.005,000] <inf> mcuboot: Starting bootloader
[00:00:00.006,000] <inf> mcuboot: Primary image: magic=good, swap_type=0x4, copy_done=0x1, image_ok=0x1
[00:00:00.006,000] <inf> mcuboot: Scratch: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.006,000] <inf> mcuboot: Boot source: none
[00:00:00.006,000] <inf> mcuboot: Swap type: none
*** Booting Zephyr OS build zephyr-v2.4.0-1531-gf004410aa1a7 ***
Hello World from hello1 on frdm_k64f!
run-tests.shを使ったテスト
同じフォルダ内のrun-tests.sh
を実行すると、様々な条件でのテストが行えるようになっていました。
No | Test Name | テスト内容 | hello1書込み後 | hello2書込み後 | リセット後 |
---|---|---|---|---|---|
1 | GOOD RSA | hello1/hello2ともにRSA2048で署名 | hello1が起動 | hello2が起動 | hello1が起動 |
2 | GOOD ECDSA | hello1/hello2ともにEDSA256で署名 | hello1が起動 | hello2が起動 | hello1が起動 |
3 | OVERWRITE | Overwriting Modeでビルド | hello1が起動 | hello2が起動 | hello2が起動 |
4 | BAD RSA | hello1はRSA2048で署名、hello2はECDSA256で署名 | hello1が起動 | hello1が起動 | hello1が起動 |
5 | BAD ECDSA | hello1はECDSA256で署名、hello2はRSA2048で署名 | hello1が起動 | hello1が起動 | hello1が起動 |
6 | NO BOOTCHECK | Primary Partitionの完全性チェックを行わないモードでビルド | hello1が起動 | hello1が起動 | hello1が起動 |
7 | WRONG RSA | hello2を異なるRSA2048鍵で署名 | hello1が起動 | hello1が起動 | hello1が起動 |
8 | WRONG ECDSA | hello2を異なるECDSA256鍵で署名 | hello1が起動 | hello1が起動 | hello1が起動 |
なお、FRDM-K64Fで実行する場合、スクリプト中のpyocd erase --chip
を実行するとエラーが出て動かなくなるので、この行(8ヶ所)をコメントアウトしておくと実行できます。
ひとまず今回の実験はここまで。