简介:这是一份面向单片机初学者的定时器/计数器实验报告,围绕51单片机内部定时器与计数器T0、T1的四种工作方式、中断处理及计数编程展开。报告以G6W仿真器、MCS-51实验板为平台,完整记录了计数器模式方式一的硬件接线、TMOD寄存器设置、TH与TL初值装载、中断服务程序编写,以及十六进制转8421码显示的核心思路,并附有汇编源码与实验现象对比。实验报告包含实验目的、器材、内容与要求、框图、源码和思考小结等完整环节;通过信号发生器输入一千赫兹与三百赫兹脉冲,验证LED显示计数为五十和十五,同时扩展了脉搏计数功能,展示了定时器与计数器协同工作的典型应用。思考小结部分梳理了DA指令误用、显示顺序调整等常见排错经验,适合课程设计或实验复习参考;资源为单个PDF文档,容量496KB,内容经重新排版,清晰易读,目前已有3859人学习下载,对于想快速掌握单片机定时器计数器编程与中断机制的学习者具有实际帮助。 早些年还在学校那会儿,每次做到定时器计数器实验,总感觉像是在和一堆寄存器“斗智斗勇”。尤其是刚从51单片机切到STM32,再从STM32切到Verilog仿真,你会发现“定时”和“计数”这两个概念,几乎贯穿了单片机学习的所有阶段。今天不聊虚的,就以一份典型的《单片机定时器计数器实验报告》为核心,把我自己动手做这块项目时积累的调试心得、原理验证和踩坑经历,全都摊开来聊聊。无论你是刚接触51单片机还在琢磨TMOD寄存器,还是已经开始用STM32的HAL库配置TIM定时器,这篇文章应该都能给你一些能直接上手的参考。
1. 实验核心:定时器与计数器到底在解决什么问题
很多朋友做实验的时候,代码能跑、灯能闪,但问起“定时器究竟是靠什么来定时的”,就有点含糊了。说到底,定时器计数器就是一个对脉冲信号进行计数的硬件电路,只不过它数的“脉冲”有两个来源:一个是单片机内部的机器周期,另一个是外部引脚输入的信号。
当它数内部机器周期时,因为机器周期的时间是固定的(比如晶振12MHz时,一个机器周期是1微秒),所以数了多少个脉冲,就等于过了多少时间——这就是“定时”。当它数外部引脚(比如51的T0、T1引脚)输入的脉冲时,脉冲的间隔不一定固定,它统计的是“来了多少个信号”——这就是“计数”。这个区分是整个实验的基石,也是最容易被忽略的地方。
我在指导学弟学妹做实验时发现一个很有代表性的误区:有人把定时器和计数器当成两个完全独立的东西,觉得“我这节课做的是定时器实验,那计数功能就不管了”。其实它们共享同一套硬件资源,区别仅在于时钟源的选择。你通过配置寄存器决定让谁去驱动这个计数器,它就变成了你需要的那个工具。
对51单片机来说,核心是TMOD、TCON寄存器;对STM32来说,核心是PSC(预分频器)、ARR(自动重装载值)和CNT(计数器)这三个寄存器的配合。理解了这一点,你就等于拿到了所有定时器实验的万能钥匙。
2. 从寄存器到波形:一次完整实验的设计与实现
2.1 实验平台的硬件基础
我最早是在普中科技的51开发板上完成这个实验的,板载12MHz晶振,单片机型号是STC89C52RC。之所以选这个平台,是因为它的定时器资源非常典型——两个16位定时器T0和T1,四种工作模式,尤其模式1(16位定时器)是使用率最高的。
这里插一句,很多教材里会说“12MHz晶振一个机器周期是1微秒”,这个说法在STC89C52上成立,是因为这个芯片的时钟是12T模式(12个时钟周期等于1个机器周期)。但如果你用的是STC15系列或者某些1T增强型单片机,同样的12MHz晶振,一个机器周期直接变成1/12微秒,定时器初值的计算就完全不一样了。这一点后面章节会详细拆解,做实验前务必先确认自己芯片的工作模式。
实验需要的硬件非常基础:一块开发板、一个USB下载器、一个LED模块或数码管模块,外加示波器或逻辑分析仪(没有的话用LED闪烁配合秒表也能凑合验证)。软件方面用Keil uVision编写C语言程序,STC-ISP软件下载烧录。
2.2 工作模式的选择与初值计算逻辑
51定时器有四种工作模式,但实验中真正好用的就两个:
- 模式0:13位计数器,TH高8位加TL低5位,几乎没人用,兼容老古董的遗物。
- 模式1:16位计数器,TH和TL各8位,范围0到65535,最常用。
- 模式2:8位自动重装载,适合需要精确定时中断周期的场景,比如串口波特率发生器。
- 模式3:定时器0被拆成两个独立的8位计数器,应用场景极窄。
我做这个实验时选了模式1。核心思路是这样的:要求定时50毫秒,机器周期1微秒,那么总共需要计数50*1000=50000次。16位计数器满值是65536,所以初值就是65536-50000=15536。把15536转换成十六进制是0x3CB0,于是TH0=0x3C,TL0=0xB0。
这段计算看起来简单,但有个细节非常关键:定时器溢出后,寄存器归零需要软件重新赋初值。如果你在中断服务函数里忘了重新赋值,那么下一次定时就不是50毫秒,而是从0数到65536的65.536毫秒,频率直接跑偏。我记得某次课堂上有个同学做完数码管秒表实验,第一秒还挺准,越走越快,找了一节课的原因才发现是主函数里只赋了一次初值。
2.3 代码骨架与中断处理
核心代码结构大致如下:
#include <reg52.h> sbit led = P1^0; unsigned int timer_count = 0; void Timer0_Init(void) { TMOD &= 0xF0; // 清空T0控制位,保留T1配置 TMOD |= 0x01; // 设置T0为模式1,16位定时器 TH0 = 0x3C; // 50ms定时初值高8位 TL0 = 0xB0; // 50ms定时初值低8位 ET0 = 1; // 使能T0中断 EA = 1; // 开总中断 TR0 = 1; // 启动定时器0 } void Timer0_ISR(void) interrupt 1 { TH0 = 0x3C; // 重新装载初值 TL0 = 0xB0; timer_count++; if(timer_count == 20) // 累计20次,即1000ms { timer_count = 0; led = !led; // LED状态翻转 } } void main() { Timer0_Init(); while(1) { // 主循环可以处理其他任务 } }这里有个容易被新手忽略的细节:TMOD &= 0xF0;这一步是有意为之的。TMOD寄存器高四位控制T1,低四位控制T0,如果你在别的地方已经配置了T1作为波特率发生器,直接给TMOD赋0x01会把T1的配置给抹掉。养成用“先清零再置位”的习惯,在复杂项目中能省去很多联调痛苦的排查时间。
2.4 定时周期误差的实测与校准
代码写完烧录后,我用示波器测了一下P1.0引脚的波形,理论值应该是1Hz方波(0.5秒高电平,0.5秒低电平)。实测结果大致是0.4998秒,误差很小,来源主要有两个:一是12MHz晶振本身有几十ppm的频率误差,二是中断响应和软件重装载需要执行几条指令,占用了几个微秒的时间。
这个误差在LED闪烁实验里几乎感知不到,但要是做高精度时钟或者PWM输出,就得认真对待了。一个很实用的校准手段是:先用示波器测出实际频率,再反向修正重装载值。比如你发现1秒慢了一毫秒,就把初值的余量相应减少,让定时周期缩短一毫秒来补偿。
STM32平台的做法类似,但更优雅一点。配置PSC和ARR时,先确定定时器时钟是72MHz(APB1分频后通常这个值),然后按这个公式算:
定时频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)
比如想要1kHz的更新频率,(PSC+1)*(ARR+1)=72000,取PSC=71,ARR=999,就得到1kHz。每次计算初值,强烈建议推导公式都写在注释里,否则过俩月回头维护代码,你大概率已经想不起来那个数字是怎么来的了。
3. 从51到STM32再到Verilog:同源不同形的计数器机制
3.1 STM32定时器的高级骨架
做完成功的51实验,再上手STM32,你会发现定时器的设计思路完全升级了。STM32的定时器分为高级定时器(TIM1、TIM8)、通用定时器(TIM2-TIM5)和基本定时器(TIM6、TIM7),它们结构上是一层套一层的。高级定时器包含通用定时器的全部功能,通用定时器又包含基本定时器的全部功能。
用HAL库配置定时器做PWM输出时,很多人只看懂了TIM_OC_InitTypeDef的结构体配置,却不清楚底层硬件是怎么工作的。其实核心就是三件事:
- CNT计数器不停地加1(或者加PSC分频后的值)
- CNT和ARR自动重装载寄存器不断比较
- 比较匹配时触发事件(更新中断或者输出比较翻转)
库函数把这三件事封装成了结构体参数:Prescaler对应PSC,Period对应ARR,Pulse对应比较值。明白寄存器层的对应关系,以后用寄存器直接操作也能举一反三。
我特别想提一下CubeMX里那个容易看懵的配置项——timebase source。很多人在CubeMX里创建一个基础工程,默认的timebase是SysTick定时器,也就是滴答定时器。SysTick需要维持系统时钟(HAL_Delay函数的实现基础),一般情况下不建议在timebase里改成某个通用定时器,除非你确实有特殊需求,比如SysTick要留给RTOS使用。我曾见过有人为了让“定时器看起来高级点”去改timebase,结果HAL_Delay到处出问题,查了半天才发现是自己把系统的心跳源给换了。
3.2 Verilog视角下的计数器本质
如果你学完单片机开始接触FPGA,用Verilog写过计数器后,会突然领悟到前面说的那些寄存器其实本质上就是一堆触发器。
一段最基础的模100计数器Verilog代码就长这样:
module counter_100( input clk, input rst_n, output reg [6:0] count ); always @(posedge clk or negedge rst_n) begin if(!rst_n) count <= 7'd0; else if(count == 7'd99) count <= 7'd0; else count <= count + 1'b1; end endmodule第一次在仿真波形里看到count信号从99跳回0的那一刻,再回头看51单片机里TL0从0xFF跳到0x00的溢出行为,你会意识到底层逻辑是完全一致的:计数器就是一个状态寄存器,到了边界值就翻转回初始状态。硬件描述语言帮你把“硬件电路的数字逻辑”从物理世界抽离出来,让你更关注逻辑设计本身,但背后的思想是相通的。
3.3 捕获与比较:定时器进阶应用的必经之路
进阶到“定时器捕获测频率”这个实验时,很多人的思路开始有点卡了。STM32的输入捕获模式到底是怎么测频率的?其实核心就两个步骤:
- 设置通道为输入捕获模式,在引脚上检测到上升沿(或下降沿)时,CNT的当前值会被“捕获”到捕获寄存器
- 两次捕获的CNT差值,就是两个上升沿之间的定时器计数个数,配合定时器时钟频率,就能算出信号周期
这个机制用一句话说清楚就是:利用定时器本身的高速计数器当“秒表”,外部信号当“发令枪”。设好定时器时钟频率,测出CNT差值,信号的周期甚至频率就都出来了。
这里有个很容易出问题的点:被测信号频率如果太低,两次上升沿之间的CNT差值可能会超过16位(或32位)计数器的上限,导致溢出。这种情况下要先看定时器溢出中断,用“溢出次数*满量程+当前捕获值”的方式来计算总周期,这个思路在做低速PWM信号解调时尤其常见。
4. 踩坑实录:那些实验报告里不会写的边界情况
4.1 晶体振荡器频率误差带来的累积偏差
先讲一个我至今印象很深的教训。某次做“定时器控制LED实现精确1秒闪烁”实验,同学兴冲冲地来给我看他的成果——用手机秒表计时,闪烁周期偏差肉眼可见地漂移,大概每过一分钟就要慢一秒多。我第一反应是程序逻辑哪里出了问题,或者初值算错了。检查了一遍代码,初值没问题,重装载也做了,逻辑完全正确。
后来把示波器探头夹到晶振引脚上,测量实际频率,发现那个开发板上的12MHz晶振实际输出是11.989MHz——偏差大约0.09%。这个偏差单独看不大,但累积到一分钟,误差就是0.09%*60秒≈54毫秒,跟肉眼观察到的现象基本吻合。
这类问题几乎是开发板的老毛病,尤其是使用了质量一般的贴片晶振或内部RC振荡器时,误差往往在0.5%到1%之间,做高精度时钟项目时根本不可接受。解决方案有三条路:换更高精度的有源晶振,外接温补晶振,或者在软件里做校准因子。软件校准的思路是最直接的——测出实际秒周期,按比例修正初值,比如实测慢了几毫秒,就把初值相应调小。这个技巧在电子设计竞赛里是常规操作,但在课堂实验报告里往往不会被提及。
4.2 TMOD寄存器引发的“静默故障”
另一个很常遇见的坑是TMOD寄存器的配置冲突。在复杂的项目里,T0用来做LED闪烁定时,T1用来做串口波特率发生器,两个定时器同时工作,一丝丝配置疏忽就会让某个外设“静默失效”——不报错,不崩溃,就是功能不对。
症状是:串口发送乱码,LED定时闪烁不准。排查思路是:先怀疑波特率配置有没有问题,检查串口初始化代码,发现被调用的初始化函数里TMOD整体赋了值,比如TMOD=0x20;,表面看是把T1设成模式2正确配置,但这一句同时把T0的工作模式也改成了0,导致T0定时的初值逻辑不再匹配。
这类“静默故障”很考验人的经验,尤其是新学者往往不习惯位操作,直接用赋值操作搞定寄存器,结果一个寄存器服务多个外设时互相覆盖。我在代码规范里推荐的做法就是前文写过的分段操作:
TMOD &= 0xF0; // 只清低4位 TMOD |= 0x01; // 设置低4位同理清高4位是TMOD &= 0x0F。把“善用位操作”刻进肌肉记忆里,比依赖任何IDE检查都管用。
4.3 定时中断里的代码耗时失控
第三种典型问题更隐蔽:中断服务函数里塞了耗时操作。不少人写定时器中断时,喜欢直接在中断函数里处理数码管动态扫描、按键消抖,甚至干脆放个delay函数进去。这在逻辑上感觉没什么问题——反正每次定时器溢出就进来跑一段。但问题是,CPU执行这些代码是需要时间的,如果中断函数体本身的执行时间接近或超过了定时周期,主循环就像被“饿死”了一样,LED的闪烁时序也完全错乱。
我见过最夸张的案例:一位同学在1毫秒定时中断里做了一个电机控制的PID运算,外加串口打印调试数据。中断函数执行耗时接近0.8毫秒,虽然当时功能看起来都正常,但后期把串口波特率从9600调到115200时,中断函数执行时间翻了一倍,整个系统直接卡死。
正确的做法是:中断函数只做“置标志位、读值、清标志”这类轻量操作,处理逻辑放到主循环里。要让LED闪,就在中断里翻转一个标志位,主循环检测到标志位后再改变LED状态。这种“中断置位,主循环处理”的思想,是嵌入式开发中极其重要的基础素养。
4.4 从代码到报告:组件“计数器”的思路延伸
借着前面这些踩坑经验,再回头来聊实验报告这件事。我在查阅资料时看到热搜词条里有一个很有意思的短语——“计数器文件分类”。字面上它可能指的是文件管理器里对带计数器的文档做分类,但换个角度想,把你的实验报告、代码片段、调试图也当作“工程计数器”来分类管理,是个非常值得养成的习惯。
我有一个文件夹叫“Timer_Lab”,里面细分了/docs(实验报告、芯片手册、波形截图)、/code_51(每个实验版本一个子目录)、/code_stm32(HAL和标准库各放一层)、/sim_verilog(仿真工程)。坚持用“硬件平台+功能模块+版本号”的文件命名规范,比如T0_50ms_Interrupt_v1.2.c,配合简单的版本说明,你会在做毕业设计或者参加电赛时,深刻地体会到这种组织方式的巨大价值。
5. 动手验证:亲手搭建一个完整的定时器测频实验
到这一步,理论知识铺垫得差不多了,我来分享一个综合实验的完整路径,这个实验把定时器和计数器的功能都串了起来,非常适合作为进阶练习——用单片机的定时器测量外部方波信号的频率。
5.1 硬件连接与平台选型
可以使用51开发板或者STM32最小系统板。51平台外部方波信号接到P3.4引脚(对应T0外部计数输入),STM32平台则接到某个定时器的输入捕获引脚(比如TIM2的CH1,通常对应PA0)。信号源可以用另一个开发板的PWM输出,或者直接用信号发生器。
51方案的核心思路:T0工作在计数模式,对外部信号计数;T1工作在定时模式,提供1秒的时间基准。1秒内T0数到的脉冲个数,就是方波信号的频率。
5.2 关键代码与实现
// T0计数模式初始化,1秒内计数得到频率 void Counter0_Init(void) { TMOD &= 0xF0; // 清空T0配置 TMOD |= 0x05; // T0工作模式1,计数模式(C/T=1) TH0 = 0x00; TL0 = 0x00; // 初值清零,从0开始计数 ET0 = 1; EA = 1; } // T1定时器产生1秒中断 void Timer1_Init(void) { TMOD &= 0x0F; // 清空T1配置 TMOD |= 0x10; // T1模式1,定时模式 TH1 = 0x3C; TL1 = 0xB0; // 50ms初值 ET1 = 1; } unsigned int freq = 0; void Timer1_ISR(void) interrupt 3 { static unsigned int cnt = 0; TH1 = 0x3C; TL1 = 0xB0; cnt++; if(cnt == 20) // 1秒到达 { cnt = 0; freq = (TH0 << 8) | TL0; // 读取T0计数值 TR0 = 0; // 停止T0计数 TH0 = 0x00; TL0 = 0x00; // 清零 TR0 = 1; // 重新启动 } }上面这段代码隐藏着一个很关键的工程经验:读取计数器的值前,先停止计数,再读取,再清零重启。如果不先停止,T0可能在你读取TH0和TL0的间隙又跳了一个数,导致高位和低位不匹配,计数值出现跳动误差。
5.3 实测数据与误差分析
用信号发生器输出1kHz方波实测,读到的freq大约在999或1000之间波动。1Hz的跳动一方面来自T1定时周期的微小偏差,另一方面来自信号源本身的抖动。
如果你追求更精准的测量,可以使用“多周期平均法”:让T0连续计数10秒,再把总计数除以10,得到平均频率。这个办法可以显著降低单次计数时因相位差带来的截断误差。做这个实验时,把实测数据、理论频率、误差百分比整理成一张表格,你会发现实验报告立刻显得有血有肉,比单纯贴代码有说服力得多。
6. 关于定时器实验的几点个人体会
把这个实验完整地做下来、调明白,再回头看那些最初让人头大的寄存器配置,其实背后是同一个逻辑框架:时钟源的选择决定它是定时器还是计数器,初值的计算决定它的定时间隔或计数范围,重装载和中断决定它工作的连续性,而软件与硬件的配合决定了系统的效率和稳定性。
我个人的体会是,定时器这东西“纸上得来终觉浅”,你读十篇教程,不如自己动手把一个闪烁的LED调准一次。我做51实验时,老喜欢反复问自己一个问题:“如果现在晶振频率变成22.1184MHz(这是串口常用的奇葩晶振),我的初值该改成多少?”每次都能逼着自己重新走一遍计算逻辑,这个习惯让我后来切到STM32的时候几乎没费什么劲就理解了PSC和ARR的关系。
最后再分享一个小技巧:从做实验的第一天起,就在代码注释里记录下你的计算过程和实测结果。比如:
// 12MHz晶振,12T模式,机器周期1us // 定时50ms,初值=65536-50000=15536=0x3CB0 // 实测周期0.4998s,偏差较小,暂不修正别小看这几行注释,过半年回来改代码,你一定会感谢当时那个认真记录的自己。定时器计数器是整个单片机学习体系中承上启下的关键一环,把这一环真正吃透,后续的PWM、输入捕获、编码器接口,都会变得顺畅得多。
本文还有配套的精品资源,点击获取