公開日:2019/08/31
title.png

7th Season


#7-3 Raspberry Piで気温・湿度センサを読み出す

 前回環境を立ち上げたRaspberry Pi ZeroWを使って、気温・湿度センサからデータを読み出してみようと思います。
 筆者は、vi使いで、csh使いで、awk使いなので、まずは、awk(またはcsh)スクリプトを使っての制御にトライしてみようと思います。

気温・湿度センサ

 以前何度か使用した、AM2302または、DHT11モジュールを使います。それぞれ、以下の記事をご参照ください。

  AM2302:「第6回 気温と湿度の測定」、等、Season 1全般
  DHT11:「#2-4 二代目のスタータキット購入

 Raspberry Pi による電子制御の例題として、AM2302/DHT11を選んだのは、独自仕様の1線式シリアル通信が必要だったからです。
 UART/SPI/I2C等、規格物の通信手段であれば、Raspbianのデバイスドライバを叩いて比較的容易に通信可能だと思うのですが、AM2302/DHT11は、独自仕様の1線式シリアル通信なので、ソフトウェアでI/Oポートの状態を細かく制御しないと、データを受信できません。

 AM2302/DHT11との通信仕様(波形など)については、「第6回 気温と湿度の測定」をご覧ください。同ページから、ビット値"0"および"1"の通信タイミング図を再掲載します(以下図)。

ar6_bit01.png

 ビット値0/1とも、LOW幅は変わらず50μsで、HIGH幅が"0"だと26μs、"1"だと70μsとなります。つまり、AM2302/DHT11が送信してくるシリアルデータから、ビット値0/1を読み取るためには、少なくとも26μsのパルス幅をサンプリングして、かつ、26μsのHIGHパルスと70μsのHIGHパルスを区別できることが必要となります。

 Arduino UNO(@16MHz)では、余裕でした。16MHzのATmega328ですので、1命令当たりの実行時間は1/16MHz=62.5nsです。I/Oポートのサンプリングと、LOW/HIGHの判定、時間の計測を含めた計測ループが、仮に100命令かかったとしても、高々100命令×62.5ns=6.25μsですので、まだ26μsのHIGH幅をサンプリングするのに余裕があります。
 きちんと命令数を勘定していないので何ですが、そもそも、100命令もかかっていないはずなので、もっと余裕を持っているはずです。

 ところが、Raspberry Pi ZeroW(ARM11 @1GHz)では、もしかしたら、厳しいかも?と、言うのが、心配事です。と、いうのは、OSのせい。今から作成する、AM2302/DHT11通信用のプログラムだけが、全CPUタイムを使って動作可能なら、ATmega328 @16MHzにできて、ARM11 @1GHzにできないはずは無いのです。クロックスピードだけみても60倍以上の能力を持っているのですから。

 問題は、「全てのMCUリソースは僕の物」だったArduinoに対して、Raspbianの場合、AM2302/DHT11の電子制御を実行するアプリケーションプログラムの他にも、OSの処理(タスクスイッチ等)やインストールしてある様々なサービス類が同時並行で処理されているからです。
 アプリケーションプログラムの実行に割り当てられるCPUタイムがどの程度の割合で、かつ、最長どのくらいの時間間隔で実行されることになるのか?そこが定かでないのが、RTOSではない、Raspbianの最大の欠点なので、この点を見極める必要があります。

※すごく単純に考えると、アプリケーションプログラムの他に、同時並行で実行されるプログラムが60個存在したと仮定して、全てのプログラムが同じ優先順位で満遍なくCPUタイムを割り当てられたとすると、1個のプログラムが使用可能なCPUタイムは1/60になるので、1GHz/60≒16.7MHz相当に低下します。ここへさらに、OSのタスクスイッチなどのオーバヘッドがかかるので、恐らく、16MHz相当を下回ることになるでしょう。

