UltraV: 안전한 햇빛 노출을 위한 휴대용 UV 지수 측정기
구성품 및 부품
| 부품 |
수량 |
| Arduino Nano R3 |
1 |
| Texas Instruments ML-8511 UV 센서 |
1 |
| 128x64 OLED 디스플레이 (SSD1306) |
1 |
| MT3608 DC-DC 스텝업 컨버터 |
1 |
| CR2 (3V) 배터리 |
1 |
| CR2 배터리 홀더 |
1 |
프로젝트 소개
피부과 문제로 햇빛 노출이 어려워, 해변에서 보낼 시간을 자외선 측정기 제작에 투자했습니다. 그게 바로 UltraV입니다.
아두이노 나노 리비전 3, UV 센서, 3V 배터리 전압을 승압하는 DC/DC 컨버터, 소형 OLED 디스플레이로 구성됩니다. 주 목표는 휴대성으로, 언제 어디서든 UV 지수를 바로 확인할 수 있게 하는 것입니다.
1단계: 부품 및 구성품
- 마이크로컨트롤러: Arduino Nano rev.3
- ML8511 UV 센서
- 128×64 OLED 디스플레이 (SSD1306)
- MT3608 DC-DC 스텝업 컨버터
- CR2 배터리
- CR2 배터리 홀더
- 스위치
- 인클로저 케이스
2단계: 센서 (ML8511)
ML8511(Lapis Semiconductors)은 실내외 자외선 강도 측정에 적합한 UV 센서입니다. 내부 증폭기가 있어 광전류를 UV 강도에 따라 전압으로 변환하므로 ADC 등 외부 회로와 인터페이스가 쉽습니다. 파워다운 모드에서 대기 전류가 0.1µA에 불과해 배터리 수명이 깁니다.
특징:
- UV-A 및 UV-B 감지 포토다이오드
- 내장 연산 증폭기
- 아날로그 전압 출력
- 저전류 소모 (동작 300µA, 대기 0.1µA)
- 소형 박형 패키지 (4.0×3.7×0.73mm, 12핀 세라믹 QFN)
불행히도 센서를 보호할 UV 투과 소재를 구하지 못했습니다. 테스트한 모든 투명 커버(플라스틱, 유리 등)가 UV 측정을 감쇠시켰습니다. 최선은 융합 석영 유리지만 합리적 가격에 구할 수 없어 센서를 박스 밖 개방 공간에 노출시켰습니다.
3단계: 작동 방법
측정하려면 기기를 켜고 태양 방향으로 몇 초간 향하게 한 뒤 디스플레이를 봅니다. 왼쪽 인덱스는 순간 측정값(200ms마다 갱신), 오른쪽은 세션 중 최대값으로 이것이 실제 참고값입니다. 좌측 하단에는 WHO 기준 등급(LOW, MODERATE, HIGH, VERY HIGH, EXTREME)이 표시됩니다.
4단계: 배터리 전압 및 측정
크기와 용량(800mAh) 때문에 CR2 배터리를 선택했습니다. 여름 내내 사용했는데 배터리가 아직 2.8V로 만족스럽습니다. 동작 시 약 100mA 소모하지만 측정은 몇 초면 끝납니다. 배터리 공칭 전압 3V를 MT3608로 9V로 승압해 Vin 핀에 연결했습니다.
배터리 전압 표시를 위해 아날로그 입력 A2를 사용했습니다. 아두이노 아날로그 입력은 0-5V DC 측정 가능하지만 보정이 필요합니다. 멀티미터로 아두이노 5V 핀(레귤레이터 출력) 실측값을 스케치에 반영하세요(예: 5.023V 읽혔다면 voltage = ((long)sum / (long)NUM_SAMPLES * 5023) / 1024.0;). 스케치에서 10개 샘플 평균을 냅니다.
5단계: 회로도 및 연결
(회로도 이미지 참조)
6단계: 소프트웨어
디스플레이는 U8g2lib를 사용해 폰트 선택과 위치 제어가 자유롭습니다.
ML8511 전압 읽기는 아두이노 3.3V 레퍼런스 핀(1% 정확도)을 ADC 기준으로 사용합니다. 3.3V 핀을 A1에 연결해 ADC 읽기를 한 뒤, 센서 읽기와 비교해 실제 전압을 역산출합니다(VIN이 3.4V 이상이면 상관없음).
int uvLevel = averageAnalogRead(UVOUT);
int refLevel = averageAnalogRead(REF_3V3);
float outputVoltage = 3.3 / refLevel * uvLevel;
전체 코드는 여기서 다운로드 가능합니다.
7단계: 인클로저 케이스
시판 플라스틱 박스에 디스플레이 창을 직접 뚫어보니 영 안 예뻐서 CAD로 직접 설계했습니다. 최대한 작게 만들기 위해 CR2 배터리를 배터리 홀더와 함께 후면 외부에 장착했습니다.
STL 파일: UltraV.stl.zip
8단계: 향후 개선 사항
- UV 분광기로 실제 UV 지수 측정 및 다양한 조건에서 ML8511 출력 동시 기록
- 대기 조건별 ML8511 출력을 실시간 실제 UVI 값과 연관시키는 알고리즘 개발
9단계: 이미지 갤러리
(완성된 기기 사진들)
10단계: 크레딧
코드
UltraV (C/C++)
/* Fabio Marzocca @ 2018
ADC는 VCC에 완전히 의존. USB 전원 시 4.75V~5.25V로 불확실해 부정확.
정확한 온보드 3.3V 레퍼런스(1% 정확도) 사용해 보정.
v2.0.0 - 2018.07 - 16x2 LCD에서 OLED로 변경
v2.0.1 - 2018.09 - 배터리 읽기 함수 변경 */
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, SCL, SDA, U8X8_PIN_NONE);
#define FIRST_ROW_Y 16
#define FIRST_ROW_X 16
#define BOX_H 38
const int UVOUT = A0;
const int REF_3V3 = A1;
const int VBATT = A2;
float maxUV = 0;
void setup() {
pinMode(UVOUT, INPUT);
pinMode(REF_3V3, INPUT);
pinMode(VBATT, INPUT);
u8g2.begin();
}
void loop() {
u8g2.firstPage();
do {
int uvLevel = averageAnalogRead(UVOUT);
int refLevel = averageAnalogRead(REF_3V3);
float outputVoltage = 3.3 / refLevel * uvLevel;
float uvIntensity = mapfloat(outputVoltage, 0.99, 2.6, 0.0, 15.0);
readBattery();
if (maxUV < uvIntensity) maxUV = uvIntensity;
u8g2.drawFrame(0, FIRST_ROW_Y+1, 128, BOX_H);
u8g2.setFont(u8g2_font_logisoso18_tf);
u8g2.setCursor(10, 40); u8g2.print(uvIntensity); // 순간 UV
u8g2.setCursor(70, 40); u8g2.print(maxUV); // 최대 UV
u8g2.setFont(u8g2_font_u8glib_4_tf);
u8g2.setCursor(10, 52); u8g2.print(F("INSTANT"));
u8g2.setCursor(75, 52); u8g2.print(F("MAXIMUM"));
showUVCategory();
u8g2.setCursor(88, 64); u8g2.print(F("F.Marzocca"));
} while (u8g2.nextPage());
delay(200);
}
void showUVCategory() {
char strCat[12];
byte categ = (byte)(maxUV + 0.5); // 반올림
if (categ >= 0 && categ < 3) strcpy(strCat, "LOW");
else if (categ >= 3 && categ < 6) strcpy(strCat, "MODERATE");
else if (categ >= 6 && categ < 8) strcpy(strCat, "HIGH !");
else if (categ >= 8 && categ < 10) strcpy(strCat, "VERY HIGH!");
else if (categ >= 11) strcpy(strCat, "EXTREME!");
u8g2.setCursor(0, 64); u8g2.print(strCat);
}
int averageAnalogRead(int pinToRead) {
byte numberOfReadings = 16;
unsigned int runningValue = 0;
for (int x = 0; x < numberOfReadings; x++) runningValue += analogRead(pinToRead);
return runningValue / numberOfReadings;
}
float mapfloat(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
void readBattery() {
long battery = readBattVcc();
long VccMin = 2300;
byte batteryBar = (battery - VccMin) / 140;
u8g2.setFont(u8g2_font_open_iconic_embedded_2x_t);
u8g2.drawGlyph(1, FIRST_ROW_Y, 73); // 배터리 아이콘
u8g2.setFont(u8g2_font_open_iconic_play_1x_t);
for (byte i = 1; i <= batteryBar; i++) {
u8g2.drawGlyph(128 - 9*i, FIRST_ROW_Y - 4, 75); // 배터리 잔량 바
}
u8g2.setFont(u8g2_font_freedoomr10_tu);
u8g2.setCursor(25, FIRST_ROW_Y);
u8g2.print(float(battery) / 1000, 3); // 전압 표시
}
long readBattVcc() {
int sum = 0, sample_count = 0;
long voltage = 0;
#define NUM_SAMPLES 10
while (sample_count < NUM_SAMPLES) {
sum += analogRead(VBATT);
sample_count++;
delay(2);
}
voltage = ((long)sum / (long)NUM_SAMPLES * 5020) / 1024.0; // 보정된 레퍼런스 전압
return voltage; // 밀리볼트 단위
}