Robot courier Lesson Ф.2 All coursesFor teachersStudy in the app Student Arduino Uno + L298P motor shield
Topic 7 · Lesson F.2

Controllers: on/off, P, PD and PID on the same track

The robot itself measures how accurately it drives, and the team uses the numbers to figure out which controller is better.

2 × 45 min1–2 students per robotPractice

Lesson result

The team compared on/off, P, PD and PID controllers on the same track and speed in terms of average error, largest error and number of line transitions and explained the result. Not needed for the final task.

You will learn

  • Make a fair comparison: one track, one speed, one change at a time, three runs.
  • Explain how a on/off controller differs from a P-controller.
  • Explain what the D and I parts of the controller react to.
  • Make a judgment about controllers based on measurements rather than impressions.

Need to know

  • 3.1 Line sensors and first follow
  • 3.2 P-controller: smooth and fast

Will need

Line sensor (analog, Trema) × 2USB cable for Arduino

Why this lesson

In the course, the robot drove along a line with two controllers: a on/off in lesson 3.1 (“the wheel moves or stands”) and a P-controller in lesson 3.2 (“the further the line, the stronger the turn”). Engineers use two more: PD and PID. Which of the four is better? Today we will answer not “by eye”, but by numbers: the robot itself will measure how accurately it drove, and the team will compare all four controllers on one track and one speed.

The elective is not needed for the final task. It is needed by those who want to understand how real autopilots work and test it with experience.

Warm up · remember
  1. What does the wheel in Lesson 3.1 do when there is a line under its sensor?
  2. How were errors and corrections counted in Lesson 3.2?
  3. Why when selecting kp changed one number at a time?
1
Lesson 1 · 45 min
We measure, not by eye
1

How to compare honestly

8 min

What should a robot do. The robot needs to hold the line at different deviations. Let's compare four ways to select commands for wheels on the same sensors and motors to understand what exactly the algorithm changes.

What we use and how it works. The on/off rule selects one of the predefined actions. The P-component reacts to the current error, D to the change in error between steps, and I to the accumulated error. In this sketch, the step is fixed, so the coefficients are adjusted for its duration. Accumulation is limited so that a large past error does not overcorrect upon returning to the line.

How to link a dimension to an action. Run tests with the same base motor command on the same field, and compare deviations and oscillations. A larger gain does not mean a better result. Checking the formula with numbers helps find coding errors but does not replace testing on the track; the test gains are not settings for your robot.

To compare controllers, everything else should be the same: one track, one starting place, one speed, same white and black measurements. Only the controller changes. Every controller goes three times: one run may be random.

  1. Select the comparison route: straight section and 250 mm arc from lesson 3.2. Mark the starting point with a pencil.
  2. Redraw the table in your journal - you will fill it out by the end of the lesson.
ControllerCheck-inAverage errorBiggest errorLine crossingsLost line?
On/off1, 2, 3
P1, 2, 3
PD1, 2, 3
PID1, 2, 3
Predict

Which controller do you think will pass the most accurately, and which one will do the worst? Write it down in a journal.

It should work

The route and start are marked, the table is redrawn, the forecast is recorded.

A comparison is fair only if it changes one: controller. The track, start, speed, measurements and length of the race are the same. Each controller has three runs.

  1. Select the comparison route: straight section and 250 mm arc from lesson 3.2. Mark the start.
  2. Write in your journal a list of things that should remain the same across all runs. Find at least five points.
  3. Redraw the comparison table.
ControllerCheck-inAverage errorBiggest errorLine crossingsLost line?
On/off1, 2, 3
P1, 2, 3
PD1, 2, 3
PID1, 2, 3
Predict

Rank the four controllers from best to worst based on average error and explain the order in one sentence.

It should work

A list of identical conditions, a table and a forecast with an explanation are in the logbook.

Design the experience first, then write the code.

  1. Select the comparison route: straight section and 250 mm arc from lesson 3.2. Mark the start.
  2. Write down the plan of the experiment in your journal: - what changes (one thing); - what remains the same (at least five points); - what we measure and why these numbers are enough to name the best controller; - how many runs we do and what number we take out of three, if one run is very different.
  3. Write down the prediction: the order of the four controllers based on the average error.