※更に、OSによって、一度CPUタイムをはく奪(タスクスイッチ)されてから、次に再びCPUタイムを割り当てられるまでの時間も、重要です。この時間が26μsを超えるようでは、HIGHパルスを計測しようとした瞬間にCPUタイムをはく奪されると、次に制御が戻って来た時には、既にHIGHパルスは消えてしまっています。

※「最低でも、10μsの内1μsは、タスクにCPUタイムを割り当てる」等と、言うことを保証してくれるOSでない限り、上の問題は解決できませんが、Raspbian(Linux)にはそのような機能がありません。

 「RTOSではないRaspbianで、μsオーダの電子制御ができるかどうか?」、これが今回のメインテーマ。

スクリプト言語

 事前調査によると、何やら、Raspbianでのプログラミングは、Pythonとか、JAVAとかが主流(?)みたいです。確かにね。GUIとか使ったアプリケーションを組むなら、Cを使うよりずっと楽に組めますから。
 本HP(Arduino DIY)では、電子工作(電子制御)をすることが目的で、RaspbianのGUIを使ったアプリケーションを組むのは想定外です。PythonもJAVAも使ったことが無いし、今から覚える気もさらさらないので、使い慣れたawkを使ってプログラミングしてみたいと思います。

 まずは、26μsのパルスを計測できる必要があるので、これが可能かどうか?

時間の取得

 awkで、millis()やmicros()の様に時間を計測できる必要があるのですが、awkにはそのような機能はありません。C言語のライブラリには、clock_gettime()と言う関数があって、特定の基準時間からの経過時間を計測可能です。そこで、clock_gettime()関数を叩くだけのユーティリティコマンドをCで作成して、awkから参照できるようにして見ます。

 前回ご紹介したmanコマンドは、C言語ライブラリも対象なので、【man clock_gettime】と実行すると、clock_gettime()関数のドキュメントを参照できます。clock_gettime()関数をCで使用するために必要な#includeファイルなども表示されるので、とても便利です。

  実験サンプル:【 】内はshellウインドウでのコマンド入力
  【man clock_gettime】
   : clock_gettime()関数のドキュメントが表示される。

  完成プログラム:clock_gettime.cc

  コンパイル:【g++ clock_gettime.cc -o clock_gettime】

  使い方:

 う〜ん、イイネ👍。久しぶりに、main()で始まって、コマンドラインパラメータのCコードを書いたよ。正直、GUIなんて、性に合わないのです(1^o^)/。
 コンパイルしたら、適切にパスの通ったディレクトリにコピーしておいてください。例えば、/home/pi/binRPI/というディレクトリを作っておいて、起動時にパスを通しておくと便利です(.cshrcとか、.bashrcとかで……)。

  実験サンプル:【 】内はshellウインドウでのコマンド入力
  【clock_gettime】
  12436.381992000

  【clock_gettime】
  12438.300449000

  【clock_gettime】
  12439.314357000 …… どうやら、下3桁は、ずっと000?

  【clock_gettime -r】
  0.000000001 …… なのに、解像度は1ns??

  【clock_gettime REALC】
  1565581267.943046019 …… ns単位まで数字が表示されるけど、

  【clock_gettime REALC -r】
  0.010000000 …… 解像度は10ms

 いくつかシステムタイマの参照オプションがあって、基準日時や解像度が異なるので、【man clock_gettime】などで必要なパラメータを確認して使ってください。例えば、上の実験サンプルの様に、デフォルトの「MONOR」は解像度が1nsですが、「REALC」(CはCOARS;粗い)では解像度が10msと粗いです。あと、「CPUTIME」を返す「PROC」と「THRD」は、今回のケースには不適切なので、使用できません。

 あと、ちょっと解せないのが、「-r」オプションで表示される解像度は1nsとなっているのに、表示される数値は、1μs程度の解像度しか持っていないことです。これだとどうも、nsオーダの時間を正確に計測するのは無理みたいです。

