IoT を使ってみる(その17:ESP32 OTAの基本を徹底解説:フラッシュメモリ構成から動作の仕組みまで)

| 9 min read
Author: shuichi-takatsu shuichi-takatsuの画像

前回は、ESP32 を使って "OTA (Over The Air)" に挑戦しました。
順番は前後しましたが、今回は OTA の基本的な仕組みについて、改めて詳しく解説します。

はじめに

#

ESP32は、無線経由でプログラムを書き換える「Over The Air(OTA)」機能を標準サポートしています。
本記事では、フラッシュメモリの構成OTAの動作の仕組みに踏み込み、「なぜ2つのアプリ領域が必要なのか?」「どうやって更新後のアプリを選んで起動しているのか?」といった疑問に答えます。

OTAを支える仕組み

#

フラッシュメモリの構成

#

ESP32のフラッシュメモリは「パーティションテーブル」で管理されています。
パーティションとは、フラッシュメモリ上の領域の「区切り」です。

PlatformIOで「Arduinoフレームワーク」を指定している場合、パーティションテーブル情報は以下のフォルダに含まれています。
<ユーザーフォルダパス>\.platformio\packages\framework-arduinoespressif32\tools\partitions

上記のフォルダの中に default.csv ファイルが格納されています。
このファイルにデフォルトで選択されるパーティションテーブル情報が格納されています。
私の環境では以下のように設定されていました。

Name Type SubType Offset Size Flags
nvs data nvs 0x9000 0x5000
otadata data ota 0xe000 0x2000
app0 app ota_0 0x10000 0x140000
app1 app ota_1 0x150000 0x140000
spiffs data spiffs 0x290000 0x160000
coredump data coredump 0x3F0000 0x10000

各パーティションの役割は以下のとおりです。

  • nvs : NVSストレージ(Non-Volatile Storage)。設定やユーザーデータなどの永続的な保存領域。電源が切れてもデータが保持されます。
  • otadata :OTA(Over-The-Air)アップデートの状態を管理。どちらのアプリケーション領域(ota_0またはota_1)を起動するかを示します。
  • app0 / app1 :OTAアップデート用のアプリケーション領域。OTA更新時には、app0 と app1 が交互に使用されます。一方が実行中の場合、もう一方が更新対象となります。
  • spiffs : SPIFFS(Serial Peripheral Interface Flash File System)を使用したファイル保存領域。設定ファイルやログなどのデータを保存します。
  • coredump : コアダンプ領域。システム障害(クラッシュなど)発生時にメモリダンプを保存し、後で障害解析に使用されます。

また、フラッシュメモリマップ は以下のようになります。
アプリ領域は OTA に対応するため「2つ(app0 と app1)」用意されており、交互に書き換えて使用されます。

それぞれの領域に対応したアプリ、データが書き込まれます。
なお、OTAにより新しいアプリを書き込む際、現在実行中の領域とは別のスロット(app0またはapp1)に書き込むことで、失敗時に前の状態へ戻すフェイルセーフ機構が働きます。

開始アドレス 終了アドレス サイズ 名前 タイプ サブタイプ 備考
0x00000000 0x00007FFF 0x8000 bootloader - - ブートローダー
0x00008000 0x00008FFF 0x1000 partition - - パーティションテーブル
0x00009000 0x0000DFFF 0x5000 nvs data nvs NVSストレージ
0x0000E000 0x0000FFFF 0x2000 otadata data ota OTAデータ
0x00010000 0x0014FFFF 0x140000 app0 app ota_0 OTAスロット 0
0x00150000 0x0028FFFF 0x140000 app1 app ota_1 OTAスロット 1
0x00290000 0x003EFFFF 0x160000 spiffs data spiffs SPIFFSファイルシステム
0x003F0000 0x003FFFFF 0x10000 coredump data coredump コアダンプ領域

(注:Flash全体は通常 4MB(= 0x400000)を想定)

Arduinoフレームワーク含むESP32用開発環境では、パーティションテーブル自体(partition-table.bin)は常にフラッシュの 0x8000 に書き込まれるのがデフォルトです。

ブートローダーの動き

#

