自作冷却ミラー式湿度計(チルドミラーヒグロメーター)──Arduinoで高精度な温湿度測定
必要な部品と材料
- Arduino UNO × 1
- 12V 10A 電源 × 1
- 12V 40mm 高性能ファン × 1
- 12V 40mm ペルチェ素子(TEC) × 1
- 40mm ヒートシンク × 1
- 小型ミラー × 1
- 熱伝導接着剤 × 1
- 熱伝導グリス × 1
- BTS7960 モータードライバ × 1
- 3mm 黄色LED × 1
- OPT101 光センサ × 1
- IRF520 MOSFETモジュール × 1
- DS18B20 防水温度センサ(1-Wire) × 1
- Si7021 温湿度センサブレイクアウト × 1
必要な工具と機器
- グルーガン(汎用)
使用するアプリとオンラインサービス
- Arduino IDE
プロジェクト概要
このプロジェクトについて
湿度測定には様々な方法がありますが、最も一般的なのは静電容量式センサです。しかし、高湿環境で連続使用すると精度が急速に低下します。冷却ミラー式湿度計(チルドミラーヒグロメーター)はこの問題がなく、特に高湿域で高精度です。市販品は数十万円しますが、原理はシンプルなので自作可能です。市販品ほどの0.1℃精度は出せませんが、ほとんどの用途で十分実用的です。本プロジェクトは概念実証(PoC)であり、量産レベルではありませんが、動作することを証明しています。
原理
ペルチェ素子(TEC)でミラー表面を冷却し、結露が現れる温度(露点)を検出します。光源(LEDまたはレーザー)と光センサ(OPT101)でミラー表面の反射光量を監視し、結露で反射光が減少した瞬間のミラー温度=露点とみなします。露点と周囲温度から相対湿度を算出します。
市販品との違い(簡易化ポイント)
- ミラー材質:市販品は白金/ロジウムメッキ銅等を使用し熱伝導と反射率を最適化。入手性・コストの観点から普通のミラーを使用(小型角ミラーや研磨ステンレス板がより良い)。
- 温度センサ:市販品はミラー直下に白金測温抵抗体(PRT)を埋め込み。本プロジェクトではSMDセンサ(Si7021)をミラー表面に貼り付け。熱分布を多少乱すが製作容易で十分な精度。
- ミラーサイズ:市販品は約5mm径。本機は約4cm径。大型化で電力増・大型化だが入手性良好。小型ペルチェは低電圧駆動で専用ドライバ必要。
- 遮光ケース:外光遮断用の3Dプリントケース未実装。強く推奨。通気確保とミラー清掃アクセスを考慮必要。
- 霜点測定未対応:氷晶と水滴の区別(散乱光測定)には第2光センサが必要。
組み立て手順
- ペルチェとヒートシンクの固定:両面を清掃し、熱伝導接着剤(なければ熱伝導グリス+端に瞬間接着剤少量)を均一に塗布、ヒートシンクを押し当て乾燥。
- ミラーの取り付け:同様にペルチェ冷却面とミラーを接着。熱伝導グリスを均一に、気泡なきよう注意。不均一だと結露パターンが偏る。
- ファン取り付け:ヒートシンクにファンを固定(グルーガン等)。十分な風量が必要。
- 結露パターンの確認:ファン→ペルチェの順で通電し、ミラー上で「同時に結露が現れる領域」を確認。長時間通電すると全面結露してわからなくなるので注意。写真撮影かマーキングしておく。
- 温度センサ(Si7021)のミラー貼り付け:センサ基板に非導電性熱伝導グリスを大量に塗り、基板四隅にグルーガンで大きな接着点を作り、結露が同時に現れた位置に水平に強く押し付け固定。基板に結露したらグルーやスプレー絶縁で保護。
- ファン制御:IRF520 MOSFETモジュールでPWM制御。
- 光源と光センサ:OPT101は高感度・低ノイズ・扱いやすい。光源は低出力レーザーダイオードまたは指向性のあるLED。出力が強すぎる場合は直列抵抗で減光(PWMだとセンサにノイズが入るため)。銅線を捩って剛性を持たせ、両端をグルーで固定して位置決め(試作レベル)。本格運用なら3Dプリントホルダー兼遮光ケース推奨。
- ペルチェ駆動:BTS7960モータードライバでPWM駆動(理想は可変電流駆動だが入手性優先)。
- 動作確認・調整:スケッチ書き込み後、OPT101感度を半固定抵抗で調整(データシートFig.3参照)。デバッグ時はミラーに息を吹きかけたり物体をかざしたりして結露擬似。エアコンのオンオフで湿度変化を観測可能。



