MKR1000 멀티모드 환경 센서 덱 - 번개, 가스, UV, 온도, 습도, 기압 통합 모니터링
부품 및 소모품
부품
수량
아두이노 MKR1000
1
MOD-1016 번개 감지 센서 (AS3935)
1
DFRobot MQ-9 가스 센서 (일산화탄소/가연성 가스)
1
Adafruit HTU21D-F 온습도 센서
1
Adafruit BMP085/BMP180 기압/고도/온도 센서
1
SparkFun ML8511 UV 센서
1
ChronoDot RTC (배터리 백업 실시간 시계)
1
SparkFun Photon Weather Shield
1
Adafruit 네오픽셀
1
브레드보드
1
점퍼 와이어
1세트
프로젝트 개요
날씨 관측을 좋아하여 데이터를 수집하고 그래프로 시각화하고 싶었습니다. 가정용 기상 관측기는 '현재' 상태와 과거 1시간/1일 통계 정도만 보여줍니다. 더 많은 데이터를 아두이노로 수집하고 싶었습니다. 온습도, 기압은 기본이고, 풍속과 강우량 측정도 하드웨어 입력이 필요했습니다. I2C를 익힌 후 AS3935 번개 감지기 같은 흥미로운 센서들을 발견했습니다!
기본 아두이노는 하드웨어 인터럽트 핀이 부족해 모든 센서를 동시에 사용할 수 없었습니다. 스파크펀의 포톤용 웨더 쉴드도 한계가 있었습니다. 센서를 선택해서 포기해야 했죠. :-(
그때 MKR1000을 발견했고, 최고의 특징인 8개의 하드웨어 인터럽트를 보고 이거다 싶었습니다! 이제 모든 센서를 다 쓸 수 있습니다!
전압 레벨 주의: MKR1000, 포톤, 많은 아두이노는 3.3V I/O를 사용합니다. CPU와 센서 전압이 다르면 레벨 시프터가 필요합니다.
사용한 센서들은 아다프루트, 스파크펀, 엘리먼트14 등에서 쉽게 구할 수 있으며 개당 $5~10(가스 센서 $10~20) 수준입니다. 번개 감지기(AS3935 칩)는 임베디드 어드벤처스 MOD-1016($26, 안테나 포함)을 선택했습니다. 포톤 웨더 쉴드도 구매했고, '웨더 미터'(풍속/풍향계)도 추가 예정입니다. 토양 수분 센서도 장착 위치 정하면 추가할 계획입니다. 일부 센서는 긴 배선을 싫어합니다.
핀 할당 계획
사용할 센서가 정해지면 각 센서를 어떤 핀에 연결할지 종이에 계획하고, 코드에도 블록 주석으로 기록해두세요.
내 MKR1000 센서 덱 핀 할당:
핀
용도
A0
DAC 용도로 예약
A1
ML8511 UV 센서
A2
가시광 센서 (TEMT6000)
A3
TMP36 로컬 온도 센서
A4
MQ-9 가스 센서
A5
토양 수분 센서
A6
풍속 센서? (아날로그)
0
(HW INT) 푸시버튼
1
(HW INT) AS3935 번개 감지기
2
(HW INT) 풍속 안테모미터 (회전당 인터럽트)
3
(HW INT) 강우량 팁핑 버킷
4, 5
예비
6
피에조 부저 (온보드 LED와 공유)
7
네오픽셀 출력 (시각화용)
11, 12
I2C SDA, SCL
시작하기
초보자라면 각 센서를 하나씩 테스트하며 시작하세요. 예제 코드 실행, 라이브러리 설치, 핀 번호 수정, 프로젝트 폴더에 복사본 저장. 시리얼 모니터로 값이 잘 읽히면 다음 센서로 넘어가세요. 모든 센서 테스트 완료 후 배선이 다 된 상태에서 메인 스케치 작성을 시작하세요.
원래 i2c RTC로 시간 기록하며 SD 카드에 로깅하려 했으나, MKR1000 내장 RTC가 배터리만으로 시간을 유지하지 못해 ChronoDot v2.1 i2c RTC를 병용합니다.
브레드보드 배선 (주황색: 3.3V 전원, 센서별로 꽂아 테스트)
라이브러리 관련 주의사항
일부 라이브러리는 MKR1000이나 아두이노 IDE 1.6.7에서 완벽하게 작동하지 않을 수 있습니다. 예: 구형 ATOMIC_* 매크로 의존성 (Arduino IDE 1.6.5에서 제거됨). 오렌지색 에러 메시지가 라이브러리 쪽에서 뜨면 웹 검색으로 해결책을 찾거나 작성자에게 연락하세요. 칩 단품으로 사서 비용 아끼려다 라이브러리 문제로 고생하지 말고, 아다프루트/스파크펀 모듈을 사는 게 정신건강에 좋습니다.
WiFi와 암호화
MKR1000에는 암호화 엔진이 내장되어 IoT 데이터 보안에 유리합니다. 하지만 이번 프로젝트에서는 범위를 넘어 다음 버전에서 다룰 예정입니다. SAMD21 모듈 블록 다이어그램 참조.
MKR1000 심장부 SAMD21 블록 다이어그램
팁:반드시 최신 버전 WiFi101 라이브러리를 사용하세요! 구버전이면 와이파이 모듈을 인식 못 합니다. Charif Mahmoudi 님의 'Getting Started with MKR1000' 튜토리얼 덕분입니다.
아날로그 센서 팁
대부분 아날로그 출력 센서는 단순 전압 출력을 내며 analogRead()로 읽습니다. 이후 map() 함수나 수식으로 변환 필요. 저항성 요소는 추가 회로 필요. 포텐쇼미터로 전압 미세 조정 가능.
방정식이 필요한 경우도 있습니다. TMP36 온도 센서가 대표적: 전압 읽고 수식으로 섭씨 변환, 화씨 필요시 추가 변환. 데이터시트 읽기가 정석이지만, 선배들 발자국 따라가는 게 빠릅니다.
증폭기가 필요한 센서(로드셀, 수분, 가속도계)는 증폭기와 라이브러리가 내장된 모듈을 사는 게 좋습니다.
I2C 디바이스 사용 팁
I2C는 '투와이어 인터페이스'라고도 합니다. 클럭(SCL)과 데이터(SDA) 두 선 필요. 마스터와 슬레이브가 번갈아 데이터 선 사용. 당연히 그라운드와 전원도 필요하며 그라운드는 CPU와 공통으로 연결.
모든 I2C 디바이스는 16진수 버스 주소를 가집니다. 주소 중복 피하세요. (중복 시 인에이블/셧다운 핀으로 하나씩 활성화 관리 필요. 전원만 끄면 SDA/SCL에서 기생 전원으로 잘못된 읽기 발생 가능)
I2C 사양상 가장 먼 센서에 클럭과 데이터 선에 '강한' 풀업 저항 필요. 일부 모듈은 온보드 저항 있음. 문서 확인하세요.
한 해커의 팁: SDA, SCL 선에 직렬로 저항 추가하면 신뢰도 향상. MKR1000(3.3V)에서는 220Ω, 5V CPU면 330Ω 추천. 배치 차이:
버스 종단 저항 vs 직렬 저항 배치 비교
데이터 활용 방안
현재는 시리얼 모니터로 출력. LCD 추가 가능. SD 카드 대신 내장 WiFi로 클라우드 업로드 계획. 여러 대 설치하여 위도/경도 알면 번개 위치 삼각측량도 가능!
추가 메모 (2017-11-29): MKR1000에서 하드웨어 인터럽트 작동 못 시킴. 9학년(13-14세) 수업용 노트와 센서 실험 중. Adalogger M0 보드 기반으로 SD 카드 저장, WiFi 모듈 장착 가능. 수업 자료
IKEA SODERSVIK 램프 피규어 발견. 내부 흰색 LED를 네오픽셀 20개로 교체하고 IR 리모컨 수신기 추가하면? 바람=구름, 색상=온도 표시 가능. 리모컨으로 지난 12시간 온도 변화 등 순간 표시도 가능.
IKEA SODERSVIK 램프 - 네오픽셀 개조 아이디어
여러분은 무엇을 모니터링하고 표시하고 싶으신가요?
코드
전체 모니터링 스케치
여러 센서가 함께 작동하며 라이브러리 간 호환됨. 단, 최신 아두이노 IDE에서 AVR-libc의 클래식 인터럽트 잠금 매크로 제거로 네오픽셀과 번개 센서 라이브러리가 비활성화됨. 올해 내 해결 예상되어 최종 스케치에서는 제외.
/* RTC-Weather-Sensors_v6_MKR1000 by Zonker Harris Spring 2016
* 8개 하드웨어 인터럽트 + WiFi + 암호화( IoT 필수!)를 가진 소형 보드!
* 주석은 업로드 시 무시되어 메모리 안 먹음. 넉넉히 달길 권장.
* 라이브러리는 프로그램 메모리 사용함.
*/
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP085_U.h>
#include <Adafruit_HTU21DF.h>
// 네오픽셀은 라이브러리 인터럽트 벡터 업데이트 필요로 주석 처리
// #include <Adafruit_NeoPixel.h>
Adafruit_BMP085_Unified bmp = Adafruit_BMP085_Unified(10180);
Adafruit_HTU21DF htu = Adafruit_HTU21DF();
// 핀 정의
int UVOUT = A1;
int lightOut = A2;
int tmp36out = A3;
int mq9out = A4;
int sounderPin = 6;
int neopixelPin = 7;
// ChronoDot RTC 변수들
int addrRTC = 0x68;
int seconds, minutes, hours, day, date, month, years;
int temprtc, tempfrtc;
void setup() {
Serial.begin(9600);
delay(1000);
Serial.println("i2c Sensor Deck");
pinMode(UVOUT, INPUT);
pinMode(lightOut, INPUT);
pinMode(tmp36out, INPUT);
pinMode(sounderPin, OUTPUT);
pinMode(neopixelPin, OUTPUT);
if(!bmp.begin()) {
Serial.println("BMP085/180 not detected!");
while(1);
}
displaySensorDetails();
if (!htu.begin()) {
Serial.println("HTU21-DF not found!");
while (1);
}
// RTC 시간 설정은 주석 해제 후 업로드, 다시 주석 처리
// seconds=0; minutes=41; hours=20; day=7; date=3; month=1; years=16; initChrono();
}
void loop() {
beep(50, 2);
// ChronoDot RTC 읽기 (0x68)
Wire.beginTransmission(0x68);
Wire.write((byte)0);
Wire.endTransmission();
Wire.requestFrom(0x68, 13);
while(Wire.available()) {
seconds = Wire.read(); minutes = Wire.read(); hours = Wire.read();
day = Wire.read(); date = Wire.read(); month = Wire.read(); years = Wire.read();
// 알람, 상태, 에이징, 온도 읽기...
// BCD to decimal 변환 로직...
}
get_date();
// 시리얼 출력...
// BMP180 기압/온도 (0x77)
sensors_event_t event;
bmp.getEvent(&event);
float BMPtemperature;
bmp.getTemperature(&BMPtemperature);
// 시리얼 출력: 온도, 기압, 고도...
// HTU21-DF 온습도 (0x40)
float HTUtemperature = htu.readTemperature();
// 시리얼 출력: 온도, 습도...
// TMP36 아날로그 온도 (A3)
int reading = averageAnalogRead(tmp36out);
float tmp36voltage = 0.0032258064516129 * reading; // 3.3V/1023
float tmp36temperatureC = (tmp36voltage - 0.5) * 100;
// 시리얼 출력...
// TEMT6000 가시광 (A2)
int vLevel = averageAnalogRead(lightOut);
// 시리얼 출력...
// ML8511 UV (A1)
int uvLevel = averageAnalogRead(UVOUT);
float newOutVolts = 0.0032258064516129 * uvLevel;
float uvIntensity = mapfloat(newOutVolts, 0.99, 2.8, 0.0, 15.0);
// 시리얼 출력...
// MQ-9 가스 (A4)
int MQ9volts = analogRead(mq9out);
// 시리얼 출력...
Serial.println("");
delay(3500); // 약 5초 주기
}
// 평균 아날로그 읽기 함수
int averageAnalogRead(int pinToRead) {
byte numberOfReadings = 8;
unsigned int runningValue = 0;
for(int x = 0; x < numberOfReadings; x++) runningValue += analogRead(pinToRead);
runningValue /= numberOfReadings;
return runningValue;
}
// float용 map 함수
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 beep(int duration, int count) {
for(int loop = 0; loop < count; loop++) {
digitalWrite(sounderPin, HIGH); delay(duration);
digitalWrite(sounderPin, LOW); delay(duration);
}
}
// RTC 관련 서브루틴들 (initChrono, set_date, get_date, set_time, get_time, decToBcd, bcdToDec 등)
// displaySensorDetails() 등 생략
평균 아날로그 읽기 함수 (재사용 가능)
int averageAnalogRead(int pinToRead) {
byte numberOfReadings = 8;
unsigned int runningValue = 0;
for(int x = 0; x < numberOfReadings; x++) runningValue += analogRead(pinToRead);
runningValue /= numberOfReadings;
return runningValue;
}
I2C 버스 스캐너
디바이스 주소를 모를 때 유효 주소 범위(1~127) 스캔. 알려진 센서 주소는 주석으로 표시.