1. 项目概述:从零打造你的掌上平衡车
去年冬天,我在工作室里折腾这个小玩意儿的时候,怎么也没想到它会引起这么多朋友的兴趣。这个基于ESP32的迷你平衡车项目,不仅让我重新认识了PID控制的精妙之处,更让我体会到嵌入式开发的乐趣所在。
miniBot的核心原理其实很简单:通过MPU6050传感器感知车身倾斜角度,用ESP32计算控制量,再通过电机驱动板调整两个轮子的转速,让小车保持直立平衡。但要把这个看似简单的原理变成实际可用的装置,中间需要跨越不少技术门槛。
这个项目特别适合已经有一定Arduino基础,想要挑战更复杂嵌入式系统的开发者。通过完成它,你将掌握:
- 六轴传感器的数据采集与姿态解算
- 电机PWM控制与闭环调速
- PID算法的实际应用与参数整定
- ESP32的双核任务调度与无线通信
提示:在开始前请确保你已具备基本的电路焊接能力和Arduino编程经验。如果还没玩过ESP32,建议先完成几个基础项目(如WiFi控制LED)热热身。
2. 硬件选型与电路设计
2.1 核心部件选型指南
选择硬件时我踩过不少坑,这里分享我的实战经验:
主控芯片:
ESP32-WROOM-32是性价比之选,但要注意区分不同版本。我推荐用ESP32-S3,它的外设更丰富(USB OTG、更多PWM通道)。如果预算充足,可以考虑ESP32-C3,RISC-V架构功耗更低。
姿态传感器:
MPU6050确实便宜好用,但更推荐MPU9250(集成磁力计)或BMI160。实测发现MPU6050的温度漂移较明显,需要更频繁的校准。购买时认准GY-521模块,山寨货的I2C地址可能不对。
电机驱动:
DRV8833和TB6612我都用过,对比下来:
- DRV8833:最大电流1.5A,内置过流保护,但发热较大
- TB6612:双路1.2A,效率更高,需要外接保护二极管
新手建议用TB6612,它的逻辑电压兼容性更好。
电机选择:
N20电机要选带编码器的版本!我最初用了不带编码器的,速度控制简直是一场噩梦。关键参数:
- 减速比:1:50左右比较合适
- 额定电压:6V(实际用7.4V锂电池驱动没问题)
- 编码器分辨率:建议12PPR以上
2.2 电源系统设计详解
电源设计是平衡车稳定的关键,这是我的方案:
code复制7.4V锂电池
├─ LM1084 → 5V → 主控外设
│ └─ AMS1117 → 3.3V → ESP32
└─ DRV8833 → 电机
实测数据:
- 空载时电机电流约0.2A
- 加速瞬间峰值电流可达1.8A
- ESP32+传感器常态功耗约120mA
警告:千万不要把MPU6050的VCC接到5V!我烧过两个传感器才记住这个教训。虽然有些手册说支持5V,但长期使用会导致数据漂移。
2.3 关键接线方案优化
原方案的接线有几个可以改进的地方:
I2C总线优化:
- SDA/SCL线上建议加1kΩ上拉电阻
- 线长超过10cm时要采用双绞线
- MPU6050的AD0引脚最好通过跳线选择地址
电机驱动改进:
c复制// 更合理的引脚分配
#define MOTOR_AIN1 12 // 左电机PWM1
#define MOTOR_AIN2 13 // 左电机PWM2
#define MOTOR_BIN1 14 // 右电机PWM1
#define MOTOR_BIN2 15 // 右电机PWM2
#define MOTOR_STBY 16 // 驱动芯片使能
加入STBY控制可以在异常时快速切断电机电源,这个硬件保护措施非常必要。
3. 软件架构深度解析
3.1 开发环境配置技巧
Arduino IDE虽然简单,但对于这个项目我推荐改用PlatformIO。这是我的配置步骤:
- 安装VSCode + PlatformIO插件
- 创建新项目,选择ESP32开发板
- 在platformio.ini中添加:
ini复制lib_deps =
tockn/MPU6050_tockn@^1.5.2
powerbroker2/ArduPID@^1.2.0
espressif/ESP32Servo@^0.11.0
PlatformIO的库管理更智能,还能方便地启用ESP32的双核特性。
3.2 核心代码模块剖析
主程序框架有几个关键优化点:
双核任务分配:
c复制void core0Task(void *pvParameters) {
// 核心0负责传感器读取和PID计算
while(1) {
mpu6050.update();
calculatePID();
vTaskDelay(2 / portTICK_PERIOD_MS); // 500Hz
}
}
void core1Task(void *pvParameters) {
// 核心1负责电机控制和通信
while(1) {
updateMotors();
handleBluetooth();
vTaskDelay(5 / portTICK_PERIOD_MS); // 200Hz
}
}
这样分配可以确保PID计算的实时性,避免蓝牙通信影响控制周期。
改进的PID实现:
c复制class BalancePID {
public:
BalancePID(float kp, float ki, float kd) {
// 初始化参数
}
float compute(float input, float setpoint) {
float error = setpoint - input;
_integral += error * _dt;
_integral = constrain(_integral, -_maxIntegral, _maxIntegral);
float derivative = (error - _lastError) / _dt;
_lastError = error;
return _kp*error + _ki*_integral + _kd*derivative;
}
private:
float _kp, _ki, _kd;
float _integral = 0;
float _lastError = 0;
float _maxIntegral = 1000;
float _dt = 0.01; // 100Hz控制周期
};
这个类封装了PID算法,支持抗积分饱和和微分滤波,比原始方案更稳定。
3.3 姿态解算算法升级
原始方案只用了互补滤波,我融合了Mahony算法:
c复制void updateIMU() {
mpu6050.update();
// Mahony AHRS算法
float gx = mpu6050.getGyroX() * DEG_TO_RAD;
float gy = mpu6050.getGyroY() * DEG_TO_RAD;
float gz = mpu6050.getGyroZ() * DEG_TO_RAD;
float ax = mpu6050.getAccX();
float ay = mpu6050.getAccY();
float az = mpu6050.getAccZ();
// 加速度计归一化
float recipNorm = 1.0 / sqrt(ax*ax + ay*ay + az*az);
ax *= recipNorm;
ay *= recipNorm;
az *= recipNorm;
// 计算误差
float halfvx = q2*q3 - q1*q4;
float halfvy = q1*q3 + q2*q4;
float halfvz = q1*q1 + q2*q2 - 0.5f;
float halfex = ay*halfvz - az*halfvy;
float halfey = az*halfvx - ax*halfvz;
float halfez = ax*halfvy - ay*halfvx;
// 积分误差
integralFBx += twoKi*halfex*dt;
integralFBy += twoKi*halfey*dt;
integralFBz += twoKi*halfez*dt;
// 应用反馈
gx += twoKp*halfex + integralFBx;
gy += twoKp*halfey + integralFBy;
gz += twoKp*halfez + integralFBz;
// 四元数积分
gx *= 0.5*dt;
gy *= 0.5*dt;
gz *= 0.5*dt;
float qa = q1;
float qb = q2;
float qc = q3;
q1 += (-qb*gx - qc*gy - q4*gz);
q2 += (qa*gx + qc*gz - q4*gy);
q3 += (qa*gy - qb*gz + q4*gx);
q4 += (qa*gz + qb*gy - qc*gx);
// 归一化
recipNorm = 1.0 / sqrt(q1*q1 + q2*q2 + q3*q3 + q4*q4);
q1 *= recipNorm;
q2 *= recipNorm;
q3 *= recipNorm;
q4 *= recipNorm;
// 转换为欧拉角
roll = atan2(q1*q2 + q3*q4, 0.5 - q2*q2 - q3*q3) * RAD_TO_DEG;
pitch = asin(-2.0*(q2*q4 - q1*q3)) * RAD_TO_DEG;
}
这个算法对快速运动时的姿态估计更准确,但计算量较大,需要测试ESP32能否实时处理。
4. PID控制实战技巧
4.1 双环PID控制实现
我最终采用的控制器结构:
code复制角度环(PID)
↓
速度环(PI) ← 编码器反馈
↓
电机输出
具体实现:
c复制void balanceControl() {
// 角度环
float angleError = targetPitch - currentPitch;
float angleOutput = anglePID.compute(angleError);
// 速度环
float speedError = targetSpeed - currentSpeed;
float speedOutput = speedPID.compute(speedError);
// 混合输出
float output = angleOutput + speedOutput;
setMotorOutput(output);
}
关键点:
- 角度环用PD控制(ki=0),响应快
- 速度环用PI控制(kd=0),平滑速度波动
- 两个环的输出直接相加,权重通过PID参数调节
4.2 PID参数调试实录
我的调试过程记录:
第一阶段:直立控制
- 初始参数:kp=2, ki=0, kd=0
- 现象:小车剧烈振荡
- 调整:kp=1.5, kd=5
- 现象:振荡减弱,但恢复慢
- 最终:kp=1.8, ki=0, kd=8
- 效果:能保持平衡3-5秒
第二阶段:速度环加入
- 初始参数:kp=0.1, ki=0.5, kd=0
- 现象:小车缓慢移动但容易倒
- 调整:kp=0.08, ki=0.3
- 效果:移动平稳,能自动维持位置
调试技巧:用蓝牙实时调整参数比反复烧录高效多了。我写了个简单的协议:
PID_SET angle_kp=1.5,angle_kd=8,speed_kp=0.08,speed_ki=0.3
4.3 常见问题解决方案
问题1:小车总是向一侧偏
- 检查步骤:
- 放在水平面上,查看MPU6050的原始加速度值
- 检查机械结构是否对称
- 调整targetAngle微调平衡点
- 我的解决方法:在代码中加入自动零点校准
问题2:电机发出吱吱声但不转
- 可能原因:
- PWM频率太低(应1-3kHz)
- 电机驱动供电不足
- 死区时间设置不当
- 解决方法:
c复制ledcSetup(0, 2000, 8); // 2kHz PWM
ledcWrite(0, 255); // 测试全占空比是否转动
问题3:快速移动时失控
- 优化方案:
- 加入加速度限制
c复制void setMotorOutput(float output) { static float lastOutput = 0; float delta = constrain(output - lastOutput, -MAX_ACCEL, MAX_ACCEL); lastOutput += delta; // 实际输出 }- 增加前馈控制补偿惯性
5. 进阶功能开发
5.1 编码器速度反馈实现
带编码器的N20电机接线:
code复制编码器A相 → ESP32 GPIO (外部中断引脚)
编码器B相 → ESP32 GPIO
速度测量代码优化:
c复制void IRAM_ATTR leftEncoderISR() {
if(digitalRead(ENC_A_LEFT) == digitalRead(ENC_B_LEFT)) {
leftEncoderCount++;
} else {
leftEncoderCount--;
}
}
float getSpeed() {
static uint32_t lastTime = 0;
static long lastCount = 0;
uint32_t currentTime = millis();
long currentCount = leftEncoderCount;
float dt = (currentTime - lastTime) / 1000.0;
float speed = (currentCount - lastCount) / (PPR * dt);
lastTime = currentTime;
lastCount = currentCount;
return speed * 60; // 转为RPM
}
5.2 WiFi功能扩展实例
通过AsyncWebServer实现控制页面:
c复制#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
AsyncWebServer server(80);
void setupWebServer() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
String html = "<form action='/control'>"
"Pitch: <input type='range' name='pitch' min='-30' max='30'>"
"<input type='submit'></form>";
request->send(200, "text/html", html);
});
server.on("/control", HTTP_GET, [](AsyncWebServerRequest *request){
if(request->hasParam("pitch")) {
targetPitch = request->getParam("pitch")->value().toFloat();
}
request->send(200, "text/plain", "OK");
});
server.begin();
}
5.3 安全保护机制完善
多级保护系统设计:
- 软件保护:
c复制if(fabs(currentPitch) > 45.0) {
motorStop(); // 角度过大立即停止
emergencyFlag = true;
}
- 硬件保护:
- 在电机电源线上串联MOSFET,可由ESP32控制通断
- 添加物理急停开关,直接切断主电源
- 电池保护:
c复制float batteryVoltage = analogRead(BATT_PIN) * 3.3 / 4096 * (R1+R2)/R2;
if(batteryVoltage < 6.0) {
shutdown(); // 电量过低保护
}
6. 项目优化与改进方向
经过三个版本的迭代,我的miniBot已经能稳定运行30分钟以上。以下是一些优化建议:
机械结构改进:
- 采用碳纤维框架减轻重量
- 使用橡胶轮胎增加摩擦力
- 重心高度控制在轮轴下方1-2cm
控制算法升级:
- 尝试LQR控制替代PID
- 加入机器学习自动调参
- 实现MPC模型预测控制
功能扩展:
- 添加ToF传感器实现避障
- 接入HomeAssistant智能家居系统
- 开发竞技模式(平衡车相扑)
这个项目最让我惊喜的是ESP32的性能表现——双核240MHz的主频不仅能流畅运行控制算法,还能同时处理WiFi和蓝牙通信。如果你也想挑战更复杂的控制项目,ESP32绝对是性价比之选。
