为什么需要闭环控制?
开环控制(比如直接用 PWM 驱动电机)有个致命问题:你不知道电机实际转了多少。负载变化、电压波动、摩擦力变化都会导致实际转速和位置偏离预期。
闭环控制通过编码器实时反馈电机实际状态,与目标值比较后动态调整输出,实现精准控制。这就是为什么 CNC 机床、机器人关节、云台相机都用闭环系统。
编码器类型详解
增量式编码器(Incremental Encoder)
最常见、最便宜。输出两路相位差 90° 的方波(A 相和 B 相),通过计数脉冲数计算转角,通过相位判断方向。
优点: 结构简单、成本低、响应快
缺点: 断电后丢失位置信息,需要回零操作
绝对式编码器(Absolute Encoder)
每个位置对应唯一的二进制编码,断电后位置不丢失。
优点: 无需回零、位置绝对准确
缺点: 价格高、接线复杂
磁编码器 vs 光编码器
| 类型 | 精度 | 成本 | 抗污染 | 典型分辨率 |
|---|---|---|---|---|
| 光电式 | 高 | 中 | 差 | 1000-5000 PPR |
| 磁电式 | 中 | 低 | 好 | 100-4096 PPR |
| 电容式 | 高 | 高 | 中 | 1000-10000 PPR |
DIY 项目推荐磁编码器(如 AS5600),便宜又耐造。
硬件清单
| 部件 | 型号 | 单价 | 数量 | 备注 |
|---|---|---|---|---|
| 直流电机 | N20 减速电机 | ¥25 | 1 | 带编码器版本 |
| 磁编码器 | AS5600 | ¥15 | 1 | I2C 接口,12 位分辨率 |
| 电机驱动 | TB6612FNG | ¥8 | 1 | 双路 H 桥,支持 PWM |
| 主控板 | Arduino Nano | ¥15 | 1 | 或 ESP32 |
| 电源 | 12V 2A 适配器 | ¥20 | 1 | 根据电机电压选择 |
| 杜邦线 | – | ¥5 | 1 包 | 连接线 |
总计:约¥88
AS5600 磁编码器接线
AS5600 支持 I2C 和模拟电压输出,这里用 I2C 模式:
| AS5600 | Arduino Nano |
|---|---|
| VCC | 5V |
| GND | GND |
| SCL | A5 (SCL) |
| SDA | A4 (SDA) |
| OUT | 悬空(I2C 模式不用) |
电机磁铁安装在电机轴上,AS5600 固定在距离磁铁 1-3mm 的位置。
闭环控制原理
闭环控制的核心是 PID 算法:
误差 = 目标位置 - 实际位置
输出 = Kp×误差 + Ki×误差积分 + Kd×误差微分
- P(比例): 误差越大,输出越大。但纯 P 控制会有稳态误差
- I(积分): 累积历史误差,消除稳态误差。但过大会导致超调
- D(微分): 预测误差变化趋势,抑制超调。对噪声敏感
完整代码示例
1. 读取编码器角度
#include
#include
AS5600 as5600;
void setup() {
Serial.begin(115200);
Wire.begin();
as5600.begin(Wire);
}
void loop() {
int angle = as5600.readAngle(); // 0-4095 (12 位)
float degrees = angle * 360.0 / 4096.0;
Serial.print("Angle: ");
Serial.print(angle);
Serial.print(" (");
Serial.print(degrees);
Serial.println("°)");
delay(100);
}
2. 完整闭环位置控制
#include
#include
// 编码器
AS5600 as5600;
// 电机驱动引脚
const int PWM_PIN = 9;
const int IN1_PIN = 7;
const int IN2_PIN = 8;
// PID 参数
float Kp = 2.0;
float Ki = 0.5;
float Kd = 1.0;
// 控制变量
int targetPosition = 0;
int currentPosition = 0;
float integral = 0;
float lastError = 0;
unsigned long lastTime = 0;
void setup() {
Serial.begin(115200);
Wire.begin();
as5600.begin(Wire);
pinMode(PWM_PIN, OUTPUT);
pinMode(IN1_PIN, OUTPUT);
pinMode(IN2_PIN, OUTPUT);
// 初始化电机停止
analogWrite(PWM_PIN, 0);
}
void loop() {
// 读取目标位置(串口输入)
if (Serial.available()) {
targetPosition = Serial.parseInt();
integral = 0; // 重置积分
lastError = 0;
Serial.print("Target: ");
Serial.println(targetPosition);
}
// 读取当前位置
currentPosition = as5600.readAngle();
// PID 计算
int error = targetPosition - currentPosition;
unsigned long currentTime = millis();
float dt = (currentTime - lastTime) / 1000.0; // 秒
if (dt > 0) {
integral += error * dt;
float derivative = (error - lastError) / dt;
float output = Kp * error + Ki * integral + Kd * derivative;
// 限制输出范围
output = constrain(output, -255, 255);
// 驱动电机
driveMotor(output);
lastError = error;
lastTime = currentTime;
}
// 调试输出
Serial.print("Pos: ");
Serial.print(currentPosition);
Serial.print(" Err: ");
Serial.print(error);
Serial.print(" Out: ");
Serial.println((int)output);
delay(10); // 100Hz 控制频率
}
void driveMotor(float pwm) {
if (pwm > 0) {
digitalWrite(IN1_PIN, HIGH);
digitalWrite(IN2_PIN, LOW);
analogWrite(PWM_PIN, pwm);
} else if (pwm < 0) {
digitalWrite(IN1_PIN, LOW);
digitalWrite(IN2_PIN, HIGH);
analogWrite(PWM_PIN, -pwm);
} else {
analogWrite(PWM_PIN, 0);
}
}
3. PID 参数整定技巧
没有万能参数,必须根据实际系统调整:
- 先调 P: 设置 Ki=0, Kd=0,增大 Kp 直到系统开始振荡,然后减小到 70%
- 再加 D: 逐渐增大 Kd 抑制超调,直到响应平滑
- 最后加 I: 如果有稳态误差,慢慢增大 Ki 消除
经验值参考:
- 小电机(N20):Kp=1.5-3.0, Ki=0.3-0.8, Kd=0.5-1.5
- 中电机(37GB):Kp=2.0-4.0, Ki=0.5-1.2, Kd=1.0-2.5
- 大电机:需要更大 Kp 和 Kd
速度闭环控制
位置控制是目标角度,速度控制是目标转速:
// 速度控制模式
float targetSpeed = 100; // 度/秒
float currentSpeed = 0;
int lastPosition = 0;
void loop() {
int currentPosition = as5600.readAngle();
// 计算速度(考虑编码器溢出)
int delta = currentPosition - lastPosition;
if (delta > 2048) delta -= 4096;
if (delta < -2048) delta += 4096;
currentSpeed = delta * 100.0; // 10ms 间隔,转换为度/秒
lastPosition = currentPosition;
// 速度 PID
float error = targetSpeed - currentSpeed;
// ... 后续 PID 计算同位置控制
}
常见问题排查
问题 1:编码器读数跳动
原因: 磁铁安装距离不当或干扰
解决:
- 调整磁铁与传感器距离到 1-3mm
- 检查电源稳定性,加 100uF 电容
- 使用屏蔽线或双绞线
问题 2:电机振荡不止
原因: PID 参数过大
解决:
- 减小 Kp 和 Kd
- 检查控制频率是否稳定(用 micros() 而不是 delay)
- 增加微分滤波:
derivative = 0.7 * lastDerivative + 0.3 * rawDerivative
问题 3:有稳态误差
原因: Ki 太小或积分限幅
解决:
- 增大 Ki(每次 10-20%)
- 检查积分是否被意外清零
- 确认电机驱动有能力克服摩擦力
问题 4:响应太慢
原因: Kp 太小或控制频率低
解决:
- 增大 Kp
- 提高控制频率到 200-500Hz
- 检查电机驱动电流是否足够
进阶优化
1. 积分限幅
防止积分累积过大导致超调:
float integralMax = 500;
integral = constrain(integral, -integralMax, integralMax);
2. 微分滤波
减少噪声影响:
float derivativeFilter = 0.8;
derivative = derivativeFilter * lastDerivative +
(1 - derivativeFilter) * rawDerivative;
3. 死区补偿
克服静摩擦力:
if (abs(output) < 20 && abs(error) > 50) {
output = (output > 0) ? 20 : -20;
}
实际应用场景
- 云台相机: 保持相机稳定,抵消手部抖动
- 机器人关节: 精准控制机械臂角度
- 平衡车: 实时调整电机保持平衡
- CNC 进给轴: 精确控制刀具位置
- 卷扬机: 恒张力控制
总结
闭环控制是电机控制的进阶技能,核心在于:
- 选对编码器: DIY 用 AS5600 足够,工业用多圈绝对编码器
- 调好 PID: 没有捷径,必须实际测试
- 注意细节: 电源、接线、控制频率都会影响性能
从开环到闭环,你的项目会从"能动"升级到"精准可控"。
希望这篇博客文章对您有所帮助!