
가속도계 센서(Accelerometer)는 물체의 가속도를 측정하는 장치입니다. 가속도는 시간에 대한 속도 변화의 비율을 뜻합니다. (속도 벡터가 단위시간 동안 얼마나 변했는지 나타내는 벡터량)

3차원 공간에서 X축, Y축, Z축을 따라 가속도를 측정하는 것이 3축 가속도 센서입니다.
여기에 대해 피치(Pitch), 롤(Roll), 요(Yaw)는 3차원 공간에서 물체의 자세(Attitude) 또는 회전을 설명하는 세 가지 기본 축을 나타내는 용어입니다. 상식으로 알고 계십쇼.
💻 이미지 출처: https://simple.wikipedia.org/wiki/Pitch,_yaw,_and_roll

피치 (Pitch) - 횡축 회전 ↕️: 좌우 방향(날개)을 관통하는 횡축(Lateral Axis, Y축)을 중심으로 하는 회전입니다. 비행기가 상승하거나 하강할 때 발생합니다.
롤 (Roll) - 종축 회전 ↔️: 물체의 앞뒤 방향(기수부터 꼬리까지)을 관통하는 종축(Longitudinal Axis, X축)을 중심으로 하는 회전입니다. 비행기가 선회(Turns)하기 위해 자세를 잡을 때 사용됩니다.
요 (Yaw) - 수직축 회전 🔄: 물체의 위아래 방향을 관통하는 수직축(Vertical Axis, Z축)을 중심으로 하는 회전입니다. 비행기의 방향(Heading)을 수평면에서 미세하게 바꾸기 위해 사용합니다.
가속도계는 정적인 상태에서는 중력 가속도를 감지하고, 움직일 때는 중력 가속도가 3축으로 얼만큼의 영향을 주었는가를 측정하여 운동 변화(진동, 충격 등)를 감지합니다.
다음의 정보를 파악할 수 있습니다.
예를 들어볼까요? 스마트폰을 기울였을 때 화면 회전을 생각해보면, 수평 상태일 때는 X=0g, Y=0g, Z=1g (중력이 Z축에만)인데, 90도를 기울이면 X=1g, Y=0g, Z=0g (중력이 X축으로 이동)가 되겠죠.
또한, 선형 가속도 측정을 통해 좌우 움직임(X축 가속도), 전후 움직임(Y축 가속도), 상하 움직임(Z축 가속도)을 알 수 있습니다.
💻 참고: 스마트폰의 3차원 가속도 센서(오일러 각, 롤 피치 요)

💻 이미지 출처. https://koreascience.kr/article/JAKO201914260900141.pdf

비행기를 생각해보세요. 처음에는 Z축에만 중력 가속도 영향 받습니다. 비행기가 이륙한다고 생각했을 때 Y축이 서서히 올라가다가 세워지는 순간 Y축에만 중력 가속도가 영향을 받습니다. 이번에는 비행기가 극단적으로 좌측 혹은 우측으로 돈다고 생각해보면 X축에만 중력 가속도가 영향을 받겠죠.
가속도계는 단독으로 사용될 때 몇 가지 주요 문제에 직면하며, 이로 인해 정확한 위치, 방향, 회전각을 계산하는 데 한계가 있습니다.
#1. 회전 및 방위각 측정 불가
가속도계만으로는 중력 방향과 일치하는 축을 중심으로 한 회전을 감지할 수 없습니다.
Z축 중심 회전 (요/Yaw) 감지 불가: 물체가 수평면에서 회전하는 방위각(Yaw)은 가속도의 크기(중력 방향)를 변화시키지 않기 때문에 가속도계만으로는 이 회전 각도를 전혀 측정할 수 없습니다. 이 문제를 해결하기 위해서는 자이로스코프 센서를 함께 사용하여 회전 속도 정보를 보완해야 합니다.
스마트폰을 수평면에다가 놓고 회전시켜봐도 Z축에만 중력 가속도가 영향을 받고, 나머지 X축, Y축은 전혀 영향이 없으니까 그런것이죠. (아하...)
#2. 중력과 운동 가속도의 구별 불가
가속도계는 자신이 겪는 모든 가속도를 하나의 힘으로 측정합니다.
가속도계가 자유 낙하할 때는 중력의 영향을 받지 않는(무중력 상태와 같은) 것으로 간주되어 측정값이 0g로 나옵니다. 반면, 지표면에 가만히 놓여 있을 때는 지구 중력에 의해 1g의 가속도가 측정됩니다.
가속도계는 중력과 순수한 운동으로 인한 가속도를 분리하여 인식할 수 없습니다. 따라서 움직이는 상태에서 중력의 영향을 보정하여 순수한 운동 가속도만을 얻으려면 별도의 계산과 필터링이 필요합니다.
#3. 적분 오차의 누적 (Drift)
가속도계는 속도()와 위치()를 계산하기 위해 측정된 가속도()를 시간()에 대해 적분합니다. 이 과정에서 작은 오차들이 누적되어 속도와 위치 정보의 정확도가 크게 떨어집니다.

