В современных системах автоматики PID контроллеры (регуляторы) являются самым распространенным типов регуляторов, предназначенных для стабилизации выходного процесса каких либо систем (механизмов). PID означает Proportional-Integral-Derivative, что расшифровывается как пропорционально-интегрально-дифференциальный (ПИД). Эти три управляющих механизма скомбинированы таким образом, что сигнал ошибки, формируемый ими, используется в качестве обратной связи для управления конечным приложением (устройством). PID (ПИД) контроллеры применяются во множестве современных устройств: в дронах для стабилизации их полета, в системах автоматической регулировки температуры (например, жала паяльника), для регулировки числа оборотов двигателей и т.д. Без использования PID контроллеров реализация подобных устройств значительно бы усложнилась. Конечно, существуют и другие типы автоматических регуляторов, превосходящие ПИД контроллеры по адаптивности и стабильности, но их реализация и настройка существенно сложнее, а с настройкой ПИД контроллеры с и использованием современных инструментов может справиться даже начинающий радиолюбитель.
В данной статье мы рассмотрим основные принципы работы PID контроллера, рассмотрим его использование для реализации энкодера для двигателя и проблемы, которые возникают при этом. В завершение мы реализуем PID алгоритм управления двигателем с помощью платы Arduino. Ранее на нашем сайте мы использовали PID контроллер на основе Arduino для создания самобалансирующегося робота.
Основные принципы работы PID контроллера (регулятора)
Как мы уже выяснили, PID (ПИД) состоит из таких понятий как пропорциональный, интегральный, дифференциальный. Допустим, нам необходимо создать машину которая бы останавливалась бы строго в заданном месте. Без использования PID контроллера реализовать такой алгоритм действия машины достаточно сложно вследствие наличия у любого движущегося тела момента инерции.
Более подробно о том, что такое PID контроллер (регулятор) и как он работает, вы можете прочитать в Википедии. Но как применить принципы работы PID контроллера в микроконтроллере? Этой информации в Википедии уже нет.
Основные принципы работы PID контроллера показаны на следующей картинке. Рассмотрим кратко основные составляющие этого процесса.
PID контроллер получил такое название из-за принципов обработки сигналов ошибки, которые возникают в управляемом им процессе. На представленном рисунке вы можете увидеть, в пропорциональной части регулятора ошибка (сигнал ошибки) умножается на константу Kp. В интегральной части ошибка умножается на константу Ki и затем интегрируется, а в дифференциальной части ошибка умножается на константу Kd, а затем дифференцируется. После всего этого все эти три значения суммируются чтобы сформировать выходное значение регулятора. В PID контроллере параметры Kp, Kd и Ki называются коэффициентами усиления. Также они называются P, I и D параметрами регулятора. Эти коэффициенты настраиваются индивидуально, чтобы обеспечить выполнение заданного набора требований к системе, например, насколько чувствительной или устойчивой она должна быть. Рассмотрим каждый из этих параметров более подробно.
P-параметр (пропорциональный)
Допустим, ошибка в системе изменяется во времени, как показано красной линией на представленном рисунке. В пропорциональном контроллере эта ошибка умножается на коэффициент усиления Kp. То есть если ошибка большая, то и значение ошибки на выходе контроллера будет большим, если ошибка равна 0, то и на выходе будет 0, если ошибка отрицательная – на выходе также будет отрицательное значение.
I-параметр (интегральный)
В интегральном контроллере происходит суммирование всех значений ошибки, при этом полученная сумма умножается на константу Ki. В подобном контроллере легко видеть, что результатом интегрирования является область лежащая под кривой: на представленном рисунке эта кривая показана синим цветом для положительной области и желтым цветом для отрицательной области. В сложных системах интегральный контроллер используется для устранения постоянной ошибки, возникающей при работе системы. При этом неважно насколько мала величина ошибки, все равно суммирование достаточно большого их числа обеспечит достаточное значение на выходе контроллера. На представленном рисунке эта ошибка показана зеленым цветом.
D-параметр (дифференциальный)
Дифференциальный контроллер показывает скорость изменения ошибки. Когда изменение величины ошибки происходит достаточно медленно (как на представленном рисунке), мы в качестве стартовой позиции (то есть кривой, аппроксимирующей это изменение) можем использовать синусоиду. При этом на выходе дифференциального контроллера будет маленькая величина – зеленая линия на представленном рисунке. И чем быстрее будет изменяться ошибка, тем больше будет становиться значение на выходе контроллера.
Значение на выходе PID контроллера является суммой с выхода трех рассмотренных контроллеров. Но необязательно чтобы всегда работали эти три контроллера, при желании любой из этих контроллеров можно выключить из работы, просто приравняв нулю коэффициент усиления в его ветви. К примеру, если мы установим D-параметр в ноль, то мы получим PI контроллер, а если мы установим I-параметр в ноль, то мы получим PD контроллер.
Что такое двигатель с энкодером и как он работает
Двигатель с энкодером (encoder motor) - это электродвигатель постоянного тока (со щетками), к которому прикреплен энкодер. Ранее на нашем сайте мы уже рассматривали подключение инкрементального энкодера к плате Arduino, можете прочитать эту статью если есть желание.
В двигателе с энкодером инкрементальный энкодер закрепляется на электродвигатель постоянного тока и обеспечивает обратную связь в этой связке при помощи отслеживания скорости или положения оси двигателя. На рынке сейчас доступно много типов двигателей, также существуют различные типы энкодеров: инкрементальные, абсолютные, оптические, магнитные и т.д. Для различных типов двигателей доступны различные применения. Кроме двигателей постоянного тока существуют еще серводвигатели, шаговые двигатели и т.п. На представленном рисунке показан электродвигатель типа N20 с энкодером магнитного типа, который уменьшает выходные обороты двигателя (RPM) до 15 с помощью прикрепленной коробки передач. Также на рисунке вы можете видеть два закрепленных датчика Холла, с помощью которых можно определять направление вращения двигателя – с помощью микроконтроллера мы можем делать это достаточно просто.
Необходимые компоненты
- Плата Arduino Nano (купить на AliExpress).
- N20 Encode Motor (двигатель N20 с энкодером) (купить на AliExpress).
- Регуляторы напряжения BD139 (2 шт.) и BD140 (2 шт.) (купить на AliExpress).
- Транзистор BC548 – 2 шт. (купить на AliExpress).
- Резистор 4,7 кОм – 2 шт. (купить на AliExpress).
- Резистор 100 Ом – 2 шт. (купить на AliExpress).
- Макетная плата.
- Соединительные провода.
- Источник питания.
Реклама: ООО "АЛИБАБА.КОМ (РУ)" ИНН: 7703380158
Внешний вид необходимых для проекта компонентов показан на следующем рисунке.
Схема проекта
Схема энкодера для двигателя на Arduino и PID контроллере представлена на следующем рисунке.
Принцип работы схемы достаточно прост. Двигатель N20 с экнкодером имеет 6 контактов, контакты M1, M2 используются для подачи питания на двигатель (он у нас очень маленький, работающий от 3.3V). Контакты VCC и GND используются для питания цепи энкодера. Для питания энкодера необходимо подавать напряжение +5V, иначе цепь энкодера не будет корректно работать. Контакты PIN_A и PIN_B двигателя непосредственно подключены к энкодеру. Измеряя состояние этих контактов, мы легко можем определить число оборотов двигателя в минуту (RPM). Наш двигатель N20 имеет 15RPM и передаточное число 1:2098, что означает, что ось двигателя должна совершить 2098 оборотов для того чтобы вспомогательный вал (на выходе коробки передач) совершил один оборот. Контакты PIN_A и PIN_B двигателя подключены к контактам 9 и 10 платы Arduino – они оба имеют возможность формирования ШИМ (широтно-импульсная модуляция) сигналов. Эти контакты должны обязательно иметь возможность формирования ШИМ сигналов, иначе код программы работать не будет. PID контроллер управляет двигателем при помощи ШИМ.
Также наша схема содержит драйвер двигателя в виде H-моста. Драйвер двигателя у нас имеет такую схему, что с помощью него мы можем управлять двигателем с использованием всего 2-х контактов платы Arduino. Также он защищает двигатель от ложных срабатываний.
Объяснение программы для Arduino
Полный код программы приведен в конце статьи, здесь же мы кратко рассмотрим его основные фрагменты. Комментарии к коду программы также переведены в конце статьи, в этом разделе я их оставил без перевода.
Сначала мы должны скачать и установить библиотеку PID контроллера для Arduino по следующей ссылке:
• Download PID Controller Library for Arduino
Далее в коде программы мы должны подключить заголовочные файлы всех используемых библиотек – мы в нашем проекте используем только библиотеку управления PID контроллером. Затем мы укажем все используемые контакты.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <PIDController.h> /* ENCODER_A and ENCODER_B pins are used to read the encoder * Data from the microcontroller, the data from the encoder * comes very fast so these two pins have to be interrupt enabled * pins */ #define ENCODER_A 2 #define ENCODER_B 3 /* the MOTOR_CW and MOTOR_CCW pins are used to drive the H-bridge * the H-bridge then drives the motors, These two pins must have to * be PWM enabled, otherwise the code will not work. */ #define MOTOR_CW 9 #define MOTOR_CCW 10 |
Также в коде программы мы зададим значения коэффициентов __Kp, __Ki и __Kd. Они будут непосредственным образом влиять на выходное значение нашего контроллера.
1 2 3 4 5 6 7 |
/*In this section we have defined the gain values for the * proportional, integral, and derivative controller I have set * the gain values with the help of trial and error methods. */ #define __Kp 260 // Proportional constant #define __Ki 2.7 // Integral Constant #define __Kd 2000 // Derivative Constant |
Далее мы инициализируем все переменные, необходимые для проекта. Переменная encoder_count будет использоваться для подсчета количества генерируемых прерываний, то есть она будет подсчитывать число оборотов. В переменной integerValue будет храниться число, которое мы будем вводить в окне монитора последовательной связи. В символьной переменной incomingByte будут храниться символы, поступающие через последовательный порт связи. Переменная motor_pwm_value будет содержать значение, рассчитываемое в результате работы PID алгоритма, это значение будет поступать на ШИМ контакт для управления двигателем. Также мы создаем объект для работы с PID контроллером.
1 2 3 4 5 |
volatile long int encoder_count = 0; // stores the current encoder count unsigned int integerValue = 0; // stores the incoming serial value. Max value is 65535 char incomingByte; // parses and stores each character one by one int motor_pwm_value = 255; // after PID computation data is stored in this variable. PIDController pid_controller; |
Далее, в функции setup(), мы задаем режимы работы для контактов ENCODER_A и ENCODER_B на ввод данных, а для контактов MOTOR_CW и MOTOR_CCW – режим работы на вывод данных. Далее мы задействуем на контакте ENCODER_A обработку прерывания по восходящему импульсу (RISING edge), при срабатывании прерывания будет вызываться функция encoder(). Более подробно о работе с прерываниями вы можете прочитать в следующей статье. Далее мы инициализируем PID контроллер с помощью функции begin(), затем мы производим его настройку с помощью параметров Kp, Ki и Kd. И, наконец, мы задаем ограничение на выходные значения контроллера.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void setup() { Serial.begin(115200); // Serial for Debugging pinMode(ENCODER_A, INPUT); // ENCODER_A as Input pinMode(ENCODER_B, INPUT); // ENCODER_B as Input pinMode(MOTOR_CW, OUTPUT); // MOTOR_CW as Output pinMode(MOTOR_CCW, OUTPUT); // MOTOR_CW as Output /* attach an interrupt to pin ENCODER_A of the Arduino, and when the pulse is in the RISING edge called the function encoder(). */ attachInterrupt(digitalPinToInterrupt(ENCODER_A), encoder, RISING); pidcontroller.begin(); // initialize the PID instance pidcontroller.tune(__Kp , __Ki , __Kd); // Tune the PID, arguments: kP, kI, kD pidcontroller.limit(-255, 255); // Limit the PID output between -255 to 255, this is important to get rid of integral windup! } |
В функции loop() мы первым делом проверяем доступна ли последовательная связь. Если последовательная связь доступна, мы принимаем из нее значение целого типа и сохраняем ее в переменной. Если мы принимаем символ ‘/n’, мы продолжаем цикл, далее мы устанавливаем точку назначения с помощью функции pidcontroller.setpoint(integerValue). Также мы выводим это значение целого типа в окно монитора последовательной связи для целей отладки.
Вычисляемое алгоритмом PID значение мы сохраняем в переменной motor_pwm_value. Если это значение больше 0, мы вызываем функцию motor_ccw(motor_pwm_value), иначе мы вызываем функцию motor_cw(abs(motor_pwm_value)).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void loop() { while (Serial.available() > 0) { integerValue = Serial.parseInt(); // stores the integerValue incomingByte = Serial.read(); // stores the /n character pidcontroller.setpoint(integerValue); // The "goal" the PID controller tries to "reach", Serial.println(integerValue); // print the incoming value for debugging if (incomingByte == '\n') // if we receive a newline character we will continue in the loop continue; } motor_pwm_value = pidcontroller.compute(encoder_count); //Let the PID compute the value, returns the calculated optimal output Serial.print(motor_pwm_value); // print the calculated value for debugging Serial.print(" "); if (motor_pwm_value > 0) // if the motor_pwm_value is greater than zero we rotate the motor in clockwise direction MotorCounterClockwise(motor_pwm_value); else // else, we move it in a counter-clockwise direction MotorClockwise(abs(motor_pwm_value)); Serial.println(encoder_count);// print the final encoder count. } |
Функция encoder() вызывается когда происходит прерывание на контакте ENCODER_B. В этой функции мы проверяем состояние контакта ENCODER_B, если оно HIGH, то мы инкрементируем счетчик, иначе мы декрементируем счетчик.
1 2 3 4 5 6 |
void encoder() { if (digitalRead(ENCODER_B) == HIGH) // if ENCODER_B is high increase the count Encoder_count++; // increment the count else // else decrease the count Encoder_count--; // decrement the count } |
Далее рассмотрим функцию вращения двигателя по часовой стрелке. В этой функции проверяется больше ли 100 значение power. Если больше, то мы вращаем двигатель по часовой стрелке, иначе мы останавливаем двигатель.
1 2 3 4 5 6 7 8 9 10 11 |
void motor_cw(int power) { if (power > 100) { analogWrite(MOTOR_CW, power); digitalWrite(MOTOR_CCW, LOW); } // both of the pins are set to low else { digitalWrite(MOTOR_CW, LOW); digitalWrite(MOTOR_CCW, LOW); } } |
Аналогичные операции производятся и в функции вращения двигателя против часовой стрелки. Если значение power больше 100, то мы вращаем двигатель против часовой стрелки, иначе мы останавливаем двигатель.
1 2 3 4 5 6 7 8 9 10 |
void motor_ccw(int power) { if (power > 100) { analogWrite(MOTOR_CCW, power); digitalWrite(MOTOR_CW, LOW); } else { digitalWrite(MOTOR_CW, LOW); digitalWrite(MOTOR_CCW, LOW); } } |
Тестирование работы PID контроллера двигателя
Для тестирования работы проекта мы прикрепили двигатель с помощью двухстороннего скотча на распределительный ящик. Также мы использовали небольшой понижающий конвертер для питания двигателя поскольку он у нас питается от 3.3V.
Также мы подключили USB кабель к плате Arduino, с его помощью устанавливается начальная тока PID контроллера. Также по USB кабелю мы передаем отладочную информацию.
Более подробно работу нашего проекта вы можете посмотреть на видео, приведенном в конце статьи.
Исходный код программы (скетча)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
#include <PIDController.h> /* контакты ENCODER_A и ENCODER_B используются для считывания микроконтроллером данных от энкодера, эти данные поступают очень быстро, поэтому необходимо чтобы на них можно было использовать прерывания */ #define ENCODER_A 2 #define ENCODER_B 3 /* контакты MOTOR_CW и MOTOR_CCW используются для управления двигателем с помощью H-моста, на этих двух контактах должно быть доступно формирование ШИМ сигнала, иначе код программы работать не будет */ #define MOTOR_CW 9 #define MOTOR_CCW 10 /*в этой части программы мы зададим коэффициенты усиления для пропорциональной, интегральной и дифференциальной частей контроллера */ #define __Kp 260 // пропорциональная константа #define __Ki 2.7 // интегральная константа #define __Kd 2000 // дифференциальная константа volatile long int encoder_count = 0; // переменная счетчика энкодера unsigned int integerValue = 0; // сохраняет значение, поступающее по последовательной связи. Максимальное значение - 65535 char incomingByte; // в этой переменной будут храниться символы, поступающие по последовательной связи int motor_pwm_value = 255; // в этой переменной сохраняется значение, рассчитанное с помощью PID алгоритма PIDController pidcontroller; void setup() { Serial.begin(115200); // последовательная связь для целей отладки // устанавливаем режимы работы используемых контактов pinMode(ENCODER_A, INPUT); // ENCODER_A as Input pinMode(ENCODER_B, INPUT); // ENCODER_B as Input pinMode(MOTOR_CW, OUTPUT); // MOTOR_CW as Output pinMode(MOTOR_CCW, OUTPUT); // MOTOR_CW as Output /* делаем контакт ENCODER_A контактом обработки прерывания, при поступлении импульса с восходящим фронтом (RISING edge) вызывается функция encoder(). */ attachInterrupt(digitalPinToInterrupt(ENCODER_A), encoder, RISING); pidcontroller.begin(); // инициализируем PID контроллер pidcontroller.tune(260, 2.7, 2000); // настраиваем PID аргументы kP, kI, kD pidcontroller.limit(-255, 255); // ограничиваем выход PID значений от -255 до 255, this is important to get rid of integral windup! } void loop() { while (Serial.available() > 0) { integerValue = Serial.parseInt(); // сохраняем принятое значение в integerValue incomingByte = Serial.read(); // сохраняем символ /n if (incomingByte == '\n') // если мы приняли символ новой строки newline character) мы будем продолжать цикл (loop) continue; } pidcontroller.setpoint(integerValue); // задаем цель, которую PID контроллер будет пытаться достигнуть Serial.println(integerValue); // печатаем поступающее значение для целей отладки motor_pwm_value = pidcontroller.compute(encoder_count); //PID алгоритм рассчитывает оптимальное значение и мы сохраняем его в переменной Serial.print(motor_pwm_value); // печатаем рассчитанное значение для целей отладки Serial.print(" "); if (motor_pwm_value > 0) // если значение motor_pwm_value больше нуля, мы вращаем двигатель по часовой стрелке motor_ccw(motor_pwm_value); else // иначе мы вращаем его против часовой стрелки motor_cw(abs(motor_pwm_value)); Serial.println(encoder_count);// печатаем окончательное значение переменной encoder count } void encoder() { if (digitalRead(ENCODER_B) == HIGH) // если на контакте ENCODER_B уровень high увеличиваем count encoder_count++; // increment the count else // иначе уменьшаем count encoder_count--; // decrement the count } void motor_cw(int power) { if (power > 100) { analogWrite(MOTOR_CW, power); //вращаем двигатель если значение больше чем 100 digitalWrite(MOTOR_CCW, LOW); // make the other pin LOW } else { // both of the pins are set to low (останавливаем двигатель) digitalWrite(MOTOR_CW, LOW); digitalWrite(MOTOR_CCW, LOW); } } void motor_ccw(int power) { if (power > 100) { analogWrite(MOTOR_CCW, power); digitalWrite(MOTOR_CW, LOW); } else { digitalWrite(MOTOR_CW, LOW); digitalWrite(MOTOR_CCW, LOW); } } |