Web Serial API+ESP32(Arduino)研究 その4 Erase

概要

前回はプロトコルについて調べてみました。今回はStubの転送とEraseコマンドを実装していきたいと思います。

Stubについて

前回の調査によりEraseコマンドはブートローダーのROMには存在しておらず、Stubプログラムを転送することで利用可能になります。

そのためまずはStubプログラムを転送するところを作ってみました。

  1. ESP_MEM_BEGINで.text(コード)領域に書き込み開始の通知
  2. ESP_MEM_DATAで実際のデータを分割して送信する
  3. ESP_MEM_BEGINで.data(データ)領域に書き込み開始の通知
  4. ESP_MEM_DATAで実際のデータを分割して送信する
  5. ESP_MEM_ENDで書き込み完了の通知

上記の手順で転送可能です。ESP_MEM_BEGINとESP_MEM_ENDは対応していないので注意してください。

まずStubのコードですが、esptoolから抜き出してくる必要があります。

上記の最後の方にありますがzlibで圧縮されています。Python上で展開したものを加工して転送します。最初16進数文字列にしようかと思いましたが、さすがにサイズが大きくなったのでBase64で保存したいと思います。

チップの種類について

Stubはチップによって異なるのでESP32でもS2やS3、C3、C6など複数種類あるので気をつけてください。またESP32の開発ツールに入っているesptoolは古くESP8266とESP32、ESP32S2の3種類しか対応していません。

差分をみてみたのですが、機種判定の方法が現行バージョンと最新esptoolで異なっておりました。現行バージョンは0x60000078あたりのレジスタで判定していましたが、最新版のesptoolは0x40001000で判定しておりました。

async function espGetChipType() {
    // Chip type
    let ret = await espReadRegister(0x40001000);
    if (ret == 0xfff0c101) {
        espChipType = "ESP8266";
    } else if (ret == 0x00f01d83) {
        espChipType = "ESP32";
    } else if (ret == 0x000007c6) {
        espChipType = "ESP32-S2";
    } else if (ret == 0xeb004136) {
        espChipType = "ESP32-S3(beta2)";
    } else if (ret == 0x9) {
        espChipType = "ESP32-S3(beta3)";
    } else if (ret == 0x6921506f) {
        espChipType = "ESP32-C3";
    } else if (ret == 0x0da1806f) {
        espChipType = "ESP32-C3";
    } else if (ret == 0x1b31506f) {
        espChipType = "ESP32-C6 BETA";
    }

    return espChipType;
}

こんな感じで実装していますがS3はさらに細かいバージョンがあるんですね。。。

UART通信の注意点

UARTはプロトコル的に文字単位で送信する形になりますので文字化けが発生したり、欲しかったデータが途中までしか受信できないなどの問題があります。

ESP32のダウンロードモードの場合には0xC0をスタートとエンドにつけることになっています。そのため通信中に0xC0のデータがあった場合には他の文字に置換する処理が必要になります。

async function espSendPacket(packet) {
    let dv = new DataView(packet);
    const espWriter = espPort.writable.getWriter();
    let sendPacket = [];

    sendPacket.push(0xC0);
    for (let i = 1; i < dv.byteLength - 1; i++) {
        let byte = (dv.getInt8(i) >>> 0) & 0xff;
        if (byte == 0xDB) {
            sendPacket = sendPacket.concat([0xDB, 0xDD]);
        } else if (byte == 0xC0) {
            sendPacket = sendPacket.concat([0xDB, 0xDC]);
        } else {
            sendPacket.push(byte);
        }
    }
    sendPacket.push(0xC0);
    console.log("espSendPacket", sendPacket);
    await espWriter.write(new Uint8Array(sendPacket));
    espWriter.releaseLock();

    // Wait
    await new Promise(resolve => setTimeout(resolve, 100));
}

送信部分ですが、0xC0が送信データにあった場合には0xDB, 0xDCに置換しています。ここで0xDBに置換しているので、0xDBだったデータは0xDB, 0xDDに置換しています。つまり受診時にもこれを戻す処理が必要になります。

送信の最後に100ミリ秒のウエイトを入れていますが、これは送信直後に受信をするとすべてのパケットを受信前に取得してしまう可能性があるので適度にウエイトを入れています。ちょっと大きすぎる値な気がしますが今の所速度を重視していないので大きめに入れています。

作成物

