🔌

ラズベリーパイにてシャットダウンボタンを試す(C言語+pigpioライブラリ)

2023/10/17に公開

概要

ラズベリーパイ(Raspberry Pi)は、元々教育用に開発された小型のボード型コンピュータですが、汎用性の高さから、様々な場所で利用されています。たとえば、小型・軽量、各種センサとの親和性の高さ、TCP/IPなどネットワーク機能の充実等により、IoTの分野に使われることがあります。

ただ、ラズベリーパイは、LinuxなどのOSをSDカードから起動する方式のため、電源を切りたいときには、PCと同じようにシャットダウン操作を行う必要があります。シャットダウン操作を行わないと、最悪SDカードが壊れてしまいます。しかし、IoT利用では、キーボードやモニタを接続せず、ラズベリーパイ単体で用いられることが多く、シャットダウン操作をどのように行うかを考える必要があります。

今回、ラズベリーパイに押しボタンスイッチを取り付け、それを長押してシャットダウンさせる機能を試してみました。Pythonを使用した記事はよく見かけるため、C言語とGPIOライブラリとしてpigpioを使用しました。

ボタンの回路

ラズベリーパイの外部入出力ピン(GPIO)

ラズベリーパイには、「GPIO」(General-purpose input/output)と呼ばれる外部入出力ピンが40ピン装備されています。このうち入力として利用できるピンが「GPIO 0」~「GPIO 27」の28個あります(GPIO 0~3は特殊)。ボタンが押されたかどうかを見るには、GPIO 0~27のいずれかにボタンを接続し、そのピンをプログラムで監視します。

写真1.ラズベリーパイのGPIOピン(赤丸部)
写真1.ラズベリーパイのGPIOピン(赤丸部)

参考URL1:
Raspberry Pi公式ドキュメント内の「Raspberry Pi hardware」

ボタンの構造

また、ボタン自体の構造ですが、図1はタクトスイッチと呼ばれるボタンの断面図です。タクトスイッチは、ボタンを押すと接続され(電気が流れる状態となり)、離すと切断される(電気が流れなくなる)形式のスイッチです。内部に固定接点1と固定接点2の2つの端子があり、ボタンを離した状態では両者は切断状態で、ボタンを押すことで可動接点により固定接点1と固定接点2が接続されます。

図1.ボタン(タクトスイッチ)の構造
図1.ボタン(タクトスイッチ)の構造

参考URL2:
タクタイルスイッチとは?構造と動作原理(Panasonic Industry)

豆電球の回路

小学生の頃でしょうか、スイッチを入れると豆電球が点灯するような実験を思い出しますと、ボタンの端子の片側を電池の+につなぎ、もう片側を電球、さらに電池の-につないで、ボタンを押したり離したりすれば、電球が点灯したり消灯したりします。

図2.乾電池、ボタン、豆電球の回路
図2.乾電池、ボタン、豆電球の回路

ラズベリーパイにて、ボタンの押した/離したを検出するには、豆電球の回路と同じように、ボタンを押したときに電気が流れ、離したときに流れなくなる回路とし、流れる先は、GPIO(外部入出力ピン)の入力ピンとします。そして、プログラムにて入力ピンを監視すれば、ボタンが押されたか、離されたかを知ることができます。なお、電流が流れる/流れないと言っていますが、実際には、電圧が高い/低いでボタンの状態を検出します。

ラズベリーパイの電源ピン

ボタンを押した/離したを電圧の高い/低いで見るには、豆電球回路の乾電池に相当する何らかの電源(電圧源)が必要です。ラズベリーパイのGPIOには、電源として使用可能な+専用のピンがあります。上記の参考URL1に、どのようなピンが存在するかの図があります。「3V3 power」や「5V power」となっているピンが+専用のピンで、「3V3」の方は 3.3V の電圧であり、「5V」の方は 5V の電圧となっています。これらは、使用可能な電流の上限はありますが、汎用的に使用できる電源です。

ちなみに、0Vのピンもあります。「Ground」と書かれたピンがこれです。回路図では「GND」のように記載されることもあります。上記「3V3」や「5V」の端子と「Ground」の端子を線でつなぐと電気が流れます(が、直接つなぐとショートしますので、抵抗等を介して接続します)。

ラズベリーパイにてボタン状態を読み取る回路

以上から、ボタンの押した/離したを検出するには、豆電球の回路と同じように(ただし豆電球は付けません)、
 GPIOの+ → ボタン → GPIOの入力ピン
という回路を作ります。ラズベリーパイのGPIO入力ピンは、電圧が3.3Vと決められていますので、3.3V の電源に接続します。また、下の図3では、GPIO入力ピンのうち「GPIO 21」につないだ例となっています。

図3.ラズベリーパイにてボタン状態を読み取る回路
図3.ラズベリーパイにてボタン状態を読み取る回路

この回路にて、ボタンを押せば 3.3V につながり、GPIO 21のピンが電圧が高い状態となり、離せば高くない状態となります。

プルアップとプルダウン

上の図3では、ボタンが離れているときに、GPIO 21はどこにもつながっていない状態となります。これは、ボタンの状態を読み取る際に不安定要素となり、ボタンを離しているにもかかわらず、ノイズの影響等によりボタンを押していると読み取れてしまう現象も発生します。

そこで、「どこにもつながれていない状態」をなくすのが一般的で、通常は、0V(Ground)か +(3.3V)に接続します。今回の回路では、ボタンを押すと+(3.3V)に接続されますので、ボタンを離した状態で 0V につながるようにします。これにより、ボタンを押したとき 3.3V、離したとき 0V が確実に取得できます。

このように、「どこにもつながれていない状態」をなくすために、0V に接続することを「プルダウン」と呼び、また、+に接続することを「プルアップ」と呼びます。「ボタンをつなげたGPIO 21をプルダウンする」のように言います。

ラズベリーパイの内部プルダウン回路

ラズベリーパイでは、設定により、内部的な回路でプルアップやプルダウンを行うことができます。以下の図4は、ラズベリーパイ内部プルダウン回路をオンに設定したイメージ図です。

図4.ラズベリーパイ内部のプルダウン回路の使用
図4.ラズベリーパイ内部のプルダウン回路の使用

図4には抵抗が記載されていますが、このような抵抗を「プルダウン抵抗」と呼びます(同様に、プルアップ時に付ける抵抗を「プルアップ抵抗」と呼びます)。電源からGroundへの直結状態(短絡、ショート)を避けるなどの役割があります。ラズベリーパイの内部プルダウン回路には、プルダウン抵抗も含まれています。

外部プルダウン回路

ラズベリーパイでは、内部回路でプルアップやプルダウンが行えますが、内部回路を使わない場合や、内部回路の機能がないマイコンを使用する場合には、外部にプルアップやプルダウンの回路を設けます。以下は、今回の回路で、ラズベリーパイ内部のプルダウン回路は使わずに、外部にプルダウン回路を設けた例です。

