在嵌入式系統和工業自動化領域,透過 GPIO 控制外部裝置至關重要。本文介紹 Linux 工業 I/O 子系統和 libgpiod 函式庫的 GPIO 控制方法,提供實務操作指導和程式碼範例,包含字元裝置控制和 libgpiod 應用。同時,闡述 Nunchuk 提供者和消費者模組開發,並以 Nunchuk 加速度計為例,講解工業 I/O 子系統驅動程式開發的流程,涵蓋初始化、註冊、通道屬性定義、資料讀取和裝置匹配等核心環節,讓開發者快速掌握 Linux GPIO 控制和 IIO 驅動程式開發技巧。

工業I/O子系統中GPIO控制的實務應用

前言

在工業自動化與嵌入式系統開發中,GPIO(通用輸入/輸出)介面扮演著至關重要的角色。透過GPIO,開發者能夠控制與讀取外部裝置的狀態,實作硬體層級的互動。本文將探討在Linux環境下,如何利用工業I/O子系統及libgpiod函式庫進行GPIO控制,並提供具體的程式碼範例與實務操作指導。

工業I/O子系統與GPIO控制簡介

工業I/O(Industrial I/O)子系統是Linux核心提供的一個強大的框架,用於處理各種工業與嵌入式裝置中的I/O操作。GPIO控制是其中的一項重要功能,透過該子系統,開發者能夠以統一且高效的方式管理GPIO資源。

使用libgpiod控制GPIO

libgpiod是一個使用者空間函式庫,提供了一套API用於控制GPIO。相較於傳統的sysfs介面,libgpiod提供了更為現代化和高效的GPIO控制方式。

實務操作:使用libgpiod控制PIXI™ CLICK板上的GPIO

  1. 硬體連線:將PIXI™ CLICK板的port19(GPO)連線到Color click eval board上的紅色LED,並將port18或port7組態為GPI。

  2. 軟體操作

    • 使用gpioset命令設定port19(GPO)為高或低電平。
    • 使用gpioget命令讀取port18或port7(GPI)的狀態。

範例如下:

# 設定port19(GPO)為高電平
root@raspberrypi:/home/pi# gpioset gpiochip3 3=1

# 讀取port18(GPI)的狀態
root@raspberrypi:/home/pi# gpioget gpiochip3 2

程式碼範例:使用字元裝置控制GPIO

以下是一個簡單的應用程式,用於切換PIXI™ CLICK板上port19的狀態十次,使連線的紅色LED閃爍。

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

#define GPIO_CHIP "/dev/gpiochip3"
#define GPIO_LINE 3 // 對應port19

int main() {
    int fd, ret;
    struct gpiohandle_request req;
    struct gpiohandle_data data;

    fd = open(GPIO_CHIP, O_RDONLY);
    if (fd < 0) {
        perror("開啟GPIO晶片失敗");
        exit(1);
    }

    req.lineoffsets[0] = GPIO_LINE;
    req.flags = GPIOHANDLE_REQUEST_OUTPUT;
    strcpy(req.consumer_label, "LED控制");
    req.lines = 1;

    ret = ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &req);
    if (ret < 0) {
        perror("取得GPIO線路控制控制程式碼失敗");
        exit(1);
    }

    int handle_fd = req.fd;

    for (int i = 0; i < 10; i++) {
        data.values[0] = (i % 2 == 0) ? 1 : 0; // 切換電平
        ret = ioctl(handle_fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
        if (ret < 0) {
            perror("設定GPIO線路值失敗");
            exit(1);
        }
        sleep(1); // 等待1秒
    }

    close(handle_fd);
    close(fd);

    return 0;
}

編譯與執行

# 將程式碼傳送到Raspberry Pi並編譯
root@raspberrypi:/home/pi# gcc -o gpio_device_app gpio_device_app.c

