FRDM-K64F上でwolfBootのOTAを動かしてみた

# はじめに
組込み向け暗号ライブラリを提供しているwolfSSL社のラインナップに、セキュアなOTAが可能なwolfBootというライブラリがあります。IoTの場面で需要が増すであろうSecure OTAの一例として、試しに動かしてみました。
[wolfBoot](https://www.wolfssl.jp/products/wolfboot/)

以下の場所にFRDM-K64Fで動かせるサンプルがあるのでこれを使ってみました。
https://github.com/wolfSSL/wolfBoot-examples/tree/master/freeRTOS-Freescale-K64F-https-TLS1.3

サンプルなのですぐ動くだろうと思っていたら意外と苦労したので、実施内容を記録しておきます（ただし、一番最初に苦労したのはこれ：[FRDM-K64Fの修復（bootloader update)](https://qiita.com/hisashij/items/cf33904d2e2bf2d0d27f)）。

【今回の環境】

- [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
- GNU Arm Embedded Toolchain version 9-2020-q2-update
- MCUXpresso SDK v2.8.2
- wolfBoot v1.6
- wolfSSL v4.5.0
- PicoTCP v1.7.0

# wolfBootの仕組み
自分の理解のために大まかな仕組みの絵を書いてみました。
FlashのアドレスはFRDM-K64F用のサンプルで使われているもので、環境によって調整が必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679422/1b4c1b7d-e5da-adf7-c587-e3e9cdebd97f.png)

# 必要資材の入手
## wolfBootサンプル
githubからサンプルコード一式を持ってきます。

```
cd ~
git clone https://github.com/wolfSSL/wolfBoot-examples.git
```

サンプルは以下の構成になっていますが、今回はこの中の`freeRTOS-Freescale-K64F-https-TLS1.3`を使います。使い方はこの中の`README.md`にざっくり書いてあります。

```
wolfBoot-examples
 ├ contiki-nrf52
 ├ freeRTOS-Freescale-K64F-https-TLS1.3
 │  ├ freeRTOS
 │  ├ picotcp  
 │  ├ png
 │  └ src
 ├ riotOS-samr21
 └ wolfBoot
 　 └ lib
 　 　 └ wolfssl
```

## wolfBoot
サンプルの中に`wolfBoot`というディレクトリがありますが、中身が空だったのでgithubからwolfBootを入手してここに設置します。

```
cd ~
git clone https://github.com/wolfSSL/wolfBoot.git
mv wolfBoot wolfBoot-examples/
```

## wolfSSL
wolfBootの中でwolfSSLを使いますが、中身が空だったのでgithubからwolfSSLを入手して設置します。

```
cd ~
git clone https://github.com/wolfSSL/wolfssl.git
mv wolfssl wolfBoot-examples/wolfBoot/lib/
```

## PicoTCP
このサンプルはTCP/IP stackとしてPicoTCPを使いますが、こちらもディレクトリの中身が空だったのでgithubから入手して設置します。

```
cd ~
git clone https://github.com/tass-belgium/picotcp.git
mv picotcp wolfBoot-examples/freeRTOS-Freescale-K64F-https-TLS1.3/
```

## FRDM-K64F用MCUXpresso SDK
ボード用のSDKも必要です。NXP社のSDK Builderからzipでダウンロードして展開しておきます。
[SDK Builder](https://mcuxpresso.nxp.com/en/builder)

```
cd ~
unzip SDK_2.8.2_FRDM-K64F.zip
```

## Toolchain
ToolchainはArm社のページからダウンロードして展開して、パスを通しておきます。
[GNU Arm Embedded Toolchain Downloads](https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads)

```
tar -xf gcc-arm-none-eabi-9-2020-q2-update-x86_64-linux.tar.bz2
echo export PATH='~/gcc-arm-none-eabi-9-2020-q2-update/bin:"$PATH"' >> ~/.bashrc
source ~/.bashrc
```

## cu
Ubuntu上でシリアル出力を確認するためにcuコマンドの設定もしておきました。
設定にあたってはこちらを参考に。
[LinuxPCでserial通信をしようとした話](https://qiita.com/sttn/items/567c9f49b88ff275b51a)

```
sudo apt-get install cu
ls -l /dev/serial/by-id/
cu -s 115200 -l /dev/ttyACM0
```

# ビルド → 一部修正
これでビルドできると思いきや、コンパイルエラーが出たりしたのでいくつか修正しました。

```
cd ~/wolfBoot-examples/freeRTOS-Freescale-K64F-https-TLS1.3/
make KINETIS=~/SDK_2.8.2_FRDM-K64F
```

## MCUXpresso SDK API呼出しの修正
そのままだと以下のコンパイルエラーが発生。

```
src/pico_enet_kinetis.c: In function 'enet_poll':
src/pico_enet_kinetis.c:167:13: error: too few arguments to function 'ENET_GetRxFrameSize'
  167 |         if (ENET_GetRxFrameSize(&g_handle, &size) == kStatus_ENET_RxFrameEmpty)
      |             ^~~~~~~~~~~~~~~~~~~
src/pico_enet_kinetis.c:169:9: error: too few arguments to function 'ENET_ReadFrame'
  169 |         ENET_ReadFrame(enet->base, &g_handle, g_frame, size);
      |         ^~~~~~~~~~~~~~
src/pico_enet_kinetis.c: In function 'enet_send':
src/pico_enet_kinetis.c:179:9: error: too few arguments to function 'ENET_SendFrame'
  179 |     if (ENET_SendFrame(enet->base, &g_handle, buf, len) != kStatus_ENET_TxFrameBusy)
      |         ^~~~~~~~~~~~~~
```

ENET_???はMCUXpresso SDKのAPI。API Reference Manualを見ると、multiple-ringサポートのためにringIdを指定する引数が追加された模様。Single ringの場合は0とかNULLとかを追加すれば良いようなので、上記3ヶ所の関数呼出しを修正。

```（修正前）src/pico_enet_kinetis.c
ENET_GetRxFrameSize(&g_handle, &size)
ENET_ReadFrame(enet->base, &g_handle, g_frame, size)
ENET_SendFrame(enet->base, &g_handle, buf, len)
```

```（修正後）src/pico_enet_kinetis.c
ENET_GetRxFrameSize(&g_handle, &size, 0)
ENET_ReadFrame(enet->base, &g_handle, g_frame, size, 0, NULL)
ENET_SendFrame(enet->base, &g_handle, buf, len, 0, false, NULL)
```

## wolfBoot本体のコンパイルエラー対応
wolfBoot本体のmakeでエラーが出てしまいました。MCUXpresso SDKのインクルードファイルが読み込めていません。

```
hal/kinetis.c:25:10: fatal error: fsl_common.h: No such file or directory
   25 | #include "fsl_common.h"
      |          ^~~~~~~~~~~~~~
```

wolfBoot本体のMakefileから参照される`../wolfBoot/arch.mk`に、target別のコンパイルオプションが定義されていますが、そこで参照している変数が定義されていないことが原因。これら環境変数はサンプルの`src/wolfboot.config`に定義しておくのが良さそうなので、以下の記述を追記。

```（追記内容）src/wolfboot.config
MCUXPRESSO?=$(KINETIS)
MCUXPRESSO_DRIVERS?=$(MCUXPRESSO)/devices/MK64F12
MCUXPRESSO_CMSIS?=$(MCUXPRESSO)/CMSIS
MCUXPRESSO_CPU?=MK64FN1M0VLL12
```

## デバッグメッセージの追加
動かしたときに処理がどう進んでいるかわからなかったので、シリアルポートにデバッグメッセージを出すようにしました。参考にしたのはMCUXpresso SDKに付属のデモアプリ`hello_world`。

`main()`関数の初期フェーズで`BOARD_InitDebugConsole()`を実行します。`BOARD_InitDebugConsole()`の実体はMCUXpresso SDKの`devices\MK64F12\project_template\board.c`から該当部分だけコピー。
これで、任意の場所に`PRINTF("message");`と書くことでシリアルポートにメッセージ出力できるようになりました。

```（追記後）src/main.c
#include "fsl_debug_console.h"
(...snip...)
void BOARD_InitDebugConsole(void)
{
    uint32_t uartClkSrcFreq = BOARD_DEBUG_UART_CLK_FREQ;
    DbgConsole_Init(BOARD_DEBUG_UART_INSTANCE, BOARD_DEBUG_UART_BAUDRATE, BOARD_DEBUG_UART_TYPE, uartClkSrcFreq);
}

int main(void) {
    BOARD_InitPins();
    BOARD_BootClockRUN();
    BOARD_InitDebugConsole();
    PRINTF("main(): START\r\n");
(...snip...)
}
```

このDebugConsoleの仕組みを使うためには、インクルードディレクトリやオブジェクトファイルの追加が必要なので、Makefileの該当箇所に以下の通り追記。

```（追記前）Makefile.
CFLAGS+=-I$(KINETIS_DRIVERS)/drivers -I$(KINETIS_DRIVERS) -DCPU_MK64FN1M0VLL12 -I$(KINETIS_CMSIS)/Include -I$(PHY) -DDEBUG_CONSOLE_ASSERT_DISABLE=1
(...snip...)
OBJS:= \
  $(KINETIS_DRIVERS)/drivers/fsl_clock.o \
(...snip...)
```

```（追記後）Makefile.
CFLAGS+=-I$(KINETIS_DRIVERS)/drivers -I$(KINETIS_DRIVERS) -DCPU_MK64FN1M0VLL12 -I$(KINETIS_CMSIS)/Include -I$(PHY) -DDEBUG_CONSOLE_ASSERT_DISABLE=1
CFLAGS+=-DSDK_DEBUGCONSOLE=1 -DPRINTF_FLOAT_ENABLE=0 -DSERIAL_PORT_TYPE_UART=1 -I$(KINETIS)/components/uart -I$(KINETIS)/components/serial_manager -I$(KINETIS_DRIVERS)/utilities/str -I$(KINETIS_DRIVERS)/utilities/debug_console
(...snip...)
OBJS:= \
  $(KINETIS)/components/uart/uart_adapter.o \
  $(KINETIS)/components/serial_manager/serial_manager.o \
  $(KINETIS)/components/serial_manager/serial_port_uart.o \
  $(KINETIS_DRIVERS)/utilities/str/fsl_str.o \
  $(KINETIS_DRIVERS)/utilities/debug_console/fsl_debug_console.o \
  $(KINETIS_DRIVERS)/drivers/fsl_uart.o \
  $(KINETIS_DRIVERS)/drivers/fsl_clock.o \  
(...snip...)
```

## make cleanの修正
理由は分かってないのですが、一部を修正してmakeするとリンク時に`ld: error: image.elf uses VFP register arguments, ???.o does not`というエラーが出てしまうため、ビルド時には一旦すべてのオブジェクトファイルを削除するのが良さそう。
`make KINETIS=~/SDK_2.8.2_FRDM-K64F clean`としただけだと、MCUXpresso SDK内にできたオブジェクトファイルが削除されなかったため、以下の通り修正。

```（追記前）Makefile.
clean:
        rm -f *.bin *.elf $(OBJS) wolfboot.map *.bin  *.hex src/*.o freeRTOS/*.o $(FREERTOS_PORT)/*.o *.map tags
```

```（追記後）Makefile.
clean:
        rm -f *.bin *.elf $(OBJS) wolfboot.map *.bin  *.hex src/*.o freeRTOS/*.o $(FREERTOS_PORT)/*.o *.map tags
        rm -f $(PHY)/*.o $(KINETIS_DRIVERS)/drivers/*.o $(KINETIS)/components/uart/*.o $(KINETIS)/components/serial_manager/*.o $(KINETIS_DRIVERS)/utilities/str/*.o $(KINETIS_DRIVERS)/utilities/debug_console/*.o
```

# OTA実験
## ビルド
ようやくビルドが通るようになりました。
実験前にもう一つ、IPアドレスの修正が必要です。

```（修正前）src/main.c
    pico_string_to_ipv4("192.168.178.211", &addr.addr);
    pico_string_to_ipv4("255.255.255.0", &mask.addr);
    pico_string_to_ipv4("192.168.178.1", &gw.addr);
```

デフォルトでは`192.168.178.211`を使うようになっているので、ここを利用するNWに合わせて修正してから、ビルド。

```
cd ~/wolfBoot-examples/freeRTOS-Freescale-K64F-https-TLS1.3/
make KINETIS=~/SDK_2.8.2_FRDM-K64F
```

ビルドに成功すると以下のファイルができます。

```
wolfboot-align.bin    ... wolfBoot Bootloader
image.bin             ... firmwareイメージ（署名前）
image_v1_signed.bin   ... sign.pyで署名入りヘッダを付加したもの(v1)
image_v2_signed.bin   ... sign.pyで署名入りヘッダを付加したもの(v2)
factory.bin           ... Flashに書き込むためにwolfboot-align.binとimage_v1_signed.binをcatでつなげたもの
```

以降の実験用に、v3,v4のイメージも作っておきました。

## firmware v1を直接書込み、起動

```
cp factory.bin /media/???/DAPLINK/
```

リセットスイッチを押すと、シリアルにメッセージが表示されました。うまく動いているようです。

```
main(): START
main(): PicoTask is Created
main(): MainTask is Created
```

Firefoxでhttpsアクセスすると画面が表示されました（理由は不明ですがChromeやEdgeではアクセスできず）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679422/695cd32d-4f40-44a9-11a2-718ed1becef4.png)

## firmware v2をOTA書込み
次はWeb画面から`image_v2_signed.bin`をアップロードします。
以下はシリアルに出力された処理の進行状況。アップロードが終わるまで3分ほどかかります。

```
https_request(): START
https_request(): "POST " is detected
parse_update(): START
parse_update(): filename="image_v2_signed.bin"
parse_update(): Start to write to flash. Size:    3641C
parse_update(): Writing...     800
parse_update(): Writing...    1000
parse_update(): Writing...    1800
(...snip...)
parse_update(): Writing...   35800
parse_update(): Writing...   36000
parse_update(): Writing...   3641C
parse_update(): Finish to write to flash. Rebooting...
main(): START
main(): PicoTask is Created
main(): MainTask is Created
https_request(): START
https_request(): "GET /" is detected
set_version(): FW version: 2
https_request(): END
```

アップロード後、リブートの所要時間は15秒ほど。この間にBoot PartitionとUpdate Partitionの内容を交換しているものと思われます。
リブート後はバージョンが2に上がってます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679422/512b9a57-d630-35ab-e984-10180e13ae7e.png)

## firmware v3は一部改ざんしてからOTA書込みしてみる
`image_v3_signed.bin`については、バイナリエディタで中身を一部を改ざんしてからアップロードしてみました。

```
https_request(): START
https_request(): "POST " is detected
parse_update(): START
parse_update(): filename="image_v3_signed.bin"
parse_update(): Start to write to flash. Size:    3641C
parse_update(): Writing...     800
parse_update(): Writing...    1000
parse_update(): Writing...    1800
(...snip...)
parse_update(): Writing...   35800
parse_update(): Writing...   36000
parse_update(): Writing...   3641C
parse_update(): Finish to write to flash. Rebooting...
main(): START
main(): PicoTask is Created
main(): MainTask is Created
https_request(): START
https_request(): "GET /" is detected
set_version(): FW version: 2
https_request(): END
```

今度はリブートが一瞬で終わり、バージョンは２のままです。
Update Partitionの署名検証に失敗したことで、Boot Partitionを交換せずに起動されたようです。

## 改めてfirmware v1をOTA書込みしてみる
これも試しにやってみました。

```
https_request(): START
https_request(): "POST " is detected
parse_update(): START
parse_update(): filename="image_v1_signed.bin"
parse_update(): Start to write to flash. Size:    3641C
parse_update(): Writing...     800
parse_update(): Writing...    1000
parse_update(): Writing...    1800
(...snip...)
parse_update(): Writing...   35800
parse_update(): Writing...   36000
parse_update(): Writing...   3641C
parse_update(): Finish to write to flash. Rebooting...
main(): START
main(): PicoTask is Created
main(): MainTask is Created
https_request(): START
https_request(): "GET /" is detected
set_version(): FW version: 2
https_request(): END
```

今度もリブートが一瞬で終わり、バージョンは２のまま。
バージョン番号が戻っていないかどうかも見ているようです。

## 最後にfirmware v4をOTA書込み

```
https_request(): START
https_request(): "POST " is detected
parse_update(): START
parse_update(): filename="image_v4_signed.bin"
parse_update(): Start to write to flash. Size:    3641C
parse_update(): Writing...     800
parse_update(): Writing...    1000
parse_update(): Writing...    1800
(...snip...)
parse_update(): Writing...   35800
parse_update(): Writing...   36000
parse_update(): Writing...   3641C
parse_update(): Finish to write to flash. Rebooting...
main(): START
main(): PicoTask is Created
main(): MainTask is Created
https_request(): START
https_request(): "GET /" is detected
set_version(): FW version: 4
https_request(): END
```

バージョン2からバージョン4へ、無事にアップデートが行われました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679422/587ac9b7-8484-f59d-1e38-0b77e8676ea4.png)