湿度を正確に測る意味を考えながら その1 相対湿度は温度とともに
湿度センサの表示値はRHxx%と書かれています。相対湿度のことで、温度が±1℃変化すると、絶対湿度が変わらなくてもRHは±3%ずれます。したがって、湿度センサは温度を同時に測って補正した結果を出力しています。エアコンを入れ、50%に湿度と25℃に保っている部屋でも、ドアを開けて人が入れば、制御しているコンピュータは大変です。
湿度が手軽に測れるようになったのはつい最近です。初期のころはセラミックが使われ、汚れて劣化が起こり、寿命が短かった時代がありました。
●湿度センサのカタログ・スペック
入手しやすいセンサの主な特徴を調べました。
型名 | 確度 | センサの材質 | 参考価格[円] | そのほかの測定項目 | インターフェース | 電源電圧[V] |
---|---|---|---|---|---|---|
DHT11 | ±5%RH | ポリマ | 300 | 温度 | 独自シリアル | 3.5~5.5 |
AM2322 | ±2%RH | ポリマ | 700 | 温度 | I2C | 3.1~5.5 |
SHT31 | ±2%RH | CMOS? | 950 | 温度 | I2C | 2.4~5.5 |
SHT11 | ±3.5%RH | ポリマ | 4300 | 温度 | 2線 | 2.4~5.5 |
BME280 | ±3%RH | ? | 1080 | 温度、気圧 | I2C、SPI | 1.71~3.6 |
一つを選ぶとすれば、AM2322もしくはSHT31ですが、AM2322のリードのピッチが狭いので、SHT31を利用します。
SHT31は2.6×2.6mmという小さなパッケージですが、秋月電子通商の変換基板付きは、ブレッドボードに挿して利用できます。また、同社のWebにはArduinoのライブラリが用意されています。
ピッチ変換基板付きは、Adafruitやebayでも購入できます。
●接続
ラズパイとの接続時にプルアップ抵抗を取り外した状態ですが、そのままArduinoのI2Cバスに接続します。一緒にAQM1602も使います。配線のしやすさからI2Cは、アナログ入力ピンA4 (SDA), A5 (SCL)を使いました。SHT31およびAQM1602は前の実験でラズパイにつないだため、プルアップ抵抗を外しています。8.2kΩで5Vへプルアップ抵抗をつなぎました。
Arduino | SHT31 | AQM1602 |
---|---|---|
A4 SDA | SDA | SDA |
A5 SCL | SCL | SCL |
GND | GND | GND |
5V | +V(VDD) | +V |
接続が終わったらUSBケーブルをつなぎ、I2Cにつながっているデバイスを検出するスケッチi2c_scannerを動かします。
●CRC
I2Cバスはプリント基板上のような短い距離で使われることを想定しています。バスにぶら下がるデバイスが数十個と多くなると、並列に接続されるために容量が増え、波形がなまってくるので、整形しなおすICもNXPから出ています。データ転送速度が速くなるとノイズや電源のGNDの揺れなどでデータが化けることがあります。スレーブ・デバイス側でCRCコードを生成し、データと一緒に送ってきて、受け取ったマスタ側はデータからCRCを計算し、送られてきたCRCと一致したら、データが途中で化けていないと判断します。
もちろん、そういうことは起こらないと信じて、送られてきたCRCを捨ててしまってもかまいません。湿度に変換したときに異常なデータだったら捨ててしまうという方法でもかまいませんが、なにをもって異常とするかは判断がむずかしいです。
CRCの長さが8ビットだとCRC8と呼ぶようです。その中にも幾通りも計算式があります。CRC8-ATMはこちらの記事で使いました。SHT-31はCRC-8-Dallas/Maximと呼び多項式 がx^8+x^5+x^4+1のCRCです。CRCの計算は、大きく分けて、筆算のようにプログラムで解読する方法と、いったんテーブルを作っておき、それを引いて解読する二通りがあるようです。
Adafruitのライブラリでは、次のように計算しています。
uint8_t Adafruit_SHT31::crc8(const uint8_t *data, int len)
{
const uint8_t POLYNOMIAL(0x31);
uint8_t crc(0xFF);
for ( int j = len; j; --j ) {
crc ^= *data++;
for ( int i = 8; i; --i ) {
crc = ( crc & 0x80 )
? (crc << 1) ^ POLYNOMIAL
: (crc << 1);
}
}
return crc;
}
マキシムの資料ではルックアップ・テーブルが掲載されています。
Var
CRC : Byte;
Procedure Do_CRC(X: Byte);
{
This procedure calculates the cumulative Maxim 1-Wire CRC of all bytes passed to it.
The result accumulates in the global variable CRC.
}
Const
Table : Array[0..255] of Byte = (
0, 94, 188, 226, 97, 63, 221, 131, 194, 156, 126, 32, 163, 253, 31, 65,
157, 195, 33, 127, 252, 162, 64, 30, 95, 1, 227, 189, 62, 96, 130, 220,
35, 125, 159, 193, 66, 28, 254, 160, 225, 191, 93, 3, 128, 222, 60, 98,
190, 224, 2, 92, 223, 129, 99, 61, 124, 34, 192, 158, 29, 67, 161, 255,
70, 24, 250, 164, 39, 121, 155, 197, 132, 218, 56, 102, 229, 187, 89, 7,
219, 133, 103, 57, 186, 228, 6, 88, 25, 71, 165, 251, 120, 38, 196, 154,
101, 59, 217, 135, 4, 90, 184, 230, 167, 249, 27, 69, 198, 152, 122, 36,
248, 166, 68, 26, 153, 199, 37, 123, 58, 100, 134, 216, 91, 5, 231, 185,
140, 210, 48, 110, 237, 179, 81, 15, 78, 16, 242, 172, 47, 113, 147, 205,
17, 79, 173, 243, 112, 46, 204, 146, 211, 141, 111, 49, 178, 236, 14, 80,
175, 241, 19, 77, 206, 144, 114, 44, 109, 51, 209, 143, 12, 82, 176, 238,
50, 108, 142, 208, 83, 13, 239, 177, 240, 174, 76, 18, 145, 207, 45, 115,
202, 148, 118, 40, 171, 245, 23, 73, 8, 86, 180, 234, 105, 55, 213, 139,
87, 9, 235, 181, 54, 104, 138, 212, 149, 203, 41, 119, 244, 170, 72, 22,
233, 183, 85, 11, 136, 214, 52, 106, 43, 117, 151, 201, 74, 20, 246, 168,
116, 42, 200, 150, 21, 75, 169, 247, 182, 232, 10, 84, 215, 137, 107, 53);
Begin
CRC := Table[CRC xor X];
End;
ルックアップ・テーブルを利用しました。ただし、CRC-8-Dallas/Maximは1-Wireで使われるため、ビットの並びが逆になるので、reflect()で逆順にしています。また計算結果も逆順にします。CRCの計算では2バイトのデータ処理に限定しています。
周期的連続測定に設定しているので、連続して読み出しができます。Micro:bitでも使ったので、そちらの解説も参照ください。
#include <Wire.h>
unsigned char SHT31_address = 0x44;
int Stemp;int SRH;
float temp;float RH;
byte readbuffer[6];
void setup() {
Wire.begin();
Wire.beginTransmission(SHT31_address);
Wire.write(0x23);Wire.write(0x34);
Wire.endTransmission();
Serial.begin(9600);
delay(10);
Serial.println("start ");
}
void loop() {
Wire.requestFrom(SHT31_address, 6);
readbuffer[0] = Wire.read();
readbuffer[1] = Wire.read();
readbuffer[2] = Wire.read(); // CRC
readbuffer[3] = Wire.read();
readbuffer[4] = Wire.read();
readbuffer[5] = Wire.read(); // CRC
// Serial.println(readbuffer[0]);Serial.println(readbuffer[1]);Serial.println(readbuffer[2]);Serial.println(readbuffer[3]);Serial.println(readbuffer[4]);Serial.println(readbuffer[5]);
if (readbuffer[2] == crc8(readbuffer)) {
Serial.println( "CRC-temp "+String(readbuffer[2])+" CRC "+String(crc8(readbuffer)));
Stemp = readbuffer[0] << 8 ;
Stemp = Stemp + readbuffer[1];
// Serial.println(String(Stemp,HEX)+" ");Serial.println(Stemp);
temp = -45 + Stemp * 175.0 / 65535.0;
}
if (readbuffer[5] == crc8(readbuffer+3)) {
Serial.println( "CRC-RH "+String(readbuffer[5])+" CRC "+String(crc8(readbuffer+3)));
SRH = readbuffer[3] << 8 ;
SRH = SRH + readbuffer[4];
// Serial.println(String(SRH,HEX)+" ");Serial.println(SRH);
RH = 100.0 * SRH / 65535.0;
}
Serial.println(" temp= "+String(temp,1)+"'C");
Serial.println(" RH%= "+String(RH,0));
delay(5000);
}
unsigned char table[256] ={
0, 94, 188, 226, 97, 63, 221, 131, 194, 156, 126, 32, 163, 253, 31, 65,157, 195, 33, 127, 252, 162, 64, 30, 95, 1, 227, 189, 62, 96, 130, 220,35, 125, 159, 193, 66, 28, 254, 160, 225, 191, 93, 3, 128, 222, 60, 98, 190, 224, 2, 92, 223, 129, 99, 61, 124, 34, 192, 158, 29, 67, 161, 255,70, 24, 250, 164, 39, 121, 155, 197, 132, 218, 56, 102, 229, 187, 89, 7, 219, 133, 103, 57, 186, 228, 6, 88, 25, 71, 165, 251, 120, 38, 196, 154, 101, 59, 217, 135, 4, 90, 184, 230, 167, 249, 27, 69, 198, 152, 122, 36,248, 166, 68, 26, 153, 199, 37, 123, 58, 100, 134, 216, 91, 5, 231, 185,140, 210, 48, 110, 237, 179, 81, 15, 78, 16, 242, 172, 47, 113, 147, 205,17, 79, 173, 243, 112, 46, 204, 146, 211, 141, 111, 49, 178, 236, 14, 80,175, 241, 19, 77, 206, 144, 114, 44, 109, 51, 209, 143, 12, 82, 176, 238, 50, 108, 142, 208, 83, 13, 239, 177, 240, 174, 76, 18, 145, 207, 45, 115,202, 148, 118, 40, 171, 245, 23, 73, 8, 86, 180, 234, 105, 55, 213, 139,87, 9, 235, 181, 54, 104, 138, 212, 149, 203, 41, 119, 244, 170, 72, 22,233, 183, 85, 11, 136, 214, 52, 106, 43, 117, 151, 201, 74, 20, 246, 168, 116, 42, 200, 150, 21, 75, 169, 247, 182, 232, 10, 84, 215, 137, 107, 53
};
unsigned char crc8(const unsigned char * data){
unsigned char crc = 0xff;
crc = table[reflect(data[0]) ^ crc];
crc = table[reflect(data[1]) ^ crc];
return reflect(crc);
}
unsigned char reflect(unsigned char c){
unsigned char r=0;
for (int i=0;i<8;i++){
if (c&(1<<i)){
r=r|(1<<(7-i));
}
}
return r;
}
(※)ビットの逆順は、こちらのプログラムを利用させていただきました。
●LCDに結果を表示
単発測定コマンド0x2400(繰り返し精度レベル;高、クロック・ストレッチは無効)を実行して読み取るように変更しました。
#include <Wire.h>
#include <i2clcd.h>
unsigned char LCD_address = 0x3e;
i2clcd lcd(LCD_address, 5);
unsigned char SHT31_address = 0x44;
int Stemp;int SRH;
float temp;float RH;
byte readbuffer[6];
void setup() {
Wire.begin();
Serial.begin(9600);
delay(10);
Serial.println("start ");
lcd.init_lcd();
}
void loop() {
lcd.lcdclear();
Wire.beginTransmission(SHT31_address);
Wire.write(0x24);Wire.write(0x00);
Wire.endTransmission();
Wire.requestFrom(SHT31_address, 6);
readbuffer[0] = Wire.read();
readbuffer[1] = Wire.read();
readbuffer[2] = Wire.read(); // CRC
readbuffer[3] = Wire.read();
readbuffer[4] = Wire.read();
readbuffer[5] = Wire.read(); // CRC
Serial.println( "read CRC-temp "+String(readbuffer[2])+" "+String(crc8(readbuffer))+" <-calculatedCRC ");
if (readbuffer[2] == crc8(readbuffer)) {
Stemp = readbuffer[0] << 8 ;
Stemp = Stemp + readbuffer[1];
temp = -45 + Stemp * 175.0 / 65535.0;
}
Serial.println( "read CRC-RH "+String(readbuffer[5])+" "+String(crc8(readbuffer+3))+" <-calculatedCRC ");
if (readbuffer[5] == crc8(readbuffer+3)) {
SRH = readbuffer[3] << 8 ;
SRH = SRH + readbuffer[4];
RH = 100.0 * SRH / 65535.0;
}
Serial.println(" temp= "+String(temp,1)+"'C");
Serial.println(" RH%= "+String(RH,0));
lcd.lcdcu_set(0, 0);lcd.i2cprint( "Temp="+String(temp,1) +"'C" );
lcd.lcdcu_set(0, 1);lcd.i2cprint( "RH%= "+String(round(RH)) );
delay(5000);
}
unsigned char table[256] ={
0, 94, 188, 226, 97, 63, 221, 131, 194, 156, 126, 32, 163, 253, 31, 65, 157, 195, 33, 127, 252, 162, 64, 30, 95, 1, 227, 189, 62, 96, 130, 220, 35, 125, 159, 193, 66, 28, 254, 160, 225, 191, 93, 3, 128, 222, 60, 98, 190, 224, 2, 92, 223, 129, 99, 61, 124, 34, 192, 158, 29, 67, 161, 255, 70, 24, 250, 164, 39, 121, 155, 197, 132, 218, 56, 102, 229, 187, 89, 7, 219, 133, 103, 57, 186, 228, 6, 88, 25, 71, 165, 251, 120, 38, 196, 154, 101, 59, 217, 135, 4, 90, 184, 230, 167, 249, 27, 69, 198, 152, 122, 36, 248, 166, 68, 26, 153, 199, 37, 123, 58, 100, 134, 216, 91, 5, 231, 185, 140, 210, 48, 110, 237, 179, 81, 15, 78, 16, 242, 172, 47, 113, 147, 205, 17, 79, 173, 243, 112, 46, 204, 146, 211, 141, 111, 49, 178, 236, 14, 80, 175, 241, 19, 77, 206, 144, 114, 44, 109, 51, 209, 143, 12, 82, 176, 238, 50, 108, 142, 208, 83, 13, 239, 177, 240, 174, 76, 18, 145, 207, 45, 115, 202, 148, 118, 40, 171, 245, 23, 73, 8, 86, 180, 234, 105, 55, 213, 139,87, 9, 235, 181, 54, 104, 138, 212, 149, 203, 41, 119, 244, 170, 72, 22,233, 183, 85, 11, 136, 214, 52, 106, 43, 117, 151, 201, 74, 20, 246, 168,116, 42, 200, 150, 21, 75, 169, 247, 182, 232, 10, 84, 215, 137, 107, 53
};
unsigned char crc8(const unsigned char * data){
unsigned char crc = 0xff;
crc = table[reflect(data[0]) ^ crc];
crc = table[reflect(data[1]) ^ crc];
return reflect(crc);
}
unsigned char reflect(unsigned char c){
unsigned char r=0;
for (int i=0;i<8;i++){
if (c&(1<<i)){
r=r|(1<<(7-i));
}
}
return r;
}
実行中です。
(※1)プログラム・リストは、表示の関係でTabキーが無視されるので、スペースに代えてあります。また、リスト中を2回クリックすると全選択になるので、CTRL-Cでコピーし、テキスト・エディタにCTRL-Vで貼り付けて利用してください。エディタに持っていくと、スペースやリターン・コードなどが化けていることがあるので、一度消して、Arduino IDEのテキスト・エディタで修正してください。
(※2)キャラクタ表示LCD AQM1602のライブラリの組み込み方はこちらの記事を参照ください。