C
Linux
RaspberryPi
kernel
デバイスドライバ

組み込みLinuxデバイスドライバの作り方 (5)

5回目: ラズパイ用のGPIOデバドラの実装

本連載について

組み込みLinuxのデバイスドライバをカーネルモジュールとして開発するためのHowTo記事です。本記事の内容は全てラズパイ(Raspberry Pi)上で動かせます。

本記事に登場するソースコード全体

https://github.com/take-iwiw/DeviceDriverLesson/tree/master/05_01

今回の内容

前回までで、デバイスドライバで基本的な操作を行うシステムコール(open, close, read, write)の実装方法が分かりました。今回は、これまでの内容を使って、ラズパイのGPIOデバイスドライバを実装します。

しっかりしたGPIOデバドラを作るのが目的ではないので、話を簡単にするために、対象はGPIO4のみで、常に出力モードとします。writeで、High/Lowを出力します。readで、現在の出力レベルを"1"/"0"の文字列で返します。

前提知識

メモリのお話(再び)

前回、ラズパイのメモリについて説明しました。Raspberry Pi 2において、ARM(CPU)から見たペリフェラルレジスタの物理アドレスは0x3F000000になります。これは、初代Raspberry Piだと0x20000000だったようです。実際、BCM2835のデータシートのメモリマップを見ると、I/O Peripheralsは0x20000000と記載されています。前回紹介した通り、このアドレスはbcm_host_get_peripheral_addressで取得できます。ただし、話を簡単にするため、今回は0x3F000000固定で考えます。もしも将来ラズパイ4とか5が出たら、このアドレスは変わる可能性があります。

ラズパイのハードウェアレジスタアクセス

上記の通り、ペリフェラルレジスタの物理アドレスが0x3F000000から始まるということはわかりました。GPIO制御のために具体的にどのレジスタを叩けばよいかも、BCM2835のデータシートに記載されています。この時、このデータシートの見方で1つ注意点があります。このデータシートには各レジスタの説明が記載されており、それぞれアドレスも記されています。しかし、そのアドレスは0x7E000000から始まるバスアドレスになります。我々が必要なのは、CPUから見た物理アドレスです。その先頭アドレスが0x3F000000ということは既に知っていますので、オフセットだけを見るようにしてください。
例えば、GPFSEL0(GPIO Function Select 0)は、データシートでは0x7E200000となっていますが、CPUから見た物理アドレスは0x3F200000(Raspberry Pi2の場合)になります。

ちなみに、今回使用するレジスタは下記のとおりです。

  • GPFSEL0 (GPIO Function Select 0)
    • 0x3F200000
    • 出力モード設定するのに使う
  • GPSET0 (GPIO Pin Output Set 0)
    • 0x3F20001C
    • High出力する
  • GPCLR0 (GPIO Pin Output Clear 0)
    • 0x3F200028
    • Low出力する
  • GPLEV0 (GPIO Pin Level 0)
    • 0x3F200034
    • 現在の出力レベルを取得する

ユーザ空間プログラムでお試し実装する

今までカーネルモジュールを作ってきたのですが、実はレジスタアクセスだけならユーザ空間からでもできます。今回はGPIO制御のレジスタを叩くだけなので、ひとまずユーザ空間上のプログラムで実装してみます。これでうまく動いたら、後でカーネルモジュール側でデバイスドライバの実装に取り掛かります。事前にユーザ空間で試すことで、デバッグがやりやすいという利点があります。

ユーザ空間プログラムから物理アドレスにアクセスするためには、まず/dev/memをopenします。そしてそのファイルディスクリプタ(fd)を使用して、mmapします。この時、オフセットとして物理アドレスを指定します。サイズは使用する分だけでもいいと思うのですが、ページサイズ(4KByte)確保している例が多いようです。なお、openするときにO_SYNCを指定しています。これによってキャッシュが無効になり、即レジスタアクセスが行われます。(そもそも、レジスタだからキャッシュは使われない設定になっていると思うのですが、念のため)

コードは以下のようになります。mmapで取得した仮想アドレスに対して読み書きすることで、レジスタ(0x3F000000)に対してアクセスが出来ます。各レジスタへ設定する値の解説は省略します。データシートをご確認ください。基本的には、GPIO4に対しての設定や値の入出力を行っているだけです。

