예열

이번 단계는 예열에 대해서 좀 더 자세하게 알아보도록 하겠습니다. 이전의 코드에서 예열을 하는 핵심 코드는 아래 코드입니다.
1digitalWrite(9, HIGH);
노즐 열선과 연결된 디지털 9번핀을 HIGH 로 설정하게 되면, 모스펫을 통해 전류가 공급되어, 온도가 올라갑니다.
그리고 목표온도에 도달하면, LOW 로 변경하여 전류를 차단합니다. 이런 방식에는 문제가 하나 있습니다.

../../_images/Step4_11.jpg

OnOff 방식은 목표 온도에 도달해야만 Off가 됨으로 목표 온도에 도달이 어렵고, 출렁이는 것이 반복 됩니다.
보통 온도에 근접하려면, 목표온도에 도달하기 전에 Off가 되어야 근접하게 도달합니다.
이런 문제를 해결하는 제어가 있습니다.

PID 제어라고 보통은 목표치에 도달을 부드럽게 해주는 제어방식입니다.
P는 비례, I는 적분, D는 미분을 뜻하며, 아래의 그림과 같이 부드럽게 올라갈 수 있도록 해줍니다.

../../_images/Step4_21.jpg

PID 제어는 주변에서 많이 사용됩니다. 에어컨, 히터의 온도유지, 전기레인지의 온도 유지, 가습기의 습도 유지, 드론의 균형유지등이 있습니다.
아래는 PID 제어의 수식입니다.

../../_images/Step4_31.jpg

적분이 있고 미분도 있고, 식 자체가 어렵게 느껴질 수 있습니다. 하지만 이걸 이해하는것은 지금 3D 펜을 만드는덴 필요하지 않습니다. 우리는 어떻게 사용하는지만 알고 있으면 됩니다.
사용 법은 Kp, Ki, Kd 값을 조절하는 것입니다. 자세한 설명보다 그림을 보도록 하겠습니다.

../../_images/Step4_4.gif
※ 위 영상이미지의 출처는 https://ko.wikipedia.org/wiki/PID_%EC%A0%9C%EC%96%B4%EA%B8%B0 입니다.

위 이미지 처럼 Kp와 Ki, Kd 값이 변경됨에 따라 목표 온도에 도달하고, 도달 후의 그래프가 달라집니다.
이렇게 동작하는 부분을 코드로 작성해보려 합니다.
미분과 적분에 대해 배운적이 없거나 이해가 어려우신 분들은 아래 단계는 훑어보고 넘어가셔도 됩니다.
1float Kp=36, Ki=2.5, Kd=0.625, dT = 0.05;
2int16_t error, previousError = 0; // 오차 변수
3float integral, derivaitve = 0;   // 적분 미분 변수
4
5void getPIDoutput(int targetTemp, int actualValue)
6{
7    float outputValue;
8}
먼저 getPIDoutput 이라는 함수를 만들어 줍니다. 함수의 매개변수 는 targetTemp(목표온도), curTemp(현재온도) 로 되어 있습니다.
다양한 변수도 생성했습니다. Error는 오류라는 뜻이 아니고 여기에서는 오차라는 뜻으로 사용됩니다. 목표온도와 현재온도의 차이라고 보시면 됩니다.

integral은 적분 값을 저장하는 변수, derivaitve는 미분 값을 저장하는 변수입니다.
PID 값은 각각 15,1,1 로 설정한 상태이며, dT는 적분, 미분을 위한 변수입니다.
 1float Kp=36, Ki=2.5, Kd=0.625, dT = 0.05;
 2int16_t error, previousError = 0; // 오차 변수
 3float integral, derivaitve = 0;   // 적분 미분 변수
 4
 5void getPIDoutput(int targetTemp, int actualValue)
 6{
 7    float outputValue;
 8
 9    // 에러 값 저장
10    error = targetTemp - actualValue;
11
12    // 적분 값 저장
13    integral = sumError + (float)error*dT;
14}
적분 값은 측정의 한계로 인해 근사적으로 계산됩니다.

