☰
单片机定时器溢出计算公式详解:五大品牌对比与避坑指南
2026/10/14 17:53:52 网站建设 项目流程

搞单片机开发的朋友应该都有这种经历:拿到一块不熟悉的芯片,别的功能可以先放着,但定时器一定要先搞定。点灯、扫按键、做PWM、算波特率,全得靠它。我这些年用过的片子少说也有十几个系列,三星、现代、中颖、合泰、松翰这几家算是国内中小项目里出现频率最高的了。其中最基础、最绕不开的,就是定时器溢出计算公式。今天我把这几家的定时器计算思路放在一起聊,核心公式其实都一样,但每家又有各自的脾气,尤其是合泰,很容易让你栽跟头。

1. 定时器溢出到底在算什么

先把最底层的东西讲清楚。定时器本质就是一个计数器,内部时钟每跳一下,计数寄存器就加一或者减一,加到溢出(或减到0)的时候,触发一次中断。我们需要算的,就是“从启动到溢出这中间到底花了多长时间”,反过来,就是“我想要多长时间溢出一闪,初值该怎么填”。

1.1 机器周期才是时间基准

8位单片机里,定时器的计数脉冲通常不是直接来自晶振,而是来自经过分频后的“机器周期”。这个概念搞不懂,后面全乱套。

标准8051架构的芯片,12MHz晶振下,12T模式时一个机器周期就是12个时钟周期,也就是1微秒。公式是:

机器周期 = 12 / 晶振频率(12T模式)

机器周期 = 1 / 晶振频率(1T/6T等增强模式)

三星、现代、中颖这三家,绝大多数型号都是8051内核兼容,所以这套规则直接通用。松翰是类PIC的RISC架构,指令周期跟系统时钟的关系要看具体型号的配置位,但定时器时基来源的思路也是一样的,都是“外部晶振经过分频器之后给计数器供时钟”。

1.2 溢出时间的通用公式

对16位定时器来说,计数器是从初值往65535方向加,加到65536溢出。所以公式长这样:

溢出时间 = (65536 - 初值) × 分频系数 × 机器周期

反推初值就是:

初值 = 65536 - (目标时间 / (分频系数 × 机器周期))

举个例子,12MHz晶振、12T模式、定时器不分频,想要1毫秒溢出一次:

机器周期 = 12 / 12000000 = 1微秒

初值 = 65536 - (1000 / 1) = 64536

64536换算成16进制是0xFC18,所以TH0 = 0xFC,TL0 = 0x18。

这个计算过程,三星S3C系列、现代GMS系列、中颖SH88系列,全部适用。说白了,你只要确认了“指令周期/定时器时钟源”这两个参数,剩下的就是一乘一除的事。

2. 五家芯片的定时器差异逐个看

虽然大公式通用,但每家芯片在具体实现上还是有区别的。这些差异才是实际项目中容易卡壳的地方。

2.1 三星与中颖:最省心的8051标准派

三星的老款8051系列(比如S3C825、S3C94系列)和中颖的SH88F/SH79F系列,定时器模块基本就是标准8051的翻版。Timer0和Timer1都是16位,有4种工作模式,常用的是模式1(16位不自动重载)和模式2(8位自动重载)。

这类芯片算初值的时候,我一般直接用上面的公式,然后把结果写成两个宏定义:

#define FOSC 12000000UL #define TIMER_CLK (FOSC / 12) // 12T模式 #define TIME_1MS 64536 // 65536 - 1000us / 1us void Timer0_Init(void) { TMOD &= 0xF0; TMOD |= 0x01; // Timer0模式1,16位 TH0 = TIME_1MS >> 8; TL0 = TIME_1MS & 0xFF; ET0 = 1; TR0 = 1; }

要提醒的是,三星的S3F系列里有些型号提供了1T模式,中颖的部分型号也支持6T/12T切换。代码移植的时候一定要去数据手册里搜“SMOD”或者“CKCON”这类寄存器,确认当前是几T模式。很多人把程序从STC抄到中颖,忘记改时钟模式,定时时间直接差了12倍。

2.2 现代单片机:注意看是“现代”还是“现代兼容”

现代(HYUNDAI)的单片机在国内用得不算特别多,但GMS系列的性价比在某些家电项目里确实能打。它同样是8051内核,所以定时器计算方式和三星几乎一样。GMS87系列的标准Timer0/Timer1也是16位,唯一要注意的是它的时钟源选择位可能放在特殊功能寄存器里,而且有个别型号支持外部引脚计数模式,这个模式下一旦配置错误,定时器会变得极其慢或者干脆不走。我建议在初始化代码里把所有控制位都显式写一遍,不要依赖复位默认值。