※Pi ZeroWのCPUクロック周波数は1GHzで、恐らく、1CPI(Clocks Per Instruction)のCPUなので、1命令の実行時間は1nsです。なので、nsオーダの制御ができるポテンシャルは持っているのに、時間を計測できない様では、厳しいですね……。

awkの実行速度

 clock_gettimeコマンドで、時間を取得可能になったので、Arduinoで言うところの、micros()の様に使用可能です。AM2302/DHT11と通信するためには、gpioコマンドでI/Oポートの状態をポーリングして、パルス幅を測定することになるので、awkでI/Oポートの状態を読み取るコードを組んで、時間を計測します。

 以下、gpioコマンドのreadオプションの使い方です。

  実験サンプル:【 】内はshellウインドウでのコマンド入力
  【gpio -1 mode 11 input】

  【gpio -1 read 11】
  0

 上のサンプルでは、物理ピン番号#11のI/Oポートを入力(input)に設定して、その値を読み出し(read)ました。ポートからはLOWが読めたので、標準出力に0が表示された、と言うことです。#11ピンがHIGHだった場合は、1が表示されます。詳しくは、【man gpio】をご覧ください。

 awkの場合、コマンドの標準出力を取り込むためには、以下の様に記述します。

  "gpio -1 read 11" | getline;
  close ("gpio -1 read 11");  # 上で開いた標準出力をcloseする必要がある。
  val=$0 + 0;


 gpioの標準出力をgetlineにリダイレクトすることによって、gpioの標準出力が$0に代入されます。$0は文字列なので、"+0"することによって、数値に変換して使用します(……文字列のまま扱ってもOKです)。結果、valには数値0または1が入ります。
 clock_gettimeコマンドで時間を取得するのも、同じ方法を使います。

 以下にテスト用スクリプトをリンクしておきます。ダウンロードしてRaspberry Piに転送したら、【chmod ugo+x test_poll.awk】などとして、実行フラグをセットしておいてください。Windows→Raspbianのファイル転送は、VNCから可能です。

  テスト用スクリプト:test_poll.awk

  実験サンプル:【 】内はshellウインドウでのコマンド入力
  【test_poll.awk】
  17175.453416 - 17172.931716 = 2.521700/100 (0.025217)

  【test_poll.awk】
  17179.366149 - 17176.826281 = 2.539868/100 (0.025399)

  【test_poll.awk】
  17184.679617 - 17181.011623 = 3.667994/100 (0.036680)

 筆者の環境では、と言う前提付きで、awkを使ったgpioコマンドによるI/Oポート読み出し1回分の実行時間(上の表示サンプルで、カッコ内の数値、単位:秒)は、約25msでした。これがawk+gpioコマンドでのI/Oポートのサンプリングレートです。何度か実行すると、10数msの幅で値がばらつきます。
 Raspbianで実行中のサービス次第では、異なる結果になることは必至で、十人十色の結果が得られるはずです。
 と、その前に、遅すぎます。前節で解説したように、26μsのパルス幅を計測しなければならないのですが、サンプリングレートが3桁遅い25msでは話になりません。

 念のためcshスクリプトでも試してみました。

  テスト用スクリプト:test_poll.csh

  【test_poll.csh】
  17252.845630 - 17250.432548 = 2.413082/100 (0.024131)

  【test_poll.csh】
  17256.467049 - 17254.356021 = 2.111028/100 (0.021110)

  【test_poll.csh】
  17260.039839 - 17257.754293 = 2.285546/100 (0.022855)

 同じですね、団栗の背比べ……。cshスクリプトの方が少し、速い様子。

 RTOS云々以前に、スクリプト(インタープリタ型)では、全くスピードが足りないようです。1GHzもある高速CPUだったので、ちょっと期待していたのですが、デスクトップのモッサリ感と言い、スクリプト言語でμsオーダの電子制御を実行するには、CPUの力不足みたいです。