userGpio.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>

/* ペリフェラルレジスタの物理アドレス(BCM2835の仕様書より) */
#define REG_ADDR_BASE        (0x3F000000)       /* bcm_host_get_peripheral_address()の方がbetter */
#define REG_ADDR_GPIO_BASE   (REG_ADDR_BASE + 0x00200000)
#define REG_ADDR_GPIO_LENGTH 4096
#define REG_ADDR_GPIO_GPFSEL_0     0x0000
#define REG_ADDR_GPIO_OUTPUT_SET_0 0x001C
#define REG_ADDR_GPIO_OUTPUT_CLR_0 0x0028
#define REG_ADDR_GPIO_LEVEL_0      0x0034

#define REG(addr) (*((volatile unsigned int*)(addr)))
#define DUMP_REG(addr) printf("%08X\n", REG(addr));

int main()
{
    int address;    /* GPIOレジスタへの仮想アドレス(ユーザ空間) */
    int fd;

    /* メモリアクセス用のデバイスファイルを開く */
    if ((fd = open("/dev/mem", O_RDWR | O_SYNC)) < 0) {
        perror("open");
        return -1;
    }

    /* ARM(CPU)から見た物理アドレス → 仮想アドレスへのマッピング */
    address = (int)mmap(NULL, REG_ADDR_GPIO_LENGTH, PROT_READ | PROT_WRITE, MAP_SHARED, fd, REG_ADDR_GPIO_BASE);
    if (address == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return -1;
    }

    /* GPIO4を出力に設定 */
    REG(address + REG_ADDR_GPIO_GPFSEL_0) = 1 << 12;

    /* GPIO4をHigh出力 */
    REG(address + REG_ADDR_GPIO_OUTPUT_SET_0) = 1 << 4;
    DUMP_REG(address + REG_ADDR_GPIO_LEVEL_0);

    /* GPIO4をLow出力 */
    REG(address + REG_ADDR_GPIO_OUTPUT_CLR_0) = 1 << 4;
    DUMP_REG(address + REG_ADDR_GPIO_LEVEL_0);

    /* 使い終わったリソースを解放する */
    munmap((void*)address, REG_ADDR_GPIO_LENGTH);
    close(fd);

    return 0;
}

動かしてみる

GPIO4のピン(SCLの右隣のピン)を、LEDと抵抗経由で3.3Vに接続します。下記コマンドでビルドと実行をします。

gcc userGpio.c
sudo ./a.out

コードを見るとわかると思うのですが、最後にGPIO4をLow出力しているので、LEDが点灯します。この行をコメントアウトしたら、GPIO4をHigh出力するのでLEDが消灯します。

デバイスドライバをカーネルモジュールとして実装する

仕様

ついに、本当のデバイスドライバを作ります。冒頭で述べた通り、今回は簡単のために非常にシンプルな仕様とします。

  • open
    • GPIO4を出力設定にする
  • close
    • なにもしない
  • write
    • "1"でHigh出力。"0"でLow出力
  • read
    • 現在の出力値を"0"か"1"で返す

実装

カーネル空間では、物理アドレスから仮想アドレス(非キャッシュ)への変換にはioremap_nocacheを使用します。このアドレス変換ですが、カーネルモジュールのロードやopen時に、一括で大きい領域(例えば4KByte)を変換してしまおうと思ったのですが、カーネル空間は、全体で1つの仮想アドレス空間しか持ちません。なので、たった1つのモジュールがそんなに占有していいのかな? と思い、レジスタアクセスするたびに4Byteずつ変換して使うことにしました。当然速度の面では不利になります。ふつうはどうするんだろう? 知見のある方、ご存知でしたら教えてください。(カーネル空間の場合は、仮想アドレスは割り当てているのではなくて、物理アドレスから変換しているだけな気もします。なので、アドレス空間の無駄遣いにはならないのかもしれない)

アドレス変換が出来たら、あとはユーザ空間のプログラムと同じようにレジスタアクセスが出来ます。コードは以下のようになります。mydevice_open, mydevice_read, mydevice_write内の処理が重要なところになります。それ以外のinitとexitは前回と同じです。省略しようと思ったのですが、別の記事なので一応全部載せます。

