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

П-регулятор: плавно и быстро

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

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

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

Робот плавно едет по линии с П-регулятором и проходит дугу своего уровня. Коэффициент и скорость подобраны и записаны в журнал.

Ты научишься

  • Перевести показания двух датчиков в общую шкалу от 0 до 100.
  • Посчитать ошибку как разницу показаний и объяснить её знак.
  • Объяснить, как поправка меняет скорости колёс.
  • Подобрать коэффициент и скорость по наблюдениям на дорожке.

Нужно знать

  • 3.1 Датчики линии и первое следование
  • 2.1 Моторы: едем и останавливаемся

Понадобится

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

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

В уроке 3.1 робот ехал по линии рывками: колесо либо крутилось, либо стояло. На крутых дугах итоговой задачи так не проехать. Сегодня робот научится поворачивать чуть-чуть, когда линия ушла немного, и сильно, когда ушла далеко. Так работают автопилоты и роботы-курьеры.

Разминка · вспомни
  1. Как программа из урока 3.1 решала, что под датчиком линия?
  2. Почему робот в уроке 3.1 ехал рывками?
  3. Что делает функция drive()?
1
Занятие 1 · 45 мин
Ошибка вместо «да или нет»
1

Плавно — это как?

8 мин

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

Что используем и как это работает. Сначала переводим измерения каждого датчика в общую шкалу по его белому и чёрному. Разность уровней называется ошибкой. Коэффициент kp умножает ошибку: чем больше отклонение, тем больше поправка. Одному колесу прибавляем её к базовой команде, у другого вычитаем. Новое измерение позволяет снова скорректировать движение — это обратная связь.

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

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

  1. Встань с напарником: один — «робот», второй держит линию (шнур или полоску бумаги) и плавно смещает её влево и вправо.
  2. «Робот» поворачивает корпус тем сильнее, чем дальше линия от середины.

Если ты один: положи на стол изогнутый шнур и веди вдоль него ладонью — это «робот». Поворачивай ладонь тем сильнее, чем дальше шнур ушёл от её середины.

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

Ты можешь объяснить: сила поворота зависит от того, насколько далеко ушла линия.

Представь, что ты ведёшь велосипед по линии: чуть ушла — чуть поворачиваешь руль, сильно ушла — сильнее. Программа из урока 3.1 так не умеет: она знает только «линия есть» или «нет».

  1. Запусти программу из урока 3.1 на дуге 350 мм и посмотри, как едет робот.
  2. Запиши в журнал одной фразой: чего не хватает программе, чтобы поворачивать плавно?
Должно получиться

Ты можешь объяснить: сила поворота должна зависеть от того, насколько далеко ушла линия.

Исследуй проблему, прежде чем решать.

  1. Запусти свою программу из урока 3.1 на дуге 350 мм, потом на дуге 125 мм. Запиши, как едет робот: рывки, виляние, сход с линии.
  2. Объясни в журнале, какую информацию с датчиков программа 3.1 выбрасывает и почему из-за этого робот едет рывками.
  3. До того как читать дальше, придумай своё правило: как скорости колёс должны зависеть от показаний датчиков. Запиши словами или формулой. В конце урока сравни его с П-регулятором.
Подсказка: Если не знаешь, с чего начать

Программа 3.1 сравнивает показание с порогом и получает «да или нет». А что, если использовать само число?

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

В журнале — наблюдение на двух дугах, объяснение причины рывков и твоё правило управления.

2

Общая шкала для двух датчиков

15 мин

У каждого датчика свои числа белого и чёрного. Чтобы сравнивать их, переведём оба в одну шкалу: чёрное — 0, белое — 100, край линии — где-то посередине.

  1. Создай скетч L32_Error. Перенеси из журнала свои измерения белого и чёрного.
Шкала и ошибкаL32_Error.ino.ino
// Наблюдение в 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);  }}
  1. Загрузи, открой монитор порта. Проверь: на белом оба числа около 100, на чёрном — около 0.
  2. Медленно проведи край линии под левым датчиком. Как меняется число?
Должно получиться

Оба датчика показывают около 100 на белом и около 0 на чёрном, на краю линии — промежуточные числа.

Как это устроено: Как работает функция level

value - black — насколько показание отличается от чёрного. white - black — весь путь от чёрного до белого. Их отношение, умноженное на 100, — «сколько процентов пути до белого». Формула работает, даже если у датчика чёрное — большое число, а белое — маленькое. constrain(…, 0, 100) обрезает случайные выбросы за границы шкалы.

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

  1. Создай скетч L32_Error по шаблону, перенеси измерения из журнала.
  2. Напиши функцию level сам. Ошибку в loop() пока не трогай.
