PID-контроль не работает. После вычисления значений значения просто становятся равными 0

Итак, у меня есть рабочий код проекта стабилизатора с использованием 2 серводвигателей, mpu6050, Arduino Uno. Однако я хочу добавить часть PID для настройки движения серводвигателей. Я последовал примеру библиотеки PID и просто изменил ввод в виде данных, полученных от датчика mpu6050, и вывод в виде данных, которые мы отправляем в серводвигатель. Проблема в том, что после добавления PID-части проект перестал работать должным образом. Серводвигатели больше не реагировали на движение акселерометра. Я попытался распечатать значения, вычисленные с помощью PID, и все они оказались равными 0, и они не меняются, даже если я поворачиваю датчик. Может ли кто-нибудь заметить ошибку в коде с частью PID? Я почти уверен, что все остальное работает идеально.

/*
                        DIY Gimbal - MPU6050 Arduino Tutorial
                        by Dejan, www.HowToMechatronics.com
                        Code based on the MPU6050_DMP6 example from the i2cdevlib library by Jeff Rowberg:
                        https://github.com/jrowberg/i2cdevlib
*/
// I2Cdev and MPU6050 must be installed as libraries, or else the .cpp/.h files
// for both classes must be in the include path of your project
#include "I2Cdev.h"

#include "MPU6050_6Axis_MotionApps20.h"
//#include "MPU6050.h" // not necessary if using MotionApps include file
#include "PID_v1.h"

// Arduino Wire library is required if I2Cdev I2CDEV_ARDUINO_WIRE implementation
// is used in I2Cdev.h
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
#include "Wire.h"
#endif
#include <Servo.h>
// class default I2C address is 0x68
// specific I2C addresses may be passed as a parameter here
// AD0 low = 0x68 (default for SparkFun breakout and InvenSense evaluation board)
// AD0 high = 0x69
MPU6050 mpu;
//MPU6050 mpu(0x69); // <-- use for AD0 high

double Setpointx, Setpointy, Inputx, Inputy, Outputx, Outputy, pidX, pidY;
double Kp = 2, Ki = 5, Kd = 1;
PID myPID0 (&Inputx, &Outputx, &Setpointx, Kp, Ki, Kd, DIRECT) ;
PID myPID1 (&Inputy, &Outputy, &Setpointy, Kp, Ki, Kd, DIRECT) ;
// Define the 3 servo motors
Servo servo0;
Servo servo1;
Servo servo2;
float correct;
int j = 0;

#define OUTPUT_READABLE_YAWPITCHROLL

#define INTERRUPT_PIN 2  // use pin 2 on Arduino Uno & most boards

bool blinkState = false;

// MPU control/status vars
bool dmpReady = false;  // set true if DMP init was successful
uint8_t mpuIntStatus;   // holds actual interrupt status byte from MPU
uint8_t devStatus;      // return status after each device operation (0 = success, !0 = error)
uint16_t packetSize;    // expected DMP packet size (default is 42 bytes)
uint16_t fifoCount;     // count of all bytes currently in FIFO
uint8_t fifoBuffer[64]; // FIFO storage buffer

// orientation/motion vars
Quaternion q;           // [w, x, y, z]         quaternion container
VectorInt16 aa;         // [x, y, z]            accel sensor measurements
VectorInt16 aaReal;     // [x, y, z]            gravity-free accel sensor measurements
VectorInt16 aaWorld;    // [x, y, z]            world-frame accel sensor measurements
VectorFloat gravity;    // [x, y, z]            gravity vector
float euler[3];         // [psi, theta, phi]    Euler angle container
float ypr[3];           // [yaw, pitch, roll]   yaw/pitch/roll container and gravity vector

// packet structure for InvenSense teapot demo
uint8_t teapotPacket[14] = { '$', 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, '\r', '\n' };



// ================================================================
// ===               INTERRUPT DETECTION ROUTINE                ===
// ================================================================

volatile bool mpuInterrupt = false;     // indicates whether MPU interrupt pin has gone high
void dmpDataReady() {
  mpuInterrupt = true;
}

// ================================================================
// ===                      INITIAL SETUP                       ===
// ================================================================