现代单片机的代码风格偏老式,寄存器命名跟标准的SFR不完全一致,查数据手册时别按习惯直接找“TCON”,要按厂家手册索引来查。

2.3 松翰:8位定时器为主,初值反而更好算

松翰的SN8P系列是另一种风格。它很多型号的定时器是8位的,比如T0模块的计数器是8位,溢出值是256。公式变成:

溢出时间 = (256 - 初值) × 分频系数 × 指令周期

初值 = 256 - (目标时间 / (分频系数 × 指令周期))

8位的范围小,一次最多计255个周期,如果定时时间很长,就得靠分频器或者中断里用软件变量累加。比如我用SN8P2711做红外遥控解码,定时器T0配置成64分频,指令周期按系统时钟算,目标定时是50微秒一次中断。假设系统时钟4MHz,那么:

初值 = 256 - (50 / (64 / 4000000)) = 256 - 3.125 = 252.875

取253,实际误差大约4%,因为8位计数器一次只能计3个周期多一点,索性就让它溢出频繁一点,配合软件计数器做毫秒基准。

松翰的PWM功能经常复用定时器,比如T1既做定时又输出PWM,这时候初值不只是决定中断周期,还决定PWM频率。改PWM占空比的时候不要乱动初值,否则定时中断周期会跟着漂。建议把PWM周期寄存器当定时器周期用,中断标志去读而不依赖溢出。

2.4 合泰:最容易算错的加1问题

这里必须单独拿一段出来说。合泰的HT66F系列等型号,定时器模块跟传统8051完全不同。它用的是“周期寄存器+1”的模式。什么意思?就是计数器的上限不是固定的65535或者255,而是你写进周期寄存器(比如STM的STMPH/STMPL)的那个值。计数器从0往上加,加到等于周期寄存器+1的时候溢出/匹配。

所以合泰的计算公式不是“65536减初值”,而是:

溢出时间 = (周期寄存器值 + 1) × 分频系数 × 指令周期

反推:

周期寄存器值 = 目标时间 / (分频系数 × 指令周期) - 1

这个“减1”是无数人踩过的坑。包括我自己,第一次用合泰HT66F004写1毫秒定时中断,直接套8051的思路,结果中断周期多了一个计数节拍,放在I2C时序里直接导致通信不稳定。

合泰的定时器还有一点要注意:它支持向上计数、向下计数、和中心对齐模式,不同模式下,溢出中断的发生点不一样。中心对齐模式下,溢出/匹配中断一个周期内可能产生两次,严格来说中断频率是定时周期的两倍。做PWM时问题不大,但做固定周期时基任务调度时,你会觉得系统时间走的忽快忽慢,其实不是晶振问题,是模式问题。

简单总结:合泰的标准定时器(STM)是16位,通用定时器(CTM)是10位,周期型定时器(PTM)有10/12/16位版本。位数变了,计数上限变了,但“加1再乘周期”的逻辑不变。不同位数只是范围差异,公式结构一致。

3. 手把手算一遍:从需求到代码

现在用三个常见场景来完整走一遍计算过程,这样大家可以直接套用。

3.1 场景一:12MHz外部晶振下的1ms系统时基

这是一个很常见的需求,用来做按键扫描、LED刷新、或者简单的软件定时器。假设用的是一颗标准8051内核的芯片,比如中颖SH88F4051,配置为12T模式。

第一步,确认机器周期。12MHz的12T模式下,机器周期=1微秒。 第二步,确认分频。标准Timer0没有额外分频的话,计数脉冲频率=1MHz。 第三步,算初值。1ms=1000微秒,初值=65536-1000=64536=0xFC18。

这个值无论在三星、现代还是中颖,代码都通用。唯一要改的是芯片头文件里的寄存器名称。

3.2 场景二:32768Hz外部晶振下的1秒定时

低功耗项目常用32.768kHz做RTC或者休眠唤醒定时。假设一颗松翰SN8P系列芯片,指令周期直接等于系统时钟(即1T执行),定时器T0配置为32分频,那么计数脉冲频率=32768/32=1024Hz,一个计数周期约等于0.9765625毫秒。

要在128微秒级比较麻烦,但如果只是定时1秒,可以这样:让定时器每1毫秒中断一次,软件计数1000次。1ms对应的初值(8位定时器)为:

