RaspberryPi は、元が子供向けの教育用コンピュータということや、ハードディスク ドライブではなく、シリコン ディスク(SD カード)で動作するので、いきなり電源を落としてもディスク クラッシュなどの可能性は低いようですが、やはり電源を落とす際には、行儀よく shutdown してから電源ケーブルを抜いた方が良いでしょう。しかし、その都度、パソコンからシェル ログインしてコマンドを叩くのも面倒です。そこで、GPIO 端子に取り付けたスイッチでリブート/シャットダウンを行うというのは、もはやRaspberryPi 入門(?)の鉄板ネタになっています。
先日作成したばかりの I/O ボードを使って、私もコレを使ってやってみようと思い立ちました。その際、GPIO 入力端子のプルアップ/ダウン設定について調べた覚書きです。
スイッチなどが接続された入力端子の電位を、10kΩ程度の抵抗を介して Vcc または GND 側に弱く引っ張り(pull)ます。スイッチが導通状態であれば、入力端子の電位は Vcc または GND で決定しますが、スイッチがオフになって端子が解放されている状態では、入力端子の電位が不定となり、微弱なノイズ程度で入力値が変動してしまいます。そこで、入力端子をあらかじめ Vcc 電位側に引っ張るか(pull-up)、GND 電位側に引っ張って(pull-ddown)おくことで、入力端子の電位を安定させるのです(Fig.1 )。
蝶番で開閉できる片開きのドアを想像してみてください。それだけでは、風が吹いたり、家が揺れたりするたびに、ドアがフラフラと動いてしまうため、開いている(オン状態)のか、閉まっている(オフ状態)のかハッキリしません。そこで、ドアに軽いバネを取り付けて、手を放せば、ドアが勝手に開いたり閉まったりするようにしておきます。こうしておけば、ドアが開いている(オン状態)のか、閉まっている(オフ状態)のかハッキリします。プルアップ/ダウン抵抗が、このバネの役割をします。
/sys/class/gpio/* にある仮想ファイル システム(sysfs)を介して GPIO*1 ポートを扱う方法で、シェル スクリプトなどから echo や cat などのコマンドを利用して読書きを行います。また、C 言語などでも普通のファイルと同様に fopen して fread/fwrite して fclose すればよいので扱いやすいのですが、調べた範囲では入力端子のプルアップ/ダウンの設定は行えないようです。
調べている最中、「プルアップ(ダウン)を設定するために echo "high"("low") > /sys/class/gpio/gpiox/direction とする」という記述をネット上で見かけました[1][2]が、多分、これはダメな感じです。ソースコードを読んだところ、これは"出力ポートに設定した上で出力値を low または high に設定する"という動作のエイリアス(別名)になっており、ハイ インピーダンスの入力ポートにはなりません。その上、"プルダウンしたつもりの入力ポート"に Vcc を接続したり、"プルアップしたつもりの入力ポート"に GND を接続すると、回路上、ショート状態になってしまいます*2。
# echo 2 > /sys/class/gpio/export ← GPIO2 を使う # echo in > /sys/class/gpio/gpio2/direction ← 入力ポートに設定する # cat /sys/class/gpio/gpio2/direction ← ポートの入出力方向を確認する in # echo high > /sys/class/gpio/gpio2/direction ← プルアップを設定したつもり # cat /sys/class/gpio/gpio2/direction out ← 出力ポートに設定が変化している
出力に設定したポートであっても、入力ポートと同様に cat /sys/class/gpio/gpiox/value コマンドでポートの値を読めてしまうのですが、この時、Vcc⇒GND とほぼ短絡状態のポートは L レベルになるので、一見、正しく動作しているように思えます。今回のユースケースのように、数秒間ボタンを押した後にシャットダウンされるような使い方であれば、短絡状態が短時間のため、それほどダメージはありません。しかし、仮にトグル スイッチなどが接続されるなどして、長時間に渡って短絡状態が続くようなケースでは、チップが損傷する可能性が高いと思われます。
また、入力として使う場合でも、各ポートのプルアップ/ダウンの初期状態が異なるため、スイッチを単純に違うポートに振り替えると動作しなくなることが考えられます。内部的にプルダウンされているポートに、外部でプルダウンされたスイッチ回路を接続した場合、おおむね正しく動作すると考えられますが、回路をそのままに、今度は内部的にプルアップされたポートに付け替えてしまうと、プルダウン抵抗の抵抗値によっては H/L を正しく読み取れなくなる可能性が考えられます*3。
ウェブサイトを参考にして同じ電子回路を組んでも、利用するポートを変えると途端に動かなくなったりします。ユーザとしては、ソフトウェア側でポート番号を正しく変更した上で、回路をそのまま別のポートに移しただけなのに…と思っているのでハマるかもしれません。
echo high > ... で行っている。gpio_direction_store 関数を見ても、low/high にプルアップ/ダウンを設定する機能はなく、出力ポートを設定するエイリアスであることが判る。入力ポートのプルアップ/ダウン設定などを考えると、sysfs では力不足ということで、最終的には WiringPi を利用することで落ち着きました。メリットとしては;
gpio がある
先日作成した I/O ボードで GPIO2/3/4 を使って実験している際にハマった覚書きです。GPIO2/3 は、チップ外の回路上でプルアップされており、チップ内蔵のプルダウン設定が効きません。
自作の I/O ボードを GPIO=(1,3,5,7,9) に挿入した場合、GPIO2 には正論理のプッシュ スイッチが接続されるので、gpio -g mode 2 down として GPIO2 のプルダウンを設定しようとしたのですが、どう頑張っても端子電圧が下がりません。そのため、gpio -g read 2 としても、常に 1 が返ってくるため、スイッチの状態を読めません。なんでじゃー、なんでプルダウンできんのじゃー。WiringPi のバージョンを変えてみたり、OS イメージを再インストールしてみたり、I2C 関連の情報を探してみたり、しばらく右往左往したのですが、「GPIO2 はプルダウン設定できない」なんて情報はどこにも見当たりません。RPi Low-level peripherals に "1K8 pull up resistor" とあって、「この 1.8KΩ のプルアップをオフにしたいんだよ~」と悶絶していました。そんな時、ふと、回路図(中央あたり)を眺めていると...
GPIO2/3 がチップの外でプルアップされている/(^o^)\「1.8kΩのプルアップ」ってそういうことかよ!
この 1.8kΩ は、チップの外で GPIO に結線されており、しかも 1.8kΩという低抵抗で、かなり強力に Vcc に引っ張っているため、チップ側の設定ではどう頑張ってもオフにできません。つまり、GPIO2/3 をプルダウンされた入力として使うことはできないということが判りました*5*6。えぇ~
GPIO2 側のプッシュ スイッチは使えないことが判ったので、気を取り直して GPIO4 のスイッチでやってみます。GPIO4 は普通にプルアップ/ダウンの設定ができました。はぁ 先人の製作例を参考にしながら、仕様としては次のようなモノを考えます;
ちょっと長いですが、完成したスクリプトは次の通りです;
#!/bin/bash
# $Id$
### スイッチが接続された GPIOx
GPIO_SW=4
### $TIME_SHUTDOWN 秒以上押すと、do_shutdown します。
### $TIME_SHUTDOWN 秒未満、$TIME_REBOOT 秒以上押すと、do_reboot します。
TIME_REBOOT=4
TIME_SHUTDOWN=6
### 基盤上の LED(0,1) を動作インジケータに指定。空欄にすると使用しません。
LED=1 # 0=Green, 1=Red
### 動作確認用。通常運用時はコメントアウト。
#VERBOSE=1
### LED を点滅させる
### @see http://blog.livedoor.jp/victory7com/archives/43512294.html
function LED_BLINK_NORMAL(){
[ -z $LED ] && return
echo oneshot > /sys/class/leds/led$LED/trigger
echo 500 > /sys/class/leds/led$LED/delay_on
echo 1 > /sys/class/leds/led$LED/shot
}
### LED を早く点滅させる
function LED_BLINK_FAST() {
[ -z $LED ] && return
echo oneshot > /sys/class/leds/led$LED/trigger
echo 100 > /sys/class/leds/led$LED/delay_on
echo 1 > /sys/class/leds/led$LED/shot
}
### LED を初期状態に戻す
function LED_RESET() {
[ -z $LED ] && return
[ $LED -eq 0 ] && echo mmc0 > /sys/class/leds/led$LED/trigger
[ $LED -eq 1 ] && echo input > /sys/class/leds/led$LED/trigger
}
### reboot 処理
function do_reboot() {
echo Do rebooting...
[ $VERBOSE ] && return
reboot
}
### shutdown 処理
function do_shutdown() {
echo Do shutting-down...
[ $VERBOSE ] && return
shutdown -hP now
}
### WiringPi が必須 http://wiringpi.com/
GPIO=/usr/local/bin/gpio
if [ ! -x $GPIO ]; then
echo WiringPi not installed. Terminated.
exit
fi
### 実行は root ユーザのみ
if [ $UID -ne 0 ]; then
echo Need root authorized. Terminated.
exit
fi
### GPIO の初期設定
$GPIO -g mode $GPIO_SW in # 入力方向に設定
$GPIO -g mode $GPIO_SW up # プルアップ/ダウン設定
### TIME_SHUTDOWN 秒間押されるまで待つ
push_cnt=0
while :; do
push_sw=`$GPIO -g read $GPIO_SW`
if [ $push_sw -eq 0 ]; then
### GPIO_SW が押されたら、長押しカウンタを増加させる
push_cnt=`expr $push_cnt + 1`
[ $VERBOSE ] && echo push_cnt= $push_cnt
if [ $push_cnt -lt $TIME_REBOOT ]; then
LED_BLINK_NORMAL # まだ reboot するような時間じゃない
elif [ $push_cnt -lt $TIME_SHUTDOWN ]; then
LED_BLINK_FAST # まだ shutdown するような時間じゃない
else
### $TIME_SHUTDOWN 以上の長さ押されていたら do_shutdown
LED_RESET
do_shutdown
break
fi
else
### GPIO_SW が離された/押されていない
LED_RESET
if [ $TIME_REBOOT -le $push_cnt ]; then
### $TIME_REBOOT 以上の長さ押されていたら do_reboot
do_reboot
break
fi
### 長押しカウンタをリセット
push_cnt=0
fi
sleep 1
done
VERBOSE=1 として実行すると、動作確認用に幾つか画面表示を行い、実際の reboot/shutdown は行いません。