51单片机定时器中断实现占空比可调PWM(附C51代码)

51单片机定时器中断实现占空比可调PWM(附C51代码) 简介基于51单片机与C语言实现的占空比可调方波PWM生成程序面向电子设计、单片机入门及嵌入式开发学习者。程序通过配置片内定时器/计数器产生中断在中断服务中切换引脚电平输出频率与占空比可控的PWM波形可作为电机调速、LED调光等控制场景的参考实现。资源共13个文件压缩包约80KB内容涵盖C源文件、Keil工程配置、hex烧录文件、编译中间文件、备份文件及电路图PDF文件结构清晰便于理解工程组成并直接烧录验证。已有2431人学习下载适合课程设计或电子竞赛中需要快速掌握定时器PWM原理的读者。借助该资源可学习51单片机定时器初值计算、中断服务编写与占空比动态调节思路在此基础上修改参数即可灵活适配不同应用需求。无论是入门学习还是实际项目参考都具备较强的实用性。1. 51单片机占空比可调PWM先搞清要不要靠delay大家做舵机或电机调速时最顺手想到的写法是“高电平delay一下低电平delay一下”。这串代码在Proteus里能转真放到舵机上要么抖成筛子要么一转就卡死原因很简单CPU在执行delay的那几百纳秒里管不了按键和显示把整个程序串成了一个人工定时的木偶。51单片机里做占空比可调的方波正向路子是用定时器溢出中断让硬件去计时主循环只负责改一个“比较值”这正是本标题要拆的东西用51单片机的定时器在C语言里生成频率稳定、占空比能随时改的PWM。它需要你懂一点定时器初值、一点中断开销概念不依赖增强型51的硬件PWM模块换到89C52、STC89C51甚至老旧的AT89S51都成立。PWM的“占空比可调”在代码里只是一个0到100的数字难的是让这个数字精确映射到引脚电平时长上。下面先从定时器数学模型入手再给一套可抄的代码最后用按键现场改占空比并告诉你怎么用LED亮度判断波形对不对。2. 定时器中断如何生成PWM一个计数器变量吃遍周期与占空比2.0.1 51单片机没有硬件PWM时输出方波靠“托底定时”基础型5180C51内核内部没有独立的PWM外设这不是缺陷而是设计上把时序控制交给定时器/计数器。要产生占空比可调方波最自然的方案是让定时器以固定的间隔产生中断比如每50µs一次每进一次中断一个软件计数器累加1这个计数器的取值空间代表整个PWM周期比如从0加到199周期就是50µs × 200 10ms即100Hz。引脚在计数值小于阈值时输出高电平大于等于阈值时输出低电平——这个阈值就是“占空比设定值”。整个过程不需要delayCPU被中断打断的时间只有几十条指令主循环还能腾出手去扫描按键、刷新数码管。2.0.2 占空比调的比较值决定“高电平保持多久”占空比的数学定义是高电平时间占总周期时间的百分比。在上述模型里设周期计数上限为PERIOD比较值为duty那么高电平时间 duty × 中断间隔占空比 duty / PERIOD。注意这里调的是“中断次数”不是直接调延时毫秒数。这样的好处是如果你要改变PWM频率只改中断间隔或者改PERIOD占空比不会跑偏反过来你改duty时频率始终被定时器锁死不会像delay版本那样一边调占空比一边改变了周期。两步分离这就是软件PWM被广泛用在51上的原因。2.0.3 定时器初值的计算公式与误差来源以12MHz晶振、机器周期12个时钟周期为例定时器每计数一次耗时1µs。要让定时器每50µs中断一次就要求计数50次。采用方式116位定时器初值 65536 - 50 65486 0xFFCETH0 (65536 - 50) 8; // 高8位0xFF TL0 (65536 - 50) 0xFF; // 低8位0xCE如果晶振是11.0592MHz一个机器周期约1.085µs50µs对应的机器周期数不是整数初值会有截断误差实测频率会偏移百分之零点几驱动舵机和LED完全够用。下表给出两种晶振下常用中断间隔的初值帮助你快速选用晶振频率中断间隔µs定时器初值十进制实测PWM频率误差% 按PERIOD20012MHz5065486012MHz10065436011.0592MHz5065512截断约0.511.0592MHz10065482截断约0.5提示中断服务函数里要重新赋初值否则定时器会从0开始继续跑周期时间变成65536个机器周期波形频率和计算值完全对不上。这个模型的关键在于中断间隔决定了PWM的时间分辨率。间隔越小、计数周期越大占空比的步进就越精细但中断占用的CPU时间也越多。50µs间隔下100Hz的PWM有200个细分档位舵机控制、呼吸灯、直流电机调速都足够。下面进入能直接烧录的代码环节。3. 一套能直接用的占空比可调PWM的C51核心程序3.0.1 定时器0初始化函数只定周期不管电平用一个定时器实现全部PWM逻辑代码量可以控制在30行内。下面这套代码基于STC89C52RC、12MHz晶振输出引脚为P1.0中断间隔50µs周期计数上限200初始占空比50%#include reg52.h sbit PWM_OUT P1^0; // PWM输出引脚 #define PWM_PERIOD 200 // 周期计数上限200 × 50µs 10ms unsigned char cnt 0; // 软件计数器 unsigned char duty 100; // 占空比设定值范围0~200 void Timer0_Init(void) { TMOD 0xF0; // 低四位清零保留定时器1设置 TMOD | 0x01; // 定时器0方式116位 TH0 (65536 - 50) 8; TL0 (65536 - 50) 0xFF; ET0 1; // 允许定时器0中断 EA 1; // 总中断开 TR0 1; // 启动定时器0 } void Timer0_ISR(void) interrupt 1 { TH0 (65536 - 50) 8; // 重装初值保证间隔稳定 TL0 (65536 - 50) 0xFF; cnt; if (cnt duty) { PWM_OUT 1; // 计数值小于占空比设定值输出高 } else { PWM_OUT 0; } if (cnt PWM_PERIOD) { cnt 0; // 计数到周期末回0开始新周期 } } void main(void) { Timer0_Init(); while (1) { // 主循环可以扫描按键或刷新显示PWM由中断自行输出 } }这段代码里duty初值设成100即占空比50%100/200。你可以观察中断函数它没有用乘除法没有计算百分比只是拿计数器和设定值比较这个设计决定中断函数能多快跑完。51在12MHz下执行一条指令约1~2µs中断函数即使在最坏情况下也就是几微秒远小于50µs的中断间隔主循环不会感觉到明显的卡顿。3.0.2 占空比参数怎么改0~200和百分比的换算上面的duty值范围是0到PWM_PERIOD200而不是0到100。如果上位机下发的是百分比例如60%换算关系是duty 百分比 × 2。这种取值的用意是提高占空比分辨率每改变1个计数值占空比变化0.5%。如果你想要1%的步进可以把中断间隔改成100µs周期计数上限设为100此时duty直接用百分比数字省去换算。3.0.3 中断里做乘除会引发占空比抖动新手最常见的改动是写“百分比转比较值”时直接在中断里做乘法PWM_OUT (cnt duty * 2) ? 1 : 0; // 不推荐一旦duty较大duty * 2这一步会占用多条指令周期导致中断执行时间拉长输出波形的上升沿位置产生几微秒的随机抖动。更好的做法是在主循环里预计算好duty值中断里只做一次比较。程序结构因此遵循一个原则中断越短越好计算东西移到主循环。这也是软件PWM和硬件PWM的本质差异——软件PWM占CPU硬件PWM完全不占但51基础型号没有这个硬件所以我们要用代码换时间。上述代码跑起来后用示波器能看到一个10ms周期、高电平宽度稳定的方波。接下来把它接到按键上实现真正“可调”的交互。4. 按键调占空比实战消抖、步进、边界处理4.0.1 按键直接改全局变量中断自动响应PWM的优势在这里表现得最明显主循环扫描按键发现按下就把duty加/减一个步进值中断服务函数下一次进入时比较值已经变了输出波形随即跟上。不需要停掉定时器、不需要重新初始化也没有任何过渡期毛刺。关键代码片段sbit KEY_UP P3^0; // 增加占空比按键低电平有效 sbit KEY_DOWN P3^1; // 减少占空比按键 #define STEP 4 // 每次按键改变4即2%占空比 void Key_Scan(void) { if (KEY_UP 0) { // 检测到按下 DelayMs(10); // 简单延时消抖 if (KEY_UP 0) { // 再次确认 if (duty PWM_PERIOD - STEP) { duty STEP; // 防止增量超出周期上限 } else { duty PWM_PERIOD; } while (KEY_UP 0); // 等待释放避免连续触发 } } if (KEY_DOWN 0) { DelayMs(10); if (KEY_DOWN 0) { if (duty STEP) { duty - STEP; } else { duty 0; } while (KEY_DOWN 0); } } }这段代码里duty PWM_PERIOD - STEP和duty STEP这两个边界判断卡死了占空比的上下限防止加到溢出。STEP取4意味着每次按键占空比变化2%100Hz下肉眼可见地变亮变暗。如果你希望调到100%后“封顶”上面第二分支置成PWM_PERIOD即可此时引脚一直输出高电平。4.0.2 消抖延时放主循环不放中断里上面用了DelayMs(10)这个函数要占用主循环10ms左右。由于PWM在中断里输出主循环卡10ms并不会让波形停摆只是按键扫描变慢。这个特性使得软件PWM在交互场景中比delay那种“堵死”结构从容得多。注意一点while (KEY_UP 0)等待释放的写法是阻塞式的如果用户一直按住按键主循环会卡在这一行但PWM照常输出。有些工程师偏好“按下一次、步进一次、按住连发”那就要加定时器扫描来判断长按这与PWM无关按下不表。4.0.3 完整连接与验证流程连接关系很简单P1.0接LED限流电阻典型220Ω到GNDP3.0和P3.1各接一个按键按键另一端接GNDI/O口内部上拉或外部上拉电阻到VCC。烧录程序后先记录LED亮度按几下增强键亮度明显上升这就是占空比升高的直观表现。如果你手头有示波器把探头夹在P1.0会看到高电平宽度随按键增加而变宽下图为“按一次增加2%”时波形预计变化按键次数duty值高电平时间msLED亮度感受01005.0明显亮11045.2略有提升51206.0明显变亮2520010.0全亮常亮从LED亮度反推占空比有一个麻烦人眼感知亮度不是线性的占空比从50%升到100%视觉变化远没有示波器上明显。所以验证占空比是否精准逻辑分析仪比人眼靠谱。如果你手里只有LED建议在10%、50%、90%三档间切换体会“微亮、半亮、几乎全亮”的区别基本能判断程序正确。按键调完占空比离“能交付”还差一步——在真实负载舵机、直流电机上跑有几个容易踩的坑要在下章说清楚。5. 验证占空比可靠的三个技巧顺带看看硬件PWM用一个数值实例来验证占空比配置是否精确设duty150、周期上限200、中断间隔50µs理论占空比 150 / 200 75%高电平时间 150 × 50µs 7.5ms。实测示波器上高电平宽度偏差应该在±20µs以内。关于重装初值的时间误差可以采用“提前重装”法在进入中断后先不重装而是在中断服务函数的靠近末尾、且与下次触发临界位置较近时重装。我的建议是维持第3章的写法——进入中断立即重装。虽然这会让重装指令距离下次定时器溢出点相差几微秒但由于每次都是固定的偏差最终展现为频率略微偏高不影响占空比精度。最后提一个进阶方向如果你用的芯片是STC89C52RC、STC15系列内部其实带了可编程计数器阵列PCA/CCP模块它可以做到“硬件PWM”而不占中断// 使用STC89C52RC的PCA模块配置PWM示意 CCON 0x00; CMOD 0x02; // PCA时钟源为定时器0溢出 CCAPM0 0x42; // 允许PWM输出 CCAP0H 100; // 比较值决定占空比 CCAP0L 100;这种模式下一旦配置好单片机完全不需要额外的中断开销占空比由比较寄存器硬件控制精度更高、可用的PWM通道也更多通常有2~4路。不过寄存器配置随芯片型号差异较大代码不像定时器模拟那样通吃。学习占空比可调PWM的原理时先用定时器模拟理解“比较值决定高电平时间”这个本质再迁移到硬件PWM后续调舵机、调电机、矩阵键盘配合显示时才不会一头雾水。本文还有配套的精品资源点击获取