Raspberry Pi のGPIOをいじる

Raspberry Pi にシャットダウンボタンをつける というエントリではGPIOを使ってシャットダウンボタンを作りましたが、
pythonのライブラリを使ってスクリプトを書いただけでローレベルのAPIがどうなっているかはノータッチでした。
ただ、一般ユーザー権限で走らせると /dev/mem にアクセスできないというエラーが出るのでそこを叩いているということは分かりました。
つまりきちんとしたドライバのAPIではなくって、メモリ空間にマップされたレジスタを直接いじってるっぽいんですよね。
いやそれはちょっとイカンでしょうと思いますので、もう少しまともなAPIでコントロールできるようにいじってみました。

LinuxのGPIOインタフェース

カーネルソースの Documentation/gpio.txt に一通り書いてありますが、ユーザーランドから設定を動的にいじるためにはsysfsを使用します。
/sys/class/gpio というディレクトリを見ると、以下のようなエントリがあります。

% sudo ls -l /sys/class/gpio
合計 0
-rwxrwx--- 1 root gpio 4096  1月  1  1970 export
lrwxrwxrwx 1 root gpio    0  1月  1  1970 gpiochip0 -> ../../devices/virtual/gpio/gpiochip0
-rwxrwx--- 1 root gpio 4096  1月  1  1970 unexport

ここでexportというエントリにコントロールしたいGPIO番号を書き込むとそのGPIOに関するエントリが作成されます。

% sudo sh -c 'echo 23 > /sys/class/gpio/export'  
% sudo ls -l /sys/class/gpio
合計 0
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 export
lrwxrwxrwx 1 root gpio    0 10月  6 22:47 gpio23 -> ../../devices/virtual/gpio/gpio23
lrwxrwxrwx 1 root gpio    0  1月  1  1970 gpiochip0 -> ../../devices/virtual/gpio/gpiochip0
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 unexport
% sudo ls -l /sys/class/gpio/gpio23/
合計 0
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 active_low
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 direction
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 edge
drwxrwx--- 2 root gpio    0 10月  6 22:47 power
lrwxrwxrwx 1 root gpio    0 10月  6 22:47 subsystem -> ../../../../class/gpio
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 uevent
-rwxrwx--- 1 root gpio 4096 10月  6 22:47 value

