Day 26:IIO (Part 4) - 幫感應器寫驅動程式!以 TCRC5000 為例

這篇將會綜合前面的 GPIO 與 IIO 的知識,幫一個常見的紅外線感測器 -- TCRC5000 實作 Linux 上的 IIO 驅動程式。

簡介:TCRC5000

這是一個紅外線感應模組。以紅外線作為距離感應,當距離小於閾值時,輸出高電位; 反之則輸出低電位。而這個閾值大小可以透過調整其上的可變電阻更動。更多敘述可以參考 OSOYOO 公司底下的 Product description 一節。

img

硬體配置

VCC 接 Raspberry Pi 的 5V,GND 接 Raspberry Pi 的任意 GND; OUTGPIO17。如圖所示:

img

裝置樹

這個 Device Tree Overlay 的部分跟前幾天 GPIO 的部分幾乎一樣,除了把名稱更換掉,以及 GPIO 由輸入改為輸出以外:

/dts-v1/;
/plugin/;
/ {
    compatible="brcm,brcm2835";
    fragment@0 {
        target = <&gpio>;
        __overlay__ {
            tcrc5000: tcrc5000_gpio_pins {
                brcm,pins = <0x11>;
                brcm,function = <0x0>;
                brcm,pull = <0x1>;
            };
        };
    };
    fragment@1 {
        target-path = "/";
        __overlay__ {
            tcrc5000 {
                tcrc5000-gpios = <&gpio 0x11 0x0>;
                compatible = "tcrc5000";
                status = "ok";
                pinctrl-0 = <&tcrc5000>;
                pinctrl-names = "default";
            };
        };
    };
};

Step 1:資料結構

為了方便,把這個資料結構中需要的硬體資源相關的資料結構,包成一個結構體:

struct tcrc5000 {
    struct gpio_desc *gpiod;
    struct mutex mutex;
    struct device *dev;
};

這邊的 gpiod 就是讀取時對應的 GPIO 所對應的 GPIO Descriptor。除此之外,還配置一個 mutex 來保護。避免 IIO 的 sysfs 介面有不同的執行單元同時讀取。

Step 2:申請 iio_dev 與 GPIO

首先在 probe 中,先幫各種資料結構配置空間。為了方便資源管理,這邊使用 devm_* 系列的函數 (因為所這個模組中的資源都是以 devm_* 函數配置的,所以就沒有實作 remove)。在 devm_gpiod_get_index 當中 ,tcrc5000 的參數是搭配裝置樹使用的。因為前面處理裝置樹時,這個感測器所使用的 GPIO 編號,是放在這個感測器對應的裝置節點的 tcrc5000-gpios 屬性中,所以就可以直接用 *gpiod_get_index 函數找出「前綴 (也就是 tcrc5000) 對應的第 0 個 GPIO」:

static int tcrc5000_probe(struct platform_device *pdev)
{
    ...
    iio = devm_iio_device_alloc(dev, sizeof(struct tcrc5000));
    tcrc5000 = iio_priv(iio);
    tcrc5000->dev = dev;
    tcrc5000->gpiod = devm_gpiod_get_index(dev, "tcrc5000", 0, GPIOD_IN);
    mutex_init(&tcrc5000->mutex);
    ...
}

Step 3:給定 iio_chan_spec

因為感應器現在只有一道輸出 (就是那個 OUT)。所以在 iio_chan_spec 中就只給宣告一道輸出。又因為這是屬於接近相關的感測器,所以種類為 IIO_PROXIMITY

#define IIO_CHANNEL_DEFINE(num)    {\
        .type = IIO_PROXIMITY,\
        .indexed = 1,\
        .channel = (num),\
        .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),\
    }\

static const struct iio_chan_spec tcrc5000_channels[] = {
    IIO_CHANNEL_DEFINE(0)
};

Step 4:讀取資料

這邊就是實作 iio_info 中的 read_raw 函數。現在的數值是從 Raspberry Pi 上的其中一個 GPIO 讀取,那麼就把那個 GPIO 的輸入用 gpiod_get_value 讀取,接著存到 *val0 當中 (也就是最終會在 sysfs 檔案中出現的值)。最後用回傳值 IIO_VAL_INT 提醒現在回傳的東西是一個整數:

static int tcrc5000_read_raw (struct iio_dev *iio, struct iio_chan_spec const *chan,
        int *val0, int *val1, long mask)
{
    struct tcrc5000 *tcrc5000;
    struct gpio_desc *gpiod;

    tcrc5000 = iio_priv(iio);
    gpiod = tcrc5000 -> gpiod;
    mutex_lock(&tcrc5000->mutex);
    (*val0) = gpiod_get_value(gpiod);
    mutex_unlock(&tcrc5000->mutex);
    return IIO_VAL_INT;
}

Step 5:填寫 iio_info 與 iio_dev

實作完之後,填寫對應的 iio_info

struct iio_info tcrc5000_info = {
    .read_raw = tcrc5000_read_raw,
};

並且回到 probe 當中,把剩下的初始化做完:

static int tcrc5000_probe(struct platform_device *pdev)
{
    ...
    ...
    iio -> name = pdev->name;
    iio -> info = &tcrc5000_info;
    iio -> modes = INDIO_DIRECT_MODE;
    iio -> channels = tcrc5000_channels;
    iio -> num_channels = ARRAY_SIZE(tcrc5000_channels);

    return devm_iio_device_register(dev, iio);
}

