尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

51单片机可调PWM发生器设计与实现:定时器中断与占空比调节详解

51单片机可调PWM发生器设计与实现:定时器中断与占空比调节详解 简介面向51单片机入门与进阶学习者的可调PWM发生器工程资源基于定时器中断生成PWM信号支持按键动态调节占空比并借助1602液晶显示当前参数解决PWM波形生成与调节的核心问题。压缩包共30个文件涵盖main.c、delay.c、key.c、1602.c等C语言源码配套头文件、Keil工程文件及编译生成的hex、obj、lst等文件既可直接烧录验证也便于结合工程进行断点调试整体约49KB轻量易用。已有1900人下载学习适合单片机课程设计或日常练习。通过该资源可掌握定时器初值计算、PWM占空比控制、中断服务程序编写、按键消抖及LCD1602显示驱动等实用技能并能在其基础上快速扩展出电机调速、呼吸灯等典型应用场景。1. 可调PWM发生器在51上要解决什么无硬件PWM的约束与主要设计目标想用一块 51 单片机做一个频率、占空比都能现场改的方波信号源说穿了就是要把两个很简单的问题串起来参数怎么通过按键或者电位器改以及改完以后怎么输出一个稳定、能带负载的 PWM 波形。问题是基础型 51 单片机AT89C52、STC89C52 这一类片内没有硬件 PWM 外设更没有死区插入、刹车输入这些增强功能绝大多数工程师最后走的是同一条路用定时器中断翻转 IO用两个不同的计数初值分别控制高电平时间和低电平时间拼出一个完整的 PWM 周期。这个标题在课程设计和毕业设计里出现频率很高因为它覆盖了“51 单片机课程设计”最常见的几个得分点定时器、按键扫描、动态数码管、Proteus 仿真波形。同时也覆盖了一部分产品场景比如把 PWM 输出端接一个 MOS 管去做风扇调速或者 LED 调光。文章不打算复述某一个现成源码包而是把定时器参数怎么算、按键怎么不干扰 PWM 中断、输出级怎么接负载这三件事讲透。新手可以照着接线和抄代码老手能从这里看到软件 PWM 的误差边界、故障保护和死区这些容易被忽略的地方。2. 定时器中断模拟PWM频率、占空比公式与误差边界2.1 为什么51上做PWM首选“定时器IO翻转”翻 AT89C52 的数据手册你会找到定时器、串口、外部中断就是找不到一个叫 PWM 的定时器通道。这不是芯片缩水是这类 8051 架构的年代设定决定的。既然片内没有现成硬件我们只能把定时器 T0 当时基在中断服务函数里翻转 IO。高电平期间装一个较长的定时初值低电平期间装另一个初值两个初值合在一起决定 PWM 周期高电平初值单独决定占空比。常见做法是选用定时器 0 的方式 1也就是 16 位定时器。单次定时范围可以到 65536 个机器周期假设晶振 12MHz、标准 12T 内核机器周期是 1us那么单次定时范围就是 1us 到 65536us对应频率能覆盖 16Hz 到最高几十 kHz。对课程设计里的直流电机调速、LED 亮度调节、舵机控制来说已经足够。如果晶振换成 11.0592MHz机器周期约 1.085us公式里要同步替换否则频率偏差会超过 8%这是很多 Proteus 仿真里波形对不上示波器读数的第一原因。#include reg52.h sbit PWM_OUT P1^0; // PWM 输出引脚 unsigned int highCount 500; // 高电平半周期计数值 unsigned int lowCount 500; // 低电平半周期计数值 void Timer0_Init(void) { TMOD 0xF0; // 只改低四位保留 T1 原有配置 TMOD | 0x01; // T0 工作方式 116 位定时 TH0 (65536 - highCount) 8; TL0 (65536 - highCount) 0xFF; ET0 1; // 使能 T0 中断 EA 1; // 开总中断 TR0 1; // 启动定时器 } void Timer0_ISR(void) interrupt 1 { static bit isHigh 1; // 当前输出电平状态 if (isHigh) { // 高电平结束翻低装载低电平时长 isHigh 0; PWM_OUT 0; TH0 (65536 - lowCount) 8; TL0 (65536 - lowCount) 0xFF; } else { // 低电平结束翻高装载高电平时长 isHigh 1; PWM_OUT 1; TH0 (65536 - highCount) 8; TL0 (65536 - highCount) 0xFF; } } void main(void) { PWM_OUT 1; Timer0_Init(); while (1) { // 按键扫描、数码管刷新都放这里 } }这段核心逻辑是每次中断意味着一个半周期结束翻转引脚后重新装入另一段定时值。高电平持续时间由 highCount 决定低电平持续时间由 lowCount 决定。如果想让输出从 0% 平滑渐变到 100%只要在主循环里修改这两个变量下一次半周期翻转时新值就会生效。需要注意的是这种软件模拟方式做不到硬件 PWM 那么均匀。中断响应本身有 3 到 8 个机器周期的延迟重装 TH0/TL0 时定时器也没有暂停所以每个边沿都会有几微秒的抖动。手头如果只有普通万用表察觉不到但示波器放到微秒级时隙时能看到波形边缘不齐。因此我一般把可调上限控制在 10kHz 到 20kHz频率再高就换带硬件 PWM 的型号没必要在 51 上死磕。2.2 PWM频率、占空比计算公式与频率分辨率在 12MHz、12T 内核、机器周期 1us 的前提下高电平计数 H 和低电平计数 L 直接决定了输出波形参数。下面这几个式子是在 51 上调参之前必须写在本子上的目标参数计算公式说明PWM 周期T (H L) usH 与 L 各自是计数值不是初值PWM 频率f 1000000 / (H L) Hz晶振频率不同要替换分子占空比D H / (H L) × 100%只调 H 不调 L频率一定会变频率分辨率Δf ≈ f² / 1000000 Hz每加减 1 个计数值频率变化量单次计数上限H 或 L ≤ 65535超过后定时器溢出周期边长举个例子如果 H 250、L 750那么周期是 1000us频率 1kHz占空比 25%。如果保持频率不变、把占空比调到 50%应让 H 500、L 500。很多初学代码只写了 H 的变化按下占空比加一后 L 不动示波器上一看频率跟着跑了还以为芯片坏了。这里还藏着一个设计细节调占空比的步进应该是多少计数值。如果软件里用一个 pwmPeriodConst 固定总周期比如 1000那么 1% 占空比正好是 10 个计数值用整数运算不会丢精度。可如果用一个任意数比如总周期等于 99799% 占空比算出 987 个计数百分比之间有跳变。所以我做面板时习惯把频率做成固定档位例如 100Hz、200Hz、500Hz、1kHz、2kHz、5kHz每档对应一个固定的总周期常数占空比用归一化方式去算这样的可调 PWM 在显示和实际输出之间不会有“调 50% 实际是 49.2%”的尴尬。2.3 为什么“可调PWM”不能只调占空比参数联动策略如果标题里的“可调”只指占空比那实现非常简单。但“PWM 发生器”通常意味着频率也要可调于是问题来了频率和占空比是两个独立变量而中断重装只有两个寄存器初值用户每按一次“占空比加”按键软件必须决定是只改 H 还是同时改 H 和 L。只改 H周期就变长频率会漂移到别处同时改 H 和 L才维持了频率不变。“频率再细调”和“占空比细调”还要处理一个手感问题1kHz 时每档 1Hz 和 100Hz 时每档 1Hz到底是哪种常见做法是频率用查表而非连续加减档位之间按整数倍或对数间隔分布这样在低频段和高频段都能保证可用的调整精度。占空比则固定 1% 步进配合下一章要写的按键消抖逻辑。unsigned int pwmPeriodConst 1000; // 目标周期计数值12MHz 下对应 1kHz unsigned int highCount; unsigned int lowCount; void RebuildPwmByDuty(unsigned char dutyPercent) { // 限制上下限避免 H 为 0 或等于周期导致输出停振 if (dutyPercent 99) dutyPercent 99; if (dutyPercent 1) dutyPercent 1; // 用整数乘法代替浮点百分比避免中断外计算浮点 highCount (unsigned int)((unsigned long)pwmPeriodConst * dutyPercent / 100UL); lowCount pwmPeriodConst - highCount; }这段代码没有浮点运算是因为 C51 的浮点库很占资源而且在快速连按按键时浮点除法可能让主循环卡顿影响数码管刷新。pwmPeriodConst 为 1000 时dutyPercent 从 1 到 99 都能得到精确结果当它不能被 100 整除时最后一位会出现跳动但控制在 1% 步进可接受的范围内。实际调试时还有一点容易被忽略如果中断正在执行主循环恰好改了 highCount下一次中断装载会用新值那么当前这个半周期会被截短或者拉长。对电机、LED 来说影响可以忽略对于要求波形连贯的场合可以在主循环里设一个 updateFlag由中断在翻转点去读取新值保证参数只在边沿生效。51 上做这个很简单加一个 if(updateFlag) 判断即可但很多人只有等到示波器上看到毛刺才会去补这一层。3. 可调面板与参数显示按键、数码管和Proteus仿真联调3.1 硬件面板设计和接线要点四个按键加四位数码管够用把“可调”这层意思落地最少需要两个设置键一个管频率、一个管占空比。但实际做起来四个独立按键频率加、频率减、占空比加、占空比减更符合直觉按键扫描代码也更容易组织。显示部分用四位数码管动态扫描前两位显示频率单位 Hz后两位显示占空比0 到 99。如果想区分当前在调哪个参数可以加一个 LED调频率时点亮调占空比时熄灭。功能建议引脚说明PWM 输出P1.0默认准双向口可换推挽模式按键 ×4P2.4 ~ P2.7低有效未按下时为高电平数码管段码P0 口P0 是开漏结构需要外接 10k 上拉排阻数码管位选P2.0 ~ P2.3共阴数码管位选高有效故障输入P3.2INT0 外部中断第 4 章会用到这里把按键放在 P2 口而不是 P3 口是为了把 P3.2 留出来给外部中断故障保护。很多课程设计里按键直接占满 P3 口后面想加过流保护时才发现 INT0 冲突只能飞线很狼狈。如果你完全没有故障保护需求按键放在 P3 口也完全可以。数码管部分我直接用共阴四位数码管位选通过 P2.0 到 P2.3 外接 NPN 三极管驱动。要注意的是P0 口本身没有内部上拉直接接共阴数码管段码时要靠外部上拉电阻提供高电平驱动电流。如果省略上拉电阻仿真可能看起来能亮实物上会出现段码亮度不均甚至缺笔画。3.2 主循环里扫按键中断里只改计数初值按键逻辑最忌讳的就是在定时器中断里做消抖。中断服务函数的执行时间每增加 100usPWM 最高可用频率就下降一截而且消抖延时会让 PWM 波形产生不可预测的缺口。正确的分工是中断只负责翻转引脚和重装初值按键扫描、数码管刷新全部放在主循环里。sbit KEY_FREQ_UP P2^4; sbit KEY_FREQ_DN P2^5; sbit KEY_DUTY_UP P2^6; sbit KEY_DUTY_DN P2^7; void DelayMs(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) for (j 0; j 120; j); } void ScanKeys(void) { if (KEY_FREQ_UP 0) { DelayMs(10); // 软件消抖 while (KEY_FREQ_UP 0); // 等待松手防止一次按下连续触发 if (freqIndex 5) { freqIndex; pwmPeriodConst periodTable[freqIndex]; RebuildPwmByDuty(dutyPercent); } } if (KEY_DUTY_UP 0) { DelayMs(10); while (KEY_DUTY_UP 0); if (dutyPercent 99) { dutyPercent; RebuildPwmByDuty(dutyPercent); } } // 频率减和占空比减的代码逻辑与此一致只是方向相反 }消抖用 10ms 延时是惯例这一版用松手检测简单可靠缺点是按着不放不会连加。对于课程设计够了。如果不想阻塞主循环可以把“等待松手”改成边沿检测记录上一次按键电平只在电平从 1 变 0 时响应一次这样主循环的数码管刷新不会被卡住。数码管动态扫描的套路同样在 while(1) 里每次调用只点亮一位轮流点亮 4 位利用人眼视觉暂留形成完整显示。下面这段用查表法0~9 的共阴段码直接放在 code 区省下昂贵的 RAM。unsigned char code segTab[] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; void DisplayProcess(unsigned int value) { unsigned char i; unsigned char dispBuf[4]; dispBuf[0] segTab[value / 1000 % 10]; dispBuf[1] segTab[value / 100 % 10]; dispBuf[2] segTab[value / 10 % 10]; dispBuf[3] segTab[value % 10]; for (i 0; i 4; i) { P2 0x0F; // 关闭所有位选消隐 P0 dispBuf[i]; // 先送段码 P2 | (1 i); // 再打开第 i 位防残影 DelayMs(1); } }这里有个顺序问题必须先关位选、再送段码、最后打开位选。如果先开位选再送段码切换瞬间旧段码会在新一位上闪一下肉眼看到的就是拖影。DelayMs(1) 让每位停留约 1ms4 位一轮约 4ms刷新率 250Hz足够稳定。想提升亮度就把延时加大到 2ms但按键响应会变慢要自己权衡。3.3 Proteus仿真看波形示波器读频率和占空比的验证步骤Proteus 仿真 51 单片机几乎是这类课程设计的标配环境。接线仿真时除了单片机再放一个示波器到 P1.0 上运行后要重点确认三件事频率对不对、占空比对不对、边沿是否干净。操作顺序是这样的先在 Proteus 里放置 AT89C52双击芯片把晶振频率改成 12MHz。很多人的仿真从库里面拖出来就用默认晶振是 11.0592MHz而代码里按 12MHz 手写初值结果示波器读出频率比预期偏低去改代码折腾半天。然后 Keil 里勾选 Output 页的 Create HEX File编译后把生成的 hex 文件烧进 Proteus 的单片机模型里。运行后示波器如果显示一条直线先查 EA、ET0、TR0 三个位有没有打开再查中断函数里的 interrupt 编号是不是 1。提示Proteus 自带的示波器自动量程下周期数字会频繁跳动。把它固定到 1ms/div打开光标模式量一个完整周期的两个边沿再量高电平宽度比直接看屏幕左下角读数可靠得多。频率、占空比都验证完再回头看看按键和数码管显示。如果按下“占空比加”后示波器波形不变大概率是中断函数里直接用旧的 highCount/lowCount主循环改了参数但没调用 RebuildPwmByDuty。这是软件 PWM 最容易踩的坑调参入口和参数应用点脱节。4. 输出端才是分水岭MOS管驱动、舵机电机负载与PWM故障保护4.1 51的IO口带不动功率负载用AO3400A做低边MOS开关51 单片机准双向 IO 口的高电平驱动能力非常弱数据手册典型值只有几十到几百微安直接推 LED 都要限流更别说电机和灯带。所以接了负载的输出端通常要用一个 N 沟道 MOS 管做低边开关。PWM 引脚接栅极源极接地漏极接负载下端负载上端接电源正极。这个电路里最常用的管子是 AO3400A它是逻辑电平 MOS栅极阈值电压很低5V 单片机的电平可以直接打开不需要额外加栅极驱动芯片。器件参数推荐取值作用栅极串联电阻 RG10Ω ~ 100Ω抑制高频振铃栅源下拉电阻 RGS10kΩ防止上电瞬间误开通续流二极管1N5819 或 SS34感性负载关断时续流负载连接负载一端接正电源一端接 D低端开关不能用于高端驱动栅源下拉电阻是很多人会漏掉的。单片机上电复位时 IO 口是弱上拉还是高阻不定如果在 mos 管栅极悬空时误触发电平负载会先转一下再停。接一个 10kΩ 下拉把栅极默认电压钳在 0V才能保证上电瞬间输出是关闭的。感性负载例如电机或者继电器线圈则必须在负载两端反向并联一个快恢复二极管否则关断瞬间的尖峰电压会通过漏极反灌到 MOS 管一次就可能击穿。4.2 负载决定频率舵机看脉宽、电机看噪声、LED看频闪同一个可调 PWM 发生器接不同的负载时频率选择完全不同。舵机控制的重点是脉冲宽度而不是占空比。标准 50Hz 模拟舵机周期 20ms0.5ms 脉宽对应 0 度1.5ms 对应 90 度2.5ms 对应 180 度。如果这个可调 PWM 发生器把频率调成 200Hz占空比再对脉宽时间也对不上舵机会抖动、发烫甚至嗡嗡响。所以“51 单片机 舵机”场景里调节对象一定要设计成脉宽而非百分占比空比。直流电机调速则看转速稳定性和噪音。频率低于 5kHz 时电机容易听到明显的电枢啸叫和转速脉动频率超过 20kHz 以后进入超声波范围人耳听不到但 MOS 管开关损耗明显增加波形边沿也会因为栅极电容变差。我一般在小功率 12V 电机上取 10kHz 到 15kHz这个范围内转速平稳、开关电源也不会尖啸。LED 和 RGB 灯带调光最好不低于 1kHz。低频 PWM 调光在慢速移动的摄像头画面里会产生滚动暗条频率提到 800Hz 以上基本看不出来。做“呼吸灯”效果时频率选 1kHz占空比按正弦表缓慢变化效果最顺滑。注意这里说的是单色 LED 调光如果是 WS2811 这类灯珠数据和 PWM 是两回事51 只需要按协议发数据不涉及可调 PWM 波形。4.3 故障保护与死区软件PWM绕不开的安全边界搜索热词里“pwm 故障保护”出现频率很高但课程设计代码里很少见原因是它不属于“能点亮灯就行”的范畴。等你驱动电机或者加热丝时堵转、过流、MOS 管发热烧掉都是真实事故。低成本方案是给 MOS 管低边串一个采样电阻电流变大时电阻压降升高比较器 LM393 输出跳变接到 INT0 外部中断。中断里立刻停掉 TR0、拉低 PWM 引脚、置故障标志比 CPU 在主循环里轮询 ADC 快得多。void Ext0_ISR(void) interrupt 0 { EX0 0; // 先关外部中断防止持续触发 TR0 0; // 停掉 PWM 定时器 PWM_OUT 0; // 输出拉到低电平关闭功率管 faultFlag 1; // 给主循环一个报警标志 } void main(void) { // 初始化定时器和外部中断后进入主循环 while (1) { if (faultFlag) { // 这里可以驱动蜂鸣器或 LED 报警 // 按键确认并清除故障后重新打开中断重新启动 } else { ScanKeys(); DisplayProcess(currentValue); } } }中断服务函数里不能做长时间等待也不建议在这里清故障否则主循环永远没有机会去处理其他任务。faultFlag 只是把问题抛给主循环由主循环负责显示报警和等待复位。这样硬件保护速度足够软件逻辑又清爽。再来谈死区。51 做单路 PWM 不涉及死区但如果你把标题扩展到双路 H 桥、驱动直流电机正反转事情就变了。上下两个 MOS 管绝不能同时导通否则电源到地直接短路。单片机的上下桥驱动波形在逻辑上还要加一个延迟先让当前导通的管子关闭等待几微秒后再打开另一个管子。51 没有硬件死区这个延迟必须由软件强制插入而且由于中断响应时间不确定死区时间会有抖动。所以做 H 桥时最好把 PWM 频率控制在 5kHz 以内给软件留足余量想上更高频率、更可靠地做硬件死区就得换带 CCU6 或高级定时器的单片机了这也是为什么“CCU6 输出 3 相 6 路 PWM 波控制 BLDC”这类方案不会选择 51 的原因。提示如果这个项目只是一台纯信号发生器不带功率负载故障保护这一段可以直接省掉。但标题里如果带了“温控风扇”“电机控制”这类词过流保护和栅极下拉电阻就是必须的答辩时讲到这一层反而比单纯堆代码更出彩。5. 让“可调”可验证示波器校准、ADC无级调节和闭环温控扩展5.1 用示波器做最终验收三个参数一次量到位设备调试的完成标准不是代码能编译、数码管能亮而是示波器读数要和面板设定值一致。先设 100Hz、50%测量高电平时间 5ms、周期 10ms再设 1kHz、25%高电平 250us、周期 1ms。如果数值对不上优先检查晶振频率和定时器初值公式而不是去怀疑按键。示波器测量占空比时触发电平最好调到波形幅度的一半否则高低电平判定阈值偏移读数会虚高或者偏低。启动一瞬间还要看一眼很多 51 单片机上电默认 IO 为高电平软件加载完成后 PWM 才启动中间这几十毫秒输出可能存在一个短暂高电平。对于接 LED、驱动小电机没问题但如果负载是高灵敏执行器最好在 main 函数第一行就把 PWM_OUT 拉低再初始化定时器能减少绝大多数上电误动作。5.2 ADC电位器实现无级调节以及温控风扇这种闭环场景按键按 1% 步进调节适合课程设计但做“无级可调”时手感更好的方式是电位器。用 STC15 系列内置的 10 位 ADC或外接常见的 PCF8591把电位器电压映射成 0 到 99 的占空比。电位器阻值选 10kΩ 或者 50kΩ太小了 ADC 源阻抗高读数会抖动。unsigned int adcValue; unsigned char dutyPercent; while (1) { adcValue GetAdcValue(0); // 读取 ADC 通道 0 dutyPercent (unsigned char)(adcValue * 99UL / 1023); RebuildPwmByDuty(dutyPercent); DelayMs(20); // 采样滤波避免电位器毛刺 }这段 ADC 到占空比的线性映射是最常见的。注意如果用在小风扇温控项目里风速和温度的关系并不线性合理做法是 NTC 热敏电阻分压经 ADC 读温度查表得到目标 PWM 占空比再叠加一点比例调节让风扇转速随温度连续上升。这类“基于 51 单片机温控风扇设计”的方案核心就是这一节的可调 PWM 前端加上一个温度采样表闭环以后就不再需要手工按键调节了。5.3 几个容易误判的排错点现象常见原因处理方式频率整体偏低晶振配置不是 12MHzProteus 里检查单片机属性按下按键占空比不变主循环没调用 RebuildPwmByDuty在按键分支里加函数调用数码管一位特别亮动态扫描顺序反了确认先关位选再送段码示波器波形边缘振铃栅极电阻过大或飞线过长减小 RG缩短驱动线上电瞬间电机转一下栅极没有下拉电阻在栅源之间并联 10kΩ频率高时波形严重畸变中断软件占用时间太长降低 PWM 频率或换硬件 PWM 芯片可调 PWM 发生器在 51 上的调试技巧归根结底就是拿着示波器在“改参数”和“看波形”之间来回走。代码上的分层理清了按键、显示、输出三个环节各管各的剩下的问题基本都出在晶振配置和接线顺序上而这两样在 Proteus 仿真里最容易暴露出来。本文还有配套的精品资源点击获取
返回列表