制作Arduino自平衡机器人并了解机器人的控制原理
本文将介绍如何使用Arduino来制造一个自动平衡机器人,并介绍其控制原理。
自平衡机器人的控制原理

自平衡机器人听名字比较高大上,但实际生活中其中已经可以看到很多成品了,像比较早期高端的赛格威,满大街跑的小米平衡车等,都属于自平衡机器人的范畴。那他是如何工作的呢?首先我们要知道,自平衡机器人为了让机器人保持平衡,电机的运动必须要能抵消机器人的自然重力导致的姿态变化,比如倾倒、后仰等。要实现这个抵消的动作平衡机器人就需要能反馈并纠正这些变化的因素。在本项目中,反馈元件是MPU6050及其内置的陀螺仪和加速度计,它在三个轴上都提供加速度和旋转反馈功能。详情可阅读:MPU6050 。Arduino控制器通过它来知道机器人当前的姿态信息,从而通过这些信息控制电机和车轮的运动,使机器人能够保持平衡。

自平衡机器人的电路原理图

自平衡机器人的电路原理图
在电路连接方面, 首先我们要将MPU6050连接到Arduino,可查阅:Arduino如何使用MPU6050,然后参照上图所示连接其余的组件。L298N模块可以为Arduino提供所需的+5V(详情见: Arduino直流电机控制教程 中的相关内容 )。本文中,我们选择了单独的电源为电机和电路供电。请注意,如果您计划为L298N模块使用大于+12V的电源电压,您需要移除刚好位于+12V输入上方的跳线。
构建机器人

机器人框架采用一些简单的材料黏贴完成,如果有3D打印机也可以尝试设计一款适合自己风格外壳框架。具体可参阅:设计自平衡机器人。

主电路板主要包括Arduino Nano和MPU6050和一块L298N电机驱动板。
自平衡机器人本质上是一个倒立钟摆。如果质量中心相对于轮轴较高,则可以更好地平衡。较高的质心意味着较高的质量惯性矩,这对应于较低的角加速度(下降较慢)。这就是为什么我把电池组放在上面。然而,机器人的高度是根据材料的可用性来选择的。 制作时不必过分强求。
PID控制原理
在控制中,保持某个变量(机器人的位置)的稳定需要一个称为PID(比例积分导数)的特殊控制器。这些参数都存在“增益”的问题,通常称为Kp、Ki和Kd。PID需要在期望值(输入值)和实际值(输出值)之间进行校正。输入和输出之间的差异称为“误差”。PID控制器通过不断调整输出将误差减小到最小值。在本文的Arduino自平衡机器人中,这些值的设置遵循以下原则:
- 使Kp Ki Kd等于0。
- Kp的调整:Kp值太低会使机器人摔倒,因为没有足够的校正值。太大的Kp会使机器人疯狂地来回移动。一个合适的Kp值会使机器人稍微来回移动或者稍微摆动即可。
- 设定好Kp值后,需要调整Kd值。适当的Kd值可以减小振动,直到机器人基本稳定。同时,即使它被推动,适当的Kd值也能让机器人始终保持站立。
- 最后就是设置Ki值。因为我们即使设定了Kp和Kd值,机器人在打开电源时也会发生振动,但它会在一定的时间后稳定下来。正确的Ki值可以缩短机器人稳定的时间。
Arduino自平衡机器人代码
要成功实现代码安装,需要四个外部库文件来让Arduino自平衡机器人正常工作。PID库的引入使得计算P、I和D值变得相对容易。LMotorController库文件用于L298N模块驱动两个电机,I2Cdev库和MPU6050_6_Axis_MotionApps20库用于从MPU6050读取数据。你可以在这里下载包含这些库文件的代码。
#include <PID_v1.h> #include <LMotorController.h> #include "I2Cdev.h" #include "MPU6050_6Axis_MotionApps20.h" #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE #include "Wire.h" #endif #define MIN_ABS_SPEED 20 MPU6050 mpu; // 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 VectorFloat gravity; // [x, y, z] gravity vector float ypr[3]; // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector //PID double originalSetpoint = 173; double setpoint = originalSetpoint; double movingAngleOffset = 0.1; double input, output; //adjust these values to fit your own design double Kp = 50; double Kd = 1.4; double Ki = 60; PID pid(&input, &output, &setpoint, Kp, Ki, Kd, DIRECT); double motorSpeedFactorLeft = 0.6; double motorSpeedFactorRight = 0.5; //MOTOR CONTROLLER int ENA = 5; int IN1 = 6; int IN2 = 7; int IN3 = 8; int IN4 = 9; int ENB = 10; LMotorController motorController(ENA, IN1, IN2, ENB, IN3, IN4, motorSpeedFactorLeft, motorSpeedFactorRight); volatile bool mpuInterrupt = false; // indicates whether MPU interrupt pin has gone high void dmpDataReady() { mpuInterrupt = true; } void setup() { // join I2C bus (I2Cdev library doesn't do this automatically) #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE Wire.begin(); TWBR = 24; // 400kHz I2C clock (200kHz if CPU is 8MHz) #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE Fastwire::setup(400, true); #endif mpu.initialize(); devStatus = mpu.dmpInitialize(); // supply your own gyro offsets here, scaled for min sensitivity mpu.setXGyroOffset(220); mpu.setYGyroOffset(76); mpu.setZGyroOffset(-85); mpu.setZAccelOffset(1788); // 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 mpu.setDMPEnabled(true); // enable Arduino interrupt detection attachInterrupt(0, dmpDataReady, RISING); mpuIntStatus = mpu.getIntStatus(); // set our DMP Ready flag so the main loop() function knows it's okay to use it dmpReady = true; // get expected DMP packet size for later comparison packetSize = mpu.dmpGetFIFOPacketSize(); //setup PID pid.SetMode(AUTOMATIC); pid.SetSampleTime(10); pid.SetOutputLimits(-255, 255); } 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(")")); } } 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) { //no mpu data - performing PID calculations and output to motors pid.Compute(); motorController.move(output, MIN_ABS_SPEED); } // 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 & 0x10) || fifoCount == 1024) { // reset so we can continue cleanly mpu.resetFIFO(); Serial.println(F("FIFO overflow!")); // otherwise, check for DMP data ready interrupt (this should happen frequently) } else if (mpuIntStatus & 0x02) { // 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; mpu.dmpGetQuaternion(&q, fifoBuffer); mpu.dmpGetGravity(&gravity, &q); mpu.dmpGetYawPitchRoll(ypr, &q, &gravity); input = ypr[1] * 180/M_PI + 180; } }
本文中的Kp、Ki、Kd的值可能适用也可能不适用你的机器人。如果不适用,那么按照上面PID控制原理中提到的方法来设当调整。注意,代码中的倾斜值设置为173度。如果有必要,你可以更改这个值,这是机器人必须保持的倾斜角度。此外,如果你的电机速度太快,你可以适当调整motorSpeedFactorLeft和motorSpeedFactorRight值,一个成功的DIY是需要不断调整的,重点是要有耐心,最后希望大家都能完成自己的自平衡机器人。