はじめに
組込み向け暗号ライブラリを提供しているwolfSSL社のラインナップに、セキュアなOTAが可能なwolfBootというライブラリがあります。IoTの場面で需要が増すであろうSecure OTAの一例として、試しに動かしてみました。
wolfBoot
以下の場所にFRDM-K64Fで動かせるサンプルがあるのでこれを使ってみました。
https://github.com/wolfSSL/wolfBoot-examples/tree/master/freeRTOS-Freescale-K64F-https-TLS1.3
サンプルなのですぐ動くだろうと思っていたら意外と苦労したので、実施内容を記録しておきます(ただし、一番最初に苦労したのはこれ:FRDM-K64Fの修復(bootloader update))。
【今回の環境】
- 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用のサンプルで使われているもので、環境によって調整が必要です。
必要資材の入手
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
cd ~
unzip SDK_2.8.2_FRDM-K64F.zip
Toolchain
ToolchainはArm社のページからダウンロードして展開して、パスを通しておきます。
GNU Arm Embedded Toolchain 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通信をしようとした話
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ヶ所の関数呼出しを修正。
ENET_GetRxFrameSize(&g_handle, &size)
ENET_ReadFrame(enet->base, &g_handle, g_frame, size)
ENET_SendFrame(enet->base, &g_handle, buf, len)
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
に定義しておくのが良さそうなので、以下の記述を追記。
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");
と書くことでシリアルポートにメッセージ出力できるようになりました。
# 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の該当箇所に以下の通り追記。
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...)
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内にできたオブジェクトファイルが削除されなかったため、以下の通り修正。
clean:
rm -f *.bin *.elf $(OBJS) wolfboot.map *.bin *.hex src/*.o freeRTOS/*.o $(FREERTOS_PORT)/*.o *.map tags
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アドレスの修正が必要です。
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ではアクセスできず)。
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に上がってます。
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
今度はリブートが一瞬で終わり、バージョンは2のままです。
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
今度もリブートが一瞬で終わり、バージョンは2のまま。
バージョン番号が戻っていないかどうかも見ているようです。
最後に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へ、無事にアップデートが行われました。