今回の講義の目的は、外部の情報をマイコンでキャッチする方法をマスターすることです。ただし、今回扱うのはデジタル信号です。具体的には、スイッチが押されたことをマイコンで検知する、ということをやります。
目次
- 5.1 スイッチ
- 5.2 デジタル入力
- 5.3 チャタリング
- 5.4 実習
5.1 スイッチ
スイッチというのは、電線を連結したり切断したりするためのデバイスです。ボタンということもあります。スイッチには様々な種類が存在しますが、よく使うものはタクトスイッチと、トグルスイッチです。
タクトスイッチ(tactile switch)とは、ボタンの形状をしていて、押すと電線が連結され、離すと切断されます。

写真のタクトスイッチはとても小さくて、5ミリ角くらいです。このタイプのものは、そのままブレッドボードに差し込んで使うことができます。
足が4つ付いていますね。これらの足は、2つずつの2グループに分かれていて、同一のグループに属する足は、常に通電するようになっています。どの足とどの足が繋がっているかは、背面にマークがあることが多いです。写真の製品の場合には、背面に線が書いてあって、繋がっている足がわかります。マルチメータを使ってどの足とどの足が通電しているか確認しておきましょう。

タクトスイッチが押された時にLEDが光る回路は以下のようになります。電源と抵抗の間にあるのが、タクトスイッチの記号です。

タクトスイッチをブレッドボードに挿す時には以下のように真ん中の仕切りをまたぐようにすると良いでしょう。

トグルスイッチ(toggle switch)とは、電線の連結と切断を切り替えられるようにしてあるスイッチです。どちらかを選択すると、他を選択し直すまで同じ状態を保ちます。

スイッチの電子部品記号は、いくつかあります。この講義では以下の2つを使います。

おまけとして、ディップスイッチの写真も載せておきます。これは、複数のトグルスイッチが1つのパッケージになったものです。

5.2 デジタル入力
スイッチの状態を感知するというのは、マイコンにとってはデジタル信号の入力を行うことです。ピンにかかる電圧は3.3Vか0Vのどちらかになり、3.3Vならば1、0Vならば0という数値で認識します。
スイッチは、直感的にわかりやすいデバイスですが、マイコンへの入力として使うときには少し注意が必要です。以下のように回路を作ると、タクトスイッチが押されたかどうかをマイコンで検知できそうに思いますよね。

しかし、これは正しくありません。タクトスイッチが押されているときには GPIO 17 が3.3Vにつながりますから正しく検知できます。しかし、タクトスイッチが離されているときには、GPIO 17はどこにも繋がっていない状態になります。このような状態を、ピンがオープンになっていると言い、マイコンのピンの電圧は周りのピンの影響を受けて安定しません。すると、ピンの電圧が3.3Vと計測された場合に、スイッチが押されていて3.3Vにつながっているのかオープンで安定しないために3.3Vとなっているのかが見分けられません。
また、以下のような回路もよく見られる間違いです。

この回路では、スイッチを押したときに3.3Vとグランドがショートしてしまうので大変危険です。火花が飛んだり、導線が焼き切れて焦げた匂いがしたり、回路やマイコンボードが破損する恐れがあります。
正しくは以下のような回路を組みます。回路に使われる抵抗は、10kΩがよいでしょう。

GPIO 17 をデジタル入力として使用する際には、マイコンの中では以下のようなことが行われています。これはピンをデジタル入力として使う場合で、前回実験したデジタル出力の場合には当てはまりません。

GPIO 17 をデジタル入力に使っている時には、 GPIO 17 の先は非常に大きな抵抗を介してグランドに接続されています。図では仮に3MΩ(メガオーム)としています。このような状態をハイインピーダンスと呼びます。 GPIO 17 の内部では、マイコンがこの大きな抵抗にかかる電圧を計測しています。
この例では、10KΩの抵抗と3MΩの抵抗は並列に接続されていますね。並列に接続されているということは、どちらにも同じ電圧がかかるということです。タクトスイッチが押されると、並列回路の部分に3.3Vの全てがかかるはずです。よって、 GPIO 17 では3.3Vが計測されます。タクトスイッチが離されている時には、抵抗に電流は流れませんので両端で電圧差はなく、 GPIO 17 はGROUNDと同じ電圧になりますから0Vになるわけです。
このような回路をプルダウン回路と呼び、10KΩの抵抗をプルダウン抵抗と呼びます。
プルダウン回路とは逆に、タクトスイッチが離されている時に3.3Vが計測され、押されている時に0Vとなる回路も作れます。