やっぱりCなのね……

 Cを使ってゴリゴリプログラムするのも楽しいけれど、結構しんどいので、出来ればスクリプトを使ってサクッとプログラムを組めたら良いな〜と、目論んでいたのだけど、スピードが足りない様では仕方がありません。

 wiringPiライブラリとC言語(gcc)を使ってプログラムを組むことにします。以下、Wiring Pi HPへリンクを貼っておきます。関数のドキュメントもここから参照可能です。

  Wiring Pi HP (GPIO Interface library for the Raspberry Pi)

 wiringPiライブラリを使うと、digitalRead()/digitalWrite()/delay()/micros()等のお馴染みの関数が使えるようになるので、Arduinoから移植するのも、そんなに苦にならないと思われます。

※wiringPiライブラリのインストールについては、前回簡単に説明しておきました。最近のNOOBSでは、デフォルトでインストールされているようです。

 Arduinoのmeas_am2302.inoで使った、pulseIn()関数は、wiringPiライブラリに含まれていなかったので、これだけは自分で起こす必要がありました。
 以下、完成版のプログラムをリンクしておきます。

  完成プログラム:meas_am2302.cc

  コンパイル:【g++ -lwiringPi meas_am2302.cc -o meas_am2302】

  使い方:【meas_am2302 -h】で表示されるメッセージをご覧ください

回路の構築

 手元のAM2302は、1時間毎に室温・湿度のデータロギングに使用していて流用できないので、姉妹品のDHT11モジュールを使います(下図)。

ar24_dht11mdl.jpg

 DHT11のデータシートによると、定格電圧は3.3V(MIN)〜5.5V(MAX)で、5.0V(TYP)なので、5V定格品です。
 Pi ZeroWには、5V電源出力はあるので、電源の供給は問題ありませんが、I/Oは3.3V定格なので、レベルシフタが必要です(下図)。

ar47_lvshft.jpg

 レベルシフタの必要性については、「#4-7 ENC28J60を使う」で解説済みです。
 以下、回路図です。

ar73_schem.png

 DHT11モジュールには、信号線に必要なプルアップ抵抗が載っています。AM2302など、単体で使用する場合には、外付けにプルアップ抵抗が必要となります。今回使うレベルシフタは、オープンドレイン+プルアップタイプなので、上図の様に接続した時点で、信号線SDAは、レベルシフタによってプルアップされていることになります。従って、プルアップ抵抗を追加する必要はありません。
 プルアップ抵抗が付いてしまっているDHT11モジュールの場合には、二重にプルアップされることになりますが、今回の使い方では、特に問題ないと思います。

※量産用に設計するならまだしも、ちょっとお試しだけなら、3.3V電源を供給して、DHT11を定格の下限(3.3V)で使う手も、無きにしも非ず……。いや、独り言。各自の責任にて、お願い申し上げる。

双方向自動判定レベルシフタ

 AM2302/DHT11の1線式シリアル信号線SDAは、双方向の通信線ですので、今回使用したような、双方向自動判定のレベルシフタが必要です。
 手持ちのレベルシフタを双方向信号のレベル変換に使うのは初めてですので、簡単に説明しておきます。調べた範囲では、上で示したオープンドレイン+プルアップタイプのレベルシフタは、以下図のような回路になっているようです。

ar73_lvshft.png

※回路図を見てしまうと、「オープンドレイン」では無いですね。「オープンドレインみたいに、LOWはシンクするけど、HIGHはソースしない」という意味で使っています。

 上図の様なトリッキィな回路で、双方向(3.3V⇒5Vおよび、5V⇒3.3V)のレベル変換ができるみたいです。今回の様な双方向信号線のレベル変換にうってつけです。例えば、I2Cの通信線等も、入出力方向が途中で頻繁に切り替わるので、このような双方向自動判定のレベルシフタが必要になります。

 LOWに引く側は、I/Oドライバがシンクするので問題ないのですが、HIGHに押す側はプルアップに頼っています。プルアップ抵抗が10kΩと大きめなので、例えば低電圧(3.3V等)で使う側は、電流がI=3.3V/10kΩ=330μAしかソースできません。あまり高負荷の駆動は無理です。負荷容量によっては、LOW→HIGH遷移時間が、長くかかる場合があります。
 高速な信号をレベル変換するには、プッシュ・プルタイプのレベルシフタの方が優れています。ただし、プッシュ・プルタイプのレベルシフタの場合、信号の方向制御が必要になる場合があるので、良し悪しです。お手元のレベルシフタの仕様をご確認の上、ご使用ください。

