今回の講義の目的は、外部から情報をマイコンで受け取る方法をマスターすることです。ただし、今回扱うのはデジタル信号です。具体的には、スイッチが押されたことをマイコンで検知する、ということをやります。
目次
- 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をマイコンボードからとっている場合には、マイコンボードが破損する恐れがあります。
正しくは以下のような回路を組みます。

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

GPIO 17をデジタル入力に使っている時には、GPIO 17の先は非常に大きな抵抗を介してグランドに接続されています。図では仮に3MΩ(メガオーム)としています。このような状態をハイインピーダンスと呼びます。GPIO 17の内部では、マイコンがこの大きな抵抗にかかる電圧を計測しています。
この例では、10KΩの抵抗と3MΩの抵抗は並列に接続されていますね。並列に接続されているということは、どちらにも同じ電圧がかかるということです。タクトスイッチが押されると、並列回路の部分に3.3Vの全てがかかるはずです。よって、GPIO 17では3.3Vが計測されます。タクトスイッチが離されている時には、GPIO 17はGROUNDと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に内蔵されているプルアップ・プルダウン抵抗を無効にするための指定です。これをpull_up=False
にすると内部のプルダウン抵抗が有効になります。つまり、ブレッドボード上に10KΩの抵抗をつながなくてもよくなるということです。
button.is_pressed
はbool型の変数で、ボタンが押されているときはTrue
になり、押されていないときにはFalse
になります。
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 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() |
このプログラムは、タクトスイッチが押されるたびにLEDの状態が変わることを意図して作ったものです。led.toggle()
というメソッドは、LEDの状態を反転させます。LEDが消灯している時にタクトスイッチを押すと点灯し、点灯している時に押すと消灯して欲しいです。
タクトスイッチが押しっぱなしになっているときに、LEDがコロコロ反転してしまうのは困るので、タクトスイッチが押された瞬間を検知して反転させることにします。GPOP 17の値を読んだ時にそれがTrue
で前回読んだ時にはFalse
だった場合が、タクトスイッチが押された瞬間と考えられそうですね。
今、回路はプルアップ回路なので、図で表すと以下のような時ですね。

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

閾値(しきいち)は、この値以上あるいは以下でHIGHやLOWを認識するという境界です。この図で示すとおり、人間が1回だけスイッチを押したつもりでも、複数回のHIGHとLOWが起こります。この結果、人が意図しないスイッチのオンオフが起こってしまいます。そのために、先ほどのプログラムは、意図通りに動かなかったのです。※ 実際には、Raspberry PiとGPIO Zeroの組み合わせではあまりチャタリングが起こりません。
チャタリングはスイッチを押した時にも、離した時も起こります。スイッチによっても違いますが、授業で配布したタクトスイッチの場合、離した時に起こることの方が多いように思います。
チャタリングは、ハードウェアの工夫でも、ソフトウェアの工夫でも防ぐことができます。実際の工業製品の場合には、ハードとソフトの工夫を組み合わせることが多いと思います。この講義では、ソフトウェアの工夫のみを行います。ただし、ソフトウェアの工夫のみでチャタリングを完全に除去することはなかなか難しいです。
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 |
from gpiozero import Button from gpiozero import LED from time import perf_counter button = Button(17) led = LED(27) timediff = 0.0 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 led.toggle() 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 |
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() |
以下の部分が、どのくらい短い間隔の変化を無視するかを指定しているところです。
1 |
button = Button(17, bounce_time=0.05) |
bounce_time=0.05
の0.05
が50msの指定です。
button.when_pressed
は、タクトスイッチが押されたら実行される関数を定義しておけます。これを定義すると、ボタンが押されたときに自動的に関数を呼び出すことができます。ここでは、toggle_led()
という関数を呼び出すようにしています。
メインループの本体は空です。ただし、Pythonは本体が空の繰り返しを定義できません。pass
というのは、何もしない命令です。このpass
を使うと、実質的に空の繰り返しが定義できます。
5.4 実習
実験3はオプションです。実験1と実験2ができて80点とします。実験3までできると100点です。
実験1. ボタンでカウントアップ
タクトスイッチを押すたびに、カウンタが1つずつ増えて行き、7までカウントアップしたら次は0に戻るようにします。カウンタの値は3つのLEDを使って2進表現します。
第4回講義資料の実験5で作ったbin_led()
関数を使います。

実験2. ストップウォッチを作る
ストップウォッチを作ってみましょう。使う部品は、タクトスイッチ1つとLED1つです。プログラムを起動すると待機中になります。このときLEDは消灯しています。タクトスイッチを押すと計測が開始されます。このときLEDは点灯します。計測中にタクトスイッチが押されると計測終了です。計測が終了すると、計測結果がターミナルの画面に出力されてLEDは消灯します。

計測結果の出力は分と秒とミリ秒に分けて表示します。
1 2 3 |
started stopped 2 分 18 秒 527 |
ストップウォッチの状態は以下のように変化します。

この状態遷移図では、開始、待機中、計測中という3つの状態があります。矢印は状態を遷移させるイベントです。「スイッチが押される」というのがイベントで、「リセット」はその際に行われるアクションです。開始状態から待機中への矢印にはイベントが書かれていませんね。これは、無条件で即時に遷移することを意味します。つまりこれは、プログラムが起動すると即座に待機中になることを表しています。
状態を識別するためには、変数を用意する必要があります。例えば、is_active
という変数を用意して、is_active = False
ならば待機中、is_active = True
ならば計測中という具合に使います。
実験3. ストップウォッチの改良
この実験はオプションです。ストップウォッチを改良して、ラップタイムを計測できるようにしましょう。タクトスイッチは2つ使います。
状態は以下のように変化します。

— by 石井 健太郎、沼 晃介、飯田 周作 専修大学ネットワーク情報学部