# 執行程式
root@raspberrypi:/home/pi# ./gpio_device_app
內容解密:
  1. 程式邏輯:該程式首先開啟對應的GPIO晶片裝置,然後請求對指定的GPIO線路(port19)進行輸出控制。接著,在一個迴圈中,切換該GPIO線路的電平狀態,使其在高低電平之間切換,從而控制連線的LED閃爍。
  2. 關鍵API:程式中使用了ioctl系統呼叫來取得GPIO線路控制控制程式碼以及設定GPIO線路的值。其中,GPIO_GET_LINEHANDLE_IOCTL用於取得控制控制程式碼,而GPIOHANDLE_SET_LINE_VALUES_IOCTL用於設定線路的電平狀態。
  3. 錯誤處理:程式中對每個可能失敗的操作都進行了錯誤檢查,一旦發生錯誤,便會輸出錯誤訊息並離開程式,確保了程式的穩定性。
  4. 技術考量:使用字元裝置介面進行GPIO控制提供了靈活且高效的方式來管理GPIO資源。相較於傳統的sysfs介面,這種方法更為現代化,並且能夠支援同時設定/讀取多個GPIO線路。
  5. 實務應用:這種技術在嵌入式系統開發、工業自動化等領域具有廣泛的應用前景,能夠滿足對硬體控制精確且高效的需求。

工業I/O子系統應用實務

使用GPIO控制LED閃爍的實作範例

在這一節中,我們將介紹如何使用Linux的GPIO介面控制LED的閃爍。首先,我們將使用「gpio char device」方法來實作這一功能。

使用「gpio char device」方法

我們首先建立一個名為gpio_device_app.c的應用程式檔案,其內容如以下程式碼所示:

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

/* 組態port19為輸出,並閃爍LED */
#define DEVICE_GPIO "/dev/gpiochip3"

int main(int argc, char *argv[]) {
    int fd;
    int ret;
    int flash = 10;
    struct gpiohandle_data data;
    struct gpiohandle_request req;

    /* 開啟GPIO裝置 */
    fd = open(DEVICE_GPIO, 3);
    if (fd < 0) {
        fprintf(stderr, "Failed to open %s\n", DEVICE_GPIO);
        return -1;
    }

    /* 請求GPIO線3作為輸出(紅色LED) */
    req.lineoffsets[0] = 3;
    req.lines = 1;
    req.flags = GPIOHANDLE_REQUEST_OUTPUT;
    strcpy(req.consumer_label, "led_gpio_port19");
    ret = ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &req);
    if (ret < 0) {
        printf("ERROR get line handle IOCTL (%d)\n", ret);
        if (close(fd) == -1)
            perror("Failed to close GPIO char device");
        return ret;
    }

    /* 初始LED狀態為關閉 */
    data.values[0] = 1;
    for (int i = 0; i < flash; i++) {
        /* 切換LED狀態 */
        data.values[0] = !data.values[0];
        ret = ioctl(req.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
        if (ret < 0) {
            fprintf(stderr, "Failed to issue %s (%d)\n", "GPIOHANDLE_SET_LINE_VALUES_IOCTL", ret);
            if (close(req.fd) == -1)
                perror("Failed to close GPIO line");
            if (close(fd) == -1)
                perror("Failed to close GPIO char device");
            return ret;
        }
        sleep(1);
    }

    /* 關閉GPIO線 */
    ret = close(req.fd);
    if (ret == -1)
        perror("Failed to close GPIO line");

    /* 關閉GPIO裝置 */
    ret = close(fd);
    if (ret == -1)
        perror("Failed to close GPIO char device");

    return ret;
}

程式碼解析:

  • 此程式碼首先開啟/dev/gpiochip3裝置,並請求GPIO線3作為輸出。
  • 然後,它透過ioctl呼叫設定GPIO線的值,使LED閃爍。
  • 程式中使用了一個迴圈來切換LED的狀態,並在每次切換後暫停一秒。

使用libgpiod函式庫控制GPIO

除了使用「gpio char device」方法外,我們還可以使用libgpiod函式庫來控制GPIO。下面是一個使用libgpiod函式庫的範例程式碼:

#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <gpiod.h>