筆者の場合、熱伝導グリスの塗りムラで不均一な結露パターンになりました(上写真)。




安全上の注意
現在のコードは冷却のみ対応ですが、ハードウェアはペルチェ極性反転で加熱も可能です。結露除去で応答速度向上や汚れ除去に有効ですが、ミラー側にヒートシンクがないため、暴走すると接着剤溶解や配線溶融・火災のリスクがあります。加熱機能を実装する場合は十分な安全策を講じてください。
精度について
- 露点温度は絶対値なので、静電容量式ほどキャリブレーションはクリティカルではない。
- ミラー表面の温度センサ位置と光学検出位置の温度差が誤差源。市販品で校正可能。
- ミラー汚染:反射光基準は冷却開始時の「結露なし状態」なので、汚れによる反射率低下は検出に影響しにくい。ただし汚れが結露温度自体を変える可能性あり。定期清掃推奨。
- 温度センサは高分解能(ジッター<0.1℃)が必須。TSYS01等の高精度センサ推奨。
- センサとミラーの熱的接触は非導電性熱伝導グリスで確実に。
- 冷却速度がセンサ応答速度を超えると露点検出が遅れる。緩やかな冷却が精度向上に有利だが応答遅くなる。露点付近で温度振動させる手法で両立可能。
- 赤外線温度計はミラーが熱放射を反射するため周囲の影響を受け不向き。
- 気圧依存性は通常環境では無視可能。ドア開閉等の急激な圧力変化の方がノイズになり得る。
- ペルチェ排熱がミラーにかからないよう気流設計に注意。