これをプルアップ回路と呼び、10KΩの抵抗をプルアップ抵抗と呼びます。この場合、スイッチが離されている時に3.3Vが計測されます。
今度は、以下の図のように考えます。

タクトスイッチが押されていない状態では直列回路です。10KΩにかかる電圧と3MΩにかかる電圧の和が3.3Vになります。こういう回路を分圧回路と呼ぶのですが、これはもう少し先の回で説明したいと思います。ここでは、10KΩと3MΩの差があまりにも大きいので、3.3Vのほとんどが3MΩの方にかかることになる、としておきます。
タクトスイッチが押された状態では直列回路と並列回路の組み合わせです。並列回路になっている抵抗は3MΩとタクトスイッチです。タクトスイッチは、ほぼ0Ωと思って良いはずですね。この2つの抵抗を組み合わせた抵抗値を求めてみましょう。
1/R = 1/R1 + 1/R2
R1 が0Ω、 R2 が3MΩです。 1/R1 はほぼ無限大ですから、 1/R1 + 1/R2 もほぼ無限大です。ということは、 R はほとんど0Ωだということです。よって、10KΩに3.3Vのほぼ全てがかかり、3MΩにかかる電圧は0Vです。
スイッチの応用例
それでは実際に回路を作って試してみましょう。プルダウン回路にしてみます。回路図は、前の章で説明したものを参照してください。
プログラムは、以下の通りです。ファイル名はbutton_pull_down.py
にしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from gpiozero import Button from time import sleep button = Button(17, pull_up=None, active_state=True) def main_loop(): while True: if button.is_pressed: print('pressed') else: print('not pressed') sleep(0.1) if __name__ == '__main__': main_loop() |
button_pull_down.py
を実行すると、not pressed
が連続して出力されます。タクトスイッチを押すとpressed
と出力されるはずです。
このプログラムは GPIO Zero のButtonクラスを使っています。ボタンからの入力を監視しているのは以下のオブジェクトです。
1 |
button = Button(17, pull_up=None, active_state=True) |
17
はGPIO 17の17です。pull_up=None
は Raspberry Pi のGPIOに内蔵されているプルアップ・プルダウン抵抗を無効にするための指定です。button.is_pressed
はbool型の変数で、ボタンが押されているときはTrue
になり、押されていないときにはFalse
になります。より詳細は、以下の GPIO Zero のドキュメントを参照してください。
https://gpiozero.readthedocs.io/en/stable/api_input.html#button
ここまでの方法は、原理に沿った原則的な方法ですが、 Raspberry Pi にはマイコン内部にあらかじめ載っている抵抗を使うことによって、もっと簡易に回路を作る方法があります。Buttonオブジェクトの引数をpull_up=False
にすると、マイコン内部のプルダウン抵抗が有効になります。つまり、ブレッドボード上に10KΩの抵抗をつながなくてもよくなるということです。
GPIOに内蔵されているプルダウン抵抗を使う場合の回路図は以下のようになります。

Buttonオブジェクトは以下のように作成します。
1 |
button = Button(17, pull_up=False) |
GPIOに内蔵されているプルアップ抵抗を使う場合の回路図は以下のようになります。

Buttonオブジェクトは以下のように作成します。
1 |
button = Button(17, pull_up=True) |
上記のpull_up=True
はデフォルト値なので、以下のようにしても同じことになります。
1 |
button = Button(17) |
5.3 チャタリング
先ほどの回路にLEDを追加して、以下のような回路を作りました。

