Робот-курьер Урок Ф.2 Все курсыДля учителяПройти в программе Ученик Arduino Uno + шилд моторов L298P
Тема 7 · Урок Ф.2

Регуляторы: релейный, П, ПД и ПИД на одной трассе

Робот сам измеряет, насколько точно едет, и команда по числам выясняет, какой регулятор лучше.

2 × 45 мин1–2 ученика на роботаПрактика

Результат урока

Команда сравнила релейный, П-, ПД- и ПИД-регуляторы на одной трассе и одной скорости по средней ошибке, наибольшей ошибке и числу переходов через линию и объяснила результат. Не нужен для итоговой задачи.

Ты научишься

  • Провести честное сравнение: одна трасса, одна скорость, одно изменение за раз, три заезда.
  • Объяснить, чем релейный регулятор отличается от П-регулятора.
  • Объяснить, на что реагируют Д- и И-части регулятора.
  • Сделать вывод о регуляторах по измерениям, а не по впечатлению.

Нужно знать

  • 3.1 Датчики линии и первое следование
  • 3.2 П-регулятор: плавно и быстро

Понадобится

Датчик линии (аналоговый, Trema) × 2USB-кабель для Arduino

Зачем этот урок

В курсе робот ездил по линии с двумя регуляторами: релейным в уроке 3.1 («колесо едет или стоит») и П-регулятором в уроке 3.2 («чем дальше линия, тем сильнее поворот»). Инженеры используют ещё два: ПД и ПИД. Какой из четырёх лучше? Сегодня ответим не «на глаз», а по числам: робот сам измерит, насколько точно проехал, а команда сравнит все четыре регулятора на одной трассе и одной скорости.

Факультатив не нужен для итоговой задачи. Он нужен тем, кто хочет понять, как устроены настоящие автопилоты, и проверить это опытом.

Разминка · вспомни
  1. Что делает колесо в программе урока 3.1, когда под его датчиком линия?
  2. Как в уроке 3.2 считали ошибку и поправку?
  3. Почему при подборе kp меняли одно число за раз?
1
Занятие 1 · 45 мин
Измеряем, а не на глаз
1

Как сравнить честно

8 мин

Что должен делать робот. Роботу нужно удерживать линию при разных отклонениях. Сравним четыре способа выбирать команды колёсам на тех же датчиках и моторах, чтобы понимать, что меняет именно алгоритм.

Что используем и как это работает. Релейное правило выбирает одно из заранее заданных действий. П-составляющая реагирует на текущую ошибку, Д — на изменение ошибки между шагами, И — на накопленную ошибку. В этом скетче шаг фиксирован, поэтому коэффициенты настроены для его длительности. Накопление ограничивается, чтобы большая прошлая ошибка не давала чрезмерную поправку после возвращения к линии.

Как связать измерение с действием. Проводим заезды с одинаковой базовой командой и на одном поле, сравниваем отклонения и колебания. Больший коэффициент не означает лучший результат. Проверка формулы на числах помогает найти ошибку кода, но не заменяет испытания на трассе; коэффициенты теста не являются настройками вашего робота.

Чтобы сравнить регуляторы, всё остальное должно быть одинаковым: одна дорожка, одно место старта, одна скорость, одни и те же измерения белого и чёрного. Меняется только регулятор. Каждый регулятор едет три раза: один заезд может оказаться случайным.

  1. Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте карандашом место старта.
  2. Перерисуй в журнал таблицу — её вы заполните к концу урока.
РегуляторЗаездСредняя ошибкаНаибольшая ошибкаПереходов через линиюСошёл с линии?
Релейный1, 2, 3
П1, 2, 3
ПД1, 2, 3
ПИД1, 2, 3
Предскажи

Какой регулятор, по-твоему, проедет точнее всех, а какой — хуже всех? Запиши в журнал.

Должно получиться

Трасса и старт отмечены, таблица перерисована, прогноз записан.

Сравнение честное, только если меняется одно: регулятор. Трасса, старт, скорость, измерения и длина заезда — одни и те же. Каждый регулятор — три заезда.

  1. Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте старт.
  2. Запиши в журнал список того, что должно оставаться одинаковым во всех заездах. Найди не меньше пяти пунктов.
  3. Перерисуй таблицу сравнения.
РегуляторЗаездСредняя ошибкаНаибольшая ошибкаПереходов через линиюСошёл с линии?
Релейный1, 2, 3
П1, 2, 3
ПД1, 2, 3
ПИД1, 2, 3
Предскажи

Расставь четыре регулятора от лучшего к худшему по средней ошибке и объясни порядок одной фразой.

Должно получиться

Список одинаковых условий, таблица и прогноз с объяснением — в журнале.

Сначала спланируй опыт, потом пиши код.

  1. Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте старт.
  2. Запиши в журнал план опыта: - что меняется (одно); - что остаётся одинаковым (не меньше пяти пунктов); - что измеряем и почему этих чисел достаточно, чтобы назвать лучший регулятор; - сколько заездов делаем и какое число берём из трёх, если один заезд сильно отличается.
  3. Запиши прогноз: порядок четырёх регуляторов по средней ошибке.
Подсказка: Какое число брать из трёх заездов

Если один заезд из трёх сильно отличается, среднее арифметическое «утянет» его за собой. Есть число, которое на один случайный заезд не реагирует.

Должно получиться

В журнале план опыта и прогноз. Ты знаешь, какое число из трёх заездов пойдёт в таблицу.

2

Робот измеряет сам

15 мин