이러한 한계 때문에 실제로 자세, 속도, 위치 등 복잡한 움직임을 정밀하게 파악하기 위해서는 관성 측정 장치(IMU, Inertial Measurement Unit)가 필수적으로 사용됩니다.
IMU 란 물체의 속도, 방향, 중력, 회전(각속도)과 같은 관성력을 측정하기 위해 여러 센서를 하나로 통합한 전자 장치입니다. IMU는 일반적으로 다음과 같은 센서들로 구성됩니다.
참고. MPU6050 IMU 센서 (MPU-6050 IMU Sensor)

MEMS(Micro-Electro-Mechanical Systems, 미세 전자기계 시스템)는 초소형 기계 부품과 전자 회로를 하나의 실리콘 칩 위에 집적하는 반도체 제조 기술입니다. 마이크로미터(μm, 100만 분의 1미터) 크기의 초소형 정밀 기계 및 전자 부품을 만들 수 있습니다.
오늘날 우리가 사용하는 대부분의 IMU에 들어가는 가속도계와 자이로스코프 센서가 바로 이 MEMS 기술로 만들어집니다. MEMS 기술 덕분에 과거에는 크고 비쌌던 관성 센서들을 매우 작고 저렴하게 만들 수 있게 되었고, 이는 스마트폰이나 드론과 같은 소형 기기에 IMU를 탑재하는 것을 가능하게 했습니다.
💻 이미지 출처. Concordia University > MEMS 기술 (정말 대단하네요...)

가속도계 센서는 그 유용성 덕분에 일상생활부터 첨단 산업까지 폭넓게 활용됩니다.
가속도계 센서의 기본 작동 원리는 뉴턴의 제2법칙(F = ma)과 관성력에 기반을 둡니다. 센서 내부에 있는 작은 질량체에 가해지는 힘(F)을 측정하여, 그 힘에 비례하는 가속도(a)를 간접적으로 알아내는 방식입니다.
현대 소형 기기에 사용되는 MEMS(미세 전자기계 시스템) 가속도계를 중심으로 원리를 설명드리겠습니다.
가속도계 센서는 매우 작은 크기로 제작되며, 다음의 핵심 구성 요소를 포함합니다.
💻 이미지 출처. CircuitBread > How does an Accelerometer work?

가장 흔하게 사용되는 정전 용량형(Capacitive) 가속도계의 작동 원리는 다음과 같습니다.
#1. 관성력의 발생
센서에 외부에서 가속도(a)가 가해지면, 검증 질량(m)은 그 관성 때문에 가속도와 반대 방향으로 움직이려는 힘(F)을 받습니다. 이 힘이 바로 관성력 입니다.

관성력()은 질량체에 작용하여 스프링을 압축하거나 늘어나게 만들어 변위()를 발생시킵니다.
#2. 정전 용량의 변화
이때 검증 질량은 두 개의 고정된 전극(Capacitor Plates) 사이에 위치합니다.
질량체가 한쪽 전극으로 가까워지면 (사이의 거리가 좁아지면) 그 전극과의 정전 용량(C)이 증가합니다. 동시에 다른 쪽 전극과는 멀어지면서 (사이의 거리가 넓어지면) 정전 용량이 감소합니다.

정전 용량(C)은 전극 면적(A)에 비례하고, 전극 사이의 거리(d)에 반비례합니다.

#3. 전기 신호 변환 및 출력
센서 내부 회로는 이 두 전극 사이의 정전 용량 차이(ΔC)를 측정합니다.

이 정전 용량 변화량(ΔC)은 가속도()에 정확히 비례합니다. 이 변화량을 전압 신호로 변환하고 증폭하여 최종적으로 가속도 값(일반적으로 g 단위 또는 m/s2 단위)으로 출력합니다.

대부분의 현대 가속도계는 3차원 공간에서의 움직임을 감지하는 3축 가속도계입니다. 센서 칩 내부에 서로 직교하는 X, Y, Z축 방향으로 각각의 검증 질량-스프링 구조가 독립적으로 설계되어 있습니다.

이제 3차원 모두에서 움직임을 감지할 수 있게 되었습니다. 가속도계는 이 원리를 이용해 두 가지 종류의 가속도를 모두 측정합니다.
가속도계 센서(Accelerometer)와 자이로스코프 센서(Gyroscope)는 모두 물체의 움직임을 감지하는 관성 센서이지만, 측정하는 물리량이 근본적으로 다릅니다. 이 두 센서는 상호 보완적으로 사용되어 물체의 자세와 움직임을 정확하게 파악합니다.
아래 그림에서 왼쪽은 가속도만 측정한 경우, 오른쪽은 가속도와 자이로스코프 둘 다 측정하는 경우 입니다. 당연하게도 하나 보다는 두 개를 측정하는 게 더 정확하겠죠?

