ESP32をはじめとする小型マイコンボードにおいて、音楽プレーヤーなどを製作する際、操作インターフェイスとしてイヤホンボタンを活用するようにすればスイッチなどの実装の手間が省けます。スマホに使用できる市販のマイク付きイヤホンではAndroid用とiPhone用に分かれており、このうちAndroid用では、通常1ボタンまたは3ボタンのものが入手できるようです。このうちAndroid用3ボタンのものを使用してESP32向けに簡単なボタン操作の検出機能を実装してみます。
Androidイヤホンボタンの仕様
Androidイヤホンボタンの仕様については以下にAndroid仕様の説明(公式)があるので参考にしました。
この記事ではわかりやすく以下のように読み替えます。
- ボタンA : CENTERボタン
- ボタンB : PLUSボタン
- ボタンC : MINUSボタン
- (ボタンD : Dボタン)
回路としては、イヤホンの第4番目の端子(一番根元に近い側の端子)としてMIC端子が割り当てられており、どのボタンも押されていない場合は2.2Vにバイアスされているのですが、ボタンを押した場合はGNDとそれぞれのボタンに対応する接続された抵抗値によりMIC端子の電位が変わることにより、どのボタンが押されたか検出できる仕組みとなっています。
したがって、ESP32ではこのマイク端子をADC入力に接続して、ボタン押下の判定を行うこととします。
判別できる操作の数
ボタンの数が3つですのでそんなに複雑な操作はできませんが、超シンプルな音楽プレーヤーを想定して以下の操作ができるように考えてみます。もちろん機能の対応はあくまで一例です。一つの操作に複数の機能が割り当てられている項目は基本的には画面遷移などのモードにより対応機能を切り分けることを想定しています。
部品リスト
- 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ボタン : ボタンを離して一定時間経過したとき(長長押し以外)
シリアルメッセージの表示ではなく実際に機能をキックするように変更する際には、必要に応じて情報をキューを使用して受け渡すようにすると良いと思います。
サンプルプログラム
以下のプログラムは、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() { }
実行例
最後に実際にボタンを押してみたときのシリアルターミナル表示を載せておきます。
ボタン判定の様子 |
0 件のコメント:
コメントを投稿