../../_images/Step4_51.jpg

위와 같이 목표온도-설정온도의 차이를 dT 만큼 곱하면, 그림 상의 파란색 사각형 하나의 면적으로 계산되어서 옵니다.
그렇다면 dT가 왜 0.05초 인가? 라고 궁금할 수 있을 겁니다. 이후 최종 코드에서 함수를 0.05초마다 한 번씩 실행되도록 코드를 작성할 것이기 때문에 미리 0.05로 설정하였습니다.
 1float Kp=36, Ki=2.5, Kd=0.625, dT = 0.05;
 2int16_t error, previousError = 0; // 오차 변수
 3float integral, derivaitve = 0;   // 적분 미분 변수
 4
 5void getPIDoutput(int targetTemp, int actualValue)
 6{
 7    float outputValue;
 8
 9    // 에러 값 저장
10    error      = targetTemp - actualValue;
11
12    // 적분 값 저장
13    integral   = sumError + (float)error*dT;
14
15    // 미분 값 저장
16    derivaitve = (error - previousError)/dT;
17    previousError = error;
18}
미분 값은 이전 오차와 현재 오차간의 차이를 기울기 형태로 나타냅니다. 기울기를 측정하면 이전 오차 변수(previousError)에 현재 오차 값을 저장합니다.

../../_images/Step4_61.jpg

현재 오차(설정온도와 현재온도간의 차이)와 이전 오차간의 차이를 알면, 미분값을 근사적으로 계산할 수 있습니다.
 1float Kp=36, Ki=2.5, Kd=0.625, dT = 0.05;
 2int16_t error, previousError = 0; // 오차 변수
 3float integral, derivaitve = 0;   // 적분 미분 변수
 4
 5void getPIDoutput(int targetTemp, int actualValue)
 6{
 7    float outputValue;
 8
 9    // 에러 값 저장
10    error      = targetTemp - actualValue;
11
12    // 적분 값 저장
13    integral   = integral + (float)error*dT;
14
15    // 미분 값 저장
16    derivaitve = ((float)error - (float)previousError)/dT;
17    previousError = error;
18
19    // PID 계산
20    outputValue = Kp*error + Ki*integral + Kp*derivaitve;
21
22    // output 값이 0~255 범위를 벗어나면 최대, 최소 값을 대신 저장
23    if(outputValue > 255)
24    {
25        outputValue = 255;
26    }
27    else if(outputValue <0)
28    {
29        outputValue = 0;
30    }
31
32    // 계산된 결과값을 디지털 9번핀에 analogWrite 로 입력
33    analogWrite(9,outputValue);
34}

