본문 바로가기
Developer/Drone DIY

드론 DIY | 아두이노 PID 제어 코드 (코드 첨부)

by Doony 2016. 2. 12.


지난 포스팅에서 PID의 원리가 뭔지, 대체 왜 필요한지에 대해 구구절절 설명했다.

따라서 이번 포스팅에서는 사실 가장 중요한? 아두이노 코드에 대해 쓰려고 한다. 


서술하기에 앞서, 아두이노라는 녀석이 어떻게 동작하는지 알 필요가 있다. 이미 C언어를 어느정도 아는 사람이라면 척보면 다 알지만, 코딩을 처음 해보는 사람들에게는 매우 생소할 것이므로!


처음 C를 배우는 사람도 할 수 있게끔 설명하고자 한다.



1. 아두이노 코딩을 위해 반드시 알아야할 팩트들


아두이노 IDE를 먼저 받아야하는데, 그건 지난 포스팅에 있으므로!

오늘은 바로 코딩을 보도록 하겠다.


아두이노 IDE를 실행시키면 다음과 같은 문구들이 뜬다.



void setup() {

  // put your setup code here, to run once:


}


void loop() {

  // put your main code here, to run repeatedly:


}



void가 뭔지, setup은 뭔지, loop는 뭔지 ()는 뭐고 {}는 뭔지. 아마 처음하는 사람이라면 아무것도 모를 것이다. 그런데 모든 걸 다 알아야 코딩을 할 수 있는게 아니다. 코딩이 뭔가? 알고리즘아닌가? 결국 아두이노 드론을 만드는데 있어서 가장 중요한건 알고리즘이다!

C언어(컴퓨터 프로그래밍 언어)라는 건 결국 우리가 생각한 알고리즘을 언어적으로 풀어주는 데 필요할 뿐, 알고리즘 자체가 될 수는 없기 때문이다.


고로 알고리즘을 짜고, 그 알고리즘을 코딩으로 적용할 줄만 알면 된다. 물론 엄밀히 말하면 프로그래밍 언어에 대한 지식이 있어야 응답속도와 반응성 등과 같은 정밀한 곳에서 더 이점을 가질 수 있겠지만.. 흔히 만드는 아두이노 드론 정도는 딱히 몰라도 가능하다. 고로 시작해보자.




아두이노는 크게 2부분으로 나뉘어져 있다.

void setup, void loop 가 바로 그것이다.


먼저 셋업 부분은 뭘까?


void setup() {

  // put your setup code here, to run once: 


}


친절하게 아두이노에서 설명해주고 있다. PUT YOUR SETUP CODE HERE, TO RUN ONCE:

즉, 딱 한번만 실행될 코드를 setup 부분에 채워넣으라는 것이다. 



void loop() {

  // put your main code here, to run repeatedly:


}


다음은 루프 부분. PUT YOUR MAIN CODE HERE, TO RUN REPEATEDLY:

즉, 루프 안에 들어가 있는 코드는 리피트!! 반복된다. 아두이노에 업로드하는 순간부터 영원히 반복된다. loop 안에 있는 건 그냥 영원히~~~ 위에서부터 아래로 코드가 실행되게 되는 것이다. setup은 위에서부터 아래로 한차례 실행되고 마는 부분이고, loop는 영원히 계속 반복되면서 실행되게 된다.




따라서 PID를 코딩으로 한다면, 어떤 부분을 setup에 넣고 어떤 부분을 loop에 넣을지를 생각해야한다.


PID는 이전 포스팅에서 말했지만, P, I, D 제어를 각각 한 후 그 제어값을 모두 더하여 적용하게 된다.

즉, P제어 + I제어 + D제어 = PID 제어값 이 되는 것이다.


그리고 PID는 오차와 관련된 개념이라고 했다. loop가 어느정도 주기를 갖고 계속 반복되면서 실행될텐데, 매번 실행될때마다 오차가 달라질 것이다. 따라서 loop안에는 매번 달라지는 오차를 가지고 P, I, D 제어값을 새롭게 갱신해주어야 할 필요가 있다.