가속도에 대해서는 위에서 이야기 했으니 생략하고 자이로스코프에 대해 간단히 알아봅시다. 자이로스코프는 회전 운동을 측정합니다. 물체의 각속도(, 단위: deg/s 또는 rad/s)를 측정합니다.
동작 원리는 코리올리 힘(Coriolis Force)을 이용합니다.
측정된 각속도를 시간에 대해 적분하면 물체의 회전 각도나 방위각을 알 수 있습니다.
가속도계 센서와 자이로스코프, 두 센서는 서로 다른 유형의 움직임을 측정하며, 각각의 단점을 상호 보완합니다.
| 구분 | 가속도계 (Accelerometer) | 자이로스코프 (Gyroscope) |
|---|---|---|
| 측정 물리량 | 선형 가속도 () 및 중력 () | 각속도 (, 회전 속도) |
| 측정하는 움직임 | 기울기, 직선 이동, 진동, 충격 | 회전, 자세 변화의 속도 |
| 장점 | 기울기(정적 자세) 감지에 정확함 | 회전 움직임(동적 자세) 감지에 정확함 |
| 단점 | 중력/가속도 구분 불가, 위치 오차 누적 | 각도 오차 누적 (Drift), 직선 이동 감지 불가 |
두 센서를 하나로 통합한 장치를 위에서 말한 관성 측정 장치(IMU, Inertial Measurement Unit)라고 하며, 두 센서의 데이터를 융합하여 정밀한 움직임과 자세 정보를 얻습니다.
이 두 센서는 스마트폰의 화면 회전, 드론의 자세 제어, 내비게이션 시스템, 로봇 공학, 게임 컨트롤러 등 현대 모바일 및 정밀 제어 장치에서 필수적으로 사용됩니다.
MPU-6050은 관성 센서 모듈 중 가장 널리 사용되는 제품 중 하나이며, 3축 가속도계와 3축 자이로스코프를 단일 칩(Single Chip)에 통합한 6축 관성 측정 장치(6-Axis IMU, Inertial Measurement Unit)입니다.
💻 구매처: https://www.devicemart.co.kr/goods/view?no=1247052

GY-521 MPU6050 모듈에 대한 간단한 스펙입니다.
구체적인 내용은 InvenSense > MPU-6050 페이지나 데이터시트를 참고하십쇼.

위 그림을 확인하면 칩을 자세히 보면 좌상단에 동그라미 표시가 있죠? 이게 기준입니다. 이 상태를 기준으로 x, y, z 축이 어디인지, 움직이면 값이 어떻게 변할 것인지 예측 가능할 겁니다.
MPU-6050은 디지털 센서로, 사용자가 측정 범위를 선택할 수 있으며, 이에 따라 센서의 민감도(Sensitivity)가 달라집니다.
📌 3축 가속도계 측정 범위: ±2g, ±4g, ±8g, ±16g
g는 중력 가속도 단위입니다. 지구의 중력 가속도는 약 9.8 m/s²이고, 이를 1g라고 합니다. 따라서 2g는 중력의 2배 가속도, 8g는 중력의 8배 가속도를 의미합니다.
센서가 "±2g"라는 범위를 가진다는 것은 이 센서가 정확하게 측정하고 출력할 수 있는 최대 가속도가 양방향으로 2g 라는 것을 의미합니다. 예를 들어 ±16g 범위로 설정하면 센서는 16g의 큰 충격까지 측정할 수 있습니다.
사용 목적에 따라 적절한 범위를 선택해야 합니다.
가속도계는 측정된 아날로그 신호를 16비트와 같은 정해진 비트 수의 디지털 값으로 변환합니다 (ADC). 이때 측정 범위를 어떻게 설정하느냐에 따라 분해능(Resolution)이 달라집니다.
예를 들어, MPU-6050의 16비트 ADC 기준으로 설정하자면 ±2g는 총 의 범위를 개(65,536개)의 디지털 값으로 나누어 측정합니다. 따라서 정밀한 측정이 가능합니다. 반면에, ±16g는 총 의 범위를 동일한 개(65,536개)의 디지털 값으로 나누어 측정합니다. 측정 단위당 가속도 값이 커져 정밀도가 낮아집니다.
📌 3축 자이로스코프 측정 범위: ±250, ±500g, ±1000g, ±2000 deg/s
자이로스코프는 물체의 각속도(), 즉 1초 동안 회전하는 각도를 측정합니다. ±250로 설정했다면 이 센서는 양방향으로 초당 최대 250도의 회전 속도까지 정확하게 측정할 수 있습니다.
가속도계와 마찬가지로, 자이로스코프도 측정 범위를 어떻게 설정하느냐에 따라 분해능(Resolution)이 달라집니다. 센서의 ADC(아날로그-디지털 변환기) 비트 수(예: 16비트)는 고정되어 있습니다.
따라서, 미세한 움직임을 감지할 때는 가장 작은 범위를 선택하고, 최대 측정 가능한 회전 속도가 중요할 때는 가장 큰 범위를 선택해야 합니다.
MPU6050 데이터시트 중에 블록 다이어그램을 확인해보면서 핵심 구성요소와 동작과정에 대해 가볍게 살펴보겠습니다.
💻 데이터시트 참고: https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Datasheet1.pdf

