학습 목표

  • Open-loop와 Closed-loop Control의 차이와 적용 조건을 설명할 수 있다.
  • Setpoint, Measurement, Error와 Control Output의 관계를 설명할 수 있다.
  • P·I·D 항이 응답 속도, 정상상태 오차, Overshoot와 Noise에 미치는 영향을 설명할 수 있다.
  • Saturation, Integral Windup과 Derivative Kick의 원인과 대책을 구현할 수 있다.
  • 안전한 순서로 PID를 조정하고 Step Response를 평가할 수 있다.
  • Feedforward와 Cascaded Control이 필요한 이유를 설명할 수 있다.

1. 명령과 실제 결과는 다를 수 있다

Motor에 같은 PWM을 보내도 Robot의 속도는 항상 같지 않습니다. 하중, 경사, 마찰, 배터리 전압과 온도가 달라지기 때문입니다. 제어는 원하는 상태와 실제 상태의 차이를 줄이는 과정입니다.

Open-loop Control은 결과를 측정하지 않고 미리 정한 명령을 보냅니다. 구조가 단순하고 Sensor가 필요 없지만 조건 변화와 외란을 스스로 보정할 수 없습니다.

Closed-loop Control은 실제 결과를 측정하여 목표와 비교하고 다음 명령을 수정합니다.

01목표값
02오차 계산
03Controller
04Actuator와 Plant
05측정값
06다시 오차 계산

모든 장치가 반드시 Closed-loop인 것은 아닙니다. 결과 정밀도가 중요하지 않고 조건이 충분히 일정한 장치는 Open-loop로도 동작할 수 있습니다. 반면 속도, 위치와 힘을 정확히 제어해야 하는 Robot 축에는 일반적으로 Feedback이 필요합니다.


2. 피드백 제어의 네 가지 값

이름 기호 의미 속도 제어 예
Setpoint r 또는 SP 원하는 목표값 0.5 m/s
Measurement y 또는 PV Sensor로 측정한 실제값 0.42 m/s
Error e 목표와 측정의 차이 e = r - y = 0.08 m/s
Control Output u Actuator에 전달할 명령 Motor 전압·전류 명령

이 강의에서는 음의 Feedback을 가정하고 다음처럼 오차를 정의합니다.

e(t) = r(t) - y(t)

부호는 중요합니다. 측정값이 목표보다 작아 양의 오차가 생겼을 때 출력이 측정값을 증가시키는 방향이어야 합니다. Motor 배선, Encoder 방향 또는 좌표계 부호가 잘못되면 음의 Feedback이 양의 Feedback으로 바뀌어 빠르게 발산할 수 있습니다.


3. Plant와 Disturbance

Plant는 Controller가 제어하려는 실제 대상입니다. Wheel Motor, Robot Joint, Heater와 Drone 자세가 모두 Plant가 될 수 있습니다. Disturbance는 제어 명령 외에 Plant에 영향을 주는 요소입니다.

  • 이동 Robot의 경사와 바닥 마찰
  • Robot Arm의 중력과 Payload 변화
  • Drone에 부는 바람
  • 전원 전압 변화

Controller는 Plant의 응답, Sensor Noise, 계산·통신 지연과 Actuator 한계 안에서 동작합니다. 같은 PID Gain을 서로 다른 Robot에 그대로 복사하면 안 되는 이유입니다.


4. P 제어 — 현재 오차에 반응한다

Proportional 항은 현재 오차에 비례한 출력을 만듭니다.

u_P(t) = Kp e(t)

Kp를 높이면 일반적으로 목표에 더 빨리 접근하고 Disturbance에 더 강하게 반응합니다. 너무 높으면 Overshoot, 진동과 불안정이 생길 수 있으며 Sensor Noise도 출력에 더 크게 나타납니다.

P 제어만으로 정상상태 오차가 남는지는 Plant의 구조에 따라 달라집니다. 예를 들어 일정한 경사나 마찰을 이기기 위해 0이 아닌 출력이 계속 필요한 속도 제어에서 e=0이면 P 출력도 0이므로 일정한 오차가 남을 수 있습니다. 그러나 모든 System에서 P 제어가 반드시 정상상태 오차를 만든다고 일반화할 수는 없습니다.