int main(int argc, char *argv[]) {
    struct gpiod_chip *output_chip;
    struct gpiod_line *output_line;
    int line_value = 1;
    int flash = 10;
    int ret;

    /* 開啟/dev/gpiochip3 */
    output_chip = gpiod_chip_open_by_number(3);
    if (!output_chip)
        return -1;

    /* 取得gpiochip3裝置的線3(port19) */
    output_line = gpiod_chip_get_line(output_chip, 3);
    if (!output_line) {
        gpiod_chip_close(output_chip);
        return -1;
    }

    /* 組態port19(GPO)為輸出,並設定輸出為高電平 */
    if (gpiod_line_request_output(output_line, "Port19_GPO", GPIOD_LINE_ACTIVE_STATE_HIGH) == -1) {
        gpiod_line_release(output_line);
        gpiod_chip_close(output_chip);
        return -1;
    }

    /* 切換port19(GPO)的狀態10次 */
    for (int i = 0; i < flash; i++) {
        line_value = !line_value;
        ret = gpiod_line_set_value(output_line, line_value);
        if (ret == -1) {
            ret = -errno;
            gpiod_line_release(output_line);
            gpiod_chip_close(output_chip);
            return ret;
        }
        sleep(1);
    }

    gpiod_line_release(output_line);
    gpiod_chip_close(output_chip);
    return 0;
}

程式碼解析:

  • 此程式碼使用libgpiod函式庫開啟/dev/gpiochip3裝置,並取得線3(port19)。
  • 然後,它組態port19為輸出,並設定輸出為高電平。
  • 程式中使用了一個迴圈來切換port19的狀態,並在每次切換後暫停一秒。

Nunchuk提供者和消費者模組的開發

在這一節中,我們將開發兩個驅動程式:Nunchuk提供者驅動程式和輸入子系統消費者驅動程式。Nunchuk提供者驅動程式將讀取Nunchuk加速計感測器的3軸資料,而輸入子系統消費者驅動程式將讀取IIO通道值並將其報告給輸入子系統。

Nunchuk提供者模組

Nunchuk提供者驅動程式的主要程式碼部分如下所示:

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/delay.h>
#include <linux/iio/iio.h>

struct nunchuk_accel {
    struct i2c_client *client;
};

程式碼解析:

  • 此程式碼包含必要的標頭檔,並定義了一個名為nunchuk_accel的結構體,該結構體包含一個指向i2c_client的指標。

本章介紹瞭如何在Linux中使用GPIO控制LED閃爍,以及如何開發Nunchuk提供者和消費者模組。透過這些範例,讀者可以瞭解如何在Linux中控制GPIO和開發IIO驅動程式。

工業 I/O 子系統驅動程式開發

在 Linux 核心中,工業 I/O(IIO)子系統提供了一套用於處理各種感測器和 ADC/DAC 等裝置的框架。本文將以任天堂 Wii 遊戲機的 Nunchuk 加速度計為例,介紹如何開發一個 IIO 驅動程式。

驅動程式初始化

  1. 首先,需要在 nunchuk_accel_probe() 函式中宣告一個私有結構例項,並分配 iio_dev 結構:

    struct iio_dev *indio_dev;
    struct nunchuk_accel *nunchuk_accel;
    indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*nunchuk_accel));
    
  2. 初始化 iio_devicenunchuk_accel 私有結構。透過 iio_priv() 函式可以存取私有結構:

    nunchuk_accel = iio_priv(indio_dev);
    nunchuk_accel->client = client;
    indio_dev->name = "Nunchuk Accel";
    indio_dev->dev.parent = &client->dev;
    indio_dev->info = &nunchuk_info;
    indio_dev->channels = nunchuk_channels;
    indio_dev->num_channels = 3;
    indio_dev->modes = INDIO_DIRECT_MODE;
    

內容解密:

  • devm_iio_device_alloc() 用於分配 iio_dev 結構,並將其與客戶端裝置關聯。
  • iio_priv() 用於取得與 iio_dev 相關聯的私有結構指標。
  • indio_dev->info 指向 iio_info 結構,該結構包含了一系列用於與使用者空間互動的回撥函式。
  • indio_dev->channels 指向 iio_chan_spec 結構陣列,定義了裝置的通道屬性。
  • INDIO_DIRECT_MODE 表示裝置的資料不會被快取,可以直接從 sysfs 讀取。

