前回、ラズパイのGPIOの出力を、Cのライブラリlibgpiodを利用してLチカのプログラムを記述しました。
今回は、入力です。
callback関数の変遷?
callbackという呼び方は、検索すると今は、「関数ポインタや関数オブジェクトを使って、関数の実行を他の関数に委譲する手法」だそうです。わからん!
特徴の一つが「非同期処理」なんだそうです。いつI/Oが変化するかわからない処理を記述するので、そいう言葉で呼ぶのは理屈が通っているのでしょう。
10年前、Arduinoやラズパイでは、「割り込み関数の記述のこと」でした。
もっと前は、callback関数という用語は存在せず、割り込み関数と呼ばれていました。マイコンでは、CPUに割り込み用のピンが複数実装されていて、そこの信号がLowもしくはHighになると、特定な番地にジャンプしました。その番地に何らかの処理を書いたり、領域が狭いので、別のアドレスへジャンプして処理プログラムを記述しました。
その後、ソフト割り込みというのが実装されました。割り込みテーブルを定義できるので、複数の割り込みの記述が格段に書きやすくなりました。
しかし、割り込みの処理の途中で別の割り込みが入るなどして、4レベルぐらいの多重割り込みの処理の中を飛び回っていると、そう、デバッグがほとんどできません。なんて時代が大昔にありました。
この時代でも、I/Oに信号が入ることを知るには、ポーリングと割り込みの2種類がありました。
では、ポーリングでプログラムを書いていきます。
ポーリング
プッシュ・スイッチをGPIO16(36番ピン)と+3.3V(17番ピン)につなぎました。チャタリング対策はしていません。
#include <stdio.h>
#include <unistd.h>
#include <gpiod.h>
#include <iostream>
int value;
int main() {
struct gpiod_chip *gchip;
struct gpiod_line *glinein, *glineout;
// GPIOデバイスを開く
gchip = gpiod_chip_open("/dev/gpiochip4");
// GPIOのピンのハンドラを取得
glineout = gpiod_chip_get_line(gchip, 21);
glinein = gpiod_chip_get_line(gchip, 16);
// モード設定
gpiod_line_request_output(glineout, "LED ON/OFF", 0);
gpiod_line_request_input(glinein, "LED ON/OFF");
for (int i=0; i < 10; i++){
value = gpiod_line_get_value(glinein);
std::cout << "SW0 " << value << "\n";
if (value == 1){
// std::cout << " SW " << value << "\n";
gpiod_line_set_value(glineout, 1); // GPIO21の値を1に
}else{
gpiod_line_set_value(glineout, 0); // GPIO21の値を0に
}
std::cout << i << "\n";
sleep(1);
}
gpiod_chip_close(gchip); // GPIOデバイスを閉じる
return 0;
}
動作させます。SWを押しているとLEDが点灯し、SWをoffするとLEDは消えます。
value = gpiod_line_get_value(glinein);
GPIO16の状態を取得している部分ですが、前回、gpioinfoコマンドを動かしたとき、
$ gpioinfo gpiochip4
gpiochip4 - 54 lines:
line 16: "GPIO16" unused input active-high
active-high
と記述されていました。意味は、デフォルトはプルダウンされていて、ピンがHighになると、信号が入ったと解釈するものと思われます。
High level API
このlibgpiod 1.6.3には、High level APIという項目があります。
そして、中を見ると、callbackにかかわるtypedefが記述されています。
検索して、callback関数の書き方というのを探すと、解説には、
// コールバック関数の型を定義
typedef int(* gpiod_ctxless_event_handle_cb) (int, unsigned int, const struct timespec *, void *);
を記述するようにとあります。
うまいことに、
// コールバックとして登録される関数 Callback function
int myEvent(int x, unsigned int c, const struct timespec * v, void * b){
std::cout << " in callback function \n";
return 0;
}
も記述できます。
なので、
このHigh level APIを利用して、callback関数の記述すればいいように見えます。
まー、1週間ほどいろいろ試しているのですが、気になることがあります。それは、新しいバージョンlibgpiod 2.1.2は、検索すると見つかります。
https://libgpiod.readthedocs.io/en/latest/modules.html
それには、callbackにかかわるHigh level APIが消えてしまっているのです。代わりに、割り込み記述で使われる関数らしいのが見つかりますが、そう、どのOSで走るかもわからないので、使っている事例が見つかりません。じっくり検索するとLinaro が見つかります。Linuxが動くボード・コンピュータという認識?です。ラズパイは含まれない?ようです。
High level APIを使う
gpiod_ctxless_get_value
を使ってみます。Read current value from a single GPIO line.という機能の関数です。
#include <iostream>
#include <gpiod.h>
#include <unistd.h>
int rc;
int main(){
std::cout << " start \n";
struct gpiod_chip *gchip;
struct gpiod_line *glinein, *glineout;
// GPIOデバイスを開く
gchip = gpiod_chip_open("/dev/gpiochip4");
glineout = gpiod_chip_get_line(gchip, 16);
const char * consu = gpiod_line_consumer(glineout);
std::cout << &consu << "\n";
const char * pBank = "gpiochip4"; // device name of gpiochip
const char * c = "io-event"; // name of consumer
int in_line = 16; // offset for gpio
for (int i = 0; i < 10; i++){
rc = gpiod_ctxless_get_value(pBank, in_line, 0, c);
std::cout << "sw is " <<rc << "\n";
sleep(1);
}
gpiod_chip_close(gchip);
std::cout << "\n";
return 0;
}
実行します。
$ g++ ex405.cpp -lgpiod
$ ./a.out
start
0xffffd0eb56b0
sw is 0
sw is 0
sw is 0
sw is 1
sw is 0
sw is 1
sw is 1
sw is 0
sw is 1
sw is 0
ポーリングのプログラムのときと同様に、プッシュ・スイッチをGPIO16(36番ピン)と+3.3V(17番ピン)につないでいます。チャタリング対策はしていません。10回のループの間、SWを押したら1に、押さないと0が表示されます。
callback
C++のcallback関数の書き方は次のようになるという解説をみても、よくわかりません。
試行錯誤しました。
#include <iostream>
#include <gpiod.h>
#include <unistd.h>
// コールバック関数の型を定義
typedef int(* gpiod_ctxless_event_handle_cb) (int, unsigned int, const struct timespec *, void *);
// コールバックとして登録される関数 Callback function
int myEvent(int x, unsigned int c, const struct timespec * v, void * b){
std::cout << " in callback function x=" << x << " c=" << c << " v=" << &v << " b=" << &b << "\n";
return 0;
}
int main(){
std::cout << " start \n";
struct gpiod_chip *gchip;
struct gpiod_line *glinein, *glineout;
// GPIOデバイスを開く
gchip = gpiod_chip_open("/dev/gpiochip4");
glineout = gpiod_chip_get_line(gchip, 16);
const char * consu = gpiod_line_consumer(glineout);
std::cout << &consu << "\n";
// in my main function
gpiod_ctxless_event_handle_cb p = &myEvent; //setup callback
const char * pBank = "gpiochip4"; // name of gpiochip
const char * c = "io-event"; // name of consumer
int in_line = 16; // offset for gpio
struct timespec time;
time.tv_nsec=1000000;
time.tv_sec=1;
// int flags =0;
int rc = gpiod_ctxless_event_monitor(pBank, GPIOD_LINE_EVENT_RISING_EDGE,
in_line, 1, c, &time, NULL, p, NULL);
/*
const char * device,
int event_type, // GPIOD_CTXLESS_EVENT_CB_RISING_EDGE, GPIOD_CTXLESS_EVENT_CB_FALLING_EDGE
unsigned int offset,
bool active_low,
const char * consumer,
const struct timespec * timeout,
gpiod_ctxless_event_poll_cb poll_cb,
gpiod_ctxless_event_handle_cb event_cb, // Callback function to call for each line event.
void * data
*/
std::cout << rc << "\n";
std::cout << " set done \n";
sleep(1);
std::cout << " 1 \n";
sleep(1);
std::cout << " 2 \n";
sleep(1);
std::cout << " 3 \n";
sleep(1);
std::cout << " 4 \n";
sleep(1);
// main in kara loop
for (int i = 0; i < 10; i++){
std::cout << (int)i << " ";
sleep(1);
}
gpiod_chip_close(gchip);
std::cout << "\n";
return 0;
}
実行します。startの表示の後、SWを押します。
$ g++ ex404.cpp -lgpiod
yoshi@yoshi:~/book$ ./a.out
start
0xffffd24abc68
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=1 c=0 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=1 c=0 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
CTRL-Cで抜けます。timeoutを1秒に設定しているので、SWを押さないとこの割込み関数は、
in callback function x=1 c=0 v=0xffffd24ab700 b=0xffffd24ab6f8
を表示し続けます。
SWを押すと、
in callback function x=2 c=16 v=0xffffd24ab700 b=0xffffd24ab6f8
を表示します。チャタリングがあるので、まとまって複数行表示されています。
mainのプログラムは存続しません。
しっかり、SWのONを非同期で取得できているのはいいのですが、mainに戻ってきません。名称の通り、ずっとGPIOをモニタし続けます。
これは、利用目的には適していない動作です。
このHigh level API関数、名前の通り、ほかの関数より上位にある?という解釈をすれば、最初の、
// GPIOデバイスを開く
gchip = gpiod_chip_open("/dev/gpiochip4");
glineout = gpiod_chip_get_line(gchip, 16);
とか不要ではないかと、消してしまいました。
#include <iostream>
#include <gpiod.h>
#include <unistd.h>
// コールバック関数の型を定義
typedef int(* gpiod_ctxless_event_handle_cb) (int, unsigned int, const struct timespec *, void *);
// コールバックとして登録される関数 Callback function
int myEvent(int x, unsigned int c, const struct timespec * v, void * b){
std::cout << " in callback function x=" << x << " c=" << c << " v=" << &v << " b=" << &b << "\n";
return 0;
}
int main(){
std::cout << " start \n";
// in my main function
gpiod_ctxless_event_handle_cb p = &myEvent; //setup callback
const char * pBank = "gpiochip4"; // name of gpiochip
const char * c = "io-event"; // name of consumer
int in_line = 16; // offset for gpio
struct timespec time;
time.tv_nsec=1000000;
time.tv_sec=1;
int rc = gpiod_ctxless_event_monitor(pBank, GPIOD_LINE_EVENT_RISING_EDGE,
in_line, 1, c, &time, NULL, p, NULL);
std::cout << rc << "\n";
std::cout << " set done \n";
sleep(1);
std::cout << " 1 \n";
sleep(1);
std::cout << " 2 \n";
sleep(1);
std::cout << " 3 \n";
sleep(1);
std::cout << " 4 \n";
sleep(1);
// main in kara loop
for (int i = 0; i < 10; i++){
std::cout << (int)i << " ";
sleep(1);
}
std::cout << "\n";
return 0;
}
実行結果は同じように動きました。
ここまでで、callbackの解説にある、「関数ポインタや関数オブジェクトを使って」というのを実装していません。なぜって、どの解説を読んでも、意味がわからないからです。
それを実装すると、上記のプログラムを非同期にできるのか? できないのかもわかりません。
蛇足
うまく動かないときは、一度プログラムを動かすと、
line 16: "GPIO16" "io-event" input active-high [used]
になります。
$ gpioset gpiochip4 16=1
gpioset: error setting the GPIO line values: Device or resource busy
使用中だということで変更がききません。
なので、プログラム中で、
gpiod_line_release(glineout);
しても、初期化されないようです。
初期状態に戻すには、rebootをかけます。でも、それは、実際には利用できないので、どうにかしたいです。
もろもろ そもそも
ROS2を利用して搬送車を作ろうという目論見で、C++の勉強をしていたりします。
この手の自動機には、緊急停止ボタンがあります。
モータはオリエンタルモーターの製品をCANopen言語で動かしたいと思っています。同社のモータは、modbusでは、この緊急用スイッチに対応していますが、CANopenでは、ディジタル入力がサポートされていません。たとえば、EPOS4では、ディジタル入出力があって、緊急停止ボタンを実装できるようになっています。CANopenでサポートされていたはずです。
で、ラズパイをコントローラで使っているので、緊急停止ボタンは、ラズパイのほうで何とかしようとしてGPIOを割り込みで処理したかったのですが、すんなりいかないものですねー。
プログラムを進める
callback関数内を、SWが押されたら、押されたことを表示するに変更しました。
#include <iostream>
#include <gpiod.h>
#include <unistd.h>
bool intFlag = false;
// コールバック関数の型を定義
typedef int(* gpiod_ctxless_event_handle_cb) (int, unsigned int, const struct timespec *, void *);
// コールバックとして登録される関数 Callback function
int myEvent(int x, unsigned int c, const struct timespec * v, void * b){
// std::cout << " in callback function x=" << x << " c=" << c << " v=" << &v << " b=" << &b << "\n";
intFlag = false;
if (c == 16) {
intFlag = true;
std::cout << " intFlag(GPIO16 done)=" << intFlag << "\n";
}
return 0;
}
int main(){
std::cout << " start \n";
// in my main function
gpiod_ctxless_event_handle_cb p = &myEvent; //setup callback
const char * pBank = "gpiochip4"; // name of gpiochip
const char * c = "io-event"; // name of consumer
int in_line = 16; // offset for gpio
struct timespec time;
time.tv_nsec=1000000;
time.tv_sec=1;
int rc = gpiod_ctxless_event_monitor(pBank, GPIOD_LINE_EVENT_RISING_EDGE,
in_line, 1, c, &time, NULL, p, NULL);
std::cout << rc << "\n";
return 0;
}
SWが押されたら、シャットダウンするつもりなので、このmonitorの実行を止めるようにするにはどうしたらいいのでしょうか?
gpiod_ctxless_event_monitor_multiple()
のところに、 Both callbacks can stop the loop at any point.
と書かれています。
試しに、SWが押されたとき、returnで-1を戻してみました。
#include <iostream>
#include <gpiod.h>
#include <unistd.h>
bool intFlag = false;
// コールバック関数の型を定義
typedef int(* gpiod_ctxless_event_handle_cb) (int, unsigned int, const struct timespec *, void *);
// コールバックとして登録される関数 Callback function
int myEvent(int x, unsigned int c, const struct timespec * v, void * b){
// std::cout << " in callback function x=" << x << " c=" << c << " v=" << &v << " b=" << &b << "\n";
intFlag = false;
if (c == 16) {
intFlag = true;
std::cout << " intFlag(GPIO16 done)=" << intFlag << "\n";
return -1;
}
return 0;
}
int main(){
std::cout << " start \n";
// in my main function
gpiod_ctxless_event_handle_cb p = &myEvent; //setup callback
const char * pBank = "gpiochip4"; // name of gpiochip
const char * c = "io-event"; // name of consumer
int in_line = 16; // offset for gpio
struct timespec time;
time.tv_nsec=0;
time.tv_sec=1;
int rc = gpiod_ctxless_event_monitor(pBank, GPIOD_LINE_EVENT_RISING_EDGE,
in_line, 1, c, &time, NULL, p, NULL);
std::cout << rc << "\n";
std::cout << " --- \n ";
sleep(1);
std::cout << " 1\n ";
sleep(1);
std::cout << " 2\n ";
sleep(1);
std::cout << " 3\n ";
sleep(1);
std::cout << " 4\n ";
sleep(1);
return 0;
}
実行しました。
$ g++ ex408.cpp -lgpiod
$ ./a.out
start
intFlag(GPIO16 done)=1
-1
---
1
2
3
4
一度-1でcallback関数 myEvent(int x, unsigned int c, const struct timespec * v, void * b)
を抜けた後は、割り込みは発生しなくなりました。
抜けた後も、gpiod_ctxless_event_monitor()
は動作を続けてほしかったのですが。
以上、GPIO入力の割り込み処理は実現できなかったので、ポーリングで処理をすることにします。