Шаблон: шкала и ошибкаL32_Error_Gaps.ino.ino
// Наблюдение в 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? Посчитай до запуска.

  1. Загрузи и проверь на белом, чёрном и краю линии.
  2. Подними робота на 3–4 см над столом: датчику почти нечего отразить. Какое «сырое» число получается, что показывает шкала и какая часть функции за это отвечает?
  3. Ответь в журнале: какого типа число ты взял для расчёта и почему не обычный 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?

Возьми каркас: в нём только контакты, управление моторами и защита. Всё остальное напишешь сам.

Каркас П-регулятораL32_P_Line_Skeleton.ino.ino
// Наблюдение в 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, перенеси функцию в свою программу и проверь шкалу обоих датчиков на дорожке.

Тест функции levelL32_Level_Test.ino.ino
// Проверка функции 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)
  1. Насколько показание ушло от чёрного?
  2. Раздели на длину всего пути и умножь на 100.
  3. Обрежь результат границами шкалы.
Подсказка: Фрагмент кода (3 из 3)

long result = (long)(value - black) * 100 / (white - black); — почему здесь long и почему умножение стоит раньше деления?

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

Тест показывает «Все проверки пройдены». На дорожке шкала показывает около 0 на чёрном и около 100 на белом.

3

Считаем ошибку

15 мин

Ошибка — разница показаний: error = leftLevel - rightLevel. Когда линия ровно посередине, оба датчика видят одинаково и ошибка равна нулю.

Предскажи

Линия ушла под левый датчик. Какой знак будет у ошибки — плюс или минус? А если под правый?

  1. Посмотри на строку с ошибкой в мониторе порта.
  2. Подвинь линию под левый датчик, потом под правый. Запиши в журнал знак и примерные числа.
Где линияleftLevelrightLevelОшибка
Посередине
Под левым
Под правым
Должно получиться

Линия слева — ошибка отрицательная, справа — положительная, посередине — около нуля.

Ошибка показывает, насколько и в какую сторону линия ушла от середины. Считаем её как разницу: левое показание минус правое.

  1. Напиши расчёт ошибки в loop() сам и загрузи скетч.
Предскажи

Линия ушла под левый датчик. Какой знак будет у ошибки? Почему?

  1. Проверь прогноз: подвинь линию под левый датчик, потом под правый. Запиши в журнал.
Где линияleftLevelrightLevelОшибка
Посередине
Под левым
Под правым
  1. Ответь в журнале: в какую сторону должен повернуть робот, если ошибка отрицательная?
Подсказка: Знак ошибки

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

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

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

Требование 2. Программа считает ошибку — число со знаком: насколько и в какую сторону линия ушла от середины. Знак выбираешь ты, но потом поправка должна поворачивать робота к линии.

  1. Добавь расчёт ошибки и временный вывод leftLevel, rightLevel, ошибки в монитор порта.
  2. Заполни таблицу и запиши в журнал своё правило знака одной фразой.
Где линияleftLevelrightLevelОшибка
Посередине
Под левым
Под правым
  1. Разберись с краем: линия ушла за пределы обоих датчиков. Что покажет ошибка и что тогда сделает робот? Запиши, чем это опасно на крутой дуге.
Подсказка: Идея (1 из 2)

Если линия ровно посередине, оба датчика видят одно и то же — ошибка должна быть ноль.

Подсказка: Проверка знака (2 из 2)

Выбери: left - right или right - left. Запиши, какое колесо должно ускориться, когда линия слева, — это понадобится в требовании 3.

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

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

2
Занятие 2 · 45 мин
П-регулятор на дорожке
4

От ошибки к скоростям

10 мин

Поправка — ошибка, умноженная на коэффициент: correction = kp * error. К левому колесу поправку прибавляем, у правого — отнимаем.

  1. Создай скетч L32_P_Line, перенеси измерения.
П-регуляторL32_P_Line.ino.ino
// Наблюдение в 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);  }}
  1. Посчитай на бумаге: baseSpeed = 100, kp = 0.5, ошибка −40. Какие скорости получат колёса?
Должно получиться

Ты посчитал: поправка −20, левое колесо 80, правое 120 — робот поворачивает налево, к линии.

Как это устроено: Зачем constrain

При большой ошибке скорость может получиться −30 или 300. Мотор понимает только 0…255. constrain(число, 0, 255) ставит границы: меньше нуля станет 0, больше 255 — 255.

