ESP32 工程教程:编码器反馈的直流电机闭环调速
本文按实际动手顺序整理。示例中的 GPIO、地址和参数需要根据手中的开发板、模块版本和负载规格调整;涉及市电、机械运动、大电流或人体数据时,必须把硬件保护和使用边界放在代码之前。

一、项目目标与应用场景
只给电机一个固定 PWM,占空比相同并不代表转速相同:电池电压、负载和摩擦都会改变结果。闭环控制用编码器测量实际转速,再根据目标与实际的误差调整 PWM。这个项目把测量、定时、控制和功率电子放到同一张桌上,很适合理解“程序不是发号施令就结束,还要看看现实有没有照做”。
这篇教程不把“模块亮了、串口有数字”当作终点,而是继续处理供电、边界条件、数据有效性和故障恢复。先跑通最小链路,再做标定与状态机,最后接入网页或自动控制,排错时会省下大量来回拆线的时间。
二、硬件准备与接线
准备带编码器的低压直流电机、合适电流等级的 H 桥驱动器、独立电机电源和 ESP32。编码器 A/B 相接可用 GPIO,逻辑与功率地按驱动要求连接。电机堵转电流通常远高于空载电流,驱动器、电源、导线和保险都要按堵转情况留余量;不要用开发板 USB 口硬扛电机。
接线前断电,用万用表确认电源极性和电压。首次实验只接一个外设,保留串口日志;功能验证后再增加屏幕、网络和执行器。把开发板型号、GPIO、模块地址、供电来源与库版本写进实验记录,避免换板以后对着“默认引脚”四个字发呆。
三、工作原理
固定采样周期内统计编码器脉冲,结合每转脉冲数换算 RPM。PI 控制器计算误差,比例项快速响应,积分项消除稳态偏差,结果经过上下限后成为 PWM。方向切换必须先减速到零,再改变 H 桥方向。积分还要防饱和,否则电机被堵住时积分不断累积,解除堵转后会突然冲出去。

建议把程序拆成驱动层、数据层、业务层和展示层。驱动层只负责与外设通信;数据层保存原始值、单位和有效状态;业务层执行阈值、限位与超时;展示层读取快照,不直接操纵硬件。这样网页断线或存储变慢时,核心控制仍能保持稳定。
四、首次验证与标定
先断开闭环,以不同 PWM 测量空载转速、电流和启动死区,得到基础曲线。确认编码器每转计数与方向,再设置保守的最大 PWM。调参时先把积分设为零,只增加 Kp 到响应够快但不过度振荡;随后缓慢增加 Ki 消除偏差。每次只改一个参数并记录阶跃响应。
标定时保存原始值,不要只留下一个最终系数。原始证据能帮助判断后来出现的偏差来自传感器、机械、电源还是算法。更换模块、安装位置、负载或外壳后,都应重新进行关键测试。
五、最小代码骨架
下面代码用于验证核心路径,不代表所有库版本都使用完全相同的接口。安装对应库后,先核对示例与当前 Arduino-ESP32 版本:
const int PWM_PIN = 25, DIR_PIN = 26;
volatile int32_t pulses = 0;
float targetRpm = 120, integral = 0;
const float KP = 0.8f, KI = 0.25f;
void IRAM_ATTR onEncoder() { pulses++; }
void setup() {
pinMode(DIR_PIN, OUTPUT);
ledcAttach(PWM_PIN, 20000, 8);
attachInterrupt(digitalPinToInterrupt(34), onEncoder, RISING);
}
void controlStep(float dt) {
noInterrupts(); int32_t count = pulses; pulses = 0; interrupts();
float rpm = count * 60.0f / (600.0f * dt);
float error = targetRpm - rpm;
integral = constrain(integral + error * dt, -300.0f, 300.0f);
int duty = constrain((int)(KP * error + KI * integral), 0, 255);
ledcWrite(PWM_PIN, duty);
Serial.printf("target=%.1f rpm=%.1f duty=%d\n", targetRpm, rpm, duty);
}
启动日志至少输出固件版本、初始化结果、关键参数和第一笔有效数据。失败时不要只写 error,应附上接口、地址或错误码。没有观测点的程序一旦罢工,开发者只能围着它进行气氛分析。
六、数据与状态设计
控制循环使用固定周期,网络和日志不能改变它。状态至少包含目标转速、实际转速、PWM、电流、方向和故障码。曲线观察目标与实际的上升时间、超调和稳定误差,比盯着一串滚动数字更容易调参。测速窗口太短会抖,太长又会让控制迟钝,需要按编码器分辨率折中。
数据结构建议同时保留 raw、value、unit、valid、timestamp 和 error。显示层只消费快照,不能为了刷新页面主动触发一次耗时采样;控制输出也不能由网页直接越过状态机修改。
七、常见问题与排错
示例中的 ledcWrite API 要与安装的 Arduino-ESP32 版本匹配,初始化 PWM 后再写占空比。编码器 ISR 只计数,不做浮点计算或串口输出。电机堵转、编码器断线和方向异常必须能检测,PID 不能替代过流保护。
排错顺序建议固定:先检查电源和公共地,再确认引脚、地址和通信参数;随后运行库自带最小示例,打印原始值;最后恢复滤波、页面和自动控制。每次只修改一个变量,并保留失败输出。偶然跑通不是定位问题,只是问题暂时去喝水了。
八、工程化与安全边界
加入目标斜坡、积分限幅、编码器超时、堵转判定和温度/电流保护。控制任务放在确定周期的高优先级任务中,参数更新走队列。停止命令定义为滑行、制动还是保持,要结合机械安全明确选择。
配置保存到 NVS 时加入范围校验、默认值和版本迁移。网络请求设置超时与退避,日志限制容量并分级。执行器必须有最大持续时间、启动安全态和本地停止方式。软件重启不能成为唯一保护,关键风险应由硬件限流、保险、限位或隔离承担。
九、完整测试清单
- 冷启动、复位和断电恢复后,输出是否处于明确安全状态;
- 拔掉外设、接错地址、输入越界时,程序是否记录错误并继续运行;
- 连续运行两小时后,内存、温度、供电和通信错误是否稳定;
- 网络断开、浏览器关闭或服务器重启时,本地功能是否仍然可用;
- 阈值附近反复变化时,是否有迟滞、去抖或速率限制;
- 更换同型号模块后,校准与配置是否仍然适用;
- 故障解除后,系统是自动恢复、等待确认,还是进入安全锁定;
- 日志是否足以解释最后一次状态变化来自哪里。
测试不能只拍一张成功截图。至少要覆盖冷启动、连续运行、外设拔插、供电波动、输入越界、通信超时和网络中断。每次测试记录原始数据、处理结果、错误码、固件版本和接线变更。真正有价值的学习日志,不只是告诉读者成功时长什么样,也要告诉读者失败时先看哪里。
十、扩展方向
可扩展为小车双轮速度同步、传送带恒速、风扇稳速或卷线器。两轮小车还可以在速度环之外增加位置或航向外环,但每增加一环都应先独立验证内环稳定。
扩展时一次增加一层:新硬件先独立验证,新算法先离线检查,新网络功能先模拟断线。把每次成功与失败都写进记录,文章会从“接线说明”逐渐变成真正能复用的工程笔记。
十一、总结
这个项目的重点不只是调用一个库,而是建立从电气连接、原始数据、业务状态到异常恢复的完整链路。能运行是第一步,能验证、能排错、能安全退化才算完成。实际制作后,建议继续补上实物接线照片、串口截图、关键参数和一次真实故障记录。
参考资料
打赏
如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏支持:王庆岩
浙公网安备 33010602011771号