void setup() {
  // join I2C bus (I2Cdev library doesn't do this automatically)
  Setpointx = 10;
  Setpointy = 10;

  myPID0. SetMode (AUTOMATIC);
  myPID1. SetMode (AUTOMATIC);

#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
  Wire.begin();
  Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties
#elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
  Fastwire::setup(400, true);
#endif

  // initialize serial communication
  // (115200 chosen because it is required for Teapot Demo output, but it's
  // really up to you depending on your project)
  Serial.begin(115200);
  while (!Serial); // wait for Leonardo enumeration, others continue immediately

  // initialize device
  //Serial.println(F("Initializing I2C devices..."));
  mpu.initialize();
  pinMode(INTERRUPT_PIN, INPUT);
  devStatus = mpu.dmpInitialize();
  // supply your own gyro offsets here, scaled for min sensitivity
  mpu.setXGyroOffset(17);
  mpu.setYGyroOffset(-69);
  mpu.setZGyroOffset(27);
  mpu.setZAccelOffset(1551); // 1688 factory default for my test chip

  // make sure it worked (returns 0 if so)
  if (devStatus == 0) {
    // turn on the DMP, now that it's ready
    // Serial.println(F("Enabling DMP..."));
    mpu.setDMPEnabled(true);

    attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING);
    mpuIntStatus = mpu.getIntStatus();

    // set our DMP Ready flag so the main loop() function knows it's okay to use it
    //Serial.println(F("DMP ready! Waiting for first interrupt..."));
    dmpReady = true;

    // get expected DMP packet size for later comparison
    packetSize = mpu.dmpGetFIFOPacketSize();
  } else {
    // ERROR!
    // 1 = initial memory load failed
    // 2 = DMP configuration updates failed
    // (if it's going to break, usually the code will be 1)
    // Serial.print(F("DMP Initialization failed (code "));
    //Serial.print(devStatus);
    //Serial.println(F(")"));
  }

  // Define the pins to which the 3 servo motors are connected
  servo0.attach(10); //Z Axis
  servo1.attach(9); //Y Axis
  servo2.attach(8); //X Axis
  servo1.write(90);
  servo2.write(90);
}
// ================================================================
// ===                    MAIN PROGRAM LOOP                     ===
// ================================================================

void loop() {
  // if programming failed, don't try to do anything
  if (!dmpReady) return;


  // wait for MPU interrupt or extra packet(s) available
  while (!mpuInterrupt && fifoCount < packetSize) {
    if (mpuInterrupt && fifoCount < packetSize) {
      // try to get out of the infinite loop
      fifoCount = mpu.getFIFOCount();
    }
  }

  // reset interrupt flag and get INT_STATUS byte
  mpuInterrupt = false;
  mpuIntStatus = mpu.getIntStatus();

  // get current FIFO count
  fifoCount = mpu.getFIFOCount();

  // check for overflow (this should never happen unless our code is too inefficient)
  if ((mpuIntStatus & _BV(MPU6050_INTERRUPT_FIFO_OFLOW_BIT)) || fifoCount >= 1024) {
    // reset so we can continue cleanly
    mpu.resetFIFO();
    fifoCount = mpu.getFIFOCount();
    Serial.println(F("FIFO overflow!"));

    // otherwise, check for DMP data ready interrupt (this should happen frequently)
  } else if (mpuIntStatus & _BV(MPU6050_INTERRUPT_DMP_INT_BIT)) {
    // wait for correct available data length, should be a VERY short wait
    while (fifoCount < packetSize) fifoCount = mpu.getFIFOCount();

    // read a packet from FIFO
    mpu.getFIFOBytes(fifoBuffer, packetSize);

    // track FIFO count here in case there is > 1 packet available
    // (this lets us immediately read more without waiting for an interrupt)
    fifoCount -= packetSize;

    // Get Yaw, Pitch and Roll values
#ifdef OUTPUT_READABLE_YAWPITCHROLL
    mpu.dmpGetQuaternion(&q, fifoBuffer);
    mpu.dmpGetGravity(&gravity, &q);
    mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);

    // Yaw, Pitch, Roll values - Radians to degrees
    Inputx = ypr[2];
    myPID0.Compute();
    Outputx = pidX;
    Inputy = ypr[1];
    myPID1.Compute();
    Outputy = pidY;
    Serial.print("\nX: ");
    Serial.print(pidX);
    Serial.print("\nY: ");
    Serial.print(pidY);
    ypr[0] = ypr[0] * 180 / M_PI;
    pidX = pidX * 180 / M_PI;
    pidY = pidY * 180 / M_PI;
    // Skip 300 readings (self-calibration process)
    if (j <= 300) {
      correct = ypr[0]; // Yaw starts at random value, so we capture last value after 300 readings
      j++;
    }
    // After 300 readings
    else {
      ypr[0] = ypr[0] - correct; // Set the Yaw to 0 deg - subtract  the last random Yaw value from the currrent value to make the Yaw 0 degrees
      // Map the values of the MPU6050 sensor from -90 to 90 to values suatable for the servo control from 0 to 180
      int servo0Value = map(ypr[0], -90, 90, 0, 180);
      int servo1Value = map(pidY, -90, 90, 0, 180);
      int servo2Value = map(pidX, -90, 90, 180, 0);
      // Control the servos according to the MPU6050 orientation
      servo0.write(90);
      servo1.write(servo1Value);
      servo2.write(servo2Value);
    }
#endif
  }
}

