1. 项目概述
差速驱动是轮式机器人运动控制的核心技术之一,通过左右轮的速度差实现转向。这个项目基于Arduino平台,采用无刷直流电机(BLDC)作为执行机构,实现了速度与位置的双闭环协同控制。相比传统的有刷电机方案,BLLC电机具有效率高、寿命长、扭矩大等优势,特别适合需要精确运动控制的机器人应用。
我在实际搭建一个仓储AGV原型机时,发现市面上的差速驱动方案要么成本过高,要么精度不足。于是决定自己开发这套系统,经过三个版本的迭代,最终实现了0.1mm级的位置控制精度和±5%的速度跟踪误差。下面将详细分享从硬件选型到控制算法的完整实现过程。
2. 硬件系统设计
2.1 核心组件选型
电机与驱动器选型对比表:
| 型号 | 额定功率 | 额定转速 | 编码器分辨率 | 价格 | 选择理由 |
|---|---|---|---|---|---|
| DJI M3508 | 150W | 469RPM | 8192PPR | ¥680 | 高精度但超预算 |
| ODrive 5065 | 300W | 2000RPM | 4096PPR | ¥1200 | 性能优秀但接口复杂 |
| 国产57BLDC | 100W | 300RPM | 2000PPR | ¥220 | 性价比最优,满足需求 |
最终选用国产57BLDC电机配套TB6612FNG驱动器,主要考虑:
- 成本控制在500元以内
- 编码器分辨率满足1mm/脉冲的定位需求
- 驱动器支持PWM和方向信号输入,兼容Arduino输出
注意:购买时要确认电机轴径与轮毂匹配。我第一版就因轴径不符导致返工。
2.2 电路连接细节
典型接线示意图:
code复制Arduino Uno
├── D9 → TB6612FNG PWMA
├── D8 → TB6612FNG AIN1
├── D7 → TB6612FNG AIN2
├── D5 → TB6612FNG PWMB
├── D4 → TB6612FNG BIN1
├── D3 → TB6612FNG BIN2
├── A0 ← 左电机编码器A相
├── A1 ← 左电机编码器B相
├── A2 ← 右电机编码器A相
├── A3 ← 右电机编码器B相
└── 5V → 编码器供电
特别说明:
- 编码器AB相接法影响方向判断,接反会导致位置计算错误
- 电机电源需单独供电,建议使用3S锂电池(11.1V)
- 务必在电机电源端加装1000μF电容滤波
3. 软件控制架构
3.1 双闭环控制原理
采用位置环为外环、速度环为内环的串级控制结构:
code复制目标位置 → 位置PID → 目标速度 → 速度PID → PWM输出
↑ ↑
编码器位置反馈 编码器速度反馈
关键参数计算示例:
- 速度计算:Δpos/(Δt*PPR)*60 (RPM)
- 位置精度:轮周长/PPR = (π*65mm)/2000 ≈ 0.1mm
3.2 核心代码实现
cpp复制// 电机控制结构体
typedef struct {
volatile long encoderPos; // 编码器累计值
int pwmPin, in1Pin, in2Pin;
float kp, ki, kd; // PID参数
float targetVel, actualVel;
float targetPos, actualPos;
} Motor;
Motor left, right;
void setup() {
// 编码器中断设置
attachInterrupt(digitalPinToInterrupt(A0), leftEncoderISR, CHANGE);
attachInterrupt(digitalPinToInterrupt(A2), rightEncoderISR, CHANGE);
// PID参数初始化
left.kp = 0.8; left.ki = 0.05; left.kd = 0.1;
right.kp = 0.8; right.ki = 0.05; right.kd = 0.1;
}
void loop() {
static uint32_t lastTime = 0;
if(millis() - lastTime >= 10) { // 10ms控制周期
updateVelocity(&left);
updateVelocity(&right);
positionControl(&left);
positionControl(&right);
velocityControl(&left);
velocityControl(&right);
lastTime = millis();
}
}
// 位置环PID计算
void positionControl(Motor* m) {
float err = m->targetPos - m->actualPos;
m->targetVel = constrain(err * m->kp, -MAX_VEL, MAX_VEL);
}
// 速度环PID计算
void velocityControl(Motor* m) {
static float lastErr[2] = {0};
static float integral[2] = {0};
float err = m->targetVel - m->actualVel;
integral[m==&left?0:1] += err;
integral[m==&left?0:1] = constrain(integral[m==&left?0:1], -I_LIMIT, I_LIMIT);
float output = err * m->kp
+ integral[m==&left?0:1] * m->ki
+ (err - lastErr[m==&left?0:1]) * m->kd;
setMotorPWM(m, output);
lastErr[m==&left?0:1] = err;
}
4. 运动控制算法
4.1 差速运动模型
机器人运动学模型:
code复制V = (Vr + Vl)/2 // 线速度
ω = (Vr - Vl)/L // 角速度 (L为轮距)
逆解算公式:
code复制Vr = V + ω*L/2
Vl = V - ω*L/2
4.2 轨迹生成实现
cpp复制// 生成直线运动指令
void moveLinear(float distance, float velocity) {
float target = distance / (PI * WHEEL_DIAMETER) * ENCODER_PPR;
left.targetPos += target;
right.targetPos += target;
left.targetVel = velocity;
right.targetVel = velocity;
}
// 生成旋转指令
void rotate(float angle, float omega) {
float distance = angle * WHEEL_TRACK / 2;
float target = distance / (PI * WHEEL_DIAMETER) * ENCODER_PPR;
left.targetPos -= target;
right.targetPos += target;
left.targetVel = -omega * WHEEL_TRACK/2;
right.targetVel = omega * WHEEL_TRACK/2;
}
5. 调试与优化
5.1 PID参数整定步骤
-
先调速度环:
- 将Ki、Kd设为0,逐步增大Kp直到出现等幅振荡
- 取振荡时Kp值的60%作为最终Kp
- 增加Ki消除静差,通常Kp/10开始尝试
- 最后加Kd抑制超调,一般为Kp/100
-
再调位置环:
- 通常只需比例控制(Kp)
- 过大会导致振荡,过小响应慢
- 建议从0.5开始尝试
5.2 实测性能数据
| 指标 | 空载 | 5kg负载 |
|---|---|---|
| 最大速度 | 1.2m/s | 0.8m/s |
| 定位重复精度 | ±0.1mm | ±0.3mm |
| 速度跟踪误差 | ±3% | ±7% |
| 急停制动距离 | 50mm | 80mm |
6. 常见问题解决
6.1 电机抖动问题排查
可能原因及解决方案:
- 电源不足 → 测量电压是否低于9V
- PID参数过激 → 降低Kp/Ki值
- 机械共振 → 在电机轴加橡胶垫
- 编码器干扰 → 使用双绞线并加磁环
6.2 位置累积误差处理
采用以下策略组合:
- 定期归零校正(如限位开关)
- 增加轨迹途经点强制校正
- 使用IMU辅助航位推算
- 视觉/激光等绝对定位补偿
7. 扩展应用
7.1 加入遥控功能
通过PS2手柄扩展:
cpp复制void handleJoystick() {
int ly = map(ps2.Analog(PSS_LY), 0, 255, -255, 255);
int rx = map(ps2.Analog(PSS_RX), 0, 255, -255, 255);
left.targetVel = ly - rx;
right.targetVel = ly + rx;
}
7.2 上位机监控实现
使用Processing开发监控界面:
java复制void setup() {
Serial.begin(115200);
// 初始化界面
}
void draw() {
if(Serial.available()) {
String data = Serial.readStringUntil('\n');
float[] vals = float(split(data, ','));
// 更新实时曲线显示
}
}
Arduino端添加数据输出:
cpp复制void sendTelemetry() {
Serial.print(left.actualPos); Serial.print(",");
Serial.print(right.actualPos); Serial.print(",");
Serial.print(left.actualVel); Serial.print(",");
Serial.println(right.actualVel);
}
8. 项目优化方向
- 电流环控制:增加FOC算法实现更平滑的转矩控制
- 动态参数调整:根据负载自动调节PID参数
- 运动预测:提前计算加速度限制避免打滑
- 故障自恢复:检测堵转、失步等异常状态
这个项目从第一版简陋的速度控制,到现在的全状态反馈控制,前后迭代了5个月。最大的体会是:机械结构的刚性直接影响控制性能,建议先确保机械安装精度再调试软件。另外,无刷电机的启动特性与有刷电机不同,需要特别注意低速时的控制策略。