====== 5-2 スイッチを使う(digitalRead) ======
デジタル入力ができたので、今度はデジタル出力です!
Arduinoはデジタル出力で電気を流す/流さないという二つの動作を制御できるだけでなく、デジタル入力で電気が流れている/流れていないという二つの状態を「読み取る」こともできます。
今回はタクトスイッチを使って、電気信号の読み取りに挑戦します。
本項は割と長いので、疲れたら適度に休憩しつつ、無理せずご自身のペースで学習してください。
===== スイッチの解説 =====
==== スイッチの種類(オルタネートスイッチとモーメンタリスイッチ) ====
スイッチと一口に言っても、機能や動作の違う色々な種類のスイッチがあります。
スイッチのうち**「状態を切り替えてそれを保持する」ものを、「オルタネートスイッチ」**と呼びます。
一方、**「押している間だけONになり、指を離すとOFFになる」ものは「モーメンタリスイッチ」**と呼びます。
== オルタネートスイッチ ==
{{:gimmickkouza:electronic_basic:5:5-1.png?300|}}
上の図の4つのスイッチは、全部「オルタネート」です。
ON/OFFの2つの状態を切り替えるものが多いですが、2つ以上の状態を切り替えるものも存在します。
「オルタネートスイッチ」は主に電源などに使われます。
例:コタツのコードのスイッチ、部屋の照明のスイッチ
== モーメンタリスイッチ ==
{{:gimmickkouza:electronic_basic:5:5-2.png?300|}}
一方、今回使うタクトスイッチは「モーメンタリスイッチ」です。
「モーメンタリ」は、スイッチを押している間だけONになり、指を離すとOFFに戻ってしまいます。
==== タクトスイッチの特徴 ====
タクトスイッチは電子工作では非常によく使われるスイッチです。タクトスイッチについて説明します!
{{:gimmickkouza:electronic_basic:5:5-3.png?400|}}
電子工作用タクトスイッチの裏面には、ブレッドボードに挿すための脚が4本ついています。
この脚は** "くの字がボタン本体を挟んで向かい合っている者" 同士がペア**になります。
ペアになっている脚2本は常に内部で繋がっていて、片方の脚に電気が流れるともう片方の脚にも必ず電気が流れます。
左右のペアは普段は繋がっていません。
しかしボタンを押した瞬間に、左右の脚の間が繋がり、反対側に向かって電気が流れます。
{{:gimmickkouza:electronic_basic:5:5-4.png?400|}}
この性質を踏まえたタクトスイッチの使い方です。
(ちなみにこれは「プルダウン」の場合の使い方です。詳しくは後で説明します)
まず左右どちらかの脚に5Vから常に電気を流そうとします。今回は右から流すものと仮定しましょう。
そしてそれとは反対側、今回なら左側の電圧を常に測ります。
ボタンを押していなければ、電気は左側には流れないので、電圧は0Vを観測します。
これを「ボタンは押されていない」と解釈します。
{{:gimmickkouza:electronic_basic:5:5-5.png?400|}}
そしてあるタイミングでボタンが押されました!
その瞬間、右側の脚から左側の脚に向かって電気が流れます。
回路全体に電気が流れたことで、ボタンの左側でも5Vの電圧を観測しました。
これを「ボタンが押された!」と解釈します。
テスターを持ってる方はチェックしてみるといいですよ!
/*テスター持ってる方はチェックしてみて!とか書いても良いかも*/
===== 5-2-1_digitalRead_pulldown =====
==== 配線図 ====
{{:gimmickkouza:electronic_basic:5:5-2-1.png?600|}}
それでは早速回路を作っていきます。
タクトスイッチの左側に一本入っている抵抗は、「プルダウン」に必要な抵抗です。
とりあえず10kΩ(なければ1kΩ~100kΩ)の抵抗を挿しておいてください。
ブレッドボードにタクトスイッチを挿すと接触不良が起きやすいので、しっかり押し込んでください。
しっかり押し込んでも導通しないことがあるので、なんか動かないと思ったらタクトスイッチを疑ってください。
===使う部品リスト===
^ 部品 ^ 個数 ^
| タクトスイッチ | 1個 |
| 抵抗器(10kΩ) | 1個 |
==== サンプルコード解説 ====
サンプルコード5-2-1_digitalRead_pulldown.inoを開いてください。
/*SW=2は、switchPinとかのほうがいいです。SWのONOFF状態を取るint?、何回押されたか?など、はっきりわかる名前にしたほうがあとあと困らないです*/
int SWpin = 2; //変数SWpinでデジタル2番ソケットを指定
int count = 0; //押した回数を記録する変数
void setup() {
pinMode(SWpin, INPUT); //デジタル2番ソケットを入力モードにする
Serial.begin(9600);
}
void loop() {
if (digitalRead(SWpin) == HIGH) { //もし2番ソケットで5Vを観測したら
count = count + 1; //押した回数に+1をする
Serial.print("押した回数:");
Serial.println(count);
while (digitalRead(SWpin) == HIGH) { //スイッチから指が離れるまで待機する
delay(100);
}
}
}
今回のプログラムは、タクトスイッチを押した回数をシリアルモニタに表示するものです。
== 1行目 ==
int SWpin = 2; //変数SWpinでデジタル2番ソケットを指定
5-1_digitalWrite.inoの変数LEDと同じです。スイッチの電圧読み取り用のソケットを変数で指定します。
== 2行目 ==
int count = 0; //押した回数を記録する変数
今回は押した回数をカウントしたいです。なのでそれを記憶する変数countを宣言します。
== 5行目 ==
pinMode(SWpin, INPUT); //デジタル2番ソケットを入力モードにする
今回は2番のソケットをデジタル"入力"として使うので、pinMode はINPUTを指定します。
== 10行目 ==
if (digitalRead(SWpin) == HIGH) { //もし2番ソケットで5Vを観測したら
デジタル出力をする関数はdigitalWriteでした。今回は入力なのでdigitalReadを使います。
引数はデジタル入力をしたいソケットを指定します。今回は変数SWpinで2番ソケットを指定します。
HIGHは「(厳密に言うと3V以上の)電圧がかかっている)」状態です。
なので、if (digitalRead(SWpin) == HIGH){ } は、「もし2番ソケットで3V以上の電圧を観測したら、{ }内の処理を実行する」と読み解くことができます。
== 11~13行目 ==
count = count + 1; //押した回数に+1をする
Serial.print("押した回数:");
Serial.println(count);
変数countに1を足し、シリアルモニタにcountの中身を押した回数として表示します。
== 14~16行目 ==
if (digitalRead(SWpin) == HIGH) { //もし2番ソケットで5Vを観測したら
ここはちょっとわかりにくいのでしっかり解説します!
この項の冒頭で、**タクトスイッチは「ボタンを押している間だけONになる」**と説明しました。
逆に言うと「ボタンを押し込んだまま指を離さない限りずっとONのまま」ということになります。
もし、「モニタに押した回数を表示した後もボタンをずっと押しっ放し」だとどうなるでしょうか。
答えは、**「ボタンから指が離れるまで、"loop部の先頭に戻り→if文に突入し→count+1をして押した回数を表示し→loop部の先頭に戻り…"を繰り返し続ける」**です。
__Arduinoは人間の知覚よりも遥かに高速にloop部を繰り返す__ので、ボタンをちょっと押しただけでもloop部を何十回も繰り返し、結果的に1回の押下で何十回ものカウントをしてしまいます。
(試しに14~16行目を//でコメントアウトするとこれを再現できます。是非やってみてください)
これもこれで何かに使えそうですが、今回のプログラムではワンプッシュできちんとワンカウントをしてほしいです。そのために必要なのが14~16行目のwhile文です。
while (digitalRead(SWpin) == HIGH) { }
は、「2番ソケットでボタンが押されている時の電圧を観測している間は、{ }内の処理を繰り返す」と読み解けます。
このwhile文は、押した回数を表示した直後、if文を終了する直前に配置されています。
つまり、押した回数を表示した直後もずっとボタンを押し込み続けていると、このwhile文に突入します。
**このwhile文が終了しないと(=ボタンから指を離して電圧が0Vにならないと) if文を終了してloop部の先頭に戻ることができない、**そんな状態を作り上げます。
===== プルアップ/プルダウンってなんだろう =====
さて、5-2-1_digitalRead_pulldown.inoの回路を作成する際、「プルダウン」の為に10kΩの抵抗を入れる指示がありました。
この抵抗を入れないと、スイッチの動作はどうなってしまうのでしょうか。
{{:gimmickkouza:electronic_basic:5:5-6.png?600|}}
この図中の2番ソケットでは、黄色いワイヤを通じて電圧の入力を受け付けています。
ボタンを押さなければ電気は流れず「0V」……と言いたいところですがここで落とし穴です!
**5VにもGNDにも接続されてない2番ソケットはこの時電気的に不安定な状態になっています。**
この状態だと電子機器の周囲に発生している、目には見えないノイズ(雑音)を拾ってしまい、それを「ボタンが押された!」と誤認識・誤動作してしまう可能性があります。
この**誤認識を防ぐための2つの方法が「プルダウン」と「プルアップ」**です。
(ちなみに、この回路はそもそも抵抗がないのでボタンを押すと短絡するという危険もあります)
/*回路図で可視化できたほうがいいかも*/
==== プルダウンの仕組み ====
先ほどみなさんが作った回路では「プルダウン」という手法を使っています。
{{:gimmickkouza:electronic_basic:5:5-7.png?600|}}
2番ソケット行きのワイヤの手前に抵抗を挟むことで、スイッチが押されていない間は2番ソケットはGNDと直結した状態となり、安定して「0V」を観測できるようになります。
これが「プルダウン」です。
(ちなみにこの抵抗はプルダウンとしての役割以外にも、ボタンを押したときに5VがGNDに直接流れる(=短絡する)ことを防ぐ用途も兼ねています)
===プルダウンの回路図===
プルダウンは回路図で示すと、このような形になります。
{{:gimmickkouza:electronic_basic:5:pulldown.png?400|}}
----
一方の「プルアップ」はというと…
配線図とサンプルコードを別で用意しましたので、新しく見出しを立てて説明しますね!
===== 5-2-2_digitalRead_pullup =====
==== 配線図 ====
{{:gimmickkouza:electronic_basic:5:5-2-4.png?600|}}
こちらが「プルアップ」を採用した回路です。
抵抗はプルダウンと同じ、10kΩ(なければ1kΩ~100kΩ)の抵抗を挿してください。
===使う部品リスト===
^ 部品 ^ 個数 ^
| タクトスイッチ | 1個 |
| 抵抗器(10kΩ) | 1個 |
==== プルアップの仕組み ====
{{:gimmickkouza:electronic_basic:5:5-8.png?600|}}
**「プルアップ」では、回路の作り方やON/OFFの判定が「プルダウン」とは逆転します。**
プルアップを採用した本回路の動作順序です。
5Vから出発した電気は抵抗を通り、2番ソケットに常に入力されます。
つまり、**「プルアップ」の回路では、ボタンが押されていない状態が「5V」**となります。
プログラムでは入力電圧が「HIGH=ボタンOFF」と判定します(プルダウンではLOW=OFF)
そしてボタンが押されました!その瞬間、電気はスイッチの右から左を通ってGNDに流れます。この時、2番ソケットでは「0V」を観測します。
プログラムでは入力が「LOW=ボタンON」と判定します(プルダウンではHIGH=ボタンON)
なのでプルアップ用のサンプルコード5-2-2_digitalRead_pullup.inoでは、10行目の書き方はif (digitalRead(SWpin) == LOW){ } となっています。
(14行目のHIGHもLOWとなります)
===プルアップの回路図===
プルアップは回路図で示すと、このような形になります。
{{:gimmickkouza:electronic_basic:5:PULLUP.png?400|}}
==== サンプルコード解説 ====
サンプルコード5-2-2_digitalRead_pullup.inoを開いてください。
int SWpin = 2; //変数SWpinでデジタル2番ソケットを指定
int count = 0; //押した回数を記録する変数
void setup() {
pinMode(SWpin, INPUT); //デジタル2番ソケットを入力モードにする
Serial.begin(9600);
}
void loop() {
if (digitalRead(SWpin) == LOW) { //もし2番ソケットで0Vを観測したら
count = count + 1; //押した回数に+1をする
Serial.print("押した回数:");
Serial.println(count);
while (digitalRead(SWpin) == LOW) { //スイッチから指が離れるまで待機する
delay(100);
}
}
}
== 10行目 ==
if (digitalRead(SWpin) == LOW) { //もし2番ソケットで0Vを観測したら
先述の通り、プルアップではプルダウンと動作が逆転します。
プルダウンの回路でif (digitalRead(SWpin) == HIGH)だった部分は、プルアップではif (digitalRead(SWpin) == LOW) となります。
== 14行目 ==
while (digitalRead(SWpin) == LOW) { //スイッチから指が離れるまで待機する
この部分も同様に、HIGHをLOWにして判定を逆転させています。
==== 「なんで?」と思った方の為の解説 ====
{{:gimmickkouza:electronic_basic:5:5-8.png?600|}}
ところで、
「これボタン押した時2番ソケットにも電気が流れるのとちゃうん?なんで0Vなん?🤔🤔🤔」
と思いませんか?
講師は思いました。そんな方向けの解説です。
Arduinoのデジタル入力の仕組みを詳しく説明します。
デジタル入出力のソケットに対し、プログラムでpinModeをINPUTに設定すると、そのソケットはArduinoの内部で"100MΩの抵抗が直列に繋がっている状態"になります。
{{:gimmickkouza:electronic_basic:5:5-9.png?600|}}
※こちらは概念図です
この状態は「ハイインピーダンス」、「浮いている」、「フローティング」、「開放」とも呼ばれます。
ハイインピーダンス且つ5VにもGNDにも接続のない状態は不安定でノイズを拾ってしまうので、プルダウンならGNDに、プルアップなら5Vに抵抗を使って繋げて安定させる必要があります。
さて、電気の特性として、「流れやすい方に流れる」というものがあります。
**100MΩの抵抗はとてつもなくクソデカで強力な抵抗なので、弱い電気はほぼ流れません。**
5Vから出た電気は10kΩ抵抗を抜け、スイッチと2番ソケット(の先の100MΩ抵抗)に分岐します。
この時**スイッチが押されると、スイッチは導線とほぼ同じ状態(=ほぼ無抵抗)となり、電気は流れやすいスイッチの側に全部流れていきます。**
よって2番ソケットには流れず0Vとなります。
===== 5-2-3_digitalRead_input_pullup =====
**実はArduinoにはプルアップ用の抵抗がデフォルトで内蔵されています**。
この内蔵プルアップ抵抗を利用することで、ブレッドボード上の回路を簡素化できます。
今回はプルアップ/プルダウンの説明の為、敢えて最後に出しましたが__**これを使うと便利**__です。
==== 配線図 ====
{{:gimmickkouza:electronic_basic:5:5-2-6.png?600|}}
内蔵プルアップ抵抗を使う場合の配線図です。
スイッチと信号線2本で済むので簡素この上ないです。
===使う部品リスト===
^ 部品 ^ 個数 ^
| タクトスイッチ | 1個 |
ちなみに内蔵プルアップ抵抗を目に見える形にすると、こんな感じ(概念)になります。
{{:gimmickkouza:electronic_basic:5:5-10.png?600|}}
==== サンプルコード解説 ====
サンプルコード5-2-3_digitalRead_input_pullup.inoを開いてください。
int SWpin = 2; //変数SWpinでデジタル2番ソケットを指定
int count = 0; //押した回数を記録する変数
void setup() {
pinMode(SWpin, INPUT_PULLUP); //デジタル2番ソケットを内部抵抗を有効にして入力モードにする
Serial.begin(9600);
}
void loop() {
if (digitalRead(SWpin) == LOW) { //もし2番ソケットで0Vを観測したら
count = count + 1; //押した回数に+1をする
Serial.print("押した回数:");
Serial.println(count);
while (digitalRead(SWpin) == LOW) { //スイッチから指が離れるまで待機する
delay(100);
}
}
}
== 5行目 ==
pinMode(SWpin, INPUT_PULLUP); //デジタル2番ソケットを内部抵抗を有効にして入力モードにする
**内蔵プルアップ抵抗は、プログラムのpinModeをINPUT_PULLUPにすると使うことができます。**
外付け抵抗によるノーマルなプルアップ(PULLUP)と間違えないようご注意ください。
== 10・14行目 ==
if (digitalRead(SWpin) == LOW) { //もし2番ソケットで0Vを観測したら
while (digitalRead(SWpin) == LOW) { //スイッチから指が離れるまで待機す
**内蔵プルアップ抵抗を使うと必ず「プルアップ」の回路になります。**
なので、スイッチの判定は
押されてない=HIGH
押されてる!=LOW
となります。
----
プルアップ/プルダウンはちょっと難しい内容ですが、デジタル入力やI2Cを使う場合は必ずやらなければならないことですので、頭の片隅にそっと置いておいてください。
===== 応用編:カウント回数がおかしい時はチャタリングを疑う =====
まだあるのかーーー!!と思った方は、いったん休憩してくださいね。
一度に全部読まなくても、何度かに分けて読めば大丈夫です。
while文でスイッチを1回押すごとに1カウントしかしない対策をきっちりしたにも関わらず、1回押しただけでカウントが数回増えてしまう場合は、「チャタリング」が原因かもしれません。
=== チャタリングとは ===
「チャタリング」は、スイッチの構造的な仕様によって発生する誤動作です。
スイッチ類は、ボタンを押しこむことで離れた状態にある接点同士を触れさせて電気を通す構造になっています。
この接点が触れるごくわずかな時間の間に、反動によって接点が「付いたり離れたり」する状態が発生します。
人間には感知できないぐらい短時間の現象ですが、Arduinoがこれをしっかり読み取ってしまうと、意図せずHIGHとLOWが繰り返されてしまいます。
反動で「付いたり離れたり」するモデルは、このサイトさんのGIFが非常にわかりやすいです。
Arduinoのスケッチだけでスイッチのチャタリングを回避する(jumbleat):
https://jumbleat.com/2016/08/19/switch_without_chatter/
チャタリングによる誤動作は100%起きるとは限りません(誤動作が起きないこともあります)
誤動作が発生する場合や、未然に防ぎたい場合は対策が必要となります。
チャタリングは多種多様な回避方法が考案されています。
大きく分けると、プログラムを工夫して(ソフトウェア的な手法で)回避する方法と、コンデンサやシュミットトリガICなどを使う(ハードウェア的な手法の)回避方法があります。
=== ソフトウェア的なチャタリング対策 ===
ソフトウェア的な回避方法を考える上で、よく出てくる概念図を召喚します。
{{:gimmickkouza:electronic_basic:5:5-11.png?600|}}
この図はプルダウン回路でチャタリングが発生した場合の、スイッチの波形のモデル図です。
左下、「ボタンを押す前」は0V=LOWで電圧は安定しています。
「ここでボタンを押しました!」は、ボタンが押された瞬間です。
そこから右に向かって、色付けしてある部分がチャタリングの発生部分です。
色付けの部分では、電圧がHIGHとLOWを小刻みに繰り返しているのがわかります。
これがArduinoで読み取られることで、無駄にカウントが加算されてしまいます。
この色付けの区間は0.0数ミリ~0.数ミリ秒以下のきわめて短い期間です。それを抜けると、電圧は5V=HIGHで安定します。
プログラムを使った最もシンプルな回避策は、チャタリングをしている間はdelayを使ってプログラムの進行自体を止めてしまう方法です。
つまりこういうことです(↓)
{{:gimmickkouza:electronic_basic:5:5-12.png?600|}}
「ここでボタンを押しました!」は、サンプルコード5-2-1では10行目にあたります。
つまりif (digitalRead(SW) == HIGH)に突入する部分です。
サンプルコードではこの直後にcount+1処理をしますが、その前にdelay(100);を挟みます。
先ほどの図の最初のHIGHを観測した直後から値が安定するまでがチャタリングをしている期間ですが、この間は差し込んだdelayによって、指定した時間分待機する処理が始まります。この待機が終了するまでプログラムは次の行に進みません。
つまり、カウントの+1をしてif文を抜けloop部の先頭に戻ってスイッチの小刻みに変化する電圧を読んで再びif文に突入して…という動作が起きません。そしてチャタリングの終了と共にdelayの待機時間も終了する~~~というのが "理想" の動作です。
"理想"とわざわざ銘打っている通り、この回避策には難しい点があります。
それは「チャタリングが何ミリ秒続くかは事前にわからない」という点です。
{{:gimmickkouza:electronic_basic:5:5-13.png?800|}}
チャタリングの期間がわからないので、delayの待機時間は「チャタリングを確実に防げつつ、且つ最短の時間」をざっくり予想して入れることになります。(今回は(100)にしていますね)
左の図のように、delayの待機時間がチャタリングに対して短すぎる場合、チャタリングの一部によって誤入力が発生してしまいます。
逆にdelayを長く設定しすぎると、ボタンを長く押し続けていないと入力が始まりません。
これは人間的には「ボタンの反応が悪い、長押ししないと反応しない」という体感を得ます。
----
ソフトウェア的なチャタリングの回避方法は他にもたくさんあります。
delayで待機する以外のアプローチだと、
スイッチの値(HIGH=1、LOW=0)を変数に記憶し、連続で数回同じ値が出た(=安定した)時点でスイッチが押されたとみなす
https://lightning-brains.blogspot.com/2020/02/arduino.html
スイッチがHIGHになったら変数に+1をし続け、これが一定の値以上(=安定した)であればスイッチが押されたとみなす
https://jumbleat.com/2016/08/19/switch_without_chatter/
等の例があります。ぜひご自身で調べてみてください。
ソフトウェア的手法でのチャタリング回避のメリットは、回路上に部品を増やさず済むことです。
一方、プログラムが複雑になってしまうデメリットもあります。
=== ハードウェア的なチャタリング対策 ===
チャタリングはプログラムだけではなく、電子部品を使って回路の設計を工夫することで対策することもできます。
**な の で す が** 、こちらの詳細については講師の独断で本資料からはカットさせていただきます。
理由としましては、電子工作初心者にはかなり難易度が高いこと
それからきぐるみ搭載時には回路の部品数は少しでも減らしたくなる(と思う。少なくとも講師はそう)ので、実際に使う可能性は低いんじゃないかと思う(個人の感想)ためです。
比較的わかりやすい(と思う)参考リンクを掲載しますので興味のある方は挑戦してみてください。
リンク先を読んで頭痛がしてきた方はそっ閉じして休憩し、次の項に進みましょう。
チャタリングを解消する回路「シュミットトリガ」(氷ペンの日記):
https://hyoupendiary.com/eliminate-chattering-schmitt-trigger/
スイッチのチャタリングの概要 チャタリングを防止する方法(マルツ):
https://www.marutsu.co.jp/pc/static/large_order/1405_311_ph
とりあえず、__「プログラムを使わなくてもチャタリング防止できるんだな~」ということを知って頂ければOK__です。
もしいつか「チャタリング防止したいけどプログラムではやれないorやりたくない~~」というシーンに遭遇してしまうことがあれば、その時調べてチャレンジすればいいと思います。