GPIOを使ったシャットダウンスイッチ (2014/05/03)
最近はずっと液晶ディスプレイ一体型の自作ケース で Raspberry Pi を快適に使っています。 キーボードは必要な時に Bluetooth 接続しますが、ほとんどの場合は母艦の MacBookPro17 からSSHでWifi接続しています。
寝る前などに Raspberry Pi シャットダウンしないまま母艦のスイッチを切った場合など、キーボードをつなぐのも、母艦の再起動も面倒です。 こんな時、Rasberry Pi だけで安全にシャットダウンできると便利そうです。Raspbian のカーネルは GPIO の割り込みが使えるようになっているので、 GPIO にスイッチをつないでシャットダウンできるようにしました。
GPIO の練習
GPIO の動作確認のため、まずはブレッドボードを使って、LEDを点灯させてみました。 220オームの抵抗を直列に接続したLEDを、13x2列のピンヘッダ に接続しました。 ピンヘッダ の4つのGPIO出力(4番 [P1_07]、17番 [P1_11]、27番 [P1_13]、22番 [P1_15]、とグラウンド(0V [p1_09]) の5本のピンです。
Linux のカーネルは sysfs から GPIO を操作できるようなドライバを持っています。/sys/class/gpio/ 以下のファイルに書き込んだり、読み込んだりすることで GPIO を操作することができます。 ここでは LED を順番に点灯/消灯させてみました。
led_open.sh
まず、GPIO の準備です。/sys/class/gpio/export に対してGPIO番号を書き込むと対応する番号のディレクトリ ( /sys/class/gpio/gpio番号/ ) ができます。 次に /sys/class/gpio/gpio番号/direction に "in" または "out" を書き込むとGPIOを入力モードか出力モードに設定できます。ここでは3.3V、または0Vを出力して LED を点滅させるため、 /sys/class/gpio/gpio番号/direction に "out" を書き込みます。ルートの権限で実行する必要があるのでコマンドを1つずつ実行するには「sudo bash」などでルート権限になってから 「echo "4" > /sys/class/gpio/export」などを実行して下さい。「sudo echo "4" > /sys/class/gpio/export」ではリダイレクトの部分で失敗します。下の内容のスクリプトファイルを作った場合は 「sudo sh led_open.sh」で大丈夫です。 GPIOの4番、17番、27番、22番に対して同じように出力モードで設定するシェルスクリプトです。
#!/bin/sh # P1_07 echo "4" > /sys/class/gpio/export echo "out" > /sys/class/gpio/gpio4/direction # P1_11 echo "17" > /sys/class/gpio/export echo "out" > /sys/class/gpio/gpio17/direction # P1_13 echo "27" > /sys/class/gpio/export echo "out" > /sys/class/gpio/gpio27/direction # P1_15 echo "22" > /sys/class/gpio/export echo "out" > /sys/class/gpio/gpio22/direction
led_on_off.sh
実際に LED を点灯させるには /sys/class/gpio/gpio4/value に "1" を書き込みます。 "0" は消灯です。こちらもルート権限で実行する必要があるのでコマンドを1つずつ実行するには「sudo bash」などでルート権限になってから 「echo "1" > /sys/class/gpio/gpio4/value」などを実行して下さい。上と同じくスクリプトファイルを作った場合は 「sudo sh led_on_off.sh」で実行できます。
#!/bin/sh sleep 1 echo "1" > /sys/class/gpio/gpio4/value sleep 1 echo "1" > /sys/class/gpio/gpio17/value sleep 1 echo "1" > /sys/class/gpio/gpio27/value sleep 1 echo "1" > /sys/class/gpio/gpio22/value sleep 1 echo "0" > /sys/class/gpio/gpio22/value sleep 1 echo "0" > /sys/class/gpio/gpio27/value sleep 1 echo "0" > /sys/class/gpio/gpio17/value sleep 1 echo "0" > /sys/class/gpio/gpio4/value sleep 1
実行
シャットダウンスイッチ
今度は GPIOを入力モードで使って、スイッチの状態を読み取り、一定の条件の下でコマンドを実行します。シャットダウン以外にも色々応用できそうです。
回路
右端がスイッチです。回路では押しボタン型ですが、実際はトグルスイッチです。スイッチが変化すると割り込みが発生し、5秒後も起動時のスイッチの状態(On か Off)と異なっていたらシャットダウンします。10Kオームの抵抗でGPIO7の入力電圧を3.3Vに引き上げ、スイッチを押した場合に 0V とショートさせることで GPIO7の入力が 0 になります。 GPIO7の入力には電流制限用に1Kオームの抵抗を入れています。
Pin Switch P1_25 0V o----------------o | 1K |-- P1_26 GPIO7 o----VVV----+----o | 10K | P1_17 3.3V o----VVV----+
実物写真
自作ケース に手持ちのトグルスイッチを固定しています。宙ぶらりんの配線ですが、このくらいは大丈夫でしょう。せっかくブレッドボードを買ったので、先にブレッドボード上で試してみましたが、ハンダ付けの方が楽でしたw。
写真では 拡張ボード のピンヘッダにつないでいますが、Raspberry Pi に直接つないでも同じです。
プログラム
/etc/rc.local
/etc/rc.local の終わりの方に、以下の行を追加してマシン起動時にGPIOの初期設定をした後にgpio_shutdown.lua を常駐させます。
sh /root/gpio_shutdown.sh &
/root/gpio_shutdown.sh
GPIOの7番(ピンヘッダの26番ピン、右の最も下のピン)をユーザモードで公開(エクスポート)、入力モードにして、割り込みを立ち上がりと、立ち下がりの両方で有効にします。
#!/bin/sh cd /root echo "7" > /sys/class/gpio/export echo "in" > /sys/class/gpio/gpio7/direction echo "both" > /sys/class/gpio/gpio7/edge luajit gpio_shutdown.lua
/root/gpio_shutdown.lua
GPIOの7番の割り込みをpollシステムコールで待ち受けする LuaJIT のプログラムです。 スイッチが変更されたら初期状態と比較して異なっていたら5秒待って、スイッチの状態を再確認したのちにシャットダウンする。 スイッチのチャタリングのタイミングによっては動作しない(安全側)時があるが、スイッチを再度操作すればOK。割り込みでスイッチの状態変化を検出しているので、1時間に1回、一瞬しかCPUを消費しません。LuaJIT はテキストファイルを実行できるスクリプト言語にもかかわらず、その守備範囲はC言語とほぼ同じ領域のうえ、メモリ消費も非常に少ないため、このような用途でもお薦めです。
#!/usr/bin/luajit -- --------------------------------------------- -- gpio_shutdown.lua 2014/05/03 -- Copyright (c) 2014 Jun Mizutani, -- released under the MIT open source license. -- --------------------------------------------- local ffi = require("ffi") local C = ffi.C ffi.cdef[[ typedef unsigned long int nfds_t; struct pollfd { int fd; short int events; short int revents; }; int open(const char *pathname, int flags, int mode); int poll(struct pollfd *fds, nfds_t nfds, int timeout); void *memset(void *buf, int ch, size_t n); int lseek(int fd, int offset, int whence); int read(int fd, void *buf, int count); ]] function gpio_open() local O_RDONLY = 0 local fd = C.open("/sys/class/gpio/gpio7/value", O_RDONLY, 0) return fd end function read_gpio(pfd) local SEEK_SET = 0 local buf = ffi.new("char[8]", {}) C.memset(buf, 0, ffi.sizeof(buf)) C.lseek(pfd.fd, 0, SEEK_SET) local n = C.read(pfd.fd, buf, ffi.sizeof(buf)) return tonumber(ffi.string(buf)) end function do_shutdown() print("shutdown -h now") if os.execute() == 1 then local code = os.execute("shutdown -h now") end end local POLLPRI = 2 local pfdp = ffi.new("struct pollfd[1]") local pfd = pfdp[0] pfd.fd = gpio_open() pfd.events = POLLPRI print("Starting GPIO switch .. \n") local original_val = read_gpio(pfd) repeat local ret = ffi.C.poll(pfdp, 1, 3600000) if ret > 0 then ffi.C.poll(nil, 0, 100) local val = read_gpio(pfd) if val ~= original_val then io.write("This machine will shutdown in 5 seconds. ") for i = 5, 1, -1 do io.write(i .. " ") io.flush() ffi.C.poll(nil, 0, 1000) val = read_gpio(pfd) if val == original_val then print(" aborting shutdown.") break end end if val ~= original_val then do_shutdown() end end end until false
シャットダウンスイッチを入れる(ボタンを押す)とコンソールに以下のようなメッセージが表示されます。
This machine will shutdown in 5 seconds. 5 4 3 2 1 shutdown -h now
5秒以内にボタンをはなすと「aborting shutdown.」と表示されてシャットダウンは中止されます。
This machine will shutdown in 5 seconds. 5 4 3 2 aborting shutdown.
MacbookPro のように道具として完成された PC もいいですが、Raspberry Pi のようにハードウェアもソフトウェアも少しずつ足りないところがあるコンピュータを自分で何とかするのもいいもんです。
Raspberry Pi の GPIO の仕組み
以上のように、Raspberry Pi の GPIO は Unix系のOSらしく、ファイルシステムを使っていろいろな機能を使えるようになっています。 もっとハードウェア寄りの低レベルな部分は、CPU の周辺回路の説明書である BCM2835-ARM-Peripherals.pdf に書いてあります。 全体像は 5 ページの図にまとまっています。 右側がLinuxの論理メモリ空間、左側がGPUのメモリ空間、真ん中が物理メモリ空間になっています。 この物理メモリ空間の 0x20000000 が入出力用のメモリ空間の先頭であることが分かります。 Linux では "/dev/mem" というデバイス ファイルを使うことで物理メモリ空間がアクセスできます。入出力用のメモリ空間に周辺回路のレジスタが割り当てられていて、これらのアドレスを読み書きすることでいろいろな機能を使えるようになっています。 BCM2835-ARM-Peripherals.pdf では周辺回路用のアドレスはすべてGPU用のバスアドレスで表記されているため 0x7E000000 から始まるアドレスになっています。 したがって 0x7E を 0x20 と読み替えると ARM の物理アドレスになります。
(2015/03/21) 周辺回路のレジスタの先頭の物理メモリアドレスは Raspberry Pi2では 0x3F000000 に変更されています。 Raspberry Pi2では 0x20 を 0x3F に読み替えてください。詳細はこちら。
今回コントロールしようとしている GPIO (汎用入出力) は 89 ページから説明してあり、32ビットのレジスタが 0x7E200000 から40個ほど存在しています。
GPIOピンの機能の設定
次の図の 0x7E200000-4 の GPFSEL0 から 0x7E200014-7 の GPFSEL5 までの6個の32ビットレジスタで 54本 ある GPIO の動作モードを指定します。 3ビット1組で個々の GPIO に対して 8 種類の動作を指定できるようになっています。 GPIOのピン番号を10で割った値でレジスタ(RegNo:0-5)を選択し、 剰余 (pinNo:0-9) でビット位置が決まります。Address = 0x20200000 + RegNo * 4 がレジスタアドレス、PinNo * 3 がシフトするビット数となります。3ビットが (000) ならば入力、(001) なら出力になります。それ以外の値はピン毎にシリアル入出力など色々な機能を設定できるようになっていて、ピンヘッダにでている以外にも Raspberry Pi が内部的に使用しています。
下の表は RaspberryPi のピンヘッダ(P1) のピン番号とチップのGPIO番号の対応を示しています。GPIOの2、3、14、15 は SDA0、SCL0、TxD、RxDに初期設定されています。 「GPIO in」となっているピンはGPIOの入力モードになっています。
pin# | 機能 | GPIO# | pin# | 機能 | GPIO# |
---|---|---|---|---|---|
1 | 3.3V | - | 2 | 5v | - |
3 | SDA0 | 2 | 4 | 5v | - |
5 | SCL0 | 3 | 6 | 0v | - |
7 | GPIO in | 4 | 8 | TxD | 14 |
9 | 0v | - | 10 | RxD | 15 |
11 | GPIO in | 17 | 12 | GPIO in | 18 |
13 | GPIO in | 27 | 14 | 0v | - |
15 | GPIO in | 22 | 16 | GPIO in | 23 |
17 | 3.3v | - | 18 | GPIO | 24 |
19 | GPIO in | 10 | 20 | 0v | - |
21 | GPIO in | 9 | 22 | GPIO in | 25 |
23 | GPIO in | 11 | 24 | GPIO in | 8 |
25 | 0V | - | 26 | GPIO in | 7 |
GPIO の入出力
GPIOへの出力は、GPIOピンに「1」を出力(セット)するレジスタと「0」を出力(クリア)するレジスタにわかれています。 GPSET0、GPSET1の32ビットレジスタ2本 (0x2020001C - 0x20200023) がGPIOピンに「1」を出力するレジスタとして用意されています。GPIOの0番から31番までがGPSET0、32番から53番までが GPSET1のビットに対応しています。1が設定されたビットに対応したGPIOピンだけに「1」が出力されます。GPCLR0,GPCLR1の32ビットレジスタ2本 (0x20200028 - 0x2020002F) はGPIOへ「0」を出力するレジスタです。出力したいGPIO番号に対応するビットだけを 1 にしておいて、「1」を出力する場合はGPSET、「0」を出力する場合はGPCLRに書き込みます。 GPLEV0、GPLEV1 の32ビットレジスタ2本 (0x20200034 - 0x2020003B) から各GPIOピンの状態を読み出すことができます。
プログラム例
Raspberry Pi の GPIO関連のレジスタを直接操作して GPIOの入出力を行うプログラムを LuaJIT で作成してみました。 "/dev/mem" というデバイス ファイルをメモリにマッピングすることで、物理メモリ空間にあるハードウェアのレジスタを32ビットの整数の配列として扱うことができるようになります。 後は配列の要素である32ビットの整数に対してビット操作を行うことでハードウェアのGPIOをコントロールできます。
#!/usr/bin/luajit -- --------------------------------------------- -- gpio_led.lua 2014/05/03 -- Copyright (c) 2014 Jun Mizutani, -- released under the MIT open source license. -- --------------------------------------------- local bit = require("bit") local ffi = require("ffi") local C = ffi.C ffi.cdef[[ void *mmap(void *addr, size_t len, int prot, int flags, int fd, int offset); int munmap(void *addr, size_t len); int open(const char *pathname, int flags, int mode); typedef unsigned long int nfds_t; int poll(struct pollfd *fds, nfds_t nfds, int timeout); ]] function sleep(sec) C.poll(nil, 0, sec * 1000) end -- 物理アドレス空間のデバイスファイルをオープン function mem_open() local O_RDONLY = 0 local O_WRONLY = 1 local O_RDWR = 2 local fd = C.open("/dev/mem", O_RDWR, 0) return fd end -- メモリをマッピング function mem_map(fd, addr, length, offset) local PROT_READ = 1 local PROT_WRITE = 2 local PROT_EXEC = 4 local MAP_SHARED = 1 local MAP_PRIVATE = 2 if fd > 0 then local p = C.mmap(addr, length, PROT_READ + PROT_WRITE, MAP_SHARED, fd, offset) local mem = ffi.cast("int32_t *", p) if mem > ffi.cast("int32_t *", 0) then return mem end end return nil end -- メモリのマッピングを解除 function mem_unmap(addr, length) local p = ffi.cast("int32_t *", addr) return C.munmap(p, length) end local gpio -- 初代 Raspberry Pi 用のベースアドレス local BCM_PEREIFERAL_ADDR = 0x20200000 -- Raspberry Pi2 用のベースアドレス -- local BCM_PEREIFERAL_ADDR = 0x3F200000 local GPSET0 = 7 local GPSET1 = 8 local GPCLR0 = 10 local GPCLR1 = 11 local GPLEV0 = 13 local GPLEV1 = 14 -- GPIO のレジスタを gpio 配列に設定 function gpioOpen() local fd = mem_open() local mem = mem_map(fd, nil, 256, BCM_PEREIFERAL_ADDR) if mem == nil then print("You must run 'sudo luajit gpio.lua'.") end gpio = mem end -- GPIO のレジスタのマッピングを解除 function gpioClose() mem_unmap(BCM_PEREIFERAL_ADDR, 256) end -- pinNo の GPIO のモードを設定 -- mode: 0/input, 1/output function gpioSetPinMode(pinNo, mode) if pinNo > 53 or pinNo < 0 then print("PinNo is out of range.") return elseif mode > 7 then print("Mode must be [0..7].") return else local reg = math.floor(pinNo / 10) local shift = (pinNo % 10) * 3 local mask = bit.bnot(bit.lshift(7, shift)) -- ~(7 << shift) local val = bit.lshift(mode, shift) local orig = gpio[reg] local new = bit.bor(bit.band(orig, mask), val) gpio[reg] = new end end -- pinNo の GPIO のモードを取得 function gpioGetPinMode(pinNo) if pinNo > 53 or pinNo < 0 then print("PinNo is out of range.") return nil else local reg = math.floor(pinNo / 10) local shift = (pinNo % 10) * 3 local mask = bit.lshift(7, shift) local val = bit.band(gpio[reg], mask) local mode = bit.rshift(val, shift) return mode end end -- pinNo の GPIO に 1 を出力 function gpioSet(pinNo) if pinNo < 32 then gpio[GPSET0] = bit.lshift(1, pinNo) elseif pinNo < 54 then gpio[GPSET1] = bit.lshift(1, pinNo - 32) end end -- pinNo の GPIO に 0 を出力 function gpioClear(pinNo) if pinNo < 32 then gpio[GPCLR0] = bit.lshift(1, pinNo) elseif pinNo < 54 then gpio[GPCLR1] = bit.lshift(1, pinNo - 32) end end -- pinNo の GPIO を読み出し function gpioRead(pinNo) local val if pinNo < 32 then val = bit.band(gpio[GPLEV0], bit.lshift(1, pinNo)) elseif pinNo < 54 then val = bit.band(gpio[GPLEV1], bit.lshift(1, pinNo - 32)) end if val ~= 0 then return 1 end return 0 end -- P1_11ピンに接続したLEDを点滅させる gpioOpen() -- GPIO アクセス準備 gpioSetPinMode(17, 1) -- pin11 : 出力モード for i = 1, 10 do gpioSet(17) -- pin11 を 1 に設定 sleep(1.0) -- 1.0秒間待つ(点灯) gpioClear(17) -- pin11 を 0 に設定 sleep(0.5) -- 0.5秒間待つ(消灯) end gpioClose()
実行
$ sudo luajit gpio_led.lua
ピンヘッダの11番 (GPIO7) と9番 (0V) の間に200−300オーム程度の抵抗をLEDと直列に接続して、上記のコマンドを実行すると15秒間に10回点滅します。
注意
このプログラムは 54本のすべてのGPIOに対して設定を変更できます。Raspberry Piの内部動作に深く関わっているため、ピンヘッダに接続されているGPIO以外のピンの設定を不用意に変更すると、簡単に フリーズしたり、カーネルパニックになったり、不安定になったりします。 GPIOなどのハードウェアの操作にルート権限が必要な理由です。自己責任でお願いしますが、万一壊れても普通のPCに比べてダメージが少ないので、無茶してみるのも面白いものです。