安裝並執行

裝置樹疊加的步驟,以及 Makefile 都與之前相同,僅有檔案名稱不同而已,這邊就不再重複內容。安裝上模組之後,可以在 /sys/bus/iio/devices/ 底下找到對應的裝置節點。以這邊為例,是 iio:device1

$ cd /sys/bus/iio/devices/iio\:device1
$ ls
dev  in_proximity0_input  name  power  subsystem  uevent

在距離不同的狀況下,in_proximity0_input 的檔案內容會有所不同。如果距離低於閾值,那麼結果將會是 0:

$ cat in_proximity0_input
0

反之,結果會是 1:

$ cat in_proximity0_input
1

如果執行以下的 python 程式:

import os
from time import sleep
iio_oneshot_path = "/sys/bus/iio/devices/iio:device1/in_proximity0_input"
while 1:
    fd = open(iio_oneshot_path, "r")
    res = fd.read()
    print(res.strip())
    sleep(0.025)
    fd.close()

就可以觀察到「讀值隨距離產生變化」的結果。實驗的影片在 這個連結中

完整程式

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/iio/consumer.h>
#include <linux/iio/iio.h>
#include <linux/of.h>
#include <linux/gpio/consumer.h>
#include <linux/mutex.h>

struct tcrc5000 {
    struct gpio_desc *gpiod;
    struct mutex mutex;
    struct device *dev;
};

#define IIO_CHANNEL_DEFINE(num)    {\
        .type = IIO_PROXIMITY,\
        .indexed = 1,\
        .channel = (num),\
        .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),\
    }\


static const struct iio_chan_spec tcrc5000_channels[] = {
    IIO_CHANNEL_DEFINE(0)
};

static int tcrc5000_read_raw (struct iio_dev *iio, struct iio_chan_spec const *chan,
        int *val0, int *val1, long mask)
{
    struct tcrc5000 *tcrc5000;
    struct gpio_desc *gpiod;

    tcrc5000 = iio_priv(iio);
    gpiod = tcrc5000 -> gpiod;
    mutex_lock(&tcrc5000->mutex);
    (*val0) = gpiod_get_value(gpiod);
    mutex_unlock(&tcrc5000->mutex);
    return IIO_VAL_INT;
}

struct iio_info tcrc5000_info = {
    .read_raw = tcrc5000_read_raw,
};

static int tcrc5000_probe(struct platform_device *pdev)
{
    struct device *dev = &(pdev-> dev);
    struct iio_dev *iio;
    struct tcrc5000 *tcrc5000;

    iio = devm_iio_device_alloc(dev, sizeof(struct tcrc5000));
    if (!iio) {
        dev_err(dev, "Failed to allocate IIO/.\n");
        return -ENOMEM;
    }

    tcrc5000 = iio_priv(iio);
    tcrc5000->dev = dev;
    tcrc5000->gpiod = devm_gpiod_get_index(dev, "tcrc5000", 0, GPIOD_IN);
    if (IS_ERR(tcrc5000->gpiod)) {
        dev_err(dev, "Failed to get gpio descriptor.\n");
        return PTR_ERR(tcrc5000 -> gpiod);
    }
    mutex_init(&tcrc5000->mutex);

    iio -> name = pdev->name;
    iio -> info = &tcrc5000_info;
    iio -> modes = INDIO_DIRECT_MODE;
    iio -> channels = tcrc5000_channels;
    iio -> num_channels = ARRAY_SIZE(tcrc5000_channels);

    return devm_iio_device_register(dev, iio);
}

static const struct of_device_id tcrc5000_ids[] = {
    {.compatible = "tcrc5000",},
    {}
};

static struct platform_driver tcrc5000_driver = {
    .driver = {
        .name = "tcrc5000",
        .of_match_table = tcrc5000_ids,
    },
    .probe = tcrc5000_probe
};
MODULE_LICENSE("GPL");
module_platform_driver(tcrc5000_driver);

附註:安裝模組時出現 Unknown symbol

在嘗試 IIO 的其他功能的時候,有時候安裝模組時會出現類似下面,Unknown symbol ... (err -2) 的訊息:

[ 3508.195974] tcrc5000: Unknown symbol devm_iio_triggered_buffer_setup (err -2)

除了可能是 License 不相容之外,另外一個可能的原因是:在編譯核心的時候,一部分的功能在配置時被設為以「模組」形式編譯 (也就是 m 選項),而不是直接編在核心中 (y 選項)。比如說如果去查詢核心的配置:

CONFIG_IIO=m
CONFIG_IIO_BUFFER=y
CONFIG_IIO_BUFFER_CB=m
# CONFIG_IIO_BUFFER_HW_CONSUMER is not set
CONFIG_IIO_KFIFO_BUF=m
CONFIG_IIO_TRIGGERED_BUFFER=m
# CONFIG_IIO_CONFIGFS is not set
CONFIG_IIO_TRIGGER=y
CONFIG_IIO_CONSUMERS_PER_TRIGGER=2
# CONFIG_IIO_SW_DEVICE is not set
# CONFIG_IIO_SW_TRIGGER is not set

就會發現這當中,CONFIG_IIO_TRIGGERED_BUFFER 被設為 m。這時如果把對應的模組安裝回去:

$ sudo modprobe industrialio-triggered-buffer

就可以順利載入模組了。