프로그래밍언어에서는 변수라는 게 있다. 말그대로 변하는 수다. P, I, D 게인값들은 변하지 않는 상수로 지정되어야 하고 오차가 변수로 지정되어야 할 것이다. (꼭 그런건 아니지만 일반적으로!)

그런데 아두이노는 P, I, D 라는 개념이 없다. 아무것도 모른다. 고로 그게 뭔지, 미리 선언해줄 필요가 있다.


예를 들어, Kp, Ki, Kd를 각각 PID의 게인값이라고 생각해보자. 그리고 난 그 값에 각각 1.2, 2.5, 3.2을 지정해주고 싶다.



그렇다면 아래와 같이 선언해주면 된다.



double Kp = 1.2;

double Ki = 2.5;

double Kd = 3.2;


void setup() {

  // put your setup code here, to run once:


}


void loop() {

  // put your main code here, to run repeatedly:


}


자, 우측은 이해하기가 쉽다. Kp = 1.2 이런 부분은 직관적으로 와닿을 것이다. 그런데 좌측에 double은 뭔가?

아두이노는 우리가 선언하려는 숫자가 정수인지, 실수인지도 모른다. 그걸 우리가 말해주지 않으면 얘는 해석을 못한다. 1.2는 정수가 아니라 실수 영역에 있기 때문에 실수 변수를 얘기하는 double로 선언해주어야 한다. 정수 변수는 int 로 바꿔주면 된다.


자 이제 게인값들을 선언했으니, 오차도 선언해주도록 하자. 오차는 error 라고 하겠다. 오차는 정수보다는 실수인게 더 섬세하고 좋을 것 같으니 실수로 선언!



double Kp = 1.2;

double Ki = 2.5;

double Kd = 3.2;

double error;


void setup() {

  // put your setup code here, to run once:


}


void loop() {

  // put your main code here, to run repeatedly:


}


이 경우는 error에 특정 값을 넣지 않았다. 왜? 이건 변수니까 말이다. Kp, i, d는 상수로 고정시킬 것이기 때문에 저렇게 미리 선언해놓은 거고, error는 루프가 계속 반복실행됨에 따라 변하는 수이기 때문에 굳이 넣지 않았다. 저렇게 아무것도 넣지 않으면 초기값이 0으로 자동배정되는 걸로 알고 있다.


이제, 오차에 필요한 현재값과 목표값을 선언해보도록 하자.

목표값은 이전 포스팅에서 예를 든 것처럼, 10도라고 하고 desired_angle로 선언하겠다. 그리고 현재값은 mpu6050을 통해 추출한 각도값으로, 실제 코드는 있다고 가정하고 그 값은 current_angle 이라고 하겠다.



double Kp = 1.2;

double Ki = 2.5;

double Kd = 3.2;

double error;


double desired_angle = 10;

double current_angle;


void setup() {

  // put your setup code here, to run once:


}


void loop() {

  // put your main code here, to run repeatedly:


}



자, 이게 error을 정의할 필요가 있다. error는 오차이다. 즉, 목표값에서 현재값을 뺀 수치가 될 것이다. 그리고 이 오차는 매 순간순간 달라질 것이다. 드론이 첨엔 10도 오차가 있다가, 그 쪽으로 회전을 시작하면 9도 8도 ... 1도 0도까지 점점 오차가 줄어들게 아닌가. 고로 매 순간순간 갱신해줘야하는 변수가 되는 것이다. 그렇다면? 당연히 loop안에서 선언되어야 한다.


double Kp = 1.2;

double Ki = 2.5;

double Kd = 3.2;

double error;


double desired_angle = 10;

double current_angle;


void setup() {

  // put your setup code here, to run once:


}


void loop() {

  // put your main code here, to run repeatedly:


  current_angle갱신 코드;

  error = desired_angle - current_angle;


}


처음에 넣은 current_angle갱신코드란? 이건 현재 각도값을 의미하는데, 현재 드론이 기울어진 각도값도 mpu6050의 연산에 의해 실시간으로 변화되어야만 한다. 따라서 loop안에 넣어준거고, 이 부분은 mpu6050에 관련된 코딩이 필요하기 때문에 생략하고 위와 같이 표시하였다.


