2020年7月14日火曜日

DHT22のCRCエラーが発生した様子

いろいろとグダりながらもざっくりと変更していて少しだけ見えてきた部分があります。
現時点での結論を言ってしまえば、原因は実行時の何らかのウェイトによってうまく信号を拾えていない感じ。

そもそもpythonでリアルタイムっぽい処理が書けててすごいなぁと感心してただけなんですが、DHT11で使用していたライブラリっぽい物も大雑把にしか見ていなく細かく追って行ってようやくつかめた感じです。
肝心のデータ信号の受信部分がループを使ったソフト的なカウントのサンプリングを行ってそこからビットを起こすということをやっているのですが、そのサンプリング処理はループに依存してました。時間軸とは異なっているために、ループ自体のサイクルが一定ではない場合データ受信で失敗するということです。
しかも、時間を無視しているために、エラー原因もいくらバッファを見ても原因の究明ができません。
良くも悪くも現状1ループ10μs以下では動いているので、サンプリングレートは10μs程度はあるようです。(Raspberry pi 3b+)処理時間がどの程度変わってしまうか正直わからないままごりっと書き換えてみました。
まずはデータフォーマットも単なるサンプリングから信号が切り替わるまでのループ回数を記録するように変更。これだけで配列への格納タイミングがざっと10分の1程度にはなるはずです。
実測値は 1508個から82個へ処理速度が上がれば上がるほど元々の方はデータが増えますが、変更後はバーストや取りこぼしなどで変動することはあっても極端に巨大化することは無いはずです。(変更後のバージョンは初期の信号状態を保持する必要があると思ったので、本来は81個のはず)