実験、読み出せるか?

 回路を構成して、プログラム(meas_am2302.cc)をコンパイルしたら、実行してみましょう。以下、実験中の様子です。

ar73_expr.jpg

  実験サンプル:【 】内はshellウインドウでのコマンド入力
  【meas_am2302 -req】
  OK,32.0,78.0

  【meas_am2302 -req】
  ERR: ErrReceive
  ERR,-3,-3

  【meas_am2302 -req】
  OK,32.0,78.0

  【meas_am2302 -req】
  OK,32.0,78.0

  【meas_am2302 -req】
  ERR: ErrReceive
  ERR,-3,-3

 何ですか?これは……、エラーだらけ(部屋の不快指数もさることながら……)💦。繰り返し実行すると、ひどいときには、50%くらいの確率でエラーになります。調子の良い時でも、20%くらいはエラーですね。

※DHT11のサイクルタイム2sを満たすため、各々の実行間隔は、2s以上空けてください。例えば【sleep 2 ; meas_am2302 -req】。

 散々デバッグしましたよ〜。どこかプログラムが間違っていないか?回路が間違っていないか?Logiscopeまで持ち出して、DHT11との通信波形(デジタル、アナログ共)もモニタしてみたし。丸一日以上は悩んでデバッグを繰り返した末、ようやく、「筆者のせいではない」と、確信できました。
 最初に述べた通り、今回のメインテーマは、「RTOSではないRaspbianで、μsオーダの電子制御ができるかどうか?」を確かめることにありました。よって、途中から「Raspbianのせいでしょ?」と、疑いながらデバッグしていたのですが、中々尻尾を出しません。

 結局、以下の様なテストによって、ようやく、Raspbianの尻尾を捉まえました。

  テストプログラム:test_poll.cc

  コンパイル:【g++ -lwiringPi test_poll.cc -o test_poll】

  使い方:【test_poll -h】で表示されるメッセージをご覧ください

  実験サンプル:【 】内はshellウインドウでのコマンド入力
  【test_poll】
  param: 4 12 10000
  0, 9955
  1, 15
  2, 1
  6, 1
  9, 1
  11, 2
  19, 1
  21, 1
  24, 1
  25, 1
  52, 1
  60, 1
  76, 1
  99, 18

 このプログラムは、micros()関数で時間を測定しながら、一定時間usec_poll[μs]毎に、実行時間を記録して、その統計データを表示します。具体的処理内容に関してはソース(test_poll.cc)をご覧ください。デフォルトでは、micros()の解像度usec_reso=4μs(Arduino UNOと同じ)になっていて、usec_poll[μs]毎に実行したかった処理が、実際には、どのくらいずれて実行されたか?を知ることができるようになっています。

 上の例では、usec_poll=12[μs]毎に時間を計測しています。「param:」の後ろに表示される第一数値が時間計測の解像度usec_reso[μs]、第二数値が繰り返し目標時間usec_poll[μs]、第三数値が繰り返し回数となっています。
 「param:」の次の行から、カンマ(,)区切りで2つの数値が並んでいますが、第一数値が、<usec_poll時間からのずれ時間>/usec_reso、第二数値は、その時間内に処理した回数(頻度)を示しています。
 当然、理想的期待値は、<ずれ時間>=0の所に、全ての回数がカウントされることです。

 第一数値をusec_reso(デフォルトでは=4μs)倍したものが、目標時間からのずれ時間なので、なんと、99×4μs=396μsを超えるケースも、18回発生しています。実は、ずれ時間が99を超えると、全て99にカウントするようにプログラムしてあるので、実際には、もっと長い時間ずれているようです。
 解像度usec_resoを大きくして、最大どの程度ずれるのか、計測してみます。

  実験サンプル:【 】内はshellウインドウでのコマンド入力
  【test_poll -r 100】 …… usec_reso=100μsで実行
  param: 100 12 10000
  0, 9979
  1, 2
  2, 3
  4, 1
  5, 1
  6, 3
  7, 1
  15, 2
  33, 1
  60, 1
  76, 2
  99, 4 …… まだ、99×100μs以上ずれるケースが4回カウントされている。

  【test_poll -r 1000】 …… usec_reso=1,000μsで実行
  param: 1000 12 10000
  0, 9983
  1, 2
  2, 1
  5, 1
  8, 2
  9, 1
  10, 8
  20, 2

 解像度を1,000μsまで粗くして、ようやく最大ずれ時間が見えました。10,000回の繰り返し中、20×1,000μs=20msずれたケースが2回発生しています。10msずれだと、8回も!
 解ります?12μs毎に何らかの処理を実行し続けようとしたところ、タスクスイッチによって、OSにCPUタイムを剥奪されて、戻ってくるまでに最大20msかかるケースが存在した、と言うことです。

 DHT11からのデータを受信するためには、26μsと70μsのHIGHパルスを計測して、2者を区別することが必要なのに、その処理中に20msもシステムがCPUタイムを取って行ったら、受信失敗して当然です。逆に、「良くぞ、受信成功するケースがあった!」と、褒めてあげたいくらいです。

