イヤホンボタンによるESP32の操作

2020年11月15日日曜日

ESP32

t f B! P L
ESP32をはじめとする小型マイコンボードにおいて、音楽プレーヤーなどを製作する際、操作インターフェイスとしてイヤホンボタンを活用するようにすればスイッチなどの実装の手間が省けます。スマホに使用できる市販のマイク付きイヤホンではAndroid用とiPhone用に分かれており、このうちAndroid用では、通常1ボタンまたは3ボタンのものが入手できるようです。このうちAndroid用3ボタンのものを使用してESP32向けに簡単なボタン操作の検出機能を実装してみます。

Androidイヤホンボタンの仕様

Androidイヤホンボタンの仕様については以下にAndroid仕様の説明(公式)があるので参考にしました。

この記事ではわかりやすく以下のように読み替えます。
  • ボタンA : CENTERボタン
  • ボタンB : PLUSボタン
  • ボタンC : MINUSボタン
  • (ボタンD : Dボタン)
Androidスマホでは主にCENTERボタンが再生や通話フックに割り当てられ、PLUSボタンが音量アップ、MINUSボタンが音量ダウンに割り当てられています。

回路としては、イヤホンの第4番目の端子(一番根元に近い側の端子)としてMIC端子が割り当てられており、どのボタンも押されていない場合は2.2Vにバイアスされているのですが、ボタンを押した場合はGNDとそれぞれのボタンに対応する接続された抵抗値によりMIC端子の電位が変わることにより、どのボタンが押されたか検出できる仕組みとなっています。
したがって、ESP32ではこのマイク端子をADC入力に接続して、ボタン押下の判定を行うこととします。

判別できる操作の数

ボタンの数が3つですのでそんなに複雑な操作はできませんが、超シンプルな音楽プレーヤーを想定して以下の操作ができるように考えてみます。もちろん機能の対応はあくまで一例です。一つの操作に複数の機能が割り当てられている項目は基本的には画面遷移などのモードにより対応機能を切り分けることを想定しています。

操作種類想定する対応機能例
CENTERボタン 1回クリック選択/再生/一時停止
CENTERボタン 2回クリック戻る/停止
CENTERボタン 3回クリック特殊機能(ランダム再生等)
CENTERボタン 長長押し電源OFF
PLUSボタン クリック上に移動/音量up
PLUSボタン 長押し上に移動/音量up を連続
MINUSボタン クリック下に移動/音量down
MINUSボタン 長押し下に移動/音量down を連続

部品リスト

  • ESP32
  • 2.2Kohm
  • 4極イヤホンジャック
  • 3ボタン付き イヤホン

4極イヤホンジャックに関しては、ブレッドボードで簡単に接続できる基板には以下のようなものがあります。

また、以下のようなリモコン付き延長ケーブルを使用すれば、イヤホン無しでの使用も可能です。

配線図

3.5mm 4極イヤホンジャックを使用しますが、イヤホンボタンの操作のみであれば、MIC端子とGNDに加えて、MIC端子へのバイアス電圧供給抵抗のみを配線するだけでOKです。以下の図では、ADC2を使用する場合を記載していますが、ADC1を使用する場合はGPIO14の代わりにGPIO34に接続すればOKです。
Android仕様で2.2Vとなっているバイアス電圧は簡単に得られる3.3Vに置き換えて接続することにします。(通常のECMやMEMSマイクがこれで故障することはないと思いますが、自己責任でお願いします。)
ESP32との配線 (ADC2を使用する場合)

プログラムの構成概要

ボタン操作の判定結果を単純にシリアルターミナルに表示するサンプルプログラムをArduino環境にて実装してみました。構成概要は以下の通りです。
  • 主処理との共存を考えて、操作種類の判定は別タスクにて実行。
  • ADC1またはADC2入力で、MIC端子の電圧を100msごとにフィルタ処理込みでチェックして現在のボタン押下状態のみをまずは判定する。
  • 長押し、長長押しは特定ボタンが連続して押下される時間にて判断
  • CENTERボタンのクリック数判定は時間との比較ではなく、ボタン状態の過去履歴からボタンが離されたイベント数をカウントすることにより判定