핵심 구성 요소는 센서, ADC(아날로그-디지털 변환기), DMP, 그리고 통신 인터페이스입니다.
참고로, MotionFusion 알고리즘은 주로 TDK InvenSense의 모션 센서(예: MPU-6050, MPU-9150 등)에 내장된 독점 펌웨어 알고리즘을 의미합니다. 이는 여러 센서의 데이터를 통합하여 정확한 모션 추적 및 방향 인식을 가능하게 하는 센서 융합(Sensor Fusion) 기술입니다.
이 알고리즘은 별도로 DMP에서 실행되기 때문에 호스트 프로세서(스마트폰의 메인 CPU 등)의 개입 없이 복잡한 계산이 이루어져 시스템 전력 소비를 줄이고 개발을 간소화할 수 있습니다.
MPU-6050 동작 과정은 다음과 같은 단계로 데이터를 생성하고 출력합니다.
💻 참고. Cirkit Designer: How to Use MPU6050: Examples, Pinouts, and Specs
마이크로컨트롤러로는 ESP32 를 사용할 겁니다. 먼저 MPU6050 핀에 대해 알아봅시다.

여기서는 공급 전압이 2.3V ~ 3.4V 로 적혀있더라고요. 다른 곳에서는 5V도 가능하다고 했는데... 뭐가 맞는 걸까요. 확인해보니 센서 칩 자체의 공급 전압은 2.3V ~ 3.4V 가 맞고, 모듈의 일반적인 작동 전압은 3.3V ~ 5V 라고 합니다. (아핫... Cirkit Designer 가 좀 헷갈리게 적어놨네요)
📌 필수 연결 핀 (I²C 통신)
ESP32와 MPU-6050 모듈을 연결하는 방법은 주로 I²C(Inter-Integrated Circuit) 통신을 이용하며, 필요한 핀은 VCC, GND, SCL, SDA 네 가지입니다. 나머지 핀(XDA, XCL, AD0, INT)은 특정 고급 기능에 사용되며 일반적인 사용에서는 연결하지 않아도 됩니다.
| MPU-6050 핀 | ESP32 핀 | 설명 |
|---|---|---|
| VCC (전원) | 3.3V 또는 VCC | 센서 모듈에 전원을 공급합니다. 일반적으로 3.3V에 연결합니다. |
| GND (접지) | GND | 회로의 공통 접지입니다. |
| SCL (클럭) | GPIO 22 (I²C SCL) - 변경 가능 | I²C 통신을 위한 동기 클럭 신호선입니다. |
| SDA (데이터) | GPIO 21 (I²C SDA) - 변경 가능 | I²C 통신을 위한 양방향 데이터 신호선입니다. |
📌 고급 및 옵션 핀 설명
일반적인 가속도 및 자이로스코프 데이터 측정에서는 아래 핀들을 연결할 필요가 없으며, 특정 고급 기능을 사용할 때만 연결합니다.
| MPU-6050 핀 | 설명 | 연결 여부 |
|---|---|---|
| XDA, XCL | 보조 I²C 버스 (Auxiliary I²C Bus) | MPU-6050에 추가적인 외부 센서 (예: 지자기 센서)를 연결하여 9축 IMU처럼 사용할 때만 사용됩니다. ESP32와는 연결하지 않습니다. |
| AD0 | I²C 주소 선택 | 이 핀의 상태(GND 연결 또는 VCC 연결)에 따라 MPU-6050의 I²C 주소 (0x68 또는 0x69)가 결정됩니다. 일반적으로 내부 풀다운 저항으로 GND에 연결된 상태(0x68)로 사용되므로 별도로 연결하지 않습니다. |
| INT | 인터럽트(Interrupt) | MPU-6050에서 특정 이벤트 (예: 동작 감지, 데이터 준비 완료)가 발생했을 때 ESP32에 신호를 보내기 위해 사용됩니다. 실시간 반응이 중요한 경우에만 ESP32의 임의의 GPIO 핀에 연결하여 사용합니다. |
따라서 연결은 VCC, GND, I2C 통신을 위한 SCL, SDA. 총 4개 핀만 연결하면 된다는 거네요.
🤔 I2C 연결은 풀업 저항이 필요한거 아니야? 연결 안해도 괜찮아?
좋은 질문입니다. I²C 통신의 표준 규격 상 풀업 저항(Pull-up Resistor)은 필수적입니다. 하지만, MPU-6050 센서 모듈을 사용할 때는 보통 이 저항을 따로 연결하지 않아도 괜찮습니다.
먼저, 풀업 저항이 필요한 이유는 I²C 통신은 데이터 라인(SDA)과 클럭 라인(SCL)이 오픈 드레인(Open-Drain) 또는 오픈 컬렉터(Open-Collector) 방식으로 작동하기 때문입니다.
풀업 저항은 다음과 같은 역할을 합니다.
MPU-6050 모듈에서 풀업 저항 연결이 필요 없는 이유는 MPU-6050이 개별 칩(IC)이 아닌 모듈(Breakout Board) 형태이고, 대부분의 모듈에는 이미 이 풀업 저항이 내장되어 있습니다.
만약 사용하는 모듈이 풀업 저항을 내장하고 있지 않다면, 4.7kΩ 또는 10kΩ 저항을 SCL-VCC, SDA-VCC 사이에 연결해야 합니다.
만약 ESP32 개발 보드 자체에도 I²C 풀업 저항이 내장되어 있고, MPU-6050 모듈에도 풀업 저항이 있다면, 두 개의 풀업 저항이 병렬로 연결됩니다. 일반적으로는 문제가 발생하지 않지만, 저항 값이 너무 낮아지면(병렬 연결 시 저항 감소), 통신이 불안정해지거나 전력 소모가 증가할 수 있습니다.
코드를 작성하기 전에 MPU-6050 에서 주로 사용되는 레지스터에 대해 알아보도록 하겠습니다.
💻 참고: https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Register-Map1.pdf
#1. Power Management 1 - PWR_MGMT_1
파워 관리 1 (PWR_MGMT_1) 레지스터는 MPU-6050 칩의 전반적인 동작 모드, 슬립 상태, 리셋, 그리고 클럭 소스를 제어하는 데 사용되는 가장 중요한 레지스터 중 하나입니다.