初值 = 256 - (1 / (1 / 1024)) = 256 - 1024 = ?

这个结果一看就不对,8位计数器装不下。说明分频选小了。改成128分频:

计数脉冲频率 = 32768 / 128 = 256Hz,周期约3.90625毫秒

初值 = 256 - (500 / 3.90625) 左右是负数,还是不够。

所以正确做法是:让定时器自由溢出,不必非让它1毫秒一次。设计好时基后,中断N次代表1秒。比如采用不额外分频,32.768kHz直接输入8位定时器,每计256个脉冲溢出一次,溢出频率=32768/256=128Hz。然后软件变量累计128次,就是刚好1秒。这意味着定时器中断里执行:

if (++sec_count >= 128) { sec_count = 0; seconds++; }

这种方式,误差理论上为0,因为128×256=32768,正好一圈。这就是为什么低功耗设计里经常用2的整数幂做分频,计算方便,误差也好控制。

3.3 场景三:合泰HT66F系列,输出一个1kHz的PWM

假设系统时钟4MHz,指令周期=1/4MHz=0.25微秒。用合泰的16位标准定时器STM作为PWM周期定时器,预分频选8分频,计数脉冲周期=8×0.25=2微秒。1kHz对应的周期=1000微秒,那么周期寄存器值=1000/2-1=499,也就是写入STMPH/STMPL的值为0x01F3。

这里必须强调:如果忘了减1,写500进去,实际周期=(500+1)×2=1002微秒,对应频率998Hz。别小看这0.2%的偏差,在电机控制或者音频应用里,这个误差会让系统表现非常古怪。你要是查半天找不到原因,最后发现只是“加1减1”的问题,会很无语。

4. 定时器计算中的常见坑与排查方法

算公式只是第一步,真正写进代码能不能跑出预期时间,还要看下面这些细节。这些坑我基本都踩过,写出来大家绕开走。

4.1 初值装载顺序与瞬时误差

在标准8051的16位模式下,TH0和TL0是各自独立的8位寄存器,单片机并没有“一次写入16位初值”的原子操作。如果你先写TL0后写TH0,在这两条指令之间定时器可能已经用旧值跑了一小段时间,导致第一次定时周期偏小。

习惯性做法:

TH0 = 0xFC; TL0 = 0x18;

这个顺序是合理的。但中断里重装初值时,从“进中断”到“TH0赋值完成”之间还有几条指令的延迟,所以严格来说每次中断周期都会比理论值多出几个机器周期。短时间看无所谓,但做长时间累积计时时,一天下来可能多出几十毫秒甚至更多。解决思路有三个:一是使用支持自动重载的模式(模式2或增强型芯片的硬件自动重载);二是把初值微调,按实测误差补偿;三是尽量把初值计算放到中断入口最前面。

4.2 时钟源不分频还是分频,先查默认值

很多芯片的定时器时钟源默认是“不分频”或者“12分频”,但不一定符合你的需求。比如中颖的部分型号,复位后定时器时钟源可能是外部引脚或者低频内部RC,如果不显式配置,定时器完全不走。我遇到过一颗现代单片机,定时器中断隔好久才触发一次,后来发现是T0M寄存器里时钟源选了慢速时钟,而不是系统时钟。这种问题靠公式算不出来,只能靠“先确认时基来源”这个习惯来预防。

这里给个自查清单:

  • 当前系统时钟是多少?内部RC还是外部晶振?
  • 定时器输入时钟是系统时钟还是独立时钟?
  • 预分频值是多少?复位默认值是多少?
  • 定时器位数是8位还是16位?
  • 有自动重载还是手动重载?
  • 是否处于计数器模式而不是定时器模式?

在代码里把这几项都显式配置一遍,别省事用默认值。

4.3 中断服务时间对溢出周期的影响

定时器中断里如果执行的操作太长,下一次溢出标志可能在你还没退出中断时就置位了。标准8051里,如果中断里一直不退出,溢出标志会被挂起,直到当前中断结束才响应下一次中断,这个“排队效应”会让定时周期整体变长。更严重的情况是:你的主程序里关了总中断(EA=0)处理某些临界区,这期间定时器照样走,溢出不处理,恢复中断时系统时间就乱了。

我做红外解码时就遇到过这类问题。红外解码要求时序非常精确,如果定时中断里做了太多事情,解码结果就不可靠。后来改成“定时中断只置标志位,具体解析放到主循环”,时序稳定了很多。所以定时器中断服务函数里,原则是能短就短,优先级高的时基任务只做标记。

