🍓

raspberry piで学ぶ組込みLinuxデバイスドライバ開発Part2

2024/02/04に公開

はじめに

前回からの続きです。

前回までで基本的なデバイスドライバを作成して動作確認をしましたが、Linux上で完結するドライバであり、ラズパイ自体は使っていませんでした。
今回はラズパイにLEDとスイッチを簡単な回路で接続して、それを操作するデバイスドライバを作成してみます。

セミナーで使用したボートには4つのLEDと4つのスイッチが付いていたので1つのドライバで4つ同時に制御するものを作りましたが、回路を作るのが面倒なので1つずつです。
ご承知おきくださいませ。

LEDを操作するデバイスドライバ

GPIO18番ピンにLEDを接続してこれを点けたり消したりできるモジュールを作成します。
仕様は以下のようにします。

  • デバイスファイル名: /dev/led
  • メジャー番号: 60
  • 1が書き込まれたらLEDが点灯し、0が書き込まれたら消える

前回作成したSKELの雛形モジュールをそのまま使います。
必要となる実装はwriteのシステムコールで1が書き込まれたらGPIO18の出力をHighにして、0が書き込まれたらLowにします。
またprobe時にGPIO18番ピンの初期化処理として、出力に設定します。

LEDデバイスドライバはwriteされた時に動作すればいいので、readに対しては何も処理は行いません。

raspberry pi 3のレジスタ操作

raspberry pi 3で使われているCPUはBCM2837というSocチップです。
仕様書を見てCPUのレジスタを操作することで、GPIOの出力設定や出力操作をします。

ここで考えないといけないのが、MMU(Memory Management Uint)の存在です。
MMUによりメモリの物理アドレスは仮想アドレスに変換されます。モジュールは仮想アドレスではなく、物理アドレスを操作しないとLEDを操作できません。

※↓仕様書より

1.2.3 ARM physical addressesに以下のように書かれているので、ペリフェラルIOの設定をする物理アドレスは 0x3F000000 がスタートとわかります。

Physical addresses range from 0x3F000000 to 0x3FFFFFFF for peripherals. The
bus addresses for peripherals are set up to map onto the peripheral bus address range
starting at 0x7E000000.

というわけでモジュールの実装でも、0x3F000000 を define しています。

// raspberry pi 3のペリフェラルIOの物理アドレス
#define BCM2837_PERI_BASE   0x3F000000

仕様書の6.1 Register Viewに行きレジスタのアドレスを確認します。

0x3F0000000x7E000000 のPCIバスにマップされると書かれていますが、GPFSEL00x7E200000 にマップされているので、
コードのdefineで 0x3F000000200000 を足します。

// raspberry pi 3のペリフェラルIOの物理アドレス
#define BCM2837_PERI_BASE   0x3F000000
#define GPIO_BASE_ADDR      (BCM2837_PERI_BASE + 0x200000)

※MMUのイメージ図

MMUによりプロセスAがプロセスBの物理メモリにアクセスしてデータを破壊することはないし、その逆も起きえない。

probe関数

init関数はSKELの雛形ドライバと処理は同じです。
probe関数でデバイスファイルの作成とLEDの初期化処理を行います。

まず request_mem_region で物理メモリの範囲を取得します。
範囲を取得したら、ioremap で仮想アドレスにマップしてプログラムから物理メモリを操作できるようにします。

ioremapでセットしたgpio変数でレジスタを操作します。
GPIO18番ピンを出力に設定するためには仕様書の92ページによると GPFSEL1 の26-24bitに001をセットすればいいのでそのように操作します。

    gpio[GPFSEL1 / 4] &= ~(0x7 << ((LED_PIN % 10) * 3));  // bitをクリア
    gpio[GPFSEL1 / 4] |= (0x1 << ((LED_PIN % 10) * 3));   // 26-24bitに001をセット

デバイスファイルが作成できたら、iounmaprelease_mem_region でメモリを解放します。