, 👍1

Обсуждение

Почему вы делаете " Outputx = pidX;"? `Outputx " - это выходная переменная объекта PID. Вы не должны ему писать. Вам нужно прочитать его, а затем использовать в качестве сервоугла. И вы должны вычислять PID только тогда, когда вы фактически используете это значение. Вам это не нужно для цикла калибровки, @chrisl

@chrisl Как я должен использовать вывод после того, как он будет вычислен с помощью myPID.compute()? Я имею в виду, что я должен сделать значения, которые я посылаю сервоприводу, в градусах и сопоставить их от 0 до 180. Я сомневаюсь, что если бы я поместил outputx непосредственно в servo2.write(outputx), это было бы в градусах вместо радианов и без отображения. И последний вопрос, где тогда должен быть вычислен вывод pid? Разве это не должно быть вычислено внутри пустого цикла()?, @Akhat Mussabayev

Ваш код не использует Outputx после того, как он вычисляется с помощью myPID0.Вычислить();, вы немедленно перезаписываете то, что вычисляется, следующей строкой " Outputx = pidX;. За https://playground.arduino.cc/Code/PIDLibrarySetOutputLimits/ используйте myPID0.SetOutputLimits(0,180); & серво1.запись(вывод) и т. Д. и отбросьте все остальное, связанное с преобразованием и отображением. Константы Kp, Ki и Kd преобразуют единицы погрешности ввода уставки в единицы контролируемого выхода. (Или, по крайней мере, измените Outputx = pidX; на pidX = Outputx;`), @Dave X

pidX и pidY инициализируются нулями в объявлении, а затем обновляются только с помощью " pidX = pidX * 180 / M_PI; pidY = pidY * 180 / M_PI;, что сбрасывает их до нуля. Вам нужно изменить "Outputx = pidX;" на pidX = Outputx;`, а также для Y., @Dave X


2 ответа


1

Похоже, у вас есть некоторые неправильные представления о PID. Для объяснения я буду использовать другой вариант использования PID-элемента управления, который проще для понимания (а также упоминается в документации библиотеки PID):

ПИД-регулятор может использоваться в любой системе обратной связи с последовательной обратной связью. Когда вы ведете свой автомобиль, вы хотите, чтобы он развивал определенную скорость. Для этого вы нажимаете на педаль газа, меняя ее угол наклона, пока не достигнете целевой скорости. В этом сценарии вы выполняете ПИД-контроль. Заданное значение-это целевая скорость, вход-фактическая текущая скорость, а выход-угол наклона педали газа.

Вы видите: выход и вход/уставка не имеют одного и того же устройства. Это совершенно разные измерения разных вещей. Они подключены только через контур обратной связи в системе, что означает, что увеличение угла наклона педали газа увеличит скорость автомобиля. Тогда управление PID-это просто формула, которая включает в себя фактическую скорость, желаемую скорость и некоторые переменные масштабирования (Kp, Ki, Kd)(для настройки поведения элемента управления). И вывод формулы-это необходимый угол наклона педали газа в это время.

