ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

51单片机PWM调速实战:定时器配置与直流电机驱动全解析

51单片机PWM调速实战:定时器配置与直流电机驱动全解析 简介这是基于51单片机实现PWM直流电机调速的完整Keil工程源代码面向单片机入门学习者与自动化控制开发者适用于机器人、电动工具等需要调速控制的场景。工程利用定时器和中断产生PWM信号通过修改占空比调节电机平均电压从而控制转速核心C源码带注释可直观看到定时器初值设定、IO口输出模式配置、中断服务程序中的占空比调整等关键步骤。资源包共19个文件大小38KB除主程序外还包含Keil工程配置uvproj/uvopt、备份文件bak及hex/obj/lst/m51等编译产物便于直接打开工程查看、重新编译或烧录到开发板验证。目前已有5334人学习/下载适合希望结合完整工程理解PWM调速原理、对照代码进行调试与二次开发的读者。通过这份工程能够快速掌握51单片机控制直流电机调速的完整实现路径减少从零搭建环境和排查配置问题的时间成本。1. 51单片机PWM调速先绕过“跑马灯思维”再谈占空比拿到这份duogongnengmiaobiao.uvproj工程的时候第一反应是名字起得有点“骗人”——叫多功能秒表打开pwmzhiliufisnji.c才发现核心逻辑全是直流电机调速。这其实是很多课程设计的常态工程名沿用模板里子已经换成PWM调速。先说结论用51单片机做PWM直流电机调速真正卡人的不是PWM原理而是定时器资源怎么分配、占空比分辨率能做到多少、以及IO口直驱电机为什么会烧片。这几个问题想不清楚代码抄下来也调不出稳定转速。这篇文章基于这份带注释的源码把定时器配置、中断服务、驱动电路和调试手法完整拆一遍。适合正在做课程设计、或者想把老51片子派上用场的读者——你会发现20年前的外设做低速调速依然比一堆ARM开发板省事。2. 定时器工作模式选型方式1和方式2的分水岭在分辨率2.1 PWM频率与占空比分辨率的矛盾PWM调速的本质是让电机端电压的平均值可控。占空比 D Ton / T转速近似与D成正比。但工程实现时有一个隐藏约束如果直接用定时器溢出中断翻转IO口电平那么占空比的调节步进等于一个定时器溢出周期。假设要求PWM频率为1kHz即周期T1ms。用定时器方式28位自动重装实现中断周期是固定的比如设为50µs那么一个PWM周期需要20次中断占空比调节步进就是5%也就是只能输出 0%、5%、10%……95%、100% 这些档位。对电机调速来说5%的步进意味着转速有明显跳跃低速时尤其难受。提高分辨率的方法是把定时器溢出周期缩短但频率也跟着变。要保证1kHz的PWM频率不变同时占空比分辨率达到1%就需要一个PWM周期内有100个调节点定时器溢出周期就得设为10µs。51单片机工作在12MHz晶振下定时器最细粒度是1µs12个时钟周期10µs溢出中断勉强能做但中断服务程序里如果还有其它处理比如按键扫描、数码管刷新CPU占用率会非常高。2.2 方式1的16位计数器才是调速的正路方式1是16位计数器最大计数65535。同样在12MHz晶振下定时器溢出周期最长可达65.535ms。这意味着我们可以把整个PWM周期放进一个定时器溢出里在中断里动态修改比较值而不是用“溢出一次翻转一次”的笨办法。具体套路是定时器工作方式1设置一个初始计数值让定时器溢出周期等于PWM周期比如1ms对应1kHz。中断服务程序里做两件事——翻转IO口电平然后重新装载TH0/TL0初值。关键在这里初值不是固定不变的而是根据当前占空比动态计算。高电平期间初值设为“从当前计数到PWM周期结束还差Ton”低电平期间初值设为“从当前计数到周期结束还差T-Ton”。这种做法的分辨率取决于定时器计数粒度。12MHz晶振下1µs计数一次PWM周期1ms1000µs占空比分辨率就是0.1%1000档可调远超方式2的20档。工作模式计数器位数典型溢出周期占空比分辨率适用场景方式013位最长8.192ms低不推荐用于PWM方式116位最长65.535ms高1000档以上推荐低速高精度调速方式28位自动重装最长256µs低几十档固定频率方波、舵机控制方式3两个8位同上低串口波特率场景PWM很少用2.3 初始化代码从源码里抽出的定时器配置源码里init_timer()函数的核心配置如下void init_timer0_pwm(void) { TMOD 0xF0; // 清空Timer0的模式位保留Timer1配置 TMOD | 0x01; // Timer0工作在方式116位手动重装 TH0 0xFC; // 高8位初值配合TL0实现1ms溢出 TL0 0x18; // 低8位初值65536-100064536即0xFC18 ET0 1; // 允许Timer0中断 TR0 1; // 启动Timer0 EA 1; // 开启总中断 }初值计算逻辑12MHz晶振下机器周期12/12MHz1µs。要溢出周期为1000µs则计数值65536-1000645360xFC18。所以 TH00xFCTL00x18。如果换11.0592MHz晶振机器周期约1.085µs同样要1ms溢出计数值约65214初值就得重新算不能直接套。TMOD 0xF0这一步容易漏它的作用是只修改Timer0的配置不影响Timer1如果Timer1在跑串口。很多初学者直接TMOD0x01在单定时器工程里没问题但一旦加串口通信就会遇到Timer1波特率错乱的怪问题。3. 中断服务与占空比刷新源码主循环里的隐藏细节3.1 源码的工程结构分析看工程列表pwmzhiliufisnji.c是主程序编译产物里有.hex和.M51文件说明这个工程至少成功编译过。.M51是存储器分配映射文件调试时可以用它确认中断向量地址和变量存储位置。源码的PWM控制逻辑大概分三块初始化、占空比设置、中断服务。初始化代码上面已经拆过占空比设置通常是一个独立函数void set_pwm_duty(unsigned char duty) { if (duty 100) duty 100; // 限幅防止占空比超过100% pwm_duty duty; // 全局变量供中断服务程序读取 }注意这里有一个典型工程写法主循环里不直接操作定时器寄存器而是修改一个全局变量pwm_duty中断服务程序里才真正修改TH0/TL0。这样做的原因是避免主循环和中断同时访问寄存器造成竞争。如果主循环正在写TH0此时中断触发中断里也写TH0就会出现半个旧值半个新值的脏数据。3.2 中断服务程序的双阶段装载void timer0_isr(void) __interrupt 1 { static bit high_phase 0; // 当前处于PWM周期的高电平阶段 if (!high_phase) { // 高电平阶段装载“从当前到周期结束还需的高电平时间” TH0 (65536 - pwm_duty * 10) 8; TL0 (65536 - pwm_duty * 10) 0xFF; PWM_PIN 0; // 拉低结束高电平 high_phase 1; } else { // 低电平阶段装载剩余低电平时间 TH0 (65536 - (100 - pwm_duty) * 10) 8; TL0 (65536 - (100 - pwm_duty) * 10) 0xFF; PWM_PIN 1; // 拉高开始下一个周期 high_phase 0; } }代码逻辑拆解PWM周期为1000µs占空比变量pwm_duty范围0~100所以pwm_duty * 10就是高电平持续时间µs。进入中断先判断上一阶段如果刚结束高电平就装载低电平剩余时间到定时器同时把IO拉高准备下一轮如果刚结束低电平就装载高电平时间并拉低。这套双阶段装载方案比“单次溢出翻转”高明的地方在于它不需要在中断里做累加计数来判断是否到达翻转点每次中断只装载一次初值中断开销最小而且占空比更新从“下一周期生效”变成“当前周期剩余时间生效”——响应速度更快。实测在12MHz晶振下这段中断服务程序完整执行时间约6~8µs占PWM周期不到1%对主循环的影响很小。3.3 关于占空比极值的一个坑pwm_duty0时高电平时间为0中断逻辑变成低电平阶段装载1000µsIO保持低电机停转。pwm_duty100时高电平时间为1000µs低电平阶段装载0定时器立刻再次溢出IO几乎一直保持高电平。这两个极端状态在硬件上对应的是电机的完全停止和全速运转看起来没问题。但注意(100 - pwm_duty) * 10在pwm_duty100时等于0此时TH0/TL0装载的是65536即溢出周期为65536µs而不是0。源码里如果没做特殊处理这里会出现一个长达65ms的毛刺。我一般会在进入中断时判断一次if (pwm_duty 100) { PWM_PIN 1; TR0 0; // 停止定时器保持高电平 return; } if (pwm_duty 0) { PWM_PIN 0; TR0 0; return; }把定时器停掉比硬扛0/65536的边界更干净而且省中断开销。4. 驱动电路与隔离设计IO口直驱电机必烧单片机的5个原因4.1 电流需求远超IO能力51单片机的IO口在准双向模式下拉电流只有几百微安到几毫安灌电流稍强但也就10~20mA不同型号有差异。普通小功率直流电机空载电流在50~200mA堵转时可能到1A以上。直接拿P1.0接电机正极、电机负极接地结果只有一个IO口内部MOS管过流烧毁单片机报废。这也是为什么源码里看不到任何电机接线——因为硬件上必须经过驱动芯片或者三极管放大源码只管输出PWM波形不带载。4.2 续流二极管最容易被忽略的保命元件电机是感性负载PWM关断瞬间电流不能突变会产生一个反向电动势幅度可能是电源电压的几倍。如果没有续流二极管这个尖峰直接打到IO口上轻则复位重则击穿IO口。标准接法是二极管反向并联在电机两端阴极接电源正极阳极接电机驱动管集电极。常见做法是选用1N5819这种肖特基二极管正向压降只有0.3V左右关断损耗小。普通1N4007也能用但反向恢复时间慢高频PWM下发热明显。4.3 ULN2003与L298N两种方案的取舍驱动方案最大电流通道数是否需要光耦适用场景ULN2003达林顿管500mA/通道7不需要小功率减速电机、玩具电机L298N双H桥2A/通道2建议中大功率电机、需要正反转分立三极管MOS管取决于管子1需要定制化驱动、教学实验ULN2003内部自带续流二极管钳位到COM引脚所以用它驱动时电机的反向电动势会被内部二极管吸收只需要把COM脚接电机电源正极即可。这个细节资料里经常不写实际用过的人才知道——COM悬空的话内部钳位二极管没有回路依然会打坏前级电路。L298N的优势是自带H桥可以控制正反转。但它的导通压降有2V左右饱和压降二极管压降12V供电时电机实际只有10V转速会打折。而且L298N在PWM频率超过20kHz时开关损耗明显单片机的PWM输出最好控制在10kHz以下。4.4 共地问题驱动电路用独立电源给电机供电时必须把电机电源的GND和单片机系统的GND连在一起否则PWM信号没有参考地驱动芯片收到的电平是浮空的。这个问题在“单片机用USB供电、电机用电池供电”的场景里特别常见症状是电机转起来乱抖用示波器看PWM波形是正常的但驱动芯片输出就是不干净。检查方法是测驱动芯片输入端的电压PWM高电平时应该在2.5V以上TTL高电平门槛如果只有1.8V左右多半是共地不良或者IO口驱动能力不足需要加一级三极管放大。5. 固定频率查表法与闭环调速扩展让源码从“能转”到“转得稳”5.1 用示波器验证三个关键波形调通硬件后第一件事不是看转速而是用示波器抓PWM输出引脚比如P1.0的波形。三个指标依次验证频率是否正确。示波器光标测量两个上升沿之间的时间应该接近1ms对应1kHz。如果实测是33ms说明定时器初值算错了重新检查晶振频率和TH0/TL0装载值。占空比是否随pwm_duty变化。把占空比设为50测高电平时间应该接近500µs。如果高电平时间正确但频率变成2kHz说明双阶段装载逻辑里有一处初值算错中断里两个分支装载的时间不对称。毛刺和抖动。观察波形上升沿附近有没有额外的窄脉冲。如果有多半是中断里pwm_duty被主循环修改导致半个周期的初值漂移解决方法是把pwm_duty改成int类型或者用EA0; pwm_dutyduty; EA1;这种临界区保护。5.2 低速抖动问题与固定频率查表法占空比低于10%时电机可能出现一顿一顿的现象这不是PWM频率低而是机械死区——电机启动转矩不够PWM高电平期间电机还没转起来就进入低电平了。解决思路有两个。第一种是提高PWM频率从1kHz提到5kHz让高电平时间更密集电机更容易越过静摩擦区。但副作用是频率提高后同样的占空比分辨率下定时器初值变化更敏感需要重新计算初值。第二种是固定频率查表法。既然PWM频率固定为1kHz占空比0~100对应100组TH0/TL0初值这些初值在程序启动时就算好放进数组运行时直接查表装载省去中断里的乘法运算unsigned int pwm_table[101][2]; // 每行存TH0和TL0的初值 void build_pwm_table(void) { unsigned char i; for (i 0; i 100; i) { unsigned int high_time (unsigned int)i * 10; unsigned int low_time 1000 - high_time; pwm_table[i][0] (65536 - high_time) 8; pwm_table[i][1] (65536 - high_time) 0xFF; // 低电平初值数组在此省略实际需要存两组 } }查表法的意义在于中断服务程序里的执行时间变成完全确定值不依赖乘法编译后生成的指令条数。对于要求严格一致的PWM波形这是一个很实在的优化手法。5.3 从开环到闭环PID调速的挂载点源码实现的是开环调速——设定一个占空比电机转速跟随负载变化而波动。如果要求转速稳定可以在现有基础上加一个测速反馈电机轴上装霍尔传感器或光电码盘单片机用Timer1计数单位时间内的脉冲数。测得的实际转速与目标转速的差值经过PID计算后输出修正量叠加到pwm_duty上。这种闭环改造不需要改中断服务程序只需要在主循环里周期性调用PID计算函数。注意PID参数整定时先调P比例让系统不震荡再加I消除静差最后加D抑制超调。51单片机的计算能力有限PID频率控制在10Hz~50Hz即可太高反而浪费CPU。本文还有配套的精品资源点击获取
返回列表