公司动态

STM32定时器级联实战:实现超长定时与PWM的硬件级解决方案

📅 2026/7/31 11:09:25
STM32定时器级联实战:实现超长定时与PWM的硬件级解决方案
1. 项目概述当单个定时器不够用时在STM32单片机的项目开发里定时器Timer绝对是使用频率最高的外设之一从最基本的延时、PWM波生成到复杂的输入捕获、编码器接口都离不开它。但很多朋友尤其是刚上手的朋友可能会遇到一个瓶颈一个定时器的计数范围是有限的。比如一个16位的定时器最大计数值是65535就算把预分频器PSC调到最大在72MHz系统时钟下最长定时周期也就几十毫秒。如果你想实现一个长达数秒、甚至数分钟的精确定时或者需要生成一个周期特别长的PWM信号单个定时器就捉襟见肘了。这时候“定时器级联”技术就派上用场了。这名字听起来有点高级其实原理很直观就是把一个定时器的溢出信号作为另一个定时器的时钟源。让第一个定时器我们叫它“主定时器”每计满一次就“滴答”一下去驱动第二个定时器“从定时器”计一个数。这样从定时器的实际计数频率就变成了主定时器的溢出频率从而实现了计数能力的指数级扩展。简单说就是把两个甚至多个定时器“串联”起来形成一个超级长的“虚拟定时器”。我最近在一个需要精确记录设备累计运行时间长达数百小时的项目里就深度用到了TIM2和TIM3的级联。网上关于级联的代码片段不少但要么讲得太笼统要么只适用于特定型号真正把配置细节、中断处理和实际踩坑经验讲透的并不多。所以我想结合这个实际项目把STM32多个定时器级联使用的门道掰开揉碎了讲清楚特别是针对最常用的通用定时器TIM2、TIM3、TIM4、TIM5。2. 定时器级联的核心原理与方案选型在动手写代码之前我们必须先搞清楚STM32定时器级联的几种实现方式以及为什么我最终选择了“使用一个定时器触发另一个定时器”这种方案。理解背后的“为什么”配置的时候才不会迷糊。2.1 为何需要级联从需求倒推设计我的项目需求很明确需要一块32位的“软件秒表”精度在秒级即可但范围要能覆盖数千小时即计数达到3600秒/小时 * 1000小时 3,600,000次秒信号。显然任何一个16位的定时器都无法直接实现。最简单的“笨办法”是在一个1秒中断里对一个32位全局变量进行累加。这个方法可行但有两个问题一是CPU需要频繁进入中断在低功耗场景下不友好二是如果系统中有其他更高级的中断可能导致秒信号偶尔被延迟长期累积会产生误差。因此更优雅的方案是利用硬件定时器级联构建一个独立的、几乎不占用CPU资源的超长定时器。硬件级联的精度由系统时钟保证非常稳定。2.2 STM32定时器级联的三种常见模式STM32的定时器功能非常强大级联方式不止一种。主要可以分为三大类内部触发连接ITRx这是最正宗、最常用的硬件级联方式。STM32的定时器内部有专门的触发输入线路ITR0, ITR1, ITR2, ITR3可以将一个定时器的更新事件UEV作为另一个定时器的内部触发ITRx时钟源。这种方式完全由硬件自动完成信号传递不占用CPU和外部引脚效率最高也是本文重点讲解的。外部时钟模式1将一个定时器的某个输出通道比如CH1配置为比较输出模式并使其输出一个脉冲信号然后将这个信号通过杜邦线连接到另一个定时器的外部输入引脚如ETR从而驱动后者。这种方法需要占用GPIO引脚且受布线影响一般用于板内不同芯片间或验证阶段不推荐作为最终方案。软件同步在一个定时器的更新中断服务函数里手动对另一个定时器的计数器进行加一操作。这本质上还是软件计数只是借用了定时器的中断节奏并没有实现真正的硬件级联中断开销依然存在。对比下来内部触发连接ITRx无疑是实现纯硬件、高精度、超长定时的最佳选择。它就像在定时器内部修了一条“专用高速公路”让信号直达。2.3 定时器间的“亲缘关系”主从定时器选择不是任意两个定时器都可以通过ITRx进行级联。STM32的参考手册中有一张非常重要的“定时器内部触发连接”表格它规定了哪个定时器可以触发哪个定时器。以STM32F103系列为例TIM1可以作为TIM2, TIM3, TIM4的主定时器。TIM2可以作为TIM1, TIM3, TIM4的主定时器。TIM3可以作为TIM1, TIM2, TIM4的主定时器。TIM4可以作为TIM1, TIM2, TIM3的主定时器。你会发现通用定时器TIM2、TIM3、TIM4之间是“平等”的可以互相作为主或从。而高级定时器TIM1也参与其中。我选择TIM2作主、TIM3作从纯粹是因为在当时的板子上这两个外设恰好空闲。你可以根据你的项目资源占用情况灵活选择。注意不同系列的STM32如F0, F4, H7其定时器互联关系可能不同务必查阅你所使用芯片型号的《参考手册》中“定时器”章节的“定时器同步”部分这是最高准则。3. 硬件级联的详细配置步骤以TIM2主TIM3从为例理论清楚了我们进入实战环节。这里我使用标准外设库Standard Peripheral Library进行配置思路对于HAL库也是完全通用的只是函数名称不同。我们目标是将TIM2配置为每1秒产生一个更新事件这个事件作为TIM3的时钟使TIM3每接收到一次触发就计数一次。3.1 主定时器TIM2配置产生精准的“心跳”主定时器的角色是产生一个稳定、周期性的触发脉冲。我们将其配置为最简单的“更新模式”。// 1. 开启TIM2时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 2. 初始化定时器基本参数 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 9999; // 自动重装载值 ARR TIM_TimeBaseStructure.TIM_Prescaler 7199; // 预分频器 PSC TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_RepetitionCounter 0; // 通用定时器此参数无效 TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 3. 计算定时周期 // 系统时钟72MHzAPB1预分频器为2但TIM2挂在APB1上其时钟会被倍频x2所以实际时钟为72MHz。 // 定时器时钟 72MHz / (PSC 1) 72MHz / 7200 10 kHz // 定时周期 (ARR 1) / 10kHz 10000 / 10000 Hz 1秒 // 因此TIM2每1秒溢出一次产生更新事件。 // 4. 配置TIM2为主模式输出其更新事件作为触发源(TRGO) TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);关键点解析TIM_Prescaler和TIM_Period是计算定时周期的核心。公式为定时时间 (TIM_Period 1) * (TIM_Prescaler 1) / TimerClock。TIM_SelectOutputTrigger这个函数非常关键它决定了主定时器将什么信号发送出去。我们选择TIM_TRGOSource_Update意味着将“更新事件”作为触发输出TRGO。这个TRGO信号就会连接到内部的ITRx线路。3.2 从定时器TIM3配置接收心跳并计数从定时器需要将自己配置为“从模式”并选择正确的主定时器作为其时钟源。// 1. 开启TIM3时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 2. 初始化TIM3时基单元此时时钟源还未生效配置仅供参考 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 0xFFFFFFFF; // 作为32位计数器我们用到它的最大范围 TIM_TimeBaseStructure.TIM_Prescaler 0; // 从模式时钟来自外部内部预分频设为0 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 3. 配置TIM3为外部时钟模式1 从模式 TIM_ETRClockMode2Config(TIM3, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0); // 4. 配置TIM3的触发选择寄存器选择ITR1作为触发源 // 查表可知TIM2的TRGO连接到了TIM3的ITR1输入上。 TIM_SelectInputTrigger(TIM3, TIM_TS_ITR1); // 5. 配置TIM3为从模式 TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_External1); // TIM_SlaveMode_External1 模式意味着当时钟源ITR1的上升沿到来时TIM3的计数器CNT加1。 // 6. 使能TIM3的硬件同步启动从模式 TIM_SelectMasterSlaveMode(TIM3, TIM_MasterSlaveMode_Enable);关键点解析TIM_ETRClockMode2Config这个函数名字有点误导它并不是只能配置ETR引脚。在这里我们用它来配置“外部时钟模式1”的相关滤波器、极性等但因为我们使用的是内部ITR信号所以第一个参数TIM_ExtTRGPSC_OFF表示预分频关闭第二个参数极性通常选非反相即可。TIM_SelectInputTrigger(TIM3, TIM_TS_ITR1)这是灵魂配置它告诉TIM3“你的时钟信号来自ITR1这条内部线路”。而ITR1在芯片内部已经固定连接到了TIM2的TRGO输出上。这个对应关系必须查手册确认。TIM_SlaveMode_External1这个从模式决定了TIM3如何响应触发信号。External1模式就是最简单的“来一个脉冲计数器加一”完美符合我们的需求。3.3 启动与读取让级联系统运转起来配置完成后启动顺序有讲究必须先启动从定时器再启动主定时器。// 1. 先使能从定时器TIM3让其进入等待触发状态 TIM_Cmd(TIM3, ENABLE); // 2. 再使能主定时器TIM2开始产生触发脉冲 TIM_Cmd(TIM2, ENABLE);现在这个级联系统就开始运行了。TIM2每1秒“滴答”一下TIM3的计数器就加1。经过1小时3600秒TIM3的计数器值就是3600。要读取这个“超级定时器”的值你只需要读取TIM3的计数器寄存器uint32_t super_timer_count TIM_GetCounter(TIM3); uint32_t hours super_timer_count / 3600; uint32_t minutes (super_timer_count % 3600) / 60; uint32_t seconds super_timer_count % 60;由于TIM3是16位的但它的Period我们设置成了最大值理论上可以计数到65535秒约18小时。如果你需要更长的范围可以启用TIM3的更新中断在其中断服务函数里对一个32位甚至64位的软件变量进行累加这样就能实现近乎无限的计时范围且中断频率很低1Hz。4. 级联应用进阶构建32位绝对计数器与长周期PWM掌握了基础级联我们就可以玩出更多花样。级联不只是为了简单的计时更能构建复杂的定时逻辑。4.1 实现真正的32位硬件计数器上面的例子中TIM3仍然是16位的。如果我们想直接获得一个32位的硬件计数器可以将两个16位定时器完全“拼接”起来。一种常见做法是让主定时器工作在“重复计数器”模式Repetition Counter或者利用主定时器的每次溢出产生一个非常快的中断在这个中断里快速操作从定时器。但更优雅的方式是利用STM32部分型号如F4系列中TIM2和TIM5是32位定时器的特性或者使用TIM1和TIM8的高级特性。不过对于F1系列我们可以通过“主定时器中断从定时器计数”来模拟。让主定时器TIM2产生一个较高频率的中断例如10kHz在中断里不进行复杂操作仅作为一个“硬件节拍”。同时将从定时器TIM3的时钟源设置为内部时钟72MHz但将其预分频器设置得很大使其计数很慢。然后在主定时器中断里读取TIM3的计数值并与上一次中断的读数进行比较和累加在软件中合成一个高精度的长周期计时器。这种方法中断频率较高但对CPU占用依然很低10kHz中断在72MHz下占比很小且能获得微秒级的32位计时能力。4.2 生成超长周期的PWM信号PWM生成是定时器的另一大功能。有时我们需要生成周期为几秒甚至几分钟的PWM波比如控制慢速的散热风扇或LED呼吸灯。单个定时器受限于ARR值很难直接实现。利用级联我们可以轻松做到用级联后的“超级定时器”作为PWM生成的时钟基准。具体步骤如下按照前述方法将TIM2和TIM3级联假设产生一个1秒周期的时钟基准。使用另一个定时器如TIM4生成PWM。但将TIM4的时钟源设置为来自TIM3的触发输出如果支持或者采用软件方式。更实用的方法是将TIM3本身用于PWM输出。因为TIM3的计数器现在每1秒加1我们可以设置TIM3的捕获比较寄存器CCR为一个很大的值。例如设置ARR599CCR300。那么PWM的周期就是600秒10分钟占空比为50%高电平300秒低电平300秒。这就实现了用硬件直接生成超长周期PWM无需CPU干预。// 在TIM3级联配置的基础上增加PWM输出配置以通道1为例 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; // PWM模式1 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; // 使能输出 TIM_OCInitStructure.TIM_Pulse 300; // 比较值CCR决定占空比 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; // 输出极性 TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能预装载 // 注意此时TIM3的ARR应设置为599周期为600秒。 TIM_SetAutoreload(TIM3, 599); // 必须使能TIM3的ARR预装载 TIM_ARRPreloadConfig(TIM3, ENABLE);4.3 级联中的中断处理与资源管理级联系统虽然硬件自动运行但中断依然是我们获取状态、处理复杂逻辑的重要手段。这里有几个关键点主定时器中断可以用于执行一些需要严格同步的、周期为T1的任务。例如TIM2的1秒中断可以用来刷新显示屏时间。从定时器中断由于从定时器计数很慢它的更新中断如果使能频率会非常低。我们可以用它来处理“超级周期”事件。例如TIM3的ARR设为35991小时那么它的更新中断就是每小时触发一次可以用来保存数据、切换工作模式等。中断优先级如果同时开启了多个定时器中断必须合理设置优先级。通常产生更快节奏信号的主定时器中断优先级应高于从定时器。同时要避免在中断服务函数中进行耗时操作以免影响级联计时的准确性。实操心得在调试级联系统时我强烈建议先用一个GPIO引脚在主定时器的更新中断里翻转电平用示波器或逻辑分析仪测量实际产生的周期是否与计算值相符。这是验证主定时器配置是否正确的最直接方法。确认主定时器工作正常后再开启从定时器并通过读取从定时器CNT寄存器的值来观察是否随着主定时器中断的触发而规律增加。5. 调试技巧与常见问题排查实录即使理解了原理和步骤实际调试中还是会遇到各种问题。下面是我在项目中踩过的坑和总结的排查思路希望能帮你快速定位问题。5.1 级联不工作的通用排查流程当你发现从定时器TIM3的计数器不增长时可以按照以下步骤排查检查时钟确认TIM2和TIM3的APB总线时钟是否已使能RCC_APB1PeriphClockCmd。这是最常被忽略的一步。验证主定时器暂时屏蔽从模式配置单独测试TIM2。使能TIM2的更新中断在中断里翻转一个LED或GPIO。用示波器测量这个GPIO看中断周期是否是你预期的1秒。如果这里就不对检查TIM2的PSC和ARR计算以及系统时钟配置是否正确。检查触发信号路径确认TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update)已正确调用。可以尝试将触发源改为TIM_TRGOSource_Enable以CNT使能作为触发进行测试。检查从定时器输入源这是最容易出错的地方确认TIM_SelectInputTrigger(TIM3, TIM_TS_ITR1)中的TIM_TS_ITR1是否正确。TIM2连接到TIM3的ITR线不同型号可能不同F1系列通常是ITR1。务必务必查对你所用芯片的数据手册或参考手册中的“定时器同步”图表。检查从模式选择确认TIM_SelectSlaveMode函数调用正确模式为TIM_SlaveMode_External1。检查启动顺序确保先TIM_Cmd(TIM3, ENABLE)后TIM_Cmd(TIM2, ENABLE)。利用调试器在IDE如Keil MDK的调试模式下打开外设寄存器窗口观察TIM2和TIM3的状态寄存器SR。特别是TIM2的UIF更新中断标志和TIM3的TIF触发中断标志。当TIM2计数溢出时UIF会置1同时你应该能看到TIM3的TIF也被置1如果使能了相关中断的话。这是判断硬件触发是否成功的关键标志。5.2 常见问题与解决方案速查表问题现象可能原因解决方案从定时器完全不计数1. 主/从定时器时钟未开启。2. 主定时器未正确产生触发信号。3. 从定时器触发源ITRx选择错误。4. 从模式未正确配置或使能。1. 检查RCC配置代码。2. 单独测试主定时器中断。3. 核对芯片手册确认ITR连接关系。4. 检查TIM_SelectSlaveMode和TIM_SelectMasterSlaveMode调用。从定时器计数速度过快或过慢1. 主定时器周期计算错误。2. 从定时器预分频器PSC未设置为0。3. 主定时器使用了不合适的触发源如用了比较匹配而非更新事件。1. 重新计算PSC和ARR。2. 将从定时器PSC明确设为0。3. 确保主定时器使用TIM_TRGOSource_Update。级联工作一段时间后停止1. 主/从定时器被其他代码意外关闭或复位。2. 发生了未处理的定时器硬件错误极少见。3. 中断服务函数中进行了耗时操作影响了系统节拍。1. 检查全局是否有其他地方操作了TIM2/TIM3的ENABLE位。2. 添加看门狗或定时器状态监控代码。3. 优化中断服务函数仅置标志位在主循环处理。使用PWM输出时无波形1. PWM对应的GPIO引脚未配置为复用推挽输出。2. TIMx的通道未正确映射到引脚重映射功能。3. PWM输出未使能TIM_CtrlPWMOutputs针对高级定时器。4. 从定时器ARR值设置过大导致周期太长短时间内观察不到变化。1. 检查GPIO初始化代码。2. 查阅数据手册的引脚复用功能表。3. 对于TIM1/TIM8需要调用TIM_CtrlPWMOutputs(TIMx, ENABLE)。4. 先用小的ARR值测试PWM功能是否正常。5.3 高级调试使用逻辑分析仪抓取内部触发信号对于顽固的级联问题如果条件允许可以尝试一种“曲线救国”的调试方法虽然ITR是内部信号但我们可以利用定时器的“主模式输出”功能将内部触发信号映射到某个GPIO上观察。例如可以将TIM2的TRGO信号映射到其某个通道引脚上这需要查手册看是否支持通常高级定时器支持此功能。或者更简单的方法是在TIM2的更新中断服务函数里快速翻转一个测试引脚。同时在TIM3的触发中断如果使能了服务函数里翻转另一个测试引脚。用逻辑分析仪同时抓取这两个引脚就能清晰地看到TIM2的更新事件是否产生以及TIM3是否紧随其后被触发。两个脉冲之间的延迟应该非常小纳秒级如果看到明显延迟或第二个脉冲缺失就能精准定位问题环节。最后关于资源冲突再多说一句。STM32的定时器有些功能会共用资源比如TIM2的通道1和TIM5的通道1可能共用同一个引脚。当你同时使用多个定时器的高级功能时一定要仔细核对数据手册的“复用功能”表格避免引脚功能冲突导致外设工作异常。级联本身不占用额外引脚但如果你在级联的定时器上同时使能了PWM输出就需要规划好引脚分配了。