상세 컨텐츠

본문 제목

드론도전기 #10_20.02.15

드론도전기/software

by 퍼블리셔환 2020. 2. 28. 17:12

본문

/**  <드론도전기> 에 있는 글은 드론 직접 제작에 도전하고, 시행착오를 겪은 글들의 모음입니다.

만약 드론을 직접 만들고 성공한 글을 확인하고 싶으시면 <how to, 아두이노드론> 카테고리를 방문해 주세요:)  **/

 

#1. Pid 제어

 

pid control, pid제어는 proportional integral derivative control의 약자로, 목표값에 도달하기 위하여 loop를 이용해서 계산의 결과값을 출력하고 그 값을 토대로 다시 입력값을 조정하는 과정을 말한다.

 

드론을 예시로 들면, “드론 전체의 평형”이라는 목표값이 있다고 가정하자. 이 목표값(예_평형, 0)에 도달하기 위해서 자이로센서나 가속도센서 등을 이용해서 현재의 기울어짐 정도(예_100)를 우선 파악해야 한다. 그 뒤에,

 

ㄱ. p제어(비례제어)를 이용해서 오차를 크게 줄이는 과정이 이어지는데, 위의 수치를 다시 예로 들면 100에서 50으로 줄이고, 다시 50에서 25로 줄이는 등의 오차의 크기에 비례한 제어 방법을 이용한다. 이 방법을 사용하면 오차의 값이 크더라도 빠르고 쉽게 목표값에 가까워질 수 있다는 장점이 있다. 우리의 드론 프로젝트에서도 거의 대부분 이 p제어를 쓰면서 드론의 평형을 맞추게 될 것이다. 하지만 p제어는 큰 단점이 있는데, 드론 같은 경우에도 굉장히 정확한 평형을 요구한다거나, 혹은 반도체 공정과정이나 로켓설계와 같은 극도로 정확한 계산값을 요구하는 산업에서는 위와 같은 p제어만으로는 설계가 완성될 수 없다. 간단하게 생각을 하면, 비례적인 방법으로는 25에서 12.5 6.25 ➞ 3.125 ➞ 1.5625 ➞ … 와 같이 오차값을 줄이는 과정은 가능하지만 절대 이 오차를 0으로 만들 수는 없다는 것을 쉽게 확인할 수 있다. 여기까지는 간단하다.


ㄴ. i제어(적분제어)는 위와 같은 p제어의 문제점을 보완해 줄 수 있다. i제어의 과정은 지금까지의오차들을 누적한 값을 적분해서 라플라스 변환을 이용하는데, 한 번 루프를 돌아서 계산을 할 때마다 이 적분값들을 계속 더하거나 빼서 목표치에 더 가깝게 만들어 준다. 다음 식은 라플라스 변환의 s를 변수로 한 단순 계산식인데, 라플라스의 s변수는 봐도 봐도 적응이 잘 안된다. 

라플라스 변환의 s변수를 이용한 pid식

 

 

이전 공학수학에서 배웠던 적분을 극한으로 계산하면 특정값에 수렴하는 과정을 다시 생각해 보면, 여기서의 i제어 역시 이와 비슷한 과정으로 적분값을 이용하여 연속적으로 오차를 0으로 줄인다고 볼 수 있다. 여기서 다시 식을 보자.

final value theorem

 

위의 식은 라플라스 변환의 final value theorem이다. 이 식은 i제어를 이용해서 오차를 0으로 줄이는 과정의 원리인데, time-shifting에서 좌변과 우변이 같음을 유의해서 보자. 

기계공학과 양현석 교수님의 기계시스템제어 수업자료

 

이 과정을 보면, 목표로 하는 w_0과 w_ss가 같게 만들어지는 과정에서 라플라스 변환의 fvt가 사용됐음을 알 수 있다. 결론적으로, 오차값, e_ss를 0으로 만들 수 있다. 위의 식을 전개하면 양변이 같게 나옴을 확인할 수 있다. 하지만 이 i제어 역시 단점이 있는데, 목표값에 이르는 과정에서 오버슛(overshoot)이라는, 값이 안정하게 제어되는 것이 아니라 목표치보다 요동치듯 맞춰진다는 단점이 있다.

 

ㄷ. d제어(미분제어)의 역할이 드디어 등장한다. 이런 오버슛되는 i제어의 단점을 보완해주기 위해서, 미분을 이용해서 안예쁘게 튀어나와있는 오차값들을 더 예쁘고 smooth하게 맞춰주는 역할을 한다. 역시 공학수학에서 배웠던 damping의 역할을 생각하면 쉬울 것 같다. 이 과정을 이용하면 정상상태(steady state)에서의 진동까지 상쇄해 줄 수 있다고 한다. 이 모든 과정을 이해하기 쉽게 영상으로 설명한 내용이 있어 함께 첨부한다.

pid제어 과정 흐름도

 