자, 이제 오차인 error가 아두이노에서 선언되어 해석가능해졌으므로, 본격적으로 PID 제어를 시작해보자.



double Kp = 1.2;

double Ki = 2.5;

double Kd = 3.2;

double error;

double error_previous;


double desired_angle = 10;

double current_angle;


double P_control, I_control, D_control;

double Time = 0.004;



void setup() {

  // put your setup code here, to run once:


}


void loop() {

  // put your main code here, to run repeatedly:


  current_angle갱신 코드;

  error = desired_angle - current_angle;


  P_control = Kp * error;

  I_control = I_control + Ki * error * Time;

  D_control = Kd * (error - error_previous) / Time;


  error_previous = error;


}



이번엔 추가된 게 좀 많다. 먼저 loop안을 보면, P컨트롤, I컨트롤, D컨트롤, 그리고 error_previous, Time 등과 같은 새로운 변수들이 선언된 것을 알 수 있다.

고로 맨 윗부분에 선언해준 것이다. 특히 Time은 한 루프가 도는 데 걸리는 시간을 의미한다. 내가 만든 드론은 한 루프 도는데 걸리는 시간이 약 3~4ms기 때문에 0.004로 해준건데, 사실 꼭 이렇게 할 필요는 없다. 그냥 4로 해도되고 4000으로 해도되고. Time이 수식적으로 가지는 의미는 그냥 '상수'일 뿐이다. 따라서 아무렇게나 지정해줘도, 또 다른 상수인 Kd나 Ki로 커버할만한 수준이기만 하면 되는것이다.

쉽게 생각해서, 4 * 6이나, 3 * 8이나 똑같은 수 아닌가. 곱해서 똑같이 나오게 숫자를 조정할 수 있기 때문에 꼭 time을 주기로 맞춰줄 필요는 없다는 것이다.


그리고 가장 아래보면 error_previous = error라고 선언한 부분이 있다. 아두이노에서는 이럴 때 우측에 있는 녀석을 좌측값으로 저장하라고 해석한다.

즉, error_previous라는건 지금 루프가 아니라 바로 이 전에 루프가 실행될 때의 error값을 의미한다.


만약 처음 error가 3이라고 해보자. 그러면 loop는 위에서부터 아래로 실행될 것이므로 error_previous에도 3이 저장 될 것이다.

그리고 loop가 다시 돌았을 때, 갱신된 현재각도에 의해 오차인 error가 5가 되었다고 해보자. 그리고 D_control을 보면, Kd * (5 - 3) / 0.004 가 될 것이다.

즉 컨트롤 부분에서 계산할 때 바로 직전의 에러 값이 previous에 저장되는 것이다.


I_control 부분은, 기존 I_control 값에다가 추가로 I 제어를 통해 얻은 값을 더하라는 것이다. 이전 포스팅에서 I 제어의 역할이 오차를 누적시켜 최종적으로 오차를 0으로 보내는 데 있다고 했는데, 이와 같은 방법으로 '누적' 시킨다.

다만 언어적으로 좀 더 편하게 쓸 수 있다.


I_control += Ki * error * Time 


이렇게 +=라고 하면, 위와 똑같은 의미를 가진다. 그냥 언어적인 것이니까 외우고 사용하면 된다.



이제 다음으로 넘어가보자.



double Kp = 1.2;

double Ki = 2.5;

double Kd = 3.2;

double error;

double error_previous;


double desired_angle = 10;

double current_angle;


double P_control, I_control, D_control;

double Time = 0.004;

double PID_control;


void setup() {

  // put your setup code here, to run once:


}


void loop() {

  // put your main code here, to run repeatedly:


  current_angle갱신 코드;

  error = desired_angle - current_angle;


  P_control = Kp * error;

  I_control += Ki * error * Time;

  D_control = Kd * (error - error_previous) / Time;


  PID_control = P_control + I_control + D_control;

  PID_control = constrain(PID_control, 0, 255);

  analogWrite(6, PID_control);


  error_previous = error;


}


PID_control이라는 변수를 또 만들었다. 말했듯이, PID제어값은 P, I, D 제어를 통해 나온 값들을 모두 더한 값이므로 그냥 더해준 것이다.

