ESP32 のアナログ入力で、押しボタン 5 個を判別する

#1. はじめに
　押しボタン 5 個を 1 本の信号（GND を合わせると 2 本）で実現する方法として、アナログ信号を用いるものについて検討しました。押しボタンスイッチの過渡的な接触抵抗が問題となり、**誤動作を無くすのは困難**だとわかりました。**誤動作が少なければ運用でカバーできる範囲で実用**になります。入力結果を確認しながら操作する置き時計の時刻設定などに使えそうです。M5 シリーズ[^11] (ESP32[^12]) での利用を前提にプリント基板 (PCB) を作成し、Arduino[^13] IDE 上でコーディングしました。PCB データおよびコードを GitHub[^14] に置きました。 スイッチサイエンスさんのマーケットプレイスで頒布[^15]しています。

- GND(Ground)
- PCB(Printed Circuit Board)
- IDE(Integrated Development Environment)

[^11]: [M5Stack](https://m5stack.com/)
[^12]: [Espressif: ESP32](https://www.espressif.com/en/products/socs/esp32)
[^13]: [Arduino](https://www.arduino.cc/)
[^14]: [GitHub: botanicfields/PCB-ADC-Buttons-for-M5](https://github.com/botanicfields/PCB-ADC-Buttons-for-M5)
[^15]: [スイッチサイエンス: 「M5 用光センサ・押しボタン 5 基板」](https://www.switch-science.com/catalog/7227/)

#2. アナログ入力
　古くは SONY のカセットデッキのリモコンで使用されていたとの情報があります[^21]。「ステアリング スイッチ 回路」で Google 画像検索すると多くの回路例や自作例がみつかります。ESP32 の開発元にも例[^22]があります。回路図例[^23]を抜粋します。
![espadcbutton.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/a13ddffd-fa73-bfed-0e2c-7a525f99a8f0.png)
　Arduino 用のシールド[^24]として、LCD と押しボタン 6 個を一体化した "LCD Display & Keypad Shield"[^25] があります。1 個はリセットピンに繋がり、5 個（上・下・左・右・選択）がアナログ入力ピン 1 本に収容されています。回路図[^26]のアナログ部分を引用します。
![osepp1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/314e38b3-b466-1eab-220a-610621bf2b44.png)
　派生製品 "LCD Keypad Shield V2.0"[^27][^28] も販売されています。回路図[^29] によると分圧抵抗の値が変更されています。
![dfr1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/59ab2925-89d8-8d35-ea92-e5fa72575521.png)
　変更した理由は不明ですが、電圧値が見た目きれいな値になっています。サンプルプログラムは V1 用と V2 用でコメントアウトを切り替える必要があり使い勝手が悪いです。
![keypad1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/be81e54d-144a-8da5-20e0-acfb99921f7c.png)

[^21]: [Espressif: Audio Development Framework - ADC Button Peripheral](https://docs.espressif.com/projects/esp-adf/en/latest/api-reference/peripherals/periph_adc_button.html)
[^22]: [Espressif: schematic of ESP32-LyraTD-MSC V2.2 Upper Board](https://dl.espressif.com/dl/schematics/ESP32-LyraTD-MSC_B_V1_1-1109A.pdf)
[^23]: [SONY カセットテープデッキ用リモコン(RM-50)の製作](http://www.suwa-koubou.jp/repair/rm_50/rm_50.html)
[^24]: [Arduino: Shields](https://www.arduino.cc/en/Main/arduinoShields)
[^25]: [OSEPP: 16×2 LCD Display & Keypad Shield](https://osepp.com/electronic-modules/shields/45-16-2-lcd-display-keypad-shield)
[^26]: [OSEPP 16x2SHD-01 Schematic](https://osepp.com/downloads/pdf/OSEPP_LCD_Keypad_Shield_V1-0-SCH1.pdf)
[^27]: [DFRobot: LCD Keypad Shield V2.0 for Arduino](https://www.dfrobot.com/product-1363.html)
[^28]: [Wiki DFRobot: LCD Keypad Shield V2.0 SKU DFR0374](https://wiki.dfrobot.com/LCD_Keypad_Shield_V2.0_SKU__DFR0374)
[^29]: [Wiki DFRobot: LCD Keypad Shield V2.0 Schematic](https://github.com/CainZ/LiquidCrystal/raw/master/LCD%20Keypad%20shield%20v2.0.pdf)

### 余談: IOREF
　Arduino の IOREF ピンは、入出力の電圧をシールドに示すためのものです。3.3V のコントローラは、IOREF に 3.3V を供給します。アナログ電圧のフルスケールについても IOREF に基づく必要があります。
　"LCD Keypad Shield V2.0" の注意書き[^26]にありますが、IOREF と 5V とをシールド上で接続してしまっているという設計ミスがあります。IOREF が 5V 以外のコントローラに使う場合、"LCD Keypad Shield V2.0" 上の IOREF と 5V との間をパターンカットする必要があります。そうしないと例えば 3.3V と 5V が短絡します。アイデアを借りながらではありますが、熟慮されていない設計と感じます。

- IOREF(Input/Output Reference voltage)

#3. 押しボタンの特性
　上記回路を 3.3V に接続し ESP32 のアナログ入力で試してみると誤動作が頻発します。特に right ボタンを押しているにもかかわらず up となる誤動作、up ボタンを押しているにもかかわらず down になる誤動作が顕著です。ボタンを押したり離したりしたときの波形を観測すると、特に離すときに中途半端な電圧が頻繁に現れます。波形の電圧から、使用した押しボタンスイッチは**接触抵抗 500Ω 程度をかなりの確率で長時間維持する**ことがわかりました。上記回路では、500Ω の加算によって、1 つ上の電圧になってしまいます。安価な押しボタンスイッチの選択も前提に、少なくとも 500Ω 程度の接触抵抗では誤動作しない設計にする必要があります。また、チャタリングを含め電圧が不安定となる時間は 3 ~ 4ms におよびます。ソフトウェアによるチャタリング対策のデバウンス時間を十分大きく取る必要があります。M5Stack では 10ms、JC_button では 25ms にデフォルト設定されています。

### right ボタン波形（一例）
　カーソルライン下側が right ボタン押下時の電圧レベル (0V) です。カーソルライン上側（点線）が、up ボタンの電圧レベル (750mV) です。オン（メーク・閉）からオフ（ブレーク・開）への遷移時に、波形の乱れが多く生じます。この例では、3ms に渡って中間の電圧が発生しています。
![DS1Z_QuickPrint26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/e6d4eb42-3c08-8bb2-2693-039d56a4a7c4.png)

### up ボタン波形（一例）
　カーソルライン下側が up ボタン押下時の電圧レベル (750mV) です。カーソルライン上側（点線）が、down ボタンの電圧レベル (1.25V) です。この例では、4ms に渡って中間の電圧が発生しています。
![DS1Z_QuickPrint29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/ed7072a8-4e16-0647-2e8e-fd2328008c7f.png)

### down ボタン波形（一例）
　カーソルライン下側が down ボタン押下時の電圧レベル (1.25V) です。カーソルライン上側（点線）が、left ボタンの電圧レベル (1.7V) です。この例では、1ms 強の間、中間の電圧が発生しています。
![DS1Z_QuickPrint32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/fadc30af-f559-155e-1441-5558b71dcfb6.png)

### left ボタン波形（一例）
　カーソルライン下側が left ボタン押下時の電圧レベル (1.7V) です。カーソルライン上側（点線）が、select ボタンの電圧レベル (2.4V) です。この例では、1ms の間、中間の電圧が発生し、激しいチャタリングも見えます。
![DS1Z_QuickPrint37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/cc60a8a4-8c2f-6763-b795-5e3d2dae4e8c.png)

### 波形の例
　この例では、中間的な電圧が他のボタンの電圧レベルにかなり接近しています。
![DS1Z_QuickPrint1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/6dfd6a40-0d87-4ff8-767d-a69b4b167ff0.png)

　この例では、中間的な電圧が他のボタンの電圧レベルの上で、2.5ms に渡って激しくチャタリングしています。
![DS1Z_QuickPrint10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/77fdc826-8161-f559-7c73-47dfe52e7f5c.png)

#4. ESP32 での利用
　ESP32 の入出力電圧は 3.3V ですが、ADC の直線性が補正される範囲は、例えば 0.15 ~ 2.45V となります[^31][^32][^33]。補正した電圧が得られる関数 **analogReadMilliVolts() は arduino-esp32[^34] 1.0.5 以降で利用できますが、M5Stack 社が提供しているボードマネージャー用の環境 (1.0.4相当) では使用できません**。直線性に問題はありますが、関数 analogRead() を使用し、実験で検出値を決めることにします。

- ADC(Analog Digital Converter)

　作成したプリント基板の回路図です。M5 シリーズでの利用を考慮して、5V 電源を受けて 3.3V に変換しています。光センサを併せて搭載し、別のアナログ信号に出力します。
![bf-028.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/aa27ea82-9264-7e54-159e-a44e191a0e91.png)
　3.3V に対する分圧は以下のとおりです。
![bf-028_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/16fc6b41-725b-89f6-77f7-b828cbd53afc.png)
　分圧の $+/-5\%$ の範囲と、接触抵抗を 500Ω とした場合の範囲を並べてみました。接触抵抗なしの場合の一番左の値はゼロです。**接触抵抗ありの場合が別の電圧範囲に重ならない**ことが必要で、離れ具合が動作マージンです。分圧抵抗が小さくなっている低い電圧ほど接触抵抗の影響が大きい様子がわかります。
![bf-028_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/dcaf9935-1bac-fed6-ee0d-ba4c0d9d822e.png)

[^31]: [ESP32のADCについて](https://qiita.com/koichaman/items/8c98ee5b6028e8d80703)
[^32]: [ESP32のADC　自分のまとめ１](https://qiita.com/fumi38g/items/bb728a0ee6f9094db4b9)
[^33]: [ESPの補正ADC：とても簡単になっていた](https://qiita.com/fumi38g/items/2fe0c4474d247cf422d6)
[^34]: [GitHub: espressif/arduino-esp32](https://github.com/espressif/arduino-esp32)

#5. JC_Button
　M5 シリーズでは、ライブラリ \<M5Stack.h\>, \<M5Atom\> などを取り込むことでボタン操作ができます。M5.begin() でボタンが設定され、M5.update() でボタンの状態更新が行われます。M5.update() をなるべく頻繁に実行する必要があります。M5シリーズのボタン[^51]は、JC_Button[^52] をベースに独自に改変されています。JC_Button 自体は 2019 年 10 月以降の更新はありません。Button.h に書かれた機能を整理してみました。

| 関数 | M5Stack | JC_Button | 機能 | 備考 |
|:-:|:-:|:-:|:-:|:-:|
| begin() | - |x | 内部変数を初期化 |  |
| read() | x | x | ボタンの現在の状態を返す。以下の関数は直近の read() に基づく |　M5.update() で呼び出される |
| isPressed() | x | x | ボタンがオン状態 |   |
| isReleased() | x | x | ボタンがオフ状態 |   |
| wasPressed() | x | x | 直近でボタンがオンになった | 次の read() で消滅 |
| wasReleased() | x | x | 直近でボタンがオフになった | 次の read() で消滅 |
| pressedFor(uint32_t ms) | x | x | ボタンがオンのまま指定時間 (ms) 以上が経過した |   |
| pressedFor(uint32_t ms, uint32_t continuous_time) | x | - | ボタンがオンのまま指定時間以上 (ms) が経過した、その後 continuos_time(ms) 毎に真となる |   |
| releasedFor(uint32_t ms) | x | x | ボタンがオフのまま指定時間 (ms) 以上が経過した |   |
| wasReleasefor(uint32_t ms) | x | - | ボタンが指定時間 (ms) 以上オンで、直近でオフになった | 次の read() で消滅 |
| lastChange() | x | x | ボタンの状態が変化した直近の millis() の値 |   |

[^51]: [GitHub: M5Stack Button.cpp](https://github.com/m5stack/M5Stack/blob/master/src/utility/Button.cpp)
[^52]: [GitHub: JChristensen/JC_Button](https://github.com/JChristensen/JC_Button)

#6. コーディング
### AdcButton
　枯れた JC_Button をベースに、ESP32 用にアナログ入力のボタンを扱うコードを作成しました。JC_Button と同等の使い勝手を目標とします。JC_Button のクラス Button の継承は、プライベート変数へのアクセスのため少なくともフレンドクラスにする必要があり断念しました。新しいクラス名は AdcButton とします。コンストラクタ名にも影響します。ToggleButton は省略しました。
　.begin() に引数を追加し、新たに作成するクラス AdcButtonPort のインスタンスへの参照を受け取る様にします。受け取ったアナログポートをメンバ変数 m_adc_btn に保持します。
　.begin および .read() の中にある digitalRead() を m_adc_btn->DigitalRead() に置き換えます。

``` Adc_button.h
//class Button
class AdcButton
{
    public:
//      Button(uint8_t pin, uint32_t dbTime=25, uint8_t puEnable=true, uint8_t invert=true)
//          : m_pin(pin), m_dbTime(dbTime), m_puEnable(puEnable), m_invert(invert) {}

        AdcButton(uint8_t pin, uint32_t dbTime=25, uint8_t puEnable=true, uint8_t invert=true)
            : m_pin(pin), m_dbTime(dbTime), m_puEnable(puEnable), m_invert(invert) {}

//      void begin();
        void begin(AdcButtonPort* adc_btn);
...
    private:
...
        AdcButtonPort* m_adc_btn;  // for Adc_button
};

//void Button::begin()
void AdcButton::begin(AdcButtonPort* adc_btn)
{
    m_adc_btn = adc_btn;

//   　pinMode(m_pin, m_puEnable ? INPUT_PULLUP : INPUT);
//　   m_state = digitalRead(m_pin);

    m_state = m_adc_btn->DigitalRead(m_pin);
...
}

//bool Button::read()
bool AdcButton::read()
{
...
//    bool pinVal = digitalRead(m_pin);
    bool pinVal = m_adc_btn->DigitalRead(m_pin);
...
}
```

### AdcButtonPort
　5 個のボタンに共通なアナログ入力のクラス AdcButtonPort の機能は以下です。

- Begin()
　アナログ入力ピンを設定します。

- Update()
　アナログ入力を analogRead() で読み込み、個別の .DigitalRead() に備えます。

- DigitalRead()
　5 個の AdcButton インスタンスから呼び出され、最新のボタン状態を返します。**アナログレベルのしきい値には、実験で得た数値を反映**しています。**digitalRead() との一貫性で、押しボタンがオンで false、オフで true** を返します。

```AdcButtonPort.cpp
class AdcButtonPort {
 public:
  AdcButtonPort()
    : m_adc_read(m_adc_read_max), m_adc_pin(0) {}
  void Begin(int adc_pin);
  void Update();
  bool DigitalRead(int button);
 private:
  const int m_adc_read_max = 4095;
  int m_adc_read;
  int m_adc_pin;
};

void AdcButtonPort::Begin(int adc_pin)
{
  m_adc_pin = adc_pin;
}

void AdcButtonPort::Update()
{
  m_adc_read = analogRead(m_adc_pin);
}

bool AdcButtonPort::DigitalRead(int button)
{
  switch (button) {
    case 0: if (                     m_adc_read <  200) return false;  break;
    case 1: if (m_adc_read >  700 && m_adc_read < 1100) return false;  break;
    case 2: if (m_adc_read > 1300 && m_adc_read < 1700) return false;  break;
    case 3: if (m_adc_read > 1900 && m_adc_read < 2300) return false;  break;
    case 4: if (m_adc_read > 2700 && m_adc_read < 3100) return false;  break;
    default: break;
  }
  return true;
}
```

### インスタンス
　便宜のためインスタンスを予め宣言します。アナログ入力と 5 個のボタンのインスタンスをまとめて初期化する関数 AdcButtonBegin()、まとめて最新化する AdcButtonUpdate() も用意します。AdcButtonUpdate() は、M5.update() と同様、頻繁に呼び出す必要があります。

```AdcButtonPort.cpp
// instances
AdcButtonPort adc_btn;
AdcButton btn_right  = AdcButton(0);
AdcButton btn_up     = AdcButton(1);
AdcButton btn_down   = AdcButton(2);
AdcButton btn_left   = AdcButton(3);
AdcButton btn_select = AdcButton(4);

void AdcButtonBegin(int adc_pin)
{
  adc_btn.Begin(adc_pin);
  btn_right.begin(&adc_btn);
  btn_up.begin(&adc_btn);
  btn_down.begin(&adc_btn);
  btn_left.begin(&adc_btn);
  btn_select.begin(&adc_btn);
}

void AdcButtonUpdate()
{
  adc_btn.Update();
  btn_right.read();
  btn_up.read();
  btn_down.read();
  btn_left.read();
  btn_select.read();
}
```

#7. 基板
　作成したプリント基板の外観です。
![front.JPEG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/8518ffa1-ef19-badf-b93b-9b53b043236d.jpeg)
　手にとって操作することを考慮し、スルーホール部品は使用せず、表面実装のみとしました。製造をお願いした JLCPCB[^61] で GROVE コネクタ[^62]の扱いがなく、この部品のみはんだごてで組み立てました。GROVE コネクタの信号ピン 2 本を使って、押しボタン 5 個と光センサーのアナログ出力を取り出します。押しボタンのある手元の明るさを測定できます。明るさへの影響を考慮してパイロットランプは設けませんでした。

[^61]: [JLCPCB](https://jlcpcb.com/)
[^62]: [Seeed Studio: Grove Female Header](https://www.seeedstudio.com/Grove-Female-Header-SMD-4P-2.0mm-90D-20Pcs-p-4590.html)

# 8. M5 シリーズへの接続
　M5 シリーズの GROVE コネクタは、必ずしもアナログ入力に適していません。ESP32 ではアナログ入力として一般に G32 ～ G39 が使用されます。G2, G4, G12 ~ G15, G24 ~ G26 のアナログ入力機能は、Wi-Fi 等が使用しますので一般には使いづらいです。G0 はプルアップがあるなどアナログ入力には向きません[^81]。

| モデル | GROVE | 主な用途 |
|:-:|:-:|:-:|
| M5Stack Basic| G22, G21 | I2C |
| M5Stack Core2| G33, G32 | I2C, I/O, UART |
| M5StickC | G33, G32 | I2C, I/O, UART |
| M5Atom | G32, G26 | I2C, I/O, UART |

　**M5Stack Basic の GROVE コネクタに直結して使用することはできません**。「GROVE-ジャンパケーブル」などを用いて、アナログ入力の GPIO および 5V, GND に接続する必要があります。

### M5Stack Basic で、G35, G36, 5V, GND に接続した例
![m5stack.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/ea2b34a2-c61b-958d-12e8-2b7052c9eef1.jpeg)

### M5Atom で、拡張基板の一種を経由して接続した例
![example.JPEG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/d83fa59e-7f8e-5885-323f-907c0ee4111c.jpeg)

[^81]: [Lang-Ship: ESP32のGPIO入力について](https://lang-ship.com/blog/work/esp32-gpio-input/)

#9. おわりに
　複数の押しボタンのアナログレベルによる集約には問題があり、やはり基本的にはデジタルで用いるべきであることを改めて認識できました。波形観測では、誤動作時に GPIO に信号を出してトリガーをかける実験も経験できました。作成したプリント基板については、運用でカバーすることを前提に利用可能なシーンもあると思います。せっかくなので上手に使っていきたいと思います。