
简介面向8051单片机入门者与嵌入式系统初学者这份Proteus仿真实例以PWM脉冲宽度调制控制LED亮度为主线展示了在虚拟环境中利用定时器产生可调占空比方波信号的方法无需实体硬件即可观察模拟输出效果特别适合课堂教学、实验预习与自学排错。资源包共13个文件压缩后仅2.65MB其中DSN/DBK为Proteus电路工程文件C源码与UV2/OPT组成Keil开发工程HEX可独立加载仿真另有LST、M51等编译中间文件便于理解代码到机器码的映射关系。目前已有651人学习下载内容覆盖定时器方式选择、初值计算、中断标志处理等要点。实际使用时可反复修改源程序中的定时初值或比较寄存器赋值直观看到LED亮度随占空比改变而变化并借助Proteus虚拟仪器检验波形同时锻炼调试与排错能力为后续学习STM32等基于ARM内核的32位MCU打下基础。PWM技术广泛应用于LED调光、电机调速等领域这一实例正是理解嵌入式模拟量控制的关键入口。1. 先动手验证 8051 的 PWM 调光再考虑打板调一盏灯的亮度手边没有示波器、没有可调电源甚至板子还没画——这正是 Proteus 仿真在 8051 学习路径里不可替代的位置。PWM 控制 LED 亮度的仿真程序拆开看就是三个技术点用定时器中断在没有任何硬件 PWM 外设的 8051 上产生占空比可变的方波把方波接到 LED 电路上靠人眼对平均亮度的积分得到调光效果再用虚拟示波器把占空比量化确认。这套组合不是为了省一个电阻而是为了在画 PCB、焊板子之前先验证周期、占空比和中断开销之间的平衡。新手能照着把原理图搭出来熟手也能从中看到软件 PWM 的频率、分辨率取舍边界到底在哪。后面的内容就按这个思路展开先算参数再搭工程最后用示波器和抖动技巧把仿真做实。2. PWM 占空比与定时器参数8051 软 PWM 的数学底子2.1 为什么调 LED 亮度用 PWM而不是调限流电阻给 LED 串联一个可调电阻也能改变亮度但电流变化会同时拖动亮度特性和发热状态而且在低亮度区间人眼对电流的微小变化极不敏感旋钮拧半天才看见明显变化。PWM 的做法相反让 LED 始终工作在接近额定电流的开关状态下通过快速切换通断改变单位时间内的平均电流。人眼对 50Hz 以上的亮度波动基本无感视觉系统在时间上做了积分于是感知到的亮度就近似等于占空比 D 乘以满亮度。这个结论决定了一切后续设计仿真的核心对象不是 LED 本身而是 PWM 信号的频率和占空比。频率太低会明显闪烁频率太高则受限于定时器分度因此动手写代码之前必须先算清 8051 在这种时钟下的能力边界。和 555 搭出来的硬件 PWM 电路相比8051 软 PWM 的优点是占空比和频率改一行宏定义就能变不用换电阻电容缺点是 CPU 被中断反复打断并且定时器计数粒度直接限制分辨率这两点后面都会定量看到。2.2 8051 没有硬件 PWM轮询与中断两条路怎么选标准 8051 的 Timer0、Timer1 只有计数溢出能力没有输出比较寄存器也没有独立的 PWM 输出脚。输出引脚上高还是低完全由软件在合适的时机翻转。这和 STC 增强型 51 里带 PCA、或者现代 MCU 自带 PWM 外设完全是两回事选型时不要混为一谈。常见做法是阻塞轮询和中断加软件计数两条路线选哪条取决于对 CPU 占用率和分辨率的容忍度。实现路线定时器配置占空比分辨率CPU 占用适合场景阻塞轮询翻转Timer0/1 模式 116 位取决于延时计数值最高可到 16 位接近 100%单任务验证原理中断 软件计数Timer0 模式 2 或模式 18 位 0~255配软件计数可到任意档数每次中断约十几到二十微秒亮度渐变、多路扩展双定时器比较Timer0/Timer1 同时用理论可到 16 位较高需要精确控制占空比时间点轮询法写起来最直观也很好在 Proteus 里复现但它的致命问题是延时期间什么都干不了。中断法把翻转动作放进定时器中断服务函数主循环可以同时处理按键、显示或者其他任务这是实际工程里最常用的方案。需要记住的是8051 的每个机器周期在 12MHz 晶振、12T 模式下正好是 1μs这个单位是后面所有计算的基准。2.3 先把周期和占空比算清楚12MHz 下的定时初值以 12MHz 晶振为例12T 模式下 1 个机器周期就是 1μs。想让定时器每 100μs 溢出一次就需要计数器从初值数到 65536 正好花掉 100 个计数于是初值等于 65536 减 100。高位放 TH0低位放 TL0这是 8051 定时器最基础的装载方式也是后面所有 PWM 参数的来源。#include reg52.h sbit LED P1^0; // PWM 输出脚 void timer0_delay(unsigned int us) // 阻塞式微秒延时 { TMOD 0xF0; // 只改低 4 位不动 Timer1 TMOD | 0x01; // Timer0 模式 116 位 TH0 (65536 - us) 8; // 高 8 位 TL0 (65536 - us) 0xFF; // 低 8 位 TR0 1; // 启动定时器 while (!TF0); // 等待溢出 TR0 0; TF0 0; } void main(void) { while (1) { LED 1; timer0_delay(300); // 高电平 300μs LED 0; timer0_delay(700); // 低电平 700μs } }这段代码产生高电平 300μs、低电平 700μs 的信号周期 1ms频率 1kHz占空比 30%。逻辑说明每次翻转前后都调用一次延时函数通过控制高电平持续时间占总周期的比例来改变平均亮度。参数说明us的取值范围是 1 到 65535超过会导致 16 位回绕得到错误时序想要调整亮度只需改动高电平延时的时间长度周期保持不变即可。这个轮询版本在 Proteus 里能跑出漂亮的波形但 CPU 被占满只能作为原理验证。把延时参数换成变量再配合按键加减就是最原始的调光器雏形。3. 在 Proteus 里搭出可复现的 PWM 调光工程3.1 元件清单与 Proteus 元件库关键字对照先泼一盆冷水Proteus 这个名字在很多入门资料里被写成 Protues元件库和搜索引擎只认 Proteus 拼法按 Protues 去搜元件往往一无所获。标题里的 zip 包通常解开就是 .pdsprj 工程文件、编译好的 .hex 和 C 源文件双击工程文件就能跑如果是自己从零搭下面这张元件对照表是最常被问到的那张 Proteus 元件对照表的精简版。元件Proteus 元件库关键字关键参数作用单片机AT89C5112MHz运行 PWM 程序晶振CRYSTAL12MHz提供时钟电容CAP33pF2 个晶振负载电容电解电容CAP-ELEC10μF复位电路电阻RES330Ω、8.2kΩ限流、复位发光二极管LED-RED2V 正向压降亮度观察对象示波器OSCILLOSCOPE虚拟仪器测量占空比元件的摆放逻辑是这样的AT89C51 在 Microprocessor ICs 分类里直接搜关键字最快LED-RED 的仿真模型默认正向压降 2V配 330Ω 限流电阻5V 供电下电流约 9mA这个电流在 Proteus 里的亮度效果接近实物。电阻值不要取得太小仿真模型对过流同样会变亮甚至报错20mA 以内是安全区间。按键和示波器属于选配后者在第四章才用到但工程里最好一开始就预留出测试点避免后面反复改图。3.2 画原理图的五个步骤和三个必检点用 Proteus 8 点亮 LED 的工程搭建路径比较固定按照下面五个步骤做基本不会卡壳。第一步新建工程时选择 Schematic Capture 模板只画原理图不要勾选 PCB 布板选项。第二步从元件库取出 AT89C51、CRYSTAL、CAP、RES、LED-RED摆到画布上。注意 8051 的 VCC 和 GND 是隐藏引脚不需要手工添加只要在电源端子上放好 5V 和地线符号软件会自动连接。第三步接时钟和复位网络晶振两端接 XTAL1 和 XTAL2各带一个 33pF 电容到地复位脚 RST 接 10μF 电解电容到 VCC再经 8.2kΩ 电阻到地这是教科书式的上电复位电路。第四步连输出支路P1.0 引脚接 330Ω 电阻电阻另一端接 LED 阳极LED 阴极直接接地。第五步双击 AT89C51 元件把 Clock Frequency 改成 12MHz在 Program File 里加载编译好的 .hex点左下角运行按钮。三个必检点比步骤本身更容易让人翻车。一是 EA 引脚必须接 VCC否则单片机从外部 ROM 取指程序跑不起来二是晶振频率要双击元件确认是 12MHzProteus 默认值经常是 1MHz频率差 12 倍波形全乱三是 LED 方向阳极必须朝向限流电阻接反了直接不亮这是新手最常踩的坑而且仿真里不报错只能靠眼睛看。3.3 中断版渐变程序编译出 HEX 再加载到 AT89C51编译环境的选择上MDK5 8051 环境搭建比较省事安装 uVision5 后在 Pack Installer 里装好 C51 支持即可新建 8051 工程IAR 6.3 8051 开发环境也能用但 8051 生态里 Keil 的兼容性更省心。工程建好后在 Options for Target 的 Output 选项卡里勾选 Create HEX File编译通过就能得到加载用的 .hex。#include reg52.h sbit LED P1^0; #define TICK_US 100 // 每次定时中断 100μs #define PWM_LEVEL 100 // 占空比档位数 0~99 unsigned char duty 0; // 当前占空比 bit rising 1; // 方向1 变亮 0 变暗 void delay_ms(unsigned int ms) // 粗略毫秒延时 { unsigned int i, j; for (i 0; i ms; i) for (j 0; j 120; j); } void timer0_isr(void) interrupt 1 { static unsigned char cnt 0; TH0 (65536 - TICK_US) 8; // 重新装载初值 TL0 (65536 - TICK_US) 0xFF; cnt; if (cnt duty) LED 1; // 前 duty 个计数输出高 else LED 0; // 其余输出低 if (cnt PWM_LEVEL) cnt 0; } void main(void) { TMOD 0x01; // Timer0 模式 1 TH0 (65536 - TICK_US) 8; TL0 (65536 - TICK_US) 0xFF; ET0 1; // 使能定时器 0 中断 EA 1; // 开总中断 TR0 1; // 启动定时器 while (1) { delay_ms(30); if (rising) { if (duty PWM_LEVEL) rising 0; // 到顶转下降 } else { if (duty-- 0) rising 1; // 到底转上升 } } }逻辑说明定时器每 100μs 产生一次中断中断里用静态变量cnt从 0 数到 99cnt小于duty时输出高电平否则输出低电平于是每个周期 10ms、频率 100Hz 的 PWM 信号就成型了。duty从 0 缓慢加到 99 再减回 0LED 亮度随之完成一次从灭到亮再回灭的渐变单程约 3 秒。参数说明TICK_US 决定定时中断密度数值越小频率越高但中断开销占比越大PWM_LEVEL 决定占空比档位数100 档对肉眼已经足够平滑。这里选 100Hz 是刻意留出 CPU 余量实物上如果嫌闪可以改成 PWM_LEVEL 为 50、TICK_US 为 50周期变成 2.5ms、频率 400Hz手机摄像头也拍不出频闪。4. 用示波器核对占空比8051 模拟 16 位 PWM 的边界4.1 虚拟示波器接线与时间档设置Proteus 里测量 PWM 信号不需要额外硬件从虚拟仪器面板拖出 OSCILLOSCOPE把通道 A 的正端接到 P1.0负端接地即可。运行仿真后示波器窗口会实时显示方波。先把 Time/Div 调到 1ms 档100Hz 信号的周期是 10ms一屏能放下约 10 个波形通道增益保持默认的 1V/div0 到 5V 的方波刚好占五格。触发方式选上升沿波形会稳定停在屏幕上方便后续量宽度。暂停仿真是读数的关键操作。运行状态下波形一直滚动直接用肉眼读刻度误差很大点左下角暂停按钮冻结波形再用游标功能测量高电平起点和低电平终点之间的时间差这个读数才是可信的占空比数据。如果波形抖动得厉害先检查触发边沿是否选对再确认示波器探头接地是否真的接到了 GND 网络。4.2 从波形读数反推真实周期ISR 开销藏在哪把程序里duty固定为 50理论占空比应该是 50%。用游标量一下高电平宽度和周期大部分情况下会发现周期比 10ms 偏大大约在 11 到 12ms频率降到了 80 多赫兹。这不是 Proteus 的误差而是中断服务函数本身的执行时间被算进了周期。每次进入定时器中断CPU 要保护现场、重装 TH0 和 TL0、执行比较和翻转、再恢复现场Keil C51 编译后这段代码通常要花十五到二十几个机器周期也就是十五到二十几微秒。TICK_US 设的是 100μs但实际两次中断的间隔是 100μs 加这段开销累加 100 次周期自然被拉长。这时候占空比的百分比基本不变因为高电平和低电平被同等拉长但频率标称值就不可信了。这个现象在实物上同样存在所以一个实用的经验是一切以示波器实测为准代码注释里的理论频率只能当参考。想验证这一点可以把 TICK_US 改成 1000实测周期会明显小于理论值 100ms因为单次中断开销占比变小数学上很容易解释。4.3 模拟 16 位 PWM把低 4 位拆成 16 帧的概率补偿热词里常有人问怎么用 8051 模拟出 16 位 PWM先给结论纯软件在 12MHz 下做不到真正的 65536 级。原因很简单65536 个 1μs 计数就是 65.5ms 周期对应 15Hz肉眼能看见灯在闪而且每次中断有二十微秒左右开销最小有效分度根本到不了 1μs。常见做法是把 16 位拆成高 8 位和低 8 位高 8 位直接作为基础占空比低 4 位用时间抖动来补偿——即在 16 个 PWM 周期里挑其中若干帧把占空比临时加 1 级平均值就带上了小数部分。低 4 位目标补偿帧数 N16 帧模式示意平均占空比增量00000全 0duty 0.00010041 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0duty 0.25100081 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0duty 0.501100121 1 1 0 1 1 1 0 1 1 1 0 1 1 1 0duty 0.75#define PWM_LEVEL 100 static unsigned char duty; // 8 位基础占空比 static unsigned char comp; // 本周期实际生效的占空比 static unsigned char cyc; // 帧计数 0~15 static const unsigned char dither[16] { 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 // 3 帧补 1平均多 3/16 级 }; void timer0_isr(void) interrupt 1 { static unsigned char cnt 0; if (cnt 0) { // 新 PWM 周期开始 comp duty; // 从基础值出发 if (dither[cyc] comp PWM_LEVEL) comp; // 本帧补偿 1 级 cyc (cyc 1) 0x0F; // 16 帧循环 } TH0 (65536 - TICK_US) 8; TL0 (65536 - TICK_US) 0xFF; cnt; if (cnt comp) LED 1; else LED 0; if (cnt PWM_LEVEL) cnt 0; }这段代码的关键在comp被声明为静态变量只在cnt归零、也就是新 PWM 周期开始的那一刻更新一次整个周期内保持稳定否则每帧都重新计算就起不到整帧补偿的效果。dither数组里 1 的个数决定补偿强度3 个 1 表示 16 帧里有 3 帧占空比多 1 级平均下来相当于 duty 加 0.1875。把 16 帧换成 256 帧理论上能模拟到 16 位但 100Hz 的 PWM 周期乘 256 帧亮度刷新要 2.56 秒肉眼看到的会是明显阶梯没有实用价值。所以工程上最多做到 12 位等效这个边界是硬件时序决定的不是程序写得不够好。5. 渐变不跳变的三个细节Gamma 查表、最小占空比与上电状态5.1 用 Gamma 查表替代线性递进把占空比从 0 线性加到 100Proteus 里会看到亮度在前半段几乎不动、后半段突然跳变。原因是 LED 的视觉感知是幂函数关系感知亮度约等于占空比的 2.2 次方。常见做法是做一个 Gamma 查表把百分比映射到人眼感知线性的占空比序列用 Python 先把表生成出来# gamma 2.2 查表101 项 import math g [round(255 * (i / 100) ** 2.2) for i in range(101)] print(, .join(map(str, g)))把输出的数组贴进 C 代码声明为code unsigned char gamma_table[101] {...};主循环里直接duty gamma_table[percent];。以 percent 为 50 为例查表值是 55也就是说想看到 50% 的视觉亮度PWM 占空比只需要 55% 左右这比直接线性映射科学得多。5.2 最小占空比与上电状态低占空比区间LED 亮度的变化对人眼极不敏感3% 到 5% 的占空比几乎看不出和熄灭的区别反而会让渐变在最低端拖沓。建议设一个死区percent 小于 3 时直接输出 0这样渐变两端的节奏更干净。另一个容易忽视的是上电状态8051 复位后 P1 口默认全高LED 在程序跑起来之前会全亮闪一下。main 函数第一件事就是LED 0; duty 0;再初始化定时器和中断保证上电瞬间 LED 是灭的。5.3 验证这三个细节的步骤把 duty 固定在 50用示波器确认高电平宽度是周期一半再把线性递增改成 gamma 查表观察低亮度区是否出现明显的逐档变化。最后一个小技巧双击 Proteus 里的 LED 元件把模型属性里的 Forward Voltage 从默认 2V 改成 3.3V亮度曲线会明显变陡这个差异与实物 LED 的离散性对应调过之后仿真结果和真实电路的偏差会更小。仿真调到这一步PWM 调光的逻辑、时序和视觉感受就全部对齐了。本文还有配套的精品资源点击获取