이 레지스터의 8개 비트는 각각 고유한 기능을 담당합니다.
| 비트 | 비트 이름 - 기능 | 설명 및 용도 |
|---|---|---|
| Bit 7 | DEVICE_RESET | 초기화. 센서 사용 전, 레지스터 상태를 확실히 초기값으로 되돌리고 싶을 때 사용합니다. |
| Bit 6 | SLEEP | 슬립 모드(저전력 모드). 1로 설정하면 센서와 내부 회로가 저전력 슬립 모드로 진입합니다. 데이터를 측정할 때는 반드시 0으로 설정해야 합니다. |
| Bit 5 | CYCLE | 활성 모드(주기 모드). 1로 설정하면 센서가 주기적으로 깨어나 측정하고 다시 잠드는 저전력 모드로 작동합니다. 깨어나는 주기는 LP_WAKE_CTRL(레지스터 108)로 설정합니다. 슬립 모드(Bit 6)가 0일 때만 유효합니다. |
| Bit 4 | TEMP_DIS | 온도 센서 비활성화. 1로 설정하면 온도 센서 기능을 비활성화하여 전력 소모를 줄입니다. |
| Bit 3-0 | CLKSEL [3:0] | 클럭 소스 선택. 센서의 내부 동작 클럭으로 사용할 소스를 선택합니다. |
클럭 소스 선택은 센서의 측정 안정성과 정확도에 영향을 미치므로 중요합니다. 일반적으로 내부 발진기(Internal Oscillator)보다 자이로스코프를 참조하는 PLL(Phase-Locked Loop) 클럭이 더 안정적입니다.

MPU-6050은 3가지 클럭 소스 중 선택 가능 합니다. 내부 8MHz 오실레이터 (기본값), 자이로스코프 기반 클록, 외부 클럭 소스.
전원 켜지면 기본적으로 내부 오실레이터를 사용하지만, 안정성 향상을 위해 자이로스코프 클럭(또는 외부 클럭)을 사용하도록 설정하는 것을 강력히 권장합니다.
따라서 안정적인 동작을 위해 센서를 초기화할 때 일반적으로 PWR_MGMT_1 레지스터에 "0x01"을 씁니다. 이 설정은 센서를 활성화하고, 가장 안정적인 클럭 소스를 사용하도록 합니다.
#2. Sample Rate Divider - SMPRT_DIV
샘플링 속도 분배기 레지스터 (SMPLRT_DIV)는 MPU-6050이 내부적으로 센서 데이터를 얼마나 자주 업데이트할지 결정하는 레지스터입니다. 8비트 데이터 전체가 분배기 값(DIV)을 설정하는 데 사용됩니다.

SMPLRT_DIV 레지스터에 기록된 값(DIV)은 자이로스코프 출력 속도를 나누어 실제 출력 데이터율 (Output Data Rate, ODR)을 결정합니다.

MPU-6050의 자이로스코프는 기본적으로 1kHz 로 동작합니다. 단, DLPF (디지털 저역 통과 필터)를 비활성화하면 내부 속도가 8kHz로 빨라집니다. 기본적으로는 활성화되어있습니다. 가속도계는 보통 1kHz(또는 DLPF 설정에 따라 4kHz)로 동작하며, 자이로스코프의 샘플링 속도에 맞춰 동기화됩니다.
🔢 DIV 값에 따른 Sample Rate 계산 예시
| DIV 값 | 1 + DIV | Sample Rate (Hz) | 설명 |
|---|---|---|---|
| 0(0000_0000) | 1 | 1000 Hz | 최대 속도. 1ms 마다 데이터 갱신 |
| 9(0000_1001) | 10 | 100 Hz | 10ms 마다 데이터 갱신 |
| 19(0001_0011) | 20 | 50 Hz | 20ms 마다 데이터 갱신 |
| 124(0111_1100) | 125 | 8 Hz | 125ms 마다 데이터 갱신 |
따라서 자신이 원하는 데이터 출력 속도를 설정하고 싶으면 이 값을 변경하면 되겠습니다. 주의할 점은 SMPLRT_DIV 설정은 센서가 데이터를 생성하는 속도입니다. 이 속도는 센서가 데이터를 읽어가는 호스트 MCU (ESP32)의 I²C 읽기 주기와 일치해야 가장 효율적입니다.
쉽게 말해, 생산자 속도와 소비자 속도가 서로 일치하는 게 좋겠죠? 두 주기가 일치해야 ESP32가 매번 가장 최신의, 중복되지 않은 센서 데이터를 얻을 수 있습니다.
#3. Accelerometer Measurements - ACCEL_XOUT
ACCEL_XOUT은 MPU-6050 센서에서 측정된 X축 방향의 가속도 원시 데이터(Raw Data)를 저장하는 레지스터 세트의 이름입니다. 가속도계 측정값은 16비트 정수(int16_t)형태로 저장되며, 8비트씩 두 개의 레지스터에 나뉘어 저장됩니다.