Hint: Which number to take from three runs

If one race out of three is very different, the arithmetic average will “drag” it along with it. There is a number that does not react to one random arrival.

It should work

In the journal there is an experimental plan and forecast. You know which number of the three races will go into the table.

2

The robot measures itself

15 min

The program guides the robot for a specified time RUN_MS, and then stops and saves the results of the race into the Arduino memory. The memory is not erased when you turn off the power. At the beginning of each launch, the program prints the results to the Serial Monitor past check-in.

What does the robot type?What does it mean
Average errorHow far on average the line moved away from the middle. Less is more accurate
Biggest errorThe strongest departure from the line during the race
Line crossingsHow many times did the line jump from one side to the other: wobble
  1. Create a sketch LF2_Compare, transfer the measurements from the log and your kp from lesson 3.2.
Four controllers and arrival measurementLF2_Compare.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>struct Result; // Тип итогов до прототипов, которые добавляет Arduino.#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif #include <EEPROM.h> const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; // Регулятор заезда: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД.const int MODE = 1;int baseSpeed = 100; // одна скорость для всех регуляторовfloat kp = 0.5;      // твой kp из урока 3.2float kd = 0;        // Д: реакция на то, как быстро уходит линияfloat ki = 0;        // И: реакция на ошибку, которая копитсяconst long SUM_LIMIT = 3000;       // предел накопленной ошибкиconst unsigned long RUN_MS = 6000; // длина заезда: робот должен остановиться до конца дорожкиconst unsigned long STEP_MS = 10;  // шаг регулятора: 100 раз в секунду int lastError = 0;long errorSum = 0; // Итоги заезда: хранятся в памяти Arduino и после выключения.struct Result {  int marker;  int mode;  int baseSpeed;  float kp;  float kd;  float ki;  long steps;  long sumAbs;  int maxAbs;  int crossings;}; // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} // Поправка П-, ПД- или ПИД-регулятора по ошибке этого шага.int controller(int mode, int error) {  float correction = kp * error;  if (mode >= 2) {    correction += kd * (error - lastError);  }  if (mode == 3) {    errorSum = constrain(errorSum + error, -SUM_LIMIT, SUM_LIMIT);    correction += ki * errorSum;  }  lastError = error;  return correction;} // ===== Измерение заезда ===== const int MARKER = 2610; // признак того, что в памяти записан заездResult run;int lastSide = 0;unsigned long startTime;unsigned long nextStep; // Добавляет ошибку шага к итогам заезда.void record(int error) {  run.steps++;  run.sumAbs += abs(error);  if (abs(error) > run.maxAbs) {    run.maxAbs = abs(error);  }  int side = 0;  if (error > 10) {    side = 1;  }  if (error < -10) {    side = -1;  }  if (side != 0 && lastSide != 0 && side != lastSide) {    run.crossings++;  }  if (side != 0) {    lastSide = side;  }} void printResult(Result r) {  const char *names[] = {"релейный", "П", "ПД", "ПИД"};  Serial.print("Регулятор: ");  Serial.print(names[r.mode]);  Serial.print("  скорость=");  Serial.print(r.baseSpeed);  Serial.print("  kp=");  Serial.print(r.kp);  Serial.print("  kd=");  Serial.print(r.kd);  Serial.print("  ki=");  Serial.println(r.ki, 3);  Serial.print("Средняя ошибка: ");  Serial.println(r.steps > 0 ? (float)r.sumAbs / r.steps : 0, 1);  Serial.print("Наибольшая ошибка: ");  Serial.println(r.maxAbs);  Serial.print("Переходов через линию: ");  Serial.println(r.crossings);} // Ждёт, пока робот стоит на старте: оба датчика видят белое 1 секунду подряд.void waitForStart() {  Serial.println("Поставь робота на старт: линия между датчиками");  unsigned long whiteSince = millis();  while (millis() - whiteSince < 1000) {    courseTelemetryPoll();    int leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);    int rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);    if (leftLevel < 70 || rightLevel < 70) {      whiteSince = millis();    }  }} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   Result last;  EEPROM.get(0, last);  if (last.marker == MARKER && last.mode >= 0 && last.mode <= 3) {    Serial.println("Прошлый заезд:");    printResult(last);  } else {    Serial.println("Записанных заездов нет");  }   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }  waitForStart();  courseWait(2000); // время убрать руки от робота   run = {MARKER, MODE, baseSpeed, kp, kd, ki, 0, 0, 0, 0};  startTime = millis();  nextStep = startTime;} void loop() {  if (millis() - startTime >= RUN_MS) {    drive(FORWARD, 0, FORWARD, 0);    EEPROM.put(0, run);    Serial.println("Заезд окончен:");    printResult(run);    courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN), NAN, NAN);    while (true) {      courseTelemetryPoll();    }  }  while (millis() < nextStep) {    courseTelemetryPoll();  }  nextStep += STEP_MS;   int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);  int error = leftLevel - rightLevel;  record(error);   float observedOutput = NAN; // у релейного алгоритма нет пропорциональной поправки  int leftSpeed;  int rightSpeed;  if (MODE == 0) {    // Релейный, как в уроке 3.1: колесо над линией стоит.    leftSpeed = leftLevel < 50 ? 0 : baseSpeed;    rightSpeed = rightLevel < 50 ? 0 : baseSpeed;  } else {    int correction = controller(MODE, error);    observedOutput = correction;    leftSpeed = constrain(baseSpeed + correction, 0, 255);    rightSpeed = constrain(baseSpeed - correction, 0, 255);  }  drive(FORWARD, leftSpeed, FORWARD, rightSpeed);  courseTelemetryReport(leftRaw, rightRaw, error, observedOutput);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}
  1. Put MODE = 1 (P-controller), load the program.
  2. Put the robot at the start: the line between the sensors. In a second, the program will see the start, and after another two, the robot will move.
  3. The robot must stop to the end of the track. If it reaches the edge, reduce it RUN_MS. Picked up - more RUN_MS don't change it.