図5.外部に設けたプルダウン回路の例
図5.外部に設けたプルダウン回路の例

回路構成により読み取る値が逆転する

なお、上の図5を、Webサイトなどでよく見かける形に書き換えると、下の図6のようになります。両者は回路的には全く同じものです。

図6.よく見かける形に書き換え
図6.よく見かける形に書き換え

この回路は、ボタンを押すとGPIO 21の電圧が「高」(3.3V)、離すと電圧が「低」(0V)となりますが、ボタンと抵抗の位置を逆にすると、ボタン状態も逆になります。

図7.ボタンを押すと低、離すと高の回路
図7.ボタンを押すと低、離すと高の回路

図7の回路は、GPIO 21をプルアップしており、ボタンが離された状態で電圧「高」(3.3V)となっています。そして、ボタンを押すとGroundと接続されるため電圧「低」(0V)となります。

プログラムでは、電圧の高(3.3V)を「1」、低(0V)を「0」で表しますが、図6の回路では、
 ボタンを押して「1」、離して「0」
であるのに対して、図7では、
 ボタンを押して「0」、離して「1」
のように全く逆の値になりますので、注意が必要です。

チャタリング

今回使用する押しボタンなどでは、チャタリングを考慮する必要があります。チャタリングとは、押しボタンを押した瞬間や離した瞬間に、接点部分のたわみや変形などによる微細な振動により、オンとオフが高速に繰り返される現象のことで、誤作動の原因となります。人間としては単にボタンを押したつもりでも、信号としては、短時間ですが押した/離したが繰り返されます。機械的な接点による入/切がある部品で発生します。

図8.チャタリングのイメージ
図8.チャタリングのイメージ

チャタリングを防止するには、ハードウエア的には、高速で上下する信号は高周波信号となりますので、ローパスフィルタと呼ばれる高周波を通さない回路を使用したり、シュミットトリガと呼ばれる回路(素子)を使用したりします。

また、ソフトウエア的には、ボタンの状態が変化してから少し待ち時間を入れることでチャタリングを回避できます。ただ、どのくらい待ち時間を入れればよいかは、ボタンの構造やその他の条件により変わってきます。ボタンの機能から考えると、待ち時間1秒は人間にとっては長過ぎですので、長くても0.1~0.5秒くらいまでが適当でしょうか。

今回のプログラムでは、「ボタンを長押ししたら」という機能ですので、「待ち時間を入れる」とは少し違いますが、チャタリングを吸収できますので、これで代用します。

ラズベリーパイの物理ピン番号とBCM番号

上述の通り、ラズベリーパイにはGPIOピンが40本あります。これらを見分けるために、ピンの1本1本に番号が付けられていますが、2種類の付番体系が存在します。

  • 物理ピン番号 … 40本を1番から40番まで順番に番号付け
  • BCM番号(Broadcom番号) … 入出力が可能なピン(電源(3.3Vや5V)、グランドなど以外)に「GPIO xx」(xxが番号、0~27)と番号付け。上の例で示した「GPIO 21」などもBCM番号。

具体的な番号は、上に参考URL1として示した『Raspberry Pi公式ドキュメント内の「Raspberry Pi hardware」』のピン配置の図にて確認することができます。たとえば、上の例で使用している「GPIO 21」はBCM番号での表記ですが、物理ピン番号では「40番」となります。

プログラムでは、ライブラリによって、どちらの付番体系を使用するか決められていたり、両方が使えるものは、事前設定でどちらの付番体系を使うかを指定します。たとえば、今回使用するpigpioライブラリはBCM番号に固定されています。また、Pythonでよく使われるRPi.GPIOライブラリ(モジュール)では、RPi.GPIO.setmode(RPi.GPIO.BCM)(BCM番号を使う場合)のように事前に指定します。

GPIOライブラリによるボタン状態の取得方法

GPIOライブラリの使用

ラズベリーパイにてGPIO関連プログラムを作成する際には、GPIOライブラリを使用するのが一般的です。

プログラミング言語やライブラリの作成者によって、様々なライブラリが存在しますが、大まかな使い方は共通です。

手順1.ライブラリの初期化
手順2.GPIOに関する初期設定
手順3.ピンの値を読み取る
手順4.プログラム終了前に後処理

これらの処理は、多くのライブラリで、あらかじめ用意された関数やメソッドを呼び出すだけで済むようになっています。

今回のサンプルプログラムでは、pigpioというライブラリをC言語で使用します。

なお、pigpioは、ラズベリーパイ上でpigpioサーバ(pigpiod)を動かして、サーバがGPIO入出力を制御するデーモン版と、サーバは不要で個々のプログラム内でGPIO入出力を制御するスタンドアロン版があります。今回は、スタンドアロン版を使用した例となっています。

手順1.ライブラリの初期化

使用するライブラリによっては、ライブラリ自体の初期化が必要な場合があります。ライブラリのマニュアルやサンプルプログラムを見ることで、ライブラリ自体の初期化が必要かどうかや、その使い方がわかります。

手順2.GPIOに関する初期設定

使用するピンに関する初期設定を行います。たとえば、次のような項目を設定します。

  • 物理ピン番号を使用するか、BCM番号を使用するか
  • 何番のピンを使用するか
  • 入力として使用するか、出力として使用するか(ボタンの状態を読み取る場合は、入力)
  • 内部のプルアップ回路やプルダウン回路を使用するか

手順3.ピンの値を読み取る

ピンの値を読み取ると、電圧の高い(3.3V)/低い(0V)が読み取れます。多くのライブラリでは、実際の電圧値ではなく、電圧が高い場合を「1」、低い場合を「0」のように単純な値で扱います。

ですので、ピンの値を読み取り、

  • 値が「1」なら、ボタンが押された
  • 値が「0」なら、ボタンが離された

のように判定して、それぞれに適した処理を実行します。

ただ、今回は、下に記載する「割り込み」を使用してボタンの状態を取得していますので、直接ピンの値を読み取ることはしません。

手順4.プログラム終了前に後処理

ライブラによっては後処理が必要な場合があります。やはり、マニュアルやサンプルプログラムを確認します。

開発環境

今回は以下の開発環境でプログラムを試しました。

項目 内容
開発言語 C言語 (ラズベリーパイ上の gcc 10.2.1-6)
GPIOライブラリ pigpio 1.79-1+rpt1
ラズベリーパイ Raspberry Pi 3B+
OS Raspberry Pi OS (Bullseye, 64-bit)

開発は C言語で行っています。また、OSをインストールした初期状態で pigpio もインストール済みでした。

参考URL3:
pigpio library の「pigpio C I/F」 (スタンドアロン版 C言語リファレンス)

コード1:pigpioライブラリによるサンプルコード

#include <stdio.h>
#include <pigpio.h>

