daily

PWM 신호로 Fan 제어해 압력값 세팅하기

Juhyuck 2023. 5. 15. 20:05
728x90

보통 PID제어를 설명하는 글에서 설명할 때 아래와 같은 그림을 많이 사용한다. 

출처: https://en.wikipedia.org/wiki/PID_controller#/media/File:PID_en.svg

 

지금 개발 중인 PWM 신호로 Fan을 제어하고 원하는 pressure값에 도달하는 환경을 적용하려면 위 그림에서 Plant, Process에서, PWM 신호값으로 처리해주면 된다. 도식화 해보면,

 

만들고자하는 제어 시스템

위와 같이, PWM duty cycle을 제어하는 값으로 하고, Fan에 입력하고, 측정되는 압력값을 세팅한 압력값과의 차이를 바탕으로 PID 제어하는 로직을 구현했다.

 

import time
from time import sleep
import math
import random
import matplotlib.pyplot as plt

current_value = 10.0


def get_current_value():
    global current_value
    return current_value


def apply_control_signal(PWM, control_signal):
    global current_value
    pwm_max = 100
    pwm_min = 0
    if control_signal < 0:
        sign = -1
    else:
        sign = 1
    # + random.uniform(-0.5, 0.5)
    current_value += sign * math.sqrt(abs(control_signal))
    # current_value += sign * abs(control_signal) # + random.uniform(-.4, .4)
    res = PWM + control_signal
    if res > 100:
        res = pwm_max
    elif res < 0:
        res = pwm_min

    return res


class PIDController:
    def __init__(self, Kp, Ki, Kd):
        self.Kp = Kp
        self.Ki = Ki
        self.Kd = Kd
        self.last_error = 0
        self.integral = 0
        self.max_control = 10
        self.min_control = -10
        self._last_time = None

    def compute_control_signal(self, setpoint, measured_value):
        # Define error
        error = setpoint - measured_value

        # Calculate time difference
        current_time = time.time()
        if self._last_time is None:
            time_diff = 0
        else:
            time_diff = current_time - self._last_time
        self._last_time = current_time

        # Proportional term
        P = self.Kp * error

        # Integral term
        self.integral += error * time_diff
        I = self.Ki * self.integral

        # Derivative term
        derivative = (error - self.last_error) / \
            time_diff if time_diff != 0 else 0
        D = self.Kd * derivative

        # Compute control signal
        control_signal = P + I + D
        if control_signal > self.max_control:
            control_signal = self.max_control
        if control_signal < self.min_control:
            control_signal = self.min_control
        # print(f"{control_signal: 03.2f} = P:{P: 03.2f} + I:{I: 03.2f} + D:{D: 03.2f}")

        # Update last error
        self.last_error = error

        return control_signal


target_pressure = 70

# PID Controller
pid = PIDController(Kp=0.01, Ki=1, Kd=0.0001)

# control target
current = get_current_value()
PWM = 0
error_threshold = 0.5
converged_time = 0
convergence_duration = 2

# Initialize lists to store data for plotting
time_values = []
control_values = []
current_values = []
error_values = []

# PID control excuting
counter = 0
while True:
    # Record the starting time
    start_time = time.time()

    control = pid.compute_control_signal(
        target_pressure, current)  # Calculate pid control value
    PWM = apply_control_signal(PWM, control)  # applying control value
    current = get_current_value()  # Measure current value
    error = target_pressure - current

    time_step = random.uniform(0, 0.01)
    sleep(time_step)

    # Calculate the time spend whitin a step
    time_diff = time.time() - start_time
    print(f"[time+ {time_diff: 2.1f}, step: {counter: 4.0f}] \
    PWM: {PWM: 03.2f}, current: {current: 03.2f}, error: {error: 03.2f}, \
    converged: {converged_time: 01.1f}")
    counter += 1

    # Store data for plotting
    time_values.append(counter)
    control_values.append(PWM)
    current_values.append(current)
    error_values.append(error)

    if abs(error) < error_threshold:
        converged_time += time_diff
    else:
        converged_time = 0

    if converged_time >= convergence_duration:
        break

try:
    # Plot the control signal, current value, and error
    plt.figure(figsize=(10, 6))
    plt.subplot(311)
    plt.plot(time_values, control_values)
    plt.xlabel('Time')
    plt.ylabel('Control Signal')

    plt.subplot(312)
    plt.plot(time_values, current_values)
    plt.xlabel('Time')
    plt.ylabel('Current Value')

    plt.subplot(313)
    plt.plot(time_values, error_values)
    plt.xlabel('Time')
    plt.ylabel('Error')

    plt.tight_layout()
    plt.show()

except KeyboardInterrupt:
    plt.close()

 

Pressure 측정과 Duty 설정하는 것은 실제 실험할 때 시리얼 통신으로 구현하겠지만, 테스트를 위해 단순하게 구현했다. simple PID 라이브러리가 있으나, 간단한 알고리즘이고, 압력값 변화에 따라서 특별히 게인값을 변화할 일은 없을 것으로 봤다. 

 

혹시, 실험환경(측정환경)의 변화에 따라 수렴하지 않거나 제어가 안되는 경우에는 pid값을 자동 조정하는 기능을 추가해봐야겠다.

'daily' 카테고리의 다른 글

Dockerization [1] - AWS EB에 docker 배포하기  (1) 2023.05.15
Dockerization [0] - docker build 하기  (0) 2023.05.15
23, 19주차  (0) 2023.05.11
3-Tier, 3-Layered Architecture  (0) 2023.05.09
23.05.06. Refresh token, Access token  (0) 2023.05.06