Arduinoスケッチ(Chilled Mirror Hygrometer)
#include <math.h>
#include <avr/wdt.h> // Watchdog for safety
// Custom libraries
#include "Si7021.h"
#include <OneWire.h>
#include <DallasTemperature.h>
// Timer library: https://github.com/brunocalou/Timer
#include "timer.h"
#include "timerManager.h"
// Hardware pins
#define coolingPWM 6
#define heatingPWM 5
#define coolingEnable 13
#define heatingEnable 12
#define tecFan 7
#define opticalSensor 0 // Analog in
#define oneWireBus A3
// TEC states
#define COOLING 0
#define HEATING 1
#define OFF 2
// Timers
Timer timerMainLoop;
Timer timerTecCooling;
Timer timerSampleNoise;
// Sensors
Si7021 si7021;
OneWire oneWire(oneWireBus);
DallasTemperature sensors(&oneWire);
float humidity = 0;
float ambientTemp = 0;
float opticalDewpoint = 0;
float mirrorTemp = 30;
float optical = 30;
float dewPoint = 15; // initial < mirrorTemp
float relativeHumidity = 30;
int tecState = OFF;
bool cooling = false;
int intervalTecCooling = 200; // ms
float opticalThreshold = 0.5f; // condensation detection threshold
int pwmIncrement = 1;
int startPwm = 100;
int maxPwm = 255;
int intervalMainLoop = 200;
int tecPwm = 0;
int noiseSampleIndex = 0;
int noiseSampleAmount = 10;
float noiseSampleHighest = 0;
float noiseSampleLowest = 10000;
bool noiseSampling = false;
// August-Roche-Magnus approximation for RH calculation
float calculateHumidity(float TD, float T) {
if (TD > T) TD = T;
float rh = 100 * (exp((17.625 * TD) / (243.04 + TD)) / exp((17.625 * T) / (243.04 + T)));
return rh;
}
// TEC control via BTS7960
void SetTEC(int state, int amount) {
tecState = state;
// Note: both enable pins need HIGH for BTS7960
switch (state) {
case COOLING:
digitalWrite(heatingEnable, HIGH); analogWrite(heatingPWM, 0);
digitalWrite(coolingEnable, HIGH); analogWrite(coolingPWM, amount);
break;
case HEATING:
digitalWrite(coolingEnable, HIGH); analogWrite(coolingPWM, 0);
digitalWrite(heatingEnable, HIGH); analogWrite(heatingPWM, amount);
break;
case OFF:
default:
digitalWrite(coolingEnable, LOW); analogWrite(coolingPWM, 0);
digitalWrite(heatingEnable, LOW); analogWrite(heatingPWM, 0);
}
}
void setup() {
wdt_enable(WDTO_2S); // Safety watchdog
Serial.begin(9600);
pinMode(coolingPWM, OUTPUT);
pinMode(heatingPWM, OUTPUT);
pinMode(coolingEnable, OUTPUT);
pinMode(heatingEnable, OUTPUT);
pinMode(tecFan, OUTPUT);
pinMode(opticalSensor, INPUT);
// Timers
timerMainLoop.setInterval(intervalMainLoop);
timerMainLoop.setCallback(mainLoop);
timerMainLoop.start();
timerTecCooling.setInterval(intervalTecCooling);
timerTecCooling.setCallback(tecCoolingCallback);
timerSampleNoise.setInterval(intervalTecCooling);
timerSampleNoise.setCallback(sampleNoiseCallback);
si7021.begin();
uint64_t serialNumber = si7021.getSerialNumber();
sensors.begin();
startNoiseSampling();
}
float getOptical() {
int opt = analogRead(opticalSensor);
return (float)opt / 30.0f;
}
void tecCoolingCallback() {
digitalWrite(tecFan, HIGH);
tecPwm += pwmIncrement;
if (tecPwm > maxPwm) tecPwm = maxPwm;
SetTEC(COOLING, tecPwm);
// Condensation detected?
if (optical <= (noiseSampleLowest - opticalThreshold)) {
dewPoint = mirrorTemp;
opticalDewpoint = optical;
stopTec();
}
}
void startNoiseSampling() {
noiseSampling = true;
noiseSampleHighest = 0;
noiseSampleLowest = 10000;
timerSampleNoise.start();
}
void sampleNoiseReset() {
timerSampleNoise.stop();
noiseSampleIndex = 0;
noiseSampling = false;
}
void sampleNoiseCallback() {
if (noiseSampleIndex > noiseSampleAmount) {
sampleNoiseReset();
startTecCooling();
} else {
if (optical > noiseSampleHighest) noiseSampleHighest = optical;
if (optical < noiseSampleLowest) noiseSampleLowest = optical;
}
noiseSampleIndex++;
}
void startTecCooling() {
cooling = true;
digitalWrite(tecFan, HIGH);
tecPwm = startPwm;
timerTecCooling.start();
}
void stopTec() {
cooling = false;
digitalWrite(tecFan, LOW);
timerTecCooling.stop();
SetTEC(OFF, 0);
}
void mainLoop() {
// Ambient temp (DS18B20)
sensors.setResolution(10);
sensors.requestTemperatures();
ambientTemp = sensors.getTempCByIndex(0);
// Mirror temp (Si7021)
mirrorTemp = si7021.measureTemperature();
// Optical reading
optical = getOptical();
relativeHumidity = calculateHumidity(dewPoint, ambientTemp);
// Serial Plotter output
Serial.print(mirrorTemp, 2); Serial.print(" ");
Serial.print(optical, 2); Serial.print(" ");
Serial.println(dewPoint, 2);
// Restart cooling when condensation clears
if (!cooling && !noiseSampling && (optical >= noiseSampleLowest)) {
startNoiseSampling();
}
}
void loop() {
wdt_reset(); // Watchdog feed
TimerManager::instance().update();
}
回路図

製造プロセス
- 現代ミラーの科学と技術:古代から最新コーティングまで
- Bluetooth操作のArduinoスパイボットを自作する
- FlickMote:ジェスチャー操作のIRリモコンでスマートホームを制御
- 自作Arduino TV-B-Gone:全テレビ対応の万能リモコン
- DIY万華鏡インフィニティミラー - Arduino 101で作る美しいLEDライトディスプレイ
- 12メートル先から見える大型LEDマスタークロック(アラーム機能付き)を自作しよう
- ArduinoとBluetoothで作るスマート紛失防止タグ「Find Me」
- Arduinoプロジェクトのための最適化電源ソリューション
- Arduino三目並べ:MAX7219 LEDマトリクスと段ボールケースで自作
- スマートArduino四足歩行ロボットの製作:ステップバイステップガイド
- ゲーム用カスタムArduinoジョイスティックハンドルの製作