Программа ведёт робота заданное время RUN_MS, а потом останавливается и сохраняет итоги заезда в память Arduino. Память не стирается, когда выключаешь питание. В начале каждого запуска программа печатает в монитор порта итоги прошлого заезда.

Что печатает роботЧто значит
Средняя ошибкаНасколько в среднем линия уходила от середины. Чем меньше, тем точнее
Наибольшая ошибкаСамый сильный уход от линии за заезд
Переходов через линиюСколько раз линия перескочила с одной стороны на другую: виляние
  1. Создай скетч LF2_Compare, перенеси измерения из журнала и свой kp из урока 3.2.
Четыре регулятора и измерение заездаLF2_Compare.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>struct Result; // Тип итогов до прототипов, которые добавляет Arduino.#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif #include <EEPROM.h> const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; // Регулятор заезда: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД.const int MODE = 1;int baseSpeed = 100; // одна скорость для всех регуляторовfloat kp = 0.5;      // твой kp из урока 3.2float kd = 0;        // Д: реакция на то, как быстро уходит линияfloat ki = 0;        // И: реакция на ошибку, которая копитсяconst long SUM_LIMIT = 3000;       // предел накопленной ошибкиconst unsigned long RUN_MS = 6000; // длина заезда: робот должен остановиться до конца дорожкиconst unsigned long STEP_MS = 10;  // шаг регулятора: 100 раз в секунду int lastError = 0;long errorSum = 0; // Итоги заезда: хранятся в памяти Arduino и после выключения.struct Result {  int marker;  int mode;  int baseSpeed;  float kp;  float kd;  float ki;  long steps;  long sumAbs;  int maxAbs;  int crossings;}; // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} // Поправка П-, ПД- или ПИД-регулятора по ошибке этого шага.int controller(int mode, int error) {  float correction = kp * error;  if (mode >= 2) {    correction += kd * (error - lastError);  }  if (mode == 3) {    errorSum = constrain(errorSum + error, -SUM_LIMIT, SUM_LIMIT);    correction += ki * errorSum;  }  lastError = error;  return correction;} // ===== Измерение заезда ===== const int MARKER = 2610; // признак того, что в памяти записан заездResult run;int lastSide = 0;unsigned long startTime;unsigned long nextStep; // Добавляет ошибку шага к итогам заезда.void record(int error) {  run.steps++;  run.sumAbs += abs(error);  if (abs(error) > run.maxAbs) {    run.maxAbs = abs(error);  }  int side = 0;  if (error > 10) {    side = 1;  }  if (error < -10) {    side = -1;  }  if (side != 0 && lastSide != 0 && side != lastSide) {    run.crossings++;  }  if (side != 0) {    lastSide = side;  }} void printResult(Result r) {  const char *names[] = {"релейный", "П", "ПД", "ПИД"};  Serial.print("Регулятор: ");  Serial.print(names[r.mode]);  Serial.print("  скорость=");  Serial.print(r.baseSpeed);  Serial.print("  kp=");  Serial.print(r.kp);  Serial.print("  kd=");  Serial.print(r.kd);  Serial.print("  ki=");  Serial.println(r.ki, 3);  Serial.print("Средняя ошибка: ");  Serial.println(r.steps > 0 ? (float)r.sumAbs / r.steps : 0, 1);  Serial.print("Наибольшая ошибка: ");  Serial.println(r.maxAbs);  Serial.print("Переходов через линию: ");  Serial.println(r.crossings);} // Ждёт, пока робот стоит на старте: оба датчика видят белое 1 секунду подряд.void waitForStart() {  Serial.println("Поставь робота на старт: линия между датчиками");  unsigned long whiteSince = millis();  while (millis() - whiteSince < 1000) {    courseTelemetryPoll();    int leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);    int rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);    if (leftLevel < 70 || rightLevel < 70) {      whiteSince = millis();    }  }} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   Result last;  EEPROM.get(0, last);  if (last.marker == MARKER && last.mode >= 0 && last.mode <= 3) {    Serial.println("Прошлый заезд:");    printResult(last);  } else {    Serial.println("Записанных заездов нет");  }   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }  waitForStart();  courseWait(2000); // время убрать руки от робота   run = {MARKER, MODE, baseSpeed, kp, kd, ki, 0, 0, 0, 0};  startTime = millis();  nextStep = startTime;} void loop() {  if (millis() - startTime >= RUN_MS) {    drive(FORWARD, 0, FORWARD, 0);    EEPROM.put(0, run);    Serial.println("Заезд окончен:");    printResult(run);    courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN), NAN, NAN);    while (true) {      courseTelemetryPoll();    }  }  while (millis() < nextStep) {    courseTelemetryPoll();  }  nextStep += STEP_MS;   int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);  int error = leftLevel - rightLevel;  record(error);   float observedOutput = NAN; // у релейного алгоритма нет пропорциональной поправки  int leftSpeed;  int rightSpeed;  if (MODE == 0) {    // Релейный, как в уроке 3.1: колесо над линией стоит.    leftSpeed = leftLevel < 50 ? 0 : baseSpeed;    rightSpeed = rightLevel < 50 ? 0 : baseSpeed;  } else {    int correction = controller(MODE, error);    observedOutput = correction;    leftSpeed = constrain(baseSpeed + correction, 0, 255);    rightSpeed = constrain(baseSpeed - correction, 0, 255);  }  drive(FORWARD, leftSpeed, FORWARD, rightSpeed);  courseTelemetryReport(leftRaw, rightRaw, error, observedOutput);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}
  1. Поставь MODE = 1 (П-регулятор), загрузи программу.
  2. Поставь робота на старт: линия между датчиками. Через секунду программа увидит старт, ещё через две — робот поедет.
  3. Робот должен останавливаться до конца дорожки. Если доезжает до края — уменьши RUN_MS. Подобрал — больше RUN_MS не меняй.
