П-регулятор: плавно и быстро
Робот поворачивает тем сильнее, чем дальше ушла линия, и проходит дуги без рывков.
Результат урока
Робот плавно едет по линии с П-регулятором и проходит дугу своего уровня. Коэффициент и скорость подобраны и записаны в журнал.
Ты научишься
- Перевести показания двух датчиков в общую шкалу от 0 до 100.
- Посчитать ошибку как разницу показаний и объяснить её знак.
- Объяснить, как поправка меняет скорости колёс.
- Подобрать коэффициент и скорость по наблюдениям на дорожке.
Нужно знать
- 3.1 Датчики линии и первое следование
- 2.1 Моторы: едем и останавливаемся
Понадобится
Зачем этот урок
В уроке 3.1 робот ехал по линии рывками: колесо либо крутилось, либо стояло. На крутых дугах итоговой задачи так не проехать. Сегодня робот научится поворачивать чуть-чуть, когда линия ушла немного, и сильно, когда ушла далеко. Так работают автопилоты и роботы-курьеры.
- Как программа из урока 3.1 решала, что под датчиком линия?
- Почему робот в уроке 3.1 ехал рывками?
- Что делает функция
drive()?
Плавно — это как?
8 минЧто должен делать робот. Роботу нужно двигаться по линии плавнее: при маленьком отклонении чуть поправлять курс, а при большом — сильнее. Используем те же датчики отражения и моторы, добавив П-регулятор — правило расчёта поправки.
Что используем и как это работает. Сначала переводим измерения каждого датчика в общую шкалу по его белому и чёрному. Разность уровней называется ошибкой. Коэффициент kp умножает ошибку: чем больше отклонение, тем больше поправка. Одному колесу прибавляем её к базовой команде, у другого вычитаем. Новое измерение позволяет снова скорректировать движение — это обратная связь.
Как связать измерение с действием. Слишком малая поправка не успевает вернуть робота, слишком большая вызывает колебания. Меняем один параметр за раз и сравниваем траекторию и показания на одинаковой трассе. Нулевая ошибка означает равные показания, но не всегда правильное положение: оба датчика могут потерять линию.
Представь, что ты ведёшь велосипед по линии. Если линия ушла чуть влево, ты чуть поворачиваешь руль. Если сильно — поворачиваешь сильнее. Ты не бросаешь руль из крайнего положения в крайнее.
- Встань с напарником: один — «робот», второй держит линию (шнур или полоску бумаги) и плавно смещает её влево и вправо.
- «Робот» поворачивает корпус тем сильнее, чем дальше линия от середины.
Если ты один: положи на стол изогнутый шнур и веди вдоль него ладонью — это «робот». Поворачивай ладонь тем сильнее, чем дальше шнур ушёл от её середины.
Ты можешь объяснить: сила поворота зависит от того, насколько далеко ушла линия.
Представь, что ты ведёшь велосипед по линии: чуть ушла — чуть поворачиваешь руль, сильно ушла — сильнее. Программа из урока 3.1 так не умеет: она знает только «линия есть» или «нет».
- Запусти программу из урока 3.1 на дуге 350 мм и посмотри, как едет робот.
- Запиши в журнал одной фразой: чего не хватает программе, чтобы поворачивать плавно?
Ты можешь объяснить: сила поворота должна зависеть от того, насколько далеко ушла линия.
Исследуй проблему, прежде чем решать.
- Запусти свою программу из урока 3.1 на дуге 350 мм, потом на дуге 125 мм. Запиши, как едет робот: рывки, виляние, сход с линии.
- Объясни в журнале, какую информацию с датчиков программа 3.1 выбрасывает и почему из-за этого робот едет рывками.
- До того как читать дальше, придумай своё правило: как скорости колёс должны зависеть от показаний датчиков. Запиши словами или формулой. В конце урока сравни его с П-регулятором.
Подсказка: Если не знаешь, с чего начать
Программа 3.1 сравнивает показание с порогом и получает «да или нет». А что, если использовать само число?
В журнале — наблюдение на двух дугах, объяснение причины рывков и твоё правило управления.
Общая шкала для двух датчиков
15 минУ каждого датчика свои числа белого и чёрного. Чтобы сравнивать их, переведём оба в одну шкалу: чёрное — 0, белое — 100, край линии — где-то посередине.
- Создай скетч
L32_Error. Перенеси из журнала свои измерения белого и чёрного.
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif 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; // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) { long result = (long)(value - black) * 100 / (white - black); return constrain(result, 0, 100);} void setup() { courseTelemetryBegin(); if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) { Serial.println("Впиши измерения белого и чёрного"); while (true) { courseTelemetryPoll(); } }} void loop() { 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; courseTelemetryReport(leftRaw, rightRaw, error, NAN); Serial.print("L="); Serial.print(leftLevel); Serial.print(" R="); Serial.print(rightLevel); Serial.print(" ошибка="); Serial.println(error); courseWait(200);} // ===== Готовое: передача наблюдений — не меняй =====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); }}
- Загрузи, открой монитор порта. Проверь: на белом оба числа около 100, на чёрном — около 0.
- Медленно проведи край линии под левым датчиком. Как меняется число?
Оба датчика показывают около 100 на белом и около 0 на чёрном, на краю линии — промежуточные числа.
Как это устроено: Как работает функция level
value - black — насколько показание отличается от чёрного. white - black — весь путь от чёрного до белого. Их отношение, умноженное на 100, — «сколько процентов пути до белого». Формула работает, даже если у датчика чёрное — большое число, а белое — маленькое. constrain(…, 0, 100) обрезает случайные выбросы за границы шкалы.
У датчиков разные числа белого и чёрного, поэтому сравнивать их напрямую нельзя. Нужна общая шкала: чёрное — 0, белое — 100. Показание на шкале — это «сколько процентов пути от чёрного до белого» оно прошло.
- Создай скетч
L32_Errorпо шаблону, перенеси измерения из журнала. - Напиши функцию
levelсам. Ошибку вloop()пока не трогай.
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif // Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. 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; // ===== Твоя часть: напиши сам ===== int level(int value, int white, int black) { // Шкала: чёрное — 0, белое — 100, между ними — по прямой. // Работает и когда у датчика чёрное — большое число, и когда маленькое; не выходит за 0…100. return 0;} void loop() { 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); // Ошибка: насколько и в какую сторону линия ушла от середины; линия посередине — 0. float error = NAN; // Пока ошибка не вычислена, график показывает «нет данных». courseTelemetryReport(leftRaw, rightRaw, error, NAN); Serial.print("L="); Serial.print(leftLevel); Serial.print(" R="); Serial.print(rightLevel); Serial.print(" ошибка="); Serial.println(error); courseWait(200);} // ===== Готовое: настройка и защита — не меняй ===== void setup() { courseTelemetryBegin(); if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) { Serial.println("Впиши измерения белого и чёрного"); while (true) { courseTelemetryPoll(); } }} // ===== Готовое: передача наблюдений — не меняй =====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); }}
У датчика белое — 120, чёрное — 780. Какое число шкалы получится для показания 450? Посчитай до запуска.
- Загрузи и проверь на белом, чёрном и краю линии.
- Подними робота на 3–4 см над столом: датчику почти нечего отразить. Какое «сырое» число получается, что показывает шкала и какая часть функции за это отвечает?
- Ответь в журнале: какого типа число ты взял для расчёта и почему не обычный
int?
Подсказка: С чего начать формулу
Путь от чёрного до белого — white - black. Пройденная часть — value - black. Осталось превратить их отношение в проценты.
Подсказка: Шкала скачет у белого
На Arduino Uno int хранит числа только до 32 767. Посчитай, сколько выйдет (value - black) * 100 для показания 50 и чёрного 780.
Подсказка: Шкала уходит за 0…100
constrain(x, a, b) возвращает x, но не меньше a и не больше b.
На белом около 100, на чёрном около 0, для показания 450 из примера — 50. Шкала не уходит за 0…100, и ты знаешь почему. В журнале — ответ про тип числа.
Ты не начинаешь с чистого листа: работай в своей программе из урока 3.1. В ней уже есть контакты, измерения белого и чёрного и защита от запуска без измерений. Сохрани её копию как L32_P_Line — за урок ты превратишь её в П-регулятор, шаг за шагом, по требованиям.
Подсказка: Нет своей программы из урока 3.1?
Возьми каркас: в нём только контакты, управление моторами и защита. Всё остальное напишешь сам.
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif // Каркас П-регулятора. Требования к программе — в тексте урока, шаг за шагом.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; 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);} // Требование 1: шкала «чёрное — 0, белое — 100».int level(int value, int white, int black) { return 0;} 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); // Без измерений робот не поедет. Эту защиту не удаляй. if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) { while (true) { courseTelemetryPoll(); } } courseWait(2000);} void loop() { float observedError = NAN; float observedOutput = NAN; // Требования 2 и 3: ошибка, поправка и скорости колёс. // Для графиков сохрани свои результаты в 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); }}
Как это устроено: Справка по командам
| Команда или тип | Что делает |
|---|---|
analogRead(pin) | Показание датчика: 0…1023 |
analogWrite(pin, v) | Скорость мотора: v от 0 до 255 |
digitalWrite(pin, HIGH) / LOW | Направление мотора |
constrain(x, a, b) | x, но не меньше a и не больше b |
Serial.begin(9600) | Включает монитор порта, пишут один раз в setup() |
Serial.print(x) / Serial.println(x) | Вывод в монитор порта; println — с переводом строки |
int на Arduino Uno | Целые от −32 768 до 32 767 |
long на Arduino Uno | Целые от −2 147 483 648 до 2 147 483 647 |
/ с целыми числами | Дробная часть отбрасывается: 7 / 2 даёт 3 |
Требование 1. Функция level(value, white, black) переводит показание в шкалу 0…100:
- чёрное — 0, белое — 100, между ними — по прямой;
- работает и когда у датчика чёрное — большое число, а белое — маленькое;
- никогда не выходит за 0…100;
- не переполняется на Arduino Uno.
Приёмка. Напиши level в тестовом скетче, загрузи его и открой монитор порта. Тест проверяет восемь случаев, в том числе датчик «наоборот» и показания за пределами шкалы. Когда все строки PASS, перенеси функцию в свою программу и проверь шкалу обоих датчиков на дорожке.
// Проверка функции level. Напиши функцию здесь, загрузи и открой монитор порта.// Когда все строки PASS, перенеси level в свою программу. // Требование 1: шкала «чёрное — 0, белое — 100».int level(int value, int white, int black) { return 0;} int failed = 0; void expect(int value, int white, int black, int expected) { int actual = level(value, white, black); Serial.print(actual == expected ? "PASS " : "FAIL "); Serial.print("level("); Serial.print(value); Serial.print(", "); Serial.print(white); Serial.print(", "); Serial.print(black); Serial.print(") = "); Serial.print(actual); Serial.print(", ожидаем "); Serial.println(expected); if (actual != expected) { failed++; }} void setup() { Serial.begin(9600); // Датчик, у которого чёрное — большое число, а белое — маленькое. expect(780, 120, 780, 0); expect(120, 120, 780, 100); expect(450, 120, 780, 50); // За пределами шкалы. expect(900, 120, 780, 0); expect(50, 120, 780, 100); // Датчик, у которого наоборот: белое — большое число, чёрное — маленькое. expect(120, 900, 120, 0); expect(510, 900, 120, 50); expect(900, 900, 120, 100); if (failed == 0) { Serial.println("Все проверки пройдены"); } else { Serial.print("Ошибок: "); Serial.println(failed); }} void loop() {}
Подсказка: Идея (1 из 3)
Шкала — это доля пути от чёрного до белого. Весь путь — разница белого и чёрного.
Подсказка: План (2 из 3)
- Насколько показание ушло от чёрного?
- Раздели на длину всего пути и умножь на 100.
- Обрежь результат границами шкалы.
Подсказка: Фрагмент кода (3 из 3)
long result = (long)(value - black) * 100 / (white - black); — почему здесь long и почему умножение стоит раньше деления?
Тест показывает «Все проверки пройдены». На дорожке шкала показывает около 0 на чёрном и около 100 на белом.
Считаем ошибку
15 минОшибка — разница показаний: error = leftLevel - rightLevel. Когда линия ровно посередине, оба датчика видят одинаково и ошибка равна нулю.
Линия ушла под левый датчик. Какой знак будет у ошибки — плюс или минус? А если под правый?
- Посмотри на строку с ошибкой в мониторе порта.
- Подвинь линию под левый датчик, потом под правый. Запиши в журнал знак и примерные числа.
| Где линия | leftLevel | rightLevel | Ошибка |
|---|---|---|---|
| Посередине | |||
| Под левым | |||
| Под правым |
Линия слева — ошибка отрицательная, справа — положительная, посередине — около нуля.
Ошибка показывает, насколько и в какую сторону линия ушла от середины. Считаем её как разницу: левое показание минус правое.
- Напиши расчёт ошибки в
loop()сам и загрузи скетч.
Линия ушла под левый датчик. Какой знак будет у ошибки? Почему?
- Проверь прогноз: подвинь линию под левый датчик, потом под правый. Запиши в журнал.
| Где линия | leftLevel | rightLevel | Ошибка |
|---|---|---|---|
| Посередине | |||
| Под левым | |||
| Под правым |
- Ответь в журнале: в какую сторону должен повернуть робот, если ошибка отрицательная?
Подсказка: Знак ошибки
Чёрная линия — 0 на шкале. Если линия под левым датчиком, какое из двух чисел меньше?
Знаки в таблице совпали с прогнозом, и ты можешь объяснить, куда поворачивать при каждом знаке.
Требование 2. Программа считает ошибку — число со знаком: насколько и в какую сторону линия ушла от середины. Знак выбираешь ты, но потом поправка должна поворачивать робота к линии.
- Добавь расчёт ошибки и временный вывод
leftLevel,rightLevel, ошибки в монитор порта. - Заполни таблицу и запиши в журнал своё правило знака одной фразой.
| Где линия | leftLevel | rightLevel | Ошибка |
|---|---|---|---|
| Посередине | |||
| Под левым | |||
| Под правым |
- Разберись с краем: линия ушла за пределы обоих датчиков. Что покажет ошибка и что тогда сделает робот? Запиши, чем это опасно на крутой дуге.
Подсказка: Идея (1 из 2)
Если линия ровно посередине, оба датчика видят одно и то же — ошибка должна быть ноль.
Подсказка: Проверка знака (2 из 2)
Выбери: left - right или right - left. Запиши, какое колесо должно ускориться, когда линия слева, — это понадобится в требовании 3.
Таблица заполнена, правило знака записано. Ты знаешь, что видит регулятор, когда линия ушла за оба датчика.
От ошибки к скоростям
10 минПоправка — ошибка, умноженная на коэффициент: correction = kp * error. К левому колесу поправку прибавляем, у правого — отнимаем.
- Создай скетч
L32_P_Line, перенеси измерения.
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif 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; // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) { long result = (long)(value - black) * 100 / (white - black); return constrain(result, 0, 100);} 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; float kp = 0.5; // сила поворота: подбираем на дорожкеint baseSpeed = 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);} 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); // Без измерений робот не поедет. if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) { while (true) { courseTelemetryPoll(); } } courseWait(2000);} void loop() { 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; int correction = kp * error; int leftSpeed = constrain(baseSpeed + correction, 0, 255); int rightSpeed = constrain(baseSpeed - correction, 0, 255); drive(FORWARD, leftSpeed, FORWARD, rightSpeed); courseTelemetryReport(leftRaw, rightRaw, error, correction);} // ===== Готовое: передача наблюдений — не меняй =====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); }}
- Посчитай на бумаге:
baseSpeed = 100,kp = 0.5, ошибка −40. Какие скорости получат колёса?
Ты посчитал: поправка −20, левое колесо 80, правое 120 — робот поворачивает налево, к линии.
Как это устроено: Зачем constrain
При большой ошибке скорость может получиться −30 или 300. Мотор понимает только 0…255. constrain(число, 0, 255) ставит границы: меньше нуля станет 0, больше 255 — 255.
Правило П-регулятора: поправка пропорциональна ошибке. Чем дальше ушла линия, тем сильнее одно колесо ускоряется, а другое замедляется. Насколько сильно — задаёт kp.
- Создай скетч
L32_P_Lineпо шаблону, перенеси измерения и свою функциюlevel. - Напиши
loop()сам: ошибка, поправка, скорости колёс в пределах 0…255 и команда моторам.
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif // Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. 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;float kp = 0.5; // сила поворота: подбираем на дорожкеint baseSpeed = 100; // скорость на прямой // ===== Твоя часть: напиши сам ===== int level(int value, int white, int black) { // Перенеси свою функцию level из L32_Error. return 0;} void loop() { float observedError = NAN; float observedOutput = NAN; 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); // Ошибка — как в L32_Error. // Поправка пропорциональна ошибке, коэффициент kp. // Робот поворачивает к линии тем сильнее, чем больше ошибка; скорость каждого колеса — в пределах 0…255. // Отправь скорости на моторы. // Сохрани вычисленные ошибку и поправку в observedError и observedOutput для графиков. courseTelemetryReport(leftRaw, rightRaw, observedError, observedOutput);} // ===== Готовое: моторы, настройка и защита — не меняй ===== 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);} 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); // Без измерений робот не поедет. if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) { while (true) { courseTelemetryPoll(); } } courseWait(2000);} // ===== Готовое: передача наблюдений — не меняй =====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); }}
- Посчитай на бумаге:
baseSpeed = 100,kp = 0.5, ошибка −40. Какие скорости получат колёса и куда повернёт робот? На подставке в следующем шаге проверишь это на колёсах.
Что будет делать робот, если поставить kp = 0? А если сделать kp отрицательным?
Подсказка: Какое колесо ускорять
Линия слева — ошибка отрицательная. Какое колесо должно ехать быстрее, чтобы робот повернул к линии? Согласуй знак поправки с этим.
Подсказка: Границы скорости
Мотор понимает только 0…255. Какая команда из урока не даёт числу выйти за границы?
loop() написан. Для примера: поправка −20, левое колесо 80, правое 120 — робот поворачивает налево, к линии. Прогноз про kp = 0 записан — проверишь на подставке.
Требование 3. Робот едет вперёд со скоростью baseSpeed и поворачивает к линии тем сильнее, чем больше ошибка:
- поправка пропорциональна ошибке, коэффициент
kp; kpиbaseSpeed— именованные переменные в начале скетча, их подбирают на дорожке;- скорости колёс всегда в пределах 0…255;
- без измерений белого и чёрного колёса стоят (защита из урока 3.1 остаётся).
Приёмка на бумаге. Посчитай скорости колёс для своей программы:
baseSpeed | kp | Ошибка | Левое | Правое |
|---|---|---|---|---|
| 100 | 0.5 | −40 | ||
| 200 | 0.8 | 60 | ||
| 200 | 1.0 | 100 |
В последней строке одно колесо упирается в границу. Насколько слабее робот повернёт, чем «хотел»? Запиши в журнал, что можно с этим сделать.
Подсказка: Идея (1 из 3)
Одно колесо получает базовую скорость плюс поправку, другое — минус поправку.
Подсказка: План (2 из 3)
- Ошибка из требования 2.
- Поправка = коэффициент × ошибка.
- Левое и правое колесо: база ± поправка, каждое — в границах 0…255.
- Отправить скорости на моторы.
Подсказка: Фрагмент кода (3 из 3)
int leftSpeed = constrain(baseSpeed + correction, 0, 255); — а что для правого? Проверь знак по своему правилу из требования 2.
Программа выполняет требование 3, таблица посчитана, ограничение записано в журнал.
Проверь на подставке
7 мин- Поставь робота на подставку, загрузи программу, включи питание моторов.
- Подведи линию под левый датчик: левое колесо замедлится, правое ускорится.
- Подведи под правый — наоборот. Линия посередине — колёса крутятся одинаково.
Колёса меняют скорость плавно, а не «стоп — едет».
| Что видишь | Что сделать |
|---|---|
| Колёса не крутятся | Вписаны ли измерения? Включено ли питание моторов? |
| Ускоряется не то колесо | Проверь знак ошибки по монитору порта и формулы скоростей |
- Поставь робота на подставку, загрузи программу, включи питание моторов.
- Подведи линию под левый датчик, потом под правый, потом посередине. Запиши, какое колесо ускоряется и какое замедляется.
- Проверь свой прогноз про
kp = 0: поставьkp = 0, загрузи и подвигай линию. Что делают колёса? Верни свойkp.
| Где линия | Левое колесо | Правое колесо | Совпало с прогнозом? |
|---|---|---|---|
| Под левым | |||
| Под правым | |||
| Посередине |
Колёса меняют скорость плавно. Таблица заполнена, прогноз про kp = 0 проверен.
| Что видишь | Что сделать |
|---|---|
| Колёса не крутятся | Написан ли loop() с командой моторам? Вписаны ли измерения? |
| Ускоряется не то колесо | Проверь знак ошибки по монитору порта и формулы скоростей |
Приёмка требования 3 на подставке.
- Временно выведи в монитор порта ошибку, поправку и обе скорости колёс.
- Подводи линию под левый датчик, правый и посередине. Совпадает ли направление поправки с твоим правилом знака из требования 2?
- Найди положение линии, при котором одно колесо упирается в 0 или 255. Сравни скорости в мониторе с тем, что ты посчитал в таблице требования 3.
- Отключи измерения (нули) — колёса стоят? Верни измерения.
Направление поправки, упор в границу и защита проверены по монитору порта. Расхождения с таблицей объяснены.
Подбери коэффициент
18 минКоэффициент kp подбирают на дорожке. Меняй одно число за раз и смотри, что изменилось.
- Поставь робота на дорожку с дугой. Попробуй
kp= 0.2, 0.5, 0.8 при одной скорости. - Запиши в журнал, как робот ведёт себя при каждом значении.
- Выбери лучший
kpи попробуй увеличитьbaseSpeed.
kp | baseSpeed | Что видно |
|---|---|---|
| 0.2 | 100 | |
| 0.5 | 100 | |
| 0.8 | 100 |
Как это устроено: Как понять, куда менять kp
- Робот плавно уходит с дуги наружу — поправка слишком слабая, увеличь
kp. - Робот виляет из стороны в сторону даже на прямой — поправка слишком сильная, уменьши
kp. - Хороший
kp— робот держится линии на дуге и почти не виляет на прямой.
Подобраны kp и baseSpeed, при которых робот проходит дугу. Числа записаны в журнал.
Подбери kp сам. Правило одно: меняй одно число за раз.
- Начни с
kp = 0.5приbaseSpeed = 100. Затем попробуй одно явно меньшее и одно явно большее значение и сузь диапазон. - Запиши в журнал не меньше четырёх попыток.
kp | baseSpeed | Что видно | Что поменять дальше и почему |
|---|---|---|---|
| 0.5 | 100 | ||
| 100 | |||
| 100 | |||
| 100 |
- С лучшим
kpподнимиbaseSpeed. Что пришлось изменить, чтобы робот снова держал дугу?
Подсказка: Как понять, куда менять kp
Робот уходит с дуги наружу — поправка слабая. Робот виляет даже на прямой — поправка сильная.
В журнале четыре попытки с объяснением каждого шага и пара kp — baseSpeed, на которой робот проходит дугу.
Подбери kp и скорость методом, а не наугад, и докажи выбор записями.
- При
baseSpeed = 100начни сkp = 0.1и удваивай: 0.1, 0.2, 0.4, 0.8… Найди наименьшийkp, при котором робот начинает вилять на прямой. - Рабочий
kpвозьми примерно вдвое меньше. Проверь его на дуге своего уровня. - Поднимай
baseSpeedшагами по 20. Для каждой скорости — три заезда. Запомни последнюю скорость, на которой робот прошёл все три. - Запиши в журнал таблицу и вывод: почему именно эта пара, что ограничивает скорость.
- Вернись к правилу, которое ты придумал в первом шаге. Чем оно похоже на П-регулятор и чем отличается? Что бы ты в нём изменил теперь?
kp | baseSpeed | Заезды (из 3) | Что видно |
|---|---|---|---|
Подсказка: Робот виляет при любом kp
Проверь, не упирается ли колесо в 255 или 0 на дуге — вспомни последнюю строку таблицы из требования 3.
Таблица с методом подбора, пара kp — baseSpeed, проверенная тремя заездами, вывод с обоснованием и сравнение своего правила с П-регулятором.
Задания по уровням
10 минПройти дугу радиусом 350 мм, как на начальном поле итоговой задачи, не теряя линию.
Пройти дугу радиусом 250 мм, как на среднем поле, на скорости не ниже той, что подобрана для прямой. Если не получается — что важнее увеличить: kp или уменьшить скорость?
Пройти две дуги радиусом 125 мм подряд в разные стороны, как на продвинутом поле. Начни с пары kp — скорость, выбранной в шаге подбора. Если она не удерживает S-связку, измени один параметр и дополни существующую таблицу результатом; заново переписывать удачные и неудачные попытки не нужно.
Проверь себя
Выберите ответы для подготовки. Они не сохраняются и не оцениваются на сайте. Пройдите проверку знаний в программе курса или обсудите ответы с преподавателем.
● Основное
★ Средний уровень
★★ Продвинутый уровень
Отвечай на вопросы своего уровня. Нужно верных ответов: Начальный — 4 из 5; Средний — 4 из 5; Продвинутый — 4 из 5. Ответы проверит преподаватель или программа курса.
Итог
Робот поворачивает пропорционально ошибке — это П-регулятор. Он плавнее правила «стоп — едет» и проходит крутые дуги. Дальше робот научится находить перекрёстки на линии.
Как подтвердить результат
Для каждого опыта сохраняй условия, версию программы, наблюдение и вывод. Сравнивая попытки, меняй одну причину за раз; неудачные попытки тоже остаются в журнале. Отметка результата относится к уже выполненным действиям, повторять их ради отметки не нужно. Если результат не получен, запиши расхождение и следующую проверяемую гипотезу. Модель, измерения настоящего робота и объяснение — разные виды доказательств.
Словарь урока
| ошибка | Насколько линия ушла от середины: разница показаний левого и правого датчиков. |
| поправка | На сколько изменить скорости колёс, чтобы вернуться к линии. |
| коэффициент | Число kp: во сколько раз поправка больше или меньше ошибки. |
| П-регулятор | Правило «поправка пропорциональна ошибке»: больше ушли — сильнее поворачиваем. |
| constrain | Команда, которая не даёт числу выйти за границы, например за 0…255. |