Security

Connect the cable while holding the robot in your hand or on the stand. If you put it on a white table, the program will take the table as the start and the robot will move.

  1. Take the robot in your hands, connect the cable and open the Serial Monitor: the results of the last race will appear.
It should work

RUN_MS is selected, the robot stops to the end of the track, the results of the race are visible in the Serial Monitor.

How it works: Why the robot is waiting to start

When you open the Serial Monitor, the Arduino restarts. If the robot were to drive right away, it would drive away from the table and record the “race” over the real one. Therefore, the program waits until both sensors see white for a full second: this happens when the robot is at the start.

The program guides the robot for a specified time RUN_MS, stops it and saves the results of the race into memory, which is not erased without power. The next time it is started, the totals are printed to the Serial Monitor. The arrival measurement in the template is ready. Your part is a function controller: today it has a P-controller, in the second lesson you will add D and I.

  1. Create a sketch LF2_Compare according to the template, transfer the measurements and your kp.
  2. Write to controller only P-part - as in lesson 3.2.
Template: controller functionLF2_Compare_Gaps.ino.ino
// Наблюдение в MyRobotica включается сборщиком; обычный Arduino работает без библиотеки.#include <math.h>struct Result; // Тип итогов до прототипов, которые добавляет Arduino.#if defined(ROBOT_TELEMETRY_BUILD_ID)#include <RobotTelemetry.h>robot_telemetry::TelemetryStream courseTelemetry;#endif // Шаблон среднего уровня. Напиши код в разделе «Твоя часть» по целям в комментариях. #include <EEPROM.h> const int LEFT_SENSOR_PIN = A4;const int RIGHT_SENSOR_PIN = A5; // Измерения белого и чёрного из журнала (урок 3.1).const int LEFT_WHITE = 0;const int LEFT_BLACK = 0;const int RIGHT_WHITE = 0;const int RIGHT_BLACK = 0; const int LEFT_DIR_PIN = 7;const int LEFT_SPEED_PIN = 6;const int RIGHT_DIR_PIN = 4;const int RIGHT_SPEED_PIN = 5;const int FORWARD = HIGH; // Регулятор заезда: 0 — релейный, 1 — П, 2 — ПД, 3 — ПИД.const int MODE = 1;int baseSpeed = 100; // одна скорость для всех регуляторовfloat kp = 0.5;      // твой kp из урока 3.2float kd = 0;        // Д: реакция на то, как быстро уходит линияfloat ki = 0;        // И: реакция на ошибку, которая копитсяconst long SUM_LIMIT = 3000;       // предел накопленной ошибкиconst unsigned long RUN_MS = 6000; // длина заезда: робот должен остановиться до конца дорожкиconst unsigned long STEP_MS = 10;  // шаг регулятора: 100 раз в секунду int lastError = 0;long errorSum = 0; // Итоги заезда: хранятся в памяти Arduino и после выключения.struct Result {  int marker;  int mode;  int baseSpeed;  float kp;  float kd;  float ki;  long steps;  long sumAbs;  int maxAbs;  int crossings;}; // ===== Твоя часть: напиши сам ===== // Поправка П-, ПД- или ПИД-регулятора по ошибке этого шага.int controller(int mode, int error) {  // Занятие 1. П: поправка пропорциональна ошибке, коэффициент kp.  // Занятие 2. Д, режимы 2 и 3: прибавь kd, умноженный на то, насколько ошибка изменилась с прошлого шага.  // Занятие 2. И, только режим 3: копи ошибку в errorSum, не выпуская сумму за ±SUM_LIMIT, и прибавь ki × errorSum.  // В конце запомни ошибку этого шага в lastError: она нужна Д на следующем шаге.  return 0;} // ===== Готовое: шкала, моторы, измерение заезда и запуск — не меняй ===== // Переводит показание в шкалу: чёрное — 0, белое — 100.int level(int value, int white, int black) {  long result = (long)(value - black) * 100 / (white - black);  return constrain(result, 0, 100);} void drive(int leftDir, int leftSpeed, int rightDir, int rightSpeed) {  digitalWrite(LEFT_DIR_PIN, leftDir);  digitalWrite(RIGHT_DIR_PIN, rightDir);  analogWrite(LEFT_SPEED_PIN, leftSpeed);  analogWrite(RIGHT_SPEED_PIN, rightSpeed);} const int MARKER = 2610; // признак того, что в памяти записан заездResult run;int lastSide = 0;unsigned long startTime;unsigned long nextStep; // Добавляет ошибку шага к итогам заезда.void record(int error) {  run.steps++;  run.sumAbs += abs(error);  if (abs(error) > run.maxAbs) {    run.maxAbs = abs(error);  }  int side = 0;  if (error > 10) {    side = 1;  }  if (error < -10) {    side = -1;  }  if (side != 0 && lastSide != 0 && side != lastSide) {    run.crossings++;  }  if (side != 0) {    lastSide = side;  }} void printResult(Result r) {  const char *names[] = {"релейный", "П", "ПД", "ПИД"};  Serial.print("Регулятор: ");  Serial.print(names[r.mode]);  Serial.print("  скорость=");  Serial.print(r.baseSpeed);  Serial.print("  kp=");  Serial.print(r.kp);  Serial.print("  kd=");  Serial.print(r.kd);  Serial.print("  ki=");  Serial.println(r.ki, 3);  Serial.print("Средняя ошибка: ");  Serial.println(r.steps > 0 ? (float)r.sumAbs / r.steps : 0, 1);  Serial.print("Наибольшая ошибка: ");  Serial.println(r.maxAbs);  Serial.print("Переходов через линию: ");  Serial.println(r.crossings);} // Ждёт, пока робот стоит на старте: оба датчика видят белое 1 секунду подряд.void waitForStart() {  Serial.println("Поставь робота на старт: линия между датчиками");  unsigned long whiteSince = millis();  while (millis() - whiteSince < 1000) {    courseTelemetryPoll();    int leftLevel = level(analogRead(LEFT_SENSOR_PIN), LEFT_WHITE, LEFT_BLACK);    int rightLevel = level(analogRead(RIGHT_SENSOR_PIN), RIGHT_WHITE, RIGHT_BLACK);    if (leftLevel < 70 || rightLevel < 70) {      whiteSince = millis();    }  }} void setup() {  courseTelemetryBegin();  pinMode(LEFT_DIR_PIN, OUTPUT);  pinMode(LEFT_SPEED_PIN, OUTPUT);  pinMode(RIGHT_DIR_PIN, OUTPUT);  pinMode(RIGHT_SPEED_PIN, OUTPUT);  drive(FORWARD, 0, FORWARD, 0);   Result last;  EEPROM.get(0, last);  if (last.marker == MARKER && last.mode >= 0 && last.mode <= 3) {    Serial.println("Прошлый заезд:");    printResult(last);  } else {    Serial.println("Записанных заездов нет");  }   // Без измерений робот не поедет.  if (LEFT_WHITE == LEFT_BLACK || RIGHT_WHITE == RIGHT_BLACK) {    Serial.println("Впиши измерения белого и чёрного");    while (true) {      courseTelemetryPoll();    }  }  waitForStart();  courseWait(2000); // время убрать руки от робота   run = {MARKER, MODE, baseSpeed, kp, kd, ki, 0, 0, 0, 0};  startTime = millis();  nextStep = startTime;} void loop() {  if (millis() - startTime >= RUN_MS) {    drive(FORWARD, 0, FORWARD, 0);    EEPROM.put(0, run);    Serial.println("Заезд окончен:");    printResult(run);    courseTelemetryReport(analogRead(LEFT_SENSOR_PIN), analogRead(RIGHT_SENSOR_PIN), NAN, NAN);    while (true) {      courseTelemetryPoll();    }  }  while (millis() < nextStep) {    courseTelemetryPoll();  }  nextStep += STEP_MS;   int leftRaw = analogRead(LEFT_SENSOR_PIN);  int rightRaw = analogRead(RIGHT_SENSOR_PIN);  int leftLevel = level(leftRaw, LEFT_WHITE, LEFT_BLACK);  int rightLevel = level(rightRaw, RIGHT_WHITE, RIGHT_BLACK);  int error = leftLevel - rightLevel;  record(error);   float observedOutput = NAN; // у релейного алгоритма нет пропорциональной поправки  int leftSpeed;  int rightSpeed;  if (MODE == 0) {    // Релейный, как в уроке 3.1: колесо над линией стоит.    leftSpeed = leftLevel < 50 ? 0 : baseSpeed;    rightSpeed = rightLevel < 50 ? 0 : baseSpeed;  } else {    int correction = controller(MODE, error);    observedOutput = correction;    leftSpeed = constrain(baseSpeed + correction, 0, 255);    rightSpeed = constrain(baseSpeed - correction, 0, 255);  }  drive(FORWARD, leftSpeed, FORWARD, rightSpeed);  courseTelemetryReport(leftRaw, rightRaw, error, observedOutput);} // ===== Готовое: передача наблюдений — не меняй =====void courseTelemetryBegin() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.begin((1UL << Line0) | (1UL << Line1) |                        (1UL << LineError) | (1UL << ControlOutput));#else  Serial.begin(9600);#endif} void courseTelemetryPoll() {#if defined(ROBOT_TELEMETRY_BUILD_ID)  courseTelemetry.update();#endif} // NAN означает «ещё не вычислено», а не нулевую ошибку или поправку.void courseTelemetryReport(int leftRaw, int rightRaw, float error, float output) {#if defined(ROBOT_TELEMETRY_BUILD_ID)  using namespace robot_telemetry::wire;  courseTelemetry.setUnsigned(Line0, leftRaw);  courseTelemetry.setUnsigned(Line1, rightRaw);  if (isnan(error)) courseTelemetry.setMissing(LineError);  else courseTelemetry.setFloat(LineError, error);  if (isnan(output)) courseTelemetry.setMissing(ControlOutput);  else courseTelemetry.setFloat(ControlOutput, output);  courseTelemetry.update();#endif} void courseWait(unsigned long durationMs) {  unsigned long started = millis();  while (millis() - started < durationMs) {    courseTelemetryPoll();    delay(1);  }}
  1. Put MODE = 1, upload, set the robot to start. Pick up RUN_MS so that the robot stops until the end of the track, and do not change it again.
  2. Find the function in the finished part record and answer in the journal: when does she add a transition through the line and why is there a threshold of 10?
