حال باید برنامه لازم را جهت کنترل ربات تعادلی توسط آردوینو پیادهسازی کنیم. در روند برنامه توسط IMU میزان انحراف ربات به سمت جلو یا عقب بررسی میشود. سپس اگر به سمت جلو منحرف شده باشد، باید چرخها را در جهت عقب چرخانده شده و اگر به سمت عقب منحرف شده باشد، باید چرخها در جهت معکوس نیز بچرخند. در عین حال باید سرعت چرخش چرخها را نیز کنترل کنیم. اگر ربات از موقعیت مرکزی کمی منحرف شود ، چرخها به آرامی چرخانده شده و با دور شدن از موقعیت مرکزی ، سرعت آن افزایش مییابد. برای دستیابی به این منطق از کنترلر PID استفاده شده است. در این کنترلر موقعیت مرکز را به عنوان set-point و میزان انحراف به عنوان خروجی تعریف میشود. برای دانستن موقعیت فعلی ربات از MPU6050 استفاده شده که یک شتابسنج و سرعت سنج شش محوره است. برای به دست آوردن یک موقعیت دقیق توسط این سنسور باید از مقادیر شتابسنج و ژیروسکوپ به صورت همزمان استفاده شود. زیرا مقادیر شتاب نویزپذیر و مقادیر ژیروسکوپ با مشکل انباشتگی مواجه هستند. بنابراین با ترکیب هر دو مقدار به زاویای اویلر (رول ، پیچ و یاو) دست خواهیم یافت که در این ربات تنها مقدار یاو استفاده خواهد شد.
کتابخانه آمادهای برای کنترلر PID و محاسبهی مقدار یاو سنسور MPU6050 در دو لینک زیر قرار داده شده و در این برنامه استفاده میشود. بنابراین در گام اول باید این دو کتابخانه را به کامپایلر آردوینو IDE اضافه کنید. سورس کد کامل این پروژه در انتهای پروژه قرار داده شده است. در ادامه قسمت های مهم این برنامه توضیح داده خواهد شد.
https://github.com/br3ttb/Arduino-PID-Library/blob/master/PID_v1.h
https://github.com/jrowberg/i2cdevlib/tree/master/Arduino/MPU6050
ابتدا کتابخانههای I2C ، PID و MPU6050 فراخوانی میشوند.
#include "I2Cdev.h"
#include <PID_v1.h>
#include "MPU6050_6Axis_MotionApps20.h"
سپس متغیرهای لازم را برای بدست آوردن دادهها از سنسور MPU6050 تعریف میشوند. مقادیر بردار گرانش و کواترنیونها را خوانده و سپس مقدار پیچ و رول را محاسبه می کنیم. مقادیر نهایی از نوع float در آرایه [ypr[3 ذخیره میشوند.
bool dmpReady = false;
uint8_t mpuIntStatus;
uint8_t devStatus;
uint16_t packetSize;
uint16_t fifoCount;
uint8_t fifoBuffer[64];
Quaternion q;
VectorFloat gravity;
float ypr[3];
مهمترین بخش برنامه همین قسمت است که ضرایب کنترلر مشخص میشوند. اگردر ربات ساخته شده قطعات به صورت متقارن قرار گرفته باشند و مرکز ثقل به درستی تنظیم شده باشد (که در بیشتر موارد چنین نیست) مقدار set-point ، 180 خواهد بود. در غیر این صورت در حالیکه ربات را کج کرده اید، مقادیر خروجی را در سریال مانیتور آردوینو مشاهده کنید تا مقدار مطلوب set-point را پیدا کرده و برا ساس آن پارامترها را تنظیم کنید. مقادیر Kp ، Kd و Ki وابسته به ساختار ربات بوده و برای هر ربات متفاوت است. در پایان این آموزش نحوه تنظیم این مقادیر در یک ویدیو نشان داده شده است.
double setpoint= 176;
double Kp = 21;
double Kd = 0.8;
double Ki = 140;
در خط بعدی با وارد کردن متغیرهای ورودی، خروجی،set point، kp ،ki وkd الگوریتم کنترلی PID را راهاندازی اولیه میکنیم. قبلاً توسط کد بالا مقادیر set-point، Kp ، Ki و Kd را تنظیم شدهاند. مقدار ورودی مقدار فعلی yaw است که از سنسور MPU6050 خوانده شده و مقدار خروجی مقداری خواهد بود که توسط الگوریتم PID محاسبه می شود. بنابراین اساساً الگوریتم PID مقدار خروجی به ما می دهد که باید برای تصحیح مقدار ورودی، جهت نزدیک شدن مقدار به set point استفاده شود.
PID pid(&input, &output, &setpoint, Kp, Ki, Kd, DIRECT);
در داخل تنظیمات برنامه ماژول MPU6050 را توسط DMP ماژول راهاندازی اولیه میکنیم. این کار به ما در تلفیق دادههای شتابسنج و ژیروسکوپ کمک کرده و یک مقدار قابل اعتماد از زوایای رول ، پیچ و یاو را میدهد. یك بخشی از كدی كه باید در setup function برنامه تغییر دهید، مقادیر آفست ژیروسکوپ است. هر سنسور MPU6050 مقادیر آفست خاص خود را دارد. می توانید از این دستور استفاده کرده و خط زیر را نیز مطابق با مقدار آفست سنسور خود در برنامه به روز کنید.
mpu.setXGyroOffset(220);
mpu.setYGyroOffset(76);
mpu.setZGyroOffset(-85);
mpu.setZAccelOffset(1688);
همچنین پینهای دیجیتال PWM که برای اتصال موتورها در نظر گرفته شدهاند، به صورت خروجی تعریف کرده و در حالت پیش فرض LOW قرار داده میشوند.
pinMode (6, OUTPUT);
pinMode (9, OUTPUT);
pinMode (10, OUTPUT);
pinMode (11, OUTPUT);
analogWrite(6,LOW);
analogWrite(9,LOW);
analogWrite(10,LOW);
analogWrite(11,LOW)
در داخل حلقه اصلی برنامه بررسی میکنیم که آیا دادههای سنسور MPU6050 آماده خواندن هستند یا خیر. در صورت مثبت بودن، از دیتای سنسور برای محاسبه ضرایب کنترلر PID استفاده کرده و مقدار ورودی و خروجی PID را روی سریال مانیتور نمایش میدهیم . سپس براساس مقدار خروجی مشخص میکنیم که ربات رو به جلو یا عقب حرکت کند و یا ساکن باقی بماند. از آنجا که فرض کردهایم سنسور MPU6050 وقتی که ربات در حالت ایستا باشد، مقدار180 را بر میگرداند، بنابراین اگر ربات به سمت جلو حرکت کند مقادیر اصلاحی مثبت را دریافت خواهیم کرد و اگر ربات به سمت عقب برگردد ، مقادیر منفی را خواهیم داشت. بنابراین با بررسی این شرایط توابع مناسب را برای حرکت ربات به جلو یا عقب فراخوانی میکنیم.
while (!mpuInterrupt && fifoCount < packetSize)
{
pid.Compute();
Serial.print(input); Serial.print(" =>"); Serial.println(output);
if (input>150 && input<200){
if (output>0)
Forward();
else if (output<0)
Reverse();
}
else
Stop();
}
متغیر خروجی PID نیز سرعت چرخش موتور را مشخص میکند. اگر ربات در حالت افتادن قرار بگیرد با چرخش آرام چرخها اصلاح حرکت انجام میشود. اگر این میزان چرخش کم نتواند حرکت را اصلاح کند، سرعت موتور را افزایش میدهیم. مقدار چرخش سریع چرخها توسط ضرایب تناسبی و انتگرالگیر کنترلر (PI) انجام میشود. توجه داشته باشید که برای عملکرد معکوس مقدار خروجی در 1- ضرب شده تا به مقداری مثبت تبدیل شود.
void Forward()
{
analogWrite(6,output);
analogWrite(9,0);
analogWrite(10,output);
analogWrite(11,0);
Serial.print("F");
}
void Reverse()
{
analogWrite(6,0);
analogWrite(9,output*-1);
analogWrite(10,0);
analogWrite(11,output*-1);
Serial.print("R");
}
void Stop()
{
analogWrite(6,0);
analogWrite(9,0);
analogWrite(10,0);
analogWrite(11,0);
Serial.print("S");
}
شیوه عملکرد ربات تعادلی
پس از آماده شدن سخت افزار میتوانید برنامه را بر روی آردوینو آپلود کنید. سریال مانیتور آردوینو را برای باز کرده و مقادیر MPU6050 را بخوانید. اگر همه چیز مطابق آنچه انتظار میرفت عمل کند باید تصویر را که در این بخش قرار داده شده مشاهده کنید. در اینجا مقادیر ورودی و خروجی کنترلر PID را در input => output باید مشاهده کنید. اگر ربات در حالت تعادل باشد، مقدار خروجی 0 خواهد بود. مقدار ورودی مقدار فعلی سنسور MPU6050 است. حرف "F" بیانگر حرکت رو به جلو ربات و "R" بیانگر حرکت معکوس آن است. ویدئوی زیر کارکرد کامل ربات را نشان میدهد وهمچنین نحوه اصلاح مقادیر PID را نشان میدهد.
کد کامل برنامه:
#include "I2Cdev.h"
#include <PID_v1.h>
#include "MPU6050_6Axis_MotionApps20.h"
MPU6050 mpu;
bool dmpReady = false;
uint8_t mpuIntStatus;
uint8_t devStatus;
uint16_t packetSize;
uint16_t fifoCount;
uint8_t fifoBuffer[64];
Quaternion q;
VectorFloat gravity;
float ypr[3];
double setpoint= 176;
double Kp = 21;
double Kd = 0.8;
double Ki = 140;
double input, output;
PID pid(&input, &output, &setpoint, Kp, Ki, Kd, DIRECT);
volatile bool mpuInterrupt = false;
void dmpDataReady()
{
mpuInterrupt = true;
}
void setup() {
Serial.begin(115200);
Serial.println(F("Initializing I2C devices..."));
mpu.initialize();
Serial.println(F("Testing device connections..."));
Serial.println(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed"));
devStatus = mpu.dmpInitialize();
mpu.setXGyroOffset(220);
mpu.setYGyroOffset(76);
mpu.setZGyroOffset(-85);
mpu.setZAccelOffset(1688);
if (devStatus == 0)
{
Serial.println(F("Enabling DMP..."));
mpu.setDMPEnabled(true);
Serial.println(F("Enabling interrupt detection (Arduino external interrupt 0)..."));
attachInterrupt(0, dmpDataReady, RISING);
mpuIntStatus = mpu.getIntStatus();
Serial.println(F("DMP ready! Waiting for first interrupt..."));
dmpReady = true;
packetSize = mpu.dmpGetFIFOPacketSize();
pid.SetMode(AUTOMATIC);
pid.SetSampleTime(10);
pid.SetOutputLimits(-255, 255);
}
else
{
Serial.print(F("DMP Initialization failed (code "));
Serial.print(devStatus);
Serial.println(F(")"));
}
pinMode (6, OUTPUT);
pinMode (9, OUTPUT);
pinMode (10, OUTPUT);
pinMode (11, OUTPUT);
analogWrite(6,LOW);
analogWrite(9,LOW);
analogWrite(10,LOW);
analogWrite(11,LOW);
}
void loop() {
if (!dmpReady) return;
while (!mpuInterrupt && fifoCount < packetSize)
{
pid.Compute();
Serial.print(input); Serial.print(" =>"); Serial.println(output);
if (input>150 && input<200){
if (output>0)
Forward();
else if (output<0)
Reverse();
}
else
Stop();
}
mpuInterrupt = false;
mpuIntStatus = mpu.getIntStatus();
fifoCount = mpu.getFIFOCount();
if ((mpuIntStatus & 0x10) || fifoCount == 1024)
{
mpu.resetFIFO();
Serial.println(F("FIFO overflow!"));
}
else if (mpuIntStatus & 0x02)
{
while (fifoCount < packetSize) fifoCount = mpu.getFIFOCount();
mpu.getFIFOBytes(fifoBuffer, packetSize);
fifoCount -= packetSize;
mpu.dmpGetQuaternion(&q, fifoBuffer);
mpu.dmpGetGravity(&gravity, &q);
mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
input = ypr[1] * 180/M_PI + 180;
}
}
void Forward()
{
analogWrite(6,output);
analogWrite(9,0);
analogWrite(10,output);
analogWrite(11,0);
Serial.print("F");
}
void Reverse()
{
analogWrite(6,0);
analogWrite(9,output*-1);
analogWrite(10,0);
analogWrite(11,output*-1);
Serial.print("R");
}
void Stop()
{
analogWrite(6,0);
analogWrite(9,0);
analogWrite(10,0);
analogWrite(11,0);
Serial.print("S");
}