그 뒤에 나오는 constrain은, 아두이노에서 제공하는 함수이다.

저걸 쉽게 얘기하면, PID_control이라는 값이 0부터 255까지의 숫자 범위를 가지도록 제한하는 것이다.

예를들어 PID_control이 -100이라는 값이 되면, 0으로 저장이 되고, 1000이라는 값이 되면 255로 저장이 된다. 0과 255사이의 값은 그 값 그대로 저장된다.

이렇게 해주는 이유는, 그 뒤에 나오는 anaglogWrite이라는 함수가 0부터 255사이의 숫자만 받아들이기 때문이다.



analogWrite은 전압을 인가하는 함수로, 6번 핀에 0~255 사이에 해당하는 전압을 주라는 명령신호다. 0은 0V, 255는 5V를 주게 되는데

사실 실제 드론을 만들 때는 이 함수를 쓰지 않을 것이다. servo 라이브러리를 활용하여 다른 함수를 쓸 예정인데

이 포스팅의 목적은 아무튼 PID 제어 코드의 기본을 설명하는 것이기 때문에 그냥 그대로 가는걸로..


최종적으로 나온 PID_control 이라는 값이 가장 중요하다. 이게 PID 제어를 통해 구한 제어값이 되기 때문이다.



다음으로, 조금 보기좋게 코드를 정리하기 위해 다음과 같이 해보았다.


double Kp = 1.2;

double Ki = 2.5;

double Kd = 3.2;

double error;

double error_previous;


double desired_angle = 10;

double current_angle;


double P_control, I_control, D_control;

double Time = 0.004;

double PID_control;


void setup() {

  // put your setup code here, to run once:


}


void loop() {

  // put your main code here, to run repeatedly:

  pidcontrol();


}


void pidcontrol() {


  current_angle갱신 코드;

  error = desired_angle - current_angle;


  P_control = Kp * error;

  I_control += Ki * error * Time;

  D_control = Kd * (error - error_previous) / Time;


  PID_control = P_control + I_control + D_control;

  PID_control = constrain(PID_control, 0, 255);

  analogWrite(6, PID_control);


  error_previous = error;



}


뭐가 달라졌나?

바로 void pidcontrol 이라는걸 만들었다는 데 있다. 이건 쉽게 말해, 그냥 그룹으로 묶어놓는거라고 보면 된다.

loop가 가장 중요한 부분인데, 위위처럼 loop안에 모든 코드를 작성하게 되면 보기가 매우 불편하다. 때문에 항목별로 묶어서 따로 이렇게 정리해놓는 것이다. loop안에는 pidcontrol() 이라고만 선언해주면, pidcontrol이라고 선언된 그룹이 실행되게 된다.

고로 위랑 위위는 똑같은 코드이다! 정리해서 보기가 더 수월할 뿐!




이게 전부다!

PID 제어는 이렇게 하면 된다. 시작할때 오차는 10도였겠지만 시간이 흐르면 0으로 점점 줄어들어 원하는 목표값에 도달할 것이다.

단, Kp, Ki, Kd라는 게인값을 잘 맞추었다면 말이다. 이전 포스팅에서 움짤을 하나 사용했었는데, 그것처럼 반응을 보일 것이다. 제대로 게인값을 맞추지 않으면 오차가 오히려 더 커질 수도 있다. 드론이라는 시스템에 맞춰 적당한 값을 맞춰주어야 하는데 이 부분은 실험을 통해서 검증할 수 있다.



아무튼 제어 코드는 이와 같다. 실제 드론을 날릴때는 이중 pid라고 하여, pid 제어를 겹으로 싸서 이중으로 만든 걸 쓸텐데 별로 어려울 건 없다. 똑같다! 그냥 겹으로 한번 싸주기면 하면 된다. 

최종 코드는 계속 포스팅하면서 써가는 걸로.




*** 19.12 수정사항

안녕하세요.

많은 분들이 아두이노 코드를 요청하셔서 제작했던 코드를 아래 포스팅에 첨부하였습니다.

https://hyongdoc.tistory.com/270



댓글