지금까지가 pid제어의 대략적인 설명이었고, 실제 산업에서는 이 모든 과정을 정밀하게 사용한다고 하지만, 우리의 드론 프로젝트에서는 p제어까지만 쓸 것 같다. 필요하면 i제어도 추가될 수 있지만, 5500원짜리 아두이노로는 pid제어를 모두 돌리기에 무리가 있다. 우리의 뇌 역시 무리가 있다. 추가로, 드론에서 각 제어과정의 상수값(위 식에서의 kp, ki, kd값)들을 허용범위 밖으로 잘못 지정하면 모터가 터진다고 한다.

 

#2. 코드를 이해하자

 

아래 코드는 pid제어를 하는 코드는 아니고, pid제어를 위한 값을 받아오기 위한 코드이다. 

 

void anglevalue() {

  Wire.beginTransmission(mpu_add) ; //get acc data
  Wire.write(0x3B) ;
  Wire.endTransmission(false) ;
  Wire.requestFrom(mpu_add, 6, true) ;

  ac_x = Wire.read() << 8 | Wire.read() ;
  ac_y = Wire.read() << 8 | Wire.read() ;
  ac_z = Wire.read() << 8 | Wire.read() ;

  Wire.beginTransmission(mpu_add) ; //get gyro data
  Wire.write(0x43) ;
  Wire.endTransmission(false) ;
  Wire.requestFrom(mpu_add, 6, true) ;

  gy_x = Wire.read() << 8 | Wire.read() ;
  gy_y = Wire.read() << 8 | Wire.read() ;
  gy_z = Wire.read() << 8 | Wire.read() ;

  deg_x = atan2(ac_x, ac_z) * 180 / PI ;  //rad to deg
  deg_y = atan2(ac_y, ac_z) * 180 / PI ;
  deg_z = atan2(ac_x, ac_y) * 180 / PI ;

  dgy_x = gy_y / 131. ;  //16-bit data to 250 deg/sec
  dgy_y = gy_z / 131. ;  //16-bit data to 250 deg/sec
  dgy_z = gy_x / 131. ;  //16-bit data to 250 deg/sec

  //complementary filter
  angle_x = (0.95 * (angle_x + (dgy_x * 0.001))) + (0.05 * deg_x) ; 
  angle_y = (0.95 * (angle_y + (dgy_y * 0.001))) + (0.05 * deg_y) ;
  angle_z = (0.95 * (angle_z + (dgy_z * 0.001))) + (0.05 * deg_z) ;

}​

사실 이 코드까지 완벽히 우리가 짜면 좋겠지만, 아직 배움이 부족해서 인터넷에서 확인한 코드를 첨부한다. 

 

직접 작성하지는 못했더라도, 여기에 쓰인 코드들은 모두 이해하고 넘어가야 다음에 우리가 코드를 직접 만지는 과정이 가능할 것이다. 저기에 쓰인 대략적인 역할은 이해되는데, 각각의 값들이 왜 꼭 정확히 저 상수가 나와야 하는지를 몰랐던 우리처럼, 코드를 보고 이해하고 싶은 사람들을 위해서 코드 바이 코드로 설명을 달아놓았다. 도움이 되면 좋겠다. 

 

  Wire.requestFrom(mpu_add, 6true) ;

mpu센서에서 출력값을 가져오는 내용인데, 그 값이 6비트짜리 이진수라는 뜻이다. 하지만 위의 pid제어 코드는 16비트짜리 수를 요구한다. 그렇기에 밑의 변환과정이 필요하다. 

 

  ac_x = Wire.read() << 8 | Wire.read() ;

  ac_y = Wire.read() << 8 | Wire.read() ;

  ac_z = Wire.read() << 8 | Wire.read() ;

x,y,z의 가속도 값을 받아와서 변환을 해 주는데, 우선 처음 받았던 6비트 이진수를 <<8 함수를 이용해서 8비트 코드로 만들어준다. 그런 뒤에, | 함수를 이용해서 변환된 8비트 수 뒤에 다시 8비트짜리 Wire.read()로 받아온 수를 붙여준다. 

 

이 과정을 숫자로 예를 들면, 101010(6비트)➞10101000(8비트)➞10101000_00000000(16비트)의 형태로 변환이 이뤄진다. 

 

  Wire.requestFrom(mpu_add, 6true) ;

 

  gy_x = Wire.read() << 8 | Wire.read() ;

  gy_y = Wire.read() << 8 | Wire.read() ;

  gy_z = Wire.read() << 8 | Wire.read() ;

이 과정 역시 위와 같다고 이해할 수 있다. 

 

  deg_x = atan2(ac_x, ac_z) * 180 / PI ;  //rad to deg

  deg_y = atan2(ac_y, ac_z) * 180 / PI ;

  deg_z = atan2(ac_x, ac_y) * 180 / PI ;

각각 x,y,z 축에서 기울어진 각도를 알기 위해서 arctan함수를 사용해서 기울어진 값을 계산하는데, radian으로 도출된 값을 우리가 확인 가능한 degree로 바꾸는 과정이다. 

 

  dgy_x = gy_y / 131. ;  //16-bit data to 250 deg/sec

  dgy_y = gy_z / 131. ;  //16-bit data to 250 deg/sec

  dgy_z = gy_x / 131. ;  //16-bit data to 250 deg/sec