// probe関数
static int led_probe(struct platform_device *pdev)
{
    struct device *dev;
    printk("LED Probe\n");

    request_mem_region(GPIO_BASE_ADDR, MEM_SIZE, DEV_NAME);
    gpio = ioremap(GPIO_BASE_ADDR, MEM_SIZE);
    // GPIO18を出力に設定
    gpio[GPFSEL1 / 4] &= ~(0x7 << ((LED_PIN % 10) * 3));
    gpio[GPFSEL1 / 4] |= (0x1 << ((LED_PIN % 10) * 3));

    // デバイスファイルを作成する
    dev = device_create(&led_class, NULL, MKDEV(LED_MAJOR_NUM, 0), NULL, DEV_NAME);
    if (IS_ERR(dev)) {
        dev_err(&pdev->dev, "failed to create device.\n");
        return PTR_ERR(dev);
    }
    iounmap((void*)gpio);
    release_mem_region(GPIO_BASE_ADDR, MEM_SIZE);
    return 0;
}

write関数

echo 1 > /dev/ledのようにwriteされた時の操作です。
まずcopy_from_userでユーザ空間から書き込まれた文字を取得します。

request_mem_regionioremapでペリフェラルIOの物理アドレスを取得して操作できるようにするのはprobe関数と同じです。
echoされた文字が1だったら、GPSET0に1をセットしてGPIO18をHighにします。0ならGPCLR0でクリアしてLowにします。

// writeハンドラ
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    char *msg;
    printk("LED Write\n");
    msg = kmalloc(2, GFP_KERNEL);
    if (msg == NULL) {
        return -ENOMEM;
    }
    // echoされた文字を取得
    if (copy_from_user(msg + *ppos, buf, count)) {
        return -EFAULT;
    }
    *ppos += count;
    msg[*ppos] = '\0';

    request_mem_region(GPIO_BASE_ADDR, MEM_SIZE, DEV_NAME);
    gpio = ioremap(GPIO_BASE_ADDR, MEM_SIZE);

    if (sysfs_streq("1", msg)) {
        printk("LED Turn on\n");
        // LED点灯
        gpio[GPSET0 / 4] = (1 << (LED_PIN % 32));
    } else {
        printk("LED Turn off\n");
        // LED消灯
        gpio[GPCLR0 / 4] = (1 << (LED_PIN % 32));
    }
 
    iounmap((void*)gpio);
    release_mem_region(GPIO_BASE_ADDR, MEM_SIZE);
    kfree(msg);
    return count;
}

remove関数

remove関数では念のため、GPCLR0で出力をクリアしておきます。

// remove関数
static int led_remove(struct platform_device *pdev)
{
    printk("LED Remove\n");
    // デバイスファイルを削除する
    device_destroy(&led_class, MKDEV(LED_MAJOR_NUM, 0));

    request_mem_region(GPIO_BASE_ADDR, MEM_SIZE, DEV_NAME);
    gpio = ioremap(GPIO_BASE_ADDR, MEM_SIZE);
    // Turn off the LED
    gpio[GPCLR0 / 4] = (1 << (LED_PIN % 32));
    iounmap((void*)gpio);
    release_mem_region(GPIO_BASE_ADDR, MEM_SIZE);

    return 0;
}

LEDデバイスドライバの実装は以上です。

LEDデバイスドライバの動作確認

実際にラズパイで動作確認をしてみます。
modprobe ledをすると /dev/led ファイルが作成されました。

/dev/led に1を書き込むとLEDが点灯し、0を書くと消えることがわかります。

pi@raspberrypi:~ $ sudo modprobe led
pi@raspberrypi:~ $ ls /dev/led
/dev/led
pi@raspberrypi:~ $ sudo bash -c 'echo 1 > /dev/led'
pi@raspberrypi:~ $ sudo bash -c 'echo 0 > /dev/led'

SWの値を読み取るデバイスドライバ

GPIO18番ピンにタクトスイッチを接続して、スイッチの値を読み取るモジュールを作成します。

  • デバイスファイル名: /dev/sw
  • メジャー番号: 60
  • readされた時にスイッチが押されていれば1を返し、押されていれば0を返す。

