Skip to content

ESP32: I2C コントローラが bus busy のまま固まると、以降の転送がすべて失敗する #362

Closed

Description

@GOB52

ESP32: I2C コントローラが bus busy のまま固まると、以降の転送がすべて失敗する

症状

M5Stack Core (ESP32) の Ex_I2C (port 0) に Unit UltraSonic I2C (RCWL9620) を繋ぎ、次の手順で読み取ると、read が毎回失敗します。100 kHz でも 50 kHz でも同じです。

  1. 0x01 を write する (測定開始)
  2. 150 ms 待つ
  3. 3 バイト read する
  • 同じ配線で Arduino Wire (ESP-IDF の legacy driver) を使うと、毎回成功します。
  • NanoC6 (ESP32-C6) / NanoH2 (ESP32-H2) の Ex_I2C では起きません。

原因

測定待ちの間、ESP32 側は何も送っていないのに、I2C0 が bus_busy=1 と TIME_OUT のまま固まります。

タイミング bus_busy int_raw
write が正常に終わった直後 0 0x1803
150 ms 待った後 1 0x1903 (TIME_OUT が立つ)

beginTransaction() は、busy の解除を 128 µs 待つだけで先に進みます。

https://github.com/m5stack/M5GFX/blob/0.2.33/src/lgfx/v1/platforms/esp32/common.inl#L2280-L2290

ESP32 には FSM reset bit がないので、ここでは復旧できません (fsm_rst は ESP32 では入りません)。

https://github.com/m5stack/M5GFX/blob/0.2.33/src/lgfx/v1/platforms/esp32/common.inl#L2305-L2311

そのため、固まった状態のままアドレスフェーズに進み、最初の i2c_wait で失敗します。

ESP-IDF のドライバーは、コマンドを始める前に bus busy (または前回 TIMEOUT) であれば i2c_hw_fsm_reset() で復旧します。Wire で読めるのはこのためです。

https://github.com/espressif/esp-idf/blob/v4.4.7/components/driver/i2c.c#L1451-L1454

develop (6f81929) でも同じコードです。

修正案

save_reg() の直後で、まだ busy なら i2c_stop() でバスとコントローラを復旧します。

       i2c_context[i2c_port].save_reg(dev);
 
+      // Still busy although no transfer of ours is running: a slave glitch can leave the
+      // controller waiting for a STOP forever (ESP32 has no FSM reset bit). Recover the bus
+      // and the controller as the ESP-IDF driver does before it starts a command.
+      if (getBusBusy(dev))
+      {
+        ESP_LOGV("LGFX", "i2c::beginTransaction : bus busy, recover");
+        i2c_stop(i2c_port);
+      }
+

save_reg() の 後 に置くのは、転送の最後の load_reg() で、ESP-IDF ドライバーと共有しているレジスタ (タイミング / filter / int_ena) を元の値に戻すためです。save_reg() の前で復旧すると、リセット後の値が保存され、ESP-IDF ドライバー側の設定がその値のまま残ります。

lgfx のロックは ESP-IDF ドライバーと共有していないので、2 つのドライバーを同時に使うのは今も安全ではありません。順番に使う場合、128 µs 待っても busy であれば、どちらの転送も動いていません (ESP-IDF ドライバーはコマンドを終えてから return します)。なので、この復旧が止めるのは、空いているのに固まったコントローラだけです。

確認結果

M5GFX 0.2.33

環境: M5GFX 0.2.33、M5Unified 0.2.26、Core は Arduino-ESP32 2.0.17 / ESP-IDF 4.4.7、NanoH2 は Arduino-ESP32 3.3.12 / ESP-IDF 5.5.5。どちらも 100 kHz で、M5Unified の Ex_I2C を使っています。

M5GFX ボード 結果
0.2.33 Core + RCWL9620 最初の読み取りから失敗する (再現)
0.2.33 + 修正案 Core + RCWL9620 単発 20/20、150 ms 周期で読み取り失敗 0 回 (3 回実行)
0.2.33 + 修正案 NanoH2 + RCWL9620 失敗なし (修正前と同じ)
(比較) Arduino Wire Core + RCWL9620 単発 20/20、150 ms 周期で失敗 0 回

修正案の差分は上と同じで、0.2.33 では common.inl L2290 の save_reg(dev); の直後に入ります。

詳細 (M5GFX a85d1dc)

環境: M5GFX a85d1dc (develop, 0.2.32 相当) にこの差分を当てたもの、Arduino-ESP32 2.0.17 / ESP-IDF 4.4.7、Core 100 kHz

Core + RCWL9620 (単発 20 回 + 150 ms 周期を 30 区間)

単発 周期 (中央値 / 最大)
修正前 すべて失敗 -
修正後 20/20 150 / 150 ms
Arduino Wire 20/20 150 / 150 ms
  • RCWL9620 では、測定のたびに busy と TIME_OUT が同時に出ます (52 回中 52 回)。
  • 復旧 (i2c_stop()) にかかる時間は平均 25〜29 µs、最大 59 µs です。
  • 復旧直後の転送は一度も失敗していません。

回帰の確認