これは、 Raspberry Pi の内部抵抗を使ってタクトスイッチをプルアップ回路にして、 GPIO 27 にLEDをつないだものです。プログラムは、以下のように作ったとしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from gpiozero import Button from gpiozero import LED from time import sleep button = Button(17) led = LED(27) def main_loop(): while True: if button.is_pressed: led.toggle() if __name__ == '__main__': main_loop() |
このプログラムは、タクトスイッチが押されるたびにLEDの状態が変わることを意図して作ったものです。led.toggle()
というメソッドは、LEDの状態を反転させます。LEDが消灯している時に呼び出されると点灯し、点灯している時に呼び出されると消灯します。
しかし、このプログラムは思ったように動かないはずです。このプログラムは、タクトスイッチを押しっぱなしになっているときに、何度もled.toggle()
が呼び出されてしまうからです。プログラムは高速に動作しているので、普通にボタンを押しただけでも何度もループしてLEDのオンオフが反転してしまいます。
LEDがコロコロ反転してしまうのは困るので、タクトスイッチが押された瞬間を検知して、そのときだけ反転させることにします。考えかたとしては、 GPIO 17 の値を読んだ時にそれがTrue
で前回読んだ時にはFalse
だった場合が、タクトスイッチが押された瞬間と考えられそうです。これをもとにプログラムで表現してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from gpiozero import Button from gpiozero import LED from time import sleep button = Button(17) led = LED(27) def main_loop(): previous = False current = False while True: previous = current current = button.is_pressed if previous == False and current == True: led.toggle() if __name__ == '__main__': main_loop() |
このようにすると、previous
には前回ボタンの状態を読んだときの値が、current
には今回ボタンの状態を読んだときの値が記憶されることがわかるでしょうか。if文では、前回がFalse
で今回がTrue
のときにLEDを反転させています。
今、回路はプルアップ回路なので、図で表すと以下のような時ですね。

しかし、実際に実験してみると、これも思ったようには動かない場合があります。タクトスイッチを押してもLEDの状態が変わらない場合です。これは、スイッチを使うときに起きるチャタリング(chattering)という現象が原因です。英語だとchatteringよりもbounceと表現することが多いです。
スイッチは、金属板に金属板を押し付けることによって電線を連結します。この時、非常に短い時間で金属板同士が弾きあいます。結果として、以下の図で示すような現象が起こります。この図はプルダウン回路ではスイッチが押されたとき、プルアップ回路ではスイッチが離された時の様子です。