Безопасность

Подключай кабель, держа робота в руке или на подставке. Если поставить его на белый стол, программа примет стол за старт и робот поедет.

  1. Возьми робота в руки, подключи кабель и открой монитор порта: появятся итоги прошлого заезда.
Должно получиться

RUN_MS подобрано, робот останавливается до конца дорожки, в мониторе порта видны итоги заезда.

Как это устроено: Почему робот ждёт старта

Когда открываешь монитор порта, Arduino перезапускается. Если бы робот сразу ехал, он уехал бы со стола и записал бы «заезд» поверх настоящего. Поэтому программа ждёт, пока оба датчика целую секунду видят белое: так бывает, когда робот стоит на старте.

Программа ведёт робота заданное время RUN_MS, останавливает его и сохраняет итоги заезда в память, которая не стирается без питания. При следующем запуске итоги печатаются в монитор порта. Измерение заезда в шаблоне готово. Твоя часть — функция controller: сегодня в ней П-регулятор, на втором занятии добавишь Д и И.

  1. Создай скетч LF2_Compare по шаблону, перенеси измерения и свой kp.
  2. Напиши в controller только П-часть — как в уроке 3.2.
Шаблон: функция регулятораLF2_Compare_Gaps.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>struct Result; // Тип итогов до прототипов, которые добавляет Arduino.#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif // Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. #include <EEPROM.h> const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; // Регулятор заезда: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД.const int MODE = 1;int baseSpeed = 100; // одна скорость для всех регуляторовfloat kp = 0.5;      // твой kp из урока 3.2float kd = 0;        // Д: реакция на то, как быстро уходит линияfloat ki = 0;        // И: реакция на ошибку, которая копитсяconst long SUM_LIMIT = 3000;       // предел накопленной ошибкиconst unsigned long RUN_MS = 6000; // длина заезда: робот должен остановиться до конца дорожкиconst unsigned long STEP_MS = 10;  // шаг регулятора: 100 раз в секунду int lastError = 0;long errorSum = 0; // Итоги заезда: хранятся в памяти Arduino и после выключения.struct Result {  int marker;  int mode;  int baseSpeed;  float kp;  float kd;  float ki;  long steps;  long sumAbs;  int maxAbs;  int crossings;}; // ===== Твоя часть: напиши сам ===== // Поправка П-, ПД- или ПИД-регулятора по ошибке этого шага.int controller(int mode, int error) {  // Занятие 1. П: поправка пропорциональна ошибке, коэффициент kp.  // Занятие 2. Д, режимы 2 и 3: прибавь kd, умноженный на то, насколько ошибка изменилась с прошлого шага.  // Занятие 2. И, только режим 3: копи ошибку в errorSum, не выпуская сумму за ±SUM_LIMIT, и прибавь ki × errorSum.  // В конце запомни ошибку этого шага в lastError: она нужна Д на следующем шаге.  return 0;} // ===== Готовое: шкала, моторы, измерение заезда и запуск — не меняй ===== // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} const int MARKER = 2610; // признак того, что в памяти записан заездResult run;int lastSide = 0;unsigned long startTime;unsigned long nextStep; // Добавляет ошибку шага к итогам заезда.void record(int error) {  run.steps++;  run.sumAbs += abs(error);  if (abs(error) > run.maxAbs) {    run.maxAbs = abs(error);  }  int side = 0;  if (error > 10) {    side = 1;  }  if (error < -10) {    side = -1;  }  if (side != 0 && lastSide != 0 && side != lastSide) {    run.crossings++;  }  if (side != 0) {    lastSide = side;  }} void printResult(Result r) {  const char *names[] = {"релейный", "П", "ПД", "ПИД"};  Serial.print("Регулятор: ");  Serial.print(names[r.mode]);  Serial.print("  скорость=");  Serial.print(r.baseSpeed);  Serial.print("  kp=");  Serial.print(r.kp);  Serial.print("  kd=");  Serial.print(r.kd);  Serial.print("  ki=");  Serial.println(r.ki, 3);  Serial.print("Средняя ошибка: ");  Serial.println(r.steps > 0 ? (float)r.sumAbs / r.steps : 0, 1);  Serial.print("Наибольшая ошибка: ");  Serial.println(r.maxAbs);  Serial.print("Переходов через линию: ");  Serial.println(r.crossings);} // Ждёт, пока робот стоит на старте: оба датчика видят белое 1 секунду подряд.void waitForStart() {  Serial.println("Поставь робота на старт: линия между датчиками");  unsigned long whiteSince = millis();  while (millis() - whiteSince < 1000) {    courseTelemetryPoll();    int leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);    int rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);    if (leftLevel < 70 || rightLevel < 70) {      whiteSince = millis();    }  }} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   Result last;  EEPROM.get(0, last);  if (last.marker == MARKER && last.mode >= 0 && last.mode <= 3) {    Serial.println("Прошлый заезд:");    printResult(last);  } else {    Serial.println("Записанных заездов нет");  }   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }  waitForStart();  courseWait(2000); // время убрать руки от робота   run = {MARKER, MODE, baseSpeed, kp, kd, ki, 0, 0, 0, 0};  startTime = millis();  nextStep = startTime;} void loop() {  if (millis() - startTime >= RUN_MS) {    drive(FORWARD, 0, FORWARD, 0);    EEPROM.put(0, run);    Serial.println("Заезд окончен:");    printResult(run);    courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN), NAN, NAN);    while (true) {      courseTelemetryPoll();    }  }  while (millis() < nextStep) {    courseTelemetryPoll();  }  nextStep += STEP_MS;   int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);  int error = leftLevel - rightLevel;  record(error);   float observedOutput = NAN; // у релейного алгоритма нет пропорциональной поправки  int leftSpeed;  int rightSpeed;  if (MODE == 0) {    // Релейный, как в уроке 3.1: колесо над линией стоит.    leftSpeed = leftLevel < 50 ? 0 : baseSpeed;    rightSpeed = rightLevel < 50 ? 0 : baseSpeed;  } else {    int correction = controller(MODE, error);    observedOutput = correction;    leftSpeed = constrain(baseSpeed + correction, 0, 255);    rightSpeed = constrain(baseSpeed - correction, 0, 255);  }  drive(FORWARD, leftSpeed, FORWARD, rightSpeed);  courseTelemetryReport(leftRaw, rightRaw, error, observedOutput);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}
  1. Поставь MODE = 1, загрузи, поставь робота на старт. Подбери RUN_MS так, чтобы робот останавливался до конца дорожки, и больше его не меняй.
  2. Найди в готовой части функцию record и ответь в журнале: когда она добавляет переход через линию и зачем там порог 10?