Arduinoだと、どうなるの?

 同じテストをArduino UNOでも実行してみます。

  テストプログラム:test_poll.ino

  実験サンプル:このプログラムは、実行すると、結果をシリアルモニタに表示します。
  DBG: start test
  param: 4 12 100000
  1, 40759
  2, 57107
  3, 1752
  4, 382

 この結果も、ひどいって言えば、ひどいのですけどね……(1^^)っ。
 前節で解説したように、理想的な期待値は「<ずれ時間>=0の所に、全ての回数がカウントされる」なのですが、そもそも、<ずれ時間>=0の結果が一つもありません(1^^;)。こっちはこっちで、処理能力がちょっと足りないみたい。それでも、ほとんどの場合、2×4μs=8μs前後で実行されています。最大でも、4×4μs=16μs程で実行できているので、これなら、DHT11の送信波形を取り込むことができるでしょう。
 しかも、何度リセットしてやり直しても、全く同じ結果が得られます。毎回きっちり、同じ時間で同じ動作を繰り返していると推測できます。

※Raspbianの方は、実行する度、毎回違う結果が返ってきて、安定しません。……そう言う物です。

Raspberry Piの使い道

 心配はしていたのですが、予想以上に、全くダメでしたね。
 元々、こんなμsオーダの電子制御をするためのOSでは無いですから(→Raspbian;Linux)、DHT11からのデータ受信ができないと言って、責めるのは筋が違うのですよ。

 なんで、Linuxではなくて、RTOS系のOSを載せなかったのだろう??
 Linuxを使いたかったのなら、PCでも何でも動くし、Core何たらで動作させた方が圧倒的に速い。なにも、Raspberry Piで動かす必要は無かったのでは??

 いよいよ、困ったぞ〜。
 そもそも、Arduinoの処理速度に限界を感じて、Raspberry Piに手を出してみたというのに、これでは帯に短し、襷に長し(Arduino、Raspberry Pi共)。


 無いものを求めても、栓無きこと。Raspberry Piの使い方は、改めてよく吟味する必要があります。少なくとも、Raspberry Pi単体では、Arduinoの代わり(置き換え)にはならないことが明白になったし、Raspberry PiとArduinoを連携させて使う(互いの弱点を補いあう)とか、何か考えないと……。


btn_arduino.png

btn_prev.png btn_next.png

home