MPU-6050은 데이터를 Big-Endian 형식으로 전송하기 때문에, 통신 시 상위 바이트(H)를 먼저 읽고 하위 바이트(L)를 나중에 읽습니다.
ACCEL_XOUT_H(0x3B)가 중요한 이유는 자이로스코프, 온도 센서 등의 다른 측정값 레지스터의 시작 주소가 될 수 있기 때문입니다. 이 주소부터 총 14바이트를 연속적으로 읽으면 X, Y, Z축 가속도, 온도, X, Y, Z 축 각속도 데이터를 모두 얻을 수 있습니다.


ESP32와 같은 마이크로컨트롤러가 ACCEL_XOUT 데이터를 사용할 수 있는 실제 g 값으로 변환하는 과정은 다음과 같습니다.
A. Raw Data 재조립 (Parsing) - 두 개의 8비트 레지스터(H, L)을 하나로 합치는 과정입니다.

B. 물리량 스케일링 - 재조립된 Raw Data를 센서의 민감도 계수(Sensitivity Scale Factor)로 나누어 실제 가속도 g 단위로 변환합니다.

가속도계 민감도를 ±2g (기본값)으로 설정 시, 전체 4g 범위를 65536(2^16)으로 나누면 65536 / 4 = 16384 LSB/g 가 됩니다. 실제 가속도(g) = raw_값 / 16384.0 인거죠. 예를 들어, Z축 가속도계 값이 16384로 읽히면 16384 / 16384 = 1g (지구 중력 1g)가 나옵니다.
자이로스코프 민감도를 ±250 deg/s (기본값)으로 설정 시, 전체 500°/s 범위를 65536으로 나누면 65536 / 500 = 131.072 ≈ 131 LSB/(°/s) 가 됩니다. 실제 각속도(deg/s) = raw_값 / 131.0 인거죠.
📌 참고. 센서의 측정범위(민감도)를 설정하는 레지스터는 GYRO_CONFIG(0x1B), ACCEL_CONFIG(0x1C) 입니다.

AFS_SEL[1:0] 으로 설정할 수 있습니다.

데이터시트를 보다보니, I2C 통신에 대한 과정도 설명이 잘 되어있어서 간단히 살펴보겠습니다. (어차피 코드 작성할 때 이 내용을 알아야 합니다. 복습할 겸 정리해봤습니다)
I²C는 두 개의 신호선만 사용하며, 여러 개의 장치가 하나의 버스에 연결될 수 있는 마스터-슬레이브(Master-Slave) 구조입니다. 두 라인은 모두 풀업 저항을 통해 HIGH 상태로 유지됩니다. 또한, I²C는 반이중 방식입니다. 송신과 수신이 동시에 이루어질 수 없다는 거죠.

I²C 통신은 데이터 전송을 위해 정해진 4가지 주요 단계(시작, 주소 지정, 데이터 전송, 정지)를 거칩니다.
📌 A. 시작 조건 - 통신을 시작하는 신호입니다. 마스터가 SDA 라인을 HIGH에서 LOW로 바꾸는 동안 SCL 라인은 HIGH 상태여야 합니다. 이 신호는 버스에 연결된 모든 슬레이브 장치에게 통신이 시작되었음을 알립니다.

📌 B. 주소 지정 및 읽기/쓰기 비트 전송 - 통신을 시작한 마스터는 어떤 슬레이브와 대화할지 지정해야 합니다. 마스터는 통신하려는 슬레이브의 7비트 고유 주소를 SDA 라인을 통해 전송합니다. 주소 뒤에 1비트를 추가하여 마스터가 데이터를 쓰려는지(0) 또는 읽으려는지(1)를 결정합니다. SDA가 변경되는 구간에서 SCL은 LOW 상태여야 합니다.
주소 전송과 읽기/쓰기 비트 전송이 끝나면, 선택된 슬레이브는 SDA 라인을 LOW로 끌어내려 ACK (Acknowledge, 응답) 신호를 보냅니다. 이는 슬레이브가 주소를 인식했고 통신 준비가 되었음을 의미합니다.

📌 C. 데이터 전송 (Data Transfer) - ACK 신호를 받은 후, 마스터는 데이터를 전송하기 시작합니다. 데이터는 8비트(1바이트) 단위로 전송됩니다. 1바이트의 데이터가 전송될 때마다, 수신자는 ACK(LOW) 또는 NACK(HIGH) 신호로 다음 바이트를 받을 준비가 되었는지 응답합니다.

📌 D. 정지 조건 - 모든 데이터 전송이 완료되면, 마스터는 통신을 종료합니다. SCL가 HIGH일 때, SDA를 LOW에서 HIGH 로 바꿉니다. 이 신호는 버스가 해제되었으며, 다른 마스터가 통신을 시작할 수 있음을 나타냅니다.
정리해봅시다. 마스터가 슬레이브 레지스터의 데이터를 쓰고 싶다고 해봅시다. 전송 과정은 다음과 같습니다.

