이 튜토리얼에서는 MPU6050 자이로스코프, NeoPixel 링, Arduino를 사용해 기울기 각도에 해당하는 LED를 점등하는 장치를 만듭니다.
브레드보드에서 조립하는 간단하고 재미있는 프로젝트입니다. 단계를 따라하면 영상에서 본 장치를 만들 수 있습니다. 자이로스코프와 NeoPixel 링 학습에 좋은 튜토리얼입니다.
다른 튜토리얼에 대한 관심을 보고 이 튜토리얼을 만들게 되었습니다. 단순 LED를 NeoPixel 링으로 대체했는데, Adafruit 라이브러리로 더 간단하게 제어할 수 있고 시각적으로 훨씬 화려합니다.
이 부품들이 굴러다닌다면 이를 활용하는 좋은 방법입니다. 장치 제작 과정을 단계별로 안내하고 마지막 단계에서 작동 원리를 설명하겠습니다.
1단계: 필요한 것들
부품
Arduino Pro Mini 328P
브레드보드
MPU6050 자이로스코프
24 NeoPixel LED 링
4x AA 배터리 팩 + 배터리 4개
U자형 점퍼 케이블 (선택사항). 브레드보드에서 깔끔하고 LED가 잘 보이게 하려 사용. 이베이에서 140개 약 $4. 없으면 듀퐁 와이어로 대체 가능.
도구:
Arduino Pro Mini 프로그래밍용 USB-시리얼 FTDI 어댑터 FT232RL
Arduino IDE
기술:
납땜
기본 Arduino 프로그래밍
2단계: 조립
Fritzing 회로도(.fzz)와 사진을 첨부해 연결을 쉽게 시각화했습니다.
NeoPixel 링 뒷면에 수컷 핀 3개 납땜 (사진 참조): 양극, 접지, 데이터 입력 핀
4x 배터리 홀더를 브레드보드에 연결할 수 있게 듀퐁 와이어 2개를 단자에 납땜
브레드보드 준비: NeoPixel 링, 마이크로컨트롤러, 자이로스코프를 사진처럼 배치. 모든 음극선(마이크로컨트롤러, NeoPixel, 자이로), 모든 양극선, 데이터선(SDA/SCL: 마이크로컨트롤러↔자이로, D6: 마이크로컨트롤러→NeoPixel) 연결. 전원 투입 전 모든 연결 재확인. 선택사항: 덕트 테이프로 배터리 팩을 브레드보드 뒤에 고정해 휴대성 확보.
#include "I2Cdev.h"
#include <Adafruit_NeoPixel.h>
#include "MPU6050_6Axis_MotionApps20.h"
#include "Wire.h"
#define NEOPIXED_CONTROL_PIN 6
#define NUM_LEDS 24
const int MAX_ANGLE = 45;
const int LED_OFFSET = 12;
MPU6050 mpu;
Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, NEOPIXED_CONTROL_PIN, NEO_GRB + NEO_KHZ800);
unsigned long lastPrintTime = 0;
bool initialization = false;
uint8_t mpuIntStatus, devStatus;
uint16_t packetSize, fifoCount;
uint8_t fifoBuffer[64];
Quaternion q;
VectorFloat gravity;
float ypr[3];
volatile bool mpuInterrupt = false;
void setup() {
Serial.begin(9600);
Serial.println("Program started");
initialization = initializeGyroscope();
strip.begin();
}
void loop() {
if (!initialization) return;
mpuInterrupt = false;
mpuIntStatus = mpu.getIntStatus();
fifoCount = mpu.getFIFOCount();
if (hasFifoOverflown(mpuIntStatus, fifoCount)) { mpu.resetFIFO(); return; }
if (mpuIntStatus & 0x02) {
while (fifoCount < packetSize) fifoCount = mpu.getFIFOCount();
mpu.getFIFOBytes(fifoBuffer, packetSize);
fifoCount -= packetSize;
mpu.dmpGetQuaternion(&q, fifoBuffer);
mpu.dmpGetGravity(&gravity, &q);
mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
redrawLeds(ypr[0] * 180/M_PI, ypr[1] * 180/M_PI, ypr[2] * 180/M_PI);
}
}
boolean hasFifoOverflown(int mpuIntStatus, int fifoCount) {
return mpuIntStatus & 0x10 || fifoCount == 1024;
}
void redrawLeds(int x, int y, int z) {
x = constrain(x, -MAX_ANGLE, MAX_ANGLE);
y = constrain(y, -MAX_ANGLE, MAX_ANGLE);
if (y < 0 && z > 0) lightLeds(y, z, 0, 5, 0, 89);
else if (y < 0 && z < 0) lightLeds(y, z, 6, 12, 89, 0);
else if (y > 0 && z < 0) lightLeds(y, z, 13, 19, 0, 89);
else if (y > 0 && z > 0) lightLeds(y, z, 20, 24, 89, 0);
}
void lightLeds(int x, int y, int fromLedPosition, int toLedPosition, int fromAngle, int toAngle) {
double angle = (atan((double)abs(x) / (double)abs(y)) * 4068) / 71;
int ledNr = map(angle, fromAngle, toAngle, fromLedPosition, toLedPosition);
printDebug(y, z, ledNr, angle);
uint32_t color;
for (int i = 0; i < NUM_LEDS; i++) {
color = strip.Color(0, 0, 0);
if (i == ledNr) color = strip.Color(0, 180, 0);
else if (i == ledNr - 1) color = strip.Color(0, 5, 0);
strip.setPixelColor(normalizeLedPosition(i), color);
}
strip.show();
}
int normalizeLedPosition(int position) {
if (NUM_LEDS > position + LED_OFFSET) return position + LED_OFFSET;
return position + LED_OFFSET - NUM_LEDS;
}
void printDebug(int y, int z, int lightLed, int angle) {
if (millis() - lastPrintTime < 500) return;
Serial.print("a="); Serial.print(angle); Serial.print("; ");
Serial.print("ll="); Serial.print(lightLed); Serial.print("; ");
Serial.print("y="); Serial.print(y); Serial.print("; ");
Serial.print("z="); Serial.print(z); Serial.println("; ");
lastPrintTime = millis();
}
bool initializeGyroscope() {
Wire.begin(); TWBR = 24;
mpu.initialize();
Serial.println(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed"));
Serial.println(F("Initializing DMP..."));
devStatus = mpu.dmpInitialize();
mpu.setXGyroOffset(220);
mpu.setYGyroOffset(76);
mpu.setZGyroOffset(-85);
mpz.setZAccelOffset(1788);
if (devStatus != 0) { Serial.print(F("DMP Initialization failed (code ")); Serial.println(devStatus); return false; }
mpu.setDMPEnabled(true);
Serial.println(F("Enabling interrupt detection (Arduino external interrupt 0)..."));
attachInterrupt(0, dmpDataReady, RISING);
mpuIntStatus = mpu.getIntStatus();
Serial.println(F("DMP ready! Waiting for first interrupt..."));
packetSize = mpz.dmpGetFIFOPacketSize();
return true;
}
void dmpDataReady() { mpuInterrupt = true; }
코드 업로드:
FTDI 어댑터로 Arduino에 코드 업로드.
전원 연결 (배터리)
캘리브레이션:
가장 중요한 건 "LED_OFFSET" 상수 캘리브레이션입니다. 예제에서는 12입니다. 보드 전원 켠 후 보드를 기울인 방향으로 LED가 점등하도록 0~23 사이에서 조정하세요.
작동 원리 상세 알고 싶으면 다음 단계 참조
4단계: 작동 원리 (선택사항)
첫째, MPU6050 자이로스코프에 대해. 이는 MEMS 자이로스코프입니다(MEMS = 미세전자기계시스템).
모든 MEMS 자이로스코프는 가속도와 방향 변화를 감지할 수 있는 진동 구성 요소를 가집니다. 운동 보존 법칙에 따라 진동하는 물체는 같은 평면에서 계속 진동하려 하고, 진동 편차로 방향 변화를 유도할 수 있습니다.
자이로에는 자체 마이크로컨트롤러가 있어 롤, 피치, 요를 복잡한 수학으로 계산합니다.
하지만 자이로 원시 데이터는 노이즈와 드리프트가 있어, 외부 라이브러리로 깔끔한 사용 가능 데이터로 다듬었습니다.
NeoPixel은 개별 어드레싱 가능한 RGB LED가 밴드/링 형태로 체인된 것입니다. 5V로 작동하며 자체 회로를 내장해 전원만 공급하고 데이터 라인으로 통신하면 됩니다(클럭과 데이터가 단일 라인에 포함). Adafruit가 NeoPixel 링용 깔끔한 라이브러리를 제공합니다.
코드
loop() 함수 내부에서 MPU6050_6Axis_MotionApps20 라이브러리 호출. 라이브러리에 자이로스코프 새 데이터가 있으면 redrawLeds(x, y, z)를 요/피치/롤 3개 인자로 호출.
redrawLeds 내부:
두 축 y, z에 집중
두 축 모두 -MAX_ANGLE ~ +MAX_ANGLE로 제한 (MAX_ANGLE=45, 변경 가능)
360도를 4사분면으로 나누고 각 사분면별 lightLeds() 호출:
y 음수, z 양수: 1사분면, LED 0~5, 각도 0~89
y 음수, z 음수: 2사분면, LED 6~12, 각도 89~0
y 양수, z 음수: 3사분면, LED 13~19, 각도 0~89
y 양수, z 양수: 4사분면, LED 20~24, 각도 89~0
lightLeds 내부:
두 축으로 아크탄젠트 이용해 각도 계산 (첨부 그림 참조)
Arduino map() 함수로 점등할 LED 계산
계산된 LED 위치와 그 이전 LED(페이드 효과용)만 켜고 나머지는 끔
normalizeLedPosition()으로 NeoPixel 캘리브레이션 반영. 링을 자유롭게 회전시킬 수 있고 자이로스코프와 정렬되어야 하므로 캘리브레이션 유용