⚖️ PID-регулятор

Урок 2.3 | Level 2: Оператор

Три буквы, которые изменили робототехнику

Проблема

Робот по линии дёргается:

Цель: ехать по центру
Реальность: влево-вправо-влево-вправо 🐍

     ───────────────────────────  ← Линия
          🤖~~~~~~~~~~~~~~~~~~~~~  ← Траектория робота

Как сделать плавно?

Аналогия: Душ в отеле 🚿

Знакомая ситуация?

1. Вода холодная → Крутишь на ГОРЯЧО
2. Ждёшь...
3. КИПЯТОК! → Крутишь на ХОЛОДНО
4. Ждёшь...
5. ЛЕДЯНАЯ! → Крутишь на ГОРЯЧО
6. ... повторяется вечно

Проблема: Слишком резкие коррекции!

Как опытный человек регулирует душ?

1. Вода холодная → Крутишь ЧУТЬ-ЧУТЬ на горячо
2. Ждёшь, следишь за изменением
3. Стало теплее, но мало → Ещё чуть-чуть
4. Приближается к норме → Замедляешь коррекцию
5. Идеально! → Фиксируешь

Это и есть PID!

Формула PID

Три компонента

$$ \text{Коррекция} = K_p \cdot e + K_i \cdot \int e \, dt + K_d \cdot \frac{de}{dt} $$
БукваНазваниеЧто делает
PProportionalРеагирует на текущую ошибку
IIntegralНакапливает прошлые ошибки
DDerivativeПредсказывает будущую ошибку

P — Пропорциональная часть

Чем больше ошибка — тем сильнее коррекция

float error = target - current;
float P = Kp * error;
Ошибка:    ████████░░░░░░░░  (большая)
Коррекция: ████████░░░░░░░░  (сильная)

Ошибка:    ██░░░░░░░░░░░░░░  (маленькая)
Коррекция: ██░░░░░░░░░░░░░░  (слабая)

Проблема только P

Колебания вокруг цели (перерегулирование):

Цель ─────────────────────────────
        ╱╲      ╱╲      ╱╲
       ╱  ╲    ╱  ╲    ╱  ╲
      ╱    ╲  ╱    ╲  ╱    ╲
─────╱      ╲╱      ╲╱      ╲────
     
     Робот «перелетает» цель

D — Дифференциальная часть

Тормозит при быстром изменении (демпфер)

float derivative = (error - lastError) / dt;
float D = Kd * derivative;
lastError = error;

Аналогия: Тормоза в машине. Чем быстрее приближаешься к стене — тем сильнее тормозишь.

P + D вместе

                   Только P
Цель ─────────────────────────────
        ╱╲      ╱╲
       ╱  ╲    ╱  ╲
──────╱    ╲──╱    ╲──────────────

                   P + D
Цель ─────────────────────────────
        ╱╲
       ╱  ╲___________________________
──────╱
      
      D гасит колебания!

I — Интегральная часть

Устраняет постоянную ошибку (смещение)

integral += error * dt;
float I = Ki * integral;

Проблема: Робот систематически едет чуть левее центра.
P и D не помогут — ошибка маленькая!

I накапливает маленькие ошибки → большая коррекция.

Когда нужен I?

Без I:
Цель ─────────────────────────────
                ___________________
───────────────╱
               ↑ Постоянная ошибка (offset)

С I:
Цель ─────────────────────────────
              ╱‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
─────────────╱
              
              I устранил смещение!

Код PID-регулятора

Базовая реализация

// Коэффициенты (настраиваются!)
float Kp = 1.0;
float Ki = 0.05;
float Kd = 0.5;

// Состояние
float lastError = 0;
float integral = 0;
unsigned long lastTime = 0;

float computePID(float target, float current) {
  // Время между вызовами
  unsigned long now = millis();
  float dt = (now - lastTime) / 1000.0;  // в секундах
  lastTime = now;
  
  // Ошибка
  float error = target - current;
  
  // P
  float P = Kp * error;
  
  // I (с ограничением!)
  integral += error * dt;
  integral = constrain(integral, -100, 100);  // Anti-windup
  float I = Ki * integral;
  
  // D
  float derivative = (error - lastError) / dt;
  float D = Kd * derivative;
  lastError = error;
  
  return P + I + D;
}

Использование для line follower

const int BASE_SPEED = 150;
const float TARGET = 0;  // Центр линии

void loop() {
  float position = readLinePosition();  // -100 ... +100
  
  float correction = computePID(TARGET, position);
  
  int leftSpeed = BASE_SPEED + correction;
  int rightSpeed = BASE_SPEED - correction;
  
  // Ограничиваем скорость
  leftSpeed = constrain(leftSpeed, 0, 255);
  rightSpeed = constrain(rightSpeed, 0, 255);
  
  setMotors(leftSpeed, rightSpeed);
  
  delay(10);  // ~100 Hz
}

Использование для балансирующего робота

void loop() {
  float angle = readIMU();  // Угол наклона в градусах
  
  float correction = computePID(0, angle);  // Цель = 0° (вертикально)
  
  // Положительная коррекция = вперёд, отрицательная = назад
  setMotors(correction, correction);
}

Настройка коэффициентов

Метод Зиглера-Николса (упрощённый)

  1. Установи Ki = 0, Kd = 0
  2. Увеличивай Kp, пока робот не начнёт стабильно колебаться
  3. Запомни это значение как Ku (критическое усиление)
  4. Запомни период колебаний Tu
Kp = 0.6 × Ku
Ki = 2 × Kp / Tu  
Kd = Kp × Tu / 8

Ручная настройка (практичнее!)

Шаг 1: Только P
├── Начни с Kp = 1
├── Увеличивай, пока не начнутся колебания
└── Уменьши на 20%

Шаг 2: Добавь D
├── Начни с Kd = 0.1 × Kp
├── Увеличивай, пока колебания не исчезнут
└── Не переборщи — робот станет «вялым»

Шаг 3: Добавь I (если нужно)
├── Начни с Ki = 0.01
├── Увеличивай, пока не исчезнет постоянная ошибка
└── Осторожно — легко получить колебания!

Типичные значения

ПрименениеKpKiKd
Line follower0.5-2.00-0.10.1-0.5
Балансирующий10-500.1-11-5
Термостат1-100.01-0.10-1
Серво позиция1-500.1-0.5

Визуальная диагностика

Kp слишком мал:        Kp хороший:          Kp слишком велик:
                       
Цель ──────────        Цель ──────────      Цель ──────────
                              ╱‾‾‾‾               ╱╲  ╱╲
      __________             ╱                   ╱  ╲╱  ╲
─────╱                 ─────╱              ─────╱
     
(медленно)             (быстро, плавно)     (колебания)

Продвинутые техники

Anti-windup (ограничение интеграла)

Без ограничения интеграл «накручивается» при долгой ошибке:

// ПЛОХО:
integral += error * dt;  // Может стать ОГРОМНЫМ!

// ХОРОШО:
integral += error * dt;
integral = constrain(integral, -MAX_INTEGRAL, MAX_INTEGRAL);

Derivative kick fix

При резком изменении цели (не позиции) D даёт «пинок»:

// ПЛОХО:
float derivative = (error - lastError) / dt;

// ЛУЧШЕ: Дифференцируем только измерение
float derivative = -(current - lastCurrent) / dt;
lastCurrent = current;

Зонирование (разные PID для разных ситуаций)

void loop() {
  float error = abs(target - current);
  
  if (error > 50) {
    // Далеко от цели — агрессивный PID
    Kp = 2.0; Ki = 0; Kd = 0.5;
  } else if (error > 10) {
    // Близко — стандартный
    Kp = 1.0; Ki = 0.05; Kd = 0.3;
  } else {
    // Очень близко — мягкий
    Kp = 0.5; Ki = 0.1; Kd = 0.2;
  }
  
  float correction = computePID(target, current);
  // ...
}

Интерактивная настройка

Попробуй в симуляторе!

🔌 PID Tuner
Открыть симуляцию в Wokwi
Arduino / ESP32 симулятор с возможностью редактирования кода

Измени Kp, Ki, Kd через Serial и смотри результат!

Итоги урока

PID = P + I + D

КомпонентРеагирует наЭффект
PТекущую ошибкуБыстрая реакция
IНакопленную ошибкуУстраняет смещение
DСкорость измененияГасит колебания

Порядок настройки:

PDI (не наоборот!)

Подробнее в справочнике

📚 Алгоритмы управления
📚 Контуры обратной связи
📚 Фильтрация сигналов

Следующий урок

🏆 Соревнования!

Урок 2.4 →

Готовимся к реальным робототехническим соревнованиям!