4.4 晶振偏差与实测校准

公式算出来终究是理论值,实际晶振的频率是有误差的。晶振精度常见为±20ppm到±50ppm,如果芯片默认使用内部RC振荡器,误差更大,有些型号能达到±2%甚至±5%。12MHz的内部RC,实际可能是11.4MHz,算出来的1ms毫秒定时实际可能是1.02毫秒。

对一般LED闪烁来说无所谓,但做串口波特率、RTC时基就很致命。校准办法是:先用示波器测量某个引脚翻转的时间,对比理论值,然后修改初值补偿。比如实测定时周期比理论大了2%,就在初值里减少对应比例。要注意的是,这个补偿只在同一颗芯片、同一温度条件下近似有效,温度变化大的场合,最好还是用外部晶振。

4.5 问题速查表

现象可能原因排查方向
定时时间变成理论值12倍12T/1T模式配置错误检查时钟模式寄存器
定时器不工作时钟源选错或未启动检查定时器时钟选择位和TR0/TR1
第一次中断时间不对初值装载顺序问题中断前先赋TH再赋TL
累计时间越走越快中断重装初值未考虑进中断延迟使用自动重载或补偿初值
合泰定时周期总是偏大周期寄存器没减1STMP值 = 目标周期/计数脉冲周期 - 1
8位定时器无法定时到目标时长计数范围不够加大预分频或换16位定时器
波形频率有轻微跳变中断服务里占用了过长CPU时间精简中断函数,可配合主循环处理

5. 做一张自己的定时器速查表

讲了这么多,如果你正在做一个多型号单片机平台的项目,强烈建议自己做一张“定时器参数速查表”。表格不需要复杂,包含芯片型号、内核类型、定时器位数、定时器时钟来源、分频系数、指令周期、还有常用定时时间的初值,这样换芯片时对照查表,比临时翻数据手册快得多。

以我之前做过的某跨平台小家电项目为例,需要兼容中颖和合泰两颗芯片,项目组就做了这样一张内部速查表:

芯片型号定时器位数系统时钟定时器时钟1ms初值
中颖某8051型Timer016位12MHz外部12分频 = 1MHz0xFC18
合泰某HT66型STM16位4MHz内部8分频 = 500kHz周期寄存器 = 499

这张表做好后,固件工程师写定时器初始化代码基本不用动脑,照着表填数字就行,省下来回翻手册的时间,项目进度明显快了不少。

5.1 预留一个可配置的定时器初始化接口

为了让代码在几个芯片间可移植,我喜欢把定时器初始化封装成一个带参数的函数,参数包括时基类型(微秒或毫秒)和时间值,函数内部自动计算分频和初值。比如:

void Timer_SetPeriod(uint32_t base_freq_hz, uint32_t timeout_us) { uint32_t counts; uint32_t prescaler = 1; // 先根据timeout_us选择合适分频,保证counts在计数器范围内 counts = (base_freq_hz * timeout_us) / 1000000; while (counts > 65535) { prescaler *= 2; counts = (base_freq_hz * timeout_us) / 1000000 / prescaler; } // 再写对应寄存器 }

虽然这样代码看起来更长,但换平台的时候不用重新推导公式,只要把时钟频率和定时器位数传进去,初值自己算出来。对于要经常切换三星、现代、中颖、合泰、松翰这几家平台的团队来说,这个思路足够友好。

5.2 最终建议

定时器计算这个东西,说到底是底层基本功。真正的难点不是公式本身,而是“你这个芯片的定时器到底属于哪一种模式”。拿一颗新芯片,我建议先干三件事:第一,去数据手册里找到定时器时钟源框图,看清时钟怎么进来;第二,找官方例程里的定时器初始化代码,看默认配置;第三,用一个最简单的LED翻转程序实测,验证计算是否准确。

我在实际项目里的习惯是,每接触一款新单片机,都会先写一个“1秒翻转IO”的最小工程,配合示波器看引脚波形。波形稳定且周期准确,才继续写别的功能。这样做的成本很低,却能在项目早期就排除掉一大批定时器相关的隐性风险。后面再遇到什么“定时不准”“中断不进入”的诡异问题,排查范围会小很多,不会一头扎进公式里出不来。

虽然各家的寄存器名称千奇百怪,但只要抓住“时基来源、分频、计数位数、溢出条件”这四个要素,任何一颗8位单片机的定时器都能在五分钟内算明白。公式背不背无所谓,把推导逻辑记牢,比什么都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询