PICマイクロコントローラでセンサーを利用するための汎用ライブラリ
目次 |
1個100円程度のデジタル温湿度センサー。整数値しか取り出せず誤差も±2℃・±5%RHと大きいが、低価格の魅力には使わざるを得ない場合も…。上位機種のDHT22が350円位なので、予算に余裕があるならそちらで。
DHT11は送受信を一つの信号線で兼用するタイプのデジタル センサー モジュール。ホスト(PIC)側からLo信号を18ミリ秒送信すると、DHT11が起動してスタート・ビット(80us Lo + 80 us Hi)とデータ5バイト分を一気に送信してくる。(下図のデータファイルをダウンロード)
オシロスコープで波形観察したデータを次に示す。ホスト(PIC側)送信が終了し、読み取りモードとなった時の電圧立ち上がりカーブを右側に示した。プルアップ抵抗によりI/O線が充電される時間を考慮して当Webページに掲載したライブラリには20マイクロ秒待つように設定している。(10マイクロ秒では少し足らなかつた)
// DHT11に処理開始信号を送る (Loを18ms以上続ける) // I/O = Lo TRIS_DHT11 = 0; // 「DHT11接続ポート」を出力モードに PORT_DHT11 = 0; // 「DHT11接続ポート」 = Lo // 25 msec Loを保持(18 msec以上) __delay_ms(25); // I/O = Hi (入力モードにすることで、自動的にプルアップされる) TRIS_DHT11 = 1; // 「DHT11接続ポート」を入力モードに // プルアップ抵抗で「DHT11接続ポート」が充電される時間待つ(実測では5〜10usec以上) __delay_us(20);
ブレッドボード上に制作した回路。DHT11センサーは、網目の面が表で左からVcc, I/O, NC, GNDピンとなっている。
読込のタイミングに余裕が無いため、不必要なループ処理を削除している。ソースコード中にはタイミングが遅れて読み込みエラーとなるforループ処理の例がコメントアウトされて残存させている。
#define PORT_DHT11 PORTAbits.RA0 #define TRIS_DHT11 TRISAbits.TRISA0 #define WPUA_DHT11 WPUAbits.WPUA0 unsigned char dht11_read_value(unsigned char *value) { // DHT11に処理開始信号を送る (Loを18ms以上続ける) // I/O = Lo TRIS_DHT11 = 0; // 「DHT11接続ポート」を出力モードに PORT_DHT11 = 0; // 「DHT11接続ポート」 = Lo // 25 msec Loを保持(18 msec以上) __delay_ms(25); // I/O = Hi (入力モードにすることで、自動的にプルアップされる) TRIS_DHT11 = 1; // 「DHT11接続ポート」を入力モードに // プルアップ抵抗で「DHT11接続ポート」が充電される時間待つ(実測では5〜10usec以上) __delay_us(20); // DHT11からの開始確認信号を受信する(Lo 80usec, Hi 80usec) // 「DHT11接続ポート」 = Lo になるまで待つ unsigned char k = 150; // 300 usec以上待っても終了しない場合のカウンタ while(!PORT_DHT11) { __delay_us(2); if(--k < 1) return(0); // 異常終了 } // 「DHT11接続ポート」 = Hi になるまで待つ k = 150; // 300 usec以上待っても終了しない場合のカウンタ while(PORT_DHT11) { __delay_us(2); if(--k < 1) return(0); // 異常終了 } // データの読み出し(5バイト) *value = dht11_read_byte(); // 湿度 整数部 *(value+1) = dht11_read_byte(); // 湿度 小数部(DHT11ではゼロ) *(value+2) = dht11_read_byte(); // 温度 整数部 *(value+3) = dht11_read_byte(); // 温度 小数部(DHT11ではゼロ) *(value+4) = dht11_read_byte(); // チェックサム // ループで読み出しを行うと、タイミングがずれて読み取れない // for (short int i = 0; i < 4; i++) { // *(value + i) = dht11_read_byte(); // } // チェックサムの確認 if(*value + *(value+1) + *(value+2) + *(value+3) != *(value+4)) return(0); // チェックサム結果OK return(1); } unsigned char dht11_read_byte(void) { static unsigned short num, i; num = 0; for (i=0; i<8; i++){ while(!PORT_DHT11); __delay_us(40); // データが0の場合26-28usec, 1の場合70usecの中間あたりの秒数 if(PORT_DHT11) num |= 1<<(7-i); while(PORT_DHT11); } return num; }
AC電流測定用の変流器を用いて、PICでAC電流を測る。この測定変流器は抵抗付きのものと、抵抗を自分で用意して取り付けなければならないものがある。日本の電子部品屋で売っているものは抵抗なしの変流器のみが多く、中国の商社から直接輸入する時は抵抗付きのものが多い。
今回はURD社のCTL-6-P-Hという小型変流器を用いた。1Aから10A(100Vで100Wから1000W程度)の測定に用いたいため、特性曲線より100Ωの抵抗を選定した。
理想的な正弦波以外の電流測定の場合のプログラムはこちらのページに記述している。
交流電流センサーは、測定対象の交流波形が「そのまま縮小(減圧)」して出てくるだけなので、特性曲線で1Aと書いてあれば -1A 〜 +1A の範囲の電圧が出てくることになる。
PICのA/D変換器に負電圧を加えるわけにはいかないので、センサーの(仮想的にGNDとする)片方の極を適当な電圧までプルアップして、I/O端子に負電圧が出てこないようにする。今回は、可変抵抗(4kΩ)を用いてVcc/2=2.5V程度にプルアップしている。
実際にクランプメータとにらめっこして、だいたい次の程度の返還率になっていた。
交流センサーの全振幅 1V (±0.5V) → 測定対象の電流 3A
交流センサーの全振幅 2V (±1.0V) → 測定対象の電流 6A
実測中の回路
約2.5VプルアップしたセンサーI/O出力波形
浮動小数点処理を行わせるとファームウエアのサイズが一気に大きくなるため、表示部分は16進数を簡易的に固定小数点値に変換している。
int main(int argc, char** argv) { // 基本機能の設定 OSCCON = 0b01101010; // 内部オシレーター 4MHz TRISA = 0b00101111; // IOポートRA0(AN0),RA1(SCL),RA2(SDA),RA5(RX)を入力モード(RA3は入力専用)、RA4(TX)を出力モード APFCONbits.RXDTSEL = 1; // シリアルポート RXをRA5ピンに割付 APFCONbits.TXCKSEL = 1; // シリアルポート TXをRA4ピンに割付 ANSELA = 0b00000001; // A/D変換をAN0を有効、AN1,AN2,AN4を無効 PORTA = 0; ADCON0 = 0; // AN0選択, A/D機能停止 ADCON1 = 0b10010000; // 変換結果右詰, クロックFOSC/8, 比較対象VDD i2c_enable(); OPTION_REGbits.nWPUEN = 0; // I2C プルアップ抵抗 有効 WPUA |= 0b00000110; // pull-up (RA1=SCL, RA2=SDA pull-up enable) i2c_lcd_init(); i2c_lcd_set_cursor_pos(0); printf("AC Ammeter ..."); // 電流値の連続読み出し for(;;){ unsigned int value_max = 0, value_min = 0xffff; // 検出電流の最大・最小値 for(unsigned int i = 0; i < 2800; i++){ ADCON0 = 0b00000001; // AN0チャンネル選択, A/D機能ON __delay_us(10); // A/D変換器チャージ時間待つ ADCON0 = 0b00000011; // AN0チャンネル選択, A/D開始, A/D機能ON while(ADCON0bits.GO_nDONE){} // A/D変換完了を待つ unsigned int value = ADRESH << 8 | ADRESL; ADCON0 = 0; // AN0選択, A/D機能停止 if(value > value_max) value_max = value; if(value_min > value) value_min = value; __delay_us(50); } // PICのA/D変換は10bit(0〜1024)で5V(Vcc=5.0V)を表現する。 // A/D変換結果を204.8で割ることで電圧値に変換できる i2c_lcd_set_cursor_pos(0); printf("max%01d.%01dV min%01d.%01dV", value_max/205, (value_max%205)/21, value_min/205, (value_min%205)/21); // CTの性能が電圧差1V→3Aとする, 5V(1024)→15AなのでA/D変換結果を68.3で割ると電流値 i2c_lcd_set_cursor_pos(0x40); printf("%01d%01d.%01dA (%01d.%01dV)", (value_max - value_min)/683, ((value_max - value_min)%683)/68, ((value_max - value_min)%68)/7, (value_max - value_min)/205, ((value_max - value_min)%205)/21); } }
オシロスコープによる観察で、波形上に±0.1V程度のランダム・ノイズが乗っているのが分かる。電源としている「USB電源アダプター」から出ているものなので、乾電池で動くようにすれば少しはノイズが減る。さらに、センサーから基盤までの配線が長いと、空中の電波ノイズを拾ってしまうので、ケーブルも短く…
eBayで1個500円程度の粉塵センサー。対象とする粉塵はSPMなのかPM2.5なのか… 製造メーカーのマニュアルには「粉塵」としか書かれていない。家庭用集じん機などに使うパーツなのだろう。
センサー内のLEDをを点灯し、粉塵で散乱した光の強さを増幅して返す「アナログ」センサー。LEDの店頭タイミングや、散乱光の測定タイミングもユーザが全て制御してやる必要がある。マニュアルに示されたタイミングと、得られる出力電圧と粉じん濃度の関係は次のようなもの。
このグラフの直線部分を一次回帰曲線として数式化した [粉塵濃度] = ([電圧] - 0.78)/0.0063 をプログラム中で利用する。(一時回帰分析をしたときのLibreOffice Calcファイルをダウンロード)
また、マニュアルに示されたサンプル回路を参考に、PIC 12F1822用に少しだけアレンジしたものが次の回路図。(下図のデータファイルをダウンロード)
オシロスコープで波形観察したデータを次に示す。マニュアルに示されるとおり、10ミリ秒ごとに0.32ミリ秒だけLEDを点灯し、LED点灯開始から0.28ミリ秒後にセンサーからの出力をA/D変換するプログラムを作成した。そのときのオシロスコープでの観察結果は次の通り。
また、センサーから返される波形は、少しだけマニュアルと違うがこんな感じだ。
1回のみ、決め打ちでA/D変換して粉塵濃度をキャラクタ液晶に表示するプログラム。
void single_measure(unsigned int *value_max, unsigned int *value_min) { // 30回測定して、最大値を測定結果として画面表示する for(unsigned short int i = 0; i < 30; i++){ // GP2Y1010AU0FのLEDをONにする320usの間に、1回 A/D変換して電圧値を得る処理 ADCON0 = 0b00000001; // AN0チャンネル選択, A/D機能ON PORTAbits.RA5 = 1; // 測定LED ON __delay_us(280); // A/D変換器チャージ時間10usを含み280us待つ ADCON0 = 0b00000011; // AN0チャンネル選択, A/D開始, A/D機能ON while(ADCON0bits.GO_nDONE){} // A/D変換完了を待つ unsigned int value = ADRESH << 8 | ADRESL; ADCON0 = 0; // AN0選択, A/D機能停止 PORTAbits.RA5 = 0; // 測定LED OFF if(value > *value_max) *value_max = value; if(*value_min > value) *value_min = value; // GP2Y1010AU0Fの最低測定間隔は10msのため、残り9.7ms待つ __delay_us(9700); } } int main(int argc, char** argv) { // 基本機能の設定 OSCCON = 0b01101010; // 内部オシレーター 4MHz TRISA = 0b00001111; // IOポートRA0(AN0),RA1(SCL),RA2(SDA)を入力モード(RA3は入力専用)、RA4(未使用),RA5(SensorLED)を出力モード ANSELA = 0b00000001; // A/D変換をAN0を有効、AN1,AN2,AN4を無効 PORTA = 0; ADCON0 = 0; // AN0選択, A/D機能停止 ADCON1 = 0b10010000; // 変換結果右詰, クロックFOSC/8, 比較対象VDD i2c_enable(); OPTION_REGbits.nWPUEN = 0; // I2C プルアップ抵抗 有効 WPUA |= 0b00000110; // pull-up (RA1=SCL, RA2=SDA pull-up enable) i2c_lcd_init(); i2c_lcd_set_cursor_pos(0); printf("wait..."); PORTAbits.RA5 = 0; // 1秒待つ(GP2Y1010AU0F仕様書による指定) __delay_ms(1000); i2c_lcd_clear(); while(1){ unsigned int value_max = 0, value_min = 0xffff; // 検出電圧の最大・最小値 float v_max = 0, v_min = 0; // 換算した実電圧 float v_dust = 0; single_measure(&value_max, &value_min); v_max = (float)value_max * 5.0 / 1024.0; v_min = (float)value_min * 5.0 / 1024.0; v_dust = (v_max - 0.78) / 0.0063; i2c_lcd_set_cursor_pos(0); printf("Dust %03dug/m3", (int)v_dust); i2c_lcd_set_cursor_pos(0x40); printf("(max%03d min%03d)", (int)(v_max*100), (int)(v_min*100)); __delay_ms(1000); } return (EXIT_SUCCESS); }
上のソースコードで、1回だけA/D変換するところを、マイコンの処理能力が許す限り複数回A/D変換して最大値を得るように改変してみたのが次のプログラム。オシロスコープでタイミングを観察しながら、A/D変換できる最大回数は「たったの4回」。
void multi_measure(unsigned int *value_max, unsigned int *value_min) { // 30回測定して、最大値を測定結果として画面表示する for(unsigned short int i = 0; i < 30; i++){ // GP2Y1010AU0FのLEDをONにする320usの間に、1回 A/D変換して電圧値を得る処理 PORTAbits.RA5 = 1; // 測定LED ON for(unsigned short int j = 0; j < 4; j++){ ADCON0 = 0b00000001; // AN0チャンネル選択, A/D機能ON __delay_us(7); // A/D変換器チャージ時間4.97us以上待つ ADCON0 = 0b00000011; // AN0チャンネル選択, A/D開始, A/D機能ON while(ADCON0bits.GO_nDONE){} // A/D変換完了を待つ unsigned int value = ADRESH << 8 | ADRESL; ADCON0 = 0; // AN0選択, A/D機能停止 __delay_us(1); // 時間調整 if(value > *value_max) *value_max = value; if(*value_min > value) *value_min = value; } PORTAbits.RA5 = 0; // 測定LED OFF // GP2Y1010AU0Fの最低測定間隔は10msのため、残り9.7ms待つ __delay_us(9700); } }
複数回A/D変換するため、すこしだけLED点灯時間が伸びた。オシロスコープでのタイミングの観察結果。
マイコンの動作周波数を4MHzから引き上げれば、A/D変換回数をもっと増やすことができるが、消費電力量もそれだけ掛かることになる。