[아두이노] MPU-6050 센서 + processing 연결 제어

IOT/아두이노|2019. 5. 13. 09:00

[아두이노] MPU-6050 센서 + processing 연결 제어



어제 MPU-6050 가속도/자이로센서에 대해 간단히 테스트를 해 보았습니다. 어제는 MPU-6050 라이브러리 없이 I2C 통신으로 센서의 값을 가져왔는데 오늘은 MPU-6050 라이브러리를 가지고 쉽게 제어하는 방법을 살펴보도록 하겠습니다. 직접 식을 만들고 그 식을 통해서 제어하면 좋겠죠. 하지만 입문자나 초보분들은 수학적으로 알아야 할 것이 너무 많습니다. 입문자의 경우는 수학적으로 접근한다면 실제 MPU-6050를 다루기 전에 지쳐버리겠죠. 처음에는 흥미를 가지고 출발해야 좀 더 관심을 가지고 공부하게 됩니다. 수학적인 부분을 모르더라도 누군가 만들어 놓은 라이브러리를 활용하여 MPU-6050 모듈을 사용할 수 있으니 부담없이 처음에 접근하기 바랍니다. 가격이 저렴한 모듈이기에 정교한 제어용으로는 다소 무리가 있습니다. 단지 입문자용으로 부담없이 사용할 수 있는 모듈이라고 생각하시면 되겠습니다.

오늘은 실험에 사용할 라이브러는 인터넷에서 찾아보면 두개의 라이브러리가 있는데 아두이노 IDE 라이브러리관리 창에서 검색한 라이브러리를 설치하여 사용하겠습니다. 그리고, 이 라이브러리 예제를 통해 얻은 값을 테스트 한 후에 이 예제를 통해서 processing으로 만든 box 도형을 MPU-6050 모듈로 움직임을 제어하는 실험을 하겠습니다.


1. MPU-6050 가속도/자이로 센서


1) MPU-6050 가속도/자이로 센서 회로도


  • 준비물 : MPU-6050 모듈, 아두이노우노
  • 내용 : I2C 통신을 위해 A4(SDA), A5(SCL)핀에 연결하시오

지난 시간에 회로도와 동일합니다.


2) 라이브러리 설치


아두이노 IDE 라이브러리관리창에서 "MPU-6050"을 검색하면 아래 그림처럼 tockn님이 만든 라이브러리가 하나만 검색됩니다. 참고로 아래에 MPU6050으로 한개 더 있는데 이건 이미 설치된 라이브러인데 외부 구글검색으로 찾아서 다운받은 후 라이브러리를 추가했기 때문에 2개의 라이브러리가 표시 된 모습입니다.


직접 검색해서 다른 라이브러리를 다운 받으실려면 "mpu-6050 github"라고 구글검색하시면 해당 링크가 나옵니다.

https://github.com/jrowberg/i2cdevlib/tree/master/Arduino/MPU6050

3) 코딩



예제에 가시면 두개의 GetAllData, GetAngle 예제가 있습니다. 둘 다 한번씩 돌려보시기 바랍니다.


여기서, GetAllData 예제은 1초 단위로 전체 데이터를 출력하는 예제이고, GetAngle 예제는 회전각 X, Y, Z 값만 출력하는 예제입니다.

실험은 GetAngle 예제를 사용 하겠습니다. 이걸로 아래 Processing으로 표현한 box 회전에 사용하기 위해서 입니다.

[GetAngle 소스]-tockn님 소스

#include <MPU6050_tockn.h>
#include <Wire.h>

MPU6050 mpu6050(Wire);

void setup() {
  Serial.begin(9600);
  Wire.begin();
  mpu6050.begin();
  mpu6050.calcGyroOffsets(true);
}

void loop() {
  mpu6050.update();
  Serial.print("angleX : ");
  Serial.print(mpu6050.getAngleX());
  Serial.print("\tangleY : ");
  Serial.print(mpu6050.getAngleY());
  Serial.print("\tangleZ : ");
  Serial.println(mpu6050.getAngleZ());
}

소스를 살펴보면,

  Wire.begin();
  mpu6050.begin();
  mpu6050.calcGyroOffsets(true);

세가지 Wire, mpu6050 시작선언과 초기 작업으로 자이로 오프셋값을 계산합니다. 교정작업으로 자이로 오프셋 평균값을 계산하는데 한번 해당 함수들의 로직을 살펴봐 주세요. 이게 나중에 실험할 때 시간을 많이 잡아먹는 부분이더군요. 평균값을 구하기 위해서 자이로 x,y,z 값을 5000번 읽어고 그 값을 5000으로 나눠서 평균값을 계산하니깐 계산시간이 좀 걸려서 실험 시 대기시간이 길어지더군요.

