|
|
민선생의 전자공방 ・ 2026. 5. 31. 21:56
| 브레드보드 활용 IO 인터페이스6편 아두이노와 CdS 조도센서 + MQ-135 + DHT11 + OLED 환경 통합 모니터 빛·공기질·온습도 3센서 통합 · OLED 자동 밝기 조절 · 실시간 환경 대시보드 아두이노 프로젝트 · 민선생의 전자공방 |
📖 프롤로그 — 3센서 통합 환경 모니터의 완성
5편에서는 MQ-135 가스 센서와 DHT11을 결합하여 공기질과 온습도를 하나의 OLED 대시보드로 통합하는 방법을 배웠습니다. 이번 6편에서는 '눈'에 해당하는 CdS 광센서(조도센서)를 추가합니다.
이번 편의 핵심은 두 가지입니다. 첫째, 빛의 밝기를 측정하여 OLED 화면 밝기를 자동으로 조절하는 기능입니다. 어두운 곳에서는 화면이 눈부시지 않게, 밝은 곳에서는 잘 보이게 자동 조절됩니다. 둘째, 5편의 MQ-135·DHT11과 함께 3센서 통합 환경 모니터를 완성합니다. 빛·온습도·공기질을 하나의 화면에서 실시간으로 확인하는 완성형 대시보드입니다.
접기/펴기
Cds의 입력값을 OLED에 그래픽 바로 표시하고, 기존 프로젝트와 같이 DHT11 온도 습도, MQ-135센서로 공기질을, Cds로 밝기를 표시하고,주위 발기에 따라 OLED의 밝기를 연동해 조절하는 동작 영상
Cds의 입력값을 OLED에 그래픽 바로 표시하고, 기존 프로젝트와 같이 DHT11 온도 습도, MQ-135센서로 공기질을, Cds로 밝기를 표시하고,주위 밝기에 따라 OLED의 밝기를 연동해 조절하는 동작 영상
1장. CdS 조도 센서란? — 빛을 감지하는 마법의 저항
▶ 1.1 CdS 센서란? — 빛을 감지하는 마법의 저항
CdS 센서는 '황화카드뮴(Cadmium Sulfide)'이라는 물질로 만들어진 광전도 셀(Photoresistor)입니다. 쉽게 말해 '빛의 밝기에 따라 저항값이 변하는 가변 저항'입니다.
| 상태 | CdS 내부 | 저항값 | analogRead 출력 (풀업 회로) |
| 완전 어두움 | 전자가 묶여 이동 불가 | 수십 kΩ ~ 수 MΩ | 높은 값 (~1023) |
| 실내 조명 | 일부 전자 자유 이동 | 수 kΩ ~ 수십 kΩ | 중간 값 (~400~700) |
| 밝은 직사광선 | 전자가 활발히 자유 이동 | 수십 Ω ~ 수 kΩ | 낮은 값 (~50~200) |
Cds 센서
▶ 1.2 CdS 센서 내부 구조 — 왜 지그재그 모양일까?
CdS 센서의 상단에는 특유의 꼬불꼬불한 지그재그 패턴이 보입니다. 이것은 무작위 디자인이 아니라 매우 영리한 공학적 설계입니다.
| 🔬 지그재그 패턴의 비밀 황화카드뮴(CdS) 감지 물질은 세라믹 기판 위에 얇게 발라져 있습니다. • 면적 최대화: 같은 크기의 원형 센서 안에 빛을 받는 물질의 총 표면적을 지그재그 패턴으로 최대한 넓힙니다. 넓을수록 빛에 더 민감하게 반응합니다. • 유효 길이 연장: 두 전극(리드선) 사이를 지그재그로 길게 늘이면 저항값의 변화 폭이 커져 감도가 향상됩니다. • 비유: 운동장 트랙을 직선 1개 vs 지그재그 10줄로 그을 때, 달릴 수 있는 거리가 훨씬 길어지는 것과 같은 원리입니다. |
| 💡 투명 밀폐 캡의 역할 센서 표면의 황화카드뮴 물질은 습기와 먼지에 매우 취약합니다. 유리 또는 플라스틱 투명 캡이 이를 보호하면서도 빛은 100% 통과시킵니다. 이 캡 덕분에 실내외 어디서나 안정적으로 동작할 수 있습니다. |
▶ 1.3 빛이 저항을 바꾸는 물리적 원리 — 밴드갭과 광전자
빛이 비치면 저항이 작아진다"는 현상의 배경에는 반도체 물리학의 핵심 개념인 밴드갭(Band Gap, 금지대) 이론이 있습니다. 고등학교 물리학 '빛과 물질' 단원에서 다루는 내용입니다. 처음 접하시더라도 아래 설명을 따라오시면 충분히 이해할 수 있습니다
| ⚛️ CdS 광전도 현상의 핵심 물리 원리 — 밴드갭 이론 ① 빛(광자)의 에너지 전달 센서 표면에 빛이 닿으면 빛을 구성하는 알갱이인 광자(Photon)가 황화카드뮴(CdS) 물질 속으로 파고들어갑니다. 광자는 파장이 짧을수록(보라~파랑 계열) 더 높은 에너지를 가지고 있으며, CdS는 가시광선 전 영역의 광자에 반응합니다. ② 밴드갭 장벽과 전자의 도약 (Excitation) 반도체 물질 내부의 전자에는 머물 수 있는 에너지 구역이 두 가지 있습니다. 가전자대(Valence Band): 에너지가 가장 낮은 안정된 상태. 전자가 원자핵에 꽁꽁 묶여 이동하지 못합니다. 마치 집 안 소파에 푹 파묻혀 꼼짝 못하는 상태와 같습니다. 전기가 통하지 않아 저항이 매우 높습니다. 밴드갭(Band Gap): 가전자대와 전도대 사이의 ‘금지 구역’. 전자는 이 구간에 존재할 수 없습니다. 반드시 이 장벽보다 큰 에너지를 한 번에 받아야만 넘어갈 수 있습니다. CdS의 밴드갭은 약 2.42eV로, 가시광선 광자의 에너지(1.8~3.1eV)와 딱 맞아 빛에 잘 반응합니다. 광자의 에너지가 밴드갭보다 크면, 전자는 에너지를 흡수하여 밴드갭을 단번에 뛰어넘는 강력한 수직 점프(여기, Excitation)를 합니다. ③ 전도대의 자유전자(광전자) 탄생 밴드갭 장벽을 넘은 전자는 에너지가 높은 전도대(Conduction Band)에 안착합니다. 전도대에 도달한 전자(광전자, Photoelectron)는 원자핵의 구속에서 완전히 벗어나 물질 내를 자유롭게 이동할 수 있게 됩니다. 마치 꽉 막힌 골목을 벗어나 고속도로에 올라탄 자동차처럼, 전압이 걸리면 즉시 전류를 운반합니다. 동시에 전자가 빠져나간 가전자대에는 양전하를 띤 빈 자리 ‘정공(Hole)’이 생겨납니다. 이 정공 역시 인접 전자들이 차례로 메우며 이동하므로 전류 운반에 기여합니다. 전자-정공 쌍(Electron-Hole Pair)이 탄생하는 것입니다. ④ 물리적 결과 — 저항의 급격한 감소 빛이 강할수록 더 많은 광자가 쏟아져 전자를 전도대로 올려보냅니다. 광전자의 수가 폭발적으로 증가하면 전류를 운반할 수 있는 자유 전하 운반자(Charge Carrier)가 물질 내에 넘쳐납니다. 그 결과: • 전기 전도도(Conductivity)가 급격히 상승 • 센서 저항값이 수십 MΩ(암흑)에서 수십 Ω(강한 빛)으로 수만 배 급감 • 이 저항 변화가 분압 회로를 통해 전압 변화로 변환 → analogRead() 값 변화 결국 analogRead(A0)가 반환하는 0~1023 숫자 하나에, 광자→전자 도약→저항 변화→분압→전압 변화라는 물리 과정 전체가 압축되어 있는 셈입니다. 📊 한눈에 보는 밴드갭 전환 과정 어두울 때: [가전자대 전자 묶임] → 저항 수MΩ → analogRead 高 빛 조사 시: 광자 → 전자 여기(Excitation) → 밴드갭 도약 → [전도대 자유전자(광전자) 생성] + [가전자대 정공 생성] → 전도도 급상승 → 저항 변화(그에 따른 풀업저항과의 강하전압변화) → 전압의 변화를 읽음(analogRead) |
AI 활용
CdS 광전도 현상 — 광자(빛)가 가전자대의 전자를 때려 전도대로 올려보내는 과정. HIT! 충돌 지점에 정공(+)이 생성되고, 자유전자는 밴드갭(금지대에는 전자가 존재할 수 없는 영역입니다.)을 뛰어 넘어 전도대로 이동해 '전도대 고속도로'를 달리며 전류를 운반합니다. -> 전류가 잘흐른다(저항이 작아진다), 전류가 더 많이 잘 흐른다(저항이 더 작아진다), 그러다 에너지를 잃으면 전도대에서 가전자대로 다시 떨어지고 전류는 흐르지 않게됩니다.(저항이 다시 증가한다)
▶ 1.4 왜 저항값을 바로 읽지 못할까? — 전압 분배 법칙
아두이노의 아날로그 핀(A0~A5)은 저항값을 직접 읽을 수 없습니다. 오직 '전압(Voltage)'만 0~1023의 숫자로 읽어낼 수 있습니다. 따라서 CdS의 '저항 변화'를 '전압 변화'로 바꿔주는 설계가 필요합니다.
| 📐 전압 분배 법칙 (Voltage Divider) Vout = Vcc × R측정부 / (R고정부 + R측정부) 【 풀업 방식 (이번 편 채택) 】 5V ─── R1(10kΩ 고정) ─── V1(측정점, A0) ─── CdS ─── GND 밝을 때: CdS 저항 ↓ → V1이 GND쪽으로 당겨짐 → 전압 ↓ → analogRead ↓ 어두울 때: CdS 저항 ↑ → V1이 5V쪽으로 당겨짐 → 전압 ↑ → analogRead ↑ 즉, 밝을수록 analogRead() 값이 작아지는 '반비례' 특성을 가집니다. |
2장. 풀업(Pull-up) 회로의 원리와 설계
▶ 2.1 풀다운 vs 풀업 완전 비교
CdS를 아두이노에 연결할 때 10kΩ 고정 저항을 어디에 배치하느냐에 따라 회로 동작이 완전히 달라집니다.
Cds 풀업, 풀다운 회로 연결 방법
| 비교 항목 | 풀다운 (Pull-down) | 풀업 (Pull-up) ← 이번 편 |
| 회로 구성 | 5V → CdS → A0 → 10kΩ → GND | 5V → 10kΩ → A0 → CdS → GND |
| 밝을 때 출력 | analogRead 증가 (High) | analogRead 감소 (Low) |
| 어두울 때 출력 | analogRead 감소 (Low) | analogRead 증가 (High) |
| 코딩 직관성 | 밝음=큰값 → 직관적 | 밝음=작은값 → 반전 매핑 필요 |
| 노이즈 안정성 | 상대적으로 약함 | 뛰어남 (실무 표준 방식) |
| 실무 활용도 | 입문 교육용 | 센서·스위치 회로의 표준 |
| 📌 왜 풀업 방식이 실무 표준일까? 풀업 방식은 MCU 핀이 '기본적으로 High 상태'를 유지합니다. 배선이 끊기거나 센서가 연결 해제되었을 때 핀이 허공에 떠서(Floating) 노이즈를 마구 집어넣는 현상을 방지합니다. 아두이노 내장 풀업 저항(INPUT_PULLUP)도 같은 원리입니다. 전문 회로 설계에서 스위치·센서 입력은 대부분 풀업 방식을 씁니다. |
▶ 2.2 분압 원리를 숫자로 이해하기
10kΩ 풀업 + CdS 센서 조합에서 실제 전압이 어떻게 변하는지 계산해 봅니다.
| 조건 | CdS 저항(Rs) | Vout 계산 | analogRead 값 (약) |
| 아주 밝은 직사광선 | 100Ω | 5 × 100/(10000+100) ≈ 0.05V | 약 10 |
| 일반 실내 조명 | 5kΩ | 5 × 5000/(10000+5000) ≈ 1.67V | 약 342 |
| 어두운 실내 | 50kΩ | 5 × 50000/(10000+50000) ≈ 4.17V | 약 854 |
| 완전 암흑 | 1MΩ | 5 × 1M/(10k+1M) ≈ 4.95V | 약 1014 |
3장. CdS 센서 활용 사례와 OLED 자동 밝기 설계
▶ 3.1 CdS 센서의 일상 속 활용 예
CdS 센서는 단순하지만 견고하여 우리 주변 곳곳에서 사용됩니다.
• 스마트폰 자동 밝기: 주변이 밝으면 화면 밝게, 어두우면 화면 어둡게 자동 조절 → 눈 보호 + 배터리 절약 (이번 6편의 핵심 기능!)
• 가로등·간판 자동 제어: 해가 지면 주변이 어두워지고, CdS 저항이 커지면 → 릴레이 작동 → 조명 자동 점등
• 자동차 오토라이트: 터널 진입이나 야간 주행 시 CdS가 어두움을 감지 → 전조등 자동 점등
• 복사기·프린터 용지 감지: 용지가 있으면 빛이 차단(어두움) → 용지 유무 자동 감지
• 카메라 자동 노출: 빛의 양에 따라 셔터 속도와 조리개 자동 조절
▶ 3.2 OLED 자동 밝기 조절 원리
SSD1306 OLED는 display.ssd1306_command(SSD1306_SETCONTRAST) 명령으로 밝기(Contrast)를 0~255 사이에서 조절할 수 있습니다.
| 💡 자동 밝기 조절 설계 흐름 1단계: CdS analogRead() → 0~1023 값 읽기 2단계: map() 함수로 반비례 변환 → map(cdsVal, 밝음_최소, 어두움_최대, 255, 10) → 밝을수록(cdsVal 작음) Contrast 높게 (255) → 어두울수록(cdsVal 큼) Contrast 낮게 (10) 3단계: display.ssd1306_command()로 OLED 밝기 즉시 적용 4단계: 값이 껌벅이지 않도록 10회 평균값 사용(Smoothing) |
| ⚠️ map() 반비례 매핑 핵심 풀업 방식이므로 밝을 때 analogRead 값이 작고, 어두울 때 큽니다. OLED는 밝은 환경에서 Contrast를 높여야(255) 잘 보입니다. 따라서 map()의 출력 범위를 [255, 10]으로 뒤집어서 적용합니다. int brightness = map(cdsAvg, CDS_BRIGHT, CDS_DARK, 255, 10); brightness = constrain(brightness, 10, 255); → cdsAvg 작음(밝음) → brightness 255(최대) → cdsAvg 큼(어두움) → brightness 10(최소) |
4장. 브레드보드 회로 배선
▶ 4.1 부품 목록
| 부품 | 수량 | 비고 |
| 아두이노 Uno R3 | 1 | 또는 호환 보드 |
| OLED 128x64 I2C (SSD1306) | 1 | 1편부터 계속 사용 |
| DHT11 센서 모듈 (3핀) | 1 | 4편부터 계속 사용 |
| MQ-135 가스 센서 모듈 (Keyes) | 1 | 5편부터 계속 사용 |
| CdS 조도 센서 (20kΩ 규격) | 1 | 이번 편 신규 추가 |
| 저항 10kΩ | 1 | CdS 풀업용 고정 저항 |
| 브레드보드 | 1 | 대형 권장 (830홀) |
| 점퍼 선 | 다수 | M-M 타입 |
▶ 4.2 CdS 풀업 회로 연결
CdS 센서는 모듈 형태가 아닌 단독 부품이므로 10kΩ 저항을 직접 브레드보드에 꽂아 분압 회로를 구성합니다.
| 🔌 CdS 풀업 회로 연결 순서 ① 10kΩ 저항 한쪽 끝 → 브레드보드 전원 레일 5V (빨간선) ② 10kΩ 저항 다른쪽 끝 → 브레드보드 임의 행 (A열 예: e10) ③ CdS 센서 한쪽 리드 → 같은 행 (e10과 같은 줄) ④ CdS 센서 다른쪽 리드 → 브레드보드 전원 레일 GND (검은선) ⑤ 10kΩ 저항과 CdS의 연결 지점(e10) → 점퍼선으로 아두이노 A0 연결 ※ CdS 센서는 극성이 없습니다. 어느 쪽을 위로 꽂아도 동일하게 동작합니다. |
브레드보드 전체 배선도 (Wokwi) — OLED + DHT11 + MQ-135 + CdS 4부품 통합
▶ 4.3 전체 핀 연결표 (6편 통합)
| 부품 | 부품 핀 | 아두이노 핀 | 비고 |
| OLED | VCC | 5V | 1편 배선 그대로 |
| OLED | GND | GND | 1편 배선 그대로 |
| OLED | SDA | A4 | 1편 배선 그대로 |
| OLED | SCL | A5 | 1편 배선 그대로 |
| DHT11 | GND | GND | 4편 배선 그대로 |
| DHT11 | VCC | 5V | 4편 배선 그대로 |
| DHT11 | DATA (S) | D2 | 4편 배선 그대로 |
| MQ-135 | VCC | 5V | 5편 배선 그대로 |
| MQ-135 | GND | GND | 5편 배선 그대로 |
| MQ-135 | DO | D7 | 5편 배선 그대로 |
| MQ-135 | AO | A1 | 5편 배선 그대로 |
| CdS + 10kΩ | 분압 측정점 | A0 | 신규 연결 (풀업 회로) |
| 📌 민선생 배선 팁 — A4·A5는 I2C 전용! 아두이노 Uno에서 A4=SDA, A5=SCL은 I2C 통신(OLED)이 독점 사용합니다. CdS를 A0, MQ-135를 A1에 연결하면 A2~A3는 앞으로 추가 센서용으로 남습니다. 디지털 핀도 D2(DHT11), D7(MQ-135 DO) 외에 D3~D6, D8~D13 사용 가능합니다. |
실물 브레드보드 완성 배선 사진
5장. 라이브러리 설치
CdS 센서는 MQ-135와 마찬가지로 analogRead() 하나로 동작합니다. OLED 밝기 조절은 Adafruit SSD1306 라이브러리에 이미 포함된 명령어를 사용합니다. 추가 설치 없이 바로 진행합니다.
| 라이브러리 | 용도 | 설치 상태 |
| Wire.h | I2C 통신 (OLED) | 아두이노 기본 내장 |
| Adafruit_GFX.h | OLED 그래픽 | 1편에서 설치 완료 |
| Adafruit_SSD1306.h | SSD1306 드라이버 + 밝기 조절 | 1편에서 설치 완료 |
| DHT.h | DHT11 통신 | 4편에서 설치 완료 |
| 없음 (추가 불필요) | CdS (analogRead 사용) | 추가 설치 불필요 ✅ |
6장. 소스코드
▶ 6.1 동작 설명
| 항목 | 내용 |
| 온도/습도 표시 | DHT11로 2초마다 측정, OLED 상단 표시 |
| 공기질 표시 | MQ-135 AO로 500ms마다 측정, 바 그래프+상태 |
| 조도 표시 | CdS로 200ms마다 측정, 조도 단계(5단계) 표시 |
| OLED 자동 밝기 | CdS 값으로 OLED Contrast 자동 조절 (10~255) |
| 경보 기능 | MQ-135 DO 핀 LOW 시 화면 반전 경보 |
| Smoothing | CdS 10회 평균값으로 화면 껌벅임 방지 |
| millis() 비블로킹 | DHT11(2초), MQ-135(500ms), CdS(200ms) 독립 동작 |
▶ 6.2 OLED 화면 레이아웃 (6편 통합)
| Y 위치 | 크기 | 표시 내용 |
| y=0 | TextSize(1) | ENV MONITOR 타이틀 |
| y=10 | ───── | 가로 구분선 |
| y=13 | TextSize(1) | T: 온도 + H: 습도 + 온도상태 |
| y=23 | ───── | 가로 구분선 |
| y=26 | TextSize(1)+2 | AQI 수치(큰글자) + [GOOD/FAIR/POOR/DANGER] |
| y=38 | 바 그래프 | 공기질 바 그래프 (G/F/P/D 경계선) |
| y=48 | TextSize(1) | LUX: 조도 단계 (DARK/DIM/NORMAL/BRIGHT/SUNNY) |
| y=57 | TextSize(1) | 밝기 자동조절 표시 (AUTO BRI: XXX) |
▶ 6.3 전체 소스코드
| //================================================ // 민선생의 전자공방 - 브레드보드 IO 인터페이스 6편 // CdS 조도 + MQ-135 공기질 + DHT11 온습도 // + OLED 통합 환경 대시보드 + 자동 밝기 조절 // 보드: Arduino Uno R3 // OLED: SSD1306 128x64 I2C (A4=SDA, A5=SCL, 0x3C) // DHT11: DATA=D2 // MQ-135: AO=A1, DO=D7 // CdS: A0 (10kΩ 풀업) //================================================ // ── 라이브러리 포함 ───────────────────────────── #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <DHT.h> // ── OLED 설정 ──────────────────────────────────── #define SCREEN_W 128 #define SCREEN_H 64 #define OLED_ADDR 0x3C #define OLED_RESET -1 // ── 센서 핀 설정 ────────────────────────────────── #define DHT_PIN 2 // DHT11 DATA #define DHT_TYPE DHT11 #define MQ135_AO A1 // MQ-135 아날로그 #define MQ135_DO 7 // MQ-135 디지털 경보 #define CDS_PIN A0 // CdS 조도 (10kΩ 풀업) // ── MQ-135 공기질 임계값 ───────────────────────── #define AQI_GOOD 200 #define AQI_FAIR 400 #define AQI_POOR 600 // ── CdS 조도 캘리브레이션 ──────────────────────── // ※ 환경에 따라 아래 값을 Serial Monitor로 확인 후 조정하세요 #define CDS_BRIGHT 80 // 밝은 환경 최솟값 (직사광선) #define CDS_DARK 900 // 어두운 환경 최댓값 (암실) #define CDS_SMOOTH 10 // 평균 샘플 수 (껌벅임 방지) // ── 측정 주기 ───────────────────────────────────── #define DHT_INTERVAL 2000 // DHT11: 2초 #define MQ_INTERVAL 500 // MQ-135: 0.5초 #define CDS_INTERVAL 200 // CdS: 0.2초 #define WARMUP_SEC 60 // MQ-135 예열(초) // ── 객체 생성 ───────────────────────────────────── Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, OLED_RESET); DHT dht(DHT_PIN, DHT_TYPE); // ── 전역 변수 ───────────────────────────────────── unsigned long prevDHT = 0; unsigned long prevMQ = 0; unsigned long prevCDS = 0; float lastTemp = 0; float lastHumi = 0; int lastAQI = 0; int lastCDS = 500; // 초기값: 중간 밝기 int cdsBuf[CDS_SMOOTH]; // Smoothing 버퍼 int cdsBufIdx = 0; // ── CdS Smoothing 초기화 ────────────────────────── void initCDSBuf() { for (int i = 0; i < CDS_SMOOTH; i++) cdsBuf[i] = 500; // 중간값으로 초기화 } // ── CdS 평균값 계산 ─────────────────────────────── int readCDSSmooth() { cdsBuf[cdsBufIdx] = analogRead(CDS_PIN); cdsBufIdx = (cdsBufIdx + 1) % CDS_SMOOTH; long sum = 0; for (int i = 0; i < CDS_SMOOTH; i++) sum += cdsBuf[i]; return (int)(sum / CDS_SMOOTH); } // ── 조도 단계 문자열 반환 ──────────────────────── String getLuxStatus(int val) { // 풀업 방식: 값 작음=밝음, 값 큼=어두움 if (val < 100) return "SUNNY"; // 직사광선 else if (val < 300) return "BRIGHT"; // 밝은 실내 else if (val < 600) return "NORMAL"; // 일반 실내 else if (val < 850) return "DIM"; // 어두운 실내 else return "DARK"; // 암실 } // ── OLED 밝기 자동 설정 ────────────────────────── void setAutoBrightness(int cdsVal) { // 풀업: cdsVal 작음(밝음) → Contrast 높게(255) // cdsVal 큼(어두움) → Contrast 낮게(10) int brightness = map(cdsVal, CDS_BRIGHT, CDS_DARK, 255, 10); brightness = constrain(brightness, 10, 255); display.ssd1306_command(SSD1306_SETCONTRAST); display.ssd1306_command(brightness); } // ── 공기질 단계 ─────────────────────────────────── String getAQIStatus(int val) { if (val <= AQI_GOOD) return "GOOD"; else if (val <= AQI_FAIR) return "FAIR"; else if (val <= AQI_POOR) return "POOR"; else return "DANGER"; } // ── 온도 상태 ───────────────────────────────────── String getTempStatus(float t) { if (t < 10) return "COLD"; else if (t <= 28) return "NORM"; else if (t <= 35) return "WARM"; else return "HOT!"; } // ── 예열 카운트다운 ─────────────────────────────── void showWarmup(int remaining) { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(1); display.setCursor(20, 0); display.print("MQ-135 WARM-UP"); display.drawLine(0, 10, 127, 10, SSD1306_WHITE); display.setCursor(10, 18); display.print("Heating sensor..."); display.setTextSize(3); display.setCursor(44, 32); display.print(remaining); display.setTextSize(1); display.setCursor(20, 56); display.print("sec remaining"); display.display(); } // ── 경보 화면 ───────────────────────────────────── void showAlarm(int aqiVal) { display.invertDisplay(true); display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(2); display.setCursor(15, 10); display.print("!! ALARM !!"); display.setTextSize(1); display.setCursor(10, 35); display.print("Air Quality DANGER"); display.setCursor(25, 48); display.print("AQI: "); display.print(aqiVal); display.display(); delay(1500); display.invertDisplay(false); } // ── 통합 대시보드 ───────────────────────────────── void showDashboard(float temp, float humi, int aqi, int cds) { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); // 타이틀 display.setTextSize(1); display.setCursor(20, 0); display.print("ENV MONITOR"); display.drawLine(0, 10, 127, 10, SSD1306_WHITE); // 온도 display.setCursor(0, 13); display.print("T:"); if (!isnan(temp)) { display.setCursor(12, 13); display.print((int)temp); display.print((char)247); display.print("C "); display.print(getTempStatus(temp)); } else { display.print("--"); } // 습도 display.setCursor(80, 13); display.print("H:"); if (!isnan(humi)) { display.setCursor(92, 13); display.print((int)humi); display.print("%"); } else { display.print("--"); } display.drawLine(0, 23, 127, 23, SSD1306_WHITE); // 공기질 String aqiStat = getAQIStatus(aqi); display.setCursor(0, 26); display.print("AQI:"); display.setTextSize(2); display.setCursor(28, 23); display.print(aqi); display.setTextSize(1); display.setCursor(80, 26); display.print("["); display.print(aqiStat); display.print("]"); // AQI 바 그래프 display.drawRect(0, 36, 128, 7, SSD1306_WHITE); int aqiBar = constrain(map(aqi, 0, 1023, 0, 126), 0, 126); display.fillRect(1, 37, aqiBar, 5, SSD1306_WHITE); display.drawFastVLine(25, 35, 9, SSD1306_WHITE); // G/F 경계 display.drawFastVLine(50, 35, 9, SSD1306_WHITE); // F/P 경계 display.drawFastVLine(74, 35, 9, SSD1306_WHITE); // P/D 경계 // 조도 단계 display.setCursor(0, 46); display.print("LUX:"); display.print(getLuxStatus(cds)); display.print(" ("); display.print(cds); display.print(")"); // 자동 밝기 표시 int bri = constrain(map(cds, CDS_BRIGHT, CDS_DARK, 255, 10), 10, 255); display.setCursor(0, 57); display.print("AUTO BRI:"); display.print(bri); // 밝기 미니 바 int briBar = map(bri, 10, 255, 0, 60); display.fillRect(68, 58, briBar, 4, SSD1306_WHITE); display.drawRect(67, 57, 62, 6, SSD1306_WHITE); display.display(); } // ───────────────────────────────────────────────── void setup() { Serial.begin(9600); pinMode(MQ135_DO, INPUT); dht.begin(); initCDSBuf(); if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { while(1); } display.clearDisplay(); // MQ-135 예열 (60초) for (int i = WARMUP_SEC; i > 0; i--) { showWarmup(i); // 예열 중에도 CdS 읽어서 OLED 밝기 미리 적용 setAutoBrightness(readCDSSmooth()); delay(1000); } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(20, 25); display.print("Warm-up Complete!"); display.setCursor(15, 40); display.print("Starting monitor..."); display.display(); delay(2000); } // ───────────────────────────────────────────────── void loop() { unsigned long now = millis(); // ── DHT11: 2초마다 ────────────────────────── if (now - prevDHT >= DHT_INTERVAL) { prevDHT = now; lastHumi = dht.readHumidity(); lastTemp = dht.readTemperature(); if (isnan(lastHumi) || isnan(lastTemp)) { Serial.println("DHT read failed!"); } else { Serial.print("T:"); Serial.print(lastTemp,1); Serial.print("C H:"); Serial.print(lastHumi,1); Serial.println("%"); } } // ── MQ-135: 0.5초마다 ─────────────────────── if (now - prevMQ >= MQ_INTERVAL) { prevMQ = now; lastAQI = analogRead(MQ135_AO); if (digitalRead(MQ135_DO) == LOW) { showAlarm(lastAQI); } Serial.print("AQI:"); Serial.print(lastAQI); Serial.print(" ["); Serial.print(getAQIStatus(lastAQI)); Serial.println("]"); } // ── CdS: 0.2초마다 ────────────────────────── if (now - prevCDS >= CDS_INTERVAL) { prevCDS = now; lastCDS = readCDSSmooth(); // 10회 평균값 setAutoBrightness(lastCDS); // OLED 밝기 즉시 적용 Serial.print("LUX:"); Serial.print(lastCDS); Serial.print(" ["); Serial.print(getLuxStatus(lastCDS)); Serial.println("]"); } // ── 대시보드 갱신 (CdS 측정 직후) ────────── if (now - prevCDS < 10) { showDashboard(lastTemp, lastHumi, lastAQI, lastCDS); } } |
7장. 핵심 코드 해설
▶ 7.1 Smoothing — 껌벅임 없는 부드러운 밝기 조절
CdS 센서는 형광등 깜빡임이나 순간적인 그림자에도 값이 흔들립니다. 10회 평균(Smoothing)을 적용하면 안정적인 밝기 제어가 가능합니다.
| int cdsBuf[10]; // 최근 10회 측정값 저장 배열 int cdsBufIdx = 0; int readCDSSmooth() { cdsBuf[cdsBufIdx] = analogRead(CDS_PIN); // 새 값 저장 cdsBufIdx = (cdsBufIdx + 1) % 10; // 순환 인덱스 long sum = 0; for (int i = 0; i < 10; i++) sum += cdsBuf[i]; return (int)(sum / 10); // 10회 평균 반환 } // 배열이 원형으로 순환하며 항상 최근 10개만 유지합니다. // delay()나 별도 루프 없이 loop()가 돌 때마다 1개씩 갱신됩니다. |
▶ 7.2 OLED 밝기 자동 조절 — ssd1306_command()
Adafruit SSD1306 라이브러리의 ssd1306_command()로 OLED의 Contrast(밝기)를 직접 제어합니다.
| void setAutoBrightness(int cdsVal) { // 풀업 방식이므로 cdsVal 작음 = 밝은 환경 // 밝은 환경 → OLED 밝기 높여야 잘 보임 (255) // 어두운 환경 → OLED 밝기 낮춰야 눈부시지 않음 (10) int brightness = map(cdsVal, CDS_BRIGHT, CDS_DARK, 255, 10); brightness = constrain(brightness, 10, 255); display.ssd1306_command(SSD1306_SETCONTRAST); // 0x81 명령 display.ssd1306_command(brightness); // 0~255 값 } // SSD1306_SETCONTRAST(0x81)은 다음 바이트를 밝기값으로 해석합니다. // 이 두 줄이 스마트폰의 '자동 밝기 조절' 기능과 완전히 같은 원리입니다! |
▶ 7.3 세 센서의 독립적 millis() 비블로킹
DHT11(2초), MQ-135(500ms), CdS(200ms) 세 센서가 각자 다른 주기로 완전히 독립 동작합니다.
| unsigned long prevDHT = 0; // DHT11 전용 (2초) unsigned long prevMQ = 0; // MQ-135 전용 (0.5초) unsigned long prevCDS = 0; // CdS 전용 (0.2초) void loop() { unsigned long now = millis(); if (now - prevDHT >= 2000) { prevDHT=now; /* DHT 코드 */ } if (now - prevMQ >= 500) { prevMQ =now; /* MQ 코드 */ } if (now - prevCDS >= 200) { prevCDS=now; /* CdS 코드 */ } // 세 조건이 완전히 독립적! 서로 기다리지 않음 // CdS는 1초에 5번, MQ-135는 2번, DHT11은 0.5번 실행됨 } |
| 💡 millis() 비블로킹의 진정한 힘 — 3편부터 6편까지 관통하는 핵심! 3편(초음파 100ms) + 4편(DHT11 2000ms) + 5편(MQ-135 500ms) + 6편(CdS 200ms) 이 4가지가 하나의 loop()에서 동시에, 각자의 주기로, 간섭 없이 동작합니다. delay()를 사용했다면 절대 구현할 수 없는 진정한 멀티태스킹입니다. |
▶ 7.4 캘리브레이션 — 환경에 맞게 임계값 설정
CdS_BRIGHT와 CDS_DARK 값은 센서 개체차와 설치 환경에 따라 다릅니다. 반드시 실제 환경에서 확인 후 조정하세요.
| 📌 CdS 캘리브레이션 방법 1단계: 코드 업로드 후 Serial Monitor(9600bps) 열기 2단계: 센서에 밝은 빛(손전등 또는 창가 직사광선)을 비추며 최솟값 확인 → 이 값을 CDS_BRIGHT에 입력 (예: 50) 3단계: 센서를 손으로 완전히 가려 최댓값 확인 → 이 값을 CDS_DARK에 입력 (예: 950) 4단계: 코드 수정 후 재업로드 → 자동 밝기가 더 정확하게 동작합니다. |
▶ 7.5 주요 함수 정리
| 함수 | 설명 |
| analogRead(A0) | CdS 분압 전압 읽기 → 0~1023 |
| readCDSSmooth() | 10회 이동 평균으로 안정적 조도값 반환 |
| setAutoBrightness(val) | CdS 값으로 OLED Contrast 자동 설정 |
| display.ssd1306_command(cmd) | SSD1306 하드웨어 명령 직접 전송 |
| map(val, in_min, in_max, out_min, out_max) | 범위 변환 (반비례 포함) |
| getLuxStatus(val) | 조도 단계 문자열 반환 (5단계) |
| initCDSBuf() | Smoothing 배열 초기화 (setup에서 1회 실행) |
8장. 동작 확인 및 테스트
▶ 8.1 동작 확인 순서
| 단계 | 확인 내용 | 정상 동작 |
| 1 | 전원 인가 | OLED 예열 카운트다운 (60초) |
| 2 | 60초 후 | 'Warm-up Complete!' 표시 |
| 3 | 대시보드 진입 | 온도/습도/AQI/조도 동시 표시 |
| 4 | 손전등으로 CdS 비추기 | LUX: BRIGHT/SUNNY, OLED 화면 밝아짐 |
| 5 | 손으로 CdS 완전히 가리기 | LUX: DARK, OLED 화면 어두워짐 |
| 6 | 알코올 솜 MQ-135에 접근 | AQI 상승, POOR/DANGER, 화면 반전 경보 |
| 7 | 시리얼 모니터 확인 | T/H/AQI/LUX 4항목 출력 확인 |
OLED 화면 출력
아두이노 OLED+ DHT11+MQ-135+Cds 동작 영상
Cds의 입력값을 OLED에 그래픽 바로 표시하고, 기존 프로젝트와 같이 DHT11 온도 습도, MQ-135센서로 공기질을, Cds로 밝기를 표시하고,주위 밝기에 따라 OLED의 밝기를 연동해 조절하는 동작 영상
8.2 시리얼 모니터 출력
출력 예시
T:24.0C H:55.0%
AQI:190 [GOOD]
LUX:420 [NORMAL]
LUX:85 [SUNNY] ← 손전등 비춤, OLED 밝아짐
LUX:950 [DARK] ← 손으로 가림, OLED 어두워짐
동작 모니터를 위한 시리얼 모니터 출력
| 🎁 보너스 노트 — 아날로그 센서의 공통 원리 이번 시리즈에서 배운 아날로그 센서들을 돌아보면 놀라운 공통점이 있습니다. • 4편 DHT11 습도: 폴리머 저항 변화 → MCU가 %RH로 변환 • 4편 DHT11 온도: NTC 서미스터 저항 변화 → MCU가 °C로 변환 • 5편 MQ-135: SnO₂ 반도체 저항 변화 → 분압 → analogRead() • 6편 CdS: 황화카드뮴 저항 변화 → 분압 → analogRead() 모두 '물리량 → 저항 변화 → 전압 변화 → 디지털 숫자'의 흐름입니다. 이 원리를 이해하면 앞으로 어떤 아날로그 센서를 만나도 같은 방식으로 다룰 수 있습니다. 새로운 센서 = 새로운 저항 특성 + 같은 회로 패턴입니다! |
| 다음 편 예고 — 7편 ▶ 7편: RGB LED + 공기질 연동 컬러 인디케이터 • RGB LED PWM 제어 원리와 색상 혼합 • MQ-135 공기질 단계에 따라 색상 자동 변경 (초록→노랑→주황→빨강) • CdS 조도에 따른 LED 자동 밝기 조절 (어두울 때 LED 밝기 낮춤) • 4~6편 전체 통합 — 빛·온습도·공기질·RGB LED 완전체 환경 모니터 |
민선생의 전자공방 | blog.naver.com/hansikmin
태그#아두이노#CdS#조도센서#광센서#광전도셀#풀업저항#풀다운저항#분압회로#밴드갭#광전자#가전자대#전도대#OLED자동밝기#SSD1306#DHT11#MQ135#환경모니터#브레드보드실습#아두이노프로젝트#민선생의전자공방#전자공방#피지컬코딩#아두이노입문#analogRead#millis비블로킹#Smoothing#이동평균#ssd1306_command#전압분배#브레드보드
