1. 先说这个项目到底在解决什么问题
做51单片机直流电机调速的人,多半都经历过这种尴尬:旋钮拧到某个位置,电机空载跑得挺欢,手一捏阻力上来,转速立刻掉一截;供电电压从12V跌到9V,转速也跟着缩水。占空比明明没有变,实际转速却完全不受控制。这就是开环调速的典型症状。PID算法控制转速项目的核心价值,就是把这堵墙拆掉——实时测出电机真实转速,和设定转速做比较,再用误差去修正PWM占空比,让转速稳定收敛到目标值。下面我把这个项目从硬件选型、算法原理、C语言实现到参数整定踩坑记录完整写一遍,给想在51上做闭环调速的朋友一条能直接复现的路。
1.1 开环调速为什么调不住转速
开环调速的逻辑很简单:给电机一个固定占空比的PWM,占空比越大,平均电压越高,转速越快。听起来直接,但转速从来不是只由占空比决定的。负载、供电电压、电机温度、机械摩擦,全都在无声地改变实际转速。
拿常见的12V直流减速电机举例,空载时占空比50%可能跑1200转,挂上一个小阻力负载,同一个占空比可能只剩900转。占空比没变,转速却变了——因为系统根本不看输出,只朝着一个方向喊话,喊多大声和实际跑多快之间没有闭环校准。
电池电压的影响更隐蔽。PWM占空比是一个比例概念,50%占空比对应的是"母线电压的一半",母线电压本身跌了,电机端的平均电压也跟着跌。锂电池放电到平台期之后电压明显下降,电机转速自然往下掉。电压波动、负载波动、摩擦变化这三样凑在一起,开环调速的转速误差能做到百分之二三十。有些场合无所谓,但凡要求"转速稳定"的,比如小型传送带、搅拌器、云台、定速绕线机,全都忍不了。
1.2 闭环控制其实就是把误差反复逼近零
解决方案是闭环。在系统里加一个测速环节,实时算出电机当前转速,然后做减法:误差 = 设定转速 - 实测转速。知道了误差,再用一种策略把它压到最小,这个"策略"就是PID算法。
可以把它想成开车跑高速,设定转速就是巡航目标,你要看着仪表盘决定踩油门还是松油门。车速低了深踩,车速高了收一收。PID做的正是这件事,只不过它把"踩油门的程度"用数学公式量化了:误差越大,修正越大;误差持续存在,修正就持续积累;误差变化越快,越要提前收油防超调。
51单片机在这个项目里的角色就是那个"驾驶员"。它不负责输出多漂亮的波形,只负责在固定的时间节拍里循环执行"测速、比较、修正"三个动作。这也是为什么一颗51内核的芯片完全够用——控制决策本身的计算量很小,真正的功夫在测速精度、控制周期和参数匹配上。
2. 硬件选型与接线细节:别让供电和测速拖后腿
2.1 主控、电机、驱动、测速四件套怎么搭
先说整体搭配,我用的是一套非常常规的组合,适合在常见学习板上直接扩展:
| 部件 | 参考选型 | 注意事项 |
|---|---|---|
| 主控 | 51内核的89C52系列开发板 | 优先选带硬件PWM(PCA)的型号,软件PWM会占用大量CPU |
| 电机 | 12V直流减速电机带编码器 | 编码器线数决定测速分辨率,直接影响控制效果 |
| 电机驱动 | L298N或TB6612模块 | 驱动电源单独供,逻辑电源与单片机共用5V |
| 测速 | 霍尔编码器或光电编码器 | 脉冲信号接到T0引脚(P3.4),用计数器模式硬件数脉冲 |
主控没什么好纠结的,89C52系列是目前学习板最常见的51内核器件,IO资源、定时器数量都够。电机驱动方面,L298N模块便宜皮实,缺点是内部压降大,发热明显;TB6612模块是MOSFET结构,压降小、效率高,更省电,接口也干净。我自己最后用的是TB6612,因为项目是用电池供电的,效率敏感;如果你手头只有L298N,也能做,注意给模块留出散热空间就行。
2.2 测速分辨率:13脉冲和1080线编码器的差距有多大
这是整个项目最容易低估的一环。很多便宜的减速电机自带一块小磁钢加霍尔元件,输出轴每转一圈给13个脉冲。这种电机拿来转着玩没问题,但做PID调速就尴尬了。
先算一笔账。测速窗口取20ms,电机1000转/分钟时,13脉冲/圈的编码器每秒产生约217个脉冲,20ms内只能数到4到5个。换算成转速,每个脉冲代表约230转/分钟的跳变——也就是说,速度反馈在1000转上下以230转的台阶跳来跳去。PID看到的是一个量化噪声极大的反馈值,比例项和微分项都会跟着剧烈抖动,占空比被带得忽大忽小,转速根本稳不住。
想让这种低分辨率编码器勉强可用,只能把测速窗口拉长到200ms甚至更长,让一个窗口内能数到40多脉冲,分辨率降到20多转。代价是控制周期变慢,系统响应迟钝,负载突变后要很久才恢复。这不是参数能救的,是反馈通路的天花板。
所以我最终选了一款带光电编码器的减速电机,每圈输出1080个脉冲。同样20ms窗口、1000转/分钟时,一个窗口能数到约360个脉冲,每个脉冲对应约2.8转/分钟的转速分辨率。这个精度对PID来说就非常舒服了。选硬件时记住一句话:测速分辨率决定控制上限,编码器太差,后面算法写得再好也白搭。
2.3 共地与供电:这个项目八成故障都出在这里
接线看起来简单,翻车点却非常集中。第一是共地。电机的驱动电源(12V)和单片机逻辑电源(5V)的参考地必须连在一起,编码器信号才有统一的电平参考。不共地的话,测速脉冲信号会在0和1之间随机漂移,转速反馈乱跳,PID输出跟着疯狂摆动,现象就像电机"抽风"。
第二是别用USB口或开发板5V直接驱动电机。电机启动瞬间电流可能到一两安甚至更高,USB口直接进入保护,开发板上的稳压芯片也会过热。正确做法是驱动模块的VM接12V电源,VCC接5V(可从板子取),两路电源共地。
供电纹波也不能忽略。电机是感性负载,换向瞬间会产生尖峰,建议在驱动板电源输入端并一只470μF到1000μF的电解电容,再加一只104瓷片电容吸收高频。编码器的信号线尽量和PWM输出线、电机线分开走,避免干扰串进测速通路。
3. PID算法在51上的落地:从公式到C语言
3.1 先用大白话理解PID公式
PID三个字母对应三项输出:P是比例项,误差多大就修正多大,误差为零就停止修正;I是积分项,把历史上所有误差累加起来,专门对付"有误差但P项修正不动"的静差;D是微分项,看误差的变化趋势,误差快速增大时提前反向压制,抑制超调。
位置式PID公式长这样:
u(k) = Kp × e(k) + Ki × Σe(j) + Kd × [e(k) - e(k-1)]
其中e(k)是本次误差,e(k-1)是上次误差,Σe(j)是误差累加。u(k)最终映射到PWM占空比。对51来说,这个公式全是整数乘法和加减法,写着不复杂,真正的坑在后面那些"工程处理"上。
3.2 控制节拍怎么定:20ms采样周期为什么合适
控制系统是周期性运行的:每隔固定时间测一次速、算一次PID、更新一次占空比。这个周期叫采样周期Ts。电机是惯性环节,机械响应时间通常在几十毫秒量级,采样周期选10ms到50ms都能工作。我选了20ms,理由很朴素:
第一,20ms在11.0592MHz晶振、12T模式下正好对应18432个机器周期,定时器装载值可以是整的,误差为零;第二,220ms的周期给51留足了时间去做串口发送、按键扫描这类杂事;第三,配合1080线编码器,20ms窗口内能数到足够多的脉冲,测速分辨率足够。
采样周期一旦确定,后面所有PID参数都依赖这个时间基准。换周期就等于换一套参数,这是新手最容易忽略的。
3.3 定时器资源分配:计数、测速、PWM三件事不打架
89C52的资源很有限,但刚好够这个项目用。我的分配方案是:
- T0(P3.4)配置成计数器模式,专门数编码器A相脉冲。硬件计数,不占CPU时间,只有溢出才中断。1080线编码器在3000转/分钟内都不可能在一个采样窗口里数到65536个脉冲,所以根本不用管溢出。
- T1配置成定时器模式,产生20ms定时中断,承担"采样+算PID+更新占空比"三件事。
- PWM优先用PCA硬件PWM模块,占空比更新只需要改一个寄存器,不消耗CPU。不同的51内核芯片硬件PWM寄存器命名有差异,具体以你手头的头文件和手册为准。
- 如果主控没有硬件PWM,只能拿定时器做软件PWM,我后面会单独说它的局限性。
初始化代码大致长这样:
// T0: 16位计数器模式; T1: 16位定时器模式 TMOD = 0x15; // T1: 11.0592MHz, 12T模式, 20ms = 18432个机器周期 TH1 = 0xB8; TL1 = 0x00; ET1 = 1; TR1 = 1; // T0: 计数模式, 从0开始 TH0 = 0; TL0 = 0; TR0 = 1; // 打开总中断 EA = 1;这里面唯一要注意的是TR0的用法。计数过程中TR0始终是1,读取计数值之前先把它清零,防止读数的瞬间T0还在变化,读完再恢复。这就是典型的"先停表、再读表、再走表"操作。
3.4 测速换算和PID核心代码拆解
脉冲数换算转速的公式可以写成这样:转速(RPM) = 脉冲数 × 60 / (每转脉冲数 × 采样周期秒数)。代入20ms和1080线编码器,就是:
RPM = 脉冲数 × 60 / (1080 × 0.02) = 脉冲数 × 3000 / 1080
也就是每个脉冲大约2.8转。代码里我统一用整数运算,先转成long型再除,避免中间溢出。下面是T1中断里的完整处理逻辑:
#define PWM_FULL 1000 // 占空比满刻度,千分比,方便调参 int set_rpm = 1000; // 设定转速,可由按键或串口修改 int act_rpm; // 实测转速 int pwm_duty; // 输出占空比 0~1000 long err_sum; // 积分累加 int err_now, err_last; // 本次、上次误差 // PID参数,Ts = 20ms int Kp = 12; int Ki = 3; int Kd = 8; // 三点滑动平均用的缓存 int speed_buf[3]; void timer1_isr(void) interrupt 3 { unsigned int cnt; long pid_out; // 1. 读T0计数值,清零后重新计数 TR0 = 0; cnt = (unsigned int)((TH0 << 8) | TL0); TH0 = 0; TL0 = 0; TR0 = 1; // 2. 脉冲数换算成转速 act_rpm = (int)(((long)cnt * 3000L) / 1080L); // 3. 三点滑动平均滤波,压掉量化台阶 speed_buf[2] = speed_buf[1]; speed_buf[1] = speed_buf[0]; speed_buf[0] = act_rpm; act_rpm = (speed_buf[0] + speed_buf[1] + speed_buf[2]) / 3; // 4. 计算误差 err_now = set_rpm - act_rpm; // 5. 积分分离:误差太大时不积分,防止启动瞬间积分饱和 if (err_now > -300 && err_now < 300) { err_sum += err_now; if (err_sum > 800) err_sum = 800; // 积分限幅 if (err_sum < -800) err_sum = -800; } else { err_sum = 0; } // 死区:误差在正负3转以内,清积分,避免占空比抖动 if (err_now > -3 && err_now < 3) { err_sum = 0; } // 6. PID计算,用long型防止中间结果溢出 pid_out = (long)Kp * err_now + (long)Ki * err_sum + (long)Kd * (err_now - err_last); err_last = err_now; // 7. 输出限幅 if (pid_out > PWM_FULL) pid_out = PWM_FULL; if (pid_out < 0) pid_out = 0; pwm_duty = (int)pid_out; }中断里的顺序是有讲究的。先读计数、再换算、再滤波、最后算PID。换算和滤波放前面,是因为任何一步占了太多时间都会影响下一次计数精度。PID计算本身放在最后,就算多个十几微秒,影响的也只是本次输出更新的时间点,不会丢失脉冲计数。
3.5 输出限幅、积分限幅、死区为什么一个都不能少
这三个东西看着像补丁,实际上缺一个系统就要崩。
输出限幅好理解,占空比不能超过100%,也不能为负——本项目的调速只要求正转,负占空比没有意义。不加限幅,一旦计算结果超界,赋值给硬件PWM寄存器时会发生截断或翻转,波形完全错乱。
积分限幅是防积分饱和的保险。电机刚上电时设定1000转、实际0转,误差巨大,积分如果照常累加,几秒钟就能攒出一个天文数字。等电机真正跑起来,这个巨大的积分项还要慢慢"吐"回来,表现为转速冲到设定值一大截才回落,甚至来回震荡。所以我做了积分分离:误差超过300转直接清积分,系统启动时以纯比例和微分控制为主。
死区处理则是为了让稳态更安静。误差在正负3转以内时,转速差已经小于一个编码器脉冲对应的分辨率,这时候继续微调占空比没有任何意义,只会让PWM输出在某个值附近抖动,电机发出滋滋声。清掉积分、固定占空比,系统反而更稳。
占空比换算到硬件PWM寄存器这一步,我放在主循环里做,格式大概是:
// 把千分比占空比映射到硬件PWM的8位比较值 // 具体换算方向和寄存器名以数据手册为准 pwm_reg = (unsigned char)((unsigned long)pwm_duty * 255UL / 1000UL);3.6 没有硬件PWM的51怎么办
如果你的51内核芯片没有PCA或CCP模块,只能做软件PWM。软件PWM的痛点是频率和档位不可兼得。想达到1kHz的PWM频率再加100个占空比档位,定时器中断就得每10微秒一次,51根本扛不住。实际能做的往往是1kHz频率配10到20个档位,占空比比较粗糙,电机在低速区会有可闻噪声和转矩脉动。
我的建议很直接:要么换一颗带硬件PWM的51内核芯片,要么老老实实在驱动端外接一个555电路或专门的PWM芯片。把宝贵的定时器中断留着做PID,比什么都重要。
4. 参数整定实录:我把P、I、D一个个加回去
4.1 只加P项:从迟钝到等幅震荡
整定参数没有任何捷径,至少第一次没有。我把Ki和Kd先关掉,从Kp=2开始,每次改完参数后跑一次1000转的阶跃启动。
Kp=2时的表现:电机慢慢悠悠爬升,大概1秒出头才到设定值附近,不超调,但太肉。Kp=10时,上升明显变快,过冲大约100转,两三个衰减震荡之后能收敛。Kp=15继续加快,过冲更大。到Kp=20时,转速开始围绕目标值等幅震荡,幅度大约正负80转,电机发出明显的嗡鸣声——这就是临界振荡状态。
记录下这个临界点很有用。临界增益Ku约20,临界振荡周期Tu约0.2秒。按经典的经验公式,PI控制器推荐值大概是Kp等于0.6倍Ku,也就是12左右,积分时间取0.5倍Tu算下来Ki的推荐值在2.4附近。这个计算不用照搬,但能给你一个靠谱的起点。
4.2 加积分项:消静差,但积分饱和更可怕
只靠P项时,我发现一个现象:电机空载冲到设定值附近后,最终转速比设定低了一截。比如设定1000转,稳定在950转就不动了。这是因为P项在误差为零时输出为零,而在有摩擦阻力的系统里,需要持续输出一个占空比才能维持转速。这个持续输出只能靠积分项提供。
所以我从Ki=2开始加。效果立竿见影,误差慢慢被抹平,最终能稳定在设定值附近。但Ki加大到5之后出问题了:启动瞬间电机比之前凶得多,转速一口气冲到1400多转才回头,然后来回甩了两三次才稳定。这就是典型的积分饱和。我的处理正是前面代码里那套组合拳:误差大于300转时清积分、积分累加值限幅在正负800、死区范围内清积分。
4.3 微分项:加还是不加,先滤波再上D
到这一步,系统用P加I已经能工作,静态精度也达标,剩下的主要问题是过冲。想进一步压超调,就得靠微分项。但D项对噪声极端敏感:编码器测速有量化台阶,滤波之前速度估计每次跳几个转,误差的差分会被噪声放大,表现为占空比高频抖动。
我的做法是先加三点滑动平均滤波,把速度反馈磨平,再上Kd。滤波会给系统带来额外延迟,所以Kd也不要一下给太大。我从Kd=5开始试,超调从约10%降到6%,再提到8,超调收到5%以内,动态过程变得干脆。继续加大到12,系统又开始轻微抖动——到了这个点再往加就没意义了。
4.4 我最后用的一组参数和整定顺序
最终定下来的参数如下,采样周期固定20ms:
| 参数 | 数值 | 说明 |
|---|---|---|
| Kp | 12 | 每1转误差对应1.2%占空比修正 |
| Ki | 3 | 消除静差,兼顾恢复速度 |
| Kd | 8 | 配合滤波,压制超调 |
| 积分限幅 | 正负800 | 防积分饱和 |
| 误差分离阈值 | 300转 | 误差过大时禁积分 |
| 死区 | 正负3转 | 避免稳态占空比抖动 |
整定的顺序口诀是:先P后I再D,每次只动一个参数。P从0加到临界震荡,取临界增益的一半到六成当初值;I从很小往上加,盯着静差和超调的平衡;D去解决震荡和过冲,但必须先滤波再上手。每次改完参数,只跑一次阶跃,拿笔记录超调量、调节时间、稳态误差三个数,比盯着波形瞎猜强得多。
5. 实测结果与调试:怎么验证转速是真的稳了
5.1 串口把曲线发出来,让数据说话
调试闭环系统最忌讳凭手感。我给系统加了串口输出,每个控制周期发一行数据,格式类似:
S=1000,A=998,P=520S是设定转速,A是实测转速,P是当前占空比。电脑上随便一个串口助手就能看,也可以把数据导进表格软件里画曲线。
曲线能告诉你很多细节。比如实测转速在稳态时如果有规律的锯齿状波动,大概率是测速量化台阶太大,需要更细的编码器或更长的测速窗口;如果PWM占空比在高频抖动,多半是D项被噪声带着跑,先把滤波加上;如果曲线呈等幅正弦状,那是参数还压在临界点上。数据一画出来,问题定位比听声音靠谱一百倍。
串口初始化和发送代码不复杂,注意用11.0592MHz晶振配9600波特率时,定时器初值误差为零,这是最标准的组合。
5.2 两类关键测试:阶跃响应和负载扰动
参数定完,得跑两个标准测试才算真正验证。
第一个是阶跃测试:设定转速从600转直接跳到1200转,看转速曲线的上升时间、超调量和调节时间。我最终这组参数的表现是:上升时间约0.35秒,超调在5%以内,大约0.6秒内进入正负5转的稳态带。这个表现对一般小型设备完全够用。
第二个是负载扰动测试:电机稳定在1000转时,用手轻捏电机轴,模拟负载突变。注意不要完全捏死,防止电流过大。捏住瞬间转速掉了大约80转,松开后约0.5秒内恢复回1000转。恢复速度靠P项,能不能回得到设定值靠I项——如果捏住后转速拉不回去,说明Ki偏小或者积分限幅卡得太死。
还可以做一次供电扰动:把12V电源换成9V试一次。转速如果还能稳定在设定值,说明闭环对母线电压波动有真正的抑制作用,这也是开环系统做不到的。
5.3 常见故障排查表
整理一下我踩过和见过的坑,方便你对照排查:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 电机完全不转 | 驱动使能脚没拉高、PWM脚接错、电源功率不足 | 检查IN/EN引脚电平,用示波器或逻辑分析仪看PWM波形 |
| 转速疯狂乱跳 | 单片机与驱动电源未共地、编码器线受干扰 | 确认共地;编码器线绞合或做RC滤波,必要时加上拉电阻 |
| 设定1000转只能跑到800 | P太小或I项没起作用 | 适当加大Kp,检查积分限幅是否太紧 |
| 启动瞬间猛冲一下 | 积分饱和 | 加积分分离、积分限幅、输出限幅 |
| 稳态等幅震荡 | Kp偏大、缺D项、滤波不足 | 降Kp、提高滤波强度、谨慎加Kd |
| 占空比波形毛刺多 | 逻辑电源纹波大 | 加强电源滤波电容,编码器线远离PWM线 |
| 电机有明显高频噪声 | PWM频率偏低 | 调高硬件PWM时钟分频,或改用带硬件PWM的芯片 |
调试时还有一条经验:每次只改一个参数,改完必须重新上电跑一次完整测试。连续改三个参数再一起测试,出了问题根本不知道是哪一个的锅。
最后说一点个人体会。这个项目做完之后我最深的感触是,PID本身不难,公式背下来五分钟,代码抄一遍十分钟,真正决定成败的是三件事:测速分辨率够不够、控制周期和反馈量化匹不匹配、积分饱和这类工程细节有没有处理到位。很多系统调不稳,不是算法不行,是反馈通路的精度配不上算法的胃口。另外,如果你还没买电机,我强烈建议直接选带光电编码器的高分辨率型号,别在13脉冲的霍尔电机上省钱——省下的钱最后都会变成调试时的头发。先把这一套跑通,后面不管是改成按键调速、加数码管显示,还是换串口下发目标转速,都只是在这个骨架上添砖加瓦的事。