简介:一份以Proteus仿真与Keil C语言为载体的51单片机定时器/计数器实验资源,面向电子、自动化等专业初学者,聚焦定时器工作方式、中断响应、计数模式及外围电路仿真等核心知识点。压缩包共16个文件,整体约184KB,其中pdsprj为Proteus电路工程、pdsbak为自动备份工程,c/uvproj/hex/obj为Keil下的源码与编译产物,pdf为实验说明,并附有列表文件与中间存档,目录结构清晰,可直接打开仿真验证。已有4935人学习下载。内容覆盖从基础到拓展的递进任务:用定时器实现BCD码秒表并由P0口LED显示、按键启停;在P1.7引脚输出宽度500ms、周期1.5s的矩形波,使用虚拟示波器观察;拓展部分让T0按计数器模式统计P3.4外部脉冲,T1每0.1s中断一次读取计数值,通过P1口以二进制形式点亮LED。整套资料保留了完整的C语言注释、编译中间文件与Proteus存档,方便对照理解定时器初值计算、中断服务程序设计和电路连接方法。
1. Proteus仿真下的51定时器实验:验证的不只是延时函数
刚开始学51单片机定时器时,常见做法是在开发板上烧一个延时点灯程序,LED按预期闪了就当作跑通,然后照着现成代码改参数。而把定时器、计数器实验挪到Proteus仿真里做,侧重点会明显不同:你需要面对晶振频率、初值计算、中断响应、外部脉冲源设置这些环节,任何一个对不上,仿真画面就会给出明确的异常现象。这个方向对三类人比较有用:正在做课程设计、需要把实验步骤写到报告里的在校生;想在写驱动前先验证定时逻辑的开发者;以及做方案选型时需要评估51定时器能力的工程师。这篇文章不打算把寄存器手册重新抄一遍,而是围绕基于Proteus仿真做51定时器、计数器应用实验的完整链路,把可复现的操作步骤、参数设置和容易翻车的细节讲清楚。
2. 定时器与计数器的底层逻辑:TMOD、初值与中断的三角关系
在Proteus里做实验,仿真现象只是结果,真正决定现象的是三件事:TMOD寄存器怎么配置、初值怎么算、中断怎么配合。这三者不在一处,却互相牵制。先理清它们的关系,后面的实验才不会靠猜。
2.1 定时和计数的本质区别:内部机器周期计数与外部脉冲触发
先分清“定时器”和“计数器”两种工作方式。语法上这两者的唯一区别是TMOD寄存器中的C/T位。C/T=0时,计数源是系统时钟分频后的机器周期。以12MHz晶振为例,机器周期是12个时钟周期即1μs,定时器每1μs加1,定时1ms就要计1000次。C/T=1时,计数源变成引脚P3.4/T0或P3.5/T1上的外部脉冲,每来一个脉冲,寄存器加1。在Proteus里做实验,这个区别最容易被忽视:定时器是自动跑的,计数器则需要额外放置脉冲源并手动设置脉冲参数。
如果C/T位没有配对应的工作方式,就会出现“明明是定时实验,LED却不按预期闪;明明是计数实验,计数值却不增长”的现象。而且这个结论在Proteus仿真里和实体芯片上是一致的,仿真得到的判断可以直接迁移到后续硬件调试,这也是这个实验方向值得先做仿真的主要原因。
2.2 TMOD寄存器配置:四种工作模式的适用边界
TMOD寄存器的低四位控制定时器0,高四位控制定时器1,每四位内部的结构完全一样。
| TMOD位 | 名称 | 作用 |
|---|---|---|
| bit3 | GATE | 门控位,为1时定时器启动受INT0/INT1引脚电平控制,为0时仅由TR0/TR1控制 |
| bit2 | C/T | 为0是定时器模式,为1是计数器模式 |
| bit1:0 | M1:M0 | 00=模式0,01=模式1,10=模式2,11=模式3 |
模式0是13位计数器,高8位加低5位,虽然存在但用得很少,不推荐在初学实验里用,因为低5位的初值拆分比较麻烦,很容易算错。模式1是16位计数器,初值范围0到65535,在12MHz下定时范围可以覆盖1μs到约65ms,是定时实验里最直观、最好算的模式。模式2是8位自动重装模式,THx装初值,TLx负责计数,溢出时THx自动重新装入TLx,定时误差小,适合做周期性的中断源,但计数范围只有256,定时范围最多256μs,更长的时间要靠软件变量累加。模式3是把定时器0拆成两个独立的8位定时器,TL0和TH0各自工作,但TH0占用了定时器1的TR1和中断标志位,属于特殊用法,常规实验不碰。
2.3 初值计算与THx/TLx拆分:一个公式贯穿所有定时实验
不管模式1还是模式2,初值计算的思路都一致:初值 = 计数上限 - 需要的计数个数。以12MHz晶振、模式1、定时1ms为例,1ms需要1000个机器周期,初值就是65536减1000等于64536,转成十六进制是0xFC18,于是TH0=0xFC、TL0=0x18。TH0和TL0的顺序不能搞混,高字节放TH0,低字节放TL0。这个拆分的错误很常见,因为两个字节都是0xFC和0x18的组合,方向放反也能编译,但定时时间会变成4秒多,现象就完全对不上。
如果是模式2,计数上限是256,定时250μs需要的计数个数是250,初值就是256减250等于6,TH0和TL0都填6。模式2不需要在中断里重装初值,省去两行代码。日常做实验时,我习惯在代码注释里写明“定时xxms、晶振xxMHz、初值xxx”,这样换频率、换时间时改起来不用重新推导。
2.4 中断配置三件事:ET0、EA和中断号
定时器中断是实验里最常用的获取定时信号的方式。写程序要配三处:TMOD选择工作模式;IE寄存器里打开ET0和EA;再写一个以中断号声明的服务函数。IE寄存器中,ET0是定时器0中断允许位,EA是所有中断的总开关。这两个必须同时置1,缺一个中断就进不去。第一次做实验很多人只开了ET0、忘了EA,主循环看着在跑,LED却完全不变,因为中断被总开关挡住了。
定时器0的中断服务函数要写成“interrupt 1”,定时器1对应“interrupt 3”。中断服务函数里要做的第一件事通常是重装初值。模式1溢出后是从0开始重新计数,若不重装,下一次定时的时间就会比预期长很多。模式2是自动重装,无需在中断里再赋值。还有一条很容易忽略:中断函数里不要放延时等待或死循环,否则定时器中断反复被打断,主循环就被挤死了。
3. 在Proteus里搭51定时器最小系统:从晶振、复位到LED闪烁的实验链路
这一章解决“怎么搭起来”的问题。Proteus仿真和实物搭建有个显著差别:元件选择、连线方式、频率设置全在图形界面完成,看似简单,但每一步的错误都会直接体现在仿真结果上。
3.1 元件选择与连线:AT89C51最小系统的Proteus实现
新建Proteus工程后,需要从元件库中搜索并放置以下器件。
| 元件 | 搜索名称 | 作用 |
|---|---|---|
| 单片机 | AT89C51 | 实验主控 |
| 晶振 | CRYSTAL | 12MHz时钟源 |
| 电容 | CAP | 30pF起振电容 |
| 电解电容 | CAP-ELEC | 10μF复位电容 |
| 电阻 | RES | 10kΩ复位电阻、330Ω限流电阻 |
| LED | LED-RED | 输出显示 |
| 电源端子 | POWER | 接VCC和GND |
AT89C51在Proteus的Microprocessor ICs类别下,搜索框直接输入名称即可。放置后双击元件可以修改标号和参数。接线按固定套路:第40脚VCC接+5V,第20脚VSS接地;第31脚EA接高电平,否则单片机会从外部ROM取指,程序执行异常;第18、19脚接晶振并各对地接一个30pF电容;第9脚RST接复位电路,10μF电容正极接VCC、负极接RST,同时10kΩ电阻从RST接地。最后在P1.0引脚接一个330Ω电阻再串LED到地,最小系统完成。
Proteus里有一个很特殊的点:CRYSTAL元件即使不放置,仿真也能正常运行,真正决定单片机执行频率的是AT89C51属性里的Clock Frequency。但工程上我仍然会把晶振画上,因为这是电路图完整性的一部分,也方便日后对照实物接线。
3.2 用Keil生成HEX并加载进Proteus:完整流程
写代码用Keil C51,编译后会生成HEX文件,Proteus需要这个文件来执行程序。常见流程是:在Keil里新建工程,选择AT89C51芯片,把代码写进main.c,然后在Options for Target的Output选项卡里勾选Create HEX File,编译通过后再回Proteus双击AT89C51,在Program File一栏选择生成的hex文件,设置好Clock Frequency,运行仿真。
这里有一个容易被忽略的操作:Keil工程里如果芯片型号选错,有些寄存器定义会有差异,编译报错不直观。我一般直接选AT89C51或AT89C52这一类兼容型号,这能避免头文件里寄存器地址对不上的问题。另外,Proteus编辑仿真开始后单片机属性里的Program File路径是固定的,若Keil重新编译但文件名变了,需要重新选择,否则运行的是旧程序。
3.3 第一个定时实验:LED每0.5秒翻转一次的完整代码
实验要求:LED亮0.5秒、灭0.5秒,周期1秒。选择定时器0模式1,定时50ms,配合软件变量累加10次实现。采用50ms作为中断周期,可以避免中断过于频繁,也给主循环留出余量。
#include <reg51.h> sbit LED = P1^0; unsigned char t_count = 0; void Timer0_Init(void) { TMOD &= 0xF0; // 只清零T0所在的低四位,不动T1 TMOD |= 0x01; // T0工作于16位模式1 TH0 = 0x3C; // 定时50ms,12MHz晶振,初值=65536-50000=15536 TL0 = 0xB0; // 15536的十六进制是0x3CB0 ET0 = 1; // 允许T0中断 EA = 1; // 开放总中断 TR0 = 1; // 启动T0计数 } void main(void) { Timer0_Init(); while (1) { // 主循环留空,定时翻转放在中断中处理 } } void Timer0_ISR(void) interrupt 1 { TH0 = 0x3C; TL0 = 0xB0; t_count++; if (t_count >= 10) { t_count = 0; LED = ~LED; } }代码逻辑很直接:中断每50ms进一次,t_count累加到10就是500ms,然后翻转LED。注意TMOD操作的写法,先“与”一个0xF0清零低四位,再“或”一个0x01写入模式值,这样不会破坏高四位里T1的配置,比直接赋值TMOD=0x01更稳妥。初值0x3CB0来自65536减50000再转十六进制,换晶振频率时必须重新计算,不能直接搬用。
3.4 定时初值速查表:12MHz晶振下常见定时需求
| 定时时间 | 所需机器周期数 | 模式1初值 | 实际写入 |
|---|---|---|---|
| 1ms | 1000 | 64536 = 0xFC18 | TH0=0xFC, TL0=0x18 |
| 10ms | 10000 | 55536 = 0xD8F0 | TH0=0xD8, TL0=0xF0 |
| 50ms | 50000 | 15536 = 0x3CB0 | TH0=0x3C, TL0=0xB0 |
模式1的16位计数器最大定时约65.5ms,更长的时间一律用多次中断累加实现,这是嵌入式定时实验的基本功。定时1s就是50ms累加20次,或者10ms累加100次。选择哪种基数,取决于主循环里其他任务对实时性有多敏感。
3.5 用Proteus示波器观察LED波形的操作步骤
Proteus右侧仪器工具栏里有虚拟示波器,拖到原理图后把A通道接在LED阳极,运行仿真,就能看到LED引脚上的电压波形呈方波状,周期1秒。如果波形不对,先检查示波器时间刻度:时间基设得太小,屏幕上只有一条直线;设得太大,方波被压成一条细带。我一般先把时间基调到100ms/格,看到两个周期的完整波型后,再逐步细化。
对于定时实验,观察LED闪动只能确认“大概在工作”,用示波器量波形周期才能确认定时精度。这个习惯在后面做PWM或频率测量时尤其重要。
4. 计数器模式实战:用Proteus的时钟激励源模拟外部脉冲
计数器和定时器的代码结构几乎一样,差别只在TMOD的C/T位和外部脉冲源。这一章从电路接到代码,完整走一遍“外部每来6个脉冲,LED翻转一次”的实验,同时说明如何调整计数初值适配不同应用场景。
4.1 计数器模式的引脚连接和脉冲源设置
计数器实验在上一章电路基础上,只需在P3.4引脚接一个脉冲发生器。在Proteus的工具栏中点击Generator图标,也就是绿色圆圈那个,选择DCLOCK放置到原理图,双击打开属性设置。关键参数有三个:起始电压设为0V,幅度设为5V,频率按实际需要设,比如10Hz。这样P3.4收到的是频率10Hz、幅值0到5V的TTL矩形波,能被T0引脚正确识别为外部脉冲。
这里的常见错误是DCLOCK默认幅度不是标准TTL电平,直接接上去,引脚的逻辑阈值无法稳定判断高低电平,计数结果要么不增长、要么乱跳。每次放置脉冲源后,我都会双击确认幅度值再开始仿真。另外,Proteus里还有一种PULSE源,参数更灵活,可以设上升沿、下降沿时间,但DCLOCK简单够用,计数器实验用DCLOCK就足够了。
4.2 每6个脉冲翻转一次LED:计数器模式完整代码
计数器模式用模式1,C/T位设为1。每计满6个脉冲产生一次溢出中断,翻转LED状态。计数初值等于65536减6等于65530,即0xFFFA。
#include <reg51.h> sbit LED = P1^0; void Counter0_Init(void) { TMOD &= 0xF0; TMOD |= 0x05; // T0:模式1、C/T=1、GATE=0 TH0 = 0xFF; // 初值65530,即0xFFFA TL0 = 0xFA; ET0 = 1; EA = 1; TR0 = 1; } void main(void) { Counter0_Init(); while (1) { // 主循环可放数码管刷新等任务 } } void Counter0_ISR(void) interrupt 1 { TH0 = 0xFF; TL0 = 0xFA; LED = ~LED; }TMOD |= 0x05这一句要拆开理解:低四位二进制是0101,bit2的C/T=1表示计数器模式,bit1:0的01表示模式1,bit3的GATE=0表示启动不受外部引脚控制。如果把0x05错写成0x01,程序就变成了定时器模式,LED会按系统时钟闪烁,而不是按外部脉冲闪烁,现象完全错误,这是计数器实验最容易翻车的位置。
这个实验在仿真中适合观察一个明确的比例关系:脉冲源频率设为10Hz时,LED翻转周期应该是0.6秒,因为每6个外部脉冲才翻转一次。在示波器上可以看到LED输出频率明显低于脉冲源频率,这种倍数关系写进实验报告比单纯描述LED闪烁更有说服力。
4.3 如果把需求改成“每100个脉冲计一次数”:初值换算方法
实际场景中,计数需求五花八门,比如流水线上每50个产品走一箱,霍尔传感器每转一圈输出一个脉冲、一圈对应一个固定距离。这种情况下只需要换初始值。
以50个脉冲计满一次为例,初值等于65536减50等于65486,十六进制是0xFFCE,于是TH0=0xFF、TL0=0xCE。中断里执行的就不再是单纯翻转LED,而是可以把计满次数累加到一个变量中,再交给主循环做显示或控制。
unsigned int pulse_count = 0; void Counter0_ISR(void) interrupt 1 { TH0 = 0xFF; TL0 = 0xCE; pulse_count++; // pulse_count每加1,代表外部来了50个脉冲 }这样设计的好处是逻辑分层:中断只负责计数和置标志,显示、阈值判断、报警等耗时操作全部放在主循环。初值换算的思路是:先确定多少脉冲计满一次,再用65536减去这个数,转十六进制拆成高、低字节。无论模式1还是模式2,这个公式都成立。
4.4 三种验证计数结果的手段
验证计数器实验是否准确,Proteus里至少有三条路径。第一种,把示波器接到P3.4引脚,观察脉冲源输出波形,确认频率、幅度符合预期;第二种,把示波器同时接到P3.4和LED引脚,对比两个波形的频率关系,如果LED翻转周期是脉冲周期的6倍,说明计数逻辑正确;第三种,在Keil的调试窗口里查看TH0、TL0的在运行时的值,确认初值被正确装载。
我个人的习惯是先把脉冲源频率降到1Hz以下,用人眼直接观察LED翻转间隔,确认逻辑无误后,再把频率升上去用示波器测周期。这种方式可以把现象和逻辑逐步对应起来,出问题时也更容易定位。
5. 避坑须知:Proteus仿真定时器/计数器应用的5个常见问题
仿真环境比硬件环境少了很多不确定性,但也有它特有的问题。以下五个坑,是我在重复这类实验时最常遇到的翻车现场。
5.1 程序烧进去LED不按预期时间闪:晶振频率和初值来源不一致
现象:从网上下了一个定时1ms的示例代码,在Proteus里运行时LED闪烁时间明显偏离预期,差得离谱。
原因:示例代码里的初值是按11.0592MHz晶振计算的,而Proteus里单片机的Clock Frequency属性默认是12MHz。时钟基准不同,机器周期就不同,初值自然对不上。
解决:在Proteus里双击AT89C51,把Clock Frequency改成和代码计算时用的频率一致,然后重新仿真。如果需求强制用12MHz,就按12MHz重算初值。最可靠的预防手段是代码注释里写明初值对应的晶振频率,换环境时一眼就能定位。
5.2 定时器启动了但中断没进来:EA总开关或中断号写错
现象:主循环在运行,TR0也置1了,但LED从不变化。用Proteus调试看寄存器,发现TF0溢出标志都置位了,程序却没有跳进中断服务函数。
原因:中断允许位没配全。最常见的是只写了ET0=1,忘了写EA=1;另一种是把定时器0的中断号误写成interrupt 3。
解决:检查IE寄存器配置,确保ET0和EA都置1。中断服务函数用interrupt 1对应定时器0,定时器1用interrupt 3。可以在Proteus里配合Keil联调,单步观察TF0置位后是否有跳转,这样能直接定位是中断号错误还是总中断未开。
5.3 计数器模式计数不增或乱跳:脉冲源电压幅度和方向不适合TTL输入
现象:P3.4接了脉冲发生器,但计数器不动,或者LED翻转节奏和脉冲频率对不上。
原因:脉冲源的输出电压幅度不是标准TTL电平,比如默认7V或者负脉冲,T0引脚无法识别有效的上升沿。
解决:双击脉冲源,把起始电压设为0V,脉冲幅度设为5V,脉宽按占空比设置,确保送到P3.4的是0到5V的数字脉冲。然后用示波器接在P3.4上确认波形,再看计数寄存器是否随脉冲沿变化,两件事要分开排查。
5.4 Keil联调时单步执行定时器跑飞:两种工具的时间步进机制不同
现象:使用Keil调试器的单步功能逐行执行代码时,Proteus里的定时时间变得很奇怪,LED状态和预期完全对不上。
原因:Keil单步执行时,每条指令的实际耗时和连续运行不同,Proteus的定时器模块依赖仿真时间连续推进,单步执行期间定时器仍然在跑,导致寄存器状态和代码执行不同步。
解决:不要在单步模式下判断定时结果。验证定时逻辑使用全速运行配合断点,断点只用来观察当前状态,不作为时间测量依据。时间测量一切以Proteus里示波器捕获到的波形为准。
5.5 加入数码管扫描后定时器不准了:中断服务函数占比过高
现象:原本LED定时翻转实验正常,加了一段数码管动态显示代码后,显示出现闪烁,LED的翻转节奏也乱了。
原因:动态显示需要在主循环里反复扫描数码管,如果示例代码图省事把扫描直接放进中断,中断服务时间被拉长,主循环得不到足够执行时间,显示刷新不及时,出现闪烁。
解决:中断服务函数里只保留标志位置位、变量累加这类轻量操作,显示扫描、按键处理全部放回主循环轮询。这样定时器中断周期稳定,显示也不受影响。能在这个阶段暴露架构设计问题,恰恰是做仿真的价值所在。
6. 进阶验证:用Proteus虚拟逻辑分析仪核对定时器输出波形
做定时器实验时,我最后一步通常不是直接复制代码到开发板,而是在Proteus里打开虚拟逻辑分析仪,把通道接到LED引脚,让它跑几秒钟,再用逻辑分析仪自带的时间标尺测量两个上升沿之间的周期。这个操作看似多加了一步,实际能快速确认中断重装和初值计算是否准确。
具体做法是:在Proteus右侧仪器面板选择Logic Analyser,拖入原理图,把D0通道接到P1.0引脚,设置采样频率为被测信号频率的10倍以上。比如被测信号是1Hz方波,采样频率至少设10kHz。点击运行,等波形出现两三个完整周期后停止。用鼠标将测量标尺拖到第一个上升沿,再拖到第二个上升沿,读出时间差。
以定时50ms、累加10次翻转一次的实验为例,理论周期是1秒,实测值应当接近1000ms。实测如果偏大,说明中断重装初值有误,或者重装指令的执行时间被累计进来了;偏小则大概率是单片机属性里的时钟频率和初值计算用的频率不一致。这套用逻辑分析仪做时序验证的方法,和硬件上用示波器测波形是同一套思维,也是我习惯先仿真后上板的原因之一。
还有一个值得长期保持的习惯:初值计算和Proteus里的Clock Frequency属性必须坚持同一个时间基准。之前我在做模拟项目X时,Keil代码按12MHz算好了初值,Proteus里单片机属性却是默认的11.0592MHz,LED闪烁节奏比预期慢了约8%,查了半天才发现是这个属性没有改。后来每次新建工程,我会先双击单片机改好频率,再开始写代码。这个习惯保持住,后续做再多的定时器、计数器实验都很少为这类低级问题返工。
希望这篇关于Proteus仿真51定时器、计数器应用实验的实战笔记,能帮你把时间花在真正值得调试的时序逻辑上,少走几步弯路。
本文还有配套的精品资源,点击获取