ESP32について調べ始めました。
ESP32ではfreeRTOSが標準で採用されていてexamplesのアプリケーションではmain_app_taskというタスクのアプリケーションが動き始めます。
freeRTOSは初めて触るのですが、電源ONからどのようにESP32/freeRTOSが動くのか分からなかったので調べておきたいと思います。
目次
ブートについて
First Stage Bootloader と Second Stage Bootloader
ESP32の起動フローは以下のページで解説されています。
docs.espressif.com
この解説によれば、First Stage BootloaderはFlashROMの0x1000に書いてあるSecond Stage Bootloaderをメモリ上にロードし実行するそうです。
CPUのリセットベクタのコードはESP32内のマスクROM領域に保存されており変更できないそうです。
PRO CPUと APP CPU
コードを読む際に並行してesp-idfのドキュメントも読んでいると唐突にPRO CPUとAPP CPUという用語が登場します。
こちらのフォーラムのコメントによれば、何のことはないESP32はマルチコアなので
PRO CPU→Core 0
APP CPU→Core 1
の意味でした。
PROはProtocol AppはApplicationの略で、ESP32が非対称な初期化(asymmetric multiprocessor setup)を行う設計に起因しているようです。FreeRTOSではSMPをサポートしているのでPRO、APPという呼称はあまり意識する必要はないかもしれません。ただし、ログ出力では Pro cpu, App cpuといった単語でログ出力が行われている箇所があるので意味を把握しておく必要はあります。
Second Stage Bootloader
Second Stage Bootloaderではオフセット0x8000 に配置されているパーティションテーブルを読み出し、必要に応じてOTA Updateを行うそうです。
ソースコードの場所
esp-idfから提供されるOS・ライブラリ等のソースコードはesp-idf-vX.X.X\components 以下に配置されています。(X.X.Xはインストールしたesp-idfのバージョンで読みかえてください)
Second Stage Bootloaderのコードは components\bootloader\subproject\mainに
- bootloader_start.c
- bootloader_hooks.h
というファイルがあり、同じ階層のldフォルダにはリンカスクリプトが配置されています。
エントリポイント
bootloader.ldでは
/* Default entry point: */ ENTRY(call_start_cpu0);
の記述があり、Second Stage Bootloaderは call_start_cpu0 から始まります。
この関数では bootloader_init という初期化関数を呼び出しています。
bootloader_init の実体はbootloader_support/src以下のCPU種別(esp32,esp32c2,esp32s2,esp32h2...etc)のフォルダ以下のファイルで実装されています。
具体的な処理としてはCPUの初期化などになります。
この後 パーティションテーブルからブートすべきパーティションを選んで bootloader_utility_load_boot_imageにより該当するパーティションをロードしてそちらに処理が移ります。
アプリケーションの起動
ロードされたイメージのエントリポイントは esp_system/port/cpu_start.c で定義されている call_start_cpu0 になります。
Second Stage Bootloader のエントリポイントも call_start_cpu0 という関数名で定義されていたのでこれは正直違う関数名の方が良かったのではという気がします。
call_start_cpu0 ではCPU、HWに関わるいくつかの初期化処理を行ったあとSYS_STARTUP_FNのマクロ関数を呼び出しています。
SYS_STARTUP_FNの実体は esp_system/include/esp_private/startup_internal.h で次のように定義されています。
#define SYS_STARTUP_FN() ((*g_startup_fn[(esp_cpu_get_core_id())])())
ようするにコア毎の初期化用関数ポインタのテーブルに登録されている関数を呼び出しているのですがこの
g_startup_fnの実体は esp_system/startup.cで定義されており
- PRO CPU(core 0): start_cpu0
- APP CPU(core 1〜): start_cpu_other_cores
が登録されています。
start_cpu0,start_cpu_other_coresの実体はそれぞれ、
- start_cpu0_default
- esp_startup_start_app_other_cores_default
で weak pointer として登録されているのでユーザが任意の処理に上書き可能です。
freeRTOSの起動
start_cpu0_default では主にライブラリやC++のコンストラクタの呼び出しなどソフトウェア的な初期化を行っているようです。
本格的にアプリケーションの機能を利用できるようにする前段階という雰囲気があります。
ソフトウェア的な初期化処理が終わったあと、 freertos/app_startup.c に定義されている esp_startup_start_appを呼び出します。
esp_startup_start_appでは
BaseType_t res = xTaskCreatePinnedToCore(main_task, "main", ESP_TASK_MAIN_STACK, NULL, ESP_TASK_MAIN_PRIO, NULL, ESP_TASK_MAIN_CORE);
を実行した後、vTaskStartSchedulerを呼び出しています。vTaskStartSchedulerは文字通りカーネルのスケジューリングを開始するので、ここからfreeRTOSが起動した状態になります。
main_taskはデーモンタスクとして位置づけられています。
ただし、必ずしもこのタスクがループを持って動作し続ける必要がある訳ではありません。他のタスクを起動したらそのまま終了しても特に問題が生じるわけではありません。
まとめ
浅くですが、esp-idfでの起動処理を追いかけてみました。
個人的な印象として他のCPUベンダーのSDKよりオープンで開発者が使いやすいように考えられている印象を受けました。
そもそもSDKをgithubで提供しているというのもあまりない気がします。
CPUベンターが提供する初期化処理やライブラリには時々バグがあってふとしたことでそのバグを踏むことがありますが、報告する場所がオープンになっていなくてそれが本当にバグなのかどうかも疑わないといけない状況だったりします。*1
そういう経験を踏まえるとgithubである程度気軽にISSUEを立てたりPRを出したりできるエコシステムがあるのはとてもありがたいです。
ということで、主にブート周りのコードを読んでみましたが、気が向いたらfreeRTOSの使い方もまとめて記事にするかもしれません。