습도 측정에는 여러 방법이 있습니다. 가장 대중적인 방법은 정전 용량식 습도 센서를 사용하는 것입니다. 불행히도 이 센서들은 매우 습한 환경에서 지속적으로 작동할 때 정확도가 빠르게 떨어집니다. 칠드 미러 습도계(Chilled Mirror Hygrometer)는 이 문제가 없으며, 특히 고습도 범위에서 훨씬 더 정확합니다. 안타깝게도 상용 칠드 미러 습도계는 말 그대로 수천 달러가 듭니다. 기본 원리는 꽤 간단해서 직접 만들 수 있습니다. 상용 장치처럼 0.1도 정확도는 아니지만, 대부분의 목적에는 충분하며 정전 용량식 센서보다 확실히 더 정확합니다.
이 프로젝트는 개념 증명(Proof of Concept)이며 결코 양산 준비가 된 것은 아닙니다. 많은 개선이 가능하지만 작동한다는 사실을 증명합니다.
작동 원리
칠드 미러 습도계는 펠티어(TEC)를 사용하여 반사 표면을 냉각시켜 결로가 생길 때까지 내립니다. 결로는 광원과 광학 센서로 감지합니다. 결로가 나타나는 순간의 거울 표면 온도를 기록합니다. 이 온도는 이슬점(Dew Point)과 동일합니다. 상대 습도는 이슬점과 주변 온도를 사용하여 계산할 수 있습니다.
단순화
이 DIY 칠드 미러 습도계는 상용 장치와 몇 가지 차이점이 있습니다.
상용 칠드 미러 습도계는 일반 거울을 사용하지 않습니다. 열 전도와 분배가 잘 안 되어 정확도와 효율이 떨어지기 때문입니다. 주로 백금 또는 로듐 도금 구리를 거울로 사용합니다. 이는 구하기 쉽지도 않고 비용 효율적이지도 않아 이 DIY 버전에서는 일반 거울을 사용했습니다. 작은 정사각형 거울이 원형보다 나을 텐데 구할 수 없었습니다. 대안으로 연마된 스테인리스 스틸 판이 더 좋을 수 있습니다.
상용 장치는 거울 아래 내장된 백금 저항 온도계(PRT)를 사용하는 반면, 이 프로젝트에서는 거울 표면에 장착된 SMD 온도계를 사용합니다. PRT는 추가 전자회로가 필요하고 펠티어와 거울 사이에 장착하면서 열전도성을 유지하기가 어렵습니다. 거울 표면에 온도계를 장착하면 열 분배 특성이 변해 정확도가 떨어지는 단점이 있지만, 제작이 훨씬 쉽고 충분히 정확합니다.
상용 장치는 약 5mm의 훨씬 작은 거울을 사용하여 전력 소모가 적고 폼팩터가 작습니다. 하지만 작은 거울과 맞는 펠티어는 큰 것만큼 쉽게 구할 수 없습니다. 또한 작은 펠티어는 매우 낮은 전압을 필요로 해 맞춤 드라이버가 필요합니다. 작은 거울 표면에 SMD 온도계를 장착하면서 빛 반사 공간을 남기는 것도 더 어렵습니다.
현재 버전에는 차광 인클로저가 없습니다. 쉽게 3D 프린트할 수 있으며 외부 광원 간섭을 차단하기 위해 적극 권장합니다. 광원과 광 센서를 고정하는 데도 사용할 수 있습니다. 3D 프린트 인클로저를 만든다면 통풍이 잘 되게 하세요. 펠티어 팬으로 환기시킬 수 있지만, 안으로 불어넣지 말고 바깥 공기를 안으로 끌어들이게 하세요. 내부도 거울 청소를 위해 쉽게 접근 가능해야 합니다.
현재 구현은 서리점(Frost Point)은 측정할 수 없습니다. 얼음 입자와 물방울을 구분하려면 산란광을 측정하는 두 번째 광학 센서가 필요하기 때문입니다.
제작 방법
펠티어를 가져와 양면 표면을 깨끗이 닦고, 열전도 접착제를 발열 면(뜨거운 면)에 바릅니다. 고르게 분포되게 하세요. 열전도 접착제가 없으면 열전도 페이스트에 가장자리 근처에 순간접착제 몇 방울을 섞어 써도 똑같이 잘 작동합니다. 히트싱크 표면을 닦고 히트싱크와 펠티어를 눌러 붙여 접착제가 마르도록 두세요.
히트싱크가 펠티어에 부착되고 접착제가 마르면, 같은 방식으로 거울을 부착하되 바닥면을 먼저 깨끗이 닦으세요. 열전도 페이스트가 고르게 분포되고 기포가 없도록 하는 것이 중요합니다. 그렇지 않으면 거울의 열 분포가 불균일해집니다.
모든 접착제가 마르면 팬을 부착하세요. 히트싱크가 꽤 뜨거워지므로 충분히 강력한 팬을 사용하세요. 팬을 히트싱크에 고정하는 방법은 사용하는 히트싱크 유형에 따라 다릅니다. 저는 글루건으로 그냥 붙였습니다.
팬에 전원을 넣고, 펠티어에 전원을 넣어 거울에서 최소 두 군데에서 결로가 동시에 나타나는지 확인하세요. 펠티어에 너무 오래 전원을 넣지 마세요. 그러면 확실히 균일한 결로 프로파일이 생깁니다. 결로가 막 보이기 시작할 때 모습을 봐야 합니다. 결과가 마음에 들지 않으면 새 펠티어, 거울, 히트싱크로 다시 시도하세요. 부품을 떼어내고 매끄러운 표면을 복원하기는 어렵습니다.
저는 열전도 페이스트를 균일하게 바르지 않아 위 사진에서 보듯 불규칙한 결로 프로파일이 나타났습니다.
결로가 동시에 나타나는 영역을 사진으로 찍거나 표시해두는 것이 중요합니다. 이곳이 거울 표면 온도 센서를 장착하고 거울에서 반사되는 빛 양을 측정할 위치이기 때문입니다.
이 프로젝트에서는 Si7021을 거울 온도 측정용으로 사용했는데, 노이즈가 적은 출력을 주기 때문입니다. 불행히도 I2C 주소가 하드코딩되어 있어 하나만 사용할 수 있습니다. 주변 온도 측정용으로는 DS18B20 온도 센서를 사용했지만 정확도가 아주 높지는 않습니다. 이 센서들을 사용한 건 그냥 마침 있어서입니다.
Si7021(또는 PCB 기반 온도 센서)을 거울에 장착하려면, 먼저 온도 센서 위에 비전도성 열전도 페이스트를 큰 방울로 올리세요. PCB도 열전도 페이스트로 덮어 전자부품에 결로가 생기는 것을 방지하세요. 그런 다음 PCB 네 모서리에 글루건으로 큰 덩어리 4개를 만드세요. 빠르게 작업해야 합니다. 결로가 균일하게 나타난 위치에 센서를 거울에 대고 누르세요. 센서를 수평으로 놓고 거울에 단단히 눌러주세요. 글루건은 거울에 닿자마자 빠르게 굳습니다. 재배치하려면 박스 커터로 긁어내고 글루를 벗겨내 다시 시도하면 됩니다. PCB에 결로가 생기면 글루건이나 스프레이 플라스틱으로 절연하세요.
팬은 IRF520 MOSFET 모듈로 제어합니다.
광 센서로는 OPT101을 사용했는데, 감도가 좋고 출력 노이즈가 적으며 사용이 쉽습니다.
광원으로는 저전력 레이저 다이오드나 일반 LED를 쓸 수 있습니다. 둘 다 써봤는데 둘 다 잘 작동합니다. 레이저는 반응이 더 좋지만 정렬이 어렵습니다. LED는 빛이 더 많이 산란되어 반응이 평탄하지만 정렬이 쉽습니다. LED를 쓴다면 포커스된 빔을 내는 것을 쓰세요.
LED와 레이저 출력이 너무 높을 수 있는데, PWM으로 출력을 줄이면 광 센서에 간섭이 생기므로 선택지가 아닙니다. 대신 LED나 레이저에 직렬로 저항을 달아 출력을 낮추세요.
LED/레이저와 OPT101 광 센서 고정에는 구리 와이어를 꼬아 잠금 와이어로 만들어 강성을 높였습니다. 양끝은 글루건으로 고정했습니다. 개념 증명에는 좋지만 실험실(또는 지하실) 밖에서 쓰기엔 너무 약합니다. 센서와 광원 정렬은 쉽습니다. 더 나은 방법은 3D 프린터로 이 장치들을 위한 홀더를 출력하는 것인데, 외부 광선 간섭을 막기 위해 3D 프린트 인클로저는 필수입니다.
펠티어 제어에는 BTS7960 모터 드라이버를 사용했습니다. 펠티어를 구동하는 가장 효율적인 방법은 전류를 변화시키는 것이지 PWM을 쓰는 게 아닙니다. 하지만 펠티어 컨트롤러는 BTS7960 모터 드라이버만큼 쉽게 구할 수 없고, 이 개념 증명에서는 전력 소모가 문제가 아닙니다.
조립이 완료되면 아두이노에 코드를 업로드하고 포텐셔미터로 OPT101 감도 범위를 조정하세요. 저항이 높을수록 같은 빛 양에 대해 더 높은 전압 출력이 나옵니다. OPT101 데이터시트 Figure 3 - 전압 응답도 vs 조사량 참조. 디버깅용으로 거울에 입김을 불어 결로를 만들거나 센서 앞에 물체를 대보세요. 에어컨이 있다면 켜고(또는 끄고) 기다리세요. 습도 변화를 볼 수 있습니다.
아래 그래프에서 온도(파랑), 광학 읽기(빨강), 측정된 이슬점(초록)을 볼 수 있습니다. 에어컨을 막 껐을 때 이슬점이 상승하는 것을 볼 수 있습니다.
안전
제공된 코드는 지원하지 않지만, 하드웨어로는 거울을 냉각뿐만 아니라 가열할 수도 있습니다. 펠티어 극성을 반전시키면 됩니다. 거울 가열은 결로를 빠르게 제거하고 응답 시간을 개선하는 데 쓸 수 있습니다. 또한 결로를 급속 증발시키면 미세 오염물질도 제거됩니다. 하지만 이는 잠재적 안전 위험도 내포합니다. 거울 표면에 히트싱크가 없기 때문입니다. 코드가 가열 모드에서 멈추면, 잘해야 온도계 고정용 글루가 녹고, 최악이면 전선 용융으로 합선이 생겨 화재가 날 수 있습니다.
정확도
측정된 이슬점 온도가 절대값이므로, 정전 용량식이나 저항식 습도계만큼 보정이 중요하지 않습니다. 하지만 거울 표면 온도 센서 영역과 광 감지 영역 사이에는 최소한의 온도 차이가 있을 수 있습니다. 읽기 정확도를 검증해야 한다면 보정된 상용 칠드 미러 습도계로 장치를 보정할 수 있습니다.
거울 오염은 부분적으로만 문제입니다. 반사광 읽기는 절대값이 아니라 냉각 사이클 시작을 기준으로 한 상대값입니다. 냉각 사이클이 시작되면 거울에는 결로가 없습니다. 반사광 양을 측정해 기준점으로 삼고 결로를 감지합니다. 거울이 오염되어 반사광이 적어도 결로 감지에는 영향을 주지 않아야 합니다. 하지만 일부 오염물질은 결로 발생 온도를 낮추거나 높이므로, 최적의 정확도를 위해 거울 표면을 주기적으로 청소하세요.
거울과 주변 온도 센서는 높은 교정 정확도가 필요하지 않지만, 분해능은 높아야 합니다. 예를 들어 실제 온도가 24.0도인데 24.5도로 측정된다면, 거울과 주변 온도계 모두 24.5도를 측정하고(정규화 가능) 숫자가 소수점 한 자리만 지터링한다면 괜찮습니다. 많은 온도계는 0.2~0.3도 지터링이 있습니다. 거울 표면 온도와 주변 온도 측정 모두에 TSYS01 온도 센서를 사용하는 것이 더 나을 것입니다. 이 센서들은 백금 저항 온도계와 같은 0.1도 정확도를 제공합니다.
온도 센서가 거울 표면과 좋은 접촉을 하는 것이 중요합니다. 비전도성 열전도 페이스트 사용이 필수적입니다.
온도계의 응답 시간보다 빠르게 거울을 냉각시키지 마세요. 그렇지 않으면 감지된 이슬점이 부정확해집니다.
온도 센서는 광 감지 영역의 결로가 동시에 나타나는 거울 위치에 장착해야 합니다.
거울 표면에 온도 센서를 장착하면 열 분포가 변해 정확도가 떨어집니다. 적외선 온도계를 대안으로 쓰고 싶을 수 있지만, 불행히도 거울이 열복사를 일부 반사하므로 주변 환경 영향을 받아 측정값이 부정확해집니다.
기술적으로 습도 읽기는 기압에도 의존하지만 주변 환경에서는 영향이 매우 작습니다. 문 닫는 충격, 밖에서 부는 바람으로 인한 건물 내 차압 변화가 득보다 실이 클 가능성이 높습니다.
펠티어 히트싱크의 뜨거운 공기가 거울 위로 흘러가지 않게 하세요.
천천히 온도를 낮추면 더 정확한 읽기가 나오지만 응답 시간도 느려집니다. 이슬점 근처에서 온도를 진동시키면 응답 시간을 개선할 수 있습니다.
코드
칠드 미러 습도계 (C/C++)
#include
#include // 워치독 충돌 감지
// 커스텀 라이브러리들
#include "Si7021.h" // 히터 내장 습도 센서
#include // DS18B20 온도 센서
#include // DS18B20 온도 센서
// 타이머 라이브러리: https://github.com/brunocalou/Timer
#include "timer.h"
#include "timerManager.h"
// 아두이노 보드 하드웨어 핀 정의
#define coolingPWM 6
#define heatingPWM 5
#define coolingEnable 13
#define heatingEnable 12
#define tecFan 7
#define opticalSensor 0 // 아날로그 입력
#define oneWireBus A3 // DS18B20 온도 센서
// TEC 상태
#define COOLING 0
#define HEATING 1
#define OFF 2
// 타이머
Timer timerMainLoop;
Timer timerTecCooling;
Timer timerSampleNoise;
// 온도 센서 (습도 미사용)
Si7021 si7021;
// DS18B20 온도 센서
OneWire oneWire(oneWireBus);
DallasTemperature sensors(&oneWire);
float humidity = 0;
float ambientTemp = 0;
float opticalDewpoint = 0;
// 시리얼 플로터 범위 맞추려 초기값을 높게 설정
float mirrorTemp = 30;
float optical = 30;
float dewPoint = 15; // 초기값은 거울 온도보다 낮아야 함
float relativeHumidity = 30;
int tecState = OFF;
bool cooling = false;
int intervalTecCooling = 200; // TEC 타이머 업데이트 주기(ms)
float opticalThreshold = 0.5f; // 0.5
// 광학 읽기가 기준값 아래로 이만큼 떨어져야 결로 감지 플래그 설정. 신호 노이즈보다 커야 함.
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;
float calculateHumidity(float TD, float T) {
// 이슬점은 온도보다 높을 수 없음
if (TD > T) TD = T;
// August-Roche-Magnus 근사식
float rh = 100 * (exp((17.625 * TD) / (243.04 + TD)) / exp((17.625 * T) / (243.04 + T)));
return rh;
}
// TEC를 가열, 냉각, 또는 끄기로 설정
void SetTEC(int state, int amount) {
tecState = state;
// 가열과 냉각 모두에서 가열 AND 냉각 핀을 HIGH로 설정해야 함. PCB 설계자에게 물어보세요.
// TEC 제어용 드라이버: BTS7960 모터 드라이버 보드. PWM으로 TEC 구동은 비효율적, 가변 전류원 사용이 더 좋음.
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:
digitalWrite(coolingEnable, LOW);
analogWrite(coolingPWM, 0);
digitalWrite(heatingEnable, LOW);
analogWrite(heatingPWM, 0);
break;
default:
digitalWrite(coolingEnable, LOW);
analogWrite(coolingPWM, 0);
digitalWrite(heatingEnable, LOW);
analogWrite(heatingPWM, 0);
}
}
void setup() {
// 워치독 충돌 감지. 가열 모드에서 TEC가 걸리지 않게 하기 위한 안전장치.
wdt_enable(WDTO_2S); // WDTO_500MS, WDTO_1S 등 가능
Serial.begin(9600); // 9600 또는 57600
pinMode(coolingPWM, OUTPUT);
pinMode(heatingPWM, OUTPUT);
pinMode(coolingEnable, OUTPUT);
pinMode(heatingEnable, OUTPUT);
pinMode(tecFan, OUTPUT);
pinMode(opticalSensor, INPUT);
// 타이머 설정
timerMainLoop.setInterval(intervalMainLoop);
timerMainLoop.setCallback(mainLoop);
timerMainLoop.start();
timerTecCooling.setInterval(intervalTecCooling);
timerTecCooling.setCallback(tecCoolingCallback);
timerSampleNoise.setInterval(intervalTecCooling);
timerSampleNoise.setCallback(sampleNoiseCallback);
// Si7021 온도 센서 설정
uint64_t serialNumber = 0ULL;
si7021.begin();
serialNumber = si7021.getSerialNumber();
// DS18B20 원와이어 온도 센서
sensors.begin();
// 시리얼 플로터 작동을 위해 디버그 로깅 비활성화
/*
Serial.print("Si7021 serial number: ");
Serial.print((uint32_t)(serialNumber >> 32), HEX);
Serial.println((uint32_t)(serialNumber), HEX);
Serial.print("Si7021 firmware version: ");
Serial.println(si7021.getFirmwareVersion(), HEX);
*/
startNoiseSampling();
}
// 광학 센서 읽기
float getOptical() {
int opt = analogRead(opticalSensor);
float optFactored = (float)opt / 30.0f;
return optFactored;
}
// 타이머 콜백
void tecCoolingCallback() {
digitalWrite(tecFan, HIGH);
// TEC 전력을 천천히 증가
tecPwm += pwmIncrement;
// 클램핑
if (tecPwm > maxPwm) tecPwm = maxPwm;
// TEC 냉각량 설정
SetTEC(COOLING, tecPwm);
// 결로 감지?
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;
// TEC 카운터 콜백 시작
timerTecCooling.start();
}
void stopTec() {
cooling = false;
// TEC 팬 끄기
digitalWrite(tecFan, LOW);
timerTecCooling.stop();
// 냉각도 가열도 안 함
SetTEC(OFF, 0);
}
// 논블로킹 타이머
void mainLoop() {
// DS18B20 주변 온도 센서
sensors.setResolution(10); // 매 온도 측정 전 해야 함. 높은 해상도는 느림.
sensors.requestTemperatures();
ambientTemp = sensors.getTempCByIndex(0);
// Si7021 거울 온도 센서
mirrorTemp = si7021.measureTemperature();
// 광학 센서 읽기
optical = getOptical();
relativeHumidity = calculateHumidity(dewPoint, ambientTemp);
/* 읽기 쉬운 형식
Serial.print("dewPoint: ");
Serial.println(dewPoint, 2);
Serial.print("mirrorTemp: ");
Serial.println(mirrorTemp, 2);
Serial.print("ambientTemp: ");
Serial.println(ambientTemp, 2);
Serial.print("relativeHumidity: ");
Serial.println(relativeHumidity, 2);
*/
// 시리얼 플로터용
Serial.print(mirrorTemp, 2);
Serial.print(" ");
Serial.print(optical, 2);
Serial.print(" ");
Serial.println(dewPoint, 2);
// Serial.print(" ");
// Serial.println(relativeHumidity, 2);
// 결로가 사라질 때까지 대기
if (!cooling && !noiseSampling && (optical >= noiseSampleLowest)) {
startNoiseSampling();
}
}
void loop() {
// 워치독 충돌 감지
wdt_reset();
// 모든 타이머 업데이트
TimerManager::instance().update();
}