はじめに (対象読者・この記事でわかること)

この記事は、ArduinoやESP32を扱ったことがある方、IoTデバイス開発に興味がある方を対象としています。特に、ボタン入力を扱う際に複数のボタンを同時に押すケースを検出したいと考えている方に最適です。

この記事を読むことで、ESP32で複数ボタンの同時押し検出ができない問題の原因を理解し、マトリックススイッチ方式を用いたハードウェア設計方法、ソフトウェアでの同時押し検出実装方法を学べます。実際のコード例と共に、デバウンシング処理や高速スキャン技術を適用した安定した入力システムの構築方法を習得できます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Arduino IDEの基本的な使い方 - C/C++の基本的なプログラミング知識 - デジタル入力の基本的な概念 - プルアップ/プルダウン抵抗の概念

ESP32で複数ボタンの同時押し検出ができない問題

ESP32で複数ボタンの同時押し検出ができない問題は、いくつかの要因が考えられます。一般的なボタン入力回路は、1つのGPIOピンに1つのボタンを接続する設計が多く、同時押し検出には向いていません。また、ESP32のGPIOピンには、電気的特性上の制約があります。さらに、標準的なボタン入力ライブラリでは、複数ボタンの同時押しを正確に検出する機能が実装されていないことが多いです。

この問題を解決するには、ハードウェア設計とソフトウェア実装の両面からのアプローチが必要です。マトリックススイッチ方式と呼ばれる手法を用いることで、少数のGPIOピンで多数のボタンを制御し、同時押し検出を実現できます。本記事では、これらの問題点を解決するための具体的な方法について詳しく解説します。

マトリックススイッチ方式による同時押し検出の実装

ステップ1:ハードウェア設計

まず、複数ボタンの同時押し検出を実現するためのハードウェア設計について説明します。マトリックススイッチ方式は、行と列の配線を組み合わせて、複数のボタンを最小限のGPIOピンで制御する方式です。

例えば、2×2のマトリックスであれば、合計4つのボタンを2つの行用GPIOと2つの列用GPIO、合計4つのGPIOピンで制御できます。各ボタンは、行と列の交点に配置されます。

回路設計のポイント: 1. 行用GPIOピンはプルアップ抵抗を接続し、デジタル入力モードで使用します。 2. 列用GPIOピンはプルダウン抵抗を接続し、デジタル出力モードで使用します。 3. ボタンが押されていない状態では、行ピンはHIGH(プルアップされている)、列ピンはLOW(プルダウンされている)になります。 4. ボタンが押されると、対応する行ピンと列ピンが接続され、行ピンがLOWになります。

実際の回路接続例: - 行用GPIOピン:GPIO25, GPIO26 - 列用GPIOピン:GPIO27, GPIO14 - プルアップ抵抗:各行用ピンに10kΩ - プルダウン抵抗:各列用ピンに10kΩ

ステップ2:ソフトウェア実装

次に、ソフトウェア側での同時押し検出実装方法について説明します。基本的な考え方は、行と列を順番にスキャンして、どのボタンが押されているかを検出する方法です。

以下に、Arduino IDEでの実装例を示します。

Cpp
// ピン定義 const int rowPins[2] = {25, 26}; // 行用GPIOピン const int colPins[2] = {27, 14}; // 列用GPIOピン // ボタン状態を保存する配列 bool buttonStates[2][2] = {false}; // 前回のボタン状態を保存する配列 bool prevButtonStates[2][2] = {false}; // ボタンが押されたかどうかを示すフラグ bool buttonPressed[2][2] = {false}; void setup() { // シリアル通信の開始 Serial.begin(115200); // 行用ピンを入力に設定(プルアップ有効) for (int i = 0; i < 2; i++) { pinMode(rowPins[i], INPUT_PULLUP); } // 列用ピンを出力に設定(プルダウン有効) for (int i = 0; i < 2; i++) { pinMode(colPins[i], OUTPUT); digitalWrite(colPins[i], LOW); } } void loop() { // 行と列をスキャン for (int col = 0; col < 2; col++) { // 列ピンをHIGHに設定 digitalWrite(colPins[col], HIGH); // 少し待つ delay(1); // 各行ピンを読み取る for (int row = 0; row < 2; row++) { // 現在のボタン状態を取得 bool currentState = (digitalRead(rowPins[row]) == HIGH); // 状態が変化したかどうかをチェック if (currentState != prevButtonStates[row][col]) { // 状態変化を検出 if (!currentState) { // ボタンが押された buttonPressed[row][col] = true; } // 前回の状態を更新 prevButtonStates[row][col] = currentState; } // 現在の状態を保存 buttonStates[row][col] = currentState; } // 列ピンをLOWに戻す digitalWrite(colPins[col], LOW); } // ボタンが押されたかどうかをチェック for (int row = 0; row < 2; row++) { for (int col = 0; col < 2; col++) { if (buttonPressed[row][col]) { // ボタンが押されたことを報告 Serial.print("Button ["); Serial.print(row); Serial.print("]["); Serial.print(col); Serial.println("] pressed"); // フラグをリセット buttonPressed[row][col] = false; } } } // 少し待つ delay(10); }