#define SW_PIN 21
#define SLEEP_U_SEC 1000000    //1秒

int main()
{
    //手順1.ライブラリの初期化
    gpioInitialise();

    //手順2.GPIOに関する初期設定
    gpioSetMode(SW_PIN, PI_INPUT);           //入力として使用
    gpioSetPullUpDown(SW_PIN, PI_PUD_DOWN);  //内部プルダウン使用

    //ループ
    while(1)
    {
        //手順3.ピンの値を読み取る
        int val = gpioRead(SW_PIN);
        //表示
        printf("val=%d\n", val);

        //待機
        gpioDelay(SLEEP_U_SEC);
    }

    //手順4.プログラム終了前に後処理
    gpioTerminate();

    return 0;
}

このコードは、図4(下に再掲載)に示した接続を前提にしています。

図4.ラズベリーパイ内部のプルダウン回路の使用(再掲載)
図4.ラズベリーパイ内部のプルダウン回路の使用(再掲載)

では、コードを見ていきますと、今回pigpioライブラリを使用しますので、pigpio.h をインクルードしています。今回はスタンドアロン版のpigpioを使用していますが、デーモン版を使用する場合は、pigpiod_if2.hとなります。

main関数では、gpioXxxx()(Xxxxは任意)のような関数を呼び出していますが、これらは pigpio ライブラリが提供する関数です。これらの関数の多くは、エラー等が発生し失敗した場合、負の値を返します。(上のコード1では、エラー処理等を省略しています。)

pigpioの各関数を、上述の「GPIOライブラリの使用」で示した手順に当てはめると、次のようになります。

手順1.ライブラリの初期化 → gpioInitialise関数
手順2.GPIOに関する初期設定 → gpioSetMode関数、gpioSetPullUpDown関数
手順3.ピンの値を読み取る → gpioRead関数
手順4.プログラム終了前に後処理 → gpioTerminate関数

手順1.ライブラリの初期化 と 手順4.プログラム終了前に後処理

「手順1.ライブラリの初期化」は、gpioInitialise() を呼び出すだけです。また、「手順4.プログラム終了前に後処理」も、gpioTerminate() を呼び出すだけです。両者は、pigpio使用開始時と終了時に呼び出します。

手順2.GPIOに関する初期設定

定数 SW_PIN に使用するピン(ボタンを接続したピン)のBCM番号が格納されています。上のコードでは「21」としていますが、これは、ボタンをGPIO 21に接続したときの例です。実際に接続したピンによって変更します。このように、pigpioでは、ピンの指定にBCM番号を使用します。

「手順2.GPIOに関する初期設定」として、まず、gpioSetMode(SW_PIN, PI_INPUT) により、当該ピンを入力として設定しています。PI_INPUT は、pigpioが提供する「入力」を意味する定数です。もしLEDなどを接続して、出力として使用したいなら PI_OUTPUT を指定します。

  • 書式:int gpioSetMode(unsigned gpio, unsigned mode)
  • 機能:指定GPIOピンのモード(入出力方向)を指定する
  • 引数:gpio … BCM番号(0~27)
  • 引数:mode … 入出力方向を示す定数(PI_INPUT, PI_OUTPUT)
  • 戻り値:負の値 … 関数失敗、それ以外 … 成功

※このように関数の簡単な説明を記載しますが、必要最低限のもののみを示しています。正確で詳細な情報は、参考URL3に示したリファレンスで確認してください。

次に、gpioSetPullUpDown(SW_PIN, PI_PUD_DOWN) にて、ボタンを接続したピン(SW_PIN)において、内部プルアップ/プルダウン回路を使用するかどうかを設定しています。第2引数 PI_PUD_DOWN は、pigpioが提供する定数で、プルダウン回路を使うことを指定しています。(回路構成によって変更します。)

  • 書式:int gpioSetPullUpDown(unsigned gpio, unsigned pud)
  • 機能:指定GPIOピンにて内部プルアップ/プルダウン回路を使用するかどうか
  • 引数:gpio … BCM番号(0~27)
  • 引数:pud … 内部プルアップ/プルダウンの使用・不使用を示す定数(PI_PUD_UP, PI_PUD_DOWN, PI_PUD_OFF)
  • 戻り値:負の値 … 関数失敗、それ以外 … 成功

手順3.ピンの値を読み取る

gpioRead(SW_PIN)によりピンの値(電圧の高/低)を読み取ります。読み取った値は、次のprintf関数で出力しています。

  • 書式:int gpioRead(unsigned gpio)
  • 機能:指定GPIOピンの電圧レベルを読み取る
  • 引数:gpio … BCM番号(0~27)
  • 戻り値:負の値 … 関数失敗、それ以外 … 成功(1=電圧高、0=電圧低)

printfの後に、gpioDelay関数を呼び出しています。この関数は、指定時間(マイクロ秒)待機する関数です。上のコードでは、gpioDelay(SLEEP_U_SEC)となっていますが、定数 SLEEP_U_SEC は、コード冒頭で 1000000 と定義していますので、1000000マイクロ秒=1秒の待機となります。

  • 書式:uint32_t gpioDelay(uint32_t micros)
  • 機能:指定時間待機する
  • 引数:micros … 待機する時間(マイクロ秒)
  • 戻り値:実際に待機した時間(マイクロ秒)

これにより、1秒間隔でボタン状態が出力されます。

コンパイルと実行

上記のコード1を「sample.c」として保存した場合の、コンパイルと実行の例を以下に示します。

$ ls
sample.c

$ gcc sample.c -lpigpio

$ ls
a.out  sample.c

$ sudo ./a.out
val=0
val=0
val=0
val=1 ### ボタンを押す
val=1
val=0 ### ボタンを離す
val=0
^C    ### キーボード [Ctrl]+[C]
2023-xx-xx xx:xx:xx sigHandler: Unhandled signal 2, terminating

コンパイル(リンク)には「pigpio」ライブラリが必要ですので、「-lpigpio」を指定しています。エラー等がなければ、「a.out」が実行ファイルとして生成されます。

実行には管理者権限が必要です。ですので「sudo」にて実行します。実行すると、1秒間隔で「val=?」のように現在のボタンの状態を表示します。「1」が電圧高、「0」が電圧低です。

今回のコードのメイン処理部分は、whileの無限ループになっていますので、終了するには、キーボードにて [Ctrl] + [C] を押します。

ボタン長押しでシャットダウンするプログラム

大まかな流れ

たとえば、ボタンの3秒の長押しでシャットダウンするときの、プログラムの流れは、次のようになります。

  • 定期的にボタンの状態をチェックし、、、
  • 今、押された → 現在時刻を開始時刻として保存、あるいはタイマーを0から開始など
  • 押され続けている → 3秒以上経過したか確認し、経過していればシャットダウンを実行
  • 今、離された → 開始時刻をリセット、あるいはタイマーを停止など