必要となる実装はreadのシステムコールが発行された時にスイッチの状態を読み取り、1か0の文字を返します。
またprobe時にGPIO18番ピンの初期化処理として、入力に設定します。

SWデバイスドライバはreadされた時に動作すればいいので、writeに対しては何も処理は行いません。

probe関数

probe関数はGPIO18を入力にするために000をセットしているだけで、LEDドライバのprobe関数を同じです。

// probe関数
static int sw_probe(struct platform_device *pdev)
{
    struct device *dev;
    printk("SW Probe\n");

    request_mem_region(GPIO_BASE_ADDR, MEM_SIZE, DEV_NAME);
    gpio = ioremap(GPIO_BASE_ADDR, MEM_SIZE);
    // GPIO18を入力に設定
    gpio[GPFSEL1 / 4] &= ~(0x7 << ((SW_PIN % 10) * 3));
    gpio[GPFSEL1 / 4] |= (0x0 << ((SW_PIN % 10) * 3));

    // デバイスファイルを作成する
    dev = device_create(&sw_class, NULL, MKDEV(SW_MAJOR_NUM, 0), NULL, DEV_NAME);
    if (IS_ERR(dev)) {
        dev_err(&pdev->dev, "faisw to create device.\n");
        return PTR_ERR(dev);
    }
    iounmap((void*)gpio);
    release_mem_region(GPIO_BASE_ADDR, MEM_SIZE);
    return 0;
}

read関数

readのシステムコールに対してスイッチの状態を返す処理を実装します。

SWの状態を読み取ったら、文字列に変換してcopy_to_userでユーザ空間にコピーします。

gpio[13]と書いてレジスタのGPLEV0=GPIO Pin Level 0にアクセスし、GPIO18番ピンのbitの値をvalにセットします。
スイッチを押すとbitが立つので1がvalに入り、押していないと0が入ります。

文字を返すのは前回作成したSTACKドライバと処理は同じです。

// readハンドラ
static ssize_t sw_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    int len, val;
    char *msg;
    printk("SW Read\n");
    
    msg = kmalloc(2, GFP_KERNEL);
    if (msg == NULL) {
        return -ENOMEM;
    }
    request_mem_region(GPIO_BASE_ADDR, MEM_SIZE, DEV_NAME);
    gpio = ioremap(GPIO_BASE_ADDR, MEM_SIZE);

    // SWの状態を読み取る
    val = (gpio[13] & (1 << (SW_PIN % 32))) != 0;
    sprintf(msg, "%d\n", val);
    
    len = strlen(msg);
    if (len - *ppos == 0) {
        return 0;
    }
    if (count > len - *ppos) {
        count = len - *ppos;
    }
    if (copy_to_user(buf, msg, len)) {
        return -EFAULT;
    }
    *ppos += count;

    iounmap((void*)gpio);
    release_mem_region(GPIO_BASE_ADDR, MEM_SIZE);

    return count;
}

SWドライバの実装は以上です。

SWドライバの動作確認

SWモジュールをmodprobeして動作確認をしましょう。
/dev/swファイルがあることを確認してcatしてみます。

そのままでは0が返ってきますが、スイッチを押しながらcatすると1が返ってくることがわかります。

pi@raspberrypi:~ $ sudo modprobe sw
pi@raspberrypi:~ $ ls /dev/sw
/dev/sw
pi@raspberrypi:~ $ sudo cat /dev/sw
0
pi@raspberrypi:~ $ sudo cat /dev/sw
1
pi@raspberrypi:~ $ sudo cat /dev/sw
0

To be continued...

今回は実際にラズパイにLEDとSWを接続してそれらを操作するデバイスドライバを作成してみました。
これまで作成してきたデバイスドライバのファイルは、/devに作成されていました。

Linux 2.6以降ではsysfsを利用して/sys/classにデバイスドライバを作る方法がありますので次回はそれを取り上げます。

参考文献

https://www.ei.tohoku.ac.jp/xkozima/lab/raspTutorial3.html
https://youngkin.github.io/post/gpioprogramming/
https://cwshu.github.io/arm_virt_notes/notes/dma/driver_io.html#

Discussion