なお、ユーザ空間とのデータのやり取りですが、前回はcopy_to_usercopy_from_userを使用しました。今回はやり取りするデータが1Byteだけなので、put_userget_userを使ってみました。

myDeviceDriver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/current.h>
#include <asm/uaccess.h>
#include <asm/io.h>

/* ペリフェラルレジスタの物理アドレス(BCM2835の仕様書より) */
#define REG_ADDR_BASE        (0x3F000000)       /* bcm_host_get_peripheral_address()の方がbetter */
#define REG_ADDR_GPIO_BASE   (REG_ADDR_BASE + 0x00200000)
#define REG_ADDR_GPIO_GPFSEL_0     0x0000
#define REG_ADDR_GPIO_OUTPUT_SET_0 0x001C
#define REG_ADDR_GPIO_OUTPUT_CLR_0 0x0028
#define REG_ADDR_GPIO_LEVEL_0      0x0034

#define REG(addr) (*((volatile unsigned int*)(addr)))
#define DUMP_REG(addr) printk("%08X\n", REG(addr));

/*** このデバイスに関する情報 ***/
MODULE_LICENSE("Dual BSD/GPL");
#define DRIVER_NAME "MyDevice"              /* /proc/devices等で表示されるデバイス名 */
static const unsigned int MINOR_BASE = 0;   /* このデバイスドライバで使うマイナー番号の開始番号と個数(=デバイス数) */
static const unsigned int MINOR_NUM  = 1;   /* マイナー番号は 0のみ */
static unsigned int mydevice_major;         /* このデバイスドライバのメジャー番号(動的に決める) */
static struct cdev mydevice_cdev;           /* キャラクタデバイスのオブジェクト */
static struct class *mydevice_class = NULL; /* デバイスドライバのクラスオブジェクト */

/* open時に呼ばれる関数 */
static int mydevice_open(struct inode *inode, struct file *file)
{
    printk("mydevice_open");

    /* ARM(CPU)から見た物理アドレス → 仮想アドレス(カーネル空間)へのマッピング */
    int address = (int)ioremap_nocache(REG_ADDR_GPIO_BASE + REG_ADDR_GPIO_GPFSEL_0, 4);

    /* GPIO4を出力に設定 */
    REG(address) = 1 << 12;

    iounmap((void*)address);

    return 0;
}

/* close時に呼ばれる関数 */
static int mydevice_close(struct inode *inode, struct file *file)
{
    printk("mydevice_close");
    return 0;
}

/* read時に呼ばれる関数 */
static ssize_t mydevice_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    printk("mydevice_read");

    /* ARM(CPU)から見た物理アドレス → 仮想アドレス(カーネル空間)へのマッピング */
    int address = address = (int)ioremap_nocache(REG_ADDR_GPIO_BASE + REG_ADDR_GPIO_LEVEL_0, 4);
    int val = (REG(address) & (1 << 4)) != 0;   /* GPIO4が0かどうかを0, 1にする */

    /* GPIOの出力値をユーザへ文字として返す */
    put_user(val + '0', &buf[0]);

    iounmap((void*)address);

    return count;
}

/* write時に呼ばれる関数 */
static ssize_t mydevice_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    printk("mydevice_write");

    int address;
    char outValue;

    /* ユーザが設定したGPIOへの出力値を取得 */
    get_user(outValue, &buf[0]);

    /* ARM(CPU)から見た物理アドレス → 仮想アドレス(カーネル空間)へのマッピング */
    if(outValue == '1') {
        /* '1'ならSETする */
        address = (int)ioremap_nocache(REG_ADDR_GPIO_BASE + REG_ADDR_GPIO_OUTPUT_SET_0, 4);
    } else {
        /* '0'ならCLRする */
        address = (int)ioremap_nocache(REG_ADDR_GPIO_BASE + REG_ADDR_GPIO_OUTPUT_CLR_0, 4);
    }
    REG(address) = 1 << 4;
    iounmap((void*)address);

    return count;
}

/* 各種システムコールに対応するハンドラテーブル */
struct file_operations s_mydevice_fops = {
    .open    = mydevice_open,
    .release = mydevice_close,
    .read    = mydevice_read,
    .write   = mydevice_write,
};