ボード / デバイス 修正前 修正後
Core 内蔵 I2C (0x10 / 0x68 / 0x75)、read 200 回 200/200、574 µs 200/200、574 µs (復旧 0 回)
Core + Unit DAC2 (0x59)、write 200 回 200/200、416 µs 200/200、416 µs (復旧 0 回)
NanoC6 + RCWL9620 test 4/4 PASS 20/20 (busy / TIME_OUT は出ない、復旧 0 回)
NanoH2 + RCWL9620 test 4/4 PASS 20/20、test 4/4 PASS (busy / TIME_OUT は出ない、復旧 0 回)

NanoC6 / NanoH2 は Arduino-ESP32 3.3.12 / ESP-IDF 5.5.5 で確認しました。ESP32-S3 は試していません。

Activity

  1. ainyan03 commented on Oct 20, 2026

    @ainyan03
    Contributor

    詳しい調査と修正案をありがとうございます。修正案のとおりの変更を develop にマージしました (#368)。次のリリースに含まれます。

    こちらでも M5Stack Gray + Unit UltraSonic I2C (PortA、100 kHz、Arduino-ESP32 2.0.17) で再現しました。150 ms 待った後は毎回 bus_busy=1・int_raw=0x1903 のままで、修正前は 20 回中 0 回しか読めませんでした。修正後は、この固まった状態で転送が失敗することはなくなりました。内蔵 I2C (IMU の読み取り 40 回、release → begin 10 回) にも変化はありません。

    1 点だけ、手元の個体では修正後も一部の読み取りが 2 バイト目で失敗しました (connection_lost、100 回中 68〜71 回成功。Wire では 93 回)。復旧を周辺リセットだけにしても同じ率だったので、今回の変更とは別の原因とみて、別途調べます。そちらの環境では 20/20 だったとのことなので、個体や配線の差かもしれません。

    可能であれば develop で確認していただけますか。問題がなければ、この issue はクローズしてよいと思います。

  2. ainyan03 commented on Oct 20, 2026

    @ainyan03
    Contributor

    前回のコメントで「別途調べる」とした、修正後も一部の読み取りが失敗する件について、ロジックアナライザで原因がわかったので共有します。

    RCWL-9620 の仕様書には I2C クロックの上限やタイミングの記載がありません。以下は手元の 1 個体での実測に基づく情報です。

    結論

    観測した内容

    失敗していたのは、前回書いた 2 バイト目ではなく 3 バイト目でした (訂正します)。

    • 3 バイト目の先頭ビットが 0 のとき、センサーが SDA を Low にするのは、直前の SCL 立ち下がりから約 5 µs 後です。100 kHz では SCL の Low 期間が約 5.1 µs なので、マスターが次に SCL を上げるのとほぼ同時になります。
    • SDA が SCL の立ち上がりより後に落ちると、SCL が High の間に SDA が下がるので、START 条件と見なされて転送が失敗します。
    • 成否は、この SDA の立ち下がりが SCL の立ち上がりより前か後かで例外なく分かれました (計 141 回)。
    読み方 クロック 成功
    M5GFX (3 バイトまとめて読む) 100 kHz 86/100
    Arduino Wire 100 kHz 19/20
    M5GFX (3 バイトまとめて読む) 80 kHz 100/100
    • 80 kHz では、SDA の立ち下がりが SCL の立ち上がりより 1.1〜1.8 µs 前になり、余裕ができました。
    • 成功率の差 (M5GFX と Wire、測定ごとのばらつき) は、センサー側の SDA のタイミングのばらつきによるもので、読み方の違いによるものではありませんでした。

    環境: M5Stack Gray、PortA (G21/G22)、Arduino-ESP32 2.0.17、ロジックアナライザ 50 MS/s。

    同じセンサーで読み取りが時々失敗する場合は、I2C の周波数を 80 kHz 程度まで下げてみてください (M5Unified の M5.Ex_I2C なら、各関数の freq 引数に 80000 を渡します)。

  3. ainyan03 commented on Oct 20, 2026

    @ainyan03
    Contributor

    お待たせしました。この問題の修正を含む M5GFX 0.2.34 がリリースされました。ライブラリマネージャーから 0.2.34 に更新して、お手元の環境でご確認いただけますか。

    問題がなければ、この issue はクローズしていただいて構いません。

    なお、前のコメントで補足した Unit UltraSonic I2C 側のタイミングによる失敗 (100 kHz で 1 割前後) は 0.2.34 でも変わりません。発生する場合は I2C の周波数を 80 kHz 程度まで下げてください。

  4. GOB52 commented on Oct 20, 2026

    @GOB52
    ContributorAuthor

    0.2.34 で確認しました。M5Stack Basic (Core) + Unit Ultrasonic I2C (PortA) で、次のとおり問題ありません。

    • Arduino (Arduino-ESP32 2.0.17): M5Unit-DISTANCE の UnitTest 4/4 PASS (0.2.33 では 0/4)
    • ESP-IDF 5.5.4 (M5Unified 0.2.27): PaHub 経由の periodic 測定で 20 秒間読み取り失敗なし
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions