前回の話
こんにちは!サイボウズ Developer Pioneer チームです。 この記事では、チーム内のハードウェア勉強会「Arduinoで作る1x3ボタン」の内容を紹介します。
前回の「HW初心者が自作スイッチ(1x3ボタン)を作成した話-電子回路・はんだ付け編-」に続いて、ソフトウェア側を紹介します。1x3ボタンの全体を確認したい方は、前回の記事「電子回路・はんだ付け編」も合わせてご確認ください。
完成形
改めて作成する1x3ボタンの完成形を確認しておきましょう。 3つのボタンから、「a」「b」「c」が入力できるボタンを作成していきます。
開発環境の準備
Arduino IDEの準備
Arduino IDE をインストールして、開発環境を作ります。開発環境の準備手順を紹介している記事はたくさんありますので、ここでは説明を割愛します。
Arduino IDEでは、C/C++言語を基にしたプログラミング言語を使用します。 また、IDE内での用語ですが、「プログラム」のことを「スケッチ」と呼びます。
Arduino言語の基本
Arduinoで使用する2つのメイン関数を紹介します。
1. setup()関数 / 初期化
ボードを起動したタイミングで、最初に1回だけ呼ばれる関数です。 この関数では、定数や関数を宣言しています。
今回は使用するArduinoのピン番号やキーマップ関数(キーに対応する値を返す関数)等を定義しています。
// 初期化 void setup(void) { // 定数や関数を定義しておく }
2. loop()関数 / 繰り返し
setup関数が読み込まれた後に実行される関数です。電源が入っている間は、繰り返し実行されます。
今回は、1行3列のボタンのステータスを繰り返しチェックし、押下されたボタンがあれば、対応する関数を呼び出し値を返す処理を実装します。
void loop(void) { // 繰り返し処理 }
ソースコードとポイント解説
開発環境が用意できたら、ボタンを押した時に「a」「b」「c」が入力されるプログラムを書いていきます。
完成したソースコード
全てのコードはこちらを確認してください
/* HID-Projectで使える関数は以下を参照の事 https://www.arduino.cc/reference/en/language/functions/usb/keyboard/ サンプルプログラム https://github.com/NicoHood/HID/blob/master/examples/Keyboard/ImprovedKeyboard/ImprovedKeyboard.ino https://github.com/NicoHood/HID/tree/master/examples/Keyboard コードの定義 https://github.com/NicoHood/HID/blob/master/src/KeyboardLayouts/ImprovedKeylayouts.h#L61 */ #include <HID-Project.h> // デバッグフラグ #define DEBUG 1 // チャタリング対策のdelay 20ms const int DELAY = 20000; const int row = 1; // 行(横) const int col = 3; // 列(縦) const int row_pin[row] = { 15 }; // 行(横)で使用するピン const int col_pin[col] = { 7, 8, 9 }; // 列(縦)で使用するピン // 関数の宣言 void func00(void); void func01(void); void func10(void); // キーマップマトリックス void (*const keymap[][col])(void) = { { &func00, &func01, &func10 } }; // 連続入力を防ぐフラグ bool current_status[row][col]; bool before_status[row][col]; void setup(void) { #if DEBUG /* Serialで使える関数は以下を参照 https://www.arduino.cc/reference/en/language/functions/communication/serial/ */ // シリアルモニターを9600ポートで開く Serial.begin(9600); #endif Keyboard.begin(); // 行のピンのモードをセット for (int x = 0; x < row; x++) { pinMode(row_pin[x], OUTPUT); } // 列のピンのモードをセット for (int y = 0; y < col; y++) { pinMode(col_pin[y], INPUT_PULLUP); /* INPUT_PULLUPに設定されたピンは digitalRead() すると HIGH が返る ピンをGNDやマイナスに接続した状態で digitalRead() すると LOW が返る */ } // 行列のステータスを初期値をセット。 // HIGH = release // LOW = push for (int x = 0; x < row; x++) { // 行ループ for (int y = 0; y < col; y++) { // 列ループ current_status[x][y] = HIGH; // 初期値HIGHにしておく before_status[x][y] = HIGH; // 初期値HIGHにしておく } // 行ピンをHIGH(5V)にセット digitalWrite(row_pin[x], HIGH); } } void func00(void) { #if DEBUG Serial.println("func00"); #else Keyboard.press('a'); Keyboard.press(KEY_ENTER); #endif } void func01(void) { #if DEBUG Serial.println("func01"); #else Keyboard.press('b'); Keyboard.press(KEY_ENTER); #endif } void func10(void) { #if DEBUG Serial.println("func10"); #else Keyboard.press('c'); Keyboard.press(KEY_ENTER); #endif } // チャタリング対策 // DELAYマイクロ秒間ピンの状態が同じならその値を返す // 状態が変わっていたら、最初に読んだピンの値を返す int pinRead(int pin) { int first_status = HIGH, current_status = HIGH; delayMicroseconds(500); // 取り敢えず500msウェイト first_status = digitalRead(pin); int count = 0; // 読込回数カウンタ int t = 0; // 経過時間 while (t < DELAY) { /* DELAY時間の間ループ、50ms毎にDELAY / 50回分ピンを読み出す この間10回連続でピンの値が同じであれば値が安定したとみなして値を返す */ delayMicroseconds(50); t += 50; current_status = digitalRead(pin); if (current_status == first_status) { if (count <= 10) { // 10回未満のため読込カウンタを増やしてループを繰り返す count++; continue; } return first_status; // 10回繰り返し、同じならばチャタリングではないと判断する } count = 0; // チャタリングと判断して、読込回数カウンタをリセット } return current_status; } /* 初期値では、行ピン(row_pin[]])、列ピン(col_pin[])ともHIGHにしておく 行ピンの状態をLOWにすることにより、キーが押された時に0v回路(プルアップ回路)を作ることでキーの押下を検知する 探索が終わった行ピンはHIGHに戻す */ void loop(void) { /* このループでは、行ピン(x)を順繰りLOWに変更し ボタンが押された事によってLOWにされた列ピン(y)を探索 これで判明した[行(x)][列(y)]に割り当てられた動作を実行する */ for (int x = 0; x < row; x++) { // スキャン(探索)する行ピン(x)をLOWにする digitalWrite(row_pin[x], LOW); for (int y = 0; y < col; y++) { // current_status[x][y] = digitalRead(col_pin[y]); current_status[x][y] = pinRead(col_pin[y]); if (current_status[x][y] != before_status[x][y]) { // 連続入力を防ぐ if (current_status[x][y] == LOW) { // キーが押されて行ピン(x)のLOWと繋がり、LOWになった列ピン(y)を探す keymap[x][y](); } else { Keyboard.releaseAll(); } before_status[x][y] = current_status[x][y]; } } // スキャン(探索)が終わったのでHIGHに戻す digitalWrite(row_pin[x], HIGH); } }
ポイント解説
今回のボタンを実装する上で重要になるポイントを2つ解説します。
チャタリングの対応
チャタリングとは?
簡単に言うと、チャタリングとはキーを押下した時の値の二重入力のことです。 チャタリングをそのままにすると、「a」と入力したい箇所が二重入力になってしまい「aa」や「aaa」と入力されてしまいます。
重複入力を避けるために、チャタリングを防止していきます。チャタリング防止には、ハードウェア、ソフトウェアでそれぞれいくつか方法がありますが、今回はソフトウェア側でチャタリングを回避していきます。
チャタリング防止の実装
- 10回繰り返して同じ値であれば安定しているとみなす
- 安定 × 10回以上 あればキーが押されていると判断する
- 安定していない場合、チャタリング中と判断し、countを0にする
// チャタリング対策 // DELAYマイクロ秒間ピンの状態が同じならその値を返す // 状態が変わっていたら、最初に読んだピンの値を返す int pinRead(int pin) { int first_status = HIGH, current_status = HIGH; delayMicroseconds(500); // 取り敢えず500msウェイト first_status = digitalRead(pin); int count = 0; // 読込回数カウンタ int t = 0; // 経過時間 while (t < DELAY) { /* DELAY時間の間ループ。50ms毎にDELAY / 50回分ピンを読み出す この間10回連続でピンの値が同じであれば、値が安定したとみなして値を返す */ delayMicroseconds(50); t += 50; current_status = digitalRead(pin); if (current_status == first_status) { if (count <= 10) { // 10回未満のため読込カウンタを増やして、ループを繰り返す count++; continue; } return first_status; // 10回繰り返し、同じならばチャタリングではないと判断する } count = 0; // チャタリングと判断して、読込回数カウンタをリセット } return current_status; }
ボタン押下を判断の対応
ボタンが押下されたかは、ピンのステータス(HIGHとLOW)で判断していきます。
具体的には、Arduino言語のデジタル入出力関数
pinMode(pin, mode)
、digitalWrite(pin, value)
、digitalRead(pin)
を組み合わせてボタンの押下を判断していきます。まずはデジタル入出力関数のそれぞれの役割を確認しておきましょう。
デジタル入出力関数
関数名 | 説明 |
---|---|
pinMode(pin, mode) | 指定されたpinの動作を入力(INPUT)または出力(OUTPUT)に設定する |
digitalWrite(pin, value) | 指定したピンにHIGHまたはLOWを出力する
|
digitalRead(pin) | 指定したピンの値を読み取り、HIGHまたはLOWが返る |
プルアップ回路
仕組みを理解するためには、プルアップ抵抗を理解する必要があります。そもそも回路は繋がっていない(ボタンが押されず浮いている)状態では、不安定な値になってしまいます。そのため、ボタンが押されず浮いている時に敢えて5V供給し、ボタンが押下された時に0Vにする回路を作成していきます。このような回路をプルアップ回路といいます。
つまり、ボタンを押した時に回路に電流が流れる訳ではなく、元から電流を流しておいてボタンを押したタイミングで0Vにしています。普段の生活でスイッチオン・オフの感覚で考えると間違えやすいため、プルアップ回路はちゃんと理解しておきましょう!
実装手順
まず、2ピンをOUTPUTに、7,8,9ピンをINPUT_PULLUPに設定しておきます。
次に2ピンをLOW = 0Vにセットし、擬似的なGNDに変更します。
このタイミングでボタンを押下するとタクトスイッチ部分が接続され、回路に電流が流れます。電流は高い方から低い方に流れるため、今回の場合は5v(7,8,9ピンのいずれか)から0V(2ピン)に向かって流れます。 電流が流れたピンはdigitalReadするとLOWステータスが返ってきます。そのため、ステータスがLOWになっていると、該当するピンのボタンが押下されたと判断できる仕組みです。
実際にコードを見てみましょう。
void loop(void) { /* このループでは、行ピン(x)を順繰りLOWに変更し 内部ループでボタンが押された事によって、LOWにされた列ピン(y)を探索する これで判明した[行(x)][列(y)]に割り当てられた動作を実行する */ for (int x = 0; x < row; x++) { // スキャン(探索)する行ピン(x)をLOWにする // LOW = 0V<200b> ほぼGNDとして機能させる digitalWrite(row_pin[x], LOW); // 今回は3回繰り返す for (int y = 0; y < col; y++) { // current_status[x][y] = digitalRead(col_pin[y]); // ex: current_status[1][1] = pinRead(1); HIGH or LOWが入る current_status[x][y] = pinRead(col_pin[y]); if (current_status[x][y] != before_status[x][y]) { // 連続入力を防ぐ // ステータスLOWということは押下されているため、keymapを返す if (current_status[x][y] == LOW) { // キーが押されて、行ピン(x)のLOWと繋がりLOWになった列ピン(y)を探す keymap[x][y](); } else { // 押されているkey(Keyboard.press中のキー)を全て離す Keyboard.releaseAll(); } // 現在のステータスを前のステータスに代入する before_status[x][y] = current_status[x][y]; } } // スキャン(探索)が終わったので行ピンをHIGHに戻す digitalWrite(row_pin[x], HIGH); } }
動作確認
プログラムを用意できたら、最後にArduinoの左上の→
ボタンから「書き込み」を行なってください。書き込みが終わったら、実際に1x3ボタンから「a」「b」「c」が入力できているか確認してみましょう。
おまけ
他の特殊キーを適用させたい場合は、以下のドキュメントからキーを割り当て、再度書き込みを行なってみてください。
Keyboard Modifiers and Special Keys - Arduino Reference
コラム:2x2ボタンを考えてみよう
先程のボタン押下を判断するプログラムで、2ピンをずっとLOWにしておけば良いのに、なぜわざわざ繰り返しステータスをLOWとHIGHに切り替えるのか疑問を持った方もいるのではないでしょうか。
LOWとHIGHに切り替えている理由は、1x3や1x4ボタンであれば、2ピンをずっとLOWに設定しておいても問題ないのですが、2x2や3x3など行ピンが増えるとどのピンが押下されたか判断できなくなってしまうためです。
2x2ボタンの回路は、①6ピン-14ピン ②6ピン-15ピン ③7ピン-14ピン ④7ピン-15ピンとすることで4つのボタンを実現しています。
もしGND側の14,15ピンを0Vにしたままにすると、どうなるでしょうか。
左から2番目のボタンを押したはずなのに、6ピンに接続されている回路で0Vが検知されてしまい、6-14ピンか6-15ピンのどちらのボタンが押下されたのか判断できなくなってしまいます。 そのため、GND側のHIGHとLOWを繰り返すことで、押下したピンを1つだけ判断できる仕組みになっています。
参考リンク
筆者が難しく感じた点と、理解するために参考にしたリンクを紹介します。
- アナログ回路とデジタル回路の違い
- INPUT_PULLUPとプルアップ回路
0Vになる回路を作ることで、ボタンの押下を判断していること
さいごに
以上でArduinoを使用した1x3ボタンを実装することができました! 他のモジュールと組み合わせて応用してみたり、2x2ボタンを応用させることで60%キーボードも自作できたりしそうですね。