최종적으로 계산되어 반환되는 outputValue 를 노즐 열선과 연결된 디지털 9번핀(D9)에 analogWrite 함수로 입력해주면 됩니다.
디지털 핀에 analogWrite 함수가 사용가능한 이유는 PWM 핀이기 때문입니다.
  1int tempValueA0 = 0; // A0 신호 값 저장용
  2int curTemp = 0;
  3int targetTemp = 0;
  4int presetIndex = 0;
  5bool isHeating = false; // 예열 상태를 확인하는 확인하는 bool 변수
  6
  7                 // 기본, PCL, PLA
  8int presetTemp[3] = { 0,  60,  200 };
  9
 10int temptable[23][2] =
 11{
 12    {1023,0},
 13    {1022,10},
 14    {1020,20},
 15    {1016,30},
 16    {1011,40},
 17    {1009,50},
 18    {1006,60},
 19    {1004,70},
 20    {1000,80},
 21    {990,90},
 22    {983,100},
 23    {976,110},
 24    {972,120},
 25    {964,130},
 26    {955,140},
 27    {942,150},
 28    {929,160},
 29    {910,170},
 30    {895,180},
 31    {864,190},
 32    {839,200},
 33    {800,210},
 34    {744,220}
 35}; // 온도테이블
 36
 37void setup() {
 38    // put your setup code here, to run once:
 39    Serial.begin(9600);
 40
 41    pinMode(11,INPUT_PULLUP);
 42    pinMode(12,INPUT_PULLUP);
 43
 44    pinMode(9,OUTPUT);
 45}
 46
 47float Kp=36, Ki=2.5, Kd=0.625, dT = 0.05;
 48int16_t error, previousError = 0; // 오차 변수
 49float integral, derivaitve = 0;   // 적분 미분 변수
 50
 51// pid 계산을 하고, 결과값을 HEATER_EN 핀에 적용
 52void getPIDoutput(int targetTemp, int actualValue)
 53{
 54    float outputValue;
 55
 56    //설정온도에 도달하기 10도 전일때부터 PID 제어 시작
 57    if(curTemp < setTemperature - 10)
 58    {
 59      outputValue = 255;
 60    }
 61    else
 62    {
 63        // 에러 값 저장
 64        error      = targetTemp - actualValue;
 65
 66        // 적분 값 저장
 67        integral   = integral + (float)error*dT;
 68
 69        // 미분 값 저장
 70        derivaitve = ((float)error - (float)previousError)/dT;
 71        previousError = error;
 72
 73        // PID 계산
 74        outputValue = Kp*error + Ki*integral + Kp*derivaitve;
 75
 76        // output 값이 0~255 범위를 벗어나면 최대, 최소 값을 대신 저장
 77        if(outputValue > 255)
 78        {
 79            outputValue = 255;
 80        }
 81        else if(outputValue <0)
 82        {
 83            outputValue = 0;
 84        }
 85
 86        // outputValue 가 0 혹은 OFF 라면 예열이 되지 않음으로, isHeating 변수를 false로 저장
 87        if(outputValue == 0 || material_index == 0)
 88        {
 89            isHeating = false;
 90        }
 91        else
 92        {
 93            isHeating = true;
 94        }
 95    }
 96    // 계산된 결과값을 디지털 9번핀에 analogWrite 로 입력
 97    analogWrite(9,outputValue);
 98}
 99
100// 신호 값을 보정하여 온도 값을 추측해내는 계산 함수
101int tempCali(int valueA0)
102{
103    float ratioTemp;
104
105    for(int i = 0; i<23; i++)
106    {
107        if(temptable[i][0] <= valueA0)
108        {
109            ratioTemp = ((float)valueA0 - temptable[i][0])/(temptable[i-1][0] - temptable[i][0]);
110
111            return temptable[i][1] - ratioTemp*(temptable[i][1] - temptable[i-1][1]);
112        }
113
114    }
115}
116
117void loop() {
118    // put your main code here, to run repeatedly:
119    curTemp = tempCali(analogRead(A0)); // 온도 보상 함수 호출
120
121    Serial.print("신호 값 : ");
122    Serial.print(analogRead(A0));
123    Serial.print(", 현재 온도 값 : ");
124    Serial.print(curTemp);
125    Serial.print(", 설정 온도 값 : ");
126    Serial.println(targetTemp);
127
128    getPIDoutput(targetTemp, curTemp);
129
130    if(digitalRead(11)==LOW)
131    {
132        presetIndex++;
133        targetTemp = presetTemp[presetIndex];
134
135        if(presetIndex>2)
136        {
137          presetIndex = 0;
138        }
139
140        // 적분값 리셋
141        integral = 0;
142        delay(100);
143    }
144    else if(digitalRead(12)==LOW)
145    {
146        presetIndex--;
147        targetTemp = presetTemp[presetIndex];
148
149        if(presetIndex<0)
150        {
151          presetIndex = 2;
152        }
153
154        // 적분값 리셋
155        integral = 0;
156        delay(100);
157    }
158
159}
160
161| getPIDoutput 함수에서 isHeating  변수를 변경시켜주는 코드를 추가하였습니다. 신호 값을 읽는 부분에서 현재 예열중인지 아닌지를 아는 것이 중요하기 떄문입니다.