/* ロード(insmod)時に呼ばれる関数 */
static int mydevice_init(void)
{
    printk("mydevice_init\n");

    int alloc_ret = 0;
    int cdev_err = 0;
    dev_t dev;

    /* 1. 空いているメジャー番号を確保する */
    alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME);
    if (alloc_ret != 0) {
        printk(KERN_ERR  "alloc_chrdev_region = %d\n", alloc_ret);
        return -1;
    }

    /* 2. 取得したdev( = メジャー番号 + マイナー番号)からメジャー番号を取得して保持しておく */
    mydevice_major = MAJOR(dev);
    dev = MKDEV(mydevice_major, MINOR_BASE);    /* 不要? */

    /* 3. cdev構造体の初期化とシステムコールハンドラテーブルの登録 */
    cdev_init(&mydevice_cdev, &s_mydevice_fops);
    mydevice_cdev.owner = THIS_MODULE;

    /* 4. このデバイスドライバ(cdev)をカーネルに登録する */
    cdev_err = cdev_add(&mydevice_cdev, dev, MINOR_NUM);
    if (cdev_err != 0) {
        printk(KERN_ERR  "cdev_add = %d\n", alloc_ret);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }

    /* 5. このデバイスのクラス登録をする(/sys/class/mydevice/ を作る) */
    mydevice_class = class_create(THIS_MODULE, "mydevice");
    if (IS_ERR(mydevice_class)) {
        printk(KERN_ERR  "class_create\n");
        cdev_del(&mydevice_cdev);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }

    /* 6. /sys/class/mydevice/mydevice* を作る */
    for (int minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_create(mydevice_class, NULL, MKDEV(mydevice_major, minor), NULL, "mydevice%d", minor);
    }

    return 0;
}

/* アンロード(rmmod)時に呼ばれる関数 */
static void mydevice_exit(void)
{
    printk("mydevice_exit\n");

    dev_t dev = MKDEV(mydevice_major, MINOR_BASE);

    /* 7. /sys/class/mydevice/mydevice* を削除する */
    for (int minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_destroy(mydevice_class, MKDEV(mydevice_major, minor));
    }

    /* 8. このデバイスのクラス登録を取り除く(/sys/class/mydevice/を削除する) */
    class_destroy(mydevice_class);

    /* 9. このデバイスドライバ(cdev)をカーネルから取り除く */
    cdev_del(&mydevice_cdev);

    /* 10. このデバイスドライバで使用していたメジャー番号の登録を取り除く */
    unregister_chrdev_region(dev, MINOR_NUM);
}

module_init(mydevice_init);
module_exit(mydevice_exit);

動作確認

以下のように、ビルドして、カーネルに組み込みます。

make
sudo insmod MyDeviceModule.ko
echo "0" > /dev/mydevice0
echo "1" > /dev/mydevice0
cat /dev/mydevice0
1111111111111111^C

echoで"0"または"1"を書き込むことで、GPIO4の出力が変わりLEDがチカチカするはずです。また、catで読むことでその時点の出力レベルが表示されます。ただし、常に値を返すので、適当にCtrl-cで止めてあげる必要があります。

おまけ

以下のコマンドで、物理メモリマップを確認できるようです。これを見ると、今まで見てきたのと同じ内容になっていることが分かります。
SDRAMは物理アドレス0番地から配置されています。下のメモリマップでは00000000-3b3fffffとなっています。0x3b3fffff = 994050047 = 約1GByteなので、実際とも会っていそうです。また、ペリのレジスタアドレスも0x3fXXXXXXに配置されています。

sudo cat /proc/iomem
00000000-3b3fffff : System RAM
  00008000-00afffff : Kernel code
  00c00000-00d3da63 : Kernel data
3f006000-3f006fff : dwc_otg
3f007000-3f007eff : /soc/dma@7e007000
3f00b840-3f00b84e : /soc/vchiq
3f00b880-3f00b8bf : /soc/mailbox@7e00b880
3f101000-3f102fff : /soc/cprman@7e101000
3f200000-3f2000b3 : /soc/gpio@7e200000
3f201000-3f201fff : /soc/serial@7e201000
  3f201000-3f201fff : /soc/serial@7e201000
3f202000-3f2020ff : /soc/sdhost@7e202000
3f215000-3f215007 : /soc/aux@0x7e215000
3f980000-3f98ffff : dwc_otg