IoTで使うPython入門Step0-(4) 定番のLチカの改良(その1)

 前回、Lチカのプログラムを作り、CTRL-Cで実行を中断しました。通常のプログラムは中断という操作はしないので、正常に終了するプログラムに変更します。
 キーボードから、たとえば、

  • Sが押されるとLチカを中断する。
  • Rが押されるとLチカを始める。中断していたら再開する。
  • Eが押されたら、プログラムを終了する。

という取り決めをします。PCのアプリケーションならこの方針でよいのですが、ラズパイではキーボードは似合いません。
 そこで、それぞれスイッチを用意します。プチっと押すスイッチをタクト・スイッチといいます。いろいろな形状の製品がありますが、上から見ると多くは正方形、裏から見る足の配置は長方形をしています。

タクト・スイッチ

 各種リモコンやテレビ本体のクリック感のある押しボタン・スイッチが通称タクト・スイッチで、家電や産業用に広く使われています。通常カバーや軸の延長パーツがついているので、本体を見るチャンスはありません。


 短辺がON/OFFされる接点です。長辺はそれぞれショートしています。回路図では次のように書かれます。GPIOにタクト・スイッチをつなぐ

 物理ピン38番、GPIO20にタクト・スイッチをつなぎます。GPIO20はラズパイの電源が入ったで点でLowです。GPIO内部回路では約50kΩでプルダウンの設定になっているようです。
(※)機能の決まった端子があるので、LEDを使うときにはそれらを避けます。I2Cの信号は1.8kΩでプルアップされています。SPIのCE0/CE1はSPIが有効になっていなくとも、負論理の信号なのでHighになっています。UARTのTxDとRxdはマーク=High(こちらの資料とは異なる)です。

 入力に設定したとき、確実にLowかHighになるように、プルアップかプルダウンを行います。これは、不定状態だと、GPIOの入力インピーダンスは高いので、高周波ノイズなどを拾いやすいからです。
 次の回路(a)のように、プルアップ抵抗を取り付けます。よく見るのは10kΩです。
 しかし、プルダウンされている端子をプルアップするというのは無駄ですね。

 ここでは、プルダウンされている回路(b)を使います。ここで、二つ問題点があります。

  • 接点の劣化
  • チャタリング

 スイッチは、数年間利用すると、接点が空気中の物質によって酸化もしくは硫化し、接点不良になります。と、昔の回路の解説書には必ず書かれていました。したがって、対策は、しつこくスイッチを押すことですが、そういう使われ方をしないスイッチでは、接点がダメになります。したがって、接点に電流をある程度流せば、劣化が遅くなるという回路を採用します。

 チャタリングは、機械的接点で必ず起こる、数msの間にON/OFFが繰り返される症状です。積分回路を入れて鋭いパルスを除去します。

チャタリングの対策例-ハードウェア

 タクト・スイッチのチャタリング波形を見たいがために、何度もON/OFFを繰り返しますが、ほかのスナップ・スイッチなどに比べてわずかにしか起こりません。接点の構造上起こりにくいのかもしれません。

 実験だからチャタリングへの対応はしないというのもありですが、ラズパイの入力等価回路を見ると、Arduino UNOと同じくシュミット・トリガが入っています。したがって、わざわざLS14などのシュミット・トリガを取り付けなくてもCRだけでチャタリング回路が構成できるので組んでみます。

 回路図です。R1とC1で積分回路を構成しているので、スイッチS1を押すとC1に充電が開始、GPIO端子の電圧が徐々に上がっていき、あるしきい値の電圧以上になると入力のシュミット・トリガがONになります。チャタリングで起こる鋭いパルスは十分電圧が上がらなければ、シュミット・トリガはONになりません。このCRによる積分回路はハイカット・フィルタ(ローパス・フィルタ)そのものですね。
 シュミット・トリガは、電圧が上がるときと下がるときでしきい値電圧が異なる動作をします。そうでないただのバッファ回路では、しきい値付近で微妙に電圧が変わると、出力値がパタパタと変化してしまうからです。

 問題点があります。R1とラズパイのGPIOに内蔵されたプルダウン抵抗約50kΩとで分圧回路が構成されています。したがって、R1を50kΩにすると、入力電圧は3.3/2=1.6Vとなり、確実にHIGHにならないかもしれません。そこで、10kΩにしました。
 この時に接点に流れる電流は3.3/60k=55uAととても小さいです。これでは、接点にある程度の電流を流すことができません
 電流をR2で流します。

 ここでもう一つ問題があるのです。C1の電荷がたまっている状態で、マイコンの電源を切ると、入力ポートに電流が流れ、規定以上流れるとシュミット・トリガがラッチアップを起こし過熱し、次に電源を入れたときには壊れているかもしれません。どんどん問題点が出てきますね。
 対策した回路で、コンデンサの容量を変化させて効果をシミュレーションしました。R3を入れることで電流を制限します。この回路ではR2の抵抗値が低いので、入力ポートには電流は流れないかもしれません。

 スイッチにはR2=330Ωで約10mAを流しました。シュミット・トリガのValueには「Vhigh=3.3V Vlow=0 Vh=0.1」を設定しました。スレッショルドは(Vhigh-Vlow)/2なので約1.6Vです。2msぐらいのチャタリングは十分除去できているのがわかります。ラズパイのGPIOのHighレベルの認識する電圧Vihは2.1~2.3Vのようなので、少し傾向が異なるとは思います。

