
简介这是一份面向单片机初学者的定时器/计数器实验报告围绕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四种工作模式尤其模式116位定时器是使用率最高的。这里插一句很多教材里会说“12MHz晶振一个机器周期是1微秒”这个说法在STC89C52上成立是因为这个芯片的时钟是12T模式12个时钟周期等于1个机器周期。但如果你用的是STC15系列或者某些1T增强型单片机同样的12MHz晶振一个机器周期直接变成1/12微秒定时器初值的计算就完全不一样了。这一点后面章节会详细拆解做实验前务必先确认自己芯片的工作模式。实验需要的硬件非常基础一块开发板、一个USB下载器、一个LED模块或数码管模块外加示波器或逻辑分析仪没有的话用LED闪烁配合秒表也能凑合验证。软件方面用Keil uVision编写C语言程序STC-ISP软件下载烧录。2.2 工作模式的选择与初值计算逻辑51定时器有四种工作模式但实验中真正好用的就两个模式013位计数器TH高8位加TL低5位几乎没人用兼容老古董的遗物。模式116位计数器TH和TL各8位范围0到65535最常用。模式28位自动重装载适合需要精确定时中断周期的场景比如串口波特率发生器。模式3定时器0被拆成两个独立的8位计数器应用场景极窄。我做这个实验时选了模式1。核心思路是这样的要求定时50毫秒机器周期1微秒那么总共需要计数50*100050000次。16位计数器满值是65536所以初值就是65536-5000015536。把15536转换成十六进制是0x3CB0于是TH00x3CTL00xB0。这段计算看起来简单但有个细节非常关键定时器溢出后寄存器归零需要软件重新赋初值。如果你在中断服务函数里忘了重新赋值那么下一次定时就不是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为模式116位定时器 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时先确定定时器时钟是72MHzAPB1分频后通常这个值然后按这个公式算定时频率 定时器时钟 / (PSC 1) / (ARR 1)比如想要1kHz的更新频率(PSC1)*(ARR1)72000取PSC71ARR999就得到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对应PSCPeriod对应ARRPulse对应比较值。明白寄存器层的对应关系以后用寄存器直接操作也能举一反三。我特别想提一下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 7d0; else if(count 7d99) count 7d0; else count count 1b1; 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整体赋了值比如TMOD0x20;表面看是把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_stm32HAL和标准库各放一层、/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/T1 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-50000155360x3CB0 // 实测周期0.4998s偏差较小暂不修正别小看这几行注释过半年回来改代码你一定会感谢当时那个认真记录的自己。定时器计数器是整个单片机学习体系中承上启下的关键一环把这一环真正吃透后续的PWM、输入捕获、编码器接口都会变得顺畅得多。本文还有配套的精品资源点击获取