참고로 값을 알고 있다면

mpu6050.setGyroOffsets(1.45, 1.23, -1.32);

이런식으로 값을 지정 할 수 있습니다.

위, 3줄의 명령은 시작 초기화 함수입니다.

loop()에서는 mpu6050.update() 함수로 이전 라이브러리 없이 읽기 요청과 읽는 함수들의 로직이 이 함수안에 다 집어 넣었더군요. 그리고 그 안에서 상보필터로 회전각을 구하는 부분까지 다 포함되어 있는데 관심있는 분들은 링크 걸어놓은 곳에 "MPU6050_tockn.cpp" 가셔서 로직을 살펴보시면 됩니다.

  • 상보 필터 : filtered_angle = (0.02 * accel) + (0.98 * gyro) (tockn님 계산식)

아무튼 총 4개의 함수를 사용하면 입문자분들은 따로 계산식을 걱정할 필요 없이 회전각 mpu6050.getAngleX(), mpu6050.getAngleY(), mpu6050.getAngleZ() 함수를 통해서 값을 얻을 수 있게 됩니다.

라이브러리를 이용하니깐 그렇게 까지 복잡하지 않죠.

[결과]
다음과 같은 결과가 출력됩니다.


2. MPU-6050 + processing 실험


1) MPU-6050 회전각을 시리얼통신을 통해 String으로 전송하기


[아두이노 실험 소스]

#include <MPU6050_tockn.h>
#include <Wire.h>

MPU6050 mpu6050(Wire);

void setup() {
  Serial.begin(19200);
  Wire.begin();
  mpu6050.begin();
  //mpu6050.calcGyroOffsets(true);
    mpu6050.setGyroOffsets(0.75, 0.05, 0.05);
}

void loop() {
  mpu6050.update();  
  Serial.print(mpu6050.getAngleX());
  Serial.print(',');
  Serial.print(mpu6050.getAngleY());
  Serial.print(',');
  Serial.println(mpu6050.getAngleZ());
  delay(50);
}

위 소스를 보시면 getAngle X,Y,Z 사이에 콤마(,)을 붙여서 한줄로 3개의 데이터가 시리얼 통신으로 전송됩니다. delay(50)을 정도를 설정했는데 안써도 상관 없습니다.

2) getAngle 수정(선택 사항)


이상태로 실험하셔도 되지만 좀 더 빠르게 실험하기 위해서 초기값을 주도록 하겠습니다. calcGyroOffsets(true)함수로 초기값을 계산하는게 아니라 직접 처음에 초기값을 주어서 빠르게 동작하도록 변경하겠습니다.

처음 한번은 getAnglem 예제를 실행하여 아래와 같은 X,Y,Z값을 얻게 되면 그 값을 메모장에 적어놓으세요


처음 getAngle 예제를 돌리면 (X,Y,Z) 가 (0.67, 0.10, 0.10)으로 계산 되었네요. 5천번의 MPU-6050 모듈에 측정된 값을 계산하기 때문에 시간이 걸립니다. 시작 위치의 초기값을 먼저 잡아놓으면 다음 계산이 필요 없기 때문에 빠르게 회전각을 얻을 수 있게 됩니다.

변경은 다음과 같은 calcGyroOffsets()을 주석처리하고 setGyroOffsets()함수로 선언해주면 됩니다. 아래의 X,Y,Z 값은 processing과 연결하여 실험 할 당시의 X, Y, Z의 값입니다. 즉, 여러분들이 실험하는 장소와 위치에서 처음 측정된 값으로 초기값으로 할 경우 이 수치는 달라 집니다. 귀찮은 분들은 시간이 걸리더라고 calcGyroOffsets()함수로 통일해도 되겠죠.

mpu6050.setGyroOffsets(0.75, 0.05, 0.05);

어려분들은 둘중 하나를 선택해서 실험 하시면 됩니다.

mpu6050.calcGyroOffsets(true); : 대기 시간이 길어지더라도 계산하겠면 선택
mpu6050.setGyroOffsets(0.75, 0.05, 0.05); : 처음 한번만 계산해 놓고 그 값을 초기값으로 고정하겠다면 선택

2) processing 코딩


참고 : [아두이노] 조이스틱+processing 3D 도형 회전(1)


[참고 소스]

import processing.serial.*;

Serial myPort;
int x=0;
int y=0;

void setup() {

    println(Serial.list());
    myPort = new Serial(this, Serial.list()[0], 9600);
    myPort.bufferUntil('\n');
    noStroke();
    size(600,600,P3D);    
} 

