Qtの基礎 - GPIO
概要
GPIO (General Purpose Input/Output (汎用入出力)) は、マイクロコントローラやシングルボードコンピュータ等のデバイスに搭載されている汎用のピンのことである。
これらのピンは、ソフトウェアにより入力または出力として設定して、制御することができる。
GPIOの主な概念は以下に示す。
- 入力
- GPIOピンを入力として設定する場合、外部のデバイスやスイッチの状態を読み取ることができる。
- 例えば、ボタンが押下されているかどうかを検出するために使用できる。
- 出力
- GPIOピンを出力として設定する場合、ピンの状態をHIGH (通常は3.3[V]または5[V])、または、LOW (0[V]) に設定できる。
- これにより、LEDの点灯 / 消灯、他のデバイスの制御が可能になる。
- プルアップ / プルダウン抵抗
- GPIOピンは、プルアップ抵抗またはプルダウン抵抗を内部に持つことができる。
- これらの抵抗は、ピンが明確な状態 (HIGHまたはLOW) を持つようにするために使用される。
- 割り込み
- 一部のGPIOピンは、割り込み機能をサポートしている。
- これにより、ピンの状態が変化した時に、CPUに割り込み信号を送ることができる。
- これは、リアルタイムの応答が必要な場合に使用する。
- PWM
- 一部のGPIOピンは、PWM機能をサポートしている。
- PWMはデューティ比を変化させることにより、LEDの明るさ、モータの速度を制御するために使用される。
GPIOは、ボタン、スイッチ、LED、センサ、ディスプレイ、モータ等の様々な電子部品やデバイスとのインターフェースに使用されている。
GPIOを使用することにより、これらのデバイスとソフトウェアを連携させて、独自のプロジェクトや組み込みシステムを開発することができる。
ただし、GPIOを使用する際は、電気的な特性や制限に注意が必要となる。
不適切な使用は、デバイスの損傷や誤動作につながる可能性があることに注意する。
オンライン上にあるGPIOに関するドキュメントの多くは、ArduinoおよびRaspberry PiのようなSoCを対象にしている。
PCにおいては、SoCとは異なり、GPIOのあるPCはGPIOピンをCPUに直接統合していないが、スーパーI/Oチップ (SIO) を使用している。
Qtを使用して、PCのGPIOピンを直接制御することは一般的ではないが、OSやハードウェアに依存した方法を使用することで可能である。
GPIOへのアクセス
多くのLinuxディストリビューションにおいて、GPIOを扱うことができる。
最近のLinuxでは、/dev/gpiochip[n]キャラクタデバイスをマウントするカーネルモジュールを通して、GPIOを公開している。
GPIOと対話するための推奨ユーティリティは、gpiodである。
これは、GPIOの値の列挙、読み取り、書き込みを行うための一連のCLIを提供するものである。
ただし、/dev/gpiochip[n]キャラクタデバイスが無い場合は、Linuxカーネルがハードウェアのドライバが読み込まれていない。
BIOS / UEFIの設定
BIOS / UEFIにおいて、GPIOへのアクセスを提供する必要がある。
- まず、BIOS / UEFIの画面から、[Advanced]タブにある[SIO MISC Configuration]メニューから、[WDT, CASE OPEN, GPIO, DEBUG...]を選択する。
- [WDT, CASE OPEN, GPIO, DEBUG...]画面では、ウォッチドッグタイマ、2つのCOMポートモード (RS232、RS485、RS422)、複数のGPIO設定へのアクセス可否の設定がある。
- GPIOの設定
- 各ピンはGPIO[n]という名前である。 (nは0以上の数値)
- モードは入力または出力で設定できる。
- [Output]に設定すると、出力値を
Low
またはHigh
に設定することができる。 - 使用する予定の全てのGPIOピンの機能をマッピングすること。
- GPIOの設定
例えば、以下に示すように、GPIOピンはPCケースに表示されているように、左から右に番号が付けられている。
以下の例では、上段は3.3[V]出力、GPIOピン 4本、GNDピン 2本、下段は3.3[V]出力、GPIOピン 4本、12[V]および5[V]出力がある。
1 2 3 4 5 6 7 3V3 7 6 5 4 GND GND --------------------------- 3V3 3 2 1 0 12V 5V 1 2 3 4 5 6 7
スーパーI/Oチップを使用する場合 (スーパーI/Oチップ付属のPCの場合)
スーパーI/Oチップ名の確認
BIOS / UEFIには、どのスーパーI/Oチップが使用されているか表示されていない場合もある。
スーパーI/Oチップの名前を確認する場合は、PCケースを開けて、GPIOピンの付近にあるチップの表面に描かれているシルク等で確認する。
スーパーI/Oチップのカーネルモジュールの有効化
まず、付属しているスーパーI/Oチップのカーネルモジュールを知りたい場合は、Linux KernelのGithubにあるgit repoから、スーパーI/Oチップ名で検索する。
以下の例では、スーパーI/Oチップ名が"iTE IT8786E-I"の場合である。
repo:torvalds/linux iTE IT8786E-I
/* linux/drivers/gpio/gpio-it78.cファイル */
/* gpio-it78.c */
/*
* GPIO interface for IT87xx Super I/O chips
*
* Author: Diego Elio Pettenò <flameeyes@flameeyes.eu>
* Copyright (c) 2017 Google, Inc.
*
* Based on it87_wdt.c by Oliver Schuster
* gpio-it8761e.c by Denis Turischev
* gpio-stmpe.c by Rabin Vincent
*/
# linux/drivers/gpio/Makefileファイル
# ※ "CONFIG_GPIO_IT87"という部分の文字列を控えておくこと
obj-$(CONFIG_GPIO_IT87) += gpio-it87.o
# linux/drivers/gpio/Kconfigファイル Kconfig snippet: config GPIO_IT87 tristate "IT87xx GPIO support" help Say yes here to support GPIO functionality of IT87xx Super I/O chips. This driver is tested with ITE IT8728 and IT8732 Super I/O chips, and supports the IT8761E, IT8613, IT8620E, and IT8628E Super I/O chips as well. To compile this driver as a module, choose M here: the module will be called gpio_it87.
スーパーI/Oチップ名を確認して、該当するカーネルモジュールがインストールされているかどうかを確認する。
find /lib/modules/$(uname -r)/kernel/drivers/gpio -iname "*gpio*" | grep -i gpio-it87
スーパーI/Oチップ名を確認して、該当するカーネルモジュールが読み込まれているかどうかを確認する。
grep CONFIG_GPIO_IT87 /boot/config-$(uname -r) # 出力例: # mと出力される場合、そのカーネルモジュールがデフォルトでは無効であることを意味する CONFIG_GPIO_IT87=m
スーパーI/Oチップのカーネルモジュールを有効にする。
sudo modprobe gpio-it87
GPIOピンの入出力に関する設定はBIOS / UEFIを通してのみ変更可能であり、GPIOの値を設定 / 取得する場合はスーパーユーザ権限が必要である。
もし、gpiolibのピン番号がBIOS / UEFIのピン番号と一致しない場合は、各ピンをブレッドボード上のLEDに接続して、以下のようなシェルスクリプトを実行する。
以下の例では、チップセット0の全ピンを1秒間ごとにトグルしている。
ただし、実行前に、BIOS / UEFIにおいて、全てのGPIOピンを出力モードに割り当てること。
for i in $(seq 0 63)
do
echo -n "$i on "
sudo gpioset 0 "$i=1"
sleep 1
echo off
sudo gpioset 0 "$i=0"
done
sysfsの使用 (スーパーI/Oチップが無いPCおよびSBCの場合)
sysfsを使用して、GPIOを制御することができる。
GPIOの操作インタフェースの公開
まず、/sys/class/gpioディレクトリにアクセスして、カーネル空間からユーザ空間にGPIOの操作インタフェースを公開する。
exportファイルにGPIO番号を書き込む。
- direction
- GPIOの入力 / 出力を指定する。
- value
- GPIOの出力の制御、あるいは、GPIOの入力を取得する。
ファイルIOでGPIOを操作する。
これは、open、close、read、writeの4つの関数が存在する。
まず、システム内に/sys/class/gpioディレクトリが存在するかどうかを確認する。
/sys/class/gpioディレクトリにおいて、ユーザ空間の/sys/class/gpioディレクトリにgpiochipファイルが存在するかどうかを確認する。
ls /sys/class/gpio # 出力例 : export gpiochip0 unexport
gpio_operationは、/sys/ファイルインターフェースを通して、IOポートGPIOとファイルシステムのマッピングを操作する。
GPIOを制御するためのディレクトリは、/sys/class/gpioディレクトリに存在する。
/sys/class/gpio/exportファイルは、制御されているGPIOピン番号をエクスポートする必要性をシステムに通知するために使用される。
/sys/class/gpio/unexportファイルは、システムにエクスポートのキャンセルを通知するために使用される。
/sys/class/gpio/gpiochipXディレクトリは、システムのGPIOレジスタの情報を保存する。
各レジスタの制御ピンの開始番号ベース、レジスタ名、総ピン数を含む。
gpioカーネルモジュールのインストール
もし、gpioカーネルモジュールが無い場合は、インストールする必要がある。
Linuxカーネルのソースコードをダウンロードする。
- パッケージ管理システムから、Linuxカーネルのソースコードをインストールする場合
sudo zypper install kernel-source
- Linuxカーネルの公式webサイトから、Linuxカーネルのソースコードをダウンロードする場合
- Linuxカーネルの公式webサイトまたはGithubにアクセスして、対応するバージョンのLinuxカーネルのソースコードをダウンロードする。
- あるいは、Linuxカーネルの公式webサイトにあるGitから、対応するバージョンのLinuxカーネルをダウンロードする。
- ダウンロードしたファイルを解凍する。
tar xf linux-<バージョン>.tar.gz
cd linux-<バージョン>
既存のカーネルコンフィグ(/bootディレクトリ内にあるカーネルコンフィグ)を元に、新しいカーネルコンフィグを生成する。
make olderconfig
コマンドは、可能な限り既存のカーネルコンフィグの設定を使用して、ビルドディレクトリ内に必要なファイルを生成する。
make -j $(nproc) olddefconfig O=. # または sudo make -j $(nproc) olddefconfig O=.
gpioカーネルモジュールのビルド設定を行うため、make menuconfig
コマンドを実行する。
make menuconfig -j $(nproc) # または sudo make menuconfig -j $(nproc)
TUI画面が起動するので、[Device Drivers] - [GPIO Support] - [/sys/class/gpio/... (sysfs interface)]に移動して、[M]キーを押下する。
TUI画面下にある[Save]を選択して、上記の設定を保存する。
Linuxカーネルから、gpioカーネルモジュールのみをビルドする。
- INSTALL_MOD_DIRオプション
- 一般的に、カーネルモジュールは、/lib/modules/$(uname -r)/extraディレクトリにインストールされる。
- 任意のディレクトリに配置したい場合、
INSTALL_MOD_DIR
オプションを付加して、extraディレクトリに代わるパスを指定する。 - 例えば、
INSTALL_MOD_DIR=drivers/gpio
と指定する場合、インストールディレクトリは/lib/modules/$(uname -r)/kernel/drivers/gpio
ディレクトリとなる。
sudo make scripts prepare modules_prepare -j $(nproc) sudo make -C . M=drivers/gpio -j $(nproc) sudo mkdir -p /lib/modules/$(uname -r)/kernel/drivers/gpio sudo cp drivers/gpio/gpio-it87.ko /lib/modules/$(uname -r)/kernel/drivers/gpio # または sudo make prepare -j $(nproc) sudo make modules_prepare -j $(nproc) sudo make M=drivers/gpio -j $(nproc) sudo make modules M=drivers/gpio -j $(nproc) sudo make modules_install M=drivers/gpio INSTALL_MOD_DIR=kernel/drivers/gpio -j $(nproc)
Linuxカーネルモジュールのインストール後は、Linuxカーネルのビルドファイルを削除する。(ファイル容量が膨大なため)
sudo make clean -j $(nproc) # .configファイルを残す または sudo make distclean -j $(nproc) # .configファイル等の設定ファイルを全て削除する
Linuxカーネルモジュールの読み込み (セキュアブートが無効の場合)
Linuxカーネルモジュール全体を読み込む。
sudo depmod -a $(uname -r)
PCを再起動して、gpioカーネルモジュールが正常に動作するかどうかを確認する。
また、gpioカーネルモジュールが正常に読み込まれているかどうかを確認する。
lsmod | grep gpio-it87 # または sudo modinfo gpio-it87
必要であれば、F2FSモジュールをXZ形式またはZstandard形式に圧縮する。(任意)
# XZ形式の場合 cd /lib/modules/$(uname -r)/kernel/drivers/gpio sudo xz -f gpio-it87.ko # Zstandard形式の場合 cd /lib/modules/$(uname -r)/kernel/drivers/gpio sudo zstd --rm gpio-it87.ko
エラー
GPIOピンの入力を開放する場合
GPIOピンを入力設定 (pull-down
オプションを付加) にして開放する場合、該当ピンの状態を確認するとHigh
を返す場合がある。
sudo gpioget -B pull-down 0 0 # 出力例: 1
これを解決するには、外付けのプルダウン抵抗を追加することである。
1[K]、10[K]、100[K]のプルダウン抵抗を試して、カット&トライすること。
サンプルコード
以下の例では、Linuxのsysfsインターフェースを使用してGPIO 17を制御している。
- まず、exportGpio関数でGPIOをエクスポートして、使用可能にする。
- 次に、setGpioDirection関数でGPIOの方向を出力 (out) に設定する。
- setGpioValue関数を使用して、GPIOの値を1 (HIGH) 、および、0 (LOW) に交互に設定して、1秒ずつ待機することを5秒繰り返す。
- unexportGpio関数でGPIOをアンエクスポートして、GPIOピンの使用を終了する。
ただし、これを実行する場合は、適切な権限 (通常はスーパーユーザ権限) が必要である。
また、使用するGPIOピン番号やパスは、ハードウェアの構成に応じて適宜変更すること。
PCのGPIOピンを制御する場合は、ハードウェアの仕様を確認して、適切な保護回路を使用する等、十分な注意が必要である。
#include <QCoreApplication>
#include <QFile>
#include <QTextStream>
#include <QThread>
const QString GPIO_PATH = "/sys/class/gpio/";
void exportGpio(int gpio)
{
QFile exportFile(GPIO_PATH + "export");
if (exportFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&exportFile);
out << gpio;
exportFile.close();
}
}
void unexportGpio(int gpio)
{
QFile unexportFile(GPIO_PATH + "unexport");
if (unexportFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&unexportFile);
out << gpio;
unexportFile.close();
}
}
void setGpioDirection(int gpio, const QString &direction)
{
QFile directionFile(GPIO_PATH + "gpio" + QString::number(gpio) + "/direction");
if (directionFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&directionFile);
out << direction;
directionFile.close();
}
}
void setGpioValue(int gpio, int value)
{
QFile valueFile(GPIO_PATH + "gpio" + QString::number(gpio) + "/value");
if (valueFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&valueFile);
out << value;
valueFile.close();
}
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
int gpioNumber = 17; // 使用するGPIO番号
exportGpio(gpioNumber);
setGpioDirection(gpioNumber, "out");
for (int i = 0; i < 5; ++i) {
setGpioValue(gpioNumber, 1);
QThread::sleep(1);
setGpioValue(gpioNumber, 0);
QThread::sleep(1);
}
unexportGpio(gpioNumber);
return a.exec();
}