Правило П-регулятора: поправка пропорциональна ошибке. Чем дальше ушла линия, тем сильнее одно колесо ускоряется, а другое замедляется. Насколько сильно — задаёт kp.

  1. Создай скетч L32_P_Line по шаблону, перенеси измерения и свою функцию level.
  2. Напиши loop() сам: ошибка, поправка, скорости колёс в пределах 0…255 и команда моторам.
Шаблон: П-регуляторL32_P_Line_Gaps.ino.ino
// Наблюдение в 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);  }}
  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 остаётся).

Приёмка на бумаге. Посчитай скорости колёс для своей программы:

baseSpeedkpОшибкаЛевоеПравое
1000.5−40
2000.860
2001.0100

В последней строке одно колесо упирается в границу. Насколько слабее робот повернёт, чем «хотел»? Запиши в журнал, что можно с этим сделать.

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

Одно колесо получает базовую скорость плюс поправку, другое — минус поправку.

Подсказка: План (2 из 3)
  1. Ошибка из требования 2.
  2. Поправка = коэффициент × ошибка.
  3. Левое и правое колесо: база ± поправка, каждое — в границах 0…255.
  4. Отправить скорости на моторы.
Подсказка: Фрагмент кода (3 из 3)

int leftSpeed = constrain(baseSpeed + correction, 0, 255); — а что для правого? Проверь знак по своему правилу из требования 2.

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

Программа выполняет требование 3, таблица посчитана, ограничение записано в журнал.

5

Проверь на подставке

7 мин
  1. Поставь робота на подставку, загрузи программу, включи питание моторов.
  2. Подведи линию под левый датчик: левое колесо замедлится, правое ускорится.
  3. Подведи под правый — наоборот. Линия посередине — колёса крутятся одинаково.
Должно получиться

Колёса меняют скорость плавно, а не «стоп — едет».

Если не получилось
Что видишьЧто сделать
Колёса не крутятсяВписаны ли измерения? Включено ли питание моторов?
Ускоряется не то колесоПроверь знак ошибки по монитору порта и формулы скоростей
  1. Поставь робота на подставку, загрузи программу, включи питание моторов.
  2. Подведи линию под левый датчик, потом под правый, потом посередине. Запиши, какое колесо ускоряется и какое замедляется.
  3. Проверь свой прогноз про kp = 0: поставь kp = 0, загрузи и подвигай линию. Что делают колёса? Верни свой kp.
Где линияЛевое колесоПравое колесоСовпало с прогнозом?
Под левым
Под правым
Посередине
Должно получиться

Колёса меняют скорость плавно. Таблица заполнена, прогноз про kp = 0 проверен.

Если не получилось
Что видишьЧто сделать
Колёса не крутятсяНаписан ли loop() с командой моторам? Вписаны ли измерения?
Ускоряется не то колесоПроверь знак ошибки по монитору порта и формулы скоростей

Приёмка требования 3 на подставке.

  1. Временно выведи в монитор порта ошибку, поправку и обе скорости колёс.
  2. Подводи линию под левый датчик, правый и посередине. Совпадает ли направление поправки с твоим правилом знака из требования 2?
  3. Найди положение линии, при котором одно колесо упирается в 0 или 255. Сравни скорости в мониторе с тем, что ты посчитал в таблице требования 3.
  4. Отключи измерения (нули) — колёса стоят? Верни измерения.
Должно получиться

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

6

Подбери коэффициент

18 мин

Коэффициент kp подбирают на дорожке. Меняй одно число за раз и смотри, что изменилось.

  1. Поставь робота на дорожку с дугой. Попробуй kp = 0.2, 0.5, 0.8 при одной скорости.
  2. Запиши в журнал, как робот ведёт себя при каждом значении.
  3. Выбери лучший kp и попробуй увеличить baseSpeed.
kpbaseSpeedЧто видно
0.2100
0.5100
0.8100
Как это устроено: Как понять, куда менять kp
  • Робот плавно уходит с дуги наружу — поправка слишком слабая, увеличь kp.
  • Робот виляет из стороны в сторону даже на прямой — поправка слишком сильная, уменьши kp.
  • Хороший kp — робот держится линии на дуге и почти не виляет на прямой.
Должно получиться

Подобраны kp и baseSpeed, при которых робот проходит дугу. Числа записаны в журнал.

Подбери kp сам. Правило одно: меняй одно число за раз.

  1. Начни с kp = 0.5 при baseSpeed = 100. Затем попробуй одно явно меньшее и одно явно большее значение и сузь диапазон.
  2. Запиши в журнал не меньше четырёх попыток.
kpbaseSpeedЧто видноЧто поменять дальше и почему
0.5100
100
100
100
  1. С лучшим kp подними baseSpeed. Что пришлось изменить, чтобы робот снова держал дугу?
