FRDM-K64F上でZephyr OSのmcubootを試してみる

# はじめに
色々な環境のOTA（Firmware Update）を試しています。
今回はZephyr OSで使われているmcubootのOTA。
取りあえず付属のサンプルをFRDM-k64Fで動かしてみただけで、実はオンラインアップデートにもなってませんが、記録を残しておきます。

【今回の環境】

- [FRDM-K64F](https://www.nxp.com/design/development-boards/freedom-development-boards/mcu-boards/freedom-development-platform-for-kinetis-k64-k63-and-k24-mcus:FRDM-K64F)
- Ubuntu 20
- [Zephyr](https://www.zephyrproject.org/) v2.4.99
- Zephyr SDK v0.11.4
- [mcuboot](https://mcuboot.com/) v1.6.0

# mcubootについて

mcubootの仕組みは以下に説明があります。
https://mcuboot.com/mcuboot/design.html
自分の理解のために一部ですが和訳しました。
[Design document of mcuboot](https://qiita.com/hisashij/items/deed9ebbc81860616ab6)

上記にかなり細かく説明が書かれていますが、この説明だけではわからない点もあり、以下は実機で確認した内容も含みます。

## 全体的な仕組み
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679422/8bbbdef4-5340-add6-8c48-a2d6dadfa36a.png)

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](https://docs.zephyrproject.org/latest/getting_started/index.html)

## 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
```

ビルドが終わると、以下３つのファイルが作られました。

| ファイル | サイズ | 内容 |
|:--|--:|:--|
| 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ヶ所）をコメントアウトしておくと実行できます。

ひとまず今回の実験はここまで。