続いてループカウントになっている部分を時間へ変更します。
μ秒単位の時間操作がまともにできるか不安を抱えつつ、time.time()でμ秒を扱うことに。さらに、配列をarray.array('B')で1byteのデータとしてみました。(python3ではint扱いになっているので、1データあたりの消費量は4バイトだとは思います。('B')を指定したため、オーバーフローや範囲エラーが出るので想定外の事態になるとエラーで落ちるというおまけつきです(笑)
データーフローを見ると時間は最大でも80μ秒程度なので255μ秒以上は格納することが無いという前提です。
その結果がこちら
array('B', [1, 29, 81, 54, 52, 26, 52, 26, 54, 24, 53, 26, 54, 26, 51, 26, 54, 73, 53, 26, 66, 74, 52, 26, 54, 73, 52, 74, 52, 75, 52, 73, 54, 73, 54, 71, 68, 26, 54, 24, 54, 26, 56, 23, 53, 26, 51, 26, 54, 26, 52, 73, 66, 26, 54, 24, 55, 24, 55, 73, 54, 72, 53, 73, 55, 73, 52, 25, 64, 73, 54, 73, 54, 74, 52, 26, 54, 24, 54, 25, 54, 26, 52, 26, 45])
最初の1個目の1は信号線が初期状態でHIGH状態だったことを示し、その後からは切り替わるまでのμ秒時間となっています。数字を見る限りそんなに悪くない感じで入っていて、3B+のpython3でもそれなりに処理が行える感じがしました。
このサンプリング結果からビットパターンの信号のみを抜き出している部分があるのですが、もともとトリッキーな部分があったのですが、格納データを変えたのですこしすっきりしました。
データ開始の信号を除去してHIGH状態のビット情報だけのサンプリングデータは
[26, 26, 24, 26, 26, 26, 73, 26, 74, 26, 73, 74, 75, 73, 73, 71, 26, 24, 26, 23, 26, 26, 26, 73, 26, 24, 24, 73, 72, 73, 73, 25, 73, 73, 74, 26, 24, 25, 26, 26]
となります。基本的にはLOWデータは50μs前後の数値を取り除いた部分となります。
逆に取り除いた数字を並べてみましょう…手作業ですがw
52, 52, 54, 53, 54, 51, 54, 53, 66, 52, 54, 52, 52, 52, 54, 54, 68, 54, 54, 56, 53, 51, 54, 52, 66, 54, 55, 55, 54, 53, 55, 52, 64, 54, 54, 52, 54, 54, 54, 52, 45
結構疲れましたが、こんな感じ
最低は最後の45ですが、これはデータが終了した後のLOW信号なので正直関係ありません。
そうすると、範囲は51~66μsで、サンプリングサイクルが数μsということを考えれば悪くない範囲だと思います。
ここの処理以降はまた手を入れていませんが、データビットの0か1かを判断するためにサンプリングデータの最小値と最大値から閾値を決めてビット列を作成し、5byteのデータを組み立てていました。もともとはループカウントなのでこのようなことをやっていますが、サンプリング単位がμsになっているので、もうすこし決め打ちしても大丈夫なような気はします。ただスループットが低いRaspberry pi zeroとか2bとかを考えるともう少し様子を見ないとダメですね。zeroは欲しいと思いつつ、結局まだ買っていない…。

ここまできてようやくCRCエラーやビット落ちの様子が判断できるようになります。

何度かコンソールから連続してセンサー値を拾っていると、結構な確率でエラーが発生するようになります。
その時に発生したCRCエラーの様子。
pi@rasp3bprs:~ $ python3 ./setup/sbin/dht22.py --pin 6 --debug
pin : 6
Output for debugging.
array('B', [0, 74, 79, 55, 24, 55, 23, 54, 28, 52, 24, 54, 27, 54, 23, 53, 76, 51, 28, 67, 71, 54, 71, 54, 28, 54, 23, 54, 76, 50, 28, 54, 72, 54, 72, 67, 28, 54, 23, 54, 28, 54, 23, 54, 24, 56, 24, 54, 28, 54, 71, 67, 28, 50, 28, 51, 28, 51, 77, 50, 28, 54, 72, 54, 23, 54, 25, 63, 76, 54, 71, 54, 71, 54, 27, 54, 23, 54, 28, 54, 71, 54, 23, 49])
[24, 23, 28, 24, 27, 23, 76, 28, 71, 71, 28, 23, 76, 28, 72, 72, 28, 23, 28, 23, 24, 24, 28, 71, 28, 28, 28, 77, 28, 72, 23, 25, 76, 71, 71, 27, 23, 28, 71, 23]
the_bytes
02 cb 01 14 e2 ← これは正常なデータですが、初回なので破棄されますw
array('B', [0, 58, 88, 48, 25, 59, 25, 56, 25, 49, 25, 56, 25, 56, 24, 55, 76, 49, 24, 100, 39, 56, 77, 46, 24, 56, 32, 45, 77, 55, 24, 56, 65, 56, 77, 66, 24, 56, 24, 56, 25, 56, 24, 56, 24, 49, 25, 56, 24, 56, 66, 76, 24, 56, 24, 48, 24, 56, 77, 55, 24, 56, 66, 56, 24, 56, 28, 56, 76, 55, 76, 45, 75, 56, 24, 56, 24, 55, 25, 56, 66, 56, 24, 46])
[25, 25, 25, 25, 25, 24, 76, 24, 39, 77, 24, 32, 77, 24, 65, 77, 24, 24, 25, 24, 24, 25, 24, 66, 24, 24, 24, 77, 24, 66, 24, 28, 76, 76, 75, 24, 24, 25, 66, 24]
the_bytes
02 4b 01 14 e2 ← CRCエラーが発生しているデータ
CRC Error.
2nd. retry. 2
array('B', [0, 50, 76, 48, 36, 49, 25, 56, 25, 49, 25, 56, 24, 56, 24, 56, 76, 48, 25, 67, 77, 56, 66, 56, 24, 57, 32, 46, 77, 45, 35, 45, 76, 56, 66, 66, 35, 45, 35, 46, 24, 55, 24, 55, 24, 58, 24, 55, 24, 55, 76, 65, 24, 55, 24, 48, 35, 45, 76, 55, 24, 56, 66, 55, 24, 55, 28, 115, 31, 46, 66, 55, 76, 56, 24, 55, 24, 55, 26, 56, 65, 56, 24, 45])
[36, 25, 25, 25, 24, 24, 76, 25, 77, 66, 24, 32, 77, 35, 76, 66, 35, 35, 24, 24, 24, 24, 24, 76, 24, 24, 35, 76, 24, 66, 24, 28, 31, 66, 76, 24, 24, 26, 65, 24]
the_bytes
02 cb 01 14 62
CRC Error.
2nd. retry. 2
array('B', [0, 62, 85, 57, 24, 57, 24, 45, 35, 48, 25, 55, 25, 55, 24, 56, 76, 47, 24, 65, 75, 55, 76, 55, 24, 55, 30, 46, 76, 55, 65, 54, 24, 55, 24, 66, 35, 44, 34, 45, 34, 45, 35, 45, 34, 48, 24, 56, 24, 55, 76, 65, 24, 55, 24, 58, 24, 55, 66, 56, 24, 56, 75, 55, 24, 55, 27, 56, 76, 55, 75, 45, 76, 55, 24, 55, 24, 55, 25, 55, 76, 45, 76, 46])
[24, 24, 35, 25, 25, 24, 76, 24, 75, 76, 24, 30, 76, 65, 24, 24, 35, 34, 34, 35, 34, 24, 24, 76, 24, 24, 24, 66, 24, 75, 24, 27, 76, 75, 76, 24, 24, 25, 76, 76]
the_bytes
02 cc 01 14 e3
DHT22 results.
Retry count : 2 time(s)
Error code  : 0
Temperature : 276 C
Humidity    : 716 %
なんのこっちゃというような結果ですが、気温と湿度が10倍されたままというのは置いておいて、最初に正常にデータを得られているのですが、どこかに「センサー値の現時点での値を取得するためには複数回データを取得する必要がある」と書いてあった記憶があり、破棄しています。記述された資料が見当たらないので昨日この部分を外して様子を見ていたら、しばらく放置して2回センサー値を表示させたら1回目と2回目で気温が1℃以上変わったので、スリープ直後はスリープ前のセンサー値を返す感じなのだと思います。
で、問題なのが、DHTが悪いのか、Raspberry piが悪いのかわからない部分で、連続でデータを取得するとエラーが発生しやすい。でも連続で値を取得しないと現時点でのセンサー値が分からないというジレンマに陥ります。
ま、どうでもいいんですがw

で、本題の異常値を探ってみると、2つ目のサンプリングデータで100μsとかある部分があります。3つ目では115μsとなっている部分があり、おそらくこの時に何らかの原因で処理が止まってしまったのだと思われます。
今回は運良く?LOW信号がバーストしたような感じになっていてHIGH信号のビットが1になるべきものが0になっただけと考えられます。(気色塗り部分)

もう一つのデータ落ちに見えてしまったパターンはおそらくHIGH信号がバーストしてしまったようになってしまい、結果としてLOW信号への切り替わりのタイミングを取りこぼした結果だと推測しています。
さてどうでしょうか…

array('B', [0, 70, 76, 56, 27, 54, 21, 54, 26, 54, 29, 49, 26, 54, 26, 54, 73, 54, 22, 71, 70, 54, 75, 54, 25, 49, 25, 57, 27, 54, 26, 49, 77, 54, 26, 65, 24, 55, 26, 54, 26, 53, 26, 49, 26, 54, 29, 48, 26, 53, 76, 65, 26, 54, 25, 55, 24, 54, 72, 54, 26, 49, 76, 54, 26, 54, 71, 61, 77, 54, 71, 183, 30, 49, 72, 55, 26, 49, 78, 52, 27, 43])
[27, 21, 26, 29, 26, 26, 73, 22, 70, 75, 25, 25, 27, 26, 77, 26, 24, 26, 26, 26, 26, 29, 26, 76, 26, 25, 24, 72, 26, 76, 26, 71, 77, 71, 30, 72, 26, 78, 27]
pull up lengths Error. len : 39
ログから漁ってみましたが、こんなデータです。
パット見あまり変なところはありませんでしたが、赤塗したところが怪しそうな気がします。信号的には60μsを超える波形はデータ開始時のみでデータ中には現れることはありません。なのでここの間にHIGH信号を完全に取りこぼしているのかな?と推測しました。
とおもったらもっと怪しいのが183とかトンでも数字がありましたwよって正解は、HIGH信号のバーストではなく、LOW信号バースト、正確には単なる「HIGH信号の取りこぼし」でした(笑)

といった具合に、悪者がはっきりしました。Raspberry pi で動いている環境でした(´-ω-`)

Raspberry pi 4bでDHT11を動かしたときにエラーが発生しにくい気がするというのは気のせいではなく、処理速度が上がってスクリプトの実行が安定した結果だった様です。

あと気づいたのは、正常時はビット信号が綺麗にサンプリングできている感じなのに対して、エラー時は全体的にサンプリングにばらつきがあるように思えます。70μsを越えているデータが頻発している。



データシートに詳しくはホームページ見てね的なことが書かれていたので見てみると、センサーの一押しが、DHT10とか外見はあまり変わらずI2C仕様のDHTが…マジかw早速アマゾンで見てみると、3個セットで2400円ぐらいだったかな…微妙な値段だけど、悪くなさそう?

配列への追加についてグダる

時事ネタは突っ込み所が多すぎて全然関係のない話題が続いていますが、今回もそんなネタ。

最近のスクリプトや言語は型や配列に関してものすごい自由度が高いのですが、その代償として何かしらの犠牲を伴っています。

私が言語に最初にどっぷりと漬かった言語はturbo-pascalなのですが、その原因はどうしても細かい所まで見て実装を確認しないと処理が上手く回らなかったり、処理時間を短くするために内部でどのように動いているのかまで見ないといけなかったからだと思います。
あと付属するマニュアルが結構読み応え満点で実際にそうなっているかどうかは別にして、結構突っ込んだ事まで解説されてたりしました。日本語版なので訳者の思い入れが加味されていたのかもしれませんが。

基本的な考え方として、配列は単なるメモリ空間なので、最初に領域を決めたら拡張することができません。動的に変動させるためにはどうするか?というところで実装の仕方は色々な考え方でうまく乗り越えているだけのこと。
追加に対して代償を減らすには領域確保で指定された領域以上に予め確保することで時間的リソースの節約を行います。これはどんな形の配列でも変わりません。繰り返すことでメモリー空間に分断された状態になると、どうしてもアクセス効率が落ちるので、最終的にはガーベージコレクトや再生成するなどして綺麗な状態に保つ必要があるかもしれません。とはいっても、すでにCPUキャッシュやらなんちゃらキャッシュやら、広大なメモリー空間のおかげで実害は出てこないのも事実。ただおまかせ状態だとそれなりに何らかのリソースの浪費はしていると思います。

一番非効率なやり方は、新たに配列全体の領域+追加分の領域を確保して、配列の内容のコピーを行って不要になった領域を解放するという方法ではないでしょうか?これの代償としては配列に対してメモリアドレスなどで直接参照しているものがある場合、参照している部分も変更する必要があるので、結構な処理負荷が出てきたり、放置すると簡単に処理が落ちることになります。
これを結構積極的に行っている言語というかシェルというか物が、おそらくAndroidだと思います。基本的な考え方として、すべてのオブジェクトインスタンスは最悪破棄して再生成すれば元通りという発想のもと、リソース不足になると積極的に破棄したり、どこかに追いやられてしまいます。最近のバージョンではかなりうまい仕組みが導入されて改善されていますが、その辺がどうしても好きになれない場合は苦痛しかないかも。
とはいえ、非効率なやり方の利点として、追加しても常に新規に領域確保された状態になるので配列へのアクセスでまずおかしなことは発生しません。配列の要素へのアクセス速度も常に変わらない状態でアクセスできるため、タイミングの影響を受けるようなこともまず発生しません。

で、なんでこんなたかが配列への要素の追加というくだらない話をだらだら書いているのかといえば、pythonスクリプトで結構安易に .append()で要素の追加を行っている部分を見て、最近気にしたことはなかったけどこの辺どうなのかな?とちょっと突っ込んでみたところ、やはり結構処理的に割高な処理なんだと再認識したのでグダりましたw
たしかに便利なんですけどねw処理時間がμ秒単位の場合はやめた方が良いかも。どうしてもという場合はpythonの場合はarray.array('b')とか、要素の追加を減らす処理に変更するとか、そもそも要素の追加をさせない形にするとか、仕様を見直した方がいいんじゃないかな?

実際にどんな処理でこの辺の処理時間が気になっかと言えば、DHTxxのライブラリっぽいやつで、エラーが発生したときにどんな状態で発生しているのか気になったので色々と書き換えて気になりました。
どのくらいのインパクトがあるかといえば…raspberry pi 3b+でループでGPIOピンの状態をサンプリングしている処理があるのですが、そこでループのタイミングで配列に要素を追加してサンプリングしていたときは、最大25回ぐらいだったのですが、これを信号が変動した時だけにした場合、最大30回ぐらいになりました。「ただし」が付くのですが、raspberry piで使用しているCPUはマルチコアとなっていてメインとなるプロセッサー0とその他とで処理能力の差があるようで、25回→10回、30回→10回ぐらいに簡単に落ち込みました。処理が落ちてる状態なら変わりないということではなく、おそらく不利になるのがGPIOなどのIO周りのウェイトがプロセッサーによって異なっているためだと思います。逆に言えば、現状ではメモリアクセスの時間リソースへの影響が小さくなったという結果なのかもしれませんw(結局何が言いたいんだ(;´д`)

2020年7月13日月曜日

DHT11とDHT22

色々と思うところがあってセンサーの選定をしていましたが、安価で簡単に手に入りそうなDHT22をポチって見ました。2個セットで1400円。一個700円とか3個セットのBME280より高価ですね…。

で早速手持ちのソースをデータシートをみつつ変更したところ、いきなりうまく行かず…すこし試行錯誤した結果、開始時の手順はDHT11の時と同じにするのが安定しました。データシート上だと細かく記載されていて手順が増えているようにも思えたのですが、結果としてpull upされた状態からダウン状態にした後、アップして待機するだけでデータが送られてきました。

それなりにDHTの共通ライブラリっぽいソースも見てみましたが、DHT11とDHT22とではデータの加工方法が違うだけのようでした。

今回DHT22を選んだ理由として、小数点以下の数字が取得できるっていうのがポイントだったのですが、これは今日発覚したDHT11でも温度の小数点が取得できたということで半減してしまいました…。(解像度が16bitで温度が整数のみってどう考えてもおかしいだろという突っ込みはさておきw)

気を取り直してスペックを見てみましょう。

DHT11DHT22
温度範囲0~50℃
-20~60℃
-40~80℃
温度解像度16bit16bit
温度レスポンス10s10s未満
湿度範囲20~90%RH
5~95%RH
0~99.9%RH
湿度解像度16bit0.1
湿度レスポンス6s5s未満
電源電圧3.5~5.5V3.5~5.5V
消費電力0.3mA8mA
サスペンド時60μA10~20μA
スリープまでの時間2s2s
上記は各データシートからの抜粋値で、レスポンスに関してはデータシート上に前提条件として1/e(63%)とか25℃でとか書いてあるので、センサー素子から値を得るのに何度も試行する必要があるもののようです。範囲が広がっているDHT22の方が消費電力が上がっているのもこの辺が影響してそうです。

正直、アップデート後のDHT11なら一般使用は問題ないんじゃというのはさておき、気になる違いはとしてDHT22の優れている点は温度、湿度の範囲が少し広いところでしょうか。DHTの仕様特性として、ほとんどの時間をサスペンド状態で放置されるはずなので、サスペンド時の消費電力が低くなっているのもいい感じです。
デメリットとして消費電力が数字上結構上がっているところ。データシート上はturn onと書いてありましたが、絶えず値をとり続けるのであれば、DHT11は結構優位ですね。

見直せば見直すほど、DHT22を買った利点がないわけですがw



関係ないところで、Raspberry piの放熱やセンサー自体の放熱もあるようで、DHTシリーズはエラー時のリトライで取得できる温度が少し上ってしまう感じがします。手で連続で値を取得すると温度がすこし上がることが多いのもおそらくセンサー自体の発熱があるためのような。

DHT11の温度は小数点まで持っていたw

錯乱状態が少し解消され様子を見たところ、
16時過ぎぐらいからグラフの形が矩形からいい感じになりました。
今までは小数点以下は無いと思っていたので、ここで100均の基準温度計に表示を合わせてみます。
小数以下がカットされていたという感じの重なり具合に。

すでにDHT22を買ってしまったんですが、温度の小数部があるなら、基準センサーはDHT11でもよかったかも…(==;

仕様を呑み込めなく錯乱へ…

昨日電車の中で、スマホでデータシートやソースを見ながら頭の中を整理してかなりの部分がすっきりしたのですが、困ったことにDHT11の仕様で勘違いしていた部分があったようで…

そもそもの起点に戻って整理してみようかと。

まず元となったpythonのソースがどこだったのか謎だったのですが、おそらく
ここだったと思われ。再度アクセスしたらgitサーバーが落ちたのかアクセスがエラーに。
で、経緯としてはその後、修正が加わって大きな違いは
「温度と湿度ともに小数部を加味するようになった。」
という点。
で、DHT11自体は仕様がアップグレードされセンサーの感度範囲が広がったこと。
湿度は 20-90%RHから5-95%RH
温度は 0 to 50 ℃ to - 20 to 60 ℃

大きな違いは温度がマイナス温度から検知できるということ。

で、アップデート後のドキュメントで温度に関しての説明で
The temperature is high as part of the data of the temperature.
The temperature is low as part of the data of the temperature, and the temperature low bit 8 is the negative temperature, otherwise the temperature is positive
と書かれていて、あんまり意識していなかったのですが、これをグーグル翻訳にかけてみると…
気温のデータとして気温が高い。
温度データの一部として温度が低く、温度の低いビット8が負の温度で、それ以外の場合は温度が正
意味不明すぎなのですが、原文を見直せば見直すほど意味が理解できなくなりました(*´Д`)
もともと英語力なんて無いのであれなんですけどw
英語圏の人はこれで理解できるのかな?

で、色々と考えた結果、アップデート後のv1.3では「温度がマイナスの場合は、温度の下位バイトの8ビット目が1になる」というとらえ方でいいのかな?どのタイプのDHT11を手にしているのかもわからないのであれなんですが、、、現状の上記のgitのソースを見ると、
humidity = the_bytes[0] + float(the_bytes[1]) / 10
となっていて、pythonで 0b10000000ってなっててもマイナスの扱いなんてしないし、そもそも足しこんでいるのですっげーやばいと思うんですよね。
そもそも「/10」っていう部分が気になる人には気になるかもw
pi@rasp4b4g:~ $ python3
Python 3.7.3 (default, Dec 20 2019, 18:57:59)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0b10000000
128
>>> float(0b10000000)
128.0

こんな感じでfloatしても、こんなことに。 仕様上は下位バイトは0~9までしか格納されないのですが、マイナスの場合は符号ビットを取り外さないとイケないわけで。
humidity = (the_bytes[0] + (the_bytes[1] & 0x7F)*0.1)*(-1if(the_bytes[1] & 0x80)else(1))
とか、そんな感じにする必要があるような?

v1.3のセンサーも無いし、データシートのデータサンプルに気温4024℃とパリティーエラーしか載ってないので正確なことは言えませんが。

他にもなんか色々といままで見かけた仕様も矛盾してしまってたりしてほんとに錯乱状態だったので綺麗さっぱりと忘れることにしよう。

この辺が気になりだした原因は、そもそもDHT11の仕様で湿度、温度ともに整数値しかないと思っていたところ、受け取った4バイトのデータを実際に表示して気づいたという。
しかも、decimalを10進と翻訳されて、データフォーマットが回路用に設計されてるのかな?とか勘違いしてたのが原因でした。実際のデータを表示させてその勘違いに気付いた次第です。




そもそもなところで、センサー誤差を拾うために結局DHT22も入手して只今思案中w