Security

Connect the cable while holding the robot in your hand or on a stand: on a white table, the program will take the table as the start.

Hint: The robot does not move from the start

The program waits until both sensors see white (more than 70 on the scale) for a full second. Look at the gauge scale at the start.

It should work

controller returns the P-correction, RUN_MS selected, in the logbook - the answer about record.

The frame below takes on the measurement: the results of the race are accumulated in record(error), saved to memory after RUN_MS and are printed the next time you start them. You can write the rest yourself.

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

Requirement 1. One constant MODE selects the controller: 0 - on/off, 1 - P, 2 - PD, 3 - PID. Everything else is the same in all modes.

  • On/off - according to the rule of lesson 3.1, but on a scale of 0...100: the wheel under the sensor of which the line is standing, the other is moving at speed baseSpeed.
  • P, PD and PID receive correction from the function controller(mode, error) and divide it between the wheels, as in lesson 3.2.
  • Each step the error is transmitted to record(error) - in all four modes.

Write today level, error, on/off mode and P-part controller. D and I - in the second lesson.

  1. Move level and the error from lesson 3.2.
  2. Complete requirement 1 for modes 0 and 1. Pick up RUN_MS so that the robot stops before the end of the track.
  3. Find in record threshold 10. Write down in the log what changes in the number of transitions if the threshold is removed, and why.