Arduinoフレームワークを前提にしますが、ESP32の起動時、まずROM内蔵の第1ブートローダーが動作します。
その後、フラッシュに書き込まれたユーザー定義の bootloader.bin を読み込んで処理を続行します。

  • UARTブートの確認(開発モード)
    最初に、UART(シリアル)経由での書き込みモードが要求されていないかを確認します。これにより、PCからの書き込み要求があればファームウェアの書き換えに移行します。

  • フラッシュメモリの初期化とパーティションテーブルの読み込み
    フラッシュメモリの先頭(0x0000)にはブートローダーが書き込まれており、ブートローダーは0x8000 にあるパーティションテーブルを読み込んで、アプリケーションの起動処理を行います。

  • otadata の読み取り(OTA構成時)
    otadata パーティション(デフォルトでは 0xE000)から、最後に正常に起動したアプリケーションの情報や、次回起動すべきアプリケーションの情報を読み取ります。

  • 適切なアプリケーション領域を決定
    読み取った情報に基づいて、通常は ota_0 または ota_1 のどちらかのアプリケーション領域を選択します。

  • フォールバック(代替)処理
    otadata に有効な情報がない場合、またはアプリケーション領域の検証に失敗した場合、以下のようなフォールバック動作が行われます。

    • factory パーティション(※後述)が存在すれば、そちらを起動。
    • それもなければ、ブート失敗として停止、またはリセット。
  • アプリケーションのロードと実行
    選択されたアプリケーションのフラッシュ領域から、コードをRAMにロードし、実行を開始します。

factory パーティションについての補足です。

  • Arduinoでは標準で使用されないことが多いですが、OTA機能を使わない構成では factory のみを使うことも可能です。
  • factory パーティションはデフォルトのアプリケーションとして動作します。
  • OTA構成では、ota_0 / ota_1 による二重構成が推奨され、factory は省略される場合もあります。

OTAの流れ

#

ESP32のOTA(Over-The-Air)によるアップデートは、以下のようなステップで進行します。

  • 新しいファームウェアを受信
    Wi-FiやHTTP経由で、新しい .bin ファイル(ファームウェア)を受信します。

  • 書き込み先の決定
    現在動作していない方のアプリ領域(たとえば、現在 ota_0 で動作中なら ota_1)を選び、そこにファームウェアを書き込みます。

  • 書き込み完了後、otadataを更新
    書き込みが成功すると、次回の起動で新しい領域が使われるように otadata セクションが更新されます。

  • 再起動して新ファームウェアを実行
    デバイスを再起動し、新しいファームウェアを使って起動します。

  • 起動失敗時のロールバック
    起動後に問題が検出された場合、自動的に以前の正常なファームウェア領域にロールバックする仕組みがあります。

なぜ2スロットなのか?

#

OTAアップデートは「失敗する可能性」がある操作です。電源断やネットワーク切断により、バイナリの一部しか書き込めなかった場合でも、前のアプリが残っていれば復旧できます。

このように「現在のアプリを残したまま、新しいアプリを書き込む」という冗長性が、OTAの信頼性を高めています。
これはファームウェア更新において極めて重要な設計思想です。

よくある誤解とトラブル例

#

「factory領域が使われない」問題

#

一度OTAを実施すると、以降は基本的にota_0ota_1の間で切り替えが続きます。
factory領域は、あくまで「初回起動時」または「非常時のリカバリー用途」として使用されます。

「パーティション足りない」問題

#

OTAを利用する場合は、2つのOTAスロット(ota_0 と ota_1) のための領域を確保する必要があります。
領域が不足していると、OTAの更新用ファームを書き込むことができず、エラーになります。

「起動ループ」問題

#

新しいファームウェアを書き込んでも、起動に失敗するとリブートループに陥る場合があります。
これを防ぐためには、アプリケーションの検証とロールバック処理を設計に組み込むことが推奨されます。
ESP-IDFの以下の関数が有効です。(この関数は Arduinoフレームワークでも利用できます)

esp_ota_mark_app_valid_cancel_rollback();

上記の関数をアプリケーションが「正常に起動・動作した」と判断できるタイミングで呼び出すことで、ロールバックのフラグを解除し、以降の起動時にも現在のファームウェアが使用され続けます。

ただし、次の条件が満たされている必要があります。

  • Partition Table が OTA対応(ota_0 / ota_1 含む)
    default_ota.csv やカスタムで2スロット分あること。

  • esp_ota_set_boot_partition() などで書き換えた後に、再起動していること
    アプリ起動後、検証フェーズ中に esp_ota_mark_app_valid_cancel_rollback() を呼ぶ必要があります。

サンプルプログラム

前回作成したプログラムに esp_ota_mark_app_valid_cancel_rollback() を組み込んでみました。
追加したソースコードは以下の2点です。

#include "esp_ota_ops.h"
  if (WiFi.waitForConnectResult() == WL_CONNECTED) {
    esp_ota_mark_app_valid_cancel_rollback();
    Serial.println("Marked as valid, rollback canceled");
  }

ロールバック処理を組み込んだ完成版プログラムは以下です。

#include <Arduino.h>

#include <WiFi.h>
#include <WebServer.h>
#include <Update.h>
#include <ArduinoOTA.h>
#include "esp_ota_ops.h"

const char* ssid = "YOUR_SSID";         // 各自のWifi環境のSSIDを指定する
const char* password = "YOUR_PASSWORD"; // 各自のWifi環境のPasswordを指定する

WebServer server(80);

const char* upload_html = R"rawliteral(
<form method='POST' action='/update' enctype='multipart/form-data'>
  <input type='file' name='update'>
  <input type='submit' value='Update'>
</form>
)rawliteral";

// アップデート成功後にリダイレクトするHTML
const char* update_success_html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="refresh" content="5; url=/" />
</head>
<body>
  <h1>Update Successful! Rebooting...</h1>
  <p>You will be redirected to Home page in 5 seconds.</p>
</body>
</html>
)rawliteral";

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  // WiFi接続完了後など、安全なタイミングで呼ぶ
  if (WiFi.waitForConnectResult() == WL_CONNECTED) {
    esp_ota_mark_app_valid_cancel_rollback();
    Serial.println("Marked as valid, rollback canceled");
  }

  server.on("/", HTTP_GET, []() {
    server.send(200, "text/html", upload_html);
  });

  server.on("/update", HTTP_POST, []() {
    server.send(200, "text/html", update_success_html);  // アップデート成功後にリダイレクトHTMLを返す
    delay(1000);  // メッセージを表示するための時間
    ESP.restart();  // ESP32をリセット
  }, []() {
    HTTPUpload& upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
      Serial.printf("Update: %s\n", upload.filename.c_str());
      if (!Update.begin()) {
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_WRITE) {
      if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_END) {
      if (Update.end(true)) {
        Serial.println("Update complete");
      } else {
        Update.printError(Serial);
      }
    }
  });

  ArduinoOTA.setPassword("admin");  // <- platform.iniに指定した password と合わせる

  ArduinoOTA.begin();

  server.begin();
}

void loop() {
  ArduinoOTA.handle();
  server.handleClient();
}

プログラムが起動すると、以下のようなログが出力されます。

.WiFi connected
IP address: 192.168.0.65
Marked as valid, rollback canceled

実運用に向けたアドバイス

#
  • OTAは小さな差分ではなくファーム全体を書き換えるので、ファームサイズを意識する
  • 受信エラーに備えてリトライ回数タイムアウト設定を慎重に設計する
  • アプリにバージョン番号を持たせ、OTA対象と現行ファームの互換性チェックを行う
  • 工場出荷状態に戻せるよう、リセットモード(ボタン長押しなど) を用意する

デフォルト以外のパーティションテーブル

#

最新のパーティションテーブル情報は次のリポジトリにある tools/partitions/ フォルダで確認します。

デフォルトパーティションテーブル以外で、代表的なパーティションテーブルを紹介します。

ファイル名 内容
minimal.csv 最小構成(SPIFFSなし、OTAなし)
no_ota.csv OTA機能なし(アプリ1つだけ)
huge_app.csv 巨大なアプリ向け(アプリ領域を最大化)
minimal_spiffs.csv 最小SPIFFSあり(最小限ファイルシステムも欲しい場合)

platformio.ini での指定方法

#

デフォルトパーティションテーブル以外のパーティションデーブルを使用する場合、platformio.ini ファイルに以下のように設定します。
(例は huge_app.csv

board_build.partitions = huge_app.csv

以前、ESP32のカメラ付き開発キット(ESP32-WROVER-E)を使っていた時、カメラモジュールを組み込んだプログラムはサイズがかなり大きくなりました。
パーティションテーブルのアプリ領域を大きくする必要があったことから、何も知らずに「huge_app.csv」を使ってドツボにハマった経験があります。
ちなみに huge_app.csv のパーティションテーブルは以下のようになっています。

Name Type SubType Offset Size Flags
nvs data nvs 0x9000 0x5000
otadata data ota 0xe000 0x2000
app0 app ota_0 0x10000 0x300000
spiffs data spiffs 0x310000 0xE0000
coredump data coredump 0x3F0000 0x10000

ota_0 しか定義がなく、ota_1がありません。
このため、OTA すると失敗します。

まとめ

#

ESP32のOTAは、仕組みを理解すればそれほど難しくありません。
ポイントは「パーティション設計」と「エラーリカバリ設計」です。
しっかり作り込めば、安定して無線アップデートができる強力なシステムになります。

IoTに関するチュートリアルや実践テクニックをまとめています。

IoT活用の参考になれば幸いです。

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。