Raspberry Pi Pico スイッチの操作を割り込みで処理 備忘録
環境
Raspberry Pi Pico & Thonny & MicroPython
課題
2個のスイッチの操作を割り込みを使って処理する
ハードウエア概要
スイッチの接続
Pico との接続:スイッチの一端を Pico の電源 3V3(OUT) に接続。もう一方の端は 150 Ωの抵抗を介して Pico に接続。スイッチと GND の間に、0.068 μF のセラミックコンデンサーを接続。
押したときに High level になる。
抵抗 150 Ωを入れた理由
スイッチと Pico を直結したら異常な現象が発生したため。
動きがおかしかったので調べたところ、ノイズに対して異常に敏感になっているように見えた。たとえば、端子に触っただけで割り込みが発生したり、隣の端子の入力に反応したり、チャタリングによるものとは違う感じの連続した割り込みが発生したり。
手元にあった適当な抵抗を入れたら現象は消えた。最終的に、抵抗値は 150 Ωにした。特別な根拠はない。100 Ωでも効果があるかは確かめていない。220 Ω 〜 470 Ω あたりでも良いのではないかと思う。
自分の環境でしか発生しない現象かもしれない。
Pico の仕様(入力、Pull Down の設定)
・プルダウン抵抗: 50 - 80 Ω
・Low level: -0.3 - 0.8 V
・High level: 2.0 V - 電源電圧+0.3 V
(RP2040 Datasheet より)
コンデンサー 0.068 μF を入れた理由
スイッチのチャタリング防止のため。
回路の力を借りずソフトウエアだけでなんとかしたかった。しかし、割り込みを停止しているのに割り込んでくるといった不可解な現象が発生したため、割り込みをうまく処理できなかった。チャタリングによって短時間に複数の割り込みが発生することが原因と考え、コンデンサーを使ってチャタリングを減らすことにした。
0.068 μF という値にしたのは、0.1 μF の手持ちが1個しかなかったから。
ちなみに、最初の割り込みの後、割り込みを Disable せずに割り込みを繰り返すと、Pico が反応しなくなる。そうなったら、USBケーブルを抜くしかないようだった。
コード
import machine
from machine import Pin, PWM, ADC
import utime
def callback(pin):
global sw_pressed
sw_pressed = pin
def main():
global sw_pressed
sw_pressed = None
#drive True 回転、False 停止
drive = False
#direction トグルで逆転
direction = False
#Switch1: Drive motor
sw1 = Pin(10, Pin.IN, Pin.PULL_DOWN)
#Switch2: Reverse rotaion
sw2 = Pin(11, Pin.IN, Pin.PULL_DOWN)
#Output for IN1 of Driver IC
pwm1 = PWM(Pin(12))
#Output for IN2 of Driver IC
pwm2 = PWM(Pin(13))
#For LED
pwm = PWM(Pin(15))
#From potentiometer
adc = ADC(Pin(26))
pwm.freq(1000)
pwm1.freq(1000)
pwm2.freq(1000)
#割り込みの設定
sw1.irq(trigger=Pin.IRQ_RISING, handler=callback)
sw2.irq(trigger=Pin.IRQ_RISING, handler=callback)
sw1_value = sw1.value()
#出力をゼロに
pwm.duty_u16(0)
pwm1.duty_u16(0)
pwm2.duty_u16(0)
#割り込み処理をしない時間
interval = 50000
#割り込みのタイミングの基準
ref_time = utime.ticks_us()
while True:
#割り込み発生
if sw_pressed:
state = machine.disable_irq()
irq_flags = sw_pressed.irq().flags()
sw_value = sw_pressed.value()
time_diff = utime.ticks_diff(utime.ticks_us(), ref_time)
machine.enable_irq(state)
if time_diff > interval:
if sw_value == 1:
if irq_flags == 8:
if sw_pressed == sw1:
if drive == True:
drive = False
else:
drive = True
print('Drive:',drive,', Irq flags:',irq_flags,', Switch1 vlaue',sw_value,', Time difference:',time_diff)
elif sw_pressed == sw2:
if direction == True:
direction = False
else:
direction = True
print('Direction:',direction,', Irq flags:',irq_flags,', Switch2 value',sw_value,', Time difference:',time_diff)
else:
print('irq_flags:',irq_flags)
else:
print('Low level.')
else:
print('... Too fast ...')
sw_pressed = None
ref_time = utime.ticks_us()
#半固定抵抗の電圧
duty = adc.read_u16()
#ドライブモードだったら
if drive:
pwm.duty_u16(duty)
#回転方向に合わせてドライブ電圧出力端子を入れ替える
if direction:
pwm1.duty_u16(duty)
pwm2.duty_u16(0)
else:
pwm1.duty_u16(0)
pwm2.duty_u16(duty)
#停止
else:
pwm.duty_u16(0)
pwm1.duty_u16(0)
pwm2.duty_u16(0)
if __name__ == "__main__":
main()
割り込みの説明
sw1 および sw2 に割り込みを設定
sw1.irq(trigger=Pin.IRQ_RISING, handler=callback)
sw2.irq(trigger=Pin.IRQ_RISING, handler=callback)
IRQ_RISING で、Low レベルから High レベルへの変化を拾う。
ハンドラーとして callback 関数を指定。好きな名称にして良いと思うが、ここでは callback とした。
スイッチは2つだが、共通のハンドラーを使って処理する。
割り込みが発生すると、callback という名前の関数に飛んで、戻ってくる。
callback 関数
def callback(pin):
global sw_pressed
sw_pressed = pin
いろいろ試行錯誤したが、結果、これだけ。長いとダメらしい。割り込みが発生したフラグを立てるだけにしたら良いと思う。ここでは、割り込みが発生したピンの情報を変数に入れた。それにより、割り込みの発生がわかるだけでなく、どのピンに割り込みが発生したかも知ることができる。割り込みの処理も一部を共用できるので良いと思う。
引数の pin は押されたスイッチが接続されているピンの情報。p でも同じ結果が得られるようだ。しかし、誰がこの引数を渡してくれるのかとか、他に指定できる引数はないのかとか、よく分からない。
ピンの情報
Pin(10, mode=IN, pull=PULL_DOWN)
これを sw_pressed というグローバル変数を使って main ルーチンに渡す。
global 宣言について
sw_pressed はglobal 変数だが、main 関数と callback 関数の両方で値を代入する。そこで、これらの関数の両方に
global ws_pressed
という宣言文が必要。
sw1 および sw2 の設定は、次のとおり。
sw1 = Pin(10, Pin.IN, Pin.PULL_DOWN)
sw2 = Pin(11, Pin.IN, Pin.PULL_DOWN)
どちらのスイッチが押されたかを知るには、sw1 であるか sw2 であるかを調べればわかる。式は次のとおり。
if sw_pressed == sw1:
elif sw_pressed == sw2:
main ルーチンでは、割り込みが発生したことが確認できたら処理に入る。
if sw_pressed:
処理を邪魔されないよう、割り込みを停止する。
state = machine.disable_irq()
下の文は、割り込みを再開する命令。上とセットで使う。上でゲットした state を用いて、割り込み前の状態に戻す。 何の状態かは知らない。
machine.enable_irq(state)
割り込みを停止している間に、割り込みフラグおよび押されたスイッチの状態 (Low か High か)を変数に入れておく。
irq_flags = sw_pressed.irq().flags()
sw_value = sw_pressed.value()
time_diff = utime.ticks_diff(utime.ticks_us(), ref_time)
割り込みフラグ=割り込みが発生したピン.irq().flags()
次のリストでは整数で表したが、4ビットがそれぞれセットされるかクリアされることで状態を表す。
1: Low level
2: High level
4: Falling edge
8: Rising edge
12: Both edge
スイッチの状態=割り込みが発生したピン.value()
0: Low level
1: High level
time_diff で2つの割り込みの時間差を計算する。ticks_diff() 関数を使わないと正しく計算されない。この時間差が変数 interval 以下の場合には、スイッチのステータスを反転せず、スルーする。
スイッチのステータスを反転する条件は
割り込みの間隔が、変数 interval よりも大きい
スイッチの値が、 1(High level)
フラグが、8(Rising edge)
Rising edge を確認した後は、value() が 1 になっていることを確認した方が良い、と誰かが書いていたのでそのようにしている。
実際、Rising edge 検出後に value() = 0 とかいう状態も見たが、最新の回路ではそのような状態での割り込みは検出されていない。もしかしたら、Rising edge の確認だけでいけるかも。
上の条件を満たすときに、スイッチ別にステータスの処理に入る。
sw_pressed が sw1 のとき
ードライブモードのステータス drive を反転する
sw_pressed が sw2 のとき
ー回転方向のステータス direction を反転する
ふと思った。この場合分けすら不要になる書き方があるかもしれない。
終わったら、割り込み情報をクリアする。
sw_pressed = None
さらに、タイミングの基準も設定し直す。次の割り込みが発生したら、これを基準に経過時間を計算する。
ref_time = utime.ticks_us()
interval は、50 msec とした。前の割り込み処理が終わってから 50 msec 以内に発生した割り込みは、無視。
interval = 50000
検討段階でマイクロ秒レベルの時間計測が必要だったから .ticks_us() を使っているが、.ticks_ms() で十分だったかも。
一応これでブラシモーターが回っている。
└ 抵抗もコンデンサーも入っていない頃 ┘
t.koba