void draw() {  
    background(0); //배경색
    lights();  //조명

    pushMatrix();  //Start
    fill(0,255,0); //채우기
    translate(width/2,height/2,-100); //이동
    rotateX(radians(y)); //x축 회전
    rotateY(radians(x)); //y축 회전
    
    box(200,100,200); //상자
    popMatrix(); //End
}
 
void serialEvent(Serial p) { 
  String inString = myPort.readStringUntil('\n');
     
  ... 생략 ...
}

조이스틱으로 실험했던 일부 소스입니다. box를 x,y 회전을 시킨 소스의 일부분인데 오늘 사용할 부분만 제외하고 나머지 소스를 지웠습니다.

processing 문자열 읽기

void serialEvent(Serial p) { 
  String inString = myPort.readStringUntil('\n');
     
  ... 생략 ...
}

이렇게 코딩해서 시리얼통신에서 문자열일 들어오면 자동으로 이벤트 호출이 일어납니다. 그리고 해당 시리얼포트로 문자열을 읽게 됩니다. 여기까지는 아두이노에서 코딩하는 방식과 같습니다. 하지만 processing에서는 링크 걸어놓은 String 방식으로 코딩을 하지 않고 간단하게 processing 함수를 이용하여 수정하겠습니다. split()함수가 제공됨으로 쉽게 전체문자열에서 x,y,z 회전각을 분리해 낼 수 있습니다.

String[] inStrings = split(inString, ',');

inString으로 읽은 한줄의 문자열에서 콤마(,)가 기준으로 쪼개집니다. 그리고, 그 값은 문자열 배열 변수 inStrings에 저장되게 됩니다. 3개의 데이터를 전송하니깐

inStrings[0] => X축 회전
inStrings[1] => Y축 회전
inStrings[2] => Z축 회전

이 값들이 저장되어 있게 됩니다. 하지만 이 값은 문자열이기 때문에 숫자형으로 변환해야 하는데 실수형 float()로 변환을 하면 다음과 같습니다.

x=float(inStrings[0]);
y=float(inStrings[1]);
z=float(inStrings[2]);

아두이노에서 시리얼통신으로 보내 온 x,y,z값을 각각 실수형 x,y,z변수에 최종적으로 저장하게 됩니다.

void serialEvent(Serial p) { 
  String inString = myPort.readStringUntil('\n');    
  String[] inStrings = split(inString, ',');
  x=float(inStrings[0]);
  y=float(inStrings[1]);
  z=float(inStrings[2]);  
}

수신 String 중 회전각만 적용하기

문자열 읽기를 그냥 위와 같이 하면 에러가 발생합니다. 그 이유는 MPU6050_tockn 라이브러리는 초기 세팅함수 안에는 다음과 같이 여러 문자열이 출력됩니다.


회전 각이 나오기 전까지 여러문장의 문자열이 출력되는데 단순위 위와같이 코딩하면 문제가 발생하겠죠. MPU6050_tockn라이브러리에 가서 cpp 파일에 해당 함수의 들어있는 print문을 전부 주석처리하면 해결할 수 있지만 라이브러리 파일을 건들면 좀 그렇겠죠.

그래서 회전각 문자열인지 체크하는 제어문을 넣어서 해결하도록 하겠습니다.

String inString = myPort.readStringUntil('\n');   
int separator_ch = inString.indexOf(",");

if(separator_ch!=-1){
  회전각 구하기;
}else{
 print(inString);
}

이렇게 indexOf(',')로 회전각 분리기호의 위치값을 찾습니다. 회전각 문자열에는 분리기호가 있기 때문에 해당 0이상의 숫자값을 갖게 됩니다. 하지만 해당 문자열에 분리기호가 없다면 '-1'이 반환 됩니다. 이 원리를 이용해서 위와 같이 if문으로 분리기호가 있으면 방금 위에서 코딩한 문자열에서 회전각을 분리해내는 로직을 붙이면 됩니다. 분리기호가 없다면 그냥 시리얼모니터 창으로 해당 문자열을 출력하게 하면 간단히 이 문제를 해결하게 됩니다.

box 회전시키기

box 회전을 하려면 이 부분을 수정해야 합니다. rotateX(), rotateY(), rotateZ() box 회전을 시키고 해당 인자값을 아까 String으로 읽은 값을

rotateX(radians(y)); //x축 회전
rotateY(radians(x)); //y축 회전
rotateZ(radians(z)); //y축 회전
    
box(200,100,200); //상자

