여러 번 편집, 컴파일, 디버깅(느슨한 연결 발견: BMP180이 VCC 없이도 I2C 핀 누설 전원으로 작동함) 끝에 코드 완성.
주의사항 (Caveats)
고도 계산은 기압 강하 기반으로 정상 기상 조건에서만 적용 가능.
나침반 코드는 소프트웨어적으로 틸트 보정 안 됨. 장치를 수평으로 유지해야 함. 고정 원 안의 가변 반경 원으로 기울기 표시: 수평일 때 내부 원이 점으로 줄어들며 이때 방위가 가장 정확.
편각(Declination angle)은 위치와 지구 자기장 변화에 따라 다름. 태양 폭풍 영향도 받음. 편각 미포함 시 몇 도 오차 발생. 지역 편각 확인: https://www.magnetic-declination.com
나침반은 주변 자기 물체(산의 강자성 광물 등) 영향 받음.
최소 동작 전압 약 4.5V. 이하로 떨어지면 AA 배터리 교체 필요.
시간/날짜는 코드에 하드코딩되어 있어 변경하려면 코인 셀 배터리를 뺐다 끼우고 새 시간으로 코드 재업로드 필요.
개발 중 GPS 모듈이 없어 데모 좌표(위도 23.57, 경도 90.36) 사용. 실제 구현 시 GPS 라이브러리 및 코드 추가 필요.
가속도계와 자기계의 XY 평면은 PCB 장착 방향에 따라 달라지므로 코드 조정 필요.
가속도계 읽기는 진동에 민감하므로 정지 상태에서 사용 권장.
개선 가능 사항
펌웨어 측 개선:
파라미터 주기적 EEPROM 로깅
고도/위치 마일스톤 도달 시 부저 알람
물 마시기 알림, 휴식 알림
저배터리 알람, 저온/저습도 알림
나침반 소프트웨어 틸트 보정 (삼각함수 연산 필요)
GPS와 IoT 게이트웨이 앱 연동으로 편각 자동 획득
하드웨어 측 개선:
시간, 편각 등 설정을 위한 사용자 입력 스위치
맞춤형 3D 케이스
충전식 리포 배터리 사용
단일 보드 콤팩트 PCB 설계로 휴대성 향상
결론
여행과 하이킹은 쿨합니다. 주변 환경 변화를 확인할 수 있으면 더 흥미롭습니다. 특히 하이킹 몇 시간 동안 고도, 습도, 기압, 온도 변화를 체크할 수 있어 좋습니다. 이 장치로 고산병을 느끼는 고도, 피부가 건조해지는 습도 등을 알 수 있습니다. 회로 레이아웃과 코드 개선 여지가 남아 있지만, 야외 활동 시 이런 가젯 하나 있으면 정말 쿨합니다!
외부 자료
https://www.waveshare.com/wiki/Accessory_Shield
더 나은 버전을 위한 추천 하드웨어
Rapid Prototyping Kit
코드
Hiking_Tracker.ino (C/C++)
/*/////////// NOTE //////////
// D2: 코드로 소프트 리셋 가능
// D3, D9, D10: XBee 인터페이스 12,13,16번 핀으로 빼냄
// D4, D13: 접근 불가
///////////////////////////*/
//// 헤더 파일 ////
#include <math.h>
#include <inttypes.h>
#include <Wire.h>
#include <lm75.h>
#include <ADXL345.h>
#include <ChainableLED.h>
#include "U8glib.h"
#include "ds3231.h"
#include "Adafruit_Sensor.h"
#include "Adafruit_AM2320.h"
#include <Adafruit_BMP085_U.h>
#include <Adafruit_HMC5883_U.h>
#include <Adafruit_GPS.h>
////////////////// RTC 변수 //////////////////
uint8_t time[8];
struct ts t;
int s, m, h, dy, mo, yr;
/////////////// 가속도계 변수 ///////////////
float X = 0.0, Y = 0.0, Z = 0.0, Gravity = 0.0;
/////////////// 자기계 변수 ////////////////
float Xm = 0.0, Ym = 0.0, Zm = 0.0;
//////////////// RGB LED 변수 //////////////
const int rgb_pwr = 12;
const int clk_pin = 6;
const int data_pin = 5;
float hue = 0.0;
boolean up = true;
//////////////// 포텐쇼미터 ////////////////
int pot = 0;
////////////// 배터리/공급 전압 //////////////
float Vbatt = 0.0;
//////////////// 온도계 ////////////////////
float temp = 0.0;
//////////////// 습도 //////////////////////
float humid = 0.0;
//////////////// 기압 //////////////////////
double prsr = 0.0;
//////////////// 고도 //////////////////////
float alt = 0.0;
///////////////// 위도, 경도 ////////////////
float lat = 0.0, lon = 0.0;
/////////////// 나침반 방위 ////////////////
float heading = 0.0;
/////////////// 기타 변수 ////////////////
int mstime = 0;
int sensor_selector = 1;
//////////// ENUM & I2C 장치 객체 ////////////
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE | U8G_I2C_OPT_DEV_0); // I2C OLED
TempI2C_LM75 termo = TempI2C_LM75(0x48, TempI2C_LM75::nine_bits); // 온도
ADXL345 accelerometer; // 가속도
ChainableLED leds(clk_pin, data_pin, rgb_pwr, 1); // RGB LED
Adafruit_AM2320 am2320 = Adafruit_AM2320(); // 습도
Adafruit_BMP085_Unified bmp180 = Adafruit_BMP085_Unified(10085); // 기압
Adafruit_HMC5883_Unified mag = Adafruit_HMC5883_Unified(12345); // 나침반
// GPS 활성화 시 다음 두 줄 주석 해제 (시리얼 디버깅 불가)
// HardwareSerial mySerial = Serial;
// Adafruit_GPS GPS(&mySerial);
void setup(void) {
// 아날로그 레퍼런스 1.1V 내부
// analogReference(INTERNAL);
// OLED용 신호 (실드에 솔더 브리지 필요 for D2,D3,D9,D10)
pinMode(2, OUTPUT); // D2 -> RST (소프트 리셋)
pinMode(3, OUTPUT); // XBee 12번
pinMode(9, OUTPUT); // XBee 13번
pinMode(10, OUTPUT); // XBee 16번
digitalWrite(2, HIGH);
digitalWrite(3, HIGH);
digitalWrite(9, LOW);
digitalWrite(10, HIGH);
pinMode(7, OUTPUT);
pinMode(8, OUTPUT);
digitalWrite(7, HIGH);
digitalWrite(8, LOW);
// 10k POT on A0
// Vin Batt/Supply on A1 (3.91k/21.76k 분압)
// 시리얼 디버깅 (선택)
// Serial.begin(9600);
// 가속도계 초기화 대기
if (!accelerometer.begin()) delay(50);
// RTC 인터럽트 활성화
DS3231_init(DS3231_INTCN);
// 부저 초기화
pinMode(11, OUTPUT);
digitalWrite(11, LOW);
}
void loop(void) {
// OLED 화면 갱신
u8g.firstPage();
do { draw(); } while (u8g.nextPage());
// 센서 선택 로테이션 (0,1,2)
sensor_selector++;
if (sensor_selector >= 3) sensor_selector = 0;
///// 온도 읽기 /////
temp = termo.getTemp();
delay(5);
///// 기압 읽기 (selector 0일 때) /////
if (sensor_selector == 0) {
if (!bmp180.begin()) delay(11);
sensors_event_t event;
bmp180.getEvent(&event);
if (event.pressure) {
prsr = event.pressure / 10; // hPa -> kPa
delay(50);
}
}
///// GPS 초기화 /////
// GPS.begin(9600);
///// 고도 읽기 (selector 0일 때) /////
if (sensor_selector == 0) {
sensors_event_t event;
bmp180.getEvent(&event);
alt = bmp180.pressureToAltitude(1013.25, event.pressure);
delay(50);
}
///// 습도 센서 초기화 & 읽기 (selector 1일 때) /////
if (sensor_selector == 1) {
am2320.begin();
delay(50);
humid = am2320.readHumidity();
delay(50);
}
///// 가속도 읽기 /////
Vector norm = accelerometer.readNormalize();
X = norm.XAxis; Y = norm.YAxis; Z = norm.ZAxis;
Gravity = sqrt(X*X + Y*Y + Z*Z) - 0.3; // 오프셋 보정
///// 시간 읽기 /////
DS3231_get(&t);
s = t.sec; m = t.min; h = t.hour;
dy = t.mday; mo = t.mon; yr = t.year;
///// 포텐쇼미터 읽기 /////
pot = analogRead(A0) * 5 / (10 * 3);
delay(2);
///// 배터리 전압 읽기 /////
// 1.1V 레퍼런스는 이 칩에서 1.08V로 측정됨
Vbatt = (1.08 * analogRead(A1) / 1023) / 3.91 * (3.91 + 21.76);
delay(2);
///// 나침반 방위 (selector 2일 때) /////
if (sensor_selector == 2) {
if (!mag.begin()) while(1);
delay(70);
sensor_t sensor2;
mag.getSensor(&sensor2);
delay(70);
// 편각은 위치(GPS 위도/경도)로 계산 필요
float declination = 0.0;
sensors_event_t event2;
mag.getEvent(&event2);
// Xm = event2.magnetic.x; Ym = event2.magnetic.y;
delay(70);
heading = atan2(event2.magnetic.y, event2.magnetic.x);
heading += declination;
if (heading < 0) heading += 2*PI;
if (heading > 2*PI) heading -= 2*PI;
heading = heading * 180 / PI; // 도 단위 변환
}
///// 부저 제어 /////
if (pot > 80) digitalWrite(11, HIGH); else digitalWrite(11, LOW);
///// RGB LED 제어 /////
if (pot > 40) {
leds.pwr_set(PWR_ENABLE);
for (byte i = 0; i < 1; i++) leds.setColorHSB(i, hue, 1.0, 0.5);
if (up) hue += 0.025; else hue -= 0.025;
if (hue >= 1.0 && up) up = false;
else if (hue <= 0.0 && !up) up = true;
}
///// 소프트 리셋 /////
// digitalWrite(2, LOW);
}
/////////////////// OLED 그리기 함수 ///////////////////
void draw(void) {
u8g.setFont(u8g_font_7x13);
u8g.drawHLine(37, 0, 62);
u8g.setPrintPos(41, 12);
if (h < 10) u8g.print(0);
u8g.print(h); u8g.drawStr(54, 12, ":");
u8g.setPrintPos(62, 12);
if (m < 10) u8g.print(0);
u8g.print(m); u8g.drawStr(75, 12, ":");
u8g.setPrintPos(83, 12);
if (s < 10) u8g.print(0);
u8g.print(s);
u8g.drawHLine(37, 14, 62);
u8g.setFont(u8g_font_5x8);
u8g.drawVLine(37, 0, 14);
u8g.setPrintPos(0, 7);
if (dy < 10) u8g.print(0);
u8g.print(dy); u8g.drawStr(11, 7, "/");
u8g.setPrintPos(17, 7);
if (mo < 10) u8g.print(0);
u8g.print(mo); u8g.drawStr(0, 16, "/");
u8g.setPrintPos(6, 16);
u8g.print(yr);
u8g.drawVLine(99, 0, 14);
u8g.drawStr(104, 7, "BATT");
u8g.setPrintPos(102, 15);
u8g.print(Vbatt); u8g.drawStr(123, 15, "V");
u8g.print(round(temp)); u8g.drawStr(55, 25, "'C");
u8g.setFont(u8g_font_5x8);
u8g.setPrintPos(15, 40); u8g.print(round(X));
u8g.setPrintPos(60, 40); u8g.print(round(Y));
u8g.setPrintPos(100, 40); u8g.print(round(Z));
u8g.drawVLine(37, 18, 36);
u8g.drawStr(39, 24, "AIR TEMP: ");
u8g.setPrintPos(85, 24); u8g.print(temp); u8g.drawStr(118, 24, "C");
u8g.drawStr(39, 34, "HUMIDITY: ");
u8g.setPrintPos(85, 34); u8g.print(humid); u8g.drawStr(118, 34, "%");
u8g.drawStr(39, 44, "PRESSURE: ");
u8g.setPrintPos(85, 44); u8g.print(prsr); u8g.drawStr(118, 44, "hP");
u8g.drawStr(39, 54, "ALTITUDE: ");
u8g.setPrintPos(85, 54); u8g.print(round(alt)); u8g.drawStr(118, 54, "m");
u8g.drawHLine(0, 56, 128);
u8g.drawStr(19, 64, "LAT:"); u8g.setPrintPos(39, 64); u8g.print(23.57); // 데모 좌표
u8g.drawStr(69, 64, "LONG:"); u8g.setPrintPos(94, 64); u8g.print(90.36); // 데모 좌표
// 나침반 표시
int r = round(sqrt(X*X + Y*Y));
u8g.drawStr(0, 22, "heading");
u8g.drawCircle(18, 34, r, U8G_DRAW_ALL); // 내부 수평 원
u8g.drawCircle(18, 34, 10, U8G_DRAW_ALL); // 외부 고정 원
// 수평일 때 내부 원이 점이 됨 -> 방위 정확
u8g.setPrintPos(0, 53); u8g.print(heading); // 0/360=N, 90=E, 180=S, 270=W
if ((heading > 315) || (heading <= 45)) u8g.drawLine(18, 34, 8, 34); // N
if ((heading > 45) && (heading <= 135)) u8g.drawLine(18, 34, 18, 24); // E
if ((heading > 135) && (heading <= 225)) u8g.drawLine(18, 34, 28, 34); // S
if ((heading > 225) && (heading < 315)) u8g.drawLine(18, 34, 18, 44); // W
}
라이브러리
압축 해제 후 개별 라이브러리 ZIP을 아두이노 라이브러리 매니저에 추가하세요. (다운로드 필요)