註冊裝置

註冊裝置到 IIO 核心,使其對使用者空間應用程式可見:

devm_iio_device_register(&client->dev, indio_dev);

內容解密:

  • devm_iio_device_register() 用於向 IIO 子系統註冊裝置,使其可以被使用者空間存取。

定義通道屬性

定義 iio_chan_spec 結構以暴露通道屬性到使用者空間:

#define NUNCHUK_IIO_CHAN(axis) { \
    .type = IIO_ACCEL, \
    .modified = 1, \
    .channel2 = IIO_MOD_##axis, \
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \
}

static const struct iio_chan_spec nunchuk_channels[] = {
    NUNCHUK_IIO_CHAN(X),
    NUNCHUK_IIO_CHAN(Y),
    NUNCHUK_IIO_CHAN(Z),
};

內容解密:

  • NUNCHUK_IIO_CHAN 巨集定義了一個 iio_chan_spec 結構,用於描述加速度計的 X、Y、Z 軸。
  • .type = IIO_ACCEL 表示該通道為加速度計型別。
  • .modified = 1.channel2 = IIO_MOD_##axis 用於指定通道的修飾符,例如 X、Y、Z 軸。
  • .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) 表示該通道的原始資料可以被讀取。

實作讀取原始資料的回撥函式

實作 nunchuk_accel_read_raw() 函式以傳回指定通道的原始資料:

static int nunchuk_accel_read_raw(struct iio_dev *indio_dev,
                                  struct iio_chan_spec const *chan,
                                  int *val, int *val2, long mask)
{
    char buf[6];
    struct nunchuk_accel *nunchuk_accel = iio_priv(indio_dev);
    struct i2c_client *client = nunchuk_accel->client;

    nunchuk_read_registers(client, buf, ARRAY_SIZE(buf));

    switch (chan->channel2) {
    case IIO_MOD_X:
        *val = (buf[2] << 2) | ((buf[5] >> 2) & 0x3);
        break;
    case IIO_MOD_Y:
        *val = (buf[3] << 2) | ((buf[5] >> 4) & 0x3);
        break;
    case IIO_MOD_Z:
        *val = (buf[4] << 2) | ((buf[5] >> 6) & 0x3);
        break;
    default:
        return -EINVAL;
    }
    return IIO_VAL_INT;
}

內容解密:

  • 該函式讀取 Nunchuk 裝置的暫存器資料,並根據指定的通道傳回對應的加速度計資料。
  • 資料處理涉及位元操作,以正確解析從裝置讀取的原始資料。

裝置與驅動程式匹配

定義裝置與驅動程式的匹配表:

static const struct of_device_id nunchuk_accel_of_match[] = {
    { .compatible = "nunchuk_accel"},
    {}
};

static const struct i2c_device_id nunchuk_accel_id[] = {
    { "nunchuk_accel", 0 },
    {}
};

內容解密:

  • of_device_id 用於裝置樹匹配,確保驅動程式能正確匹配到對應的裝置。
  • i2c_device_id 用於傳統的 I2C 裝置匹配。

註冊 I2C 驅動程式

定義並註冊 I2C 驅動程式結構:

static struct i2c_driver nunchuk_accel_driver = {
    .driver = {
        .name = "nunchuk_accel",
        .owner = THIS_MODULE,
        .of_match_table = nunchuk_accel_of_match,
    },
    .probe = nunchuk_accel_probe,
    .remove = nunchuk_accel_remove,
    .id_table = nunchuk_accel_id,
};

module_i2c_driver(nunchuk_accel_driver);

內容解密:

  • .probe.remove 分別對應裝置的插入和移除操作。
  • .id_table 用於匹配支援的裝置。

本範例展示瞭如何開發一個基本的 IIO 驅動程式,以支援 Nunchuk 加速度計裝置。透過遵循上述步驟,可以實作一個功能完整的 IIO 驅動程式,使其能夠與使用者空間應用程式互動。