x,y,z 축으로 회전시켜서 box()을 배치하는 코딩입니다. box()함수 앞에다가 회전함수를 넣으면 최종적으로 회전시킨 곳에 box가 그려지기 때문에 box가 x,y,z 값에 의해서 이동하는 것처럼 보이게 됩니다.

그리기 함수부분은 아래과 같이 되겠죠.

void draw() {  
    background(0); //배경색
    lights();  //조명

    pushMatrix();  //Start
    fill(0,255,0); //채우기
    translate(width/2,height/2,-100); //이동
    rotateX(radians(y)); //x축 회전
    rotateY(radians(x)); //y축 회전
    rotateZ(radians(z)); //y축 회전
    
    box(200,100,200); //상자
    popMatrix(); //End
}

[processing 실험 소스]

import processing.serial.*;

Serial myPort;

float x=0;
float y=0;
float z=0;

void setup() {
    println(Serial.list());
    myPort = new Serial(this, Serial.list()[0], 19200);
    myPort.bufferUntil('\n');
    noStroke();
    size(600,600,P3D);    
} 

void draw() {  
    background(0); //배경색
    lights();  //조명

    pushMatrix();  //Start
    fill(0,255,0); //채우기
    translate(width/2,height/2,-100); //이동
    rotateX(radians(y)); //x축 회전
    rotateY(radians(x)); //y축 회전
    rotateZ(radians(z)); //y축 회전
    
    box(200,100,200); //상자
    popMatrix(); //End
}
 
void serialEvent(Serial p) { 
  String inString = myPort.readStringUntil('\n');   
  int separator_ch = inString.indexOf(",");
  if(separator_ch!=-1){
    String[] inStrings = split(inString, ',');
    x=float(inStrings[0]);
    y=float(inStrings[1]);
    z=float(inStrings[2]);
  
    print(x);
    print('/');
    print(y);
    print('/');
    println(z);
  }else{
    print(inString);
  }
}

추가로, print문으로 정확히 분리되어 x,y,z값이 나오는지 확인하기 위해서 processing 시리얼 모니터로 출력시키는 코딩을 넣었습니다.

5. 실험 결과


MPU-6050 모듈을 움직일 때 PC로 녹화한 영상입니다.



MPU-6050 모듈을 움직일 때 processing의 box가 어떻게 움직이는지 폰을 촬영한 영상합니다.



마무리


MPU-6050 라이브러리를 이용하면 해당 회전각을 가지고 여러분들이 원하는 표현을 쉽게 할 수 있습니다.

참고로, 주의할 점은 될 수 있으면 MPU-6050 모듈에 핀을 납땜해주시고 실험 해주세요. 그리고 핀을 연결한 선도 단단히 고정시켜주세요. 조금만 흔들려도 MPU-6050이 먹통이 됩니다. 그리고, 회전 각에 크게 변화할 때에도 한번 먹통이 발생하면 더이상 측정하기는 불가능합니다. 그리고 일정시간이 지나면 값의 변화가 생기기 때문에 정교한 제어용으로 쓰기에는 다소 부족합니다. 초보 입문용으로 싼 가격에 자이로센서를 사용하는데 가성비를 최고이지만 혹시 드론 같은 전문적으로 사용할시에는 비싼 자이로센서를 쓰기기 바랍니다.

그리고, 어느정도 재미를 붙이시면 제대로 회전각을 구하는 공식을 공부해주시기 바랍니다. 사실 기존 라이브러리로만 사용해서는 어떤 실험을 할지 모르지만 정교한 제어는 어렵습니다. 그냥 무조건 값을 읽고 계산해서는 정교한 제어가 되지 않습니다. 초기 보정작업과 기타 값의 변화율에 대한 처리부분을 고려해서 코딩해야 합니다. 수학적으로 접근해야 하는 부분들이 많기 때문에 전문서적이나 구글검색을 통해 관련 자료들을 많이 찾으셔서 공부하시기 바랍니다.

저는 기초적인 부분만 다루고 실험했기 때문에 좀 깊은 공부는 아직은 관심이 덜해서 해보지는 않아서 좀 더 자세한 내용을 전달해드리지 못하네요. 제 경우는 라이브러리 코딩쪽이 아니라 측정된 값을 기반으로 제어하는 코딩에 좀 더 관심이 많아서 그냥 라이브러리를 그대로 사용하고 있네요

자이로센서를 전문적으로 다루기 위해서는 배워야 할 부분이 많지만 간단히 실험하고 기초적인 기능만 활용하시더라도 라이브러리를 이용해 원하는 표현은 가능하니깐 MPU-6050 모듈을 배워 보세요.


댓글()