割り込み処理

上記「手順3.ピンの値を読み取る」では、ピンの値を読み取り、値が「1」だったらボタンが押されたときの処理、「0」だったらボタンが離されたときの処理を実行する、というような内容を書きましたが、ボタンが押されたときに何か実行する場合、「割り込み」機能を使用した方が効率的です。

割り込みとは、本来の処理を実行している最中に、指定したトリガーの発生により、別の処理を実行する機能のことです。トリガー発生の監視はシステム側で行ってくれるため、余計なプログラムを記述する必要もありません。各種マイコンやラズベリーパイには割り込みの機能が備わっています。

たとえば、定期的な処理を実行するには「タイマー割り込み」を使用します。タイマー割り込みは、一定時間の経過をトリガーとした割り込みです。100ミリ秒ごとにcheckButton関数(ボタン状態確認関数)を呼び出す、というように使用します。

また、ピンの状態(電圧レベル)変化をトリガーとした「イベント割り込み」を使用することもできます。たとえば、GPIO 21の状態が変化(0→1 あるいは 1→0)したらcheckButton関数(ボタン状態確認関数)を呼び出す、というように使用します。今回は、こちらのイベント割り込みを使ってコードを記述します。

コード2:pigpioによる実装 - 基本部分

#include <pigpio.h>

#define SLEEP_U_SEC 100000    //0.1秒

int is_mainloop;
int is_shutdown = 0;

//長押し検出時の処理
void long_press_detected(int gpio)
{
    is_mainloop = 0;  //メインループ終了
    is_shutdown = 1;  //シャットダウンを実行する

    //ボタン状態変化時の割り込み登録を解除
    gpioSetAlertFunc(sw_pin, NULL);
}

//シャットダウン実行
void do_shutdown()
{
    //コマンド実行
    if (is_shutdown) system("/sbin/poweroff");
}

//ボタン状態変化時の処理
void sw_callback(int gpio, int level, uint32_t tick)
{
    //...
}

int main()
{
    //------------------------------
    //回路構成に合わせた設定

    //ボタンのBCM番号
    int sw_pin = 21;

    //内部プルアップ(PI_PUD_UP)/プルダウン(PI_PUD_DOWN)、使用しない(PI_PUD_OFF)
    int sw_pud = PI_PUD_DOWN;

    //------------------------------
    //ライブラリの初期化
    gpioInitialise();

    //------------------------------
    //GPIOに関する初期設定

    //ボタンのピンを入力として使用
    gpioSetMode(sw_pin, PI_INPUT);

    //ボタンのピンの内部プルアップ/プルダウン/使用しないの設定
    gpioSetPullUpDown(sw_pin, sw_pud);

    //------------------------------
    //ピンの値を読み取る

    //ボタン状態変化時のコールバック関数を登録
    gpioSetAlertFunc(sw_pin, sw_callback);

    //------------------------------

    //ループするかどうかのフラグ
    is_mainloop = 1;

    //ループ
    while(is_mainloop)
    {
        //待機
        gpioDelay(SLEEP_U_SEC);
    }

    //------------------------------

    //プログラム終了前に後処理
    gpioTerminate();

    //シャットダウン実行
    do_shutdown();
}

main関数の処理

main関数を見てみます。

冒頭で、ボタンを接続したBCM番号用の変数sw_pinと内部プルアップ/プルダウンの使用/不使用の変数sw_pudを宣言および初期化しています。上で示した「コード1:pigpioライブラリによるサンプルコード」では、定数マクロを使用していましたが、コマンドライン引数等にて変更できるように、変数としています。

GPIOの基本的な流れである「手順1.ライブラリの初期化」および「手順2.GPIOに関する初期設定」、「手順4.プログラム終了前に後処理」については、上で示したコード1と同様です。

「手順3.ピンの値を読み取る」については、今回は、ピンの値を直接読み取るのではなく、ピンの状態が変化したときに割り込みとして関数が呼び出される機能を使用しています。このための、ピン番号と呼び出される関数を登録するのが gpioSetAlertFunc(sw_pin, sw_callback) です。第2引数は、上記コードでmain関数のすぐ上に記述している sw_callback 関数のことです。

つまり、gpioSetAlertFunc(sw_pin, sw_callback)の1文を記述することにより、sw_pin (GPIO 21)の状態をシステムが監視し、状態変化(電圧レベルの変化)があったら、割り込みとして sw_callback関数が呼び出されるようになります。

なお、このような、割り込みなどによりシステム側から呼び出される関数(今回の例では sw_callback関数)のことを「コールバック関数」と呼びます。

  • 書式:int gpioSetAlertFunc(unsigned gpio, gpioAlertFunc_t func)
  • 機能:指定GPIOピンに状態変化があったときに呼び出されるコールバック関数を登録する
  • 引数:gpio … BCM番号(0~27)
  • 引数:funcgpioピンに状態変化があったときに呼び出される関数、NULLを指定することで登録を解除する
  • 戻り値:負の値 … 関数失敗、それ以外 … 成功

ここで、gpioAlertFunc_t は、次のように型定義されています。

typedef void (*gpioAlertFunc_t) (int gpio, int level, uint32_t tick);

上記コード2の sw_callback関数の戻り値や引数は、これに合わせて記述しています。

main関数内のwhileループ

上のコードでは、メインの処理を記述するための whileループがありますが、今回は、gpioDelay関数を呼び出しているのみとなっています。今回、ピンの状態変化は割り込みで読み取るため、メインの処理は「待機するだけ」でよいからです。

なお、while文のループ条件は、グローバル変数 is_mainloop の値です。割り込みのコールバック関数 sw_callback 内などで、ボタンの長押しが確定した際に、is_mainloop の値を変更することで、whileループから抜けられるようにしています。

その他の関数

上記コードの long_press_detected 関数は、sw_callback 関数にて3秒の長押しが確定したときに呼び出されます。処理内容としては、コメントにもあるとおり、

  • メインループする/しないフラグを「オフ」(メインループ終了)
  • シャットダウンする/しないフラグを「オン」(シャットダウンを実行する)
  • ボタン状態変化時の割り込み登録を解除

を実行しています。

また、do_shutdown 関数は、グローバル変数 is_shutdown(シャットダウンする/しないフラグ)がオンなら、シャットダウンコマンドを実行します。この関数は、main関数の最後に呼び出されます。

コード3:pigpioによる実装 - ボタンの長押し検出

#define SW_LONG_MSEC 3000

int onLevel;  //ボタンonの電圧レベル

void sw_callback(int gpio, int level, uint32_t tick)
{
    int off_level = !onLevel;

    //on? (off→on)
    if (level == onLevel)
    {
        //ウォッチドッグ開始
        gpioSetWatchdog(gpio, SW_LONG_MSEC);
    }
    //off? (on→off)
    else if (level == off_level)
    {
        //ウォッチドッグ停止
        gpioSetWatchdog(gpio, 0);
    }
    //ウォッチドッグ タイムアウト → ■長押し検出■
    else
    {
        //長押し検出時の処理
        long_press_detected(gpio);
    }
}

