嵌入式开发 电机控制完全指南:步进/伺服/直流电机 + 闭环控制实战
引言
电机控制是嵌入式开发中最基础也最重要的技能之一。无论是 CNC 雕刻机、机器人关节、智能小车还是太阳能系统,都离不开对电机的精准控制。
本指南从 5 个经典实战项目出发,覆盖**步进电机(A4988)、舵机(SG90/MG996)、直流电机(L298N PWM)、编码器闭环 PID 控制、太阳能充电控制器(PWM vs MPPT)**五大主题,将零散知识整合为一份完整的参考资料。
一、步进电机精准控制:A4988 驱动详解
1.1 为什么选择步进电机?
步进电机可以精准控制转动角度,不需要编码器就能实现开环位置控制。NEMA 17(42mm)步进电机搭配 A4988 驱动器,是 CNC 雕刻机、3D 打印机、机器人云台的核心方案。
1.2 硬件清单
| 物品 | 型号 | 价格 | 备注 |
|---|---|---|---|
| 步进电机 | NEMA 17 (42mm) | ¥25-35 | 1.8° 步距角,1.5A |
| 驱动器 | A4988 | ¥8-12 | 最大 2A,支持微步 |
| 开发板 | Arduino Uno | ¥25 | 或其他兼容板 |
| 电位器 | 10kΩ | ¥2 | 调节电机电流 |
| 电容 | 100μF | ¥1 | 电源滤波 |
| 电源 | 12V 2A | ¥30 | 电机供电 |
总成本:约 ¥100 以内
1.3 A4988 关键引脚
+-----+
VMOT ──────┤1 16│────── GND
GND ──────┤2 15│────── 1B
VDD ──────┤3 14│────── 1A
2B ───────┤4 13│────── 2A
2A ───────┤5 12│────── GND
MS1 ────────┤6 11│────── VDD
MS2 ────────┤7 10│────── STEP
MS3 ────────┤8 9│────── DIR
+-----+
- VMOT/GND:电机电源 (8-35V),必须加 100μF 电容滤波
- VDD/GND:逻辑电源 (3-5.5V),接 Arduino 5V
- 1A/1B/2A/2B:电机线圈输出
- STEP:脉冲信号,每个脉冲走一步
- DIR:方向控制,高电平正转,低电平反转
- MS1/MS2/MS3:微步设置引脚
1.4 微步设置表
| MS1 | MS2 | MS3 | 微步模式 | 每转步数 |
|---|---|---|---|---|
| 0 | 0 | 0 | 全步 | 200 |
| 1 | 0 | 0 | 1/2 步 | 400 |
| 0 | 1 | 0 | 1/4 步 | 800 |
| 1 | 1 | 0 | 1/8 步 | 1600 |
| 1 | 1 | 1 | 1/16 步 | 3200 |
推荐使用 1/16 微步,运行更平滑,噪音更低。
1.5 电流调节(重要!)
A4988 通过参考电压 Vref 设置电机电流,公式:Vref = 电流 × 0.8
对于 1.5A 的 NEMA 17:Vref = 1.5 × 0.8 = 1.2V
调节步骤:
- 断开电机电源,准备万用表(调到直流电压档)
- 给 A4988 通电(只接 VDD,不接 VMOT),避免电机转动
- 用螺丝刀缓慢旋转 A4988 上的电位器
- 测量电位器对 GND 的电压,直到读数接近 1.2V
1.6 Arduino 控制代码
基础版:AccelStepper 库(推荐)
#include <AccelStepper.h>
// 定义步进引脚
#define STEP_PIN 9
#define DIR_PIN 8
// 创建步进对象
AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);
void setup() {
Serial.begin(9600);
stepper.setMaxSpeed(1000); // 最大速度 (步/秒)
stepper.setAcceleration(500); // 加速度 (步/秒²)
stepper.moveTo(3200); // 正转一圈 (@ 1/16 微步)
Serial.println("电机开始运动");
}
void loop() {
if (stepper.distanceToGo() == 0) {
stepper.moveTo(-stepper.currentPosition()); // 反向
Serial.print("当前位置:");
Serial.println(stepper.currentPosition());
}
stepper.run();
}
安装库: Arduino IDE → 工具 → 管理库 → 搜索 “AccelStepper” → 安装
进阶:多轴同步控制(CNC 三轴)
#include <AccelStepper.h>
AccelStepper xAxis(AccelStepper::DRIVER, 2, 3);
AccelStepper yAxis(AccelStepper::DRIVER, 4, 5);
AccelStepper zAxis(AccelStepper::DRIVER, 6, 7);
void setup() {
xAxis.setMaxSpeed(1000);
yAxis.setMaxSpeed(1000);
zAxis.setMaxSpeed(1000);
}
void loop() {
xAxis.moveTo(1000);
yAxis.moveTo(500);
zAxis.moveTo(200);
xAxis.run();
yAxis.run();
zAxis.run();
}
1.7 速度计算
转速 (RPM) = (脉冲频率 × 60) / 每转步数
例如 1/16 微步模式下,每秒 1000 个脉冲:转速 = (1000 × 60) / 3200 = 18.75 RPM
注意: A4988 最大脉冲频率约 200kHz,实际使用建议不超过 50kHz,高速时扭矩会下降。
1.8 常见问题排查
电机不转但有嗡嗡声:
- 检查线圈接线是否正确,用万用表测通断确认同一组线圈
- 增大 Vref 电流设定值(但不要超过电机额定电流)
- 尝试用手拨动电机轴,检查是否机械卡死
驱动器发烫严重:
- 检查 Vref 是否过大,降低到电机额定电流
- 加装散热片(铝制散热片效果明显)
- 检查通风环境,避免密闭空间运行
- 考虑更换为 DRV8825 或 TMC2208(效率更高)
电机抖动不连续:
- 改用更高的微步模式(1/16 或 1/32)
- 避开共振速度区间(通常在中低速范围)
- 检查接线是否牢固,杜邦线接触不良是常见原因
高速时丢步:
- 降低加速度设定,给电机足够的响应时间
- 在额定范围内适当提高电流
- 检查电源供电是否充足(电压跌落会导致扭矩不足)
Arduino 复位:
- 电机供电和 Arduino 供电分开,只共地
- VMOT 端加装 100μF 电解电容滤波
- 检查所有共地点是否牢固
- 必要时使用光耦隔离 STEP/DIR 信号
1.9 项目应用
学会步进电机控制后,可以做:
- DIY 小型 CNC 雕刻机(三轴联动)
- 3D 打印机(配合加热头和挤出机)
- XY 绘图机器人(CoreXY 结构)
- 自动传送带(精确送料)
- 天文望远镜赤道仪(跟踪天体运动)
- 蠕动泵(精确流体控制)
二、舵机角度控制:SG90 vs MG996 对比
2.1 舵机工作原理
舵机内部是个闭环控制系统:PWM 信号 → 控制电路 → 电机 → 齿轮组 → 电位器反馈 → 比较电路
PWM 信号周期 20ms(50Hz),脉宽 0.5-2.5ms 对应 0-180 度。
2.2 SG90 与 MG996 核心对比
| 对比项 | SG90 | MG996 |
|---|---|---|
| 齿轮材质 | 塑料 | 金属 |
| 轴承 | 塑料套 | 双滚珠轴承 |
| 扭矩 | 1.6kg/cm | 10kg/cm |
| 死区 | 较大(约 5°) | 较小(约 2°) |
| 精度 | ±5° | ±2° |
| 价格 | ¥8-12 | ¥35-45 |
2.3 实测数据
| 目标角度 | SG90 实测 | SG90 误差 | MG996 实测 | MG996 误差 |
|---|---|---|---|---|
| 0° | 3° | +3° | 1° | +1° |
| 45° | 47° | +2° | 46° | +1° |
| 90° | 92° | +2° | 91° | +1° |
| 135° | 133° | -2° | 134° | -1° |
| 180° | 177° | -3° | 178° | -2° |
结论: MG996 精度明显更好,平均误差 ±1° vs SG90 的 ±2-3°。
2.4 Arduino 控制代码
#include <Servo.h>
Servo myServo;
int lastAngle = -1;
const int DEAD_ZONE = 3; // 死区 3 度,减少抖动
void moveToAngle(int targetAngle) {
if (abs(targetAngle - lastAngle) > DEAD_ZONE) {
myServo.write(targetAngle);
lastAngle = targetAngle;
delay(300);
}
}
void setup() {
// 手动指定脉宽范围(单位:微秒),根据实际校准
myServo.attach(9, 500, 2500);
Serial.begin(9600);
}
void loop() {
moveToAngle(0);
delay(1000);
moveToAngle(90);
delay(1000);
moveToAngle(180);
delay(1000);
}
2.5 常见问题
舵机不转动: 检查独立 5V 供电是否充足(MG996 峰值 1A+);确认 PWM 信号线连接到 Arduino PWM 引脚;确认 Arduino 与舵机共地;用已知正常的代码测试排除程序问题。
舵机抖动严重: 供电不足是首要原因,换用 5V 3A+ 电源;在代码中加入死区(3-5°);检查 PWM 线是否过长或接触不良;避免超出舵机实际角度范围(部分舵机不是精确 0-180°)。
多舵机时 Arduino 复位: 舵机启动电流叠加导致 Arduino 供电电压跌落。解决方法:使用独立电源供电(至少 5V 3A);在电源端加装 470μF 大电容;逐个初始化舵机,避免同时启动。
2.6 选型建议
- 选 SG90:预算有限、负载轻(<500g)、精度要求不高、学习项目
- 选 MG996:需要可靠耐用、负载较大(500g-1.5kg)、精度要求 ±2°、长期运行
- 其他选择:MG90S(金属齿轮版,性价比好)、DS3235(数字舵机,±0.5°,高端项目)、总线舵机(多舵机串联,省 IO 口)
三、直流电机 PWM 调速:L298N 实战
3.1 PWM 调速原理
PWM(脉冲宽度调制)通过快速开关电源控制平均电压:
占空比 100%: ████████████████████ 平均电压 = 5V
占空比 50%: ████████████████ 平均电压 = 2.5V
占空比 25%: ████ 平均电压 = 1.25V
Arduino 的 analogWrite() 生成约 490Hz 的 PWM 信号,占空比范围 0-255。
3.2 L298N 正反转控制逻辑
| IN1 | IN2 | 电机状态 |
|---|---|---|
| HIGH | LOW | 正转 |
| LOW | HIGH | 反转 |
| LOW | LOW | 停止 |
| HIGH | HIGH | 停止(刹车) |
3.3 基础调速代码
const int ENA = 9; // PWM 调速引脚
const int IN1 = 8;
const int IN2 = 7;
void setup() {
pinMode(ENA, OUTPUT);
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
Serial.begin(9600);
}
void loop() {
// 正转加速
digitalWrite(IN1, HIGH);
digitalWrite(IN2, LOW);
for (int speed = 0; speed <= 255; speed += 10) {
analogWrite(ENA, speed);
delay(200);
}
// 反转减速
digitalWrite(IN1, LOW);
digitalWrite(IN2, HIGH);
for (int speed = 255; speed >= 0; speed -= 10) {
analogWrite(ENA, speed);
delay(200);
}
}
3.4 软启动/软停止
避免电机突然启停造成机械冲击:
void softStart(int targetSpeed, int stepTime = 10) {
for (int s = 0; s <= targetSpeed; s += 5) {
analogWrite(ENA, s);
delay(stepTime);
}
}
void softStop(int stepTime = 10) {
int currentSpeed = 255; // 假设当前全速
for (int s = currentSpeed; s >= 0; s -= 5) {
analogWrite(ENA, s);
delay(stepTime);
}
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
}
3.5 双电机差速转向
智能小车转向的经典方案:
void setMotor(int motor, int speed) {
int in1, in2, en;
if (motor == 1) { in1 = IN1; in2 = IN2; en = ENA; }
else { in1 = 3; in2 = 4; en = 10; }
if (speed > 0) {
digitalWrite(in1, HIGH);
digitalWrite(in2, LOW);
} else {
digitalWrite(in1, LOW);
digitalWrite(in2, HIGH);
speed = -speed;
}
analogWrite(en, constrain(speed, 0, 255));
}
void turnLeft(int speed) {
setMotor(1, -speed); // 左电机反转
setMotor(2, speed); // 右电机正转
}
3.6 常见问题与升级方案
电机不转: 检查电机供电电压;ENA 跳线帽是否拔掉(接 PWM 时必须拔掉);IN1/IN2 方向引脚是否正确;用万用表测量 OUT1/OUT2 是否有输出。
发热严重: L298N 在大电流下发热正常,持续电流不要超过 2A。如果烫手,加装散热片或考虑升级到 TB6612FNG(MOS 管驱动,效率高、发热小)。
| 驱动模块 | 优点 | 缺点 | 价格 |
|---|---|---|---|
| L298N | 便宜、资料多 | 发热大、效率低 | ¥15-25 |
| TB6612FNG | 效率高、发热小 | 电流较小(1.2A) | ¥20-30 |
| DRV8833 | 双通道、过流保护 | 需要 3.3V 逻辑 | ¥15-25 |
| VNH2SP30 | 大电流(30A) | 贵、体积大 | ¥50-80 |
四、编码器反馈:闭环 PID 控制
4.1 为什么需要闭环?
开环控制的致命问题:你不知道电机实际转了多少。负载变化、电压波动、摩擦力变化都会导致实际位置偏离预期。闭环控制通过编码器实时反馈,实现精准控制——这就是 CNC 机床、机器人关节都用闭环的原因。
4.2 编码器类型
| 类型 | 精度 | 成本 | 抗污染 | 典型分辨率 |
|---|---|---|---|---|
| 光电式 | 高 | 中 | 差 | 1000-5000 PPR |
| 磁电式 | 中 | 低 | 好 | 100-4096 PPR |
| 电容式 | 高 | 高 | 中 | 1000-10000 PPR |
DIY 项目推荐磁编码器 AS5600(I2C 接口,12 位分辨率,¥15)。
4.3 PID 控制原理
误差 = 目标位置 - 实际位置
输出 = Kp×误差 + Ki×误差积分 + Kd×误差微分
- P(比例):误差越大输出越大,纯 P 有稳态误差
- I(积分):累积历史误差,消除稳态误差,过大会超调
- D(微分):预测误差趋势,抑制超调,对噪声敏感
4.4 完整闭环位置控制代码
#include <Wire.h>
#include <AS5600.h>
AS5600 as5600;
const int PWM_PIN = 9;
const int IN1_PIN = 7;
const int IN2_PIN = 8;
// PID 参数
float Kp = 2.0, Ki = 0.5, Kd = 1.0;
int targetPosition = 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;
}
int currentPosition = as5600.readAngle(); // 0-4095
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, (int)pwm);
} else {
digitalWrite(IN1_PIN, LOW);
digitalWrite(IN2_PIN, HIGH);
analogWrite(PWM_PIN, (int)(-pwm));
}
}
4.5 PID 调参经验
- 先 P 后 D 再 I:先把 Kp 调到系统能响应但开始振荡,加 Kd 抑制振荡,最后加 Ki 消除稳态误差
- 控制频率要稳定:用定时器而非 delay 保证采样周期固定
- 积分限幅:防止积分饱和(integral windup)
- 死区处理:误差小于阈值时停止输出,避免持续微调造成磨损
4.6 实际应用场景
- 云台相机:保持相机稳定,抵消手部抖动
- 机器人关节:精准控制机械臂角度
- 平衡车:实时调整电机保持平衡
- CNC 进给轴:精确控制刀具位置
- 卷扬机:恒张力控制
五、太阳能充电控制器:PWM vs MPPT
5.1 PWM vs MPPT 对比
| 对比项 | PWM | MPPT |
|---|---|---|
| 转换效率 | 60%~75% | 90%~98% |
| 成本 | ¥20~50 | ¥100~500+ |
| 电路复杂度 | 低 | 高 |
| 低温/阴天表现 | 一般 | 明显更好 |
| DIY 难度 | ★★☆☆☆ | ★★★★☆ |
建议:12V 小系统(太阳能板 20W 以下)用 PWM 够用;超过 20W 或想最大化利用太阳能,选 MPPT。
5.2 PWM 充电控制器 DIY
核心思路:MOSFET 做开关,MCU 控制占空比,根据电池电压调整充电状态。总成本约 ¥20。
充电三阶段(12V 铅酸电池):
typedef enum {
CHARGE_BULK, // 恒流充电(全功率)
CHARGE_ABSORPTION, // 恒压充电(逐步减小电流)
CHARGE_FLOAT, // 浮充维护(小电流保持)
CHARGE_OFF // 停止充电
} ChargeState_t;
5.3 MPPT 扰动观察法
MPPT 核心是 DC-DC Buck 变换器加追踪算法。总成本约 ¥65。
void mppt_algorithm(void) {
uint16_t v_solar = read_solar_voltage();
uint16_t i_solar = read_solar_current();
uint32_t power = (uint32_t)v_solar * i_solar / 1000;
uint32_t delta_p = power - prev_power;
int16_t delta_v = v_solar - prev_voltage;
if (delta_p > 0) {
// 功率增加,继续同方向扰动
if (delta_v > 0) set_buck_duty(get_buck_duty() - duty_step);
else set_buck_duty(get_buck_duty() + duty_step);
} else {
// 功率下降,反向扰动
if (delta_v > 0) set_buck_duty(get_buck_duty() + duty_step);
else set_buck_duty(get_buck_duty() - duty_step);
}
prev_voltage = v_solar;
prev_power = power;
}
扰动观察法工作方式:
- 每次调整一个小的电压/占空比步长(如 50mV)
- 测量调整前后太阳能板的输出功率
- 如果功率增加,说明调整方向正确,继续同方向调整
- 如果功率下降,说明调整方向错误,反方向调整
- 如此反复,最终稳定在最大功率点附近
5.4 太阳能板选型
太阳能板功率(W) = 电池容量(Ah) × 电池电压(V) ÷ 有效日照小时数 × 系统效率
以 12V 50Ah 铅酸电池为例:600Wh ÷ 5h ÷ 0.75 ≈ 160W
5.5 锂电池 vs 铅酸电池充电参数
| 参数 | 12V 铅酸 | 12V 锂电 (4S) |
|---|---|---|
| Bulk 电压 | 14.4V | 16.8V |
| 吸收电压 | 13.8V | 16.8V |
| 浮充电压 | 13.2V | 14.6V |
| 欠压保护 | 11.5V | 12.0V |
| 最大充电电流 | 0.1C~0.25C | 0.5C~1C |
警告: 锂电池必须搭配 BMS 使用!充电控制器只管充电曲线,BMS 负责最后一道安全防线。
总结
本指南涵盖了电机控制的五大核心领域:
| 主题 | 关键芯片/模块 | 核心技能 |
|---|---|---|
| 步进电机 | A4988 | 微步设置、Vref 电流调节、AccelStepper 库 |
| 舵机 | SG90/MG996 | PWM 脉宽校准、死区处理、多舵机协同 |
| 直流电机 | L298N | H 桥控制、PWM 调速、软启动、差速转向 |
| 闭环控制 | AS5600 + TB6612 | PID 调参、编码器读取、位置/速度闭环 |
| 太阳能控制 | STM32 + MOSFET | PWM/MPPT 充电策略、扰动观察法 |
从开环到闭环,从单一电机到多轴协同,掌握这些知识后你就能应对绝大多数嵌入式电机控制项目。建议学习路径:直流电机 PWM → 步进电机开环 → 舵机角度控制 → 编码器闭环 PID → 太阳能系统。
希望这篇完全指南对您有所帮助!