閾値(しきいち)は、この値以上あるいは以下でHIGHやLOWを認識するという境界です。この図で示すとおり、人間が1回だけスイッチを押したつもりでも、複数回のHIGHとLOWが起こります。この結果、人が意図しないスイッチのオンオフが起こってしまいます。そのために、先ほどのプログラムは、意図通りに動かないときがあるのです。
チャタリングはスイッチを押した時にも、離した時も起こります。スイッチによっても違いますが、授業で配布したタクトスイッチの場合、離した時に起こることの方が多いように思います。※まったく起こらない場合もあります。
チャタリングは、ハードウェアの工夫でも、ソフトウェアの工夫でも防ぐことができます。実際の工業製品の場合には、ハードとソフトの工夫を組み合わせることが多いと思います。この講義では、ソフトウェアの工夫のみを行います。ただし、ソフトウェアの工夫のみでチャタリングを完全に除去することはなかなか難しいです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
from gpiozero import Button from gpiozero import LED from time import perf_counter button = Button(17) led = LED(27) timediff = 0.0 def toggle_led(): print('pressed') led.toggle() def main_loop(): previous = False current = False previous_time = perf_counter() print('initial counter:', previous_time) while True: previous = current current = button.is_pressed if previous == False and current == True: now = perf_counter() timediff = now - previous_time print('timediff=', timediff) previous_time = now if timediff > 0.05: # 50ms toggle_led() if previous == True and current == False: previous_time = perf_counter() if __name__ == '__main__': main_loop() |
このプログラムの意図は、タクトスイッチが押されたら前回ボタンが押された時からの時間を計測し、それが50msよりも小さければ無視するというものです。チャタリングより生ずる意図しないタクトスイッチのon/offは非常に短い間隔で起こります。ここでは50msより小さければ人間が操作してのではないと思うわけです。
time.pref_counter()
は、このプログラムが起動された時からの経過時間を秒で返します。秒よりも小さい単位は小数点以下の数字で表されます(ミリ秒やマイクロ秒)。これを使って、前回GPIO 17の状態が変わった時から今回状態が変わった時の差(timediff)を求めています。
GPIO Zero のButtonクラスには、これと同じ原理でチャタリングを回避する機能が実装されています。よって、上記のようなコードは書く必要がなく、以下のようにコンパクトに実装できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
from gpiozero import Button from gpiozero import LED button = Button(17, bounce_time=0.05) led = LED(27) def toggle_led(): print('pressed') led.toggle() def main_loop(): previous = False current = False while True: previous = current current = button.is_pressed if previous == False and current == True: toggle_led() if __name__ == '__main__': main_loop() |
以下の部分が、どのくらい短い間隔の変化を無視するかを指定しているところです。
1 |
button = Button(17, bounce_time=0.05) |
bounce_time=0.05
の0.05
が50msの指定です。
さらに、 GPIO Zero のButtonクラスには、ボタンが押された状態がFalse
からTrue
に変化するときに自動で呼び出される関数(イベントリスナー関数)を登録する機能があります。これを用いると、以下のようにさらにコンパクトに実装できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from gpiozero import Button from gpiozero import LED button = Button(17, bounce_time=0.05) led = LED(27) def toggle_led(): print('pressed') led.toggle() def main_loop(): button.when_pressed = toggle_led while True: pass if __name__ == '__main__': main_loop() |
button.when_pressed
には、ボタンが押された状態がFalse
からTrue
に変化するときに実行される関数を定義しておけます。これを定義すると、ボタンが押されたときに自動的に関数を呼び出すことができます。ここでは、toggle_led()
という関数を呼び出すようにしています。
メインループの本体は空です。ただし、Pythonは本体が空の繰り返しを定義できません。pass
というのは、何もしない命令です。このpass
を使うと、実質的に空の繰り返しが定義できます。
5.4 実習
実験1 チャタリングを観察する
チャタリング対策を施していない5.3節の2番目のサンプルプログラムを使って、チャタリングが発生するのかしないのか観察してください。配られたタクトスイッチによって、発生しやすかったりまったく発生しなかったりします。どちらの結果であっても、結果をできる限り正確にレポートしてください。発生するスイッチであっても、発生しやすいタイミング・押しかた離しかたなどがあるのが普通なので、発生しやすい・発生しにくいのはどんな条件のときかをまとめてください。2つタイプの違うタクトスイッチを配付していますので、両方のスイッチで実験してください。
実験2 ボタンでカウントアップ
タクトスイッチを押すたびに、カウンタが1つずつ増えて行き、15までカウントアップしたら次は0に戻るようにします。カウンタの値は4つのLEDを使って2進表現します。
実験1でチャタリングが発生した場合は、チャタリング対策を施していないプログラムでボタンが押された回数をカウントしてみると、チャタリングが原因の正しくない動作が観察できておもしろいでしょう。ただし、最終的には、チャタリング対策を施したプログラムをレポートに報告してください。
第4回の実験5で作ったbin_led()
関数を再利用するとよいでしょう。
実験3 ストップウォッチを作る
ストップウォッチを作ってみましょう。使う部品は、タクトスイッチ1つとLED1つです。プログラムを起動すると待機中になります。このときLEDは消灯しています。タクトスイッチを押すと計測が開始されます。このときLEDは点灯します。計測中にタクトスイッチが押されると計測終了です。計測が終了すると、計測結果がターミナルの画面に出力されてLEDは消灯します。
時間計測は、pref_counter()
を工夫して使うと可能です。 計測結果の出力は分と秒とミリ秒に分けて表示します。どのようにすれば、分と秒とミリ秒に分けられるのかを考えてみましょう。
1 2 3 |
started stopped 2 分 18 秒 527 |
ストップウォッチの状態は以下のように変化します。

この状態遷移図では、開始、待機中、計測中という3つの状態があります。矢印は状態を遷移させるイベントです。「スイッチが押される」というのがイベントで、「リセット」はその際に行われるアクションです。開始状態から待機中への矢印にはイベントが書かれていませんね。これは、無条件で即時に遷移することを意味します。つまりこれは、プログラムが起動すると即座に待機中になることを表しています。
状態を識別するためには、変数を用意する必要があります。例えば、is_active
という変数を用意して、is_active = False
ならば待機中、is_active = True
ならば計測中という具合に使います。
実験+αとして考えられること
もう1つタクトスイッチを持っていると思うので、そちらも使ってラップタイムを計測できるストップウォッチにしてみたりということが考えられます。また、チャタリングが生じると困るシステムには、ほかにどんなものがあるか考察したり調査したりしてもよいでしょう。
— by 飯田 周作、沼 晃介、石井 健太郎 専修大学ネットワーク情報学部