int main()
{
    //...

    //ボタンon時の電圧レベル
    //  ボタンを押したとき電圧高なら「1」
    //  ボタンを押したとき電圧低なら「0」
    int onLevel = 1;

    //...
}

このコードは、主に sw_callback 関数についてのコードです。「コード2:pigpioによる実装 - 基本部分」で示したコードに追加する形となります。

グローバル変数

コード3では、グローバル変数として int onLevel が宣言されています。

onLevel は、ボタンが押されたときの(オンと判断する)電圧レベルです。ボタンが押されたとき電圧が高くなる回路なら値として「1」を代入します。また、ボタンが押されたとき電圧が低くなる(0Vになる)回路なら「0」とします。実際のボタンの回路により変更します。図6と図7の違いをこの変数で吸収しています。

sw_callback関数の引数

sw_callback関数は、ボタンの状態(電圧レベル)が変化した時に呼び出されるコールバック関数です。引数の意味は、次のとおりで、pigpioにより決められています(上記「main関数の処理」の gpioAlertFunc_t の部分を参照)。

  • 引数:int gpio … 状態変化した元となるピンのBCM番号(今回はボタンを接続したピンのみ)
  • 引数:int level … 変化後の現在のレベル(値が「1」なら電圧低(0)→高(1)に変化、「0」なら電圧高(1)→低(0)に変化)
  • 引数:uint32_t tick … 変化した時点の、システム起動からの経過時間。単位はマイクロ秒。

引数 gpio は、複数のピンに対して、同じコールバック関数を使用する際に、ピンによって処理を振り分けるようなときに使用します。今回は、ボタンを接続した1つのピンのみなので、gpioによる条件分岐などはありません。

引数 level は、現在の電圧レベル(電圧高が「1」、低が「0」)です(実際には、状態変化なしを示す「2」の場合もあります。以下の「ウォッチドッグ」を参照してください)。sw_callback関数は、レベルの変化があったときに呼び出されますので、level1 なら「0 → 1」の変化があり、level0 なら「1 → 0」の変化があったことになります。

引数 tick は、システムが起動してからの経過時間を示します。単位はマイクロ秒です。符号なし32ビット(uint32_t)の値なので、最大値は4294967295マイクロ秒となり、1時間超でオーバーフローします(オーバーフロー時は0から数え直しとなります)。上のコードでは tick は使用しておりませんが、これを使用して1つ前の状態の時刻と比較するような場合は、オーバーフローに注意する必要があります。

ボタン状態(押した/離した)の判定

sw_callback関数のlevel引数は、電圧の高/低(1/0)の値となります。しかし、ボタンを押したか離したかは、これだけでは分かりません。図6と図7の部分で書いたように、ボタンを押して1の場合もあれば、押して0の場合もあるからです。

これを解決するには、上の「グローバル変数」にもあるとおり、グローバル変数onLevelにボタンを押したときの電圧レベルが代入されていますので、グローバル変数onLevelsw_callback関数の引数levelの値を比較して、等しければ「ボタンが押された」と判断します。

また、sw_callback関数の先頭で、int off_level = !onLevel;!は否定で、「1」なら「0」、「0」なら「1」)としていますので、sw_callback関数の引数leveloff_levelの値が等しければ「ボタンが離された」と判断できます。

これにより、電圧レベルの変化ではなく、ボタンの状態変化として処理を記述できます。

sw_callback関数のif文

sw_callback関数の処理は、if文により3つに分岐します。

  • ボタンが押された
  • ボタンが離された
  • それ以外(ウォッチドッグのタイムアウト)

1つ目の分岐は、ボタンが「今、押された」ときの条件分岐です。ボタンの状態変化では「off → on」の場合です。この時点から3秒間 on が継続すれば、長押し検出となります。

2つ目の分岐は、ボタンが「離された」ときです。ボタンの長押しが中断された(長押しの時間経過前に離された)状態です。

3つ目は、それ以外のときで、レベル変化がなかったときです。本来、割り込みによるコールバック関数呼び出しとしてはあり得ないのですが、ウォッチドッグを使うことで、この分岐に到達します。ウォッチドッグについては、次項を参照してください。

ウォッチドッグ

実際の処理内容を見てみると、1つ目の分岐(今、押された)では、gpioSetWatchdog関数によりウォッチドッグを開始しています。

  • 書式:int gpioSetWatchdog(unsigned gpio, unsigned timeout)
  • 機能:指定GPIOピンのウォッチドッグを開始・停止する
  • 引数:gpio … 監視するピンのBCM番号
  • 引数:timeout … 監視する時間(ミリ秒)、0を指定すると停止となる
  • 戻り値:負の値 … 関数失敗、それ以外 … 成功

マイコンなどで使われるウォッチドッグ(=番犬)は、止まってはいけいような機能が正しく動いているかを監視し、もし止まっていれば再起動させる、というような、正に番犬となる機能です。

pigpioのウォッチドッグは、指定したピン(gpio)のレベル変化を指定時間(timeoutミリ秒間)監視し、レベル変化がなかったときにgpioSetAlertFunc関数で登録したコールバック関数を呼び出す、という機能になっています。

つまり、通常割り込みでもウォッチドッグの指定時間超過でも、同じコールバック関数が呼び出されるのですが、どちらから呼び出されたのかの見分け方は、通常割り込みでは、現在のレベル(引数level)が実際の値(電圧の高低、10)となり、ウォッチドッグのタイムアウトでは「2」となります。

なお、2番目の条件分岐にあるように、timeout0 を指定すると、現在動作しているウォッチドッグを停止します。

さて、上記コードでは、1番目の条件分岐にて gpioSetWatchdog(gpio, SW_LONG_MSEC) のようにウォッチドッグを開始しています。第2引数の SW_LONG_MSEC は、コード先頭で 3000 としてマクロ定義されており、単位はミリ秒なので、3秒のウォッチドッグを開始していることになります。これは、3秒間レベル変化がないかどうか、つまり、3秒間ボタンのonが継続しているかどうかを検出する処理となります。