チャタリングの対策例-ソフトウェア

 ここでは、RPi.GPIOの使用例に掲載されているチャタリングの対策例を使います。スイッチの検出を割り込みを使います。本来、割り込みとはどういう動作かを説明しないといけないのですが、後回しにします。スイッチの状態を知るには、

  • ポーリング
  • 割り込み

の方法があります。簡単で理解しやすいのはスイッチの状態をずっと読みに行くポーリングです。割り込みは、スイッチのつながっているGPIOの状態が変化したことをCPUが感知したら、あらかじめ用意した処理プログラムに飛ばすので、通常は別の処理ができる優れた機構です。

 あらかじめ用意してあるプログラムはmy_callback()です。スイッチが押されない限り、このプログラムは実行されません。割り込みはいつ何時かかるかわからないので、そして、ふつうは割り込みの要因は複数あるので、ここの処理はなるべく短く済ますことが大切です。

GPIO.add_event_detect(6, GPIO.RISING, callback=my_callback)


 GPIO6端子の電圧が、0->3.3V(立ち上がり、RISING)に変化したとき、my_callback()の処理を行って戻ってきます。チャタリングがあったら、そのたびに割り込みがかかります。その対策は、

GPIO.add_event_detect(6, GPIO.RISING, callback=my_callback, bouncetime=200)


 200msの間、別の割り込みがかかっても無視(禁止)します。ラズパイは、仮想OSなので、プロセスの切り替えもタイマ割り込みを使っていますし、キーボードなども割り込みがバンバン使われています。割り込みの要因とプライオリティは複数あって、このPythonのプログラムの割り込みは優先度のレベルが低いと思われます。そして、このレベルで割り込みを禁止/解除をしているので、もっと優先度の高いOSが管理している割り込みに影響はないものと思われます。
 どこかでこのような解説をしていると思われますが、間違っていたらすみません。

 チャタリングは数msに数回起こります。人がスイッチを瞬間に押したときは100~150msの時間がたちます。なので、200msは妥当か、少し長めかもしれません。
 割り込み処理の中で時間のかかるprint文を実行しています。動作確認用です。

import RPi.GPIO as GPIO
import time
sw = 20

def my_callback(sw):
print('\rat GPIO20')

GPIO.setmode(GPIO.BCM)
# GPIO.setup(sw, GPIO.IN)
GPIO.setup(sw, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(sw, GPIO.RISING, callback=my_callback, bouncetime=200)
try:
while 1:
print('\nPress any key to exit.\n')
time.sleep(0.1)
except KeyboardInterrupt:
print('\n end')
GPIO.cleanup()

 ここで、前提条件が間違っていることに気が付きました。ラズパイの電源が入った時点ではGPIO20はLowです。しかし、GPIO.setup(sw, GPIO.IN)を実行した時点では不定の電圧約1.6Vになります。この電圧はVihより低いですが、一般的なCMOS入力ではVdd/2=1.6VなのでHighともLowとも認識されているようです。このただの入力に指定した時点では、プルアップとプルダウンのどちらも実行されていないことがわかりました。つまり、外部にプルアップかプルダウンの抵抗をつけなければなりません。

 入力を解放(何もつながないこと)にして次のプルアップの初期設定をすると、

GPIO.setup(channel, GPIO.IN, pull_up_down=GPIO.PUD_UP)

端子の電圧は3.3Vに、プルダウンの設定では、

GPIO.setup(channel, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)


約7mVでした。そこで、プルダウンされているという前提で話しを進めているので、

GPIO.setup(channel, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

を利用します。

 タクト・スイッチを2個にしました。2個目はGPIOは16で36番ピンです。
 メインのプログラムはprint('\n Press any key to exit.\n')の表示と1秒待つを永久に繰り返しています。sw1もしくはsw2を押すと、割り込み処理のプログラムに飛んで行って、それぞれ、 at GPIO20もしくは at GPIO16を被氏します。すぐにメインのプログラムに戻ってくるので、Press any key to exit.が表示され続けます。終了はCTRL-Cです。

import RPi.GPIO as GPIO
import time
sw1 = 20
sw2 = 16

def my_callback1(sw1):
print('\n at GPIO20')

def my_callback2(sw2):
print('\n at GPIO16')

GPIO.setmode(GPIO.BCM)
GPIO.setup(sw1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(sw1, GPIO.RISING, callback=my_callback1, bouncetime=200)
GPIO.setup(sw2, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.add_event_detect(sw2, GPIO.RISING, callback=my_callback2, bouncetime=200)

try:
while 1:
print('\n Press any key to exit.\n')
time.sleep(1)
except KeyboardInterrupt:
print('\n end')
GPIO.cleanup()

 RPi.GPIOの仕様書はここが新しいようです。