5. I 제어 — 누적된 오차를 없앤다

Integral 항은 오차를 시간에 따라 누적합니다.

u_I(t) = Ki ∫e(t)dt

Discrete Control에서는 다음처럼 계산합니다.

I_k = I_(k-1) + e_k Δt
u_I = Ki I_k

작은 오차가 오래 남으면 적분값이 커져 일정한 Disturbance를 상쇄합니다. 따라서 많은 System에서 정상상태 오차를 줄이거나 제거할 수 있습니다.

하지만 Ki가 지나치게 크면 Overshoot가 커지고 진동이 오래 지속됩니다. 적분은 과거 오차를 기억하므로 목표 변경이나 운전 Mode 전환 때 적절한 초기화 또는 Bumpless Transfer가 필요할 수 있습니다.


6. Actuator Saturation과 Integral Windup

Motor 출력에는 전압, 전류와 속도 한계가 있습니다. Controller가 u=1.6을 요구해도 실제 Driver가 1.0까지만 낼 수 있다면 출력이 Saturation된 것입니다.

Saturation 상태에서 오차를 계속 적분하면 실제 출력은 늘지 않는데 내부 적분값만 커집니다. 막힘이 풀리거나 목표가 바뀌어도 큰 적분값이 남아 Overshoot와 긴 회복 시간을 만드는 현상이 Integral Windup입니다.

주요 Anti-windup 방법은 다음과 같습니다.

  • Clamping: 적분 상태 또는 적분 출력에 범위를 둔다.
  • Conditional Integration: Saturation을 더 심하게 만드는 방향의 적분을 중단한다.
  • Back-calculation: 제한 전 출력과 실제 제한 출력의 차이로 적분 상태를 되돌린다.

오차 부호가 바뀔 때 적분값을 무조건 0으로 만드는 방법은 일부 단순 System에서는 쓸 수 있지만 연속성과 Disturbance 보상값을 잃을 수 있으므로 보편적인 규칙은 아닙니다.


7. D 제어 — 변화 속도를 보고 제동한다

Derivative 항은 오차 변화율에 반응합니다.

u_D(t) = Kd de(t)/dt

목표에 빠르게 접근할 때 미리 출력을 줄이는 제동 효과를 만들어 Overshoot와 진동을 줄일 수 있습니다. 하지만 미분은 빠른 Sensor Noise를 크게 증폭합니다.

Discrete 미분은 다음과 같이 Δt로 나누므로 측정 주기가 짧거나 불규칙할수록 Noise와 시간 오차에 민감합니다.

d_k = (e_k - e_(k-1))/Δt

그래서 실제 구현에는 Filter가 필요하며, Noise가 크거나 Plant가 충분히 감쇠되어 있다면 PI Control이 더 적합할 수 있습니다.


8. Derivative Kick과 Measurement 미분

Error를 미분하면 Setpoint가 계단처럼 바뀌는 순간 Error도 급변해 큰 D 출력이 발생합니다. 이를 Derivative Kick이라고 합니다.

Setpoint 변화에 D 항이 직접 반응하지 않도록 Measurement를 미분할 수 있습니다. e=r-y이고 Setpoint가 일정할 때 de/dt=-dy/dt이므로 다음처럼 사용합니다.

u_D = -Kd dy/dt

이 방법은 Setpoint Kick을 줄이지만 Measurement Noise 문제는 그대로이므로 Low-pass Filter와 함께 사용합니다.


9. 1차 Low-pass Filter

Raw Derivative d_raw를 그대로 사용하지 않고 다음처럼 부드럽게 만들 수 있습니다.

α = Δt / (τ_f + Δt)
d_filtered ← d_filtered + α(d_raw - d_filtered)

τ_f가 크면 더 부드럽지만 반응이 느려지고, 작으면 빠르지만 Noise가 더 남습니다. Filter는 Noise를 줄이는 대신 지연을 추가하므로 무조건 강하게 적용하면 안정성이 좋아지는 것이 아닙니다.

Sensor 원신호 전체를 과도하게 Filtering하기보다 제어 목적과 주파수 범위를 고려해야 합니다.


10. PID 제어식

