Регуляторы: релейный, П, ПД и ПИД на одной трассе
Робот сам измеряет, насколько точно едет, и команда по числам выясняет, какой регулятор лучше.
Результат урока
Команда сравнила релейный, П-, ПД- и ПИД-регуляторы на одной трассе и одной скорости по средней ошибке, наибольшей ошибке и числу переходов через линию и объяснила результат. Не нужен для итоговой задачи.
Ты научишься
- Провести честное сравнение: одна трасса, одна скорость, одно изменение за раз, три заезда.
- Объяснить, чем релейный регулятор отличается от П-регулятора.
- Объяснить, на что реагируют Д- и И-части регулятора.
- Сделать вывод о регуляторах по измерениям, а не по впечатлению.
Нужно знать
- 3.1 Датчики линии и первое следование
- 3.2 П-регулятор: плавно и быстро
Понадобится
Зачем этот урок
В курсе робот ездил по линии с двумя регуляторами: релейным в уроке 3.1 («колесо едет или стоит») и П-регулятором в уроке 3.2 («чем дальше линия, тем сильнее поворот»). Инженеры используют ещё два: ПД и ПИД. Какой из четырёх лучше? Сегодня ответим не «на глаз», а по числам: робот сам измерит, насколько точно проехал, а команда сравнит все четыре регулятора на одной трассе и одной скорости.
Факультатив не нужен для итоговой задачи. Он нужен тем, кто хочет понять, как устроены настоящие автопилоты, и проверить это опытом.
- Что делает колесо в программе урока 3.1, когда под его датчиком линия?
- Как в уроке 3.2 считали ошибку и поправку?
- Почему при подборе
kpменяли одно число за раз?
Как сравнить честно
8 минЧто должен делать робот. Роботу нужно удерживать линию при разных отклонениях. Сравним четыре способа выбирать команды колёсам на тех же датчиках и моторах, чтобы понимать, что меняет именно алгоритм.
Что используем и как это работает. Релейное правило выбирает одно из заранее заданных действий. П-составляющая реагирует на текущую ошибку, Д — на изменение ошибки между шагами, И — на накопленную ошибку. В этом скетче шаг фиксирован, поэтому коэффициенты настроены для его длительности. Накопление ограничивается, чтобы большая прошлая ошибка не давала чрезмерную поправку после возвращения к линии.
Как связать измерение с действием. Проводим заезды с одинаковой базовой командой и на одном поле, сравниваем отклонения и колебания. Больший коэффициент не означает лучший результат. Проверка формулы на числах помогает найти ошибку кода, но не заменяет испытания на трассе; коэффициенты теста не являются настройками вашего робота.
Чтобы сравнить регуляторы, всё остальное должно быть одинаковым: одна дорожка, одно место старта, одна скорость, одни и те же измерения белого и чёрного. Меняется только регулятор. Каждый регулятор едет три раза: один заезд может оказаться случайным.
- Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте карандашом место старта.
- Перерисуй в журнал таблицу — её вы заполните к концу урока.
| Регулятор | Заезд | Средняя ошибка | Наибольшая ошибка | Переходов через линию | Сошёл с линии? |
|---|---|---|---|---|---|
| Релейный | 1, 2, 3 | ||||
| П | 1, 2, 3 | ||||
| ПД | 1, 2, 3 | ||||
| ПИД | 1, 2, 3 |
Какой регулятор, по-твоему, проедет точнее всех, а какой — хуже всех? Запиши в журнал.
Трасса и старт отмечены, таблица перерисована, прогноз записан.
Сравнение честное, только если меняется одно: регулятор. Трасса, старт, скорость, измерения и длина заезда — одни и те же. Каждый регулятор — три заезда.
- Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте старт.
- Запиши в журнал список того, что должно оставаться одинаковым во всех заездах. Найди не меньше пяти пунктов.
- Перерисуй таблицу сравнения.
| Регулятор | Заезд | Средняя ошибка | Наибольшая ошибка | Переходов через линию | Сошёл с линии? |
|---|---|---|---|---|---|
| Релейный | 1, 2, 3 | ||||
| П | 1, 2, 3 | ||||
| ПД | 1, 2, 3 | ||||
| ПИД | 1, 2, 3 |
Расставь четыре регулятора от лучшего к худшему по средней ошибке и объясни порядок одной фразой.
Список одинаковых условий, таблица и прогноз с объяснением — в журнале.
Сначала спланируй опыт, потом пиши код.
- Выберите трассу сравнения: прямой участок и дуга 250 мм из урока 3.2. Отметьте старт.
- Запиши в журнал план опыта: - что меняется (одно); - что остаётся одинаковым (не меньше пяти пунктов); - что измеряем и почему этих чисел достаточно, чтобы назвать лучший регулятор; - сколько заездов делаем и какое число берём из трёх, если один заезд сильно отличается.
- Запиши прогноз: порядок четырёх регуляторов по средней ошибке.
Подсказка: Какое число брать из трёх заездов
Если один заезд из трёх сильно отличается, среднее арифметическое «утянет» его за собой. Есть число, которое на один случайный заезд не реагирует.
В журнале план опыта и прогноз. Ты знаешь, какое число из трёх заездов пойдёт в таблицу.
Робот измеряет сам
15 минПрограмма ведёт робота заданное время RUN_MS, а потом останавливается и сохраняет итоги заезда в память Arduino. Память не стирается, когда выключаешь питание. В начале каждого запуска программа печатает в монитор порта итоги прошлого заезда.
| Что печатает робот | Что значит |
|---|---|
| Средняя ошибка | Насколько в среднем линия уходила от середины. Чем меньше, тем точнее |
| Наибольшая ошибка | Самый сильный уход от линии за заезд |
| Переходов через линию | Сколько раз линия перескочила с одной стороны на другую: виляние |
- Создай скетч
LF2_Compare, перенеси измерения из журнала и свойkpиз урока 3.2.
// Наблюдение в 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); }}
- Поставь
MODE = 1(П-регулятор), загрузи программу. - Поставь робота на старт: линия между датчиками. Через секунду программа увидит старт, ещё через две — робот поедет.
- Робот должен останавливаться до конца дорожки. Если доезжает до края — уменьши
RUN_MS. Подобрал — большеRUN_MSне меняй.
Подключай кабель, держа робота в руке или на подставке. Если поставить его на белый стол, программа примет стол за старт и робот поедет.
- Возьми робота в руки, подключи кабель и открой монитор порта: появятся итоги прошлого заезда.
RUN_MS подобрано, робот останавливается до конца дорожки, в мониторе порта видны итоги заезда.
Как это устроено: Почему робот ждёт старта
Когда открываешь монитор порта, Arduino перезапускается. Если бы робот сразу ехал, он уехал бы со стола и записал бы «заезд» поверх настоящего. Поэтому программа ждёт, пока оба датчика целую секунду видят белое: так бывает, когда робот стоит на старте.
Программа ведёт робота заданное время RUN_MS, останавливает его и сохраняет итоги заезда в память, которая не стирается без питания. При следующем запуске итоги печатаются в монитор порта. Измерение заезда в шаблоне готово. Твоя часть — функция controller: сегодня в ней П-регулятор, на втором занятии добавишь Д и И.
- Создай скетч
LF2_Compareпо шаблону, перенеси измерения и свойkp. - Напиши в
controllerтолько П-часть — как в уроке 3.2.
// Наблюдение в 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); }}
- Поставь
MODE = 1, загрузи, поставь робота на старт. ПодбериRUN_MSтак, чтобы робот останавливался до конца дорожки, и больше его не меняй. - Найди в готовой части функцию
recordи ответь в журнале: когда она добавляет переход через линию и зачем там порог 10?
Подключай кабель, держа робота в руке или на подставке: на белом столе программа примет стол за старт.
Подсказка: Робот не трогается со старта
Программа ждёт, пока оба датчика целую секунду видят белое (больше 70 по шкале). Посмотри шкалу датчиков на старте.
controller возвращает П-поправку, RUN_MS подобрано, в журнале — ответ про record.
Каркас ниже берёт на себя измерение: итоги заезда копятся в record(error), сохраняются в память после RUN_MS и печатаются при следующем запуске. Остальное напишешь сам.
// Наблюдение в 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. Д и И — на втором занятии.
- Перенеси
levelи ошибку из урока 3.2. - Выполни требование 1 для режимов 0 и 1. Подбери
RUN_MSтак, чтобы робот останавливался до конца дорожки. - Найди в
recordпорог 10. Запиши в журнал, что изменится в числе переходов, если порог убрать, и почему.
Подключай кабель, держа робота в руке или на подставке: на белом столе программа примет стол за старт.
Подсказка: Робот не трогается со старта
waitForStart ждёт белого по шкале level. Пока level возвращает 0, робот не стартует никогда — это защита каркаса.
Режимы 0 и 1 работают, RUN_MS подобрано, в журнале — ответ про порог.
Релейный против П
20 мин- Поставь
MODE = 0— релейный регулятор из урока 3.1. Сделай три заезда. После каждого подключи кабель и перепиши итоги в таблицу. - Поставь
MODE = 1— П-регулятор. Ещё три заезда. - Сравни строки. Какой регулятор вилял больше? У какого меньше средняя ошибка?
| Что видишь | Что сделать |
|---|---|
| «Записанных заездов нет» после заезда | Заезд не доехал до конца RUN_MS: питание отключили раньше. Повтори |
| Робот поехал прямо со стола | Ты подключил кабель, когда робот стоял на белом. Держи его в руке |
| Релейный сходит с дуги | Это тоже результат: запиши «да» в «Сошёл с линии?» |
В таблице по три заезда релейного и П-регулятора. Ты можешь сказать, какой точнее и по какому числу это видно.
- Сделай по три заезда с
MODE = 0иMODE = 1. Итоги — в таблицу. - Для каждого регулятора выбери из трёх средних ошибок среднюю по величине — она и пойдёт в сравнение.
- Ответь в журнале: почему у релейного больше переходов через линию? Объясни через то, какие скорости получают колёса.
Подсказка: Почему средняя по величине
Если один заезд случайно вышел плохим, среднее арифметическое изменится сильно, а средняя по величине — нет.
Таблица для двух регуляторов заполнена, выбраны средние по величине, объяснение переходов записано.
- Проведи опыт по своему плану для режимов 0 и 1.
- Сравни не только средние ошибки, но и разброс трёх заездов: насколько повторяется результат каждого регулятора?
- Запиши в журнал объяснение через устройство регулятора: почему у релейного больше переходов и наибольшая ошибка, хотя оба видят одни и те же датчики.
Данные двух регуляторов, разброс и объяснение — в журнале. Прогноз из первого шага проверен для первой пары.
ПД: тормозим поворот заранее
14 минП-регулятор поворачивает, пока линия не вернулась на середину, и часто проскакивает её: робот виляет. Д-часть смотрит, как быстро меняется ошибка. Если линия быстро возвращается к середине, Д-часть заранее ослабляет поворот — как велосипедист, который начинает выпрямлять руль ещё до того, как выехал на линию.
// Наблюдение в 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 × (ошибка сейчас − ошибка на прошлом шаге).
- Посчитай на бумаге:
kp = 0.5,kd = 4, на прошлом шаге ошибка была 10, сейчас 25. - Поставь
MODE = 2и попробуйkd= 5, 10 и 20 — по одному заезду на дорожке. Выбериkd, при котором робот меньше всего виляет. - С выбранным
kdсделай три заезда и запиши итоги в таблицу.
Ты посчитал поправку: 12,5 + 60 = 72,5, программа отбросит дробную часть — 72. Выбран kd, в таблице три заезда ПД.
Как это устроено: Почему kd такой большой
Регулятор считает 100 раз в секунду. За одну сотую секунды ошибка меняется всего на несколько единиц, поэтому множитель kd нужен больше, чем kp.
П-регулятор часто проскакивает линию: поправка не ослабевает, пока ошибка не станет нулём. Д-часть реагирует на то, как быстро меняется ошибка, и гасит поворот заранее.
- Допиши Д-часть в
controllerпо цели в шаблоне.
kp = 0.5, kd = 4, на прошлом шаге ошибка 10, сейчас 25. Какую поправку вернёт controller для MODE = 2? Посчитай до запуска.
- Поставь
MODE = 2. Подбериkd: начни с 2.5 и удваивай, по одному заезду. Запиши, при какомkdробот начал дёргаться на прямой, и возьми предыдущее значение. - С выбранным
kd— три заезда в таблицу.
Подсказка: Где взять прошлую ошибку
Она уже лежит в lastError, если ты запоминаешь её в конце функции.
Подсказка: Робот дёргается даже при маленьком kd
Проверь знак: Д-часть прибавляется, а изменение ошибки — «сейчас минус раньше».
Прогноз совпал: 72. Д-часть написана, kd подобран удвоением, три заезда записаны.
Требование 2. Функция int controller(int mode, int error):
- режим 1 — только П-часть;
- режимы 2 и 3 — плюс Д-часть:
kd, умноженный на изменение ошибки с прошлого шага; - режим 3 — плюс И-часть: ошибки копятся в
errorSum, сумма не выходит за±SUM_LIMIT, поправкаki × errorSum; в других режимах сумма не копится; - прошлая ошибка обновляется на каждом шаге во всех режимах.
Приёмка. Напиши controller в тестовом скетче, загрузи и открой монитор порта. Когда все строки PASS, перенеси функцию в свою программу. Сегодня тест проверяет и И-часть — значит, её тоже нужно написать.
// Проверка функции 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)
- П-часть.
- Если режим 2 или 3 — прибавить Д-часть.
- Если режим 3 — обновить сумму, ограничить её, прибавить И-часть.
- Запомнить ошибку.
Подсказка: Фрагмент кода (3 из 3)
errorSum = constrain(errorSum + error, -SUM_LIMIT, SUM_LIMIT); — почему тест «пять шагов по 100» без этой строки даёт 175?
Тест: «Все проверки пройдены». kd подобран методом, три заезда ПД записаны.
ПИД: что даёт накопленная ошибка
11 минИ-часть складывает ошибки шаг за шагом. Если линия долго остаётся с одной стороны — например, на длинной дуге, — сумма растёт, и робот поворачивает всё сильнее. Чтобы сумма не росла бесконечно, её ограничивает SUM_LIMIT.
- Оставь свой
kd, поставьMODE = 3и попробуйki= 0.005, 0.01 и 0.02 — по одному заезду. - Выбери лучший
kiи сделай три заезда в таблицу.
Будет ли ПИД точнее ПД на нашей трассе? Запиши прогноз до заездов.
В таблице три заезда ПИД. Прогноз проверен.
И-часть копит ошибку. Пока линия долго с одной стороны, сумма растёт и усиливает поворот. Опасность — сумма продолжает тянуть в ту же сторону и после дуги.
- Допиши И-часть в
controllerпо цели в шаблоне. - Поставь
MODE = 3, подбериki: 0.005, 0.01, 0.02. Три заезда с лучшим — в таблицу. - Временно поставь
SUM_LIMIT = 30000и проедь один раз. Что изменилось после дуги? Верни 3000.
Подсказка: Как не выпустить сумму за предел
Та же команда, что держит скорость колёс в 0…255, подойдёт и для суммы.
И-часть написана, ki подобран, три заезда записаны. Ты видел, что делает робот без предела суммы.
- Поставь
MODE = 3, подбериkiметодом: от 0.005, удваивая. Три заезда с лучшим — в таблицу. - Опыт с пределом: один заезд с
SUM_LIMIT = 30000. Опиши, что происходит после дуги, и объясни через значениеerrorSum. - Ответь в журнале: в каких условиях И-часть даёт выигрыш и есть ли они на нашей трассе?
Подсказка: Когда И-часть полезна
Подумай о долгой ошибке одного знака: длинная дуга одного радиуса или робот, у которого одно колесо всегда крутится медленнее другого.
ki подобран, опыт с пределом описан, условия пользы И-части записаны.
Сравнение всех четырёх
12 мин- Выпиши для каждого регулятора среднюю ошибку из трёх заездов — среднюю по величине.
- Расставь регуляторы от лучшего к худшему.
| Место | Регулятор | Средняя ошибка | Переходов через линию |
|---|---|---|---|
| 1 | |||
| 2 | |||
| 3 | |||
| 4 |
- Сравни с прогнозом из начала урока. Что совпало, что нет?
Четыре регулятора расставлены по местам, прогноз проверен.
- Составь итоговую таблицу: регулятор, средняя по величине средняя ошибка, наибольшая ошибка, переходы, сходы.
- Назови лучший регулятор для этой трассы и скорости и обоснуй двумя числами.
- Ответь: оправдала ли И-часть своё усложнение? Почему?
- Какой регулятор ты взял бы для итоговой задачи и почему?
Итоговая таблица, обоснованный вывод и ответы — в журнале.
- Составь итоговую таблицу всех четырёх регуляторов с разбросом трёх заездов.
- Напиши вывод из трёх частей: что показали числа, чем это объясняется в устройстве регуляторов, что можно было сделать нечестно и как ты это исключил.
- Сравни результат с прогнозом и планом опыта из первого шага.
Итоговая таблица и вывод из трёх частей — в журнале.
Задания по уровням
8 минЗаполнить таблицу сравнения всех четырёх регуляторов на одной скорости, по три заезда, и назвать лучший по средней ошибке.
Подобрать kd, при котором ПД проходит трассу с меньшей средней ошибкой, чем П, и объяснить по числам почему.
Найти для каждого из четырёх регуляторов наибольшую baseSpeed, на которой он проходит трассу три раза из трёх: повышать шагами по 20. Составить рейтинг по скорости и сравнить его с рейтингом по средней ошибке.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Релейный регулятор знает только «да или нет», П-регулятор — «насколько», Д-часть — «как быстро», И-часть — «как долго». Больше частей не значит лучше: решают измерения на твоей трассе и скорости.
Как подтвердить результат
Результат опыта связывает условия, версию программы, наблюдение и вывод. Сравнение попыток с одним изменением помогает проверить причину, включая неудачные попытки. Отметка относится к уже выполненной практике; повтор ради отметки не нужен. Расхождение с прогнозом задаёт следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| релейный регулятор | Правило «всё или ничего»: колесо либо едет, либо стоит, как в уроке 3.1. |
| П-часть | Поправка, пропорциональная ошибке: kp × ошибка. Регулятор из урока 3.2. |
| Д-часть | Поправка на то, как быстро меняется ошибка: kd × (ошибка сейчас − ошибка на прошлом шаге). |
| И-часть | Поправка на ошибку, накопленную за время: ki × сумма ошибок. |
| средняя ошибка | Насколько в среднем линия уходила от середины за заезд, без учёта стороны. Чем меньше, тем точнее робот едет. |
| шаг регулятора | Постоянный промежуток между расчётами поправки, здесь 10 мс. |