Security

Connect the cable while holding the robot in your hand or on a stand: on a white table, the program will take the table as the start.

Hint: The robot does not move from the start

waitForStart is waiting for white on the scale level. Bye level returns 0, the robot never starts - this is frame protection.

It should work

Modes 0 and 1 work, RUN_MS selected, in the journal there is an answer about the threshold.

3

On/off vs P

20 min
  1. Put MODE = 0 - on/off controller from lesson 3.1. Do three runs. After each, connect the cable and write the results in a table.
  2. Put MODE = 1 - P-controller. Three more races.
  3. Compare the lines. Which controller wiggled more? Which one has a smaller average error?
If it doesn't work
What do you seeWhat to do
“There are no recorded races” after the raceThe race did not reach the end RUN_MS: Power was turned off earlier. Repeat
The robot went straight from the tableYou connected the cable while the robot was standing on the white. Hold it in your hand
On/off leaves the arcThis is also a result: write “yes” in “Are you off the line?”
It should work

The table contains three runs of the on/off and P-controller. You can tell which one is more accurate and which number shows it.

  1. Make three runs with MODE = 0 and MODE = 1. The results are in the table.
  2. For each controller, choose from three average errors average in size - she will be compared.
  3. Answer in the journal: why does the on/off have more line transitions? Explain in terms of what speeds the wheels get.
