简介:基于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晶振对应1us
- PWM频率 = 1 / (定时器中断间隔 × 周期计数值)
- 占空比 = 占空比计数值 / 周期计数值 × 100%
以定时器中断间隔50us为例,周期计数值与频率、占空比步进的对应关系如下表:
| 周期计数值 | PWM频率 | 占空比步进 | 典型应用 |
|---|---|---|---|
| 100 | 200Hz | 1% | LED亮度调节、低速电机 |
| 200 | 100Hz | 0.5% | 风扇调速、加热器功率控制 |
| 400 | 50Hz | 0.25% | 舵机控制(需配合1ms~2ms脉宽) |
| 20 | 1kHz | 5% | 高速电机、开关电源测试 |
注意最后一行,周期计数值越小,PWM频率越高,但占空比步进只有5%,调光时能看出跳变。这直接说明为什么频率与分辨率是一对矛盾,设计时要先定目标频率再反推计数范围。占空比计数值超过周期值时,输出保持常高,不能按百分比归一化,这是程序里需要做限幅处理的地方。
3. 用定时器中断写最小可调PWM发生器,并在Proteus里验波
这一章的代码可以在Keil C51里直接编译,烧录后P1.0口输出PWM波形。采用定时器0工作方式1(16位手动重装),中断间隔设计为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工作方式1,16位定时器 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.6us,50us对应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对应200Hz,100us对应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频率 | 适用场景 |
|---|---|---|---|---|
| 1 | 50us | 100 | 200Hz | LED调光 |
| 2 | 100us | 100 | 100Hz | 电机调速 |
| 3 | 200us | 100 | 50Hz | 舵机控制底层 |
| 4 | 20us | 50 | 1kHz | 开关电源测试 |
切换频率时有一个容易翻车的细节:pwm_duty的数值是按百分比存还是按计数值存。如果存的是计数值,切换频率后不需要改变;如果存的是百分比,那么每次切换都要把百分比乘以新的pwm_cycle重新计算计数值。建议统一按计数值存储和显示,显示时再做除法映射。
5.2 中断里做了太多事,频率和占空比互相拖累
这是排错章节里最重要的一个点。定时器0方式1不是自动重装,进入中断后要花几条指令重装TH0和TL0,再加上判断和反转IO的操作,总共约20个机器周期。如果中断间隔只有20us,那中断服务本身占用超过一半的CPU时间,主循环和显示扫描几乎每执行几步就被打断,按键响应变得非常迟钝。
更隐蔽的问题是:重装初值执行太慢会导致实际中断间隔大于设定间隔。比如设定20us,但中断服务里光重装和跳转就花了12us,实际溢出间隔变成32us,PWM频率就会偏低。这个误差不是晶振精度造成的,而是代码负载造成的。解决思路是选用方式2自动重装,这种方式定时器溢出后由硬件自动把TL0抄成TH0,不需要软件重装,节省近一半中断时间,但初值范围只有8位,最大溢出间隔256us,超过这个范围必须回退到方式1。另一个思路是降低中断频率、调大pwm_cycle,比如中断1ms,周期计数100,PWM频率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对应的占空比显示值,把显示数值校准到和实测一致,这是整个项目交付前最该做的一步验证。
本文还有配套的精品资源,点击获取