ラズパイ5のGPIOをC/C++で制御する
Raspberry Pi 5(ラズパイ5)でGPIOの入出力をC/C++で制御しようかと思いました。
GPIOによる入出力制御は基本中の基本なので、ネットにはその手の書き込みも沢山見かけるし、ライブラリもそろっていて楽勝だと勝手に思っていたのですが、過去に使ったことがあるWiringPiもMraaもラズパイ5はサポートしていないようです。
現状、定番のライブラリというのがあまりないようで、いくつかの方法を調べて試してみました。
実験用の回路
GPIOの入出力を試すために、GPIO5(29番ピン)とGPIO6(31番ピン)をスイッチ入力、GPIO13(33番ピン)をLED出力とした下記の回路を組みました。スイッチ入力は10kΩでプルアップしています。
ちなみに、内部のプル抵抗をプルアップに設定してあればこの10kΩの抵抗は必要ありません。逆に言うと、外部でプルアップしているので、内部のプル抵抗(50kΩくらいらしい)がどういう状態であっても、正しく動作します。
GPIOの制御を確認するためのソフトとしては、GPIO5のスイッチを押すと、GPIO13のLEDの点滅が始まり、GPIO6のスイッチを押すと点滅が終わり、プログラムも終了するというものとし、①~④の4つの方法のプログラムを作成してみました。
GPIOの状態確認・設定:pinctrl
C/C++でGPIOの制御を行う前に、コマンドラインの命令について説明しておきます。GPIOの状態を調べたり、制御するための命令としてpinctrlが準備されています。
例えば、GPIOの状態を知りたい場合は、-pオプションを付けて実行すると各ピンの状態が表示されます。
$ pinctrl -p
1: 3v3
2: 5v
3: a3 pu | hi // GPIO2 = SDA1
4: 5v
5: a3 pu | hi // GPIO3 = SCL1
6: gnd
7: no pu | -- // GPIO4 = none
8: no pd | -- // GPIO14 = none
9: gnd
10: no pd | -- // GPIO15 = none
11: no pd | -- // GPIO17 = none
12: a2 pn | lo // GPIO18 = I2S0_SCLK
13: no pd | -- // GPIO27 = none
14: gnd
15: no pd | -- // GPIO22 = none
16: no pd | -- // GPIO23 = none
17: 3v3
18: no pd | -- // GPIO24 = none
19: a0 pn | lo // GPIO10 = SPI0_MOSI
20: gnd
21: a0 pn | lo // GPIO9 = SPI0_MISO
22: no pd | -- // GPIO25 = none
23: a0 pn | lo // GPIO11 = SPI0_SCLK
24: op dh pu | hi // GPIO8 = output
25: gnd
26: op dh pu | hi // GPIO7 = output
27: ip pu | hi // GPIO0 = input
28: ip pu | hi // GPIO1 = input
29: no pu | -- // GPIO5 = none
30: gnd
31: no pu | -- // GPIO6 = none
32: no pd | -- // GPIO12 = none
33: no pd | -- // GPIO13 = none
34: gnd
35: a2 pn | lo // GPIO19 = I2S0_WS
36: no pd | -- // GPIO16 = none
37: no pd | -- // GPIO26 = none
38: a2 pn | lo // GPIO20 = I2S0_SDI0
39: gnd
40: a2 pn | lo // GPIO21 = I2S0_SDO0
特定のピンだけを指定すれば、そのピンの状態だけが表示されます。
$ pinctrl -p GPIO0,GPIO1,GPIO7
26: op dh pu | hi // GPIO7 = output
27: ip pu | hi // GPIO0 = input
28: ip pu | hi // GPIO1 = input
opは出力, ipは入力、puはプルアップ、pdはプルダウン、pnはプル抵抗なし、dhはhi出力、dlはlow出力の状態を表しています。
※プルアップ抵抗とプルダウン抵抗の総称をなんと言うのか分からなかったのでここではプル抵抗と記述しています。
setコマンドを使うことで、指定した番号のピンの設定ができます。例えば、実験用の回路に合わせて、GPIO5と6を入力、GPIO13を出力とし、入力はプルアップ抵抗を設定、出力はプル抵抗なしにする場合には次のような感じです。
$ pinctrl set 5 ip pu
$ pinctrl set 6 ip pu
$ pinctrl set 13 op pn
$ pinctrl -p GPIO5,GPIO6,GPIO13
29: ip pu | hi // GPIO5 = input
31: ip pu | hi // GPIO6 = input
33: op dl pn | lo // GPIO13 = output
GPIO13にhiを出力する場合は次のようにすれば出力されます。
$ pinctrl set 13 dh
$ pinctrl -p GPIO5,GPIO6,GPIO13
29: ip pu | hi // GPIO5 = input
31: ip pu | hi // GPIO6 = input
33: op dh pn | hi // GPIO13 = output
GPIO sysfsインタフェースによる制御(コマンドライン)
GPIOを仮想的にファイルシステムへの読み書きで制御することができるように/sys/class/gpioが準備されています。
各GPIOピンが何番に割り当てられているかは/sys/kernel/debug/gpioに一覧があります。
$ cat /sys/kernel/debug/gpio
gpiochip0: GPIOs 512-543, parent: platform/107d508500.gpio, gpio-brcmstb@107d508500:
gpio-512 (- )
gpio-513 (2712_BOOT_CS_N |spi10 CS0 ) out hi ACTIVE LOW
gpio-514 (2712_BOOT_MISO )
gpio-515 (2712_BOOT_MOSI )
gpio-516 (2712_BOOT_SCLK )
以下省略
例えば、GPIO5, 6, 13が何番かを知りたければ、下記のようにgrepで検索すれば分かります。
$ cat /sys/kernel/debug/gpio | grep -E "GPIO[56]|GPIO13"
gpio-576 (GPIO5 )
gpio-577 (GPIO6 )
gpio-584 (GPIO13 )
例えばGPIO13に1を出力する場合は、584番に割り当てられているので、次のようにすれば良いです。
$ echo 584 > /sys/class/gpio/export
$ echo out > /sys/class/gpio/gpio584/direction
$ echo 1 > /sys/class/gpio/gpio584/value
$ echo 584 > /sys/class/gpio/unexport
GPIO5の状態を知りたければ次のような感じです。
$ echo 576 > /sys/class/gpio/export
$ echo in > /sys/class/gpio/gpio576/direction
$ cat /sys/class/gpio/gpio576/value
$ echo 576 > /sys/class/gpio/unexport
① GPIO sysfsインタフェースによる制御(c/c++)
sysyfsは通常のファイルと同じようにアクセスできるので、sysfsへのコマンドラインでの操作と同じことをC/C++で行えば制御できます。
試しにsysfs経由でGPIOを制御するCGpioクラスを作って制御してみました。
sysfs経由の制御ではプル抵抗の設定はできないようなので、プル抵抗の設定はpinctrlをsystem()関数で呼び出して設定しています。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
class CGpio {
int port = -1;
public:
enum GPIO_DIRECTION {
IN_DIRECTION = 0,
OUT_DIRECTION
};
CGpio(int pnum, GPIO_DIRECTION dir);
~CGpio();
int put(int n);
int get(void);
private:
int write(char *path, char *val);
int read(char *path);
};
CGpio::CGpio(int num, GPIO_DIRECTION dir)
{
port = num;
char port_str[10];
snprintf(port_str, sizeof(port_str), "%d", port);
write((char *)"/sys/class/gpio/export", port_str);
usleep(100000);
char path[100];
snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/direction", port);
if(dir == IN_DIRECTION){
write(path, (char *)"in");
}
else{
write(path, (char *)"out");
}
}
CGpio::~CGpio()
{
char port_str[10];
snprintf(port_str, sizeof(port_str), "%d", port);
write((char *)"/sys/class/gpio/unexport", port_str);
}
int CGpio::put(int n)
{
char n_str[10];
snprintf(n_str, sizeof(n_str), "%d", n);
char path[100];
snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", port);
return write(path, n_str);
}
int CGpio::get(void)
{
char path[100];
snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", port);
return read(path);
}
int CGpio::write(char *path, char *val)
{
int fd = open(path, O_WRONLY);
if(fd < 0){
printf("Can't open %s.\n", path);
return -1;
}
int ret = ::write(fd, val, strlen(val));
close(fd);
return ret;
}
int CGpio::read(char *path)
{
int fd = open(path, O_RDONLY);
if(fd < 0){
printf("Can't open %s.\n", path);
return -1;
}
char val[10];
int ret = ::read(fd, val, sizeof(val));
close(fd);
if(ret < 0){
return ret;
}
return atoi(val);
}
int main(int argc, char *argv[])
{
CGpio in5(576, CGpio::IN_DIRECTION);
CGpio in6(577, CGpio::IN_DIRECTION);
CGpio out13(584, CGpio::OUT_DIRECTION);
system("pinctrl set 5 pu");
system("pinctrl set 6 pu");
system("pinctrl set 13 pn");
printf("push GPIO5 to start.\n");
while(in5.get() != 0){
usleep(100000);
}
printf("push GPIO6 to finish.\n");
int freq = 500000;
int cnt = 0;
while(1){
out13.put(cnt%2);
usleep(freq);
if(in6.get() == 0){
break;
}
cnt++;
}
out13.put(0);
return 0;
}
上記をファイル名cgpio_test.cppで保存した場合、下記のようにすればコンパイルできると思います。
$ g++ -o cgpio_test cgpio_test.cpp
動作としては、GPIO5のスイッチを押すと、LEDの点滅が始まり、GPIO6のスイッチを押すと点滅が終わり、プログラムも終了します。
② GPIOキャラクタデバイス経由の制御(libgpiod v1)
ラズパイ5のGPIOの制御に対応したライブラリとしてlibgpiodがあります。
現状のラズパイOS(Debian bookworm)では下記のようにパッケージでインストールできます。
$ sudo apt install libgpiod2 libgpiod-dev
libgpiodの最新版はバージョン2ですが、パッケージでインストールされるのはバージョン1です。
パッケージ名がlibgpiod2だったり、インストールされる共有ライブラリのファイル名がlibgpiod.so.2.2.2だったりするので、libgpiodのバージョン2がインストールされるように見えますが、インストールされるのは1.6.3のバージョン1です。
ライブラリの使い方は下記のような感じとなります。
libgpiodでは/dev/gpiochip4というデバイスファイル名のキャラクタデバイスを経由してGPIOの制御を行っています。
バージョン1のlibgpiodではプル抵抗の設定ができないため、pinctrlをsystem()関数で呼び出して設定しています。
#include <stdio.h>
#include <unistd.h>
#include <gpiod.h>
#define PROGNAME "gpiod_test_v1"
int main(int argc, char *argv[])
{
gpiod_chip *chip = gpiod_chip_open_by_name("gpiochip4");
gpiod_line *line5 = gpiod_chip_get_line(chip, 5); // GPIO5
gpiod_line *line6 = gpiod_chip_get_line(chip, 6); // GPIO6
gpiod_line *line13 = gpiod_chip_get_line(chip, 13); // GPIO13
gpiod_line_request_input(line5, PROGNAME);
gpiod_line_request_input(line6, PROGNAME);
gpiod_line_request_output(line13, PROGNAME, 0);
system("pinctrl set 5 pu");
system("pinctrl set 6 pu");
system("pinctrl set 13 pn");
printf("push GPIO5 to start.\n");
while(gpiod_line_get_value(line5) == 1){
usleep(10000);
}
printf("push GPIO6 to finish.\n");
int freq = 500000;
int cnt = 0;
while(1){
gpiod_line_set_value(line13, cnt%2);
usleep(freq);
if(gpiod_line_get_value(line6) == 0){
break;
}
cnt++;
}
gpiod_line_set_value(line13, 0);
gpiod_line_release(line5);
gpiod_line_release(line6);
gpiod_line_release(line13);
gpiod_chip_close(chip);
return 0;
}
上記をgpiod_test_v1.cppというファイル名で保存した場合、下記のようにすればコンパイルできると思います。
$ g++ -o gpiod_test_v1 gpiod_test_v1.cpp `pkg-config --cflags --libs libgpiod`
③ GPIOキャラクタデバイス経由の制御(libgpiod v2)
libgpiodのバージョン2はGitHubからソースコードを入手して手動でインストールする必要があります。
$ git clone https://github.com/brgl/libgpiod.git
$ cd libgpiod
$ ./autogen.sh
$ ./configure
$ make
$ sudo make install
インストールされる共有ライブラリのファイル名はibgpiod.so.3.1.0となります。
共有ライブラリ名がバージョン1ではlibgpiod.so.2、バージョン2ではlibgliod.so.3ととても紛らわしいです。
さらに、バージョン1とバージョン2では互換性がありません。なのに、インクルードファイル名、ライブラリ名が同じなので、共存できないです。
なので、上記のバージョン2のインストールを行うと、バージョン1用で作ったソースコードのコンパイルや実行ができなくなってしまいます。この辺は今後問題になるのではないかと思われます。
私はバージョン1と2を共存させるために、バージョン2でインストールされた/usr/local/include/gpiod.hのファイル名をgpiod_v2.hとリネイム。
同様に、/usr/local/libにインストールされたlibgpiod.xxをlibgpiod_v2.xxにリネイム。
さらに、/usr/local/lib/pkgconfig/libgpiod.pcをlibgpiod_v2.pcにリネイムし、
libgpiod_v2.pcの中の"-lgpiod”を"-lgpiod_v2"に書き換えています。
これで、コンパイル時に、バージョン1を使う場合は、`pkg-config --cflags --libs libgpiod`、バージョン2を使う場合は、`pkg-config --cflags --libs libgpiod_v2`とすることで一応使い分けられます。
libgpiodのバージョン2の使い方は下記のような感じとなります。
定型的ではありますが、使い方は結構複雑です。制御のためにgpiod_line_requestのオブジェクト(インスタンス)が必要となりますが、そのために、gpiod_chip , gpiod_line_settings, gpiod_line_config, gpiod_request_configの各オブジェクトを順に生成して設定する必要があります。この辺の処理はまとめてget_request_line()関数の中でまとめて行うようにしています。
バージョン2ではプル抵抗の設定もできるようになっているので、pinctrlを呼び出して設定するようなことはしないですみます。
#include <stdio.h>
#include <unistd.h>
#include <gpiod_v2.h>
#define PROGNAME "gpiod_test_v2"
gpiod_line_request *get_request_line(const char *chip_path, const unsigned int *offsets, gpiod_line_direction direction, const char *consumer, gpiod_line_bias bias = GPIOD_LINE_BIAS_DISABLED)
{
gpiod_chip *chip = gpiod_chip_open(chip_path);
gpiod_line_settings *settings = gpiod_line_settings_new();
gpiod_line_settings_set_direction(settings, direction);
gpiod_line_settings_set_bias(settings, bias);
gpiod_line_config *line_cfg = gpiod_line_config_new();
gpiod_line_config_add_line_settings(line_cfg, offsets, 1, settings);
gpiod_request_config *req_cfg = gpiod_request_config_new();
gpiod_request_config_set_consumer(req_cfg, PROGNAME);
gpiod_line_request *request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
gpiod_request_config_free(req_cfg);
gpiod_line_config_free(line_cfg);
gpiod_line_settings_free(settings);
gpiod_chip_close(chip);
return request;
}
int main(int argc, char *argv[])
{
unsigned int line_offset5 = 5;
unsigned int line_offset6 = 6;
unsigned int line_offset13 = 13;
gpiod_line_request *request5 = get_request_line("/dev/gpiochip4", &line_offset5, GPIOD_LINE_DIRECTION_INPUT, PROGNAME, GPIOD_LINE_BIAS_PULL_UP);
gpiod_line_request *request6 = get_request_line("/dev/gpiochip4", &line_offset6, GPIOD_LINE_DIRECTION_INPUT, PROGNAME, GPIOD_LINE_BIAS_PULL_UP);
gpiod_line_request *request13 = get_request_line("/dev/gpiochip4", &line_offset13, GPIOD_LINE_DIRECTION_OUTPUT, PROGNAME);
printf("push GPIO5 to start.\n");
while(gpiod_line_request_get_value(request5, line_offset5) == 1){
usleep(10000);
}
printf("push GPIO6 to finish.\n");
int freq = 500000;
int cnt = 0;
while(1){
gpiod_line_request_set_value(request13, line_offset13, (gpiod_line_value)(cnt%2));
usleep(freq);
if(gpiod_line_request_get_value(request6, line_offset6) == 0){
break;
}
cnt++;
}
gpiod_line_request_set_value(request13, line_offset13, GPIOD_LINE_VALUE_INACTIVE);
gpiod_line_request_release(request5);
gpiod_line_request_release(request6);
gpiod_line_request_release(request13);
return 0;
}
上記をgpiod_test_v2.cppのファイル名で保存し、バージョン1と2と共存できるようにしてある場合は、下記のようにコンパイルしてください。
$ g++ -o gpiod_test_v2 gpiod_test_v2.cpp `pkg-config --cflags --libs libgpiod_v2`
バージョン2をインストールしたまま(バージョン1が使えない状態)であるなら、バージョン1のときと同じようにコンパイルできます。但し、上記ソースコードの#include <gpiod_v2.h>を#include <gpiod.h>に書き換えてください。
$ g++ -o gpiod_test_v2 gpiod_test_v2.cpp `pkg-config --cflags --libs libgpiod`
④ /dev/gpiomem経由の制御
pinctrlのソースコードはC言語で書かれているの、それと同じようにすればGPIOの制御はできるはずです。
pinctrlのソースコードはraspberrypiのutilsの中にあります。
$ git clone https://github.com/raspberrypi/utils.git
$ cd utils/pinctrl
pinctrlはgpiolib.hというライブラリのようになっているので、下記のような感じでGPIOの制御ソフトを作ることができます。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "gpiolib.h"
int main()
{
int ret;
int freq = 500000;
int cnt = 0;
unsigned int gpio5 = 5;
unsigned int gpio6 = 6;
unsigned int gpio13 = 13;
ret = gpiolib_init();
if(ret < 0){
printf("fail gpiolib_init = %d\n", ret);
return -1;
}
ret = gpiolib_mmap();
if(ret < 0){
printf("fail gpiolib_mmap = %d\n", ret);
return -1;
}
gpio_set_fsel(gpio5, GPIO_FSEL_INPUT);
gpio_set_fsel(gpio6, GPIO_FSEL_INPUT);
gpio_set_pull(gpio5, PULL_UP);
gpio_set_pull(gpio6, PULL_UP);
gpio_set_fsel(gpio13, GPIO_FSEL_OUTPUT);
gpio_set_pull(gpio13, PULL_NONE);
gpio_set_drive(gpio13, DRIVE_LOW);
printf("push GPIO5 to start.\n");
while(gpio_get_level(gpio5) == 1){
usleep(10000);
}
printf("push GPIO6 to finish.\n");
while(1){
gpio_set_drive(gpio13, (GPIO_DRIVE_T)(cnt%2));
usleep(freq);
if(gpio_get_level(gpio6) == 0){
break;
}
cnt++;
}
gpio_set_drive(gpio13, (GPIO_DRIVE_T)0);
return 0;
}
上記のソースをgpiolib_test.cというファイル名で保存した場合、下記のようにすればコンパイルできると思います。後ろの方に付いているファイルはpinctrlのソースコードのディレクトリ内にあるファイルです。
$ gcc -o gpiolib_test gpiolib_test.c util.c gpiolib.c gpiochip_bcm2835.c gpiochip_bcm2712.c gpiochip_rp1.c
pinctrlは/dev/gpiomem経由でGPIOを制御しています。mmapを使って直接物理アドレスにアクセスしているので、高速ではありますが、あまりお薦めする方法ではないように思われます。
各方法による制御速度
以上、①~④の4種類のC/C++によるGPIOを試してみました。
スイッチの入力やLEDの点滅程度の利用であれば、どの方法でも大丈夫だと思います。
速度的な違いがどうなのかは気になるところなので、各方法でGPIO13への0,1の出力を繰り返し行い、オシロスコープでその繰り返し速度を見てみました。
①のsysfs経由では111KHz、②③のキャラクタデバイス経由では1.25MHz、④のmmapによる制御では20MHzとなりました。
やはり、直接メモリのレジスタに書き込む方法はOSのオーバーヘッドがないため高速なようです。
まとめ
C/C++でGPIOを制御するため4つの方法を試してみました。普段どれを使うのが良いかは微妙な感じです。
①のsysfsによる制御は非推奨になっているようですし、④はユーザーが直接メモリにアクセスするのはあまりお薦めできないという点から、②または③になるのでしょうか。
なので、現状、ラズパイOSでインストールされる②のlibgpiodのバージョン1を使っておくのが妥当なのかもしれません。ただバージョン1は、プル抵抗の設定ができなかったり、libgpiodの開発がバージョン2に移行しているという点から、③のバージョン2を今から使っておいた方が良いかもという気もします。
ただ、libgpiodのバージョン1と2が共存できないので、現状でバージョン2をインストールしてしまうと、他の人の作ったプログラムが動かなくなる可能性があるので、安易にインストールできないというジレンマがあります。
ラズパイを使う上でGPIOの制御は基本中の基本のように思うので、何か定番のライブラリがあると良いのですけどね。
C/C++で開発せずに、Pythonを使えということなのでしょうか。
libgpiod(v1)の使い方は下記も参考にしてください。
この記事が気に入ったらサポートをしてみませんか?