注意点としては、PLUS/MINUSボタンに対しては単にクリック/長押しを検出するのに対して、CENTERボタンに対しては押下された回数をカウントしなければならないため、操作検出のタイミングをボタンを離して一定時間が経過したタイミングにせざるを得ません。割り当てる機能にもよりますが、あらかじめわかっていれば実際に使用してみたときにそれほど違和感はなかったのでこれで良しとします。

操作検出のタイミング
  • PLUS/MINUSボタン : ボタンを押したとき
  • CENTERボタン : ボタンを離して一定時間経過したとき(長長押し以外)
シリアルメッセージの表示ではなく実際に機能をキックするように変更する際には、必要に応じて情報をキューを使用して受け渡すようにすると良いと思います。

サンプルプログラム

以下のプログラムは、ESP-IDFのADCサンプルをベースに作成しましたが、
Arduino環境にて inoファイルとしてプロジェクトを作成すればそのまま動作します。
ソースコードはElehobicaのGithubにも置いてあります。

#include <stdio.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/adc.h"
#include "esp_adc_cal.h"

static esp_adc_cal_characteristics_t *adc_chars;
static const adc_unit_t adc_unit = ADC_UNIT_2; // ADC_UNIT_1 or ADC_UNIT_2
static const adc_channel_t channel = ADC_CHANNEL_6; // GPIO34 for ADC1, GPIO14 for ADC2
static const adc_atten_t atten = ADC_ATTEN_DB_11; // 3.3V full scale

#define NUM_OF_SAMPLES 32 // For Multisampling
#define DEFAULT_VREF 1100 // For better estimation of adc2_vref_to_gpio()
#define HP_BUTTON_OPEN     0
#define HP_BUTTON_CENTER   1
#define HP_BUTTON_D        2
#define HP_BUTTON_PLUS     3
#define HP_BUTTON_MINUS    4
#define NUM_BTN_HISTORY    30 // for count_center_clicks()

uint8_t button_prv[NUM_BTN_HISTORY] = {}; // initialized as HP_BUTTON_OPEN
uint32_t button_repeat_count = 0;

// Task Handles
TaskHandle_t th;

static uint32_t adc_get_hp_button()
{
  uint32_t adc_reading = 0;
  int raw;
  uint32_t voltage;
  uint32_t ret;
  //Multisampling
  for (int i = 0; i < NUM_OF_SAMPLES; i++) {
    if (adc_unit == ADC_UNIT_1) {
      raw = adc1_get_raw((adc1_channel_t) channel);
    } else {
      adc2_get_raw((adc2_channel_t)channel, ADC_WIDTH_BIT_12, &raw);
    }
    adc_reading += raw;
  }
  adc_reading /= NUM_OF_SAMPLES;
  //Convert adc_reading to voltage in mV
  voltage = esp_adc_cal_raw_to_voltage(adc_reading, adc_chars);
  //Serial.println(voltage);
  // Android Headphone button conditions
  // 3.3V pull-up
  if (voltage < 140) { // < 140mV (CENTER: 0mV)
      ret = HP_BUTTON_CENTER;
  } else if (voltage >= 142 && voltage < 238) { // 142mv ~ 238mV (D: 190mV)
      ret = HP_BUTTON_D;
  } else if (voltage >= 240 && voltage < 400) { // 240mV ~ 400mV (PLUS: 320mV)
      ret = HP_BUTTON_PLUS;
  } else if (voltage >= 435 && voltage < 725) { // 435mV ~ 725mV (MINUS: 580mV)
      ret = HP_BUTTON_MINUS;
  } else { // others
      ret = HP_BUTTON_OPEN;
  }
  return ret;
}