Подсказка: Как понять, куда менять kp

Робот уходит с дуги наружу — поправка слабая. Робот виляет даже на прямой — поправка сильная.

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

В журнале четыре попытки с объяснением каждого шага и пара kp — baseSpeed, на которой робот проходит дугу.

Подбери kp и скорость методом, а не наугад, и докажи выбор записями.

  1. При baseSpeed = 100 начни с kp = 0.1 и удваивай: 0.1, 0.2, 0.4, 0.8… Найди наименьший kp, при котором робот начинает вилять на прямой.
  2. Рабочий kp возьми примерно вдвое меньше. Проверь его на дуге своего уровня.
  3. Поднимай baseSpeed шагами по 20. Для каждой скорости — три заезда. Запомни последнюю скорость, на которой робот прошёл все три.
  4. Запиши в журнал таблицу и вывод: почему именно эта пара, что ограничивает скорость.
  5. Вернись к правилу, которое ты придумал в первом шаге. Чем оно похоже на П-регулятор и чем отличается? Что бы ты в нём изменил теперь?
kpbaseSpeedЗаезды (из 3)Что видно
Подсказка: Робот виляет при любом kp

Проверь, не упирается ли колесо в 255 или 0 на дуге — вспомни последнюю строку таблицы из требования 3.

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

Таблица с методом подбора, пара kp — baseSpeed, проверенная тремя заездами, вывод с обоснованием и сравнение своего правила с П-регулятором.

7

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

10 мин
Основное · Дуга начального поля

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

Средний уровень · Дуга на скорости

Пройти дугу радиусом 250 мм, как на среднем поле, на скорости не ниже той, что подобрана для прямой. Если не получается — что важнее увеличить: kp или уменьшить скорость?

Продвинутый уровень · Крутая S-связка

Пройти две дуги радиусом 125 мм подряд в разные стороны, как на продвинутом поле. Начни с пары kp — скорость, выбранной в шаге подбора. Если она не удерживает S-связку, измени один параметр и дополни существующую таблицу результатом; заново переписывать удачные и неудачные попытки не нужно.

Проверь себя

Проверь себя

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

● Основное

1.Левый датчик показывает 20, правый — 100. Чему равна ошибка error = leftLevel - rightLevel?
2.baseSpeed = 100, kp = 0.5, ошибка = 40. Какие скорости у колёс?
3.Робот сильно виляет даже на прямой. Что сделать с kp?
4.Зачем переводить показания датчиков в шкалу 0–100?
5.Что делает constrain(baseSpeed + correction, 0, 255)?

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

1.На дуге радиусом 250 мм робот на скорости съезжает наружу. Что попробовать?
2.Почему на дуге меньшего радиуса нужна более сильная поправка?
3.Линия ушла под левый датчик, ошибка error = leftLevel - rightLevel отрицательная. Какое колесо должно ускориться, чтобы робот вернулся к линии?
4.Почему в функции level результат считают в long, а не в int?
5.Команда поменяла сразу kp и baseSpeed, и робот стал проходить дугу. Что не так с этим опытом?

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

1.На чёрном и у края линии шкала верна, но для показания 50 при white = 120 и black = 780 функция возвращает неверное число. В чём ошибка?
int level(int value, int white, int black) {  int result = (value - black) * 100 / (white - black);  return constrain(result, 0, 100);}
2.Что вернёт эта функция для показания 450 при white = 120 и black = 780?
int level(int value, int white, int black) {  long result = (long)(value - black) / (white - black) * 100;  return constrain(result, 0, 100);}
3.Ошибку посчитали как rightLevel - leftLevel, а поправку по-прежнему прибавляют к левому колесу. Линия ушла под левый датчик. Что сделает робот?
4.На крутой дуге линия ушла за пределы обоих датчиков. Что сделает П-регулятор из урока?
5.baseSpeed = 200, kp = 1.0, ошибка = 100, скорости ограничены 0…255. Что будет с поворотом?

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

Итог

Робот поворачивает пропорционально ошибке — это П-регулятор. Он плавнее правила «стоп — едет» и проходит крутые дуги. Дальше робот научится находить перекрёстки на линии.

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

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

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

ошибкаНасколько линия ушла от середины: разница показаний левого и правого датчиков.
поправкаНа сколько изменить скорости колёс, чтобы вернуться к линии.
коэффициентЧисло kp: во сколько раз поправка больше или меньше ошибки.
П-регуляторПравило «поправка пропорциональна ошибке»: больше ушли — сильнее поворачиваем.
constrainКоманда, которая не даёт числу выйти за границы, например за 0…255.