在嵌入式系統和工業自動化領域,透過 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
硬體連線:將PIXI™ CLICK板的port19(GPO)連線到Color click eval board上的紅色LED,並將port18或port7組態為GPI。
軟體操作:
- 使用
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
內容解密:
- 程式邏輯:該程式首先開啟對應的GPIO晶片裝置,然後請求對指定的GPIO線路(port19)進行輸出控制。接著,在一個迴圈中,切換該GPIO線路的電平狀態,使其在高低電平之間切換,從而控制連線的LED閃爍。
- 關鍵API:程式中使用了
ioctl系統呼叫來取得GPIO線路控制控制程式碼以及設定GPIO線路的值。其中,GPIO_GET_LINEHANDLE_IOCTL用於取得控制控制程式碼,而GPIOHANDLE_SET_LINE_VALUES_IOCTL用於設定線路的電平狀態。 - 錯誤處理:程式中對每個可能失敗的操作都進行了錯誤檢查,一旦發生錯誤,便會輸出錯誤訊息並離開程式,確保了程式的穩定性。
- 技術考量:使用字元裝置介面進行GPIO控制提供了靈活且高效的方式來管理GPIO資源。相較於傳統的sysfs介面,這種方法更為現代化,並且能夠支援同時設定/讀取多個GPIO線路。
- 實務應用:這種技術在嵌入式系統開發、工業自動化等領域具有廣泛的應用前景,能夠滿足對硬體控制精確且高效的需求。
工業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 驅動程式。
驅動程式初始化
首先,需要在
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));初始化
iio_device和nunchuk_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 驅動程式,使其能夠與使用者空間應用程式互動。