
简介基于51单片机的可调PWM发生器工程包面向嵌入式初学者与单片机爱好者解决如何利用内部定时器生成频率和占空比可调的PWM信号这一实际问题。资源共30个文件压缩包仅49KB包含C语言源文件、头文件、Keil工程配置文件以及可直接烧录的Hex文件等结构清晰便于直接编译学习和二次开发。目前已有1900人学习下载可见该主题的实用性与受欢迎程度。工程代码覆盖了定时器初始化、占空比动态调整、中断处理等核心环节通过配置预分频器与计数寄存器可灵活改变PWM频率和占空比并整合了LCD1602显示与按键调节模块便于实时观察和调整信号是一份适合系统学习51单片机定时器与PWM控制的完整实践样例。1. 基于51单片机可调PWM发生器到底在解决什么问题传统51单片机AT89S52、STC89C52这类内部并没有硬件PWM外设但这个项目依然能完成“可调PWM发生器”的全部功能。原理很直接用定时器产生固定时间基准在中断里翻转一个普通IO口再按比例改变高电平持续时间得到占空比可调的方波。换句话说你只需要一条数据线和一段几十行的C代码就能替代专用PWM芯片实现LED调光、直流电机调速和舵机控制。这个标题里的“可调”指两件事占空比可调频率最好也可调。课程设计里最常见的做法是用按键调节占空比再用数码管或LCD显示当前数值同时预留频率切换挡位。适合的人群包括正在做单片机课程设计的学生、刚入手51单片机的开发者以及需要快速验证电机驱动或灯光控制方案的硬件工程师。整套方案不依赖额外芯片Proteus里也能仿真复现成本低。2. 51单片机生成PWM的两种主流方式与占空比计算公式软件方式生成PWM前提是先搞清楚占空比和频率是怎么算出来的。一个PWM周期由高电平时间和低电平时间组成占空比 高电平时间 / 周期。比如周期10ms、高电平3ms占空比就是30%。频率则是周期的倒数10ms对应100Hz。在51单片机里周期由定时器中断来丈量高电平宽度则通过一个计数器阈值来控制。2.1 占空比可调的本质比较计数与输出翻转通用的软件PWM模式是在中断服务函数里维护一个递增计数器计数范围从0到周期值。每次中断计数器加1当计数小于占空比阈值时输出高电平否则输出低电平计数到周期值后清零重来。伪代码如下if (counter cycle) { counter 0; } if (counter duty) { PWM_PIN 1; } else { PWM_PIN 0; } counter;逻辑很简单但理解的人不多占空比的分辨率取决于周期计数值周期计数值又取决于中断频率。以12MHz晶振、12分频、机器周期1us为例定时器溢出间隔取50us周期计数设100次则PWM周期为5ms频率200Hz占空比最小步进为1/100也就是1%。如果想提高占空比分辨率要么加大周期计数值要么把定时器中断间隔调得更短比如20us或10us但后者的代价是CPU被中断占用的比例飙升主循环里的按键扫描和显示刷新会被拖慢。2.2 软件延时法与定时器中断法的适用边界有些教材和入门资料用空循环延时产生方波比如置高电平、delay_ms、置低电平、delay_ms。这种做法的优点是直观几行代码就能出波形缺点是延时期间CPU完全被占用无法同时处理按键和显示而且延时函数的实际时长受C语言编译优化影响很大Keil C51里如果开了优化等级delay循环会被改变频率漂移非常明显。定时器中断法是更可靠的做法。中断源独立于主循环波形生成不受按键扫描和数码管刷新影响频率和占空比的精度都由定时器初值决定。另外一个常被忽略的优势是中断法改占空比只需要改一个变量改动立即生效不需要重新整段延时。缺点同样是存在的每50us进一次中断意味着CPU在每次中断里只有50个机器周期可用代码必须足够精简否则中断还没执行完下一次中断请求就来了波形就开始抖动。这个点会在后面频率调节章节展开。2.3 PWM占空比计算公式与关键参数表需要记住的三个换算关系如下机器周期 12 / 晶振频率12MHz晶振对应1usPWM频率 1 / (定时器中断间隔 × 周期计数值)占空比 占空比计数值 / 周期计数值 × 100%以定时器中断间隔50us为例周期计数值与频率、占空比步进的对应关系如下表周期计数值PWM频率占空比步进典型应用100200Hz1%LED亮度调节、低速电机200100Hz0.5%风扇调速、加热器功率控制40050Hz0.25%舵机控制需配合1ms~2ms脉宽201kHz5%高速电机、开关电源测试注意最后一行周期计数值越小PWM频率越高但占空比步进只有5%调光时能看出跳变。这直接说明为什么频率与分辨率是一对矛盾设计时要先定目标频率再反推计数范围。占空比计数值超过周期值时输出保持常高不能按百分比归一化这是程序里需要做限幅处理的地方。3. 用定时器中断写最小可调PWM发生器并在Proteus里验波这一章的代码可以在Keil C51里直接编译烧录后P1.0口输出PWM波形。采用定时器0工作方式116位手动重装中断间隔设计为50us周期计数设定为100对应200Hz。占空比初始值设为30即30%。3.1 最小工程结构与定时器初始化创建Keil工程时选Atmel AT89C52或STC89C52RC都可以寄存器定义用reg52.h。初始化部分只有一个定时器需要配置串口和ADC都不开。完整初始化代码#include reg52.h #include stdio.h sbit PWM_PIN P1^0; unsigned int pwm_cycle 100; // 周期计数200Hz对应100次 unsigned int pwm_duty 30; // 占空比计数初始30% unsigned int pwm_counter 0; void Timer0_Init(void) { TMOD 0xF0; // 只清低4位保留T1配置 TMOD | 0x01; // T0工作方式116位定时器 TH0 (65536 - 50) / 256; // 50us中断间隔12MHz晶振 TL0 (65536 - 50) % 256; ET0 1; EA 1; TR0 1; } void main(void) { Timer0_Init(); while (1) { // 主循环暂时为空波形由中断独立生成 } }这段代码包含几个需要说明的参数TMOD的清位操作是为了不影响定时器1的已有配置课程设计里经常看到直接写TMOD 0x01的做法如果程序里还用串口或定时器2这种写法会把高四位配置冲掉。中断间隔50us由TH0和TL0的初值决定计算公式是65536减去机器周期数12MHz晶振下机器周期1us所以间隔正好50us。3.2 中断服务函数里切高低电平中断服务函数是整套方案的核心代码必须短。所谓短是指从进入中断到退出中断的机器周期数要控制在50个以内否则下一次溢出标志会在服务函数执行期间置位产生中断嵌套或丢失。实际实现void Timer0_ISR(void) __interrupt 1 { TH0 (65536 - 50) / 256; // 重装初值 TL0 (65536 - 50) % 256; pwm_counter; if (pwm_counter pwm_cycle) { pwm_counter 0; } if (pwm_counter pwm_duty) { PWM_PIN 1; } else { PWM_PIN 0; } }逻辑说明pwm_counter从0加到99然后归零一共100次中断构成一个完整的PWM周期。pwm_duty设为30时计数0到29期间输出高电平计数30到99期间输出低电平占空比恰好30%。需要调低占空比时把pwm_duty改小即可波形不需要重新初始化定时器。注意这里用“小于”而不是“小于等于”是为了避免pwm_duty等于pwm_cycle时发生计数回绕异常同时在pwm_duty为0时输出恒低电平。重装初值分两行写是为了保证TH0和TL0的赋值顺序准确。如果颠倒写成TL0先赋值TH0后赋值定时器可能在两条指令之间按错误初值溢出一次导致第一个中断间隔偏短。Keil C51在O2优化下也可能自动调整顺序但保险起见按先高后低写。3.3 用Proteus仿真51单片机验证波形的最小电路没有开发板时Proteus仿真可以直接验证这段代码。在Proteus原理图里放置AT89C52、两个电容和晶振电路12MHz、两个30pF电容、以及一个示波器或虚拟示波器。把P1.0引脚连接到示波器通道A单片机RST引脚接10uF电容和10k电阻复位电路然后加载Keil生成的HEX文件运行。示波器上应该看到频率约200Hz、占空比约30%的方波。如果没有波形先检查代码里是否开了EA总中断再看P1.0对应的是P1口第0位还是其他引脚。Proteus里的AT89C52默认运行频率就是12MHz如果改成了其他频率定时器初值必须按比例重算20MHz晶振下机器周期是0.6us50us对应83.33个机器周期初值就变成65536减83不再适用。另一个仿真验证技巧是在Proteus里放一个直流电压表加RC低通滤波把PWM输出经过一个1k电阻和10uF电容接地示波器观察电容两端电压稳态电压值应当接近VCC乘以占空比。这个办法能直观看出占空比是否连续可调比直接看高低电平更明显也方便后面按键调节时确认数值变化。4. 用按键和数码管把“可调”做成硬件交互标题里的“可调”落到硬件上就是按键加数码管。常见课程设计配置是两个独立按键分别加减占空比一个三位数码管显示当前数值。按键用P2口数码管用P0口加74HC245驱动或直接用共阳数码管配PNP三极管选位。主循环负责扫描按键并刷新显示中断里只负责生成波形两边互不干扰。4.1 独立按键与矩阵键盘怎么选按键数量少的时候用独立按键即可每个按键接一个IO口到地IO口内部上拉或外部上拉电阻。如果希望面板更简洁也可以做成4x4矩阵键盘过程是逐行送低电平再读列电平配合延时消抖判断组合键。独立按键优点是程序简单每个按键一个if判断就行矩阵键盘优点是按键数量多但扫描过程要放在主循环里不能放在中断里否则时间不够用。课程设计评讲时矩阵键盘更占分但独立键盘调试省事我的建议是先跑通独立键盘调完PWM再换矩阵键盘升级交互。按键部分的完整实现sbit KEY_UP P2^0; // 增加占空比 sbit KEY_DOWN P2^1; // 减少占空比 void Key_Scan(void) { static unsigned char key_state 0; if (KEY_UP 0) { key_state 1; if (pwm_duty pwm_cycle) { pwm_duty; } } else if (KEY_DOWN 0) { key_state 2; if (pwm_duty 0) { pwm_duty--; } } else { key_state 0; } }这个扫描逻辑的核心问题是不能加延时因为主循环单次执行越短数码管刷新就越及时。判断按键是否按下直接读引脚电平配合外部硬件上拉。pwm_duty的上下限限幅作用很重要超过pwm_cycle会变成恒高电平小于0则占空比含义反转这两处都必须挡住。电平整定为低有效是因为51单片机的IO口在P2口硬件复位后是高电平按下去接地为低电平这个约定在接线时不要弄反倒。4.2 按键消抖与调速步进机械按键按下和松开时引脚电平会快速跳动几十ms如果直接响应会导致一次按键变成多次加减。常见做法是延时消抖检测到电平变化后延时20ms再读一次如果电平相同才确认按键。但这种做法放在主循环里会拖慢PWM调节响应同时数码管刷新会闪烁。更专业的处理是使用定时器扫描键盘每隔5ms执行一次按键读取计满4次相同电平才认定为有效按下。课程设计的稳妥妥协是把按键读取和数据刷新都放主循环间隔5ms执行一次牺牲很少的调节速度换取代码可读性。调速步进也值得讨论。步进为1时按一下变1%从0调到100要按100次。步进为5时快但精细调节做不到。折中方案是按住超过1秒后步进自动加大到10松开再按恢复1。实现方式是记录按键按下的时长在主循环里判断。这类交互细节评审老师很吃但不要让加键消抖逻辑跑到中断里中断代码越短越好。4.3 数码管动态扫描与段码表占空比数值显示用两位或三位数码管都行。三位可以显示到100%两位则上限99%正好压住pwm_cycle等于100时的情况。共阳数码管是课程设计的主流选择段码表由共阳接法推导点亮一个段时对应引脚拉低。下面是0到9的段码表排列顺序是dp g f e d c b a共阳取反后填入unsigned char code seg_table[10] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, // 0 1 2 3 4 0x92, 0x82, 0xF8, 0x80, 0x90 // 5 6 7 8 9 };动态扫描显示代码同样放主循环原理是逐位点亮、轮流送段码。秘诀在于扫描时先关选中位再送数据防止显示残影。处理函数void Display_Value(unsigned int value) { unsigned char bai value / 100; unsigned char shi value % 100 / 10; unsigned char ge value % 10; P0 0xFF; // 先关断所有位消除拖影 P2 0x04; // 选通百位 P0 seg_table[bai]; Delay_Ms(1); P0 0xFF; P2 0x08; // 选通十位 P0 seg_table[shi]; Delay_Ms(1); P0 0xFF; P2 0x10; // 选通个位 P0 seg_table[ge]; Delay_Ms(1); }位选用的P2.2、P2.3、P2.4和独立按键共用P2口看起来冲突实际不会发生在同一时刻。Scan_Key读的是P2.0和P2.1显示选位用P2.2到P2.4按键和显示不会同时操作同一个引脚。P0口输出段码时要注意加排阻51单片机P0口是开漏输出不加上拉电阻段码电流不够数码管亮度会很暗。显示位数要与pwm_cycle匹配。周期计数值为100时显示0到100注意100是三个字符。若使用两位数码管必须把pwm_cycle限制在99以下否则显示截断会造成误解比如实际占空比100%显示为00%。5. 频率调节、误差分析与中断负载排错前面的实现固定了200Hz频率但可调PWM发生器通常要求频率也可变。频率调整最直接的方法是改变定时器初值让中断间隔变化比如50us对应200Hz100us对应100Hz。但改初值要同时考虑周期计数值如果pwm_cycle不变而初值变了PWM频率变化但占空比步进比例不变需要理解这里面哪个参数应该一起协调。5.1 定时器重装值、机器周期与频率三者的换算频率、定时器初值、周期计数值三者之间的关系如下PWM频率 1 / (定时器溢出间隔 × pwm_cycle)。定时器溢出间隔的计算公式是(65536 - 初值) × 机器周期。12MHz晶振、机器周期1us时初值65506对应溢出间隔50us。pwm_cycle为100时PWM频率为1 / (50us × 100) 200Hz。如果想把频率降到100Hz可以先保持pwm_cycle为100把溢出间隔改为100us对应的初值为65436这组参数直接可用。频率挡位切换设计为查表方式比逐个计算更可靠。预先把几组初值存成一个数组主循环判断按键后切换索引。常见挡位表如下挡位定时器间隔周期计数PWM频率适用场景150us100200HzLED调光2100us100100Hz电机调速3200us10050Hz舵机控制底层420us501kHz开关电源测试切换频率时有一个容易翻车的细节pwm_duty的数值是按百分比存还是按计数值存。如果存的是计数值切换频率后不需要改变如果存的是百分比那么每次切换都要把百分比乘以新的pwm_cycle重新计算计数值。建议统一按计数值存储和显示显示时再做除法映射。5.2 中断里做了太多事频率和占空比互相拖累这是排错章节里最重要的一个点。定时器0方式1不是自动重装进入中断后要花几条指令重装TH0和TL0再加上判断和反转IO的操作总共约20个机器周期。如果中断间隔只有20us那中断服务本身占用超过一半的CPU时间主循环和显示扫描几乎每执行几步就被打断按键响应变得非常迟钝。更隐蔽的问题是重装初值执行太慢会导致实际中断间隔大于设定间隔。比如设定20us但中断服务里光重装和跳转就花了12us实际溢出间隔变成32usPWM频率就会偏低。这个误差不是晶振精度造成的而是代码负载造成的。解决思路是选用方式2自动重装这种方式定时器溢出后由硬件自动把TL0抄成TH0不需要软件重装节省近一半中断时间但初值范围只有8位最大溢出间隔256us超过这个范围必须回退到方式1。另一个思路是降低中断频率、调大pwm_cycle比如中断1ms周期计数100PWM频率10Hz适合加热控制不适合调光。推荐的经验值是中断服务函数控制在25个机器周期以内中断间隔不少于30us即200Hz以上的PWM频率都能稳定生成。低于这个数值时要警惕监听失真用示波器看到的频率和计算值对不上先查这里。5.3 用逻辑分析仪验证波形并定位边缘误差验证环节不要只靠Proteus用逻辑分析仪或示波器实测才是工程习惯。接线方式是把P1.0接逻辑分析仪的通道0地接共地设置采样率1MHz以上触发方式选下降沿。观察以下几个指标周期是否等于理论值、高电平宽度是否稳定、有没有偶发毛刺。毛刺通常出现在pwm_duty和pwm_cycle相等的边界条件即占空比限制在100%时输出始终为高限制在0%时始终为低这两个状态下不存在PWM波形逻辑分析仪显示为直流不要误判为故障。实测时如果发现实际频率比理论值低约几个百分点是中断服务执行时间叠加的结果。比如设定50us实测周期5.2ms而不是5.0ms说明每个PWM周期里累加了约20个机器周期的服务负载。定性分析比定量修正更重要。这个误差对LED调光完全可接受对舵机控制则会产生可感知的角度偏移需要把服务开销计入重装值初值改为65536减去(50us服务周期数加设定间隔对应的机器周期数)这样实际溢出间隔等于设定值。6. 把可调PWM接到LED呼吸灯和舵机上做验证波形调通之后最终要接到真实负载上验证。课程设计最常见的两个目标是LED呼吸灯和舵机控制两者对PWM参数的需求完全不同正好用来验证前面写的“可调”。呼吸灯要求占空比在0到100之间缓慢连续变化人眼感受到亮度随占空比呈幂函数关系线性渐变会出现中间段变化快、两端变化慢的问题。优化做法是采用指数曲线目标占空比每隔20ms按指数规律递增实际值逼近目标值。代码逻辑是维护一个target变量每50次主循环更新一次占空比加减的步长随当前值动态变化当前值小时步长大当前值大时步长小观感更自然。PWM频率保持在200Hz以上低于100Hz时能看到明显闪烁。舵机控制的参数是50Hz周期、0.5ms到2.5ms的高电平脉宽。以0.5ms对应0度、2.5ms对应180度为例周期20ms内占空比范围是2.5%到12.5%。这个范围前面表格里对应中断200us、周期计数100高电平计数值应为2到12换算每1度约0.056个计数分辨率不够精准。更精确的做法是用定时器1做微秒级脉冲输出每次触发输出一个脉宽可变的单脉冲而不是连续PWM脉宽由查表获得步进10us对应约1.8度。大功率电机调速场景中注意在PWM输出和电机驱动之间加隔离和保护。先确认负载断开时PWM占空比从0缓慢上升到目标值避免上电瞬间满占空比冲击驱动管。H桥电路还需要额外安排死区时间防止上下桥臂同时导通这项功能51单片机本身不具备要在驱动芯片选型和软件顺序上留出时间余量。最后用逻辑分析仪实测舵机中点脉宽1.5ms对应的占空比显示值把显示数值校准到和实测一致这是整个项目交付前最该做的一步验证。本文还有配套的精品资源点击获取