Безопасность

Подключай кабель, держа робота в руке или на подставке: на белом столе программа примет стол за старт.

Подсказка: Робот не трогается со старта

Программа ждёт, пока оба датчика целую секунду видят белое (больше 70 по шкале). Посмотри шкалу датчиков на старте.

Должно получиться

controller возвращает П-поправку, RUN_MS подобрано, в журнале — ответ про record.

Каркас ниже берёт на себя измерение: итоги заезда копятся в record(error), сохраняются в память после RUN_MS и печатаются при следующем запуске. Остальное напишешь сам.

Каркас: измерение заездаLF2_Compare_Skeleton.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>struct Result; // Тип итогов до прототипов, которые добавляет Arduino.#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif // Каркас для сравнения регуляторов. Измерение заезда и запуск готовы;// регуляторы напиши сам по требованиям из текста урока.#include <EEPROM.h> const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; // Регулятор заезда: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД.const int MODE = 1;int baseSpeed = 100; // одна скорость для всех регуляторовfloat kp = 0.5;      // твой kp из урока 3.2float kd = 0;        // Д: реакция на то, как быстро уходит линияfloat ki = 0;        // И: реакция на ошибку, которая копитсяconst long SUM_LIMIT = 3000;       // предел накопленной ошибкиconst unsigned long RUN_MS = 6000; // длина заезда: робот должен остановиться до конца дорожкиconst unsigned long STEP_MS = 10;  // шаг регулятора: 100 раз в секунду int lastError = 0;long errorSum = 0; // Итоги заезда: хранятся в памяти Arduino и после выключения.struct Result {  int marker;  int mode;  int baseSpeed;  float kp;  float kd;  float ki;  long steps;  long sumAbs;  int maxAbs;  int crossings;};// Требование 2: здесь будет int controller(int mode, int error). // Перенеси свою функцию level из урока 3.2.int level(int value, int white, int black) {  return 0;} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} // ===== Измерение заезда ===== const int MARKER = 2610; // признак того, что в памяти записан заездResult run;int lastSide = 0;unsigned long startTime;unsigned long nextStep; // Добавляет ошибку шага к итогам заезда.void record(int error) {  run.steps++;  run.sumAbs += abs(error);  if (abs(error) > run.maxAbs) {    run.maxAbs = abs(error);  }  int side = 0;  if (error > 10) {    side = 1;  }  if (error < -10) {    side = -1;  }  if (side != 0 && lastSide != 0 && side != lastSide) {    run.crossings++;  }  if (side != 0) {    lastSide = side;  }} void printResult(Result r) {  const char *names[] = {"релейный", "П", "ПД", "ПИД"};  Serial.print("Регулятор: ");  Serial.print(names[r.mode]);  Serial.print("  скорость=");  Serial.print(r.baseSpeed);  Serial.print("  kp=");  Serial.print(r.kp);  Serial.print("  kd=");  Serial.print(r.kd);  Serial.print("  ki=");  Serial.println(r.ki, 3);  Serial.print("Средняя ошибка: ");  Serial.println(r.steps > 0 ? (float)r.sumAbs / r.steps : 0, 1);  Serial.print("Наибольшая ошибка: ");  Serial.println(r.maxAbs);  Serial.print("Переходов через линию: ");  Serial.println(r.crossings);} // Ждёт, пока робот стоит на старте: оба датчика видят белое 1 секунду подряд.void waitForStart() {  Serial.println("Поставь робота на старт: линия между датчиками");  unsigned long whiteSince = millis();  while (millis() - whiteSince < 1000) {    courseTelemetryPoll();    int leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);    int rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);    if (leftLevel < 70 || rightLevel < 70) {      whiteSince = millis();    }  }} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   Result last;  EEPROM.get(0, last);  if (last.marker == MARKER && last.mode >= 0 && last.mode <= 3) {    Serial.println("Прошлый заезд:");    printResult(last);  } else {    Serial.println("Записанных заездов нет");  }   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }  waitForStart();  courseWait(2000); // время убрать руки от робота   run = {MARKER, MODE, baseSpeed, kp, kd, ki, 0, 0, 0, 0};  startTime = millis();  nextStep = startTime;} void loop() {  if (millis() - startTime >= RUN_MS) {    drive(FORWARD, 0, FORWARD, 0);    EEPROM.put(0, run);    Serial.println("Заезд окончен:");    printResult(run);    courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN), NAN, NAN);    while (true) {      courseTelemetryPoll();    }  }  while (millis() < nextStep) {    courseTelemetryPoll();  }  nextStep += STEP_MS;   float observedError = NAN;  float observedOutput = NAN;  // Требования 1–3: ошибка, record(error), регулятор по MODE и скорости колёс.  // Для графиков сохрани свои результаты в observedError и observedOutput.  drive(FORWARD, 0, FORWARD, 0);  courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN),                        observedError, observedOutput);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}

