Skip to content

ESP32: pin_backup_t::backup() が入力選択フィールドをピン番号ではなく信号番号と比較している #348

Closed

Description

@GOB52

概要

ESP32 系の pin_backup_t::backup() は、ピンから入力信号が来ているかを判定しています。その判定で、入力選択レジスタのフィールドを信号番号と比べていますが、このフィールドに入っているのは GPIO 番号です。そのため _in_func_num が常に -1 のままになり、restore() で入力側の割り当て (GPIO.func_in_sel_cfg[...]) が復元されません。

該当箇所

size_t func_num = ((_gpio_func_out_reg >> GPIO_FUNC0_OUT_SEL_S) & GPIO_FUNC0_OUT_SEL_V);
if (func_num < sizeof(GPIO.func_in_sel_cfg) / sizeof(GPIO.func_in_sel_cfg[0])) {
#if defined ( GPIO_FUNC0_IN_SEL_CFG_REG )
_gpio_func_in_reg = *reinterpret_cast<uint32_t*>(GPIO_FUNC0_IN_SEL_CFG_REG + (func_num * 4));
bool hit = func_num == ((_gpio_func_in_reg >> GPIO_FUNC0_IN_SEL_S) & GPIO_FUNC0_IN_SEL_V);
#else
_gpio_func_in_reg = *reinterpret_cast<uint32_t*>(GPIO_FUNC1_IN_SEL_CFG_REG + ((func_num - 1) * 4));
bool hit = func_num == ((_gpio_func_in_reg >> GPIO_FUNC1_IN_SEL_S) & GPIO_FUNC1_IN_SEL_V);
#endif
if (hit) {
_in_func_num = func_num;
// ESP_LOGD("DEBUG","backup pin:%d : func_num:%d", pin_num, _in_func_num);

size_t func_num = ((_gpio_func_out_reg >> GPIO_FUNC0_OUT_SEL_S) & GPIO_FUNC0_OUT_SEL_V);
if (func_num < sizeof(GPIO.func_in_sel_cfg) / sizeof(GPIO.func_in_sel_cfg[0])) {
#if defined ( GPIO_FUNC0_IN_SEL_CFG_REG )
  _gpio_func_in_reg = *reinterpret_cast<uint32_t*>(GPIO_FUNC0_IN_SEL_CFG_REG + (func_num * 4));
  bool hit = func_num == ((_gpio_func_in_reg >> GPIO_FUNC0_IN_SEL_S) & GPIO_FUNC0_IN_SEL_V);
#else
  _gpio_func_in_reg = *reinterpret_cast<uint32_t*>(GPIO_FUNC1_IN_SEL_CFG_REG + ((func_num - 1) * 4));
  bool hit = func_num == ((_gpio_func_in_reg >> GPIO_FUNC1_IN_SEL_S) & GPIO_FUNC1_IN_SEL_V);
#endif

誤りの理由

  • func_num は、ピンの GPIO_FUNCn_OUT_SEL_CFG_REG から読んだペリフェラルの信号番号です。
  • GPIO_FUNCm_IN_SEL_CFG_REG は信号番号ごとに並んだレジスタで、その IN_SEL (func_sel / in_sel) フィールドは、その信号に接続されている GPIO (パッド) 番号です。TRM とレジスタ定義ヘッダにもそう書かれています (例: ESP32-P4 の gpio_struct.h: "s=0-56: connect GPIO[s] to this port. s=0x3F: set this port always high level.")。
  • そのため、この判定が真になるのは、信号番号とピン番号がたまたま一致したときだけです。

例: ESP32-S3 で I2C0 の SDA を GPIO2 にした場合、func_num は I2C0 SDA の信号番号です。func_in_sel_cfg[func_num] には GPIO 2 が入っています。func_num != 2 なので、_in_func_num は -1 のままです。

修正案

両方の分岐で、ピン番号と比べます。

bool hit = pin_num == ((_gpio_func_in_reg >> GPIO_FUNC0_IN_SEL_S) & GPIO_FUNC0_IN_SEL_V);
...
bool hit = pin_num == ((_gpio_func_in_reg >> GPIO_FUNC1_IN_SEL_S) & GPIO_FUNC1_IN_SEL_V);

こうすると、入力と出力の信号番号が同じペリフェラル (I2C の SDA/SCL など) の入力が見つかるようになります。「出力の信号番号から入力を探す」という元の意図にも合います。

影響

M5GFX の通常の使い方では、ピンを借りている間に入力側の割り当てが変わることはほとんどありません。そのため、復元されなくても実害はたいていありません。

問題になるのは、pin_backup_t で退避したピンの入力側の割り当てが、復元までの間に変わった場合です (esp_rom_gpio_connect_in_signal などで変えた場合)。このとき restore() の後も、ペリフェラルの入力は元に戻りません。

経緯

pin_backup_t を LovyanGFX / M5GFX から移植している M5UnitUnified で見つけました。M5UnitUnified では _in_func_num の値を「そのピンが GPIO matrix を通っているか」の判定に使っているため、このバグで機能が動かず、表に出ました。M5UnitUnified 側は、上の修正案と同じ比較にして次のリリースで直します。

LovyanGFX にも同じ問題があります: lovyan03/LovyanGFX#963

Activity

  1. changed the title [-]ESP32: pin_backup_t::backup() compares the input-select field with the signal number instead of the pin number[/-] [+]ESP32: pin_backup_t::backup() が入力選択フィールドをピン番号ではなく信号番号と比較している[/+] on Oct 12, 2026
  2. GOB52 commented on Oct 12, 2026

    @GOB52
    ContributorAuthor

    実機確認の結果

    LovyanGFX で確認しました。M5GFX の pin_backup_t::backup() も同じコードです (lovyan03/LovyanGFX#963 と同内容)。

    M5Stack Core (ESP32) と CoreS3 (ESP32-S3) で確かめました。比べたのは 2 つのビルドです。

    • 元のまま: LovyanGFX 28fd2f8a (develop)
    • 修正版: 715 行目と 718 行目の func_num == を pin_num == に変えたもの (違いはこの 2 行だけ)

    どちらも pioarduino (arduino-esp32 3.x) でビルドしました。

    手順

    1. 内蔵 I2C で Wire.begin() する。
    2. A: SDA / SCL のピンについて、GPIO_FUNCn_OUT_SEL の信号番号 (func_num) と func_in_sel_cfg[func_num] の in_sel を読む。
    3. B: lgfx::gpio::pin_backup_t で SDA / SCL を退避する。
    4. esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, I2CEXT0_SDA/SCL_IN_IDX) で入力側の割り当てを書き換える。
    5. restore() した後、in_sel を読み、内蔵デバイスに probe (endTransmission) する。

    結果

    Core (ESP32)
    元のまま
    Core (ESP32)
    修正版
    CoreS3 (ESP32-S3)
    元のまま
    CoreS3 (ESP32-S3)
    修正版
    A: SDA の func_num / in_sel 30 / 21 30 / 21 90 / 12 90 / 12
    A: SCL の func_num / in_sel 29 / 22 29 / 22 89 / 11 89 / 11
    A: func_num == in_sel false false false false
    A: pin_num == in_sel true true true true
    B: restore() 後の in_sel (SDA / SCL) 56 / 56 21 / 22 56 / 56 12 / 11
    B: restore() 後の probe (0 = ACK) 5 (タイムアウト) 0 5 (タイムアウト) 0

    (56 = 0x38 は、入力を常に High にする定数です。)

    • A から、in_sel に入っているのは信号番号ではなくピン番号であることがわかります。そのため今の判定 func_num == in_sel は成り立たず、_in_func_num は -1 のままです。
    • B から、元のままでは入力側の割り当てが復元されず、I2C が通らなくなることがわかります。2 行を直した版では元に戻り、通信も通ります。
    試験に使ったスケッチ
    // Check of lgfx::gpio::pin_backup_t input-routing detection on the internal I2C bus
    #include <Arduino.h>
    #include <Wire.h>
    #include <LovyanGFX.hpp>
    #include <soc/gpio_struct.h>
    #include <soc/gpio_reg.h>
    #include <soc/gpio_sig_map.h>
    #include <esp_rom_gpio.h>
    
    #if defined(CONFIG_IDF_TARGET_ESP32S3)
    constexpr int pin_sda = 12, pin_scl = 11;  // CoreS3 internal I2C (AXP2101 / AW9523)
    #else
    constexpr int pin_sda = 21, pin_scl = 22;  // Core internal I2C
    #endif
    constexpr uint32_t sig_sda = I2CEXT0_SDA_IN_IDX, sig_scl = I2CEXT0_SCL_IN_IDX;  // Wire = I2C0
    
    uint32_t out_sel(const int pin)
    {
        return (REG_READ(GPIO_FUNC0_OUT_SEL_CFG_REG + pin * 4) >> GPIO_FUNC0_OUT_SEL_S) & GPIO_FUNC0_OUT_SEL_V;
    }
    uint32_t in_sel(const uint32_t sig)
    {
        return (GPIO.func_in_sel_cfg[sig].val >> GPIO_FUNC0_IN_SEL_S) & GPIO_FUNC0_IN_SEL_V;
    }
    
    int find_device()
    {
        for (int a = 1; a < 127; ++a) {
            Wire.beginTransmission(a);
            if (Wire.endTransmission() == 0) {
                return a;
            }
        }
        return -1;
    }
    int probe(const int addr)
    {
        Wire.beginTransmission(addr);
        return Wire.endTransmission();
    }
    
    void part_a(const char* name, const int pin, const uint32_t sig)
    {
        const uint32_t func_num = out_sel(pin);
        const uint32_t sel      = in_sel(func_num);
        Serial.printf("[A] %s pin=%d out_sel(func_num)=%u in_sel_cfg[func_num].in_sel=%u (I2C in idx=%u) | "
                      "func_num==in_sel:%s pin==in_sel:%s\n",
                      name, pin, func_num, sel, sig, func_num == sel ? "true" : "false", (uint32_t)pin == sel ? "true" : "false");
    }
    
    void setup()
    {
        Serial.begin(115200);
        delay(3000);
        Serial.printf("\n=== pin_backup_t check (%s LovyanGFX) on %s ===\n", LGFX_VARIANT, CONFIG_IDF_TARGET);
        Wire.begin(pin_sda, pin_scl, 100000);
    
        // A: register values after Wire.begin()
        part_a("SDA", pin_sda, sig_sda);
        part_a("SCL", pin_scl, sig_scl);
    
        // B: change the input routing while the pins are backed up, then restore
        const int addr = find_device();
        Serial.printf("[B] device on internal bus: 0x%02x, probe before: %d\n", addr, addr >= 0 ? probe(addr) : -1);
        {
            lgfx::gpio::pin_backup_t backup[] = {pin_sda, pin_scl};
            esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, sig_sda, false);
            esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, sig_scl, false);
            Serial.printf("[B] after re-routing: in_sel SDA=%u SCL=%u\n", in_sel(sig_sda), in_sel(sig_scl));
            for (auto&& b : backup) {
                b.restore();
            }
        }
        Serial.printf("[B] after restore():  in_sel SDA=%u SCL=%u (expected %d / %d)\n", in_sel(sig_sda), in_sel(sig_scl),
                      pin_sda, pin_scl);
        Serial.printf("[B] probe after restore: %d (0 = ACK)\n", addr >= 0 ? probe(addr) : -1);
        Serial.println("=== done ===");
    }
    
    void loop()
    {
        delay(1000);
    }
  3. ainyan03 commented on Oct 13, 2026

    @ainyan03
    Contributor

    詳しい報告と、実機での確認・再現スケッチをありがとうございます。ご指摘のとおりで、修正の PR を出しました: #349

    ご提案の比較 (pin_num ==) をもとに、次の 2 点を加えています。

    • SIG_IN_SEL (GPIO matrix 経由のビット) が立っているときだけ一致とみなします。IO MUX 直結の入力では IN_SEL に古い値が残っていることがあり、それがたまたまピン番号と一致すると、そのピンの restore() が別のピンへの割り当てを元に戻してしまうためです。
    • restore() での入力選択の書き戻しを、IO_MUX・GPIO_PIN・出力選択を戻した後に移しました。これまで実行されていなかった経路で、借り手の設定が残ったパッドへ入力を繋ぎ直さないようにするためです。

    内部 I2C の SDA/SCL を退避し、入力を定数 High に付け替えてから restore() する試験で、修正前は IN_SEL が定数のまま、修正後は元のピン番号に戻ることを ESP32 / ESP32-S3 / ESP32-P4 の実機で確かめています。

  4. ainyan03 commented on Oct 13, 2026

    @ainyan03
    Contributor

    修正 (#349) は M5GFX 0.2.33 としてリリースされました。ライブラリを更新してお使いください。確認いただけたら、この Issue は閉じてよいと思います。

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