
简介面向51单片机初学者的闭环直流电机控制系统设计方案涵盖PWM调速、速度反馈与Proteus仿真调试。资源包共36个文件大小约381KB主要包含Keil工程源码c/hex/uvproj、Proteus仿真文件dsn、原理图SchDoc/PDF、流程图bmp和元件清单xlsx从硬件设计到软件实现均有完整呈现。系统以AT89C51为主控12MHz晶振工作通过P1.0产生占空比可调的PWM信号驱动电机P1.1P1.4四个独立按键分别实现速度设定值加10、加1、减10、减1两个四位共阴数码管同时显示设定转速和实测转速P3.2/INT0外部中断采集测速传感器脉冲完成实时速度测量与闭环调节。源码、仿真电路与流程图相互对应便于理解“转速设定—反馈测量—PWM输出”的完整控制链路适合课程设计、电子竞赛或毕业设计参考。目前已有757人学习下载资料完整、上手容易实用性较强。1. 从开环到闭环一套51单片机直流电机调速系统的核心脉络基于51单片机的闭环直流电机控制系统本质是用PWM和速度反馈把开环调速变成闭环调速。这套Proteus工程包含AT89C51、两套4位共阴数码管、四个独立按键和测速脉冲采集电路可以完整体验“设定速度—PWM输出—测速反馈—误差修正”的完整链路。它适合正在做单片机课程设计的学生也适合想把数字PID概念落到8位平台的工程师。开环PWM调速在负载变化时转速会明显漂移而接上P3.2的脉冲反馈后单片机能根据实际转速自动修正占空比这才是闭环的意义。2. 硬件设计AT89C51的引脚分工决定程序结构2.1 器件选型与电路连接的关键点AT89C51在这个系统里的定位是“低成本的集中控制器”。它没有内置ADC也不需要ADC因为测速信号是脉冲PWM输出是方波数码管显示走的是普通IO口这些恰好都是51单片机的强项。12MHz晶振的好处是机器周期为1us定时器初值计算非常直观中断周期取整数时不会产生累积误差。实际驱动直流电机时AT89C51的P1.0输出电流只有几个毫安不能直接带动电机。常见做法是在P1.0后面加一级三极管或MOS管再驱动电机要求正反转就加H桥本系统只需要调速用单管或L298N的单一通道即可。Proteus仿真里的小电机模型可以直接接P1.0但实物必须加驱动否则单片机端口会被拉垮。测速传感器部分没有限定具体型号工程中常用霍尔传感器或红外对管。传感器输出的是与电机转速成正比的脉冲接到P3.2/INT0利用外部中断计数。选择传感器时要注意输出电平是否和TTL兼容以及每转产生的脉冲数NN值越大力矩波动分辨率越高但中断频率也会成倍上升。2.2 引脚功能分配表引脚分配直接决定程序里所有变量的归属先看完整映射关系。引脚功能方向说明P1.0PWM输出输出占空比可调方波接电机驱动级P1.1按键设定值加10输入低电平有效需上拉P1.2按键设定值加1输入低电平有效需上拉P1.3按键设定值减10输入低电平有效需上拉P1.4按键设定值减1 / 设定值显示位选输入/输出原工程中分时复用P1.7~P1.5设定值数码管位选等输出与P1.4共同构成设定值显示位选P2.0~P2.7数码管段选输出共阴数码管段码a~g、dpP0.3~P0.0实时速度显示位选输出低电平选中某一位P0.7~P0.4可扩展位选输出原工程保留可接测量值另外四位P3.2/INT0测速脉冲计数输入输入下降沿触发外部中断0注意P1.4同时出现两次原工程把设定值数码管的位选安排在P1.7~P1.4而按键也占用了P1.1~P1.4。实际运行时不能同时操作按键和位选否则按键电平会被显示扫描干扰。解决方法是分时复用每秒显示刷新在定时器中断里完成按键扫描在主循环里检测两者错开实在冲突时把设定值位选改用74HC573锁存或换到P0.4~P0.7我的建议是电路允许就把显示位选全部挪到P0口避免这种软性“打架”。2.3 共阴数码管动态扫描与段码表两个四位共阴数码管一共8位如果直连IO口每位对应一个锁存器使用动态扫描能显著减少连接。段选接P2口位选由P0和P1高位控制。扫描频率要足够高否则数码管会闪烁常见做法是每1ms切换一位8位一个周期需要8ms刷新率约125Hz肉眼完全看不到闪烁。先定义引脚和段码表// AT89C51引脚宏定义Keil C51 #define PWM_PIN P1_0 // P1.0PWM输出 #define KEY_PORT P1 // P1.1~P1.4独立按键输入 #define SEG_PORT P2 // P2.0~P2.7共阴数码管段选 #define DIG_PORT P0 // P0.0~P0.7实时速度显示位选 #define SET_DIG_H P1 // P1.4~P1.7设定值显示位选 // 共阴数码管0~9段码顺序对应P2.0a ... P2.7dp unsigned char code seg_code[] { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F // 9 };段码表的顺序和PCB布局强相关。P2.0接a段、P2.1接b段……P2.7接dp时0x3F表示显示0如果P2.0接dp而P2.7接a整个表就要反转。Proteus仿真里一般按常见顺序连接改动段码表时要先确认原理图中P2口和数码管的位序不要想当然。动态扫描函数要遵循“先送段码再选位切换前消隐”的流程。消隐是为了防止上一位的段码残留在下一位上造成拖影。代码如下void display_one_digit(unsigned char pos, unsigned char num) { DIG_PORT 0xFF; // 全部位选置高关断显示防止串位 SEG_PORT seg_code[num]; // 送段码 DIG_PORT ~(1 pos); // 低电平选中对应位 }~(1 pos)是位运算pos取0到3时分别选中P0.0、P0.1、P0.2、P0.3。因为P0是开漏输出输出低电平的驱动能力尚可输出高电平需要外部上拉Proteus默认模型不一定暴露这个问题但做实物时建议在P0口加4.7k~10k上拉电阻或者用NPN三极管做位驱动。3. 软件实现PWM生成、INT0测速与按键处理3.1 定时器0中断产生可调PWM用定时器中断产生PWM好处是占空比更新不需要CPU干预适合电机这类需要连续方波驱动的负载。定时器0工作在方式116位计数定时周期取100usPWM周期取100个计数也就是10ms输出频率100Hz。这个频率对直流电机很合适太低电机会抖动太高会增加开关损耗。初值计算12MHz晶振下机器周期1us100us需要计数100次定时器初值65536-10065436即0xFF9C。代码如下unsigned char pwm_duty 50; // 占空比 50%范围0~99 void timer0_init(void) { TMOD | 0x01; // 定时器0方式116位定时 TH0 0xFF; // 100us定时初值高8位 TL0 0x9C; // 100us定时初值低8位 ET0 1; // 开定时器0中断 TR0 1; // 启动定时器0 EA 1; // 开总中断 } void timer0_isr(void) interrupt 1 { static unsigned char cnt 0; TH0 0xFF; // 重装初值 TL0 0x9C; cnt; if (cnt 100) // 一个PWM周期结束 { cnt 0; PWM_PIN 1; // 周期起点输出高电平 } else if (cnt pwm_duty) // 到达占空比翻转点 { PWM_PIN 0; // 输出低电平 } }逻辑并不复杂cnt从0到99循环周期开始立即拉高到pwm_duty时拉低。因此高电平持续时间约等于pwm_duty * 100us占空比就是pwm_duty/100。pwm_duty设置99时高电平最长但不能输出100%占空比这正是电机控制需要的始终保持一定的最小关断时间让续流二极管可靠工作。使用这个方案时要注意重装初值的时间点。中断服务函数一进来就重新写TH0和TL0而不是等计数器完全归零这样可以减少软件重装带来的时间抖动。实际示波器看到的PWM周期会有不到一个机器周期的误差对电机控制完全可接受。3.2 用INT0外部中断采集测速脉冲测速传感器输出的脉冲接到P3.2单片机只需对脉冲个数计数。假设电机每转一圈传感器输出PULSES_PER_REV个脉冲那么1秒内记录的脉冲数除以每转脉冲数就是每秒转数乘以60就是每分钟转速。外部中断0的优先级默认最高适合测速这种不能漏计数的场景。代码如下#define PULSES_PER_REV 20 // 每转脉冲数按实际传感器设置 volatile unsigned int pulse_cnt 0; unsigned int speed_rpm 0; unsigned int tick_1s 0; void ext0_isr(void) interrupt 0 { pulse_cnt; // 只计数不在中断里做除法 } void timer1_isr(void) interrupt 3 { TH1 0x3C; // 50ms定时初值 TL1 0xB0; tick_1s; if (tick_1s 20) // 累计1秒 { tick_1s 0; // 每秒转数 脉冲数 / 每转脉冲数 // 每分钟转速 每秒转数 * 60 speed_rpm (unsigned int)((unsigned long)pulse_cnt * 60 / PULSES_PER_REV); pulse_cnt 0; // 计数值清零开始下一秒计数 } }外部中断里只做pulse_cnt是因为interrupt 0的现场保护很简短如果在这里做乘除法既影响下一次脉冲的响应也让中断服务函数变长。定时器1同样使用方式150ms中断一次20次正好1s。计算转速时pulse_cnt和PULSES_PER_REV都是整数直接乘可能溢出所以用unsigned long强制转换后再乘60。每转脉冲数这个参数必须和实际硬件匹配。如果测速盘有20个齿就用20如果是霍尔传感器配一对磁极就是1。参数设置过大显示转速会偏低设置过小显示值偏高甚至超过9999的显示范围。3.3 独立按键扫描与设定值限幅四个按键接在P1.1~P1.4按下为低电平。因为P1口内部没有上拉外界必须接上拉电阻否则按键释放时引脚电平不确定。按键处理的难点是抖动按下瞬间和释放瞬间都会有几十毫秒的电平跳变直接读取会一次按键被识别成多次。下面是带去抖的扫描代码unsigned int set_value 600; // 速度设定值单位r/min初始600 void key_scan(void) { static bit key_pressed 0; unsigned char k; // 只关心P1.4~P1.1右移一位后低4位有效 k (KEY_PORT 1) 0x0F; if ((k ! 0x0F) !key_pressed) { delay_ms(10); // 延时去抖 k (KEY_PORT 1) 0x0F; if (k ! 0x0F) { switch (k) { case 0x0E: set_value 10; break; // P1.1加10 case 0x0D: set_value 1; break; // P1.2加1 case 0x0B: set_value - 10; break; // P1.3减10 case 0x07: set_value - 1; break; // P1.4减1 default: break; } key_pressed 1; } } if (set_value 9999) set_value 9999; // 上限 if (set_value 0) set_value 0; // 下限 if (k 0x0F) key_pressed 0; // 按键释放 }k (KEY_PORT 1) 0x0F把P1.1~P1.4映射到低4位按下时对应位为0。case 0x0E表示只有最低位为0对应P1.1case 0x0D表示第二位为0以此类推。key_pressed会锁存一次有效按键直到所有按键释放才清零这样能避免按住不放时数值连续跳动。需要说明的是delay_ms(10)是阻塞延时。主循环里除了按键扫描和闭环计算没有其他强实时任务阻塞10ms没问题如果显示扫描放在中断里也不受影响。若后续加入数码管静态巡回等建议把去抖改成时间戳法这里不展开。3.4 定时器资源分配与中断优先级系统里用到了定时器0、定时器1和外部中断0优先级关系要提前规划。51单片机默认优先级由查询顺序决定外部中断0最高然后是定时器0、外部中断1、定时器1。中断源用途中断频率优先级INT0测速脉冲计数由转速决定最高T0PWM周期生成10kHz100us次高T11s测速计算与显示节拍20Hz普通这种顺序是合理的测速脉冲不能丢PWM中断需要稳定显示和转速计算慢一点无妨。但要注意INT0和T0同时在频繁中断时主循环的key_scan会被打断按键扫描实际时间不是严格10ms对去抖没有影响只是会延长按键响应。4. 闭环控制逻辑从占空比修正到速度稳定4.1 闭环的起点是误差计算闭环控制和简单PWM调速的本质区别在于占空比不是直接等于设定值映射值而是根据实测转速动态修正。系统里设定值set_value和实测值speed_rpm都是r/min误差定义如下int error; error set_value - speed_rpm; // 设定值 - 测量值误差为正表示当前转速低于目标需要提高占空比误差为负表示转速过高需要降低占空比。转速测量每秒更新一次所以闭环控制周期也是1s。这个频率对机电系统足够电机惯性决定了它不会在几十毫秒内发生突变。4.2 增量式比例积分控制只做比例控制会留下静态误差当电机负载增大时需要更大的占空比才能维持转速但比例控制产生的占空比增量与误差成正比误差小到一定程度时增量不足以抵消负载最终稳定值会低于设定值。加入积分项可以持续累积误差即使误差很小积分量也会缓慢增加直到把静差推为0。以下是简化的PI控制代码#define KP 0.5f #define KI 0.05f float integral 0.0f; int last_error 0; void speed_control(void) { int error; int adjust; int new_duty; error (int)(set_value - speed_rpm); integral error; if (integral 200) integral 200; // 积分限幅 if (integral -200) integral -200; adjust (int)(KP * error KI * integral); new_duty pwm_duty adjust; if (new_duty 5) new_duty 5; // 占空比下限 if (new_duty 95) new_duty 95; // 占空比上限 pwm_duty new_duty; }adjust是这次控制周期对占空比的修正量KP * error是比例项KI * integral是积分项。注意这里输出的是占空比增量所以积分项会一直累积如果不加限幅电机长时间低速运转时积分会涨到几百等设定值突然提高占空比被积分项顶到上限系统出现严重超调。限幅200的含义是积分量最大贡献0.05*20010个占空比单位和比例项协同时不会单独支配输出。4.3 占空比上下限保护PWM占空比限制在5到95之间不是1到100主要是考虑两个因素。第一占空比过低时P1.0输出的高电平时间太短三极管驱动级还没完全导通就进入续流状态电机反而失控第二占空比长期为100%时一旦测速传感器脱落或脉冲丢失闭环系统会把电机推到全速无法从反馈端发现问题。上限95%保证系统仍然存在一个最小关断周期驱动电路可以维持正常续流。如果实际硬件允许下限可以调到10上限可以调到90具体看电机带载后的启动电压和驱动管压降。仿真时因为电机模型理想5%占空比可能就能转但实物电机的启动电压更高需要根据实验调整。4.4 转速波动时的参数调优闭环参数没有万能值但调试路径有规律。下面是我实际调试时常用的一组对照表现象可能原因调整方法设定600转实际稳定在570转比例系数太小或负载变化增大KP或增大KI转速在目标值附近来回摆动KP过大积分反应太快降低KP降低KI启动瞬间转速冲到950再回落积分饱和或初始占空比过大限制积分降低初始占空比满载时转速不够占空比上限太低将95改为98同时观察电流空载振荡带载稳定系统阻尼不足减小KP增大测速滤波具体到本项目KP0.5、KI0.05是一个偏保守的起点。Proteus仿真里电机负载是固定值参数不会表现得太敏感实物电机因惯性和摩擦力非线性同一组参数要重新调。调试顺序是先给一个大误差比如设定2000转观察pwm_duty是否平滑上升再观察是否越过设定值逐步调整。4.5 启动斜坡的必要性系统刚上电时pwm_duty如果等于50电机会突然全速启动冲击电流可能把直流电源拉低。更合理的做法是上电时从最小占空比爬升让电机逐步加速。这个逻辑可以在初始化代码里实现void motor_ramp_up(void) { unsigned char i; for (i 5; i 30; i) { pwm_duty i; delay_ms(50); } }这个函数把占空比从5%逐步增加到30%每50ms加1%整体启动时间约1.3秒。启动完成后闭环控制函数的首次执行会继续把占空比推向目标。这种做法在实物上能有效降低启动噪声和电流冲击也是判断闭环是否生效的一个手段如果启动斜坡结束后电机转速仍在上升说明闭环正在介入如果转速一直停在启动值说明反馈没接入。5. Proteus仿真验证波形、现象与参数反推5.1 加载工程与hex文件打开仿真文件夹里的仿真.DSN双击AT89C51芯片在Program File中选择motor.hex确认晶振频率为12MHz。运行仿真后一个四位数码管显示速度设定值另一个显示实测值。先按下加10和加1按键设定值会变化如果实测值还没变化说明PWM输出或测速链路有问题。5.2 用虚拟示波器直接观察PWM和测速脉冲Proteus左侧工具栏选择虚拟仪器把示波器接到P1.0可以看到频率约100Hz、占空比随设定值变化的方波。把另一个通道接到P3.2可以看到测速脉冲这两个波形是整个系统最容易出问题的两个点。P1.0波形正常但P3.2没有脉冲时优先检查测速传感器输出端是否真的连接到了INT0以及传感器地是否和单片机共地。Proteus里接地符号漏放是常见错误直接导致P3.2电平悬空INT0不触发。5.3 数码管闪烁与显示异常排查如果数码管持续闪烁优先检查动态扫描周期。扫描函数放在定时器1中断里如果中断周期超过2ms8位扫描一轮接近16ms刷新率只有62Hz视觉上会明显闪动。解决办法是把定时器1周期改到1ms或者把显示扫描拆到多个中断里。如果某一位显示的是前一位的数字残影需要确认消隐逻辑。DIG_PORT 0xFF必须在送段码之前执行顺序反了就会把上一段码带到下一位。部分同学的仿真里出现“某个数码管特别亮”往往是位选信号没有经过三极管直接由P0口驱动灌电流过大在P0到位选端之间串一个低阻值电阻可缓解。5.4 用仿真反推PI参数的正确性调PI参数时先把KI设为0只保留比例项观察系统是否能稳定在一个固定误差上。如果误差始终存在再逐步增加KI。这个步骤在Proteus和实物上都适用也能避免一上来就把积分调强产生振荡。最后对照流程图.bmp里的状态转换确认按键设置、测速计算、PWM更新三个状态的循环路径和代码一致整个闭环逻辑就算真正跑通了。本文还有配套的精品资源点击获取