Требование 1. Одна константа MODE выбирает регулятор: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД. Всё остальное во всех режимах одинаково.

  • Релейный — по правилу урока 3.1, но на шкале 0…100: колесо, под датчиком которого линия, стоит, другое едет со скоростью baseSpeed.
  • П, ПД и ПИД получают поправку из функции controller(mode, error) и делят её между колёсами, как в уроке 3.2.
  • Каждый шаг ошибка передаётся в record(error) — во всех четырёх режимах.

Сегодня напиши level, ошибку, релейный режим и П-часть controller. Д и И — на втором занятии.

  1. Перенеси level и ошибку из урока 3.2.
  2. Выполни требование 1 для режимов 0 и 1. Подбери RUN_MS так, чтобы робот останавливался до конца дорожки.
  3. Найди в record порог 10. Запиши в журнал, что изменится в числе переходов, если порог убрать, и почему.
Безопасность

Подключай кабель, держа робота в руке или на подставке: на белом столе программа примет стол за старт.

Подсказка: Робот не трогается со старта

waitForStart ждёт белого по шкале level. Пока level возвращает 0, робот не стартует никогда — это защита каркаса.

Должно получиться

Режимы 0 и 1 работают, RUN_MS подобрано, в журнале — ответ про порог.

3

Релейный против П

20 мин
  1. Поставь MODE = 0 — релейный регулятор из урока 3.1. Сделай три заезда. После каждого подключи кабель и перепиши итоги в таблицу.
  2. Поставь MODE = 1 — П-регулятор. Ещё три заезда.
  3. Сравни строки. Какой регулятор вилял больше? У какого меньше средняя ошибка?
Если не получилось
Что видишьЧто сделать
«Записанных заездов нет» после заездаЗаезд не доехал до конца RUN_MS: питание отключили раньше. Повтори
Робот поехал прямо со столаТы подключил кабель, когда робот стоял на белом. Держи его в руке
Релейный сходит с дугиЭто тоже результат: запиши «да» в «Сошёл с линии?»
Должно получиться

В таблице по три заезда релейного и П-регулятора. Ты можешь сказать, какой точнее и по какому числу это видно.

  1. Сделай по три заезда с MODE = 0 и MODE = 1. Итоги — в таблицу.
  2. Для каждого регулятора выбери из трёх средних ошибок среднюю по величине — она и пойдёт в сравнение.
  3. Ответь в журнале: почему у релейного больше переходов через линию? Объясни через то, какие скорости получают колёса.
Подсказка: Почему средняя по величине

Если один заезд случайно вышел плохим, среднее арифметическое изменится сильно, а средняя по величине — нет.

Должно получиться

Таблица для двух регуляторов заполнена, выбраны средние по величине, объяснение переходов записано.

  1. Проведи опыт по своему плану для режимов 0 и 1.
  2. Сравни не только средние ошибки, но и разброс трёх заездов: насколько повторяется результат каждого регулятора?
  3. Запиши в журнал объяснение через устройство регулятора: почему у релейного больше переходов и наибольшая ошибка, хотя оба видят одни и те же датчики.
Должно получиться

Данные двух регуляторов, разброс и объяснение — в журнале. Прогноз из первого шага проверен для первой пары.

2
Занятие 2 · 45 мин
ПД, ПИД и общий итог
4

ПД: тормозим поворот заранее

14 мин

П-регулятор поворачивает, пока линия не вернулась на середину, и часто проскакивает её: робот виляет. Д-часть смотрит, как быстро меняется ошибка. Если линия быстро возвращается к середине, Д-часть заранее ослабляет поворот — как велосипедист, который начинает выпрямлять руль ещё до того, как выехал на линию.