Parallel Form의 PID는 다음과 같습니다.

u(t) = Kp e(t) + Ki∫e(t)dt + Kd de(t)/dt

여기에 Feedforward와 출력 제한을 포함하면 실제 구현은 다음 흐름에 가깝습니다.

u_raw = u_FF + u_P + u_I + u_D
u = clamp(u_raw, u_min, u_max)

PID Parameter 표기 방식에는 Parallel, Ideal·Standard 등 여러 형태가 있습니다. 어떤 Library는 Ki, Kd 대신 적분 시간 Ti, 미분 시간 Td를 사용합니다. 숫자만 복사하지 말고 제어식, 단위와 Sample Time 반영 방식을 확인해야 합니다.


11. 제어 주기와 지연

PID Gain만큼 중요한 것이 일정한 Control Period입니다. dt가 흔들리는데 고정값으로 적분과 미분을 계산하면 Controller의 실제 동작도 흔들립니다.

실제 구현에서는 다음을 확인합니다.

  • Monotonic Clock으로 실제 dt 측정
  • dt <= 0, 지나치게 크거나 유한하지 않은 값 거부
  • Sensor Timestamp와 데이터 최신성 확인
  • 계산, 통신과 Actuator 지연 측정
  • Deadline Miss와 Jitter 기록

지연은 Controller가 과거 상태를 보고 행동하게 만듭니다. Kp를 무작정 높이면 지연 때문에 보정 방향이 늦게 바뀌어 진동하거나 불안정해질 수 있습니다.


12. Step Response를 읽는 방법

Setpoint를 한 값에서 다른 값으로 바꾸고 시간에 따른 Measurement를 기록하면 Controller 특성을 볼 수 있습니다.

지표 의미
Rise Time 목표 부근까지 처음 도달하는 데 걸리는 시간
Overshoot 목표를 넘어선 최대 크기
Settling Time 허용 오차 범위 안에 계속 머물기까지의 시간
Steady-state Error 충분한 시간이 지난 뒤 남는 오차
Oscillation 목표 주변에서 반복되는 진동

한 번의 Step Test만 보지 말고 낮은·높은 Setpoint, 양방향 움직임, 실제 하중, 배터리 상태와 예상 Disturbance에서 반복합니다. 위험한 Robot에서는 이동 범위, 속도와 출력 제한을 낮게 설정한 상태에서 시험해야 합니다.


13. 안전한 PID 튜닝 순서

모든 Plant에 통하는 단 하나의 Gain 공식은 없습니다. 다음은 낮은 위험 범위에서 사용할 수 있는 보수적인 수동 절차입니다.

  1. Robot을 고정하거나 안전 공간을 확보하고 출력·속도·가속도 제한을 설정한다.
  2. Ki=0, Kd=0에서 작은 Kp로 시작한다.
  3. Step Response와 Noise를 기록하며 Kp를 조금씩 높인다.
  4. Overshoot와 진동이 증가하기 전에 충분한 여유를 둔 Gain을 선택한다.
  5. 필요하면 Filter를 적용한 Kd를 조금씩 추가해 감쇠를 높인다.
  6. 지속적인 정상상태 오차가 있을 때만 작은 Ki를 추가한다.
  7. Saturation과 Anti-windup 동작을 의도적으로 시험한다.
  8. 실제 하중, 속도 범위, 경사와 배터리 조건에서 다시 검증한다.

“진동이 생길 때까지 Gain을 올린다”는 방식은 위험하거나 손상되기 쉬운 Robot에 그대로 적용하면 안 됩니다. 가능하면 Plant Model, System Identification, 자동 튜닝 도구와 주파수 응답 분석을 함께 사용합니다.


14. 증상으로 점검 방향 찾기

증상 가능한 원인 점검 방향
반응이 지나치게 느림 Kp가 작음, 출력 제한, Feedforward 부족 Saturation을 먼저 확인하고 Kp 검토
일정한 오차가 남음 지속 외란, Ki 부족, Sensor Bias Bias와 Actuator 상태 확인 후 Ki 검토
큰 Overshoot Kp·Ki 과다, 감쇠 부족 Gain을 낮추고 D·Profile 검토
계속 진동 Gain 과다, 지연, 탄성·Backlash 지연 측정, Gain 하향, 기구 점검
고주파 떨림 D의 Noise 증폭 Kd와 Filter, Sensor 신호 점검
막힘 해제 뒤 폭주 Integral Windup Saturation과 Anti-windup 검증
Setpoint 변경 순간 출력 Spike Derivative Kick Measurement 미분 또는 Setpoint Filter

증상 하나에 원인 하나만 있는 것은 아닙니다. 예를 들어 느린 응답은 Kp 부족이 아니라 이미 Current Limit에 걸렸기 때문일 수 있습니다. Log에서 Setpoint, Measurement, P·I·D 각 항, Raw Output, Limited Output과 dt를 함께 기록하면 진단이 쉬워집니다.


15. Feedforward — 예상 가능한 출력을 먼저 준다

Feedback은 Error가 생긴 뒤 반응합니다. 필요한 출력을 Model이나 실험으로 미리 알 수 있다면 Feedforward를 더할 수 있습니다.

u = u_FF + u_PID

예를 들어 Wheel 목표 속도에 비례한 Motor 전압, Robot Arm의 중력 보상, 목표 가속도에 따른 Torque가 Feedforward입니다. Feedback은 Model 오차와 예상하지 못한 Disturbance만 보정하므로 더 작은 Gain으로 빠른 응답을 얻을 수 있습니다.

Feedforward만으로는 Model 오차를 스스로 고칠 수 없으므로 보통 Feedback을 대체하지 않고 함께 사용합니다.


16. Cascaded Control

Robot Motor와 Joint에는 여러 Control Loop를 겹쳐 사용할 수 있습니다.

01Position Loop
02Velocity Setpoint
03Velocity Loop
04Current·Torque Setpoint
05Current Loop
06Motor

안쪽 Current Loop는 바깥 Velocity Loop보다 빠르고, Velocity Loop는 Position Loop보다 빨라야 합니다. 안쪽 Loop가 목표를 충분히 빠르게 따라간다는 가정 아래 바깥 Loop를 조정합니다.

모든 Loop를 한꺼번에 튜닝하기보다 가장 안쪽부터 안정화한 뒤 바깥쪽으로 진행합니다.


17. Mode 전환과 Bumpless Transfer

Manual Control에서 PID Control로 전환하거나 Setpoint Source가 바뀔 때 내부 적분값이 현재 출력과 맞지 않으면 출력이 갑자기 튈 수 있습니다. Bumpless Transfer는 전환 직전 출력과 새 Controller 출력이 자연스럽게 이어지도록 적분 상태나 초기값을 맞추는 방법입니다.

Controller를 Disable할 때 적분값을 무조건 유지할지, 0으로 만들지, 현재 필요한 출력으로 Tracking할지는 System의 운전 절차에 따라 결정합니다. Emergency Stop 뒤에는 안전 검증과 명시적인 재초기화가 필요합니다.


18. 실전 코드 — 포화와 Filter를 포함한 PID

다음 예제는 Measurement 미분, 1차 Filter, Conditional Integration과 Output Saturation을 구현합니다.

from __future__ import annotations

import math
from dataclasses import dataclass, field


@dataclass
class PID:
    kp: float
    ki: float = 0.0
    kd: float = 0.0
    output_min: float = -1.0
    output_max: float = 1.0
    integral_min: float = -1.5
    integral_max: float = 1.5
    derivative_tau: float = 0.05

    integral: float = field(default=0.0, init=False)
    previous_measurement: float | None = field(default=None, init=False)
    derivative_filtered: float = field(default=0.0, init=False)

    def reset(self, integral: float = 0.0) -> None:
        self.integral = max(self.integral_min, min(self.integral_max, integral))
        self.previous_measurement = None
        self.derivative_filtered = 0.0

    def update(self, setpoint: float, measurement: float, dt: float) -> float:
        values = (setpoint, measurement, dt)
        if not all(math.isfinite(value) for value in values) or dt <= 0.0:
            raise ValueError("setpoint, measurement와 dt는 유효해야 합니다.")

        error = setpoint - measurement
        p_term = self.kp * error

        if self.previous_measurement is None:
            derivative_raw = 0.0
        else:
            derivative_raw = -(measurement - self.previous_measurement) / dt
        self.previous_measurement = measurement

        alpha = dt / (self.derivative_tau + dt)
        self.derivative_filtered += alpha * (
            derivative_raw - self.derivative_filtered
        )
        d_term = self.kd * self.derivative_filtered

        integral_candidate = self.integral + error * dt
        integral_candidate = max(
            self.integral_min, min(self.integral_max, integral_candidate)
        )

        raw_candidate = p_term + self.ki * integral_candidate + d_term
        output_candidate = max(
            self.output_min, min(self.output_max, raw_candidate)
        )

        pushing_high = raw_candidate > self.output_max and error > 0.0
        pushing_low = raw_candidate < self.output_min and error < 0.0
        if not (pushing_high or pushing_low):
            self.integral = integral_candidate

        raw_output = p_term + self.ki * self.integral + d_term
        return max(self.output_min, min(self.output_max, raw_output))


def simulate(controller: PID, name: str) -> None:
    measurement = 0.0
    setpoint = 1.0
    disturbance = -0.2
    dt = 0.02
    controller.reset()

    for step in range(300):
        output = controller.update(setpoint, measurement, dt)
        measurement += (output + disturbance - 0.7 * measurement) * dt * 3.0
        if step % 50 == 49:
            print(
                f"{name:4s} t={(step + 1) * dt:4.1f}s "
                f"y={measurement:+.3f} e={setpoint-measurement:+.3f} "
                f"u={output:+.3f}"
            )


if __name__ == "__main__":
    simulate(PID(kp=1.2), "P")
    simulate(PID(kp=1.2, ki=1.0), "PI")
    simulate(PID(kp=1.2, ki=1.0, kd=0.08), "PID")

이 코드는 학습용입니다. 실제 Robot에서는 단위, 출력 방향, Hardware Limit, 명령 Timeout, Sensor Fault, Controller Enable 상태와 실시간 실행 조건을 추가해야 합니다.


19. 기록하고 검증해야 할 값

PID를 튜닝하거나 고장을 분석할 때 다음 값을 같은 Timestamp로 기록합니다.

  • Setpoint와 Measurement
  • Error
  • P, I, D, Feedforward 각 항
  • Saturation 전 Raw Output과 제한 후 Output
  • 실제 Control Period dt
  • Controller Mode와 Enable 상태
  • Sensor·Driver Fault와 Safety 상태

그래프를 보면 “Gain이 부족한지”, “출력이 이미 포화됐는지”, “적분이 쌓였는지”, “Measurement Noise가 D 항을 흔드는지”를 구분할 수 있습니다.


20. 이 장의 핵심 정리

  • Feedback Control은 목표와 측정의 Error를 이용해 다음 출력을 수정한다.
  • P는 현재 오차, I는 누적 오차, D는 변화 속도에 반응한다.
  • I는 정상상태 오차를 줄이지만 Saturation에서 Windup될 수 있다.
  • D는 감쇠를 높일 수 있지만 Noise와 불규칙한 dt에 민감하다.
  • 실제 PID에는 Output Limit, Anti-windup, Derivative Filter와 유효성 검사가 필요하다.
  • Gain은 안전 제한 아래 Step Response와 실제 운전 조건을 기록하며 조정한다.
  • Feedforward는 예측 가능한 출력을 먼저 제공하고 PID는 남은 오차를 보정한다.
  • Cascaded Loop는 가장 빠른 안쪽 Loop부터 안정화한다.

ROBOT GLOSSARY

용어 정리

전체 용어 찾아보기 →
Open-loop Control개루프 제어
실제 결과를 측정해 되먹이지 않고 미리 정한 입력이나 명령으로 대상을 작동시키는 방식입니다.
Closed-loop Control폐루프 제어
목표와 측정 결과의 차이를 이용해 다음 출력을 계속 수정하는 제어 방식입니다.
Setpoint목표값
Controller가 Plant의 출력이 도달하도록 만들려는 원하는 기준값입니다.
Measurement측정값
Sensor를 통해 얻은 제어 대상의 실제 상태 또는 출력값입니다.
Error오차
음의 Feedback에서 목표값과 측정값의 차이로 일반적으로 목표값에서 측정값을 빼서 구합니다.
Plant제어 대상
Controller의 출력을 받아 움직이며 원하는 상태로 제어하려는 실제 System입니다.
Disturbance외란
제어 명령 외에 경사, 바람이나 하중처럼 Plant의 출력에 영향을 주는 요소입니다.
PID Control비례·적분·미분 제어
현재 오차, 누적 오차와 오차 변화율을 조합해 제어 출력을 계산하는 방식입니다.
Overshoot오버슈트·초과 응답
측정값이 목표값에 접근하는 과정에서 목표를 넘어서는 현상 또는 그 최대 크기입니다.
Steady-state Error정상상태 오차
충분한 시간이 지나 응답이 안정된 뒤에도 목표값과 측정값 사이에 남는 차이입니다.
Saturation출력 포화
Controller 요구값이 Actuator의 허용 한계를 넘어 실제 출력이 더 증가하지 못하는 상태입니다.
Integral Windup적분 와인드업
출력 포화나 막힘 중 오차가 계속 누적되어 이후 큰 Overshoot와 느린 회복을 만드는 현상입니다.
Anti-windup적분 포화 방지
Clamping, Conditional Integration이나 Back-calculation으로 과도한 적분 누적을 막는 방법입니다.
Derivative Kick미분 킥
Setpoint가 급변할 때 오차 미분값과 제어 출력이 순간적으로 크게 튀는 현상입니다.
Low-pass Filter저역통과 필터
느린 변화는 통과시키고 빠른 Noise 성분을 줄여 신호를 부드럽게 만드는 Filter입니다.
Feedforward피드포워드
Model이나 목표 상태로 예상한 필요 출력을 Error가 생기기 전에 먼저 제공하는 제어 방법입니다.
Cascaded Control중첩 제어
Position, Velocity와 Current처럼 빠르기가 다른 여러 Feedback Loop를 안쪽과 바깥쪽으로 겹쳐 구성하는 방식입니다.
Bumpless Transfer무충격 전환
Controller Mode가 바뀔 때 출력이 갑자기 튀지 않도록 내부 상태와 현재 출력을 맞추는 방법입니다.

연습 문제

  1. Open-loop와 Closed-loop Control의 차이를 이동 Robot 속도 제어로 설명하세요.
  2. Setpoint가 1.0, Measurement가 0.72일 때 Error를 계산하세요.
  3. Encoder 방향이 반대로 설정되면 Feedback Loop가 위험해질 수 있는 이유는 무엇인가요?
  4. P Gain을 지나치게 높였을 때 나타날 수 있는 현상을 두 가지 쓰세요.
  5. P Control에서 정상상태 오차가 남을 수 있는 조건을 설명하세요.
  6. I 항이 정상상태 오차를 줄이는 원리를 설명하세요.
  7. Integral Windup이 발생하는 과정과 위험성을 설명하세요.
  8. Conditional Integration은 어떤 경우에 적분을 중단하나요?
  9. D 항이 Sensor Noise에 민감한 이유는 무엇인가요?
  10. Derivative on Measurement가 Derivative Kick을 줄이는 이유는 무엇인가요?
  11. Derivative Filter의 τ_f를 크게 하면 생기는 장단점은 무엇인가요?
  12. PID Library 사이에서 Gain 숫자를 그대로 복사하면 안 되는 이유는 무엇인가요?
  13. Step Response에서 확인할 대표 지표 네 가지를 쓰세요.
  14. Feedforward와 Feedback의 역할 차이를 Robot Arm 중력 보상으로 설명하세요.
  15. Cascaded Control을 안쪽 Loop부터 튜닝해야 하는 이유는 무엇인가요?

참고 자료

  • Karl J. Åström and Richard M. Murray, Feedback Systems: An Introduction for Scientists and Engineers
  • Karl J. Åström and Tore Hägglund, PID Controllers: Theory, Design, and Tuning
  • Gene F. Franklin, J. David Powell and Abbas Emami-Naeini, Feedback Control of Dynamic Systems
  • ROS 2 Control Documentation, Controllers and Hardware Interfaces
  • Brian Douglas, Control Systems Lectures