|
|
민선생의 전자공방 ・ 2026. 6. 12. 21:00
| 🛸MPU-6050 자이로·가속도 센서 사용 가이드 — 아두이노 6축 센서와 3D 자세 대시보드 만들기 MEMS 기술 · 6축 센서 · 상보필터 · OLED 3D 자세 대시보드 아두이노 IO 8편 · 민선생의 전자공방 |
📖 프롤로그 — 드론의 자세제어
안녕하세요, 민선생입니다. 지난 7편에서는 RGB LED 점(Pixel) 하나에서 시작해 GPU·AI·HBM까지 이어지는 거대한 이야기를 나눴습니다. 이번 8편에서는 본격적으로 '운동(Motion)'을 감지하는 센서, MPU-6050(MPU-6050을 사용하기쉽게 만든 브레이크아웃보드 GY-521보드)을 다룹니다.
이 작은 칩 하나는 스마트폰의 화면 자동 회전, 드론의 공중 정지(호버링), 자동차의 에어백 작동까지 모두 책임지고 있습니다. 오늘은 MPU-6050이 어떻게 '기울기'와 '회전'을 느끼는지, 그리고 이 센서가 왜 메이커 생태계의 혁명이라 불리는지 살펴봅니다.
접기/펴기
MPU 6050을 활용한 X축. Y축 각도 표시
MPU-6050 기울기에 따라 OLED 화면의 수평·자세 표시가 실시간으로 반응하는 영상
MPU6050 센서 동작 영상
1장. MPU-6050이란? — 6축 통합의 혁명
▶ 1.1 MPU-6050 이전의 시대 — 3축 제어는 왜 어려웠을까?
불과 십여 년 전만 해도 로봇이나 비행체의 자세를 제어하기 위해 3축(X, Y, Z) 기울기를 측정하는 것은 엄청난 비용과 노력이 드는 작업이었습니다.
| 문제점 | 구체적인 어려움 |
| 분리된 센서와 부피 | 가속도 센서 칩 하나, 자이로스코프 칩 하나를 따로 구매해 기판에 구성해야 했음 |
| 아날로그 센서의 불안정성 | 센서가 아날로그 전압으로 신호를 출력 → ADC로 정밀하게 읽어야 함 → 미세한 노이즈에도 각도 데이터가 요동침 |
| 높은 가격과 연산량 | 제대로 된 자세 측정 모듈(AHRS)은 수십만~수백만 원 → 데이터 융합용 행렬 연산은 당시 MCU에 큰 부담 |
▶ 1.2 혁명의 시작 — MPU-6050의 등장
이 모든 문제를 칩 하나로 해결하며 등장한 구원자가 바로 미국의 센서 반도체 기업 InvenSense(인벤센스)사의 MPU-6050입니다. 이 센서는 로봇 공학과 메이커 생태계를 영원히 바꿔놓았습니다.
MPU-6050 센서
MPU-6050을 쉽게 사용할수 있도록 만든 브레이크아웃보드 GY-521
| ✅ MPU-6050의 3대 혁신 ① 6-DOF(6자유도) 원칩 통합 3축 가속도계(Accelerometer) + 3축 자이로스코프(Gyroscope)를 하나의 실리콘 칩 안에 통합 ② 디지털 통신 (I2C) 아날로그 노이즈 걱정 없이, SDA·SCL 단 두 가닥의 통신선만으로 깔끔한 디지털 데이터를 MCU로 전달 ③ DMP(Digital Motion Processor) 내장 칩 내부에 자체 연산 장치가 있어 복잡한 필터링 연산을 센서가 직접 처리 가능 (이번 프로젝트에서는 넌블로킹 스케줄링을 위해 DMP 대신 직접 필터를 구현합니다) |
▶ 1.3 InvenSense사와 MPU-6050의 가격 — 메이커 혁명의 시작
MPU-6050은 미국의 센서 반도체 기업 InvenSense(인벤센스)사가 개발한 6축 모션 트래킹 칩입니다. InvenSense는 MEMS 기반 자이로·가속도 센서 분야의 선두주자로, 이후 일본의 TDK社에 인수되어 현재는 'TDK InvenSense' 브랜드로 공급되고 있습니다.
| 항목 | 내용 |
| 제조사 | InvenSense (現 TDK InvenSense) |
| 패키지 형태 | QFN-24 (4mm × 4mm × 0.9mm, 매우 소형) |
| 통신 방식 | I2C (최대 400kHz) |
| 전원 전압 | VDD 2.375V ~ 3.46V (모듈은 5V 레귤레터 내장) |
| 칩 단가 | 대량구매 시 단가 약 1~3달러 수준 |
| 모듈 가격 | GY-521 모듈 기준 국내 쇼핑몰 약 2,000~5,000원대 |
| 💰 가격이 가져온 혁명 — '드론의 대중화' 과거에는 자세 제어 모듈이 너무 비싸고 무거워서 군사용·전문가용 대형 헬기에만 장착할 수 있었습니다. 하지만 MPU-6050이 단돈 몇천 원(수 달러) 단위로 대량 생산되면서, 장난감 드론부터 레이싱 드론까지 저렴하고 가벼운 비행체들이 완벽하게 공중에서 중심을 잡을 수 있게 되었습니다. 저가형 드론 혁명은 사실상 MPU-6050이라는 날개를 달고 날아오른 셈입니다. |
2장. MEMS 기술 — 손톱 위의 미세 기계
▶ 2.1 MEMS란? — 손톱 위에 만들어진 미세 기계
MPU-6050은 MEMS(Micro Electro Mechanical Systems, 미세 기계 전자 시스템) 기술을 사용합니다. 반도체 공정으로 머리카락보다 얇은 미세한 기계 구조물(스프링, 질량체, 빗살 모양 전극)을 실리콘 칩 위에 직접 새겨 넣은 것입니다.
AI 활용
MPU-6050의 가속도(정전용량+질량체)계와 자이로(코리올리 효과) 동작
| 🔬 비유로 이해하기 — 손톱 위의 놀이터 MEMS 칩 내부에는 눈에 보이지 않는 초소형 그늘 그물(스프링)에 매달린 작은 추(질량체)가 들어 있습니다. 이 추는 외부 힘(가속도·진동·회전)에 반응해 아주 미세하게 흔들리거나 비틀립니다. 이 떨림의 정도를 전기 신호로 변환하면 가속도와 회전 속도를 알 수 있습니다. |
▶ 2.2 가속도계 — 정전 용량 변화로 힘을 측정한다
MPU-6050 내부의 가속도계는 미세한 질량체와 그 주변을 둘러싼 고정 전극으로 구성된 '빗살 구조(Comb Structure)'를 갖고 있습니다.
| 📐 가속도 측정의 4단계 과정 ① 가속도(중력 또는 외부 충격)가 발생하면, 스프링에 매달린 미세 질량체가 관성에 의해 살짝 이동합니다. ② 질량체가 이동하면 고정 전극과의 거리(틈)가 변합니다. ③ 거리가 변하면 두 전극 사이의 정전 용량(Capacitance)이 변합니다. (거리가 가까울수록 정전 용량이 커지는 원리) ④ 이 정전 용량 변화를 내부 회로가 전압 변화로 변환하고, ADC를 거쳐 디지털 값(예: -32768 ~ +32767)으로 출력합니다. |
| 🌍 정지 상태에서도 '1g'가 측정되는 이유 책상 위에 가만히 놓인 센서도 Z축 방향으로 약 +1g(중력가속도)가 측정됩니다. 지구가 항상 우리를 아래로 끌어당기고 있기 때문입니다. 이 원리를 거꾸로 이용하면, 가속도계의 X·Y·Z 값으로부터 센서가 지면에 대해 얼마나 기울어져 있는지(Roll·Pitch 각도)를 삼각함수로 계산할 수 있습니다. |
▶ 2.3 자이로스코프 — 코리올리 효과로 회전을 측정한다
자이로스코프는 가속도계와 전혀 다른 원리로 동작합니다. 칩 내부의 진동 구조물을 일정한 주파수로 끊임없이 진동시키면서, 코리올리 효과(Coriolis Effect)를 이용해 회전을 감지합니다.
| 🌀 코리올리 효과란? 회전하는 물체 위에서 운동하는 물체에 대해, 운동 방향에 수직으로 작용하는 가상의 힘입니다. (전향력이라고도 함) 예) 회전하는 회전목마 위에서 공을 똑바로 던지면, 공은 바깥에서 보기엔 곡선을 그리며 휘어지는 것처럼 보입니다. |
| 📐 각속도 측정의 3단계 과정 ① 칩 내부의 미세 진동 구조물이 일정한 주파수로 계속 진동합니다. ② 센서에 회전(각속도)이 가해지면, 진동 중인 구조물에 코리올리 힘이 작용하여 진동 방향에 수직으로 미세하게 추가 변형(떨림)이 발생합니다. ③ 이 추가 변형의 크기가 회전 속도(각속도)에 비례합니다. 이를 정전 용량 변화 → 전압 → 디지털 값(°/s 단위)으로 변환합니다. |
▶ 2.4 LSB와 감도(Scale Factor) — 디지털 값을 실제 단위로
MPU-6050은 16비트 ADC를 사용하여 -32768 ~ +32767 범위의 정수값(LSB, Least Significant Bit)으로 데이터를 출력합니다. 이 값을 실제 물리량(g, °/s)으로 변환하려면 감도(Sensitivity)로 나누어야 합니다.
| 설정 범위 | 가속도 감도 (LSB/g) | 자이로 감도 (LSB/°/s) |
| ±2g / ±250°/s | 16,384 | 131.0 |
| ±4g / ±500°/s | 8,192 | 65.5 |
| ±8g / ±1000°/s | 4,096 | 32.8 |
| ±16g / ±2000°/s | 2,048 | 16.4 |
| 📌 예시 — 가속도 원시값을 g 단위로 변환 원시값(AcZ) = 16,384, 감도 = 16,384 LSB/g (±2g 설정 시) 실제 가속도 = AcZ ÷ 16,384 = 16,384 ÷ 16,384 = 1.0g → 센서가 정지 상태이며, Z축이 지면과 수직(정상 자세)임을 의미합니다. |
📌 참고 — '1g'가 의미하는 두 가지
1g = 9.80665 m/s² (지구 표준 중력가속도)
① 낙하 속도의 증가율
공기 저항이 없는 진공 상태에서 물체를 떨어뜨리면, 1초마다 속도가 9.8m/s씩 빨라집니다.
1초 후: 9.8 m/s
2초 후: 19.6 m/s
3초 후: 29.4 m/s ...
② 힘의 압박감 — G-force
평소 의자에 가만히 앉아 있을 때 몸이 느끼는 무게감이 바로 1g입니다. 만약 2g를 받는다면, 몸무게가 순간적으로 2배로 느껴지는 압박감을 받는다는 뜻입니다. 롤러코스터나 비행기가 급커브를 돌 때 몸이 쏠리는 그 느낌이 바로 2g, 3g입니다.
MPU-6050과의 연결
가속도계는 ②의 의미로 동작합니다. 정지된 센서가 Z축에서 "+1.0g"를 출력하는 것은, 센서가 중력에 이끌려 떨어지지 않도록 바닥(또는 지지대)이 센서를 위로 1g만큼 떠받치고 있기 때문입니다. 센서를 기울이면 이 1g의 힘이 X·Y·Z 축에 나누어 분산되고, 그 분산 비율로부터 기울기 각도를 계산할 수 있습니다.
▶ LSB — 두 가지로 쓰이는 용어
LSB(Least Significant Bit)는 원래 '최하위 비트', 즉 16비트 숫자에서 가장 끝(오른쪽) 비트의 위치를 가리키는 말입니다.
| 1 0 1 1 0 0 1 0 1 1 0 0 1 0 1 1 ↑ ↑ MSB(최상위비트) LSB(최하위비트) |
그런데 '16384 LSB/g'처럼 단위로 쓰일 때는, '디지털 숫자가 1만큼 증가할 때 실제 물리량이 얼마나 변하는가'라는 변화량(분해능)을 의미합니다. 같은 단어가 '위치'와 '최소 단위'라는 두 가지 뜻으로 쓰이는 것이니 헷갈리지 않도록 주의해야 합니다.
| 📏 자(ruler)로 이해하는 LSB 자에서 가장 작은 눈금 한 칸을 '1mm'라고 부르는 것과 같습니다. • '눈금 한 칸'(구조적 위치) → LSB의 원래 의미 • '1mm'(그 한 칸이 나타내는 크기) → 확장된 의미 (분해능) 즉 'LSB/g'는 '디지털 눈금 1칸이 몇 g를 나타내는가'를 뜻합니다. |
▶ 측정 범위와 분해능의 트레이드오프
MPU-6050의 가속도계는 16비트 ADC를 사용합니다. 즉 -32768~32767, 총 65536단계의 디지털 값으로만 출력할 수 있습니다. 이 고정된 65536단계를 어떤 측정 범위(±2g ~ ±16g)에 나눠 쓰느냐에 따라 분해능이 달라집니다.
| ±2g 설정 → 65536단계를 ±2g 범위에 분배 → 16384 LSB/g (분해능 높음) ±16g 설정 → 65536단계를 ±16g 범위에 분배 → 2048 LSB/g (분해능 낮음) 범위(g) ↑ ⇄ 분해능(LSB/g) ↓ ← 항상 반비례 관계 |
| 📏 자(ruler)로 이해하는 트레이드오프 같은 30cm 자에 눈금을 그릴 때: • 1mm 간격으로 그리면 → ±2g처럼 세밀하게 측정 가능 (분해능 高) • 1cm 간격으로 그리면 → ±16g처럼 큰 범위를 잴 수 있지만 둔감 (분해능 低) 눈금의 총 개수(16비트=65536)는 고정되어 있으므로, 범위를 넓히면 눈금 사이 간격이 벌어집니다. |
▶ 측정 범위별 분해능 비교표
| 측정 범위 | 분해능 (LSB/g) | 1 LSB당 변화량 | 적합한 용도 |
| ±2g (채택) | 16,384 (최고) | 약 0.000061g | 기울기 측정, 자세 제어 |
| ±4g | 8,192 | 약 0.000122g | 일반적인 동작 감지 |
| ±8g | 4,096 | 약 0.000244g | 빠른 움직임, 진동 측정 |
| ±16g | 2,048 (최저) | 약 0.000488g | 충격 감지, 낙하 측정 |
▶ 그래서 이 편에서는 ±2g를 선택했습니다.
본 프로젝트는 정지 상태(±1g 중력)와 천천히 기울이는 동작만 측정합니다.
± 16g 같은 큰 측정 범위는 드론 충돌이나 급격한 진동처럼 순간적으로 큰 가속도가 발생하는 상황에 필요합니다.
8편의 자세 제어(Roll·Pitch 측정)에는 그런 큰 충격이 없으므로, ±2g로 설정하여 가장 높은 분해능(16384 LSB/g)을 확보합니다. 그 결과 1~2° 수준의 미세한 기울기 변화도 정밀하게 감지할 수 있습니다.
| 💡 한 줄 요약 측정 범위를 줄이면(±2g) → 분해능이 높아진다(16384 LSB/g) → 미세한 변화까지 정밀하게 측정 가능 → 자세 제어처럼 '작은 변화'가 중요한 작업에 최적! |
3장. 브레드보드 회로 배선
▶ 3.1 부품 목록
| 부품 | 수량 | 비고 |
| 아두이노 Uno R3 | 1 | 또는 호환 보드 |
| OLED 128x64 I2C (SSD1306) | 1 | 1편부터 계속 사용 |
| MPU-6050 모듈 (GY-521) | 1 | 이번 편 신규 추가 |
| 브레드보드 | 1 | 대형 권장 (830홀) |
| 점퍼 선 | 다수 | M-M 타입 |
▶ 3.2 MPU-6050 핀 연결표
MPU-6050과 OLED는 모두 I2C 통신을 사용하므로, 같은 SDA·SCL 라인을 공유합니다. 두 장치는 서로 다른 I2C 주소를 가지므로 동시에 연결해도 문제없습니다.
| MPU-6050 핀 | 연결 | 아두이노 핀 | 비고 |
| VCC | → 직접 | 5V | 전원 레일 빨간선 |
| GND | → 직접 | GND | 전원 레일 검은선 |
| SCL | → 직접 | A5 | I2C 클럭. OLED와 공유 |
| SDA | → 직접 | A4 | I2C 데이터. OLED와 공유 |
| XDA / XCL / AD0 / INT | 미사용 | — | 기본 연결 불필요 (AD0=GND 시 주소 0x68) |
| 📌 MPU-6050 I2C 주소 확인 방법 MPU-6050은 일반적으로 0x68 또는 0x69 주소를 사용합니다 (AD0 핀이 GND면 0x68, VCC면 0x69). OLED(0x3C)와 주소가 겹치지 않으므로 같은 I2C 버스에 안전하게 함께 연결할 수 있습니다. 정확한 주소 확인이 필요하면 아래 I2C 스캐너 코드를 먼저 업로드해 시리얼 모니터로 확인하세요. |
| // I2C 스캐너 — MPU-6050 주소 확인용 #include <Wire.h> void setup(){ Wire.begin(); Serial.begin(115200); Serial.println("\nI2C Scanner"); } void loop(){ byte error, address; int nDevices = 0; for(address=1; address<127; address++){ Wire.beginTransmission(address); error = Wire.endTransmission(); if(error==0){ Serial.print("I2C device found at 0x"); if(address<16) Serial.print("0"); Serial.println(address, HEX); nDevices++; } } if(nDevices==0) Serial.println("No I2C devices found"); delay(5000); } |
▶ MPU-6050 I2C 주소확인(Arduino IDE 시리얼 모니터 캡쳐)
▶ 브레드보드 회로배선
아두이노, OLED, MPU-6050 브레드보드 배선(wokwi)
실물 브레드보드 완성 배선 사진
4장. 라이브러리 설치
MPU-6050은 Wire.h를 이용한 레지스터 직접 통신으로 동작하므로, 추가 라이브러리 설치 없이 사용할 수 있습니다. OLED는 1편에서 설치한 라이브러리를 그대로 사용합니다.
| 라이브러리 | 용도 | 설치 상태 |
| Wire.h | I2C 통신 (OLED + MPU-6050) | 아두이노 기본 내장 |
| Adafruit_GFX.h | OLED 그래픽 | 1편에서 설치 완료 |
| Adafruit_SSD1306.h | SSD1306 드라이버 | 1편에서 설치 완료 |
| 없음 (추가 불필요) | MPU-6050 레지스터 직접 제어 | 추가 설치 불필요 ✅ |
5장. 핵심 이론 — 캘리브레이션과 상보필터
▶ 5.1 초기화 시 '절대 움직이면 안 되는 이유' — 캘리브레이션
센서 전원을 켜고 OLED에 'Calibrating...'이 표시되는 동안, 기구를 손으로 잡거나 흔들면 절대 안 됩니다.
| ⚠️ 영점(Offset) 캘리브레이션의 원리 센서는 공장 출고 시, 그리고 그날의 온도에 따라 미세한 '오프셋(기본 오차)'을 가지고 있습니다. 가만히 둬도 자이로 값이 0이 아니라 1이나 -2를 가리키는 식입니다. 초기화 로직은 수백~수천 번의 데이터를 빠르게 읽어 평균값을 내어 "이 상태가 완벽히 정지된 0이다"라는 기준점(영점)을 잡는 과정입니다. 이때 흔들리면 센서는 삐딱한 자세를 '수직'으로 착각하고 엉뚱하게 동작하게 됩니다. |
▶ 5.2 상보필터 — 두 센서의 단점을 서로 보완한다
가속도 센서와 자이로 센서는 각자 치명적인 단점을 가지고 있습니다.
| 센서 | 장점 | 단점 |
| 가속도 센서 | 장기적으로 안정적 (중력 기준) | 진동·충격에 민감 → 단기 노이즈가 큼 |
| 자이로 센서 | 단기 응답이 빠르고 부드러움 | 적분 누적 오차로 값이 서서히 틀어짐(드리프트) |
이 둘의 장점만 섞는 마법의 칵테일이 바로 상보필터(Complementary Filter)입니다.
| Angle = 0.98 * (Angle + Gyro * dt) + 0.02 * Accel // 0.98 (98%) → 자이로 적분값을 신뢰 (빠른 응답) // 0.02 (2%) → 가속도 측정값을 살짝 반영 (장기 기준점 보정) |
| 💡 상보필터를 비유로 이해하면 자이로 센서 = 눈을 감고 걸을 때의 '느낌'으로 위치를 추정 (빠르지만 시간이 지나면 점점 어긋남) 가속도 센서 = 가끔 눈을 떠서 '실제 풍경'을 확인 (느리고 흔들리지만 절대적인 기준이 됨) 두 정보를 98:2로 섞으면, 빠르면서도 흔들리지 않는 안정적인 각도 추정이 가능합니다. |
6장. 인터럽트 vs 폴링 — 우리의 선택
▶ 6.1 인터럽트(Interrupt) vs 폴링(Polling) — 여기서는 폴링을 쓸 예정입니다.
MPU-6050 데이터를 가져오는 방식은 두 가지입니다.
| 방식 | 동작 원리 | 장단점 |
| 인터럽트 | 센서가 'INT' 핀으로 "데이터 준비됐어!" 신호를 보내면 즉시 읽음 | 응답은 빠르지만, 다른 작업(모터 펄스 등)을 갑자기 끼어들어 방해할 수 있음 |
| 폴링 (채택) | 아두이노가 millis()로 정해진 시간마다 "데이터 어때?"하고 물어봄 | 다른 작업과 타이밍이 충돌하지 않아 안정적 |
| 🎯 왜 폴링을 선택했나? — 9편의 스텝 모터 제어를 위해 인버티드 펜듈럼처럼 스텝 모터의 펄스를 500µs 단위로 칼같이 제어해야 하는 시스템에서는 인터럽트가 불쑥불쑥 끼어들면 모터 펄스 간격이 틀어져 탈조(틱틱거림)가 발생합니다. 따라서 millis()를 활용해 5ms 단위로 센서에게 물어보는 폴링(Polling) 방식의 넌블로킹(Non-blocking) 구조를 사용하여 모터 제어의 안정성을 극대화합니다. 이번 8편의 예제도 이 폴링 + millis() 패턴을 그대로 따릅니다. |
7장. 사용상 주의점과 최신 동향
▶ 7.1 MEMS 센서, 물리적 충격에 매우 민감합니다
| ⚠️ 충격·낙하 주의 MEMS 센서는 손톱 위의 미세한 스프링 구조물입니다. 망치로 책상을 세게 내리치거나 센서를 바닥에 떨어뜨리면 내부의 미세 구조물이 손상되어 영원히 영점이 틀어질 수 있습니다. ✅ 해결책: 푹신한 스펀지나 방진 고무를 덧대어 장착하세요. |
▶ 7.2 최신 동향 — MPU-6050 이후의 센서들
MPU-6050은 2012년경부터 메이커 시장에서 널리 사용되기 시작한 센서로, 현재는 후속 제품들이 다양하게 출시되어 있습니다.
| 센서 | 특징 | 비고 |
| MPU-6050 | 6축 (가속도+자이로), DMP 내장 | 입문·교육용 표준, 본 시리즈 채택 |
| MPU-9250 | 9축 (+지자기 센서 추가) | 방위각(나침반) 필요 시 |
| ICM-20948 | 9축, 저전력, TDK 최신 라인업 | MPU-9250 후속, 산업용 채택 증가 |
| BNO055 | 9축 + 자체 센서퓨전 칩 내장 | 상보필터 직접 구현 불필요, 가격 높음 |
| 📌 본 시리즈에서 MPU-6050을 선택한 이유 가격이 저렴하고 자료가 풍부해 입문 교육에 최적입니다. DMP를 끄고 레지스터를 직접 다루는 과정을 통해 가속도·자이로의 원리와 상보필터를 직접 체험할 수 있습니다. 센서퓨전 칩(BNO055)을 쓰면 편리하지만, '왜 그렇게 동작하는지' 원리를 배우기에는 MPU-6050이 훨씬 교육적입니다. |
8장. 소스코드
▶ 8.1 동작 설명
| 항목 | 내용 |
| 초기 캘리브레이션 | 부팅 시 2초간 정지 → 자이로 오프셋 자동 측정 |
| 가속도 기반 각도 | Roll·Pitch를 atan2() 삼각함수로 계산 |
| 자이로 적분 | 각속도 × dt를 누적하여 각도 변화량 계산 |
| 상보필터 | 가속도(2%) + 자이로(98%) 융합 → 안정적 각도 |
| OLED 표시 | Roll·Pitch 수치 + 수평 인디케이터 바 실시간 표시 |
| millis() 비블로킹 | 센서 폴링(5ms), OLED 갱신(100ms) 독립 동작 |
▶ 8.2 OLED 화면 레이아웃
OLED 출력 화면
| Y 위치 | 크기 | 표시 내용 |
| y=0 | TextSize(1) | MPU6050 ATTITUDE 타이틀 |
| y=10 | ───── | 구분선 |
| y=13 | TextSize(1) | ROLL : 수치(°) |
| y=24 | TextSize(1) | PITCH: 수치(°) |
| y=37 | 그래픽 | ROLL 수평 인디케이터 바 (좌우 기울기) |
| y=50 | 그래픽 | PITCH 수평 인디케이터 바 (앞뒤 기울기) |
▶ 8.3 전체 소스코드
| //================================================ // 민선생의 전자공방 - 브레드보드 IO 인터페이스 8편 // MPU-6050 가속도·자이로 + 상보필터 // + OLED Roll/Pitch 자세 대시보드 // 보드: Arduino Uno R3 // OLED: SSD1306 128x64 I2C (A4=SDA, A5=SCL, 0x3C) // MPU-6050: I2C (A4=SDA, A5=SCL, 0x68) //================================================ #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> // ── OLED 설정 ──────────────────────────────── #define SCREEN_W 128 #define SCREEN_H 64 #define OLED_ADDR 0x3C #define OLED_RESET -1 // ── MPU-6050 설정 ──────────────────────────── #define MPU_ADDR 0x68 #define ACCEL_SENS 16384.0 // ±2g → 16384 LSB/g #define GYRO_SENS 131.0 // ±250°/s → 131 LSB/(°/s) // ── 측정 주기 ──────────────────────────────── #define SENSOR_INTERVAL 5 // 5ms (200Hz) 폴링 #define OLED_INTERVAL 100 // 100ms OLED 갱신 #define CALIB_SAMPLES 500 // 캘리브레이션 샘플 수 // ── 상보필터 가중치 ────────────────────────── #define ALPHA 0.98 // 자이로 신뢰도 98% // ── 객체 생성 ──────────────────────────────── Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, OLED_RESET); // ── 전역 변수 ──────────────────────────────── unsigned long prevSensor = 0, prevOLED = 0; float gyroOffsetX = 0, gyroOffsetY = 0; // 자이로 오프셋 float roll = 0, pitch = 0; // 최종 각도 // ── MPU-6050 초기화 ────────────────────────── void mpuInit() { Wire.beginTransmission(MPU_ADDR); Wire.write(0x6B); // PWR_MGMT_1 레지스터 Wire.write(0x00); // 절전모드 해제 (Wake up) Wire.endTransmission(true); } // ── 가속도·자이로 원시값 읽기 ──────────────── void readMPU(int16_t *ax, int16_t *ay, int16_t *az, int16_t *gx, int16_t *gy, int16_t *gz) { Wire.beginTransmission(MPU_ADDR); Wire.write(0x3B); // ACCEL_XOUT_H 시작 주소 Wire.endTransmission(false); Wire.requestFrom(MPU_ADDR, 14, true); // 14바이트 연속 읽기 *ax = Wire.read()<<8 | Wire.read(); *ay = Wire.read()<<8 | Wire.read(); *az = Wire.read()<<8 | Wire.read(); Wire.read(); Wire.read(); // Temp (사용 안함) *gx = Wire.read()<<8 | Wire.read(); *gy = Wire.read()<<8 | Wire.read(); *gz = Wire.read()<<8 | Wire.read(); } // ── 자이로 오프셋 캘리브레이션 ─────────────── void calibrateGyro() { int16_t ax,ay,az,gx,gy,gz; long sumX=0, sumY=0; display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(15, 20); display.print("Calibrating..."); display.setCursor(10, 35); display.print("DO NOT MOVE SENSOR!"); display.display(); for(int i=0; i<CALIB_SAMPLES; i++){ readMPU(&ax,&ay,&az,&gx,&gy,&gz); sumX += gx; sumY += gy; delay(2); } gyroOffsetX = sumX / (float)CALIB_SAMPLES; gyroOffsetY = sumY / (float)CALIB_SAMPLES; display.clearDisplay(); display.setCursor(20, 28); display.print("Calibration Done!"); display.display(); delay(1000); } // ── 상보필터로 Roll/Pitch 계산 ─────────────── void updateAngles(float dt) { int16_t ax,ay,az,gx,gy,gz; readMPU(&ax,&ay,&az,&gx,&gy,&gz); // ① 가속도 기반 각도 (atan2 사용) float accelRoll = atan2(ay, az) * 180.0 / PI; float accelPitch = atan2(-ax, sqrt((long)ay*ay + (long)az*az)) * 180.0 / PI; // ② 자이로 각속도 (오프셋 보정 + 단위 변환) float gyroRollRate = (gx - gyroOffsetX) / GYRO_SENS; float gyroPitchRate = (gy - gyroOffsetY) / GYRO_SENS; // ③ 상보필터: 자이로 적분(98%) + 가속도(2%) roll = ALPHA * (roll + gyroRollRate * dt) + (1-ALPHA) * accelRoll; pitch = ALPHA * (pitch + gyroPitchRate * dt) + (1-ALPHA) * accelPitch; } // ── 수평 인디케이터 바 그리기 ───────────────── // angle: -90~90 → 화면 중앙 기준 좌우로 이동하는 바 void drawLevelBar(int y, float angle, const char* label) { display.setCursor(0, y); display.print(label); // 바 외곽 (전체 0~127) display.drawRect(28, y, 100, 8, SSD1306_WHITE); // 중앙 기준선 display.drawFastVLine(28+50, y-2, 12, SSD1306_WHITE); // angle(-90~90) → 픽셀 위치(0~96)로 변환, 중앙=48 int pos = map((int)angle, -90, 90, 0, 96); pos = constrain(pos, 0, 96); display.fillRect(29+pos, y+1, 4, 6, SSD1306_WHITE); } // ── OLED 대시보드 표시 ──────────────────────── void showDashboard() { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); // 타이틀 display.setTextSize(1); display.setCursor(10, 0); display.print("MPU6050 ATTITUDE"); display.drawLine(0, 10, 127, 10, SSD1306_WHITE); // Roll 수치 display.setCursor(0, 13); display.print("ROLL : "); if(roll >= 0) display.print(" "); display.print(roll, 1); display.print((char)247); // ° 기호 // Pitch 수치 display.setCursor(0, 24); display.print("PITCH: "); if(pitch >= 0) display.print(" "); display.print(pitch, 1); display.print((char)247); display.drawLine(0, 34, 127, 34, SSD1306_WHITE); // 인디케이터 바 drawLevelBar(40, roll, "R:"); drawLevelBar(54, pitch, "P:"); display.display(); } // ───────────────────────────────────────────── void setup() { Serial.begin(9600); Wire.begin(); mpuInit(); if(!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { while(1); } calibrateGyro(); // 절대 흔들지 마세요! } // ───────────────────────────────────────────── void loop() { unsigned long now = millis(); // ── 센서 폴링: 5ms마다 (200Hz) ───────────── if(now - prevSensor >= SENSOR_INTERVAL) { float dt = (now - prevSensor) / 1000.0; // 초 단위 prevSensor = now; updateAngles(dt); Serial.print("Roll:"); Serial.print(roll,1); Serial.print(" Pitch:"); Serial.println(pitch,1); } // ── OLED 갱신: 100ms마다 ──────────────────── if(now - prevOLED >= OLED_INTERVAL) { prevOLED = now; showDashboard(); } } |
9장. 핵심 코드 해설
▶ 9.1 14바이트 연속 읽기 — 한 번의 통신으로 6축 모두 확보
MPU-6050의 가속도·온도·자이로 레지스터는 주소 0x3B부터 14바이트가 연속으로 배치되어 있습니다. requestFrom()으로 한 번에 읽으면 통신 횟수를 줄여 5ms 폴링 주기를 안정적으로 지킬 수 있습니다.
| Wire.beginTransmission(MPU_ADDR); Wire.write(0x3B); // 시작 레지스터 주소 Wire.endTransmission(false); // false: 연결 유지(restart) Wire.requestFrom(MPU_ADDR, 14, true); // 14바이트 한번에 // 레지스터 구성 (0x3B ~ 0x48): // ACCEL_XOUT(2) ACCEL_YOUT(2) ACCEL_ZOUT(2) // TEMP_OUT(2) // GYRO_XOUT(2) GYRO_YOUT(2) GYRO_ZOUT(2) // = 총 14바이트 |
▶ 9.2 atan2()로 기울기 각도 계산하기
정지 상태의 가속도 벡터는 항상 중력 방향(1g)을 향합니다. 이 벡터의 X·Y·Z 성분 비율로 현재 기울기를 역산할 수 있습니다.
| // Roll (X축 기준 좌우 기울기) float accelRoll = atan2(ay, az) * 180.0 / PI; // Pitch (Y축 기준 앞뒤 기울기) // sqrt(ay²+az²): Y-Z 평면에서의 합벡터 크기 float accelPitch = atan2(-ax, sqrt((long)ay*ay + (long)az*az)) * 180.0 / PI; // atan2()는 atan()과 달리 -180°~180° 전체 범위를 다룰 수 있어 // 분모가 0이 되는 상황(division by zero)도 안전하게 처리합니다. |
▶ 9.3 상보필터 — 한 줄에 담긴 두 센서의 융합
| roll = ALPHA * (roll + gyroRollRate * dt) + (1-ALPHA) * accelRoll; // └──────────┬──────────┘ └──────┬──────┘ // 자이로 적분값 (98%) 가속도값 (2%) // 빠르지만 시간이 지나면 틀어짐 느리지만 절대 기준 // dt(이전 루프와의 시간차, 초 단위)를 곱해 적분(누적)함으로써 // '각속도(°/s)'를 '각도 변화량(°)'으로 변환합니다. |
▶ 9.4 수평 인디케이터 바 — map()으로 각도를 픽셀로
-90°~90° 범위의 각도를 OLED 바 그래프의 픽셀 위치(0~96)로 변환합니다. 중앙(48)이 0°, 좌우로 갈수록 기울어진 정도를 나타냅니다.
| int pos = map((int)angle, -90, 90, 0, 96); pos = constrain(pos, 0, 96); // 범위를 벗어나지 않도록 제한 display.fillRect(29+pos, y+1, 4, 6, SSD1306_WHITE); // angle = 0° → pos = 48 (정중앙) // angle = +90° → pos = 96 (오른쪽 끝) // angle = -90° → pos = 0 (왼쪽 끝) |
▶ 9.5 millis() 비블로킹 — 센서 폴링과 OLED 갱신 분리
센서는 200Hz(5ms)로 빠르게 폴링하여 정밀한 dt를 확보하고, OLED는 100ms마다만 갱신하여 화면이 너무 자주 바뀌어 어지럽지 않도록 합니다.
| unsigned long prevSensor = 0; // 5ms 주기 unsigned long prevOLED = 0; // 100ms 주기 void loop() { unsigned long now = millis(); if(now - prevSensor >= 5) { float dt = (now - prevSensor) / 1000.0; prevSensor = now; updateAngles(dt); // 빠른 갱신 → 정밀한 각도 } if(now - prevOLED >= 100) { prevOLED = now; showDashboard(); // 느린 갱신 → 안정적인 화면 } } |
▶ 9.6 주요 함수 정리
| 함수/개념 | 설명 |
| mpuInit() | MPU-6050 절전모드 해제 (PWR_MGMT_1 레지스터에 0x00 기록) |
| readMPU() | 0x3B부터 14바이트 연속 읽기 → 가속도·온도·자이로 6축 확보 |
| calibrateGyro() | 500회 샘플 평균으로 자이로 오프셋 측정 (정지 필수) |
| atan2(y,x) | 역탄젠트 (-180°~180°), 가속도 벡터로 기울기 각도 계산 |
| 상보필터 | Angle = 0.98×(Angle+Gyro×dt) + 0.02×Accel |
| drawLevelBar() | map()으로 각도(-90~90)를 바 그래프 픽셀(0~96)로 변환 |
10장. 동작 확인 및 테스트
▶ 10.1 동작 확인 순서
| 단계 | 확인 내용 | 정상 동작 |
| 1 | 전원 인가 | 'Calibrating... DO NOT MOVE SENSOR!' 표시 |
| 2 | 2~3초 대기 | 'Calibration Done!' 표시 (이 동안 센서 고정) |
| 3 | 대시보드 진입 | ROLL·PITCH 수치 + 인디케이터 바 표시 |
| 4 | 센서를 좌우로 기울임 | ROLL 값 변화, 인디케이터 바가 좌우로 이동 |
| 5 | 센서를 앞뒤로 기울임 | PITCH 값 변화, 인디케이터 바가 좌우로 이동 |
| 6 | 수평 복귀 | 두 값 모두 0° 근처로 복귀, 바가 중앙으로 |
| 7 | 시리얼 모니터 | Roll·Pitch 값이 5ms마다 출력 |
MPU6050을 사용한 좌우상하 움직임 표시 동작영상
MPU-6050을 손으로 기울이며 OLED 인디케이터 바가 실시간 반응하는 영상
▶ 10.2 시리얼 모니터 출력
| Calibration Done! Roll:0.1 Pitch:-0.3 Roll:0.0 Pitch:-0.2 Roll:15.4 Pitch:-0.5 ← 오른쪽으로 기울임 Roll:32.7 Pitch:-0.8 Roll:0.2 Pitch:-0.1 ← 수평 복귀 Roll:0.1 Pitch:22.6 ← 앞으로 기울임 Roll:0.0 Pitch:0.3 ← 수평 복귀 |
| 🎁 보너스 노트 — 칼만 필터(Kalman Filter)는 무엇이 다를까? 상보필터는 가중치(0.98 / 0.02)를 사람이 직접 정한 고정값입니다. 반면 칼만 필터는 매 순간 센서의 신뢰도(노이즈 수준)를 통계적으로 계산하여 가중치를 동적으로 조정합니다. • 상보필터 — 구현이 간단하고 연산량이 적음 (입문용, 8편에서 사용) • 칼만 필터 — 더 정밀하지만 행렬 연산이 필요해 연산량이 많음 |
🚁 보너스 트랙: 드론은 MPU-6050을 어떻게 사용할까? — 초당 수백 번의 줄타기
드론이 허공에 못이 박힌 듯 가만히 떠 있는(호버링, Hovering) 모습을 본 적 있으신가요? 사실 드론은 가만히 있는 것이 아닙니다. 끊임없이 넘어지려 하는 것을 MPU-6050 센서가 필사적으로 막아내고 있는 상태입니다.
손바닥 위에 긴 막대기를 세워두고 떨어뜨리지 않기 위해 이리저리 손을 움직이는 놀이를 떠올려 보세요. 드론의 비행 원리도 이와 완벽하게 똑같습니다.
드론의 초고속 균형 잡기 4단계 루프
👁️ 눈 (MPU-6050 센서): "앗! 오른쪽으로 5도 기울어졌다!" (실시간 Roll·Pitch 감지)
🧠 뇌 (비행 컨트롤러, FC): "오른쪽으로 넘어지려 하니, 오른쪽 프로펠러를 더 세게 돌려서 위로 밀어 올려야 해!" (PID 제어 계산)
🦾 근육 (모터 변속기, ESC): 4개의 모터 중 오른쪽 모터들의 회전수(RPM)를 급격히 높임.
🚁 결과: 기체가 다시 수평(0도)으로 복귀.
놀라운 점은, 이 1~4번의 과정이 1초에 무려 250번에서 최대 1,000번(250Hz~1kHz)까지 반복된다는 것입니다. 인간의 눈으로는 도저히 인식할 수 없는 찰나의 순간에 끊임없이 미세 조정을 하고 있는 셈입니다.
왜 드론에는 MEMS 센서가 '생명줄'일까?
공중은 생각보다 훨씬 가혹한 환경입니다. 4개의 거대한 프로펠러가 만들어내는 엄청난 기계적 진동, 불규칙하게 불어오는 바람, 기압의 변화 등 드론을 뒤집으려는 방해 요소(외란)가 사방에 널려 있습니다.
인간의 반응 속도(약 0.2초 = 5Hz)로는 드론이 기울어지는 것을 보고 조종기로 자세를 바로잡으려 하면 이미 바닥에 추락한 뒤입니다. 진동 속에서도 오직 중력 방향(가속도)과 회전 속도(자이로)를 빠르고 정확하게 융합해 내는 MPU-6050과 같은 MEMS 센서가 없다면 드론의 비행은 아예 불가능합니다.
| ⚡ 속도 비교 — 사람 vs MPU-6050 사람의 반응 속도: 약 0.2초 (5Hz) MPU-6050 자세 보정: 1초에 250~1,000회 (250Hz~1kHz) → 최대 200배 차이! 사람이 눈을 한 번 깜빡이는 사이 센서는 자세를 200번 바로잡습니다. |
오늘의 예제와 실제 드론의 연결 고리
여러분이 오늘 브레드보드 위에서 완성한 코드는, 단순한 실험이 아니라 실제 드론의 '뇌(FC)'에 들어가는 핵심 자세 제어 코드와 그 원리가 완전히 동일합니다.
• 블로그 예제: 상보필터로 계산한 Roll·Pitch 값을 OLED 화면의 인디케이터 바를 움직이는 데 사용했습니다.
• 실제 드론: 동일하게 계산된 Roll·Pitch 값을 OLED 대신 모터 4개의 회전 속도를 결정하는 전기 신호로 변환하여 쏘아줍니다.
오늘 OLED 화면에서 손목 각도에 따라 좌우로 씰룩거리던 그 하얀 막대 바.
실제 드론에서는 모터의 출력을 제어해 기체를 수평으로 밀어 올리는 생명의 막대기입니다. ^^
| 다음 편 예고 — 9편 ▶ 9편: DRV8825 스텝모터 드라이버 + 비블로킹 가속 프로파일 • DRV8825 마이크로스텝(1/8, 1/16) 설정과 전류 제한 조정 • millis()를 이용한 비블로킹 스텝 펄스 생성 • 가속·감속 프로파일로 부드러운 모터 제어 • MPU-6050 + 스텝모터가 만나면? — 이 시리즈가 끝나면 인버티드 펜듈럼을 만들어 보려합니다. ^^ |
민선생의 전자공방 | blog.naver.com/hansikmin
태그#아두이노#Arduino#MPU6050#자이로센서#가속도센서#상보필터#MEMS#드론제어#자세제어#OLED#SSD1306#넌블로킹#millis#민선생의전자공방#전자회로#코딩교육#include#define 태그수정