Функция регулятораLF2_Compare.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>struct Result; // Тип итогов до прототипов, которые добавляет Arduino.#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif #include <EEPROM.h> const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; // Регулятор заезда: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД.const int MODE = 1;int baseSpeed = 100; // одна скорость для всех регуляторовfloat kp = 0.5;      // твой kp из урока 3.2float kd = 0;        // Д: реакция на то, как быстро уходит линияfloat ki = 0;        // И: реакция на ошибку, которая копитсяconst long SUM_LIMIT = 3000;       // предел накопленной ошибкиconst unsigned long RUN_MS = 6000; // длина заезда: робот должен остановиться до конца дорожкиconst unsigned long STEP_MS = 10;  // шаг регулятора: 100 раз в секунду int lastError = 0;long errorSum = 0; // Итоги заезда: хранятся в памяти Arduino и после выключения.struct Result {  int marker;  int mode;  int baseSpeed;  float kp;  float kd;  float ki;  long steps;  long sumAbs;  int maxAbs;  int crossings;}; // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} // Поправка П-, ПД- или ПИД-регулятора по ошибке этого шага.int controller(int mode, int error) {  float correction = kp * error;  if (mode >= 2) {    correction += kd * (error - lastError);  }  if (mode == 3) {    errorSum = constrain(errorSum + error, -SUM_LIMIT, SUM_LIMIT);    correction += ki * errorSum;  }  lastError = error;  return correction;} // ===== Измерение заезда ===== const int MARKER = 2610; // признак того, что в памяти записан заездResult run;int lastSide = 0;unsigned long startTime;unsigned long nextStep; // Добавляет ошибку шага к итогам заезда.void record(int error) {  run.steps++;  run.sumAbs += abs(error);  if (abs(error) > run.maxAbs) {    run.maxAbs = abs(error);  }  int side = 0;  if (error > 10) {    side = 1;  }  if (error < -10) {    side = -1;  }  if (side != 0 && lastSide != 0 && side != lastSide) {    run.crossings++;  }  if (side != 0) {    lastSide = side;  }} void printResult(Result r) {  const char *names[] = {"релейный", "П", "ПД", "ПИД"};  Serial.print("Регулятор: ");  Serial.print(names[r.mode]);  Serial.print("  скорость=");  Serial.print(r.baseSpeed);  Serial.print("  kp=");  Serial.print(r.kp);  Serial.print("  kd=");  Serial.print(r.kd);  Serial.print("  ki=");  Serial.println(r.ki, 3);  Serial.print("Средняя ошибка: ");  Serial.println(r.steps > 0 ? (float)r.sumAbs / r.steps : 0, 1);  Serial.print("Наибольшая ошибка: ");  Serial.println(r.maxAbs);  Serial.print("Переходов через линию: ");  Serial.println(r.crossings);} // Ждёт, пока робот стоит на старте: оба датчика видят белое 1 секунду подряд.void waitForStart() {  Serial.println("Поставь робота на старт: линия между датчиками");  unsigned long whiteSince = millis();  while (millis() - whiteSince < 1000) {    courseTelemetryPoll();    int leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);    int rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);    if (leftLevel < 70 || rightLevel < 70) {      whiteSince = millis();    }  }} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   Result last;  EEPROM.get(0, last);  if (last.marker == MARKER && last.mode >= 0 && last.mode <= 3) {    Serial.println("Прошлый заезд:");    printResult(last);  } else {    Serial.println("Записанных заездов нет");  }   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }  waitForStart();  courseWait(2000); // время убрать руки от робота   run = {MARKER, MODE, baseSpeed, kp, kd, ki, 0, 0, 0, 0};  startTime = millis();  nextStep = startTime;} void loop() {  if (millis() - startTime >= RUN_MS) {    drive(FORWARD, 0, FORWARD, 0);    EEPROM.put(0, run);    Serial.println("Заезд окончен:");    printResult(run);    courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN), NAN, NAN);    while (true) {      courseTelemetryPoll();    }  }  while (millis() < nextStep) {    courseTelemetryPoll();  }  nextStep += STEP_MS;   int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);  int error = leftLevel - rightLevel;  record(error);   float observedOutput = NAN; // у релейного алгоритма нет пропорциональной поправки  int leftSpeed;  int rightSpeed;  if (MODE == 0) {    // Релейный, как в уроке 3.1: колесо над линией стоит.    leftSpeed = leftLevel < 50 ? 0 : baseSpeed;    rightSpeed = rightLevel < 50 ? 0 : baseSpeed;  } else {    int correction = controller(MODE, error);    observedOutput = correction;    leftSpeed = constrain(baseSpeed + correction, 0, 255);    rightSpeed = constrain(baseSpeed - correction, 0, 255);  }  drive(FORWARD, leftSpeed, FORWARD, rightSpeed);  courseTelemetryReport(leftRaw, rightRaw, error, observedOutput);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}

Поправка ПД: kp × ошибка + kd × (ошибка сейчас − ошибка на прошлом шаге).

  1. Посчитай на бумаге: kp = 0.5, kd = 4, на прошлом шаге ошибка была 10, сейчас 25.
  2. Поставь MODE = 2 и попробуй kd = 5, 10 и 20 — по одному заезду на дорожке. Выбери kd, при котором робот меньше всего виляет.
  3. С выбранным kd сделай три заезда и запиши итоги в таблицу.
Должно получиться

Ты посчитал поправку: 12,5 + 60 = 72,5, программа отбросит дробную часть — 72. Выбран kd, в таблице три заезда ПД.

Как это устроено: Почему kd такой большой

Регулятор считает 100 раз в секунду. За одну сотую секунды ошибка меняется всего на несколько единиц, поэтому множитель kd нужен больше, чем kp.

П-регулятор часто проскакивает линию: поправка не ослабевает, пока ошибка не станет нулём. Д-часть реагирует на то, как быстро меняется ошибка, и гасит поворот заранее.

  1. Допиши Д-часть в controller по цели в шаблоне.
Предскажи

kp = 0.5, kd = 4, на прошлом шаге ошибка 10, сейчас 25. Какую поправку вернёт controller для MODE = 2? Посчитай до запуска.

  1. Поставь MODE = 2. Подбери kd: начни с 2.5 и удваивай, по одному заезду. Запиши, при каком kd робот начал дёргаться на прямой, и возьми предыдущее значение.
  2. С выбранным kd — три заезда в таблицу.