上記にアップしました。Connectボタンを押すとシリアルポートの選択ダイアログがでて接続されます。自動的にチップ判定からStub転送まで行われるはずです。その後にEraseボタンを押すとESP32が消えます。

あっという間に消えてしまうのでご注意ください。通常esptoolだとダウンロードモードにしてからSyncするまでが一番時間がかかり、その後にStub転送をしてからEraseをするので時間がかかるように思っていましたが、Erase自体は一瞬で終わっているようでした。

UIFlowなど画面が表示されるESP32に対してEraseを行い、電源再起動しても画面表示がされないことを確認してください。

まとめ

ここまで非常にあっさりとした記事になっていますが、実は全面的にコードを書き換えていまして結構時間がかかっています。なかなかJavaScriptは難しい。。。基本バイナリデータでも処理の途中で符号付きになってしまうので(val>>>0)&0xffみたいな感じで符号なしuint8_t相応に変換してあげています。

Promiseとthenを使って書くのがたぶんJavaScriptっぽいのですが、べたべたのC言語風のJavaScriptで組んでいます。。。

きれいなJavaScript実装はつくるっちのものが一番まとまっていると思います!

ちなみにEraseよりもFlash書き込みのほうがSutbがいらず楽なのですが、トラブったときに消せないと面倒なのでStub前提のコードにしています。Stub転送ができたらFlash転送もほぼ同じ処理なのですが今回は力尽きたのでEraseのみにしたいと思います。

コメント

  1. そーたメイ より:

    webSerialの公式サンプルはawait/asyncを使う前提で書かれているのですが、つくるっちでは元のscratchがawait/asyncを使わない前提で書かれており、とりあえずはその方針に従い泣きながらPromise/thenで実装しました。

    つくるっちのwebSerialで(私にとって)完成形なのはESP32書き込みでなく通常の通信 _sendRecvUart です。USB切断 / timeout / 通信中のportClose処理 に対応するため単純なread/writeなのに100行近いコードになってしまいました。私もjavascript初心者のため良いレベル上げにはなりましたが、正直webSerialでReaderStreamを使った(しかもそれをPromiseで使わざるを得なかった)のは失敗だったと思います。webSocketやwebBluetoothは遥かに素直でした。

    Promise/thenは超並列処理(多数のwebコンテンツを同時にfetch)記述には向いていますが同時処理1~2(通信待ち+timeout)のHW制御には記述性・可読性が低く向いていない印象です。これに関連して興味深いdocを見つけました。
    https://makecode.com/async

    For this reason, PXT lets users call async functions, as if they were regular functions. This loses information about where your thread can be interrupted, but we can hopefully recover that in the IDE (by for example displaying a little clock next to async calls).
    Supporting async functions this way is one of the main reasons why we have our own compilation scheme from TypeScript to JavaScript (cross-browser debugger is another major one).

    javascript or typescript, Promise or asyncなど様々なコーディングスタイル、考え方があるようです。これから変わっていくかもしれません。

    • たなかまさゆき より:

      UARTは面倒ですよね
      明確な通信終わりがないから結局はタイムアウトまで待つような実装になりますし

      await espWriter.write(new Uint8Array(sendPacket));

      みたいな配列とArrayBufferを行ったり来たりも面倒。。。

      つくるっちも途中から受信していないし苦労がコードから見えます!
      Stub実行するとレジスタ読み取りとかの返却が14バイトから12バイトに減ったりするし、、、

  2. そーたメイ より:

    webSerialの極悪なところは
    1. 受信バッファのゴミクリアができないこと
    2. 受信バッファにデータがあるかないかの判別が出来ないこと

    >つくるっちも途中から受信していないし苦労がコードから見えます!
    sync後のrespが8回来るところが厄介で、最初のrespを受け取り残りのrespを捨てるためburnESP32では最初から最後まで別スレッド_RecvEspBurnを回しっぱなしにしてます。その後実装した_sendRecvUartではtimeout->reader.cancelの手法が確立したため_sendRecvUart内でreaderスレッドを処理してます。本当はburnESP32も書き直したいのですが、面倒すぎて手が回ってません。

    esptool.pyのポーティングは2018年のUSB2BTplus発売のときVB.netで実装したのですが、いつの間にかesptool.pyでボーレート切り替えと書き込みデータの圧縮が追加されていて驚きました。USB2BTplusのときは苦労して921600でsyncしてました。今は115200 syncなんですね

    • たなかまさゆき より:

      いろいろ変わっているんですよね
      最新esptoolもちょっと変わっていましたし!