pigpioのコールバックを使って、超音波距離センサ「HC-SR04」のパルス幅を正確に計測する


#はじめに
Raspberry Piは、一般的にOS(Raspbian, Raspberry Pi OS)上で動作している関係で、Arduinoや ESP等のコントローラに比べて、正確なパルス生成や、パルス幅の測定は得意ではありませんが、pigpiod のサービスを活用する事により、完璧とは言えないまでも、そこそこに正確なパルス生成や測定が出来ることを最近知りました。

今回は、pigpiodが、python用のライブラリとして用意している、callback関数を用いて、超音波距離センサー(HC-SR04）による距離測定の実験をしましたので、アップしておきます。

![IMG_20200729_232437.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/2eb819c9-fd80-b1d5-d395-ff854c25a358.jpeg)

## 使用した環境

+ Raspberry pi Zero WH (Buster Lite)
+ HC-SR04 
    + 5V電源 (ラズパイから取る）
    + trig pin --> GPIO 5へ接続
    + echo pin --> 5V 信号なので 10kΩ+10kΩで分圧して GPIO 6へ接続
+ ミニブレッドボードとジャンパー線
+ ラズパイ用ヘッダー拡張基板（自作・・・ 40pin -> 40pinの全ピン接続）
+ ラズパイ用実験基板（自作）・・・MCP3208、LED、SW、I2Cデバイスや UART、VR、CdS等を色々試せる基板
+ python 3.7.3 
+ pigpio version 71
    + sudo apt install pigpiod で pigpiodインストール
    + sudo apt install python3-pigpioで pythonモジュールインストール
    + sudo systemctl enable pigpiod.service で起動時サービス許可
        + ただし、Defaultでは、pigpiod -l オプションが付いてしまうので、 /lib/systemd/system/pigpiod.serviceのファイルを修正して、-l オプションを外して置く。サービスで起動せず、sudo pigpiod をプロンプトから実行しても良いが、起動時に忘れてしまうので、サービスで起動しておいた方が便利。
        + pigpiod起動時オプションで、時間分解能を設定する事が出来ますが、細かくすればするだけ、システムへの負荷が上がるので、今回は defaultのまま(5 microseconds)としました。
        + 

写真の下部に、I2Cの温度、湿度、気圧センサーが3個接続してありますが、本記事とは、直接関係ありません。ちなみに、node-REDを使って、各種のセンサー結果や、その後の処理を統合化するのが、とても便利です。

#プログラム
今回のプログラムは、起動したら一回だけ距離を測定して終了する形にしました。ループ処理や、定期的な起動処理は全部、前述のnode-REDに任せる形にすることにより、全体の統合状態を見えやすくする意図があります。　参考までに、今回使った node-REDのフローは、文末に掲載しておきます。

pigpioの callbackに関する記事はあまり見当たらないのですが、pigpiodの作者のページ（[ここ](http://abyz.me.uk/rpi/pigpio/pigpiod.html)）に、各関数の使い方が記されているので、それを参考にしながら作成してみました。

## 初期化
pigpioライブラリをimportし、HC-SR04のtriggerピンと、echoピンのGPIOへの割り当てを設定し、triggerピンを出力モードに、echoピンを入力モードに設定します。パルス幅測定用の t_riseと t_fallはここでクリアしておきます。

##コールバック関数定義
今回のキモです。callbackの条件が成立した際に、この関数が呼ばれますが、その際、どのGPIOピンの変化なのか、変化後の信号レベルは HかLか、変化したタイミングはいつか？という3つの情報を含めて呼ばれます。タイミングの tick に関しては、microsecondsの単位で32bitの変数でシステムが管理していますが、たった 32bitしか無いため、2^32 microseconds （約 72分）でラップしてしまいます。このtickをクリアする事は出来なさそうなので、ラップした際の時間幅計算は、例外処理として計算します（実際には、ここの実験は出来ていないので、バグがあるかも・・・）

コールバックが呼ばれた際、Hのレベルなら立ち上がりエッジと考え、そのタイミングのtickを記憶しておきます。コールバックが呼ばれた際 Lのレベルなら立ち下がりエッジと考え、立上りから立下り迄の tick値から、パルス幅を求めます。tick値の差分が、そのまま microsecondsの値になります（前述のラップ時は別計算必要）

あくまでも、立上りから立下りまでの時間測定として、決め打ちしていますが、HC-SR04決め打ちのコードなので、問題ないかと・・・・。厳密には誤動作する可能性は残っています。

得られた時間を、距離に換算します。音は1秒間に340m伝搬しますので、得られた時間(microsec)を、距離 (cm）に換算し、さらに、半分の距離を求めます（測定された距離は、triggerが出てから、反射して戻ってきて echoに入った時間なので、往復の距離となるため）

距離に換算された結果を、jsonの書式の文字列にして stdoutに printします。

##　メイン処理
コールバック関数の呼び方を定義します。ここでは、「echoピン」、「立上り、立下りの両エッジを検出」、「検出時に呼ぶ関数名」を定義します。cbという変数に代入していますが、使っていません。本当は使い道があるはず・・・・

次の pi.gpio_trigger(HC_SR04_trig, 10, 1)が、実際にトリガーパルスを出力する部分です。 10usecの幅を持つ「正極性」パルスを出力できます。かなり正確に 10usecのパルスが出力できます。

トリガを出したあとは、echoパルスが入り、コールバック関数が呼ばれ、パルス幅を計測する事になりますが、そのための処理時間を sleepして待っています。この sleepが無いと、callbackを呼ばれる時間も無く、プログラムが終了してしまい正しい動作を行いません。 HC-SR04の測定範囲を考えても、100msec待てば、echo信号は確実に戻ってくるので 十分と考えられます。

## 終了処理

最初に初期化した piという pigpoidのハンドラを、終了しておきます。これをしないと、繰り返し起動し続けているうちに、正常動作をしなくなるリスクがあるかと思います（ただ、 stop()を行わない場合に実際には、どういう副作用があるのか確かめていません）

# コード
以下がコードです。このコードを、ラズパイのターミナルから起動すれば、一回測定して終了します。また、node-redから呼べば、同様に一回測定して終了します。あえてループ処理にしていないのは、node-redとの併用で全体を考えているからです。

```sr04.py
# one shot measurement only
#
import pigpio
import time

HC_SR04_trig = 5
HC_SR04_echo = 6

pi = pigpio.pi()
pi.set_mode(HC_SR04_trig, pigpio.OUTPUT)
pi.set_mode(HC_SR04_echo, pigpio.INPUT)

t_rise = 0
t_fall = 0


def cbf(gpio, level, tick):  # call back function for pulse detect _/~~\__
    global t_rise, t_fall

    if (level == 1):  # right after the rising edge
        t_rise = tick
    else:            # right after the falling edge
        t_fall = tick
        if (t_fall >= t_rise):  # if wrapped 32bit value,
            timepassed = t_fall - t_rise
        else:
            timepassed = t_fall + (0xffffffff + 1 - t_rise)

        # meter to cm, microseconds to seconds, divide by 2
        d = 340 * 100 * timepassed / 1000000 / 2
        print('{"tick":%10d, "time_us": %6d, "distance_cm": %.2f}' % (
            tick, timepassed, d))


cb = pi.callback(HC_SR04_echo, pigpio.EITHER_EDGE, cbf)
pi.gpio_trigger(HC_SR04_trig, 10, 1)  # Trig (10μs pulse)
time.sleep(0.1)  # wait for echo signal for 100msec (enough..., I believe...)

pi.stop()
```


# おわりに
ラズパイでの正確なパルス生成や、パルス測定は、現実的ではないと思っていましたが、pigpiodという素晴らしいサービス(デーモン）と、関数群のお陰で、実現できることが体験できたのは、自分にとっては良い驚きでした。　サーボモータの制御も完璧に安定して出来るレベルの PWMを出力できますし、エアコンリモコンのような長い赤外線波形も問題なく検出でき、また生成し出力できます。

C用のインターフェースライブラリも充実しており、しばらく、このサービスを使って遊んでみようと思います。node-REDに一般的に使う gpioのパレットは今後使わず、極力 pigpiodに統合していく予定です。

なお、今回の距離測定は、340m/秒という定数で距離を求めましたが、実際にはこの値は温度係数を持っているので、温度センサーで測定した温度をもとに、補正する事により、より正確な距離を求められると思います。時間がある時に、調べて試してみるつもりです。


# node-REDでのflow
HC-SR04以外のセンサー(BME280, SHT31, HDC1080)も含めて、node-REDで全体を管理するようにしました。大したフローではありませんが、ご参考まで。今回実験した sr04.py は、図の一番下の execノードから、10秒に一回実行され、debugウインドウに jsonオブジェクトとして、結果を表示しているだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266665/1b7f73b3-0067-2c4a-a43f-d65830690d16.png)

```flow.json
[{"id":"a03baba4.3f9348","type":"tab","label":"フロー 1","disabled":false,"info":""},{"id":"32a6ead2.729856","type":"pi-gpiod out","z":"a03baba4.3f9348","name":"","host":"localhost","port":8888,"pin":"4","set":"","level":"0","out":"out","sermin":"1000","sermax":"2000","x":530,"y":100,"wires":[]},{"id":"5d96dabf.e6b294","type":"inject","z":"a03baba4.3f9348","name":"1","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":210,"y":100,"wires":[["3930e4bb.0947fc"]]},{"id":"3930e4bb.0947fc","type":"function","z":"a03baba4.3f9348","name":"","func":"msg.payload ^= 1;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":370,"y":100,"wires":[["30c37801.e427a8","32a6ead2.729856"]]},{"id":"30c37801.e427a8","type":"delay","z":"a03baba4.3f9348","name":"500ms","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":360,"y":60,"wires":[["3930e4bb.0947fc"]]},{"id":"50c0eb9.848b614","type":"Bme280","z":"a03baba4.3f9348","name":"","bus":"1","address":"0x76","topic":"bme280","extra":false,"x":540,"y":240,"wires":[["4e3270f3.bf042","3d52960e.1f174a","7648f6d9.71abb8"]]},{"id":"1cd64330.b2bb3d","type":"inject","z":"a03baba4.3f9348","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"10","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":240,"wires":[["50c0eb9.848b614","9590ccfc.c5ac9","61958cf7.9b2954","53267a93.8f0324"]]},{"id":"4e3270f3.bf042","type":"debug","z":"a03baba4.3f9348","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":810,"y":240,"wires":[]},{"id":"9590ccfc.c5ac9","type":"exec","z":"a03baba4.3f9348","command":"python3 -u /home/pi/Public/python/sht/sht31.py","addpay":false,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"SHT31","x":400,"y":340,"wires":[["f3d35b8e.0553f8"],[],[]]},{"id":"f3d35b8e.0553f8","type":"json","z":"a03baba4.3f9348","name":"","property":"payload","action":"obj","pretty":false,"x":550,"y":320,"wires":[["4e3270f3.bf042","9edc8a14.d3fb38","b34c115.020e2f"]]},{"id":"61958cf7.9b2954","type":"exec","z":"a03baba4.3f9348","command":"python3 -u /home/pi/Public/python/hdc/hdc1080.py","addpay":false,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"HDC1080","x":400,"y":460,"wires":[["a274fdc4.2ebc9"],[],[]]},{"id":"a274fdc4.2ebc9","type":"json","z":"a03baba4.3f9348","name":"","property":"payload","action":"obj","pretty":false,"x":550,"y":440,"wires":[["4e3270f3.bf042","45f88bc0.caf3d4","4079567a.ca44b8"]]},{"id":"35c5e80b.64be18","type":"ui_chart","z":"a03baba4.3f9348","name":"温度","group":"2e025274.2bd5de","order":1,"width":12,"height":7,"label":"温度","chartType":"line","legend":"true","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"3","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1050,"y":360,"wires":[[]]},{"id":"3d52960e.1f174a","type":"change","z":"a03baba4.3f9348","name":"温度","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.temperature_C","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"BME280","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":320,"wires":[["35c5e80b.64be18"]]},{"id":"9edc8a14.d3fb38","type":"change","z":"a03baba4.3f9348","name":"温度","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.temperature","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"SHT31","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":360,"wires":[["35c5e80b.64be18"]]},{"id":"45f88bc0.caf3d4","type":"change","z":"a03baba4.3f9348","name":"温度","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.temperature","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"HDC1080","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":400,"wires":[["35c5e80b.64be18"]]},{"id":"7648f6d9.71abb8","type":"change","z":"a03baba4.3f9348","name":"湿度","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.humidity","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"BME280","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":460,"wires":[["dd1a5041.7ed54"]]},{"id":"b34c115.020e2f","type":"change","z":"a03baba4.3f9348","name":"湿度","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.humidity","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"SHT31","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":500,"wires":[["dd1a5041.7ed54"]]},{"id":"4079567a.ca44b8","type":"change","z":"a03baba4.3f9348","name":"湿度","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.humidity","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"HDC1080","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":540,"wires":[["dd1a5041.7ed54"]]},{"id":"dd1a5041.7ed54","type":"ui_chart","z":"a03baba4.3f9348","name":"湿度","group":"2e025274.2bd5de","order":1,"width":12,"height":7,"label":"湿度","chartType":"line","legend":"true","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"3","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1050,"y":500,"wires":[[]]},{"id":"53267a93.8f0324","type":"exec","z":"a03baba4.3f9348","command":"python3 -u /home/pi/Public/python/sr04/sr04.py","addpay":false,"append":"","useSpawn":"false","timer":"","oldrc":false,"name":"HC-SR04","x":400,"y":640,"wires":[["ae4a3b00.587498"],[],[]]},{"id":"203bb731.6e7e58","type":"debug","z":"a03baba4.3f9348","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":810,"y":620,"wires":[]},{"id":"ae4a3b00.587498","type":"json","z":"a03baba4.3f9348","name":"","property":"payload","action":"obj","pretty":false,"x":550,"y":620,"wires":[["203bb731.6e7e58"]]},{"id":"2e025274.2bd5de","type":"ui_group","z":"","name":"デフォルト","tab":"c8b4aecd.15eec","order":1,"disp":true,"width":12,"collapse":false},{"id":"c8b4aecd.15eec","type":"ui_tab","z":"","name":"ホーム","icon":"dashboard","disabled":false,"hidden":false}]
```


# 参考にさせて頂いた記事

+ [室内タンクの灯油残量をIoTで監視する](https://qiita.com/y_k/items/750be0bf8d282c16d2bb)
+ [Raspberry Pi 3でpigpioの使用](https://tomosoft.jp/design/?p=8768)
+ [Raspberry Pi3でpigpioライブラリを使ってLチカする](https://qiita.com/yuuri23/items/597fd1a40c63627e59c2)
+ [Raspberry PiのGPIO制御の決定版 pigpio を試す](https://karaage.hatenadiary.jp/entry/2017/02/10/073000)
+ [Raspberry Pi用pigpio Library - その2　[Raspberry Pi]](https://decafish.blog.ss-blog.jp/2016-10-16)
+ [Donkey Carに超音波距離計測センサを搭載する](https://github.com/coolerking/donkeypart_hc_sr04)

ありがとうございました。