static int count_center_clicks(void)
{
  int i;
  int detected_fall = 0;
  int count = 0;
  for (i = 0; i < 4; i++) {
    if (button_prv[i] != HP_BUTTON_OPEN) {
      return 0;
    }
  }
  for (i = 4; i < NUM_BTN_HISTORY; i++) {
    if (detected_fall == 0 && button_prv[i-1] == HP_BUTTON_OPEN && button_prv[i] == HP_BUTTON_CENTER) {
      detected_fall = 1;
    } else if (detected_fall == 1 && button_prv[i-1] == HP_BUTTON_CENTER && button_prv[i] == HP_BUTTON_OPEN) {
      count++;
      detected_fall = 0;
    }
  }
  if (count > 0) {
    for (i = 0; i < NUM_BTN_HISTORY; i++) button_prv[i] = HP_BUTTON_OPEN;
  }
  return count;
}

void task_get_hp_button_status(void *pvParameters)
{
  int i;
  int center_clicks;
  char str[256];
  // Center Button: event timing is at button release
  // Other Buttons: event timing is at button push
  for (int count = 0; ; count++) {
    uint8_t button = adc_get_hp_button();
    if (button == HP_BUTTON_OPEN) { // count center clicks
      button_repeat_count = 0;
      center_clicks = count_center_clicks(); // must be called once per tick because button_prv[] status has changed
      if (center_clicks > 0) {
        sprintf(str, "CENTER clicks =  %d", center_clicks);
        Serial.println(str);
      }
    } else if (button_prv[0] == HP_BUTTON_OPEN) { // push
      if (button == HP_BUTTON_D || button == HP_BUTTON_PLUS) {
        Serial.println("PLUS/D");
      } else if (button == HP_BUTTON_MINUS) {
        Serial.println("MINUS");
      }
    } else if (button_repeat_count == 10) { // long push
      if (button == HP_BUTTON_CENTER) {
        button_repeat_count++; // only once and step to longer push event
      } else if (button == HP_BUTTON_D || button == HP_BUTTON_PLUS) {
        // keep long push
        Serial.println("Long push PLUS/D");
      } else if (button == HP_BUTTON_MINUS) {
        // keep long push
        Serial.println("Long push MINUS");
      }
    } else if (button_repeat_count == 30) { // long long push
      if (button == HP_BUTTON_CENTER) {
        Serial.println("Long Long push CENTER");
      }
      button_repeat_count++; // only once and step to longer push event
    } else if (button == button_prv[0]) {
      button_repeat_count++;
    }
    // Button status shift
    for (i = NUM_BTN_HISTORY-2; i >= 0; i--) {
      button_prv[i+1] = button_prv[i];
    }
    button_prv[0] = button;
    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
}

static void init_adc()
{
  //Configure ADC
  if (adc_unit == ADC_UNIT_1) {
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten((adc1_channel_t) channel, atten);
  } else {
    adc2_config_channel_atten((adc2_channel_t) channel, atten);
  }

  //Characterize ADC
  adc_chars = (esp_adc_cal_characteristics_t *) calloc(1, sizeof(esp_adc_cal_characteristics_t));
  esp_adc_cal_value_t val_type = esp_adc_cal_characterize(adc_unit, atten, ADC_WIDTH_BIT_12, DEFAULT_VREF, adc_chars);
  //print_char_val_type(val_type);
}

// the setup function runs once when you press reset or power the board
void setup()
{
  Serial.begin(115200);
  init_adc();  
  xTaskCreate(task_get_hp_button_status, "task_get_hp_button_status", 2048, NULL, 5, NULL);
}

// the loop function runs over and over again forever
void loop()
{
}

実行例

最後に実際にボタンを押してみたときのシリアルターミナル表示を載せておきます。
ボタン判定の様子


自己紹介

自分の写真
電子工作&プログラミング、オーディオ・音楽

注目の投稿

Raspberry Pi Pico Wで電波時計を合わせる (JJY標準電波エミュレータ)

Raspberry Pi Pico Wのアプリケーションとして 最少の周辺部品で電波時計むけJJYエミュレータ(時刻合わせ用)を製作しました。 ※2023年6月6日: ソースコード修正の内容を反映させました。 時刻合わせ風景 概要 電波時計は電波が届くところで使...

QooQ