このコードでは、まず列用GPIOピンを順番にHIGHに設定し、各行の状態を読み取ります。ボタンが押されていない場合、行ピンはプルアップされているためHIGHになります。ボタンが押されている場合、列ピンと行ピンが接続されるため、行ピンはLOWになります。

また、前回の状態と現在の状態を比較することで、ボタンが押された瞬間を検出しています。これにより、ボタンが押されている間は連続して反応せず、一度だけ反応するようにしています。

ハマった点やエラー解決

実際にこのシステムを実装する際にいくつかの問題に直面しました。

  1. チャタリングの問題 - 現象:ボタンを押した際に、一度の押しで複数回の反応が発生する - 原因:機械的なチャタリングによる電気的なノイズ - 解決策:デバウンシング処理の実装

  2. 同時押し検出の精度不足 - 現象:2つのボタンを同時に押しても、一方しか検出されない - 原因:スキャン処理のタイミング問題 - 解決策:より高速なスキャンと状態保存の改善

  3. GPIOピンの電流制限 - 現象:ESP32のGPIOピンを複数同時に操作すると不安定になる - 原因:ESP32のGPIOピンには電流制限がある - 解決策:外部プルアップ/プルダウン抵抗の使用

解決策

これらの問題を解決するための改善点を以下に示します。

  1. デバウンシング処理の実装 チャタリングを防ぐためのデバウンシング処理を実装します。ソフトウェアデバウンシングとして、状態変化を検出した後に一定時間待って再度確認する方法があります。
Cpp
// デバウンシング時間(ミリ秒) const unsigned long debounceDelay = 50; // デバウンシング用のタイマー unsigned long lastDebounceTime[2][2] = {0}; // ステップ2のコード内の状態変化検出部分を以下のように変更 if (currentState != prevButtonStates[row][col]) { // 状態変化を検出 if (!currentState) { // ボタンが押された可能性がある lastDebounceTime[row][col] = millis(); } // 前回の状態を更新 prevButtonStates[row][col] = currentState; } // デバウンシング処理 if ((millis() - lastDebounceTime[row][col]) > debounceDelay) { // 状態が安定したら最終的な状態を決定 if (!prevButtonStates[row][col] && !buttonStates[row][col]) { // ボタンが確実に押された buttonPressed[row][col] = true; } }
  1. 高速スキャンと状態保存の改善 同時押し検出の精度を向上させるため、スキャン間隔を短縮し、すべてのボタンの状態を一度に保存するように改善します。
Cpp
// スキャン間隔を短縮 const unsigned long scanInterval = 5; unsigned long lastScanTime = 0; void loop() { // スキャン間隔を管理 if (millis() - lastScanTime > scanInterval) { lastScanTime = millis(); // 行と列をスキャン for (int col = 0; col < 2; col++) { // 列ピンをHIGHに設定 digitalWrite(colPins[col], HIGH); // 少し待つ delayMicroseconds(10); // 各行ピンを読み取る for (int row = 0; row < 2; row++) { // 現在のボタン状態を取得 bool currentState = (digitalRead(rowPins[row]) == HIGH); // 状態が変化したかどうかをチェック if (currentState != prevButtonStates[row][col]) { // 状態変化を検出 if (!currentState) { // ボタンが押された buttonPressed[row][col] = true; } // 前回の状態を更新 prevButtonStates[row][col] = currentState; } // 現在の状態を保存 buttonStates[row][col] = currentState; } // 列ピンをLOWに戻す digitalWrite(colPins[col], LOW); } } // ボタンが押されたかどうかをチェック for (int row = 0; row < 2; row++) { for (int col = 0; col < 2; col++) { if (buttonPressed[row][col]) { // ボタンが押されたことを報告 Serial.print("Button ["); Serial.print(row); Serial.print("]["); Serial.print(col); Serial.println("] pressed"); // フラグをリセット buttonPressed[row][col] = false; } } } }
  1. 外部プルアップ/プルダウン抵抗の使用 GPIOピンの電流制限を回避するため、外部のプルアップ/プルダウン抵抗を使用します。通常、10kΩ程度の抵抗を使用します。

まとめ

本記事では、ESP32で複数ボタンの同時押し検出を実現する方法について解説しました。マトリックススイッチ方式を用いることで、少数のGPIOピンで多数のボタンを制御できることを学びました。また、ソフトウェア側でのデバウンシング処理や高速スキャン技術を適用することで、より安定した同時押し検出が可能になることを確認しました。

この技術を応用することで、より複雑な入力インターフェースを持つIoTデバイスを開発できるようになります。今後は、より多くのボタンを扱う方法や、ジェスチャー入力の検出など、さらに高度な入力技術についても探求していきたいと思います。

参考資料