Так что же это значит для вас?

  • Вам не нужно преобразовывать выходное значение в угол. Единица измерения выходного сигнала не важна, так как PID будет контролировать себя, чтобы достичь заданного значения. Более важно настроить PID с помощью Kp, Ki и Kd, чтобы заставить PID правильно вести себя в вашей системе.

  • Рабочий процесс выглядит следующим образом:

    1. Установите переменную setpoint Setpointx в нужное входное значение.
    2. Считайте входные данные из MPU и сохраните их в Inputx
    3. Вызовите myPID0.Вычислить(); для вычисления выходных данных из заданного значения и входных данных.
    4. Считайте значение из Outputx и запишите его на свой сервопривод. Затем движение сервопривода приведет к изменению ввода считывания в следующем цикле.
    5. повторите с шага 2
  • Поскольку в настоящее время я не совсем понимаю вашу систему обратной связи: убедитесь, что петля обратной связи работает правильно. Увеличение объема производства должно привести к увеличению объема вводимых ресурсов. Убедитесь, что он последователен. Ваше входное значение не должно иметь слишком большого шума (или PID будет реагировать на этот шум). Для тестирования вы можете настроить Kp, Ki и Kd так, чтобы PID работал медленнее. Тогда вы сможете лучше видеть поведение. Убедитесь, что оба элемента управления PID не влияют друг на друга. Они должны быть независимыми.

  • Используйте myPID0.Вычисление(); каждый раз, когда вы записываете новое значение в свой сервопривод, и только тогда. Называть это между делом будет просто пустой тратой времени. Поэтому имеет смысл использовать эту строку где-нибудь непосредственно перед записью в серво.


Если это все еще не работает, вам следует подробно изучить значения считывания и то, как они связаны с углом сервопривода. Выполнены ли вышеуказанные требования? Достаточно ли они стабильны. Соответствует ли цикл обратной связи ожиданиям? Что меняет изменение Kp, Kd и Ki в результате?

,

Вход и выход PID не обязательно должны быть в соответствующих единицах измерения, но они могут быть. Диапазон ввода Servo.write() составляет от 0 до 180 для минимального до максимального значения и предназначен для отображения в градусах https://www.arduino.cc/reference/en/libraries/servo/write/ -- Внутренне Servo.write() отображает 0-180 обратно в микросекунды: https://github.com/arduino-libraries/Servo/blob/master/src/avr/Servo.cpp#L264 В этом случае "вход" и "уставка" должны быть в одних и тех же единицах измерения, но "выход" может быть в градусах, ограниченных " 0-180 или 1000-2000 микросекунд. kP` преобразует входные единицы погрешности в выходные единицы., @Dave X


0

Этот фрагмент кода подает радианы в алгоритм PID, а затем отбрасывает/перезаписывает выходные данные с инициализированными до нуля значениями pidX и pidY.

...
Inputx = ypr[2];
myPID0.Compute();
Outputx = pidX;
Inputy = ypr[1];
myPID1.Compute();
Outputy = pidY;
...

Я бы установил ограничения на выход, чтобы они соответствовали контролируемому устройству:

void setup() {
   // присоединиться к шине I2C (библиотека I2Cdev не делает этого автоматически) 
   Setpointx = 10;  // MPU degrees
   Setpointy = 10;  // MPU degrees

   myPID0. SetMode (AUTOMATIC);
   myPID1. SetMode (AUTOMATIC);
   myPID0.SetOutputLimits(0,180); // servo degrees
   myPID1.SetOutputLimits(0,180); // servo degrees
   ...

...а затем преобразуйте входные данные в соответствии с заданными значениями и передайте ограниченные PID-выходы непосредственно в элементы управления:

Inputx = ypr[2]*180.0/M_PI;
myPID0.Compute();
pid_X = Outputx;
Inputy = ypr[1]*180/M_PI;
myPID1.Compute();
pidY = Outputy;
...
servo1.write(Outputx);
servo2.write(Outputy);
...

Важно использовать PID.SetOutputLimits(min,max), чтобы процедура PID могла внутренне гарантировать, что выходные данные не будут уходить глубоко в неосуществимые режимы управления. Если вы не вызываете SetOutputLimits(), выходные ограничения по умолчанию равны 0-255, что не имеет смысла для сервопривода 0-180.

Один из способов подумать о ваших константах PID Kp = 2, Ki = 5, Kd = 1; насколько большое изменение единицы вывода (градусы настройки сервопривода) вы хотите сделать для единицы ввода (радианы MPU или градусы?) ошибка, интегрированная ошибка (ошибка*секунд/5 на https://playground.arduino.cc/Code/PIDLibrarySetSampleTime/), или дельта-ошибка. Они могут нуждаться в корректировке, если система реагирует не так, как вам хотелось бы.

,