これらのsysfsエントリを使ってinput/outputの設定や割り込みのエッジ設定等はできるのですが、
pullup/pulldownを設定するインタフェースがありません。
カーネルのコード(https://github.com/raspberrypi/linux/tree/rpi-3.6.y)を確認しても
pullup/pulldownをコントルールするGPPUD,GPPUDCLKnレジスタを扱っているらしきコードは見つかりませんでした。

カーネル修正

もちろん外部回路でプルアップすることもできるのですが、せっかくチップの機能があるのでそれを使いたいと思います。
(曲がりなりにも /dev/mem をいじれば使える訳ですし)
というわけでカーネルを少しいじって、これを扱えるようなsysfsエントリを追加してみました。

解説は省略しますが、以下にコードを置きました。
https://github.com/penkoba/linux-rpi/tree/rpi-3.6.y-gpio-pullud

このカーネルを使って起動し、先ほどと同じ手順を行うと、以下のようにpulludエントリが追加されています。

% sudo sh -c 'echo 23 > /sys/class/gpio/export'  
% sudo ls -l /sys/class/gpio/gpio23/
合計 0
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 active_low
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 direction
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 edge
drwxrwx--- 2 root gpio    0 10月  6 23:10 power
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 pullud
lrwxrwxrwx 1 root gpio    0 10月  6 23:10 subsystem -> ../../../../class/gpio
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 uevent
-rwxrwx--- 1 root gpio 4096 10月  6 23:10 value

ここに "up", "down", "none" を書き込むことによって内部のpullup/downをコントロールできます。
もちろん現在の状態の読み出しもできます。

アプリケーション

このsysfsエントリを使って、GPIOの割り込み待ちをするテストアプリケーションを書いてみます。
少し長いですが、以下に全リストを示します。

gpio-irq-demo.c

/*
 *            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
 *                    Version 2, December 2004
 * 
 * Copyright (C) 2013 penkoba
 *
 * Everyone is permitted to copy and distribute verbatim or modified
 * copies of this license document, and changing it is allowed as long
 * as the name is changed.
 *
 *            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
 *   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 *
 *  0. You just DO WHAT THE FUCK YOU WANT TO.
 */

#include <stdio.h>
#include <poll.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/signal.h>

static volatile int signal_received = 0;

static void sigint_handler(int signo)
{
        printf("signal %d received.\n", signo);
        signal_received = 1;
}

static void setup_signal(void)
{
        struct sigaction sigt;

        sigt.sa_handler = sigint_handler;
        __sigemptyset(&sigt.sa_mask);
        sigt.sa_flags = SA_NODEFER | SA_RESETHAND;
        sigt.sa_restorer = NULL;
        sigaction(SIGINT, &sigt, NULL);
}

int get_gpio_fn(char *fn, int gpio_nr, const char *basename)
{
        return sprintf(fn, "/sys/class/gpio/gpio%d/%s", gpio_nr, basename);
}

int simple_write_file(const char *fn, const char *s)
{
        int fd;

#if 0
        printf("writing %s > %s\n", s, fn);
#endif
        fd = open(fn, O_WRONLY);
        if (fd < 0) {
                perror(fn);
                return -1;
        }
        if (write(fd, s, strlen(s)) < 0) {
                perror(fn);
                return -1;
        }
        close(fd);

        return 0;
}

int setup(int gpio_nr)
{
        char fn[64];
        struct stat st;

        printf("configuring GPIO %d as\n"
               "  input, interrupt on falling edge, internal pull-up\n",
               gpio_nr);

        sprintf(fn, "/sys/class/gpio/gpio%d", gpio_nr);
        if (stat(fn, &st) < 0) {
                char nr_str[16];
                sprintf(nr_str, "%d", gpio_nr);
                if (simple_write_file("/sys/class/gpio/export", nr_str) < 0)
                        return -1;
        }

        get_gpio_fn(fn, gpio_nr, "direction");
        if (simple_write_file(fn, "in") < 0)
                return -1;

        get_gpio_fn(fn, gpio_nr, "edge");
        if (simple_write_file(fn, "falling") < 0)
                return -1;

        get_gpio_fn(fn, gpio_nr, "pullud");
        if (simple_write_file(fn, "up") < 0)
                return -1;

        return 0;
}

void teardown(int gpio_nr)
{
        char fn[64];
        char nr_str[16];

        printf("clean up GPIO %d\n", gpio_nr);

        get_gpio_fn(fn, gpio_nr, "edge");
        if (simple_write_file(fn, "none") < 0)
                return;

        get_gpio_fn(fn, gpio_nr, "pullud");
        if (simple_write_file(fn, "none") < 0)
                return;

        sprintf(nr_str, "%d", gpio_nr);
        if (simple_write_file("/sys/class/gpio/unexport", nr_str) < 0)
                return;
}

int main(int argc, char **argv)
{
        char fn[64];
        int fd, ret;
        int gpio_nr;
        struct pollfd pfd;
        char rdbuf[5];

        if (argc != 2) {
                printf("Usage: %s <GPIO>\n", argv[0]);
                return 1;
        }
        gpio_nr = atoi(argv[1]);

        if (setup(gpio_nr) < 0)
                return 1;

        setup_signal();

        get_gpio_fn(fn, gpio_nr, "value");
        fd = open(fn, O_RDONLY);
        if (fd < 0) {
                perror(fn);
                return 1;
        }
        pfd.fd = fd;
        pfd.events = POLLPRI;
        ret = read(fd, rdbuf, sizeof(rdbuf));
        if (ret < 0) {
                perror(fn);
                return 1;
        }

        for (;;) {
                lseek(fd, 0, SEEK_SET);
                ret = poll(&pfd, 1, -1);
                if (signal_received)
                        break;
                if (ret < 0) {
                        perror("poll()");
                        close(fd);
                        return 1;
                }
                if (ret == 0) {
                        printf("timeout\n");
                        continue;
                }
                ret = read(fd, rdbuf, sizeof(rdbuf) - 1);
                if (ret < 0) {
                        perror("read()");
                        return 1;
                }
                rdbuf[ret] = '\0';
                printf("interrupt, value is: %s", rdbuf);
        }
        close(fd);

        teardown(gpio_nr);

        return 0;
}

GPIOの設定はsetup()関数の中で行っています。
exportした後に、direction, edge, pullud エントリを設定してfalling edgeの割り込みを待ち受けるように設定します。

割り込み待ちはmain()のループの中で、valueエントリのPOLLPRIイベントをpollすることで実現できます。

gpio-irq-demo.cをコンパイルし、GPIO23番とGNDをスイッチにつないで(こちら参照)から以下のように実行します。

% sudo ./gpio-irq-demo 23
configureing GPIO 23 as
  input, interrupt on falling edge, internal pull-up

(スイッチを押す)
interrupt, value is: 0
interrupt, value is: 0
interrupt, value is: 0

(Ctrl-C)
^Csignal 2 received.
clean up GPIO 23

/dev/memを使わずにGPIOのpullup設定ができるようになりました。

今後

カーネルをいじりだした時は、実装したものをRPiのカーネルかあわよくばmainlineに取り込んでもらおうかと思っていたのですが、
GPIO subsystemとは別に Pin control subsystem というものが実装進行中で、
機能がマルチプレクスされたピンのコントロールやpullup/downのコントロールもそちらに含まれるようなのです。
(参考:http://www.df.lth.se/~triad/papers/pincontrol-gpio-update.pdf
そしてkernel 3.7系からbcm2835のPin controlドライバも登場しています。

GPIOとPin controlは密接に関係していて、両方をきちんと理解した上で実装を進める必要があります。
なのでPin control subsystemを勉強してからまたどうするか改めて考えてみたいと思います。
今回はとりあえずこの辺で。