Hint: Why is the average in size

If one run accidentally turns out bad, the arithmetic mean will change greatly, but the average will not.

It should work

The table for the two controllers is completed, the average values ​​are selected, and the explanation of the transitions is written down.

  1. Conduct the experiment according to your plan for modes 0 and 1.
  2. Compare not only the average errors, but also spread three runs: how repeatable is the result of each controller?
  3. Write down in the journal an explanation through the controller device: why the on/off one has more transitions and the largest error, although both see the same sensors.
It should work

The data of the two controllers, the spread and explanation are in the logbook. The prediction from the first step is verified for the first pair.

2
Lesson 2 · 45 min
PD, PID and grand total
4

PD: braking the turn in advance

14 min

The P-controller turns until the line returns to the middle, and often overshoots it: the robot wobbles. D-part looks, how fast error is changing. If the line quickly returns to the middle, the D-part eases the turn in advance - like a cyclist who begins to straighten the handlebars even before reaching the line.

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

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

  1. Calculate on paper: kp = 0.5, kd = 4, at the last step the error was 10, now it’s 25.
  2. Put MODE = 2 and try kd = 5, 10 and 20 - one run per track. Choose kd, in which the robot wobbles the least.
  3. With selected kd do three runs and write the results in the table.
It should work

You have calculated the correction: 12.5 + 60 = 72.5, the program will discard the fractional part - 72. Selected kd, there are three PD races in the table.