Подсказка: Где взять прошлую ошибку

Она уже лежит в lastError, если ты запоминаешь её в конце функции.

Подсказка: Робот дёргается даже при маленьком kd

Проверь знак: Д-часть прибавляется, а изменение ошибки — «сейчас минус раньше».

Должно получиться

Прогноз совпал: 72. Д-часть написана, kd подобран удвоением, три заезда записаны.

Требование 2. Функция int controller(int mode, int error):

  • режим 1 — только П-часть;
  • режимы 2 и 3 — плюс Д-часть: kd, умноженный на изменение ошибки с прошлого шага;
  • режим 3 — плюс И-часть: ошибки копятся в errorSum, сумма не выходит за ±SUM_LIMIT, поправка ki × errorSum; в других режимах сумма не копится;
  • прошлая ошибка обновляется на каждом шаге во всех режимах.

Приёмка. Напиши controller в тестовом скетче, загрузи и открой монитор порта. Когда все строки PASS, перенеси функцию в свою программу. Сегодня тест проверяет и И-часть — значит, её тоже нужно написать.

Тест функции controllerLF2_Controller_Test.ino.ino
// Проверка функции controller. Напиши функцию здесь, загрузи и открой монитор порта.// Когда все строки PASS, перенеси controller в свою программу.// Коэффициенты в тесте свои, удобные для счёта; на дорожке будут твои. float kp = 0.5;float kd = 2.0;float ki = 0.25;const long SUM_LIMIT = 200; int lastError = 0;long errorSum = 0; // Требование 2: режим 1 — П, 2 — ПД, 3 — ПИД.int controller(int mode, int error) {  return 0;} int failed = 0; void reset() {  lastError = 0;  errorSum = 0;} void expect(const char *name, int actual, int expected) {  Serial.print(actual == expected ? "PASS " : "FAIL ");  Serial.print(name);  Serial.print(": получили ");  Serial.print(actual);  Serial.print(", ожидаем ");  Serial.println(expected);  if (actual != expected) {    failed++;  }} void setup() {  Serial.begin(9600);   reset();  expect("П, ошибка 40", controller(1, 40), 20);  expect("П, ошибка -40", controller(1, -40), -20);   reset();  expect("ПД, первый шаг 10", controller(2, 10), 25);  expect("ПД, ошибка выросла до 20", controller(2, 20), 30);  expect("ПД, ошибка осталась 20", controller(2, 20), 10);   reset();  expect("ПИД, первый шаг 10", controller(3, 10), 27);  expect("ПИД, снова 10", controller(3, 10), 10);   // Накопленная ошибка не выходит за SUM_LIMIT.  reset();  int last = 0;  for (int i = 0; i < 5; i++) {    last = controller(3, 100);  }  expect("ПИД, пять шагов по 100", last, 100);  reset();  for (int i = 0; i < 5; i++) {    last = controller(3, -100);  }  expect("ПИД, пять шагов по -100", last, -100);   // В режиме П ошибка не копится, но прошлая ошибка запоминается.  reset();  controller(1, 100);  controller(1, 100);  controller(1, 100);  expect("ПИД после трёх шагов П", controller(3, 100), 75);   if (failed == 0) {    Serial.println("Все проверки пройдены");  } else {    Serial.print("Ошибок: ");    Serial.println(failed);  }} void loop() {}

Затем подбери kd методом: начни с 2.5 и удваивай до первого дёргания на прямой, возьми предыдущее значение. Три заезда — в таблицу.

Подсказка: Идея (1 из 3)

Скорость ухода линии — разница между ошибкой сейчас и ошибкой на прошлом шаге.

Подсказка: План (2 из 3)
  1. П-часть.
  2. Если режим 2 или 3 — прибавить Д-часть.
  3. Если режим 3 — обновить сумму, ограничить её, прибавить И-часть.
  4. Запомнить ошибку.
Подсказка: Фрагмент кода (3 из 3)

errorSum = constrain(errorSum + error, -SUM_LIMIT, SUM_LIMIT); — почему тест «пять шагов по 100» без этой строки даёт 175?

Должно получиться

Тест: «Все проверки пройдены». kd подобран методом, три заезда ПД записаны.

5

ПИД: что даёт накопленная ошибка

11 мин

И-часть складывает ошибки шаг за шагом. Если линия долго остаётся с одной стороны — например, на длинной дуге, — сумма растёт, и робот поворачивает всё сильнее. Чтобы сумма не росла бесконечно, её ограничивает SUM_LIMIT.

  1. Оставь свой kd, поставь MODE = 3 и попробуй ki = 0.005, 0.01 и 0.02 — по одному заезду.
  2. Выбери лучший ki и сделай три заезда в таблицу.
Предскажи

Будет ли ПИД точнее ПД на нашей трассе? Запиши прогноз до заездов.

Должно получиться

В таблице три заезда ПИД. Прогноз проверен.

И-часть копит ошибку. Пока линия долго с одной стороны, сумма растёт и усиливает поворот. Опасность — сумма продолжает тянуть в ту же сторону и после дуги.

  1. Допиши И-часть в controller по цели в шаблоне.
  2. Поставь MODE = 3, подбери ki: 0.005, 0.01, 0.02. Три заезда с лучшим — в таблицу.
  3. Временно поставь SUM_LIMIT = 30000 и проедь один раз. Что изменилось после дуги? Верни 3000.
Подсказка: Как не выпустить сумму за предел

Та же команда, что держит скорость колёс в 0…255, подойдёт и для суммы.

Должно получиться

