NetBSDでRaspberryPiのGPIOを使う

これはNetBSD Advent Calendar 2018の16日めの記事です。

はじめに

Raspberry PiにはIOピンヘッダがあり気軽に電子工作を楽しめるという面があります。また、登場以来、そういった電子工作を紹介する書籍や記事もたくさんあります。IOTという言葉の流行とともに語られることも多いようです。

しかしながら当然のように、サンプルプログラム等は基本的にRaspberryPiの標準環境、つまりLinux用です。

せっかくRaspberry PiでNetBSDが動くのだから、これらをNetBSDから制御してみようと思います。

使うハードウェア

NetBSDのGPIO

NetBSDではRaspberry PIのGOIOは以下のように認識されています。

bcmgpio0 at obio0: GPIO [0...31]
gpio0 at bcmgpio0: 32 pins
bcmgpio1 at obio0: GPIO [32...53]
gpio1 at bcmgpio1: 22 pins

つまり/dev/gpio0を使えばよさそうです。

GPIOを使ってみる(Lチカ)

まずはGPIOに接続されているLEDを光らせます。RPIでGPIOを使う方法は以下のような操作となります。

  1. GPIOの該当ピンを出力モードに設定
  2. GPIOの該当ピンをONにする

これをNetBSDで行うにはgpioctl(8)コマンドを使用します。

  1. GPIOのモード設定
    # gpioctl /dev/gpio0 5 set out
    
  2. GPIOに値を出力する(出力=true, onの場合)
    # gpioctl gpio0 5 on
    

これでLEDが光るはず…って、動きませんね。最初のsetで失敗してしまいます。

# gpioctl gpio0 5 set out
gpioctl: GPIOSET: Operation not permitted
しかも、gpioctlで制御可能なピン数を調べると、0と言われてしまいます。
# gpioctl gpio0
/dev/gpio0: 0 pins

これはカーネルのセキュリティレベルが0以下でしかgpioのモード制御が許されないことになっているためです。 NetBSDのRaspberryPI用デフォルトカーネルでは、起動直後はセキュリティレベルが0で通常起動すると1になります。一度レベルが上がると、下げることはできません。つまり通常起動した後では0にすることはできません。このままではGPIOのモード設定を変更することができません。gpioctl(8)のman pageにも記載があります。

じゃぁどうすればいいかというと、選択肢は2つ。

  1. 起動直後のセキュリティレベル0の時点でGPIOのモード設定を済ます。
  2. セキュリティ設定を外しデバイスアクセスを全開放するoption INSECUREを設定したカーネルを使用する。

開発中などブレッドボード等で頻繁に接続変更や追加等を行うような場合はINSECUREカーネルを使うといいかなと思います。既にデバイスの接続が固定されているのであれば起動直後に初期化を済ませばよさそうです。

GPIOのモード設定を起動時に行う

ここではカーネルの入れ替えも面倒なので起動時にモード設定を行うようにします。/etc/gpio.confというテキストファイルにgpioctlのコマンドを並べます(詳細はgpio.conf(5)のman page参照)。また、起動時の設定を有効にすることを/etc/rc.confに記述します。

設定が終わったらシステムを再起動します。前述のように一旦上がってしまったセキュリティレベルは下げることができないためです。

設定と再起動が終わったら、ピンの状態を確認してみます。

# gpioctl gpio0
/dev/gpio0: 4 pins

4つのピンを設定したため、4と返ってきました。

改めてLチカ

それではgpioctlでLEDを光らせてみます。

# gpioctl gpio0 5 on
pin 5: state 0 -> 1

これでLED1(青色)が光りました。onをoffに変えると消灯し、toggleにすると光ってれば消え、消えて入れば光ります。pinを6にすればLED2(白色)が対象になります。

ここではrootで実行しましたが、_gpioグループに所属している一般ユーザであればそのまま実行できます。

% id
uid=1000(oshima) gid=100(users) groups=100(users),0(wheel),5(operator),29(_gpio)
% gpioctl gpio0 5 toggle
pin 5: state 1 -> 0

ただし、GPIOの入出力モードを設定するgpioctl setコマンドは特権ユーザ(root)でなければ実行できません。これはセキュアレベルが0であったり、INSECUREカーネルであっても同じです(ドライバレベルで非特権ユーザでの実行が禁止されています)。

GPIOから入力(SWの状態を調べる)

