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本のピンです。


gpio_led.jpg


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。


GPIO_SW.img


写真では 拡張ボード のピンヘッダにつないでいますが、Raspberry Pi に直接つないでも同じです。


GPIO_P1.img


プログラム

/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 が内部的に使用しています。


GPFSEL.png


下の表は 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に比べてダメージが少ないので、無茶してみるのも面白いものです。


続く...



このページの目次