И-часть написана, ki подобран, три заезда записаны. Ты видел, что делает робот без предела суммы.

  1. Поставь MODE = 3, подбери ki методом: от 0.005, удваивая. Три заезда с лучшим — в таблицу.
  2. Опыт с пределом: один заезд с SUM_LIMIT = 30000. Опиши, что происходит после дуги, и объясни через значение errorSum.
  3. Ответь в журнале: в каких условиях И-часть даёт выигрыш и есть ли они на нашей трассе?
Подсказка: Когда И-часть полезна

Подумай о долгой ошибке одного знака: длинная дуга одного радиуса или робот, у которого одно колесо всегда крутится медленнее другого.

Должно получиться

ki подобран, опыт с пределом описан, условия пользы И-части записаны.

6

Сравнение всех четырёх

12 мин
  1. Выпиши для каждого регулятора среднюю ошибку из трёх заездов — среднюю по величине.
  2. Расставь регуляторы от лучшего к худшему.
МестоРегуляторСредняя ошибкаПереходов через линию
1
2
3
4
  1. Сравни с прогнозом из начала урока. Что совпало, что нет?
Должно получиться

Четыре регулятора расставлены по местам, прогноз проверен.

  1. Составь итоговую таблицу: регулятор, средняя по величине средняя ошибка, наибольшая ошибка, переходы, сходы.
  2. Назови лучший регулятор для этой трассы и скорости и обоснуй двумя числами.
  3. Ответь: оправдала ли И-часть своё усложнение? Почему?
  4. Какой регулятор ты взял бы для итоговой задачи и почему?
Должно получиться

Итоговая таблица, обоснованный вывод и ответы — в журнале.

  1. Составь итоговую таблицу всех четырёх регуляторов с разбросом трёх заездов.
  2. Напиши вывод из трёх частей: что показали числа, чем это объясняется в устройстве регуляторов, что можно было сделать нечестно и как ты это исключил.
  3. Сравни результат с прогнозом и планом опыта из первого шага.
Должно получиться

Итоговая таблица и вывод из трёх частей — в журнале.

7

Задания по уровням

8 мин
Основное · Все четыре регулятора

Заполнить таблицу сравнения всех четырёх регуляторов на одной скорости, по три заезда, и назвать лучший по средней ошибке.

Средний уровень · ПД лучше П

Подобрать kd, при котором ПД проходит трассу с меньшей средней ошибкой, чем П, и объяснить по числам почему.

Продвинутый уровень · Предельная скорость

Найти для каждого из четырёх регуляторов наибольшую baseSpeed, на которой он проходит трассу три раза из трёх: повышать шагами по 20. Составить рейтинг по скорости и сравнить его с рейтингом по средней ошибке.

Проверь себя

Проверь себя

Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.

● Основное

1.Почему все регуляторы сравнивают на одной трассе и одной скорости?
2.Что делает релейный регулятор из урока 3.1 с колесом, под датчиком которого линия?
3.Чем П-регулятор отличается от релейного?
4.Что показывает «средняя ошибка» в итогах заезда?
5.На что реагирует Д-часть регулятора?

★ Средний уровень

1.kp = 0.5, kd = 4. На прошлом шаге ошибка была 10, сейчас 25. Какую поправку вернёт ПД-регулятор (дробная часть отбрасывается)?
2.Почему ПД меньше виляет на выходе из дуги, чем П?
3.Зачем ограничивать сумму ошибок errorSum значением SUM_LIMIT?
4.Три заезда ПИД дали среднюю ошибку 8, 9 и 21. Какое число взять в сравнение?
5.Зачем регулятор считает с постоянным шагом STEP_MS?

★★ Продвинутый уровень

1.С этой функцией ПД ведёт себя как П с увеличенным kp. Почему?
int controller(int mode, int error) {  float correction = kp * error;  if (mode >= 2) {    correction += kd * (error - lastError);  }  return correction;}
2.В ПИД убрали ограничение суммы. После длинной дуги влево робот на прямой долго уходит влево за линию. Почему?
3.Средняя ошибка и переходы: релейный 30 и 40, П 14 и 6, ПД 9 и 2, ПИД 10 и 3. Какой вывод честный?
4.Почему на трассе из дуг в разные стороны И-часть почти не помогает?
5.В программе написали correction -= kd * (error - lastError). Что будет с ПД?

Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.

Итог

Релейный регулятор знает только «да или нет», П-регулятор — «насколько», Д-часть — «как быстро», И-часть — «как долго». Больше частей не значит лучше: решают измерения на твоей трассе и скорости.

Как подтвердить результат

Результат опыта связывает условия, версию программы, наблюдение и вывод. Сравнение попыток с одним изменением помогает проверить причину, включая неудачные попытки. Отметка относится к уже выполненной практике; повтор ради отметки не нужен. Расхождение с прогнозом задаёт следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.

Словарь урока

релейный регуляторПравило «всё или ничего»: колесо либо едет, либо стоит, как в уроке 3.1.
П-частьПоправка, пропорциональная ошибке: kp × ошибка. Регулятор из урока 3.2.
Д-частьПоправка на то, как быстро меняется ошибка: kd × (ошибка сейчас − ошибка на прошлом шаге).
И-частьПоправка на ошибку, накопленную за время: ki × сумма ошибок.
средняя ошибкаНасколько в среднем линия уходила от середины за заезд, без учёта стороны. Чем меньше, тем точнее робот едет.
шаг регулятораПостоянный промежуток между расчётами поправки, здесь 10 мс.