この節全体2018.12.20に追記

次に入力としてタクトスイッチの状態を参照してみます。まずはそのまま読み込んでみます。

$ gpioctl gpio0 22
pin 22: state 0
スイッチSW1を押しながら実行してみます。
$ gpioctl gpio0 22
pin 22: state 0

あれ、相変わらずstateは0のままですよ?

回路図を見直すと、SW1,2とも、GPIOのピンとGNDの間にそのまま繋がっていました。つまりSWを押すとGNDと電位が同じ=0Vになることを期待しているわけで押してない場合には電圧が高い状態(3.3V側)になる回路でなければなりません。RPIのGPIOは内部でプルアップするかプルダウンするか選べるようになってて、GPIO22/23の初期状態はプルダウン(=GND側に抵抗を付けて接続)になっているため、これを変更する必要があります。

ということで、/etc/gpio.confに以下を追加します。

gpio0 22 pu
gpio0 23 pu

設定して再起動し、再度gpioctlで状態を見てみます。

$ gpioctl gpio0 22
pin 22: state 1

次にSW1を押しつづけながら実行します。ちょっと操作的に辛いのでshell loopで回しておきます(止めるのはCntl-C)。

$ while true; do gpioctl gpio0 22; sleep 1; done
pin 22: state 1
pin 22: state 1
pin 22: state 0
pin 22: state 1
^C
$

ボタンを押しているときだけ0になることが確認できました。これでSWの状態を検出すること=GPIOからの入力ができました。

GPIOをプログラムから使う

gpioctlコマンドを使ってGPIOのピン毎にON/OFFという最低限の動作ができましたが、色々な処理を行うプログラムに組み込むにはコマンドだけでは不便です。そこでプログラムインターフェースを使ってみます。gpio(4)のman pageによると、以下のようなIOCTLが存在するようです。

CMD説明
GPIOINFO使用可能なピン数を報告する
GPIOREADピンの状態(ON/OFF)を取得する
GPIOWRITEピンに状態(ON/OFF)を設定する
GPIOTOGGLEピンの状態を反転させる
GPIOSETピンの入出力モードを設定する
GPIOUNSETピンの入出力モードを解除する
GPIOATTACHGPIOのピンに各種ドライバを割り当てる

基本的にはgpioctl(8)のコマンドがそのままioctlのコマンドになっています。このうちGPIOSET/GPIOUNSETはgpioctl setコマンドと同じく、rootでなければ実行できません。

なおGPIOATTACHは、gpioの先に他のドライバをattachするものでちょっと特殊なもののようです。よくわかってないのでここでは割愛します。

ということでプログラムからはioctl GPIOREAD/GPIOWRITEを使ってGPIOを制御できるようです。 例えば次のようなコードでLEDを光らせることができます。最初にLEDをONにして5秒間そのまま、その後5秒間点滅し消灯して終了します。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/gpio.h>
#include <sys/ioctl.h>

int main() {
    int fd;              /* ファイルディスクリプタ*/
    struct gpio_req req; /* ioctlに渡すパラメータ */

    /* deviceのopen */
    if ((fd = open("/dev/gpio0", O_RDWR))==-1) {
        perror("open");
        exit(1);
     }
    req.gp_name[0] = 0; /* ピン名称による指定はしない */
    req.gp_pin = 5;     /* ピン 5 */
    req.gp_value = GPIO_PIN_HIGH; /* ONにする(実際の値は1) */

    /* ioctlの実行 */
    if ((ioctl(fd, GPIOWRITE, &req)) == -1) {
        perror("ioctl");
        exit(1);
    }

    sleep(5); /* 5秒待ち */

     /* だいたい5秒間点滅 */
    for ( int i = 0; i< 50; i++ ) {
        usleep(100000);				/* 100ms待ち */
        req.gp_value = i%2;			/* 交互に */
        if ((ioctl(fd, GPIOWRITE, &req)) == -1) {
            perror("ioctl");
            exit(1);
        }
    }

     /* 消す */
    req.gp_value = GPIO_PIN_LOW; /* OFFにする(実際の値は0) */;
    if ((ioctl(fd, GPIOWRITE, &req)) == -1) {
        perror("ioctl");
        exit(1);
    }
}

一応思ったように動きました。

まとめ

明日はI2C接続のキャラクタディスプレイをNetBSDから使ってみます。