使用Arduino构建自平衡机器人
开源硬件Arduino社群各种创意应用层出不穷,学习门槛越来越地,今天我们就尝试从小白视角,不求甚解动手构建一个自平衡机器人
材料收集
开源社区这类资料特别丰富,可以网络搜索从扫盲到实际动手操作都有很详细的讯息
从小白的角度,我们透过各种搜索,阅读,首先需要知道做这样的一个自平衡机器人,需要哪些材料? =》这样好从淘宝购买一些物美价廉的东西:)
经过若干次网络搜索,阅读,基本了解构建这样的一个自平衡机器人需要的材料如下:
- Arduino控制板这里选用Uno or Nano,代码一模一样,唯一差异就是pin接口,以及连线;淘宝选国产的,不贵,才十几块
- 陀螺仪MPU6050,物美价廉,淘宝才几块钱
- 双轴减速电机,淘宝上一大堆,黄色的那个电机,我选择1:48减速比的电机
- 轮子,一般和上面的双轴电机配套一起买,一两块钱
- L298N电机驱动模块,可以驱动2个直流电机,同时可以输出5V电压给Arduino控制板供电;这样使用一路供电(比如2节或者3节16850电池),给电机与控制板同时供电
- 16850锂电池(2节,3节,甚至4节也可以)
- 16850电池盒(最好买带开关的盒子)
- 框架材料,我通常选择雪弗板(一般购买20cm*20cm大小,厚度>5mm,一般选8mm或者10mm)便于美工到切割
- 热熔胶
- 电烙铁可能需要(焊接)
- 美工刀
需要的软件
- Arduino集成开发环境
Arduono控制板
使用UNO一样的效果,nano结构更小;同样基于ATmega328处理器
陀螺仪MPU6050
带有3轴加速度计以及三轴陀螺仪的模块,以及能够处理复杂的运动融合算法的数字运动处理器(DMP)
L298N电机驱动模块
双轴减速电机
16850锂电池
这个Arduino机器人DIY经常使用的电池,价格不贵,容量一般2000多毫安, 使用该电池可以非常方便满足3.7伏要求,一般我使用2节
可以考虑购买一个带开关的电池盒,方便重复使用,免得焊线
制作框架
对于这个项目,我使用雪弗板切割而成,加工起来也比较方便,强度对于这小车来说基本足够
我看到网上很多教程建议将电池,电机等重物保持再底部,有一定道理,不过我把电池(最终的部分)放置在最上面,依然保持平衡很好(关键是控制参数调试的恰当)
电路图
该机器人的接线很简单, mpu6050测量运动方向上的倾斜,Arduino使用L298N电机驱动设置电机速度以克服倾斜
这里我们使用的mpu6050传感器,经济可靠。 其精度以及同时测量所有三轴旋转和运动的能力时其非常适合用于自平衡机器人。 L298N也是一款非常适合在此使用的低成本双H桥电机驱动器
MPU6050我放置在顶部,其Z轴朝向地球,当然我们可以将该传感器放置在框架中的任何位置。方向可以根据框架和设计进行修改。为此需要更改代码
由于我们使用L298N作为电机驱动器,同时也作为Arduino的电源,所以放置电源跳线,这样我们就可以使用+5v引脚作为电源。
Arduino代码
需要安装依赖的库
- PID_v1
- LMotorController
- I2Cdev
- MPU6050
完整代码:
/*
* Arduino Uno
* L298N
* HC-06 BT module
* MPU6050
* by 麦克不能飞, admin@dazuicheng.com
* www.dazuicheng.com
* 1. 如何可视化PID tuning??
* 2.勉强可以工作,但是抗扰动能力稍微有点弱?
*/
#include <PID_v1.h>
#include <LMotorController.h>
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps20.h"
#include <SoftwareSerial.h>
SoftwareSerial bluetoothSerial(4, 3); // RX, TX for bluetooth module control(HC-06 here)4和3分别表示蓝牙模块的rx,tx
#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 = 179.60;
double setpoint = originalSetpoint;
double movingAngleOffset = 0.1;
double input, output;
//my setting, cost a lot of time to fine tuning those parameters,adjust to fit your own design
double Kp = 39; //45; 地毯上可以工作,地板上有点jitter
double Kd = 1.6; //1.8
double Ki = 400; //400;这貌似可以不用这么大,还可以优化?
PID pid(&input, &output, &setpoint, Kp, Ki, Kd, DIRECT);
double motorSpeedFactorLeft = 0.8;
double motorSpeedFactorRight = 0.7;
//MOTOR CONTROLLER,因为可能电机+-接线弄反了,所这里IN1,IN2定义与实际接线故意反了(实际是IN1->6, IN2->7)
int ENA = 5;
int IN1 = 7;
int IN2 = 6;
int IN3 = 9;
int IN4 = 8;
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()
{
Serial.begin(115200);
bluetoothSerial.begin(9600);
// 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();
// 这部分偏移,只需要校正一次,使用mpu6050库里面的例子:IMU_Zero校正一下,记录这些偏移,靠不到这里
mpu.setXAccelOffset(314);//-4259
mpu.setYAccelOffset(1177);//749
mpu.setZAccelOffset(931); // 1151,1688 factory default for my test chip
mpu.setXGyroOffset(-12);//31
mpu.setYGyroOffset(184);//557
mpu.setZGyroOffset(-1);//34
// 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()
{
//randomSeed(analogRead(0)); // 使用模拟引脚0的读取值作为随机数种子
String received_from_bt = "";
while (bluetoothSerial.available()) {
char data = bluetoothSerial.read();
received_from_bt += data;
// 在这里可以添加处理接收到的数据的代码
}
if (received_from_bt.length() > 0) {
Serial.print("Received from Bluetooth: ");
Serial.println(received_from_bt);
}
// if programming failed, don't try to do anything
if (!dmpReady) return;
//Serial.println(setpoint);
// 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();
Serial.print(setpoint); Serial.print(",");Serial.print(input); Serial.print(","); Serial.println(output);
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;
}
}
平衡测试与调整
这种类型的自平衡机器人是倒立摆,类似于在手指上平衡一根棍子。在这里,车轮的旋转提供了防止跌落的平衡力。为了平衡这个机器人,你必须配置 PID 值,直到机器人达到稳定的位置和恢复能力。
为了使此任务变得简单,代码使用了 Arduino PID 的现成库。要根据您的设计调整 PID,您必须手动输入 P、I、D 值,并通过将代码上传到 Arduino 来检查稳定性(如上图中代码的以下部分)。
方法是先将Ki、Kd都设置为0,然后输入Kp的值,然后上传代码。经过几次尝试后,机器人在一段时间内保持平衡,但滚动时出现过度振荡,此时必须在不干扰 Kp 值的情况下设置 Kd 值。设置 Kd 以便发生最小振荡,这与其他两个一样手动调整 Ki。
此外,您还可以设置上图所示角度的设定值和偏移。但在许多情况下并不需要它,并且仅当陀螺仪有偏移时才进行设置。
经过这个反复试验阶段后,您将为您的机器人找到合适的值。这个过程有点棘手,如果您是控制系统的初学者和新手,有时需要几个小时才能找到完美的值。