How it works: Why kd is so big

The controller counts 100 times per second. In one hundredth of a second the error changes by only a few units, so the multiplier kd needed more than kp.

The P-controller often overshoots the line: the correction does not weaken until the error becomes zero. D-part reacts to how quickly the error changes, and extinguishes the rotation in advance.

  1. Add part D to controller by target in the template.
Predict

kp = 0.5, kd = 4, at the last step the error was 10, now it’s 25. What correction will be returned controller for MODE = 2? Do the math before you launch.

  1. Put MODE = 2. Pick up kd: start with 2.5 and double it, one run at a time. Write down at what kd the robot began to twitch on a straight line, and take the previous value.
  2. With selected kd - three runs to the table.
Hint: Where can I get the previous error?

She's already lying in lastError, if you remember it at the end of the function.

Hint: The robot twitches even with low kd

Check the sign: The D-part is added, and the error change is “now minus before.”

It should work

The forecast was correct: 72. Part D is written, kd doubled, three runs recorded.

Requirement 2. Function int controller(int mode, int error):

  • mode 1 - P-part only;
  • modes 2 and 3 - plus D-part: kd, multiplied by the error change from the last step;
  • mode 3 - plus I-part: errors accumulate in errorSum, the amount does not exceed ±SUM_LIMIT, correction ki × errorSum; in other modes the amount does not accumulate;
  • past error is updated at every step in all modes.

Acceptance. Write controller in the test sketch, upload and open the Serial Monitor. When all the lines PASS, move the function to your program. Today the test also checks the I-part, which means that it also needs to be written.

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

Then pick up kd method: start with 2.5 and double until the first twitch on the straight line, take the previous value. Three races are in the table.

Hint: Idea (1 of 3)

The speed of line departure is the difference between the error now and the error at the previous step.

Hint: Plan (2 of 3)
  1. P-part.
  2. If mode 2 or 3, add the D-part.
  3. If mode 3 - update the amount, limit it, add the I-part.
  4. Remember the error.
Hint: Code snippet (3 of 3)

errorSum = constrain(errorSum + error, -SUM_LIMIT, SUM_LIMIT); - why does the “five steps of 100” test without this line give 175?

It should work

Test: “All tests passed.” kd was selected using the method, three PD runs were recorded.

5

PID: what does the accumulated error give

11 min

I-part adds up the mistakes step by step. If the line remains on one side for a long time - for example, on a long arc - the sum grows, and the robot turns more and more. To prevent the amount from growing indefinitely, it is limited by SUM_LIMIT.

  1. Leave yours kd, put MODE = 3 and try ki = 0.005, 0.01 and 0.02 - one run each.
  2. Choose the best ki and make three runs into the table.
Predict

Will PID be more accurate than PD on our route? Write down your forecast before your races.

It should work

There are three PID runs in the table. The forecast has been verified.

I-part accumulates an error. While the line is long on one side, the amount grows and intensifies the turn. Danger - the sum continues to pull in the same direction after the arc.

  1. Add the I-part to controller by target in the template.
  2. Put MODE = 3, pick up ki: 0.005, 0.01, 0.02. Three races with the best - in the table.
  3. Temporarily set SUM_LIMIT = 30000 and drive it once. What changed after the arc? Return 3000.
Tip: How not to exceed the limit

The same command that keeps the wheel speed at 0...255 is also suitable for the sum.

It should work

The I-part is written, ki selected, three runs recorded. Have you seen what a robot does without a limit on the amount.

  1. Put MODE = 3, pick up ki method: from 0.005, doubling. Three races with the best - in the table.
  2. Experience limit: one run with SUM_LIMIT = 30000. Describe what happens after the arc and explain it in terms of meaning errorSum.
  3. Answer in the journal: under what conditions does the I-part give a win and are they available on our route?
Hint: When is the I-part useful?

Think about a long error of one sign: a long arc of one radius, or a robot whose one wheel always spins slower than the other.

It should work

ki is selected, the experience is described to the limit, the conditions for the use of the I-part are written down.

6

Comparison of all four