ボタンonから3秒間onが継続した時の流れをまとめると、

  • off → on になった(1番目の条件分岐へ)
  • 3秒のウォッチドッグ開始
  • 3秒間レベル変化がなければ、ウォッチドッグによりコールバック関数呼び出し(level引数の値は2
  • level0でも1でもないので、3番目の条件分岐を実行(→長押し検出)

となります。なお、3秒以内にレベル変化があれば、2番目の条件分岐が実行され、ウォッチドッグは停止されます。

このように、ウォッチドッグを利用することで、コードがシンプルに記述できます。もし、ウォッチドッグを使わなければ、off → on になったら、タイマー割り込みなどを使用して、別のコールバック関数で3秒経過したかを見るようなコードになります。

実際のコード

概要

実際のコードのベースとなる処理は、上で紹介したとおりです。

機能としては、少し利便性を向上させており、

  • コマンドライン引数でボタンのBCM番号や内蔵プルアップ/プルダウンの指定など可能(-hオプションで使い方表示)
  • 短押し時に別の処理を実行させることが可能(サンプルコード内ではコンソールに "short press"と表示)
  • 長押し検出時にLED点灯が指定可能
  • 複数のピンに対応

などを行っています。

また、コードとしては、ボタンに関する属性をSW_INFO構造体でまとめたり、ボタン関連の処理をsw.c/sw.hに分割したりしています。

※さらに、応用として、設定ファイルからピン番号などを読み込むような機能を追加するなどを試してみるとよいでしょう。

掲載したファイル

実際に掲載しているコードは、以下の5個で、長押し検出プログラム(名称はshutdown_btnとしています)のコードはコード4~コード6です。コード7はコンパイルに必要です。コード8は既にバックグラウンドで動作しているshutdown_btnを停止するシェルスクリプトで、特に必須のファイルではありません。

  • コード4:main.c
  • コード5:sw.h
  • コード6:sw.c
  • コード7:Makefile
  • コード8:stop_shutdown_btn.sh

コンパイルと実行方法

最初のコード「コード1:pigpioライブラリによるサンプルコード」では、gccコマンドを直接入力してコンパイルを行いましたが、今回は、ファイルが複数個になったためMakefileを用意しました。

掲載したコード4~コード6の4つのファイルを同じディレクトリに保存して、makeコマンドを実行することで、「shutdown_btn」という実行ファイルが生成されます。

$ ls
Makefile  main.c  sw.c  sw.h

$ make
gcc -Wall -O2 -c -o main.o main.c
gcc -Wall -O2 -c -o sw.o sw.c
gcc -o shutdown_btn main.o sw.o -lpigpio

$ ls
...  shutdown_btn  ...

生成されたshutdown_btnをコマンドラインオプション-hを付けて実行すると、コマンドラインオプションの簡単なヘルプが表示されます。

$ ./shutdown_btn -h
Usage: shutdown-btn options
options:
        -b bcm-num ... BCM number of button (def=21)
        -d bcm-num ... BCM number of LED. If not used, set a negative value (def=-1)
        -p pud-num ... Pull-up(2), Pull-down(1), Pull-off(0) (def=1)
        -o on-level ... level(0/1) when button is pressed, only if pud is Pull-off(0) (def=1)
        -l time ... long press time in milli-seconds (def=3000)
        -s time ... short press time in milli-seconds (def=100)

shutdown_btnを実際に動かすには、これまで使用していた回路(図4)の場合、次のようなコマンドを実行します。

$ sudo ./shutdown_btn -b 21 -p 1 &
[1] 683

short press!
short press!
short press!

コード1と同様、sudo により管理者として実行しています。また、実行時にオプションを指定しています。-bオプションはボタンを接続したピンのBCM番号(21 → GPIO 21に接続)、-pオプションは内部プルアップ/プルダウン回路の指定(1 → 内部プルダウンを使用)です。末尾の&は、バックグラウンドで動かすことを指示しています。

今回、短押しの処理として、画面上に「short press!」と表示するコードを記述しています。上では「short press!」が3行表示されいますが、ボタンを短く3回押した例です。

ここで、ボタンを3秒以上長押しすれば、ラズベリーパイはシャットダウンされます。

OS起動時に自動実行する方法

shutdown_btnをOS起動時に自動実行させるには、以下の実行例のように、/usr/local/bin等にコピーして、/etc/rc.localにコマンドラインを記述します。(shutdown_btnのオプション指定は、上とは異なっており、下の写真2の接続のオプション指定となっています。)

$ sudo cp ./shutdown_btn /usr/local/bin

$ sudo vi /etc/rc.local
...
# 写真2の接続の場合の、コマンドラインオプション
#   -b <ボタンのBCM番号=21>
#   -p <内部プルアップ(=2)の指定>
#   -d <LEDのBCM番号=16>
/usr/local/bin/shutdown_btn -b 21 -p 2 -d 16 &
...

以下の写真2は、ブレッドボード上にボタン(タクトスイッチ)とLEDを配置した例です。ボタンを GPIO 21 と Ground間に接続し内部プルアップ回路を使用しています。また、LEDを GPIO 16 と Ground 間に接続しています。(LEDは、抵抗内蔵のものを使用しています。)

写真2.実用例(ボタン:GPIO21-GND/内部プルアップ、LED:GPIO16-GND)
写真2.実用例(ボタン:GPIO21-GND/内部プルアップ、LED:GPIO16-GND)

コード4:main.c

#include <stdio.h>
#include <stdlib.h>    //strtol()
#include <signal.h>    //SIGxxx
#include <limits.h>    //INT_MIN/INT_MAX

#include <pigpio.h>

#include "sw.h"

/* ============================================================ */

//ボタンのBCM番号
#define SW_PIN 21
//内部プルアップ/ダウン回路の使用(PI_PUD_UP, PI_PUD_DOWN, PI_PUD_OFF)
#define SW_PUD PI_PUD_DOWN
//ボタンを押したときのレベル(SW_PUD が PI_PUD_OFF のときのみ有効)
#define SW_ONLVL 1
//短押し時間(ミリ秒)
#define SW_SHORT_MSEC 100
//長押し時間(ミリ秒)
#define SW_LONG_MSEC 3000
//長押し時のコマンド
#define SW_LONG_CMD "/usr/sbin/poweroff"
//LEDのBCM番号(負の値はLED使用しない)
#define LED_PIN -1

/* ============================================================ */

//最小値
#define SW_SHORT_MSEC_MIN 50
#define SW_LONG_MSEC_MIN 1000

//各種変数
int sw_pin, sw_pud;
int led_pin;

//ボタン情報構造体
SW_INFO sw_info;

/* ============================================================ */

//待機時間(マイクロ秒)
#define SLEEP_U_SEC 100000

//メインループする(1)/しない(0)
int is_mainloop;
//シャットダウンする(1)/しない(0)
int is_shutdown = 0;

/* ============================================================ */

int init_vars();
int check_arg(int argc, char *argv[]);
int str_to_int(const char *str, int def_val);
int usage();

void short_press_detected(int gpio);
void long_press_detected(int gpio);
void do_shutdown();

void sigxxx_callback(int signum);

void close_gpio();

/* ============================================================ */

//グローバル変数の初期設定
int init_vars()
{
    int tmp_short_msec, tmp_long_msec;
    int tmp_onlvl;

    //ボタンのBCM番号
    sw_pin = SW_PIN;

    //内部プルアップ/プルダウン
    sw_pud = SW_PUD;

    //ONレベル
    if (sw_pud == PI_PUD_OFF) tmp_onlvl = SW_ONLVL;
    else tmp_onlvl = sw_pud == PI_PUD_DOWN;

    //短押し時間
    tmp_short_msec = SW_SHORT_MSEC;
    if (tmp_short_msec < SW_SHORT_MSEC_MIN) tmp_short_msec = SW_SHORT_MSEC_MIN;

    //長押し時間
    tmp_long_msec = SW_LONG_MSEC;
    if (tmp_long_msec < SW_LONG_MSEC_MIN) tmp_long_msec = SW_LONG_MSEC_MIN;

    //LEDのBCM番号
    led_pin = LED_PIN;

    //ボタン情報の設定
    sw_info.pinNum = sw_pin;
    sw_info.onLevel = tmp_onlvl;
    sw_info.shortMilliSec = tmp_short_msec;
    sw_info.longMilliSec = tmp_long_msec;
    sw_info.shortDetected = short_press_detected;  //使用しない場合、NULLを設定
    sw_info.longDetected = long_press_detected;    //使用しない場合、NULLを設定

    return 0;
}

//コマンドライン引数のチェック
int check_arg(int argc, char *argv[])
{
    int tmp_sw_pin = -1;
    int tmp_led_pin = -1;
    int tmp_sw_pud = -1;
    int tmp_onlvl = -1;
    int tmp_long_msec = -1;
    int tmp_short_msec = -1;

    for(int i = 1; i < argc; i++)
    {
        if (argv[i][0] != '-' && argv[i][0] != '/') continue;
        if (argv[i][1] == '\0') continue;

        switch(argv[i][1])
        {
        //ヘルプ
        case 'h':
        case 'H':
        case '?':
            return -1;
        //ボタンのBCM番号
        case 'b':
        case 'B':
            if (i+1 >= argc) return -1;
            tmp_sw_pin = str_to_int(argv[i+1], tmp_sw_pin);
            i++;
            break;
        //LEDのBCM番号
        case 'd':
        case 'D':
            if (i+1 >= argc) return -1;
            tmp_led_pin = str_to_int(argv[i+1], tmp_led_pin);
            i++;
            break;
        //内部プルアップ/プルダウン
        case 'p':
        case 'P':
            if (i+1 >= argc) return -1;
            tmp_sw_pud = str_to_int(argv[i+1], tmp_sw_pud);
            i++;
            break;
        //ONレベル
        case 'o':
        case 'O':
            if (i+1 >= argc) return -1;
            tmp_onlvl = str_to_int(argv[i+1], tmp_onlvl);
            i++;
            break;
        //長押し時間
        case 'l':
        case 'L':
            if (i+1 >= argc) return -1;
            tmp_long_msec = str_to_int(argv[i+1], tmp_long_msec);
            i++;
            break;
        //短押し時間
        case 's':
        case 'S':
            if (i+1 >= argc) return -1;
            tmp_short_msec = str_to_int(argv[i+1], tmp_short_msec);
            i++;
            break;
        //未定義オプション
        default:
            return -1;
        }
    }

    //ボタンのBCM番号
    if (tmp_sw_pin >= 0 && tmp_sw_pin <= 27) {
        sw_pin = tmp_sw_pin;
        sw_info.pinNum = tmp_sw_pin;
    }
    //LEDのBCM番号
    if (tmp_led_pin >= 0 && tmp_led_pin <= 27) led_pin = tmp_led_pin;

    //内部プルアップ/プルダウン
    if (tmp_sw_pud == PI_PUD_UP || tmp_sw_pud == PI_PUD_DOWN || tmp_sw_pud == PI_PUD_OFF) {
        sw_pud = tmp_sw_pud;
    }
    //ONレベル
    if (sw_pud == PI_PUD_OFF) {
        if (tmp_onlvl == 0 || tmp_onlvl == 1) sw_info.onLevel = tmp_onlvl;
    }
    else {
        sw_info.onLevel = sw_pud == PI_PUD_DOWN;
    }

    //長押し時間
    if (tmp_long_msec >= SW_LONG_MSEC_MIN) sw_info.longMilliSec = tmp_long_msec;
    //短押し時間
    if (tmp_short_msec >= SW_SHORT_MSEC_MIN) sw_info.shortMilliSec = tmp_short_msec;

    return 0;
}

//数字文字列をint型数値へ
int str_to_int(const char *str, int def_val)
{
    int res;
    char *endptr;

    long ll = strtol(str, &endptr, 10);

    if (str == endptr || ll > INT_MAX || ll < INT_MIN) res = def_val;
    else res = (int)ll;

    return res;
}

//使い方表示
int usage()
{
    printf("Usage: shutdown-btn options\n");
    printf("options:\n");
    printf("\t-b bcm-num ... BCM number of button (def=%d)\n", sw_pin);
    printf("\t-d bcm-num ... BCM number of LED. If not used, set a negative value (def=%d)\n", led_pin);
    printf("\t-p pud-num ... Pull-up(%d), Pull-down(%d), Pull-off(%d) (def=%d)\n", PI_PUD_UP, PI_PUD_DOWN, PI_PUD_OFF, sw_pud);
    printf("\t-o on-level ... level(0/1) when button is pressed, only if pud is Pull-off(%d) (def=%d)\n", PI_PUD_OFF, sw_info.onLevel);
    printf("\t-l time ... long press time in milli-seconds (def=%d)\n", sw_info.longMilliSec);
    printf("\t-s time ... short press time in milli-seconds (def=%d)\n", sw_info.shortMilliSec);

    return 0;
}

/* ============================================================ */

//短押し検出時の処理
void short_press_detected(int gpio)
{
    printf("short press!\n");
}

//長押し検出時の処理
void long_press_detected(int gpio)
{
    is_mainloop = 0;    //メインループ終了
    is_shutdown = 1;    //シャットダウンを実行する

    //コールバック解除
    sw_info.longDetected = NULL;
    sw_setInfo(sw_info);

    //LED点灯
    if (led_pin >= 0) gpioWrite(led_pin, PI_HIGH);
    //ちょっと待つ
    gpioDelay(500000);
}

//シャットダウン実行
void do_shutdown()
{
    //コマンド実行
    if (is_shutdown) system(SW_LONG_CMD);
}

/* ============================================================ */

//signalのコールバック
void sigxxx_callback(int signum)
{
    fprintf(stderr, "signal #%d recieved\nquit program\n", signum);
    is_mainloop = 0;
}

/* ============================================================ */

//後処理
void close_gpio()
{
    //LED消灯
    if (led_pin >= 0 && !is_shutdown) gpioWrite(led_pin, PI_LOW);

    //pigpio終了
    gpioTerminate();
}

/* ============================================================ */

int main(int argc, char *argv[])
{
    init_vars();
    if (check_arg(argc, argv) < 0)
    {
        usage();
        return -1;
    }

    //------------------------------
    //ライブラリの初期化

    if (gpioInitialise() < 0)
    {
        fprintf(stderr, "ERROR: init pigpio\n");
        return -1;
    }

    //------------------------------
    //signalのコールバック登録

    if (
        gpioSetSignalFunc(SIGINT, sigxxx_callback) < 0
        || gpioSetSignalFunc(SIGQUIT, sigxxx_callback) < 0
        || gpioSetSignalFunc(SIGKILL, sigxxx_callback) < 0
        || gpioSetSignalFunc(SIGTERM, sigxxx_callback) < 0
    )
    {
        fprintf(stderr, "ERROR: cannot regist signal\n");
        close_gpio();
        return -1;
    }

    //------------------------------
    //GPIOに関する初期設定

    //ボタンのピンを入力として使用
    if (gpioSetMode(sw_pin, PI_INPUT) < 0)
    {
        fprintf(stderr, "ERROR: set INPUT mode\n");

        close_gpio();
        return -1;
    }

    //ボタンのピンの内部プルアップ/プルダウン/使用しないの設定
    if (gpioSetPullUpDown(sw_pin, sw_pud) < 0)
    {
        fprintf(stderr, "ERROR: set PullUp/Down\n");

        close_gpio();
        return -1;
    }

    //------------------------------
    //ボタン情報

    //ボタン情報の登録
    if (sw_setInfo(sw_info) < 0)
    {
        fprintf(stderr, "ERROR: set sw info\n");

        close_gpio();
        return -1;
    }

    //------------------------------
    //LED有効?
    if (led_pin >= 0)
    {
        if (gpioSetMode(led_pin, PI_OUTPUT) < 0)
        {
            fprintf(stderr, "ERROR: set OUTPUT mode for LED\n");

            close_gpio();
            return -1;
        }

        //消灯
        gpioWrite(led_pin, PI_LOW);
    }

    //------------------------------

    //ループするかどうかのフラグ
    is_mainloop = 1;
    //シャットダウンするかどうかのフラグ
    is_shutdown = 0;

    //ループ
    while(is_mainloop)
    {
        //待機
        gpioDelay(SLEEP_U_SEC);
    }

    //------------------------------

    //プログラム終了前に後処理
    close_gpio();

    //シャットダウン
    do_shutdown();

    return 0;
}

コード5:sw.h

#ifndef MY_SW_H_
#define MY_SW_H_

#include <stdio.h>
#include <stdint.h>    //uint32_t
#include <pigpio.h>

//------------------------------

typedef struct _SW_INFO {
    int pinNum;             //ピン番号
    int onLevel;            //オン時のレベル
    int shortMilliSec;      //短押しと判定するミリ秒
    int longMilliSec;       //長押しと判定するミリ秒
    void (*shortDetected)(int gpio);    //短押し検出時のコールバック
    void (*longDetected)(int gpio);     //長押し検出時のコールバック
} SW_INFO;

//------------------------------

int sw_setInfo(SW_INFO data);

//------------------------------

#endif /* MY_SW_H_ */

コード6:sw.c

#include "sw.h"

//tick調整用
#define TICK_MAX_MSEC 4294967    //4294967295 u sec (uint32の最大値)

#define GPIO_PIN_NUM 28        //0-27

//============================================================

static SW_INFO sw_info[GPIO_PIN_NUM];
static uint32_t before_msec[GPIO_PIN_NUM];

static void sw_callback(int gpio, int level, uint32_t tick);

//============================================================

//ボタン情報登録
int sw_setInfo(SW_INFO si)
{
    int gpio = si.pinNum;
    if (gpio < 0 || gpio >= GPIO_PIN_NUM) return -1;

    sw_info[gpio] = si;
    before_msec[gpio] = 0;

    //コールバック登録
    if (gpioSetAlertFunc(gpio, sw_callback) < 0) return -1;

    return 0;
}

//ボタン状態変化時のコールバック
static void sw_callback(int gpio, int level, uint32_t tick)
{
    int on_level, off_level;

    //短押し検出時、長押し検出時のどちらのコールバックも未定義?
    //→ 何もしない
    if (!sw_info[gpio].shortDetected && !sw_info[gpio].longDetected) return;

    on_level = sw_info[gpio].onLevel;
    off_level = !on_level;

    //現在のミリ秒(マイクロ秒→ミリ秒)
    uint32_t msec = tick / 1000;

    //on? (off→on)
    if (level == on_level)
    {
        //1つ前の時刻を保存
        before_msec[gpio] = msec;

        //長押し検出時の処理あり?
        if (sw_info[gpio].longDetected != NULL)
        {
            //ウォッチドッグ開始
            gpioSetWatchdog(gpio, sw_info[gpio].longMilliSec);
        }
    }
    //off? (on→off)
    else if (level == off_level)
    {
        //短押し検出時の処理あり?
        if (sw_info[gpio].shortDetected != NULL)
        {
            //tickがオーバーフローしてる? → 調整
            if (before_msec[gpio] > msec) msec += TICK_MAX_MSEC;
            //差を計算
            uint32_t period = msec - before_msec[gpio];

            //差が短押し時間以上? → ■短押し検出■
            if (period >= sw_info[gpio].shortMilliSec)
            {
                (*(sw_info[gpio].shortDetected))(gpio);
            }
        }

        //ウォッチドッグ停止
        gpioSetWatchdog(gpio, 0);
    }
    //ウォッチドッグ タイムアウト → ■長押し検出■
    else
    {
        //長押し検出時の処理
        (*(sw_info[gpio].longDetected))(gpio);
    }
}

コード7:Makefile

※インデントがスペースだった場合、タブに修正してください

#
TARGET_NAME = shutdown_btn

TARGET = $(TARGET_NAME)
OBJS = main.o sw.o

CC = gcc
CFLAGS = -Wall -O2
LIBS = -lpigpio

$(TARGET): $(OBJS)
	$(CC) -o $(TARGET) $^ $(LIBS)

*.o: sw.h

all: clean $(TARGET)

.PHONY: clean
clean:
	$(RM) $(TARGET) $(OBJS)

コード8:stop_shutdown_btn.sh

#!/bin/sh

APP_NAME=shutdown_btn
EXCLUDE_NAME=stop_shutdown_btn

RES_PID=`ps aux | grep $APP_NAME | grep -v 'grep' | grep -v $EXCLUDE_NAME | awk '{print $2}'`

if [ "x$RES_PID" = "x" ]; then
    echo "$APP_NAME is not working."
    exit 0
fi

for pid in $RES_PID
do
    echo "kill $pid"
    sudo kill -TERM $pid 1> /dev/null 2>&1
done

echo "$APP_NAME was stopped."

Discussion