이 131이라는 수를 이해하기가 조금 어려웠는데, 자이로센서는 [-250, +250]°/sec의 범위 안에서만 값을 인식할 수 있다고 한다. 그렇다면 16비트로 얻을 수 있는 가장 큰 수인 2의 16승(65536)을 위와 같은 [-250, +250]의 범위 안으로 넣어줘야 하는 과정이 필요하다. 따라서 65536/(250*2)를 하면 131.072라는 값이 나오게 된다. 결과적으로 [-250, +250]의 범위 안에 들어가는 수를 계산하기 위해서는 131이라는 수로 나눠줘야 한다. 

 

  angle_x = (0.95 * (angle_x + (dgy_x * 0.001))) + (0.05 * deg_x) ; //complementary filter

  angle_y = (0.95 * (angle_y + (dgy_y * 0.001))) + (0.05 * deg_y) ;

  angle_z = (0.95 * (angle_z + (dgy_z * 0.001))) + (0.05 * deg_z) ;

마지막으로 이 과정은 상보필터의 공식을 우리의 드론에 맞게 상수만 바꾸고 수식화해서 적용한 것이다.

 

상보필터 공식

 

#3. pid제어 코드

 

 

이제 드디어 pid제어 코드이다

void PID_setup() {

G1 = Pgain + Igain*T + Dgain/T;
G2 = -(Pgain + 2*Dgain/T);
G3 = Dgain/T;

}

void PID_control() {

  error_x=0-angle_x;
  m_x = m_x1 + G1*error_x + G2*error_x1 + G3*error_x2;
  PID_value=constrain(abs(m_x), 4, 7);

  if (error_x>=0) {
   r1=PID_value;
  }

  if (error_x<0) {
   r3=PID_value;    
  }

  error_x2=error_x1;
  error_x1=error_x;
  m_x1=m_x;

}​

위의 코드 역시도 우리가 직접 쓴 코드가 아니기에 자세히 볼 필요가 있다. 읽는 사람도 쓰는 사람도 지치지만, 어쩔 수 없다. 이래서 뭐든지 처음부터 오버페이스하면 나중에 더 힘들어진다. 

 

  G1 = Pgain + Igain*+ Dgain/T;

  G2 = -(Pgain + 2*Dgain/T);

  G3 = Dgain/T;

각각 pid에 해당하는 값들을 p제어, i제어, d제어의 영역으로 나눠서 값을 지정한 식이다. 위의 pid식을 수식화했음을 알 수 있다. T는 시간변수이다.

 

  error_x=0-angle_x;

  m_x = m_x1 + G1*error_x + G2*error_x1 + G3*error_x2;

  PID_value=constrain(abs(m_x), 47);

에러값을 목표치와 현재값의 차이로 지정한 뒤에, m_x의 변수를 새로 만들어서 각각 pid값에 해당하는 상수값을 곱해준 내용이다. 그렇게 해서 나온 pid_value는 4와 7 사이로 값을 제한해 주었다. 

 

  if (error_x>=0) {

    r1=PID_value;

  }

 

  if (error_x<0) {

    r3=PID_value;    

  }

마지막으로는 if구문을 이용해서 어떤 모터의 출력값을 얼마나 더 크게 해줄 것인지를 지정하면 된다. 한 축에 대해서만 기우는 방향에 따라서 +와 -부호가 바뀌기 때문에 error_x의 기준을 0으로 잡는다. 

 

위의 코드는 모터 4개를 모두 제어하는 것이 아닌, 대각선에 위치한 모터 두 개만 시범적으로 제어하는 내용이다. 그래서 모터 역시도 r_1과 r_3 이렇게 2개만 코드에 들어가 있다. 우선 코드가 잘 돌아가는지를 확인한 뒤에 나머지 모터들에 대해서도 코드를 입력해 주려고 한다. 이 단계에서의 고민은 실제로 날려보고 나서 제어의 성공 여부를 판단해야 되는데, 어느 정도의 안정성에 대해서 성공과 실패를 판단해야 되는지, 그리고 실제로 날려보면 외부 변수들도 굉장히 많을텐데 코드가 잘 돌아간다고 해도 외부 요인때문에 우리가 실패라고 잘못 판단하지는 않을지 걱정이 된다.  

 

코드출처:https://yngneers.tistory.com/25?category=566023

드론pid제어코드에 대해서 많은 도움을 받은 게시글이다. 

 

중간에 영화를 찍는다고 이 프로젝트를 떠나 잠시 외도를 한 적이 있었는데, 그럼에도 불구하고 꾸준히 프로젝트를 진행해준 친구들에게 감사를 전한다. 

 

 

publisher, 환

'드론도전기 > software' 카테고리의 다른 글

드론도전기 #9_20.02.11  (0) 2020.02.28
드론도전기 #8_20.02.06  (0) 2020.02.20
드론도전기 #7_20.02.04  (0) 2020.02.20
드론도전기 #4_20.01.30  (0) 2020.02.20

관련글 더보기

댓글 영역