12 min
  1. Write down for each controller the average error from three runs - the average in size.
  2. Rank the controls from best to worst.
PlaceControllerAverage errorLine crossings
1
2
3
4
  1. Compare with the forecast from the beginning of the lesson. What coincided and what didn’t?
It should work

Four controllers are in place, the forecast is checked.

  1. Make a summary table: controller, average error, maximum error, transitions, derailments.
  2. Name the best controller for this track and speed and justify it with two numbers.
  3. Answer: did the I-part justify its complication? Why?
  4. Which controller would you use for the final task and why?
It should work

The final table, reasoned conclusion and answers are in the logbook.

  1. Make a final table of all four controllers with a spread of three races.
  2. Write a conclusion in three parts: what the numbers showed, how this is explained in the design of the controllers, what could have been done dishonestly and how you ruled it out.
  3. Compare the result with the forecast and experiment plan from the first step.
It should work

The final table and conclusion from three parts are in the journal.

7

Quests by level

8 min
Main · All four controls

Fill out a table comparing all four controllers at the same speed, three runs each, and name the best one based on the average error.

Average level · PD is better than P

Pick up kd, in which the PD passes the route with a smaller average error than the P, and explain why by numbers.

Advanced · Ultimate Speed

Find for each of the four controllers the largest baseSpeed, on which he passes the track three times out of three: increase in steps of 20. Make a rating by speed and compare it with the rating by average error.

Test yourself

Test yourself

Select answers to prepare. They are not stored or evaluated on the site. Take the knowledge test in the course syllabus or discuss the answers with your instructor.

● Basic

1.Why are all controllers compared on the same route and the same speed?
2.What does the on/off controller from Lesson 3.1 do with the wheel with a line under the sensor?
3.How does a P-controller differ from a on/off controller?
4.What does the “average error” show in the race results?
5.What does the D-part of the controller react to?

★ Intermediate level

1.kp = 0.5, kd = 4. At the last step the error was 10, now it is 25. What correction will the PD controller return (the fractional part is discarded)?
2.Why does the PD wobble less when exiting the arc than the P?
3.Why limit the error sum errorSum to SUM_LIMIT?
4.Three PID runs gave an average error of 8, 9 and 21. What number should you compare?
5.Why does the controller count with a constant step STEP_MS?

★★ Advanced level

1.With this function, the PD behaves like a P with increased kp. Why?
int controller(int mode, int error) {  float correction = kp * error;  if (mode >= 2) {    correction += kd * (error - lastError);  }  return correction;}
2.The amount limit has been removed from the PID. After a long arc to the left, the robot on a straight line goes left for a long time beyond the line. Why?
3.Mean error and line crossings: on/off 30 and 40, P 14 and 6, PD 9 and 2, PID 10 and 3. Which conclusion is justified?
4.Why does the I-part hardly help on a track with arcs in different directions?
5.The program wrote correction -= kd * (error - lastError). What will happen to PD?

Answer questions at your level. Correct answers needed: Beginner - 4 out of 5; Average - 4 out of 5; Advanced - 4 out of 5. Answers will be checked by the teacher or course program.

Summary

The on/off controller knows only “yes or no”, the P-controller knows “how much”, the D-part knows “how fast”, the I-part knows “how long”. More parts does not mean better: measurements on your route and speed decide.

How to confirm the result

The result of the experiment relates the conditions, program version, observation and conclusion. Comparing attempts with one change helps verify the cause, including failed attempts. The mark refers to practice already completed; repetition for the sake of marking is not necessary. The discrepancy with the forecast sets the next hypothesis to be tested. A model, measurements of a real robot, and an explanation are different types of evidence.

Lesson vocabulary

on/off controllerAll or nothing rule: the wheel either moves or stands still, as in lesson 3.1.
P-partCorrection proportional to error: kp × error. The controller from lesson 3.2.
D-partCorrection for how quickly the error changes: kd × (error now − error at the previous step).
I-partCorrection for error accumulated over time: ki × sum of errors.
average errorHow much on average the line moved away from the middle per race, excluding the side. The smaller, the more accurately the robot drives.
controller stepConstant interval between correction calculations, here 10 ms.