마스터가 시작 신호를 보내고, 슬레이브의 주소와 쓰기 비트를 보냅니다. 슬레이브는 ACK 신호를 보냅니다. 마스터는 슬레이브 레지스터의 주소를 보냅니다. 슬레이브는 ACK 신호를 보냅니다. 마스터가 슬레이브에게 데이터를 보냅니다. 그러면 해당 레지스터의 데이터가 써집니다. ACK를 보냅니다. 마지막으로 정지 신호를 보냅니다.
반대로, 마스터가 슬레이브 레지스터의 데이터를 읽고 싶다고 해봅시다. 이 과정은 쓰기(레지스터 주소 지정)와 읽기(데이터 수신)의 두 부분으로 구성됩니다.

마스터가 시작 신호를 보내고, 슬레이브의 주소와 쓰기 비트를 보냅니다. ACK 신호를 응답받습니다. 슬레이브의 레지스터 주소를 보냅니다. ACK 응답 받습니다.
이제 마스터는 반복 시작 조건(Repeated START)을 전송하여 쓰기 모드를 종료하고 읽기 모드로 전환합니다. ACK 신호를 응답받습니다. 슬레이브가 지정된 레지스터부터 데이터를 전송합니다. 마스터가 ACK를 보내면 데이터를 받았고 다음 바이트도 받겠다는 응답입니다. 마스터가 NACK를 보내면 데이터를 받았고 더 이상 데이터를 받지 않겠다는 종료 신호입니다. 마지막으로 정지 신호를 보냅니다.
ESP32가 MPU-6050 센서와 I²C 통신을 통해 데이터를 읽어오고, 100Hz의 샘플링 속도를 구현하여 1초에 100번 데이터가 출력되도록 해보겠습니다.
A. 매크로 정의 및 설정
코드 상단에는 센서와 통신에 필요한 상수 값들이 정의되어 있습니다.
#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <driver/i2c.h>
#include <esp_log.h>
// --- MPU-6050 및 I2C 설정 ---
#define I2C_MASTER_SCL_IO 22 /*!< I2C master SCL gpio num */
#define I2C_MASTER_SDA_IO 21 /*!< I2C master SDA gpio num */
#define I2C_MASTER_NUM I2C_NUM_0 /*!< I2C port number for master dev */
#define I2C_MASTER_FREQ_HZ 100000 /*!< I2C master clock frequency */
#define MPU6050_SENSOR_ADDR 0x68 /*!< MPU6050 address */
#define MPU6050_PWR_MGMT_1_REG_ADDR 0x6B
#define MPU6050_SMPLRT_DIV_REG_ADDR 0x19
#define MPU6050_DATA_START_ADDR 0x3B /*!< ACCEL_XOUT_H register address */
static const char *TAG = "MPU6050_TEST";
B. MPU-6050 레지스터에 데이터 쓰기 함수
MPU-6050의 특정 레지스터 주소에 하나의 8비트(1바이트) 데이터를 기록하여 센서 설정을 변경할 때 사용됩니다.
// --- MPU-6050 레지스터에 데이터 쓰기 함수 ---
static esp_err_t mpu6050_register_write(uint8_t reg_addr, uint8_t data)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (MPU6050_SENSOR_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg_addr, true);
i2c_master_write_byte(cmd, data, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_RATE_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
C. MPU-6050 레지스터에서 데이터 읽기 함수
MPU-6050의 특정 레지스터부터 시작하여 연속적인 여러 바이트의 데이터를 읽어올 때 사용됩니다. 센서 측정값은 연속된 레지스터에 저장되므로 이 방법이 효율적입니다.
static esp_err_t mpu6050_read_bytes(uint8_t reg_addr, uint8_t *data, size_t len)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
// 1. 읽기 시작 레지스터 주소 지정
i2c_master_write_byte(cmd, (MPU6050_SENSOR_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg_addr, true);
// 2. 읽기 명령 전송
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (MPU6050_SENSOR_ADDR << 1) | I2C_MASTER_READ, true);
if (len > 1) {
i2c_master_read(cmd, data, len - 1, I2C_ACK_VAL);
}
i2c_master_read_byte(cmd, data + len - 1, I2C_NACK_VAL);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_RATE_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
D. MPU-6050 초기화 및 100Hz 샘플링 설정
이 함수는 MPU-6050을 작동 가능한 상태로 만들고 100 Hz 샘플링을 설정합니다.
static esp_err_t mpu6050_init(void)
{
// 1. 파워 관리 레지스터(0x6B): 장치 리셋 및 클럭 소스 설정 (PLL with X-axis Gyro reference)
esp_err_t ret = mpu6050_register_write(MPU6050_PWR_MGMT_1_REG_ADDR, 0x01);
if (ret != ESP_OK) return ret;
ESP_LOGI(TAG, "MPU6050: Power Management Reset OK.");
// 2. 샘플링 속도 분배기(0x19): 100 Hz 설정
// 기본 샘플링 속도 1000 Hz / (1 + DIV) = 100 Hz -> DIV = 9
ret = mpu6050_register_write(MPU6050_SMPLRT_DIV_REG_ADDR, 0x09);
if (ret != ESP_OK) return ret;
ESP_LOGI(TAG, "MPU6050: Sample Rate set to 100 Hz (Divider=9).");
// 기타 설정 (옵션): DLPF, Gyro Range, Accel Range 등 필요에 따라 추가
// ex) mpu6050_register_write(0x1A, 0x06); // DLPF 설정 (5Hz)
return ESP_OK;
}
E. I2C 마스터 드라이버 초기화
이 함수는 ESP32를 I²C 마스터로 설정하여 MPU-6050과 통신할 수 있도록 준비합니다.
static void i2c_master_init(void)
{
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_io_num = I2C_MASTER_SCL_IO,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
i2c_param_config(I2C_MASTER_NUM, &conf);
i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
ESP_LOGI(TAG, "I2C Master Initialized.");
}
F. 메인 루프
app_main 함수는 센서 초기화 후 데이터를 반복적으로 읽어 출력하는 핵심 부분입니다.
void app_main(void)
{
i2c_master_init();
if (mpu6050_init() != ESP_OK) {
ESP_LOGE(TAG, "MPU6050 Initialization Failed!");
return;
}
uint8_t data[14];
int16_t acc_x, acc_y, acc_z, temp_raw, gyro_x, gyro_y, gyro_z;
// 100 Hz 출력을 위한 딜레이 설정 (10 / 1000 = 10ms)
const TickType_t delay_ticks = 10 / portTICK_PERIOD_MS;
while (1) {
// MPU6050_DATA_START_ADDR(0x3B)부터 14바이트를 읽음 (Acc_XYZ, Temp, Gyro_XYZ)
esp_err_t ret = mpu6050_read_bytes(MPU6050_DATA_START_ADDR, data, 14);
if (ret == ESP_OK) {
// 1. 데이터 파싱 (Big-Endian 형식)
acc_x = (int16_t)(data[0] << 8 | data[1]);
acc_y = (int16_t)(data[2] << 8 | data[3]);
acc_z = (int16_t)(data[4] << 8 | data[5]);
temp_raw = (int16_t)(data[6] << 8 | data[7]);
gyro_x = (int16_t)(data[8] << 8 | data[9]);
gyro_y = (int16_t)(data[10] << 8 | data[11]);
gyro_z = (int16_t)(data[12] << 8 | data[13]);
// 2. 센서값 출력
printf("ACC: X=%d, Y=%d, Z=%d | ", acc_x, acc_y, acc_z);
// 온도 계산 (옵션)
float temperature = (temp_raw / 340.0) + 36.53;
printf("TEMP: %.2f C | ", temperature);
printf("GYRO: X=%d, Y=%d, Z=%d\n", gyro_x, gyro_y, gyro_z);
printf("\n");
} else {
ESP_LOGE(TAG, "I2C Read Failed: %d", ret);
}
// 100 Hz (10ms) 주기를 맞추기 위한 딜레이
vTaskDelay(delay_ticks);
}
}
이 코드는 Raw Data를 출력하고 있지만, 실제 애플리케이션에서는 이 Raw Data를 민감도 계수로 나누어 g와 deg/s 단위의 물리량으로 변환한 후 사용해야 합니다. (아래 참고)
// --- 민감도 계수 (Sensitivity Scale Factors) 정의 ---
// 기본 설정: Accel = +/- 2g (16384 LSB/g), Gyro = +/- 250 deg/s (131.0 LSB/(deg/s))
#define ACCEL_SCALE_FACTOR 16384.0f
#define GYRO_SCALE_FACTOR 131.0f
...
// Raw Data 파싱 이후 코드 추가
// 물리량 변환 (스케일링)
// 가속도: Raw Data를 16384.0f로 나누어 g 단위로 변환
acc_x_g = (float)acc_x_raw / ACCEL_SCALE_FACTOR;
acc_y_g = (float)acc_y_raw / ACCEL_SCALE_FACTOR;
acc_z_g = (float)acc_z_raw / ACCEL_SCALE_FACTOR;
// 자이로스코프: Raw Data를 131.0f로 나누어 deg/s 단위로 변환
gyro_x_dps = (float)gyro_x_raw / GYRO_SCALE_FACTOR;
gyro_y_dps = (float)gyro_y_raw / GYRO_SCALE_FACTOR;
gyro_z_dps = (float)gyro_z_raw / GYRO_SCALE_FACTO
제대로 실행되는지 확인해봤습니다. 음... 출력이 안되더군요. 근데 종료하니까 데이터가 출력되긴 합니다. 아무래도 1초에 100번 출력은 좀 빡센거 같아보입니다.
그래서 1초에 10번 출력(100Hz)으로 바꿨습니다. delay_ticks 값도 수정하고요. 샘플링 속도 분배기 레지스터 값도 변경해줍니다. 99(0x63) 으로 변경. 그랬더니 출력이 되더군요.

잘 바뀌는거 같습니다. 하지만 오디오 실험할 때도 그랬습니다만... 숫자로 보려니까 제대로 변하고 있는지 한번에 확인하기 어렵습니다. 역시 이것도 그래프로 실시간으로 확인해보고 싶습니다.
파이썬으로 코드를 작성하면 되는데... 글이 너무 길어져서 다음 글에서 다뤄봐야 될 거 같네요.