まなびサイエンス

RaspberryPi の GPIO のプルアップ/ダウン設定について

カバーイメージ

 RaspberryPi は、元が子供向けの教育用コンピュータということや、ハードディスク ドライブではなく、シリコン ディスク(SD カード)で動作するので、いきなり電源を落としてもディスク クラッシュなどの可能性は低いようですが、やはり電源を落とす際には、行儀よく shutdown してから電源ケーブルを抜いた方が良いでしょう。しかし、その都度、パソコンからシェル ログインしてコマンドを叩くのも面倒です。そこで、GPIO 端子に取り付けたスイッチでリブート/シャットダウンを行うというのは、もはやRaspberryPi 入門(?)の鉄板ネタになっています。
 先日作成したばかりの I/O ボードを使って、私もコレを使ってやってみようと思い立ちました。その際、GPIO 入力端子のプルアップ/ダウン設定について調べた覚書きです。

プルアップ/ダウンとは

 スイッチなどが接続された入力端子の電位を、10kΩ程度の抵抗を介して Vcc または GND 側に弱く引っ張り(pull)ます。スイッチが導通状態であれば、入力端子の電位は Vcc または GND で決定しますが、スイッチがオフになって端子が解放されている状態では、入力端子の電位が不定となり、微弱なノイズ程度で入力値が変動してしまいます。そこで、入力端子をあらかじめ Vcc 電位側に引っ張るか(pull-up)、GND 電位側に引っ張って(pull-ddown)おくことで、入力端子の電位を安定させるのです(Fig.1 )。


Fig.1

喩え

 蝶番で開閉できる片開きのドアを想像してみてください。それだけでは、風が吹いたり、家が揺れたりするたびに、ドアがフラフラと動いてしまうため、開いている(オン状態)のか、閉まっている(オフ状態)のかハッキリしません。そこで、ドアに軽いバネを取り付けて、手を放せば、ドアが勝手に開いたり閉まったりするようにしておきます。こうしておけば、ドアが開いている(オン状態)のか、閉まっている(オフ状態)のかハッキリします。プルアップ/ダウン抵抗が、このバネの役割をします。

GPIO 入力を読む - 仮想ファイル システム(sysfs)を使う

 /sys/class/gpio/* にある仮想ファイル システム(sysfs)を介して GPIO*1 ポートを扱う方法で、シェル スクリプトなどから echocat などのコマンドを利用して読書きを行います。また、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
 ウェブサイトを参考にして同じ電子回路を組んでも、利用するポートを変えると途端に動かなくなったりします。ユーザとしては、ソフトウェア側でポート番号を正しく変更した上で、回路をそのまま別のポートに移しただけなのに…と思っているのでハマるかもしれません。

まとめ

  • てっとり早く出力ポートとして使う分には簡単で良い
  • 入力ポートして使うには、プル設定ができず、ポート毎にプル方向が違うので、かなり使いづらい

リンク

GPIO 入力を読む - WiringPi

 入力ポートのプルアップ/ダウン設定などを考えると、sysfs では力不足ということで、最終的には WiringPi を利用することで落ち着きました。メリットとしては;

  1. 判りやすい命令で GPIO を操作できるコマンド gpio がある
  2. sysfs 経由で GPIO を操作するには root 権限が必要だが、WiringPi では通常ユーザ権限で操作できる。
  3. 入力ポートのプルアップ/ダウン設定が行える。
  4. C 言語用のライブラリが含まれており、今後、C 言語などで開発する際にも、自前で下準備*4などをしなくても簡単に扱える。

GPIO2/3 の罠

 先日作成した 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。えぇ~shock.gif

リブート/シャットダウン用スイッチの製作

 GPIO2 側のプッシュ スイッチは使えないことが判ったので、気を取り直して GPIO4 のスイッチでやってみます。GPIO4 は普通にプルアップ/ダウンの設定ができました。はぁsad.gif 先人の製作例を参考にしながら、仕様としては次のようなモノを考えます;

  1. 一つのプッシュ ボタンを押す長さに応じて、reboot と shutdown を切り替える
  2. プッシュ ボタンの押されている間、LED の点灯/点滅パターンを変えて動作を通知する
  3. LED は、基盤上の内蔵 LED (通電表示用の赤色 LED、もしくは SD カードへのアクセス表示用の緑色 LED)を利用する

 ちょっと長いですが、完成したスクリプトは次の通りです;

#!/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

スクリプトの解説

5 行目、71 行目、77 行目
 スイッチの接続された GPIO ポート番号とプルアップ/ダウンの設定です。この例では、GPIO4 に接続された負論理のスイッチ(押されたら 0 入力)を想定しています。お使いのスイッチ回路と相談で変えてください。
9~10 行目
 スイッチを TIME_SHUTDOWN 秒以上押すと、shutdown します。TIME_SHUTDOWN 秒未満かつ TIME_REBOOT 秒以上押すと、reboot します。TIME_REBOOT 秒未満の場合は何もしません。
13 行目
 スイッチが押されている間、基盤上の内蔵 LED を点滅させます。
16 行目
 VERBOSE=1 として実行すると、動作確認用に幾つか画面表示を行い、実際の reboot/shutdown は行いません。
18~33 行目
 LED の点滅パターンを定義しています。お好みで変えてみてください。
43~54 行目
 reboot 処理の際に呼ばれます。
50~54 行目
 shutdown 処理の際に呼ばれます。
57~61 行目
 GPIO ポートの読書きに WiringPi を利用しています。インストールされていない場合、スクリプトを終了します。
64~67 行目
 root ユーザ以外で実行された場合、スクリプトを終了します。
69~71 行目
 スイッチが接続された GPIO を入力に設定し、プルアップ/ダウンを指定します。
81~90 行目
 スイッチが押され続けたカウント(push_cnt)に応じて、reboot 未確定/reboot 確定/shutdown 確定を分岐します。
94~98 行目
 スイッチが離された場合、reboot が確定していたら do_reboot します。
  1. *1 General Purpose Input/Output; 汎用入出力
  2. *2 試してみたところ、sink/source の最大許容値である 20~30mA の電流が流れるようでした。
  3. *3 抵抗値によって電圧が分圧され、スレッショルド電圧を満たさなくなる場合など
  4. *4 これはこれで勉強にはなりそう
  5. *5 反対に、負論理のスイッチと組み合わせて、プルアップで使う分には問題なし
  6. *6 100 Ωくらいの抵抗で負けないくらいに GND にプルダウンしてやれば使えないこともない?

カテゴリー

過去ログ