Web Serial API+ESP32(Arduino)研究 その3 ダウンロードモード仕様

概要

前回は疎通確認までしました。今回はダウンロードモードのプロトコルについて調べてみたいと思います。

参考実装

本家esptool

本家転送ツールです。ここのコードが一番正しいので目を通す必要があります。

Adafruit Web Serial実装

esptoolをAdafruitがWeb Serial APIを利用して実装したものです。なんとなく動いているのですが、若干動きが怪しいです。特に速度設定周りができていないので115200限定でアップロードが遅いです。

つくりっち

そーたメイさんが作っているつくるっちのコードになります。非常に綺麗にまとまっています。ファイルの書き込みに限定するとこれが一番参考になると思います。

mbed

C言語ですが、ESP8266に書き込む最小限のコードがあります。一番小さいのでこのコードで概要を把握してから他のものを見たほうがよいと思います。

esp-serial-flasher

ESP32、Raspberry Pi、STM32でのC言語での転送方法を実装した本家実装例です。esptoolとはまったく実装が違う気がしますが、すっきりして読みやすいところもあります。

typedef struct __attribute__((packed))
{
    uint8_t direction;
    uint8_t command;    // One of command_t
    uint16_t size;
    uint32_t checksum;
} command_common_t;

typedef struct __attribute__((packed))
{
    command_common_t common;
    uint32_t erase_size;
    uint32_t packet_count;
    uint32_t packet_size;
    uint32_t offset;
    uint32_t encrypted;
} begin_command_t;

こんな感じでC言語なんでパケットの構造が定義されているのでわかりやすいところもあります。

ESP32のブートローダーの仕様

ESP32は起動時にGPIO0を確認し、LOWの場合ダウンロードモードで起動します。ダウンロードモードではファームウエアの転送などの最低限のことが実装されています。ただしEraseなどのコマンドはブートローダーROMにははいっていません。

  1. GPIO0をLOWにする
  2. ハードウエアリセットする
  3. ダウンロードモードで起動する
  4. 115200で接続する
  5. SYNCパケットを投げて起動を確認する
  6. レジスタを読み込んでチップを確定させる
  7. 拡張コマンドを実装したStubをメモリに転送する
  8. 指定した速度に変更するコマンドを投げる

上記のような動きを最初にしています。このStubが重要で、ROMに無い機能を拡張しています。またフラッシュの書き込みなどでもROMはバッファサイズが小さいので拡大しているようでした。幸いなことにStubのコードは公開されているので、ダウンロードモードの解析がしやすくなります。

上記などが重要なファイルなのですが、コマンドの一覧について記述があります。

/* Full set of protocol commands */
typedef enum {
  /* Commands supported by the ESP8266 & ESP32 bootloaders */
  ESP_FLASH_BEGIN = 0x02,
  ESP_FLASH_DATA = 0x03,
  ESP_FLASH_END = 0x04,
  ESP_MEM_BEGIN = 0x05,
  ESP_MEM_END = 0x06,
  ESP_MEM_DATA = 0x07,
  ESP_SYNC = 0x08,
  ESP_WRITE_REG = 0x09,
  ESP_READ_REG = 0x0a,

  /* Commands supported by the ESP32 bootloader */
  ESP_SPI_SET_PARAMS = 0x0b,
  ESP_PIN_READ = 0x0c, /* ??? */
  ESP_SPI_ATTACH = 0x0d,
  ESP_SPI_READ = 0x0e,
  ESP_SET_BAUD = 0x0f,
  ESP_FLASH_DEFLATED_BEGIN = 0x10,
  ESP_FLASH_DEFLATED_DATA = 0x11,
  ESP_FLASH_DEFLATED_END = 0x12,
  ESP_FLASH_VERIFY_MD5 = 0x13,

  /* Stub-only commands */
  ESP_ERASE_FLASH = 0xD0,
  ESP_ERASE_REGION = 0xD1,
  ESP_READ_FLASH = 0xD2,
  ESP_RUN_USER_CODE = 0xD3,

  /* Flash encryption debug mode supported command */
  ESP_FLASH_ENCRYPT_DATA = 0xD4,
} esp_command;

ESP8266のときには0x02から0x0aまでのコマンドが内蔵されており、ESP32では0x13まで拡張されています。Stubはすべてのコマンドを拡張していますのでESP8266でもESP32と同じコマンドが利用できるようになっています。

0xD0以降がStubのみで実装されているコマンドであり、EraseやRead FlashはStubを転送しないかぎり利用することができません。

パケットの構造

データ用途
C0Start Byte
00Send
??Command
00Data Size
00Data Size2
00Checksum
00Checksum2
00Checksum3
00Checksum4
C0Stop Byte

上記が最低限のパケット構造です。0xC0から開始して、次が送信か受信かの種別になります。00がPCからの送信で、01がESP32からの受信になります。コマンド種別は上にでていた番号を入れます。データサイズはこのしたのChecksumとStop Byteの間に入っているデータサイズです。この例だと0なので入っていません。チェックサムは0xefが初期値でstate ^= data[i]みたいな感じで計算します。単なるコマンドの場合には0固定で、データ送信系のみ使っているようでした。最後は0xC0で終わります。ヘッダ9バイト、フッタ1バイトの10バイトにデータサイズが追加されたものがパケットのフォーマットになります。

レジスタ読み出し例

データ用途
C0Start Byte
00Send
0ACommand
04Data Size
00Data Size2
00Checksum
00Checksum2
00Checksum3
00Checksum4
78Address
00Address2
00Address3
60Address4
C0Stop Byte

レジスタを呼び出す場合の例です。コマンドは0x0AのESP_READ_REGですね。データサイズは4になります。0x04, 0x00なのでリトルエンディアンで指定します。チェックサムは0固定ですね。アドレスは0x60000078になります。

データ備考
C0Start Byte
01Receive
0ACommand
04Data Size
00Data Size2
00value
25value2
12value3
15value4
00data
00data2
00data3
00data4
C0Stop Byte

返却データです。valueに戻り値が入っています。0x15122500ですね。

    SLIP_send_frame_data(error);
    SLIP_send_frame_data(status);

dataは上記の返却値のようでした。errorが発生するとここの値が変わるみたいですね。

まとめ

なんとなくプロトコルがわかってきました。あとはesptoolなどを見ながら組んでいきたいとおもいます。Stubなしで最低限のコードを作ろうかと思いましたが、EraseがないのでStubを読み込んだ標準的なesptool相当のものを作っていく予定です。

コメント

  1. そーたメイ より:

    esptool.py以外にこれだけESP32の実装があることを知りませんでした。

    つくるっちでのFW書き込みはesptoolでESP32書き込みしたときのUSBのログをwiresharkでキャプチャして実装しました。esptool.pyの実装が非常に複雑で読むのが面倒だったためです。コマンド定義については田中さん同様esptoolの.hを参考にしました。キャプチャしたログを置かせて頂きます。

    http://sohta02.web.fc2.com/release/esp32burnlog.202104.zip
    1.WiresharkでSuSE 6.3 tcpdumpで保存
    2.Windowsのwslコンソールで “don’t parse” でパース
    usage: pcap2txt filename [0-normal, 1-don’t parse]