
1. 从“定时”到“中断”为什么STM32的定时器如此重要如果你刚开始接触STM32可能会觉得定时器Timer这个概念有点抽象不就是个“闹钟”吗但当你真正用它来驱动LED闪烁、测量脉冲宽度、生成PWM控制电机甚至作为整个系统的时间基准时你就会发现它是STM32微控制器里最核心、最灵活的外设之一没有“之一”。今天我们不谈那些复杂的PWM输出、输入捕获就从最基础也最常用的定时器定时中断和它的外部时钟源配置讲起。这就像是学武功先扎马步马步扎稳了后面的擒拿格斗、刀枪剑戟才能耍得开。很多新手在配置定时器中断时常常会遇到两个困惑第一计算出来的定时时间总是不准或者根本不是自己想要的第二看到手册里“外部时钟模式1”、“外部时钟模式2”、“ETR引脚”这些名词就头大不知道它们到底用在什么场景。这篇笔记我就结合自己踩过的坑把这两个核心问题掰开揉碎了讲清楚。我们的目标很简单让你不仅能照着代码把定时中断跑起来更能彻底理解每一个配置参数背后的意义做到知其然更知其所以然。这样下次当你需要用一个外部信号来精准控制定时器时就知道该怎么下手了。2. 定时器定时中断的核心原理与配置“三步法”定时器中断的本质是让定时器在设定的时间到达时产生一个中断信号通知CPU“时间到了快来处理事情”。这样CPU就不用一直傻傻地等着即“轮询”可以先去执行其他任务等“闹钟”响了再过来。在STM32中实现这个功能主要依赖于三个核心部件时基单元、自动重装载寄存器和中断使能控制。2.1 时基单元定时器的“心脏”时基单元是定时器计时的基础主要由三部分组成计数器寄存器这是核心一个向上或向下计数的变量。预分频器负责对输入的时钟源进行分频。假设系统给定时器的时钟是72MHz这个频率太高直接计数的话计数器瞬间就溢出了。预分频器的作用就是把这个高频时钟“降速”。比如设置预分频值为7199那么分频后的实际计数时钟频率就是72MHz / (71991) 10KHz。这里的“1”是因为预分频器是一个N1的分频器配置值PSC代表分频系数减一。自动重装载寄存器这是定时的“目标值”。计数器从0开始每来一个时钟脉冲就加1当它的值等于ARR的值时就代表一个定时周期到了此时会产生一个“更新事件”并且计数器清零在向上计数模式下重新开始。定时时间的计算公式是定时时间 (ARR 1) * (PSC 1) / TIMx_CLK其中TIMx_CLK是定时器的输入时钟频率。这个公式是理解一切定时器应用的基石。举个例子我们要实现一个1秒的定时中断假设TIMx_CLK 72MHz。我们先确定PSC目的是让计数器每计一次数的时间是一个整毫秒或整微秒方便计算。比如我们让分频后的时钟为1MHz即每微秒计数一次那么PSC 72MHz / 1MHz - 1 71。然后计算ARR1秒 1,000,000微秒。所以ARR 1000000 - 1 999999。 这样计数器从0数到999999正好是1,000,000微秒即1秒。注意ARR和PSC都是16位或32位寄存器有最大值限制如0xFFFF。当计算出的ARR或PSC值超过寄存器范围时就需要调整思路比如增大PSC来降低计数频率从而减小ARR的值。2.2 配置定时中断的“三步法”实操以STM32F103C8T6的通用定时器TIM2为例使用标准外设库。配置流程可以固化成一个清晰的“三步法”第一步开启时钟和初始化时基任何外设使用前必须先开启它的时钟。TIM2挂载在APB1总线下。RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_Period 9999; // ARR值决定周期 TIM_TimeBaseInitStruct.TIM_Prescaler 7199; // PSC值72M/(71991)10KHz TIM_TimeBaseInitStruct.TIM_ClockDivision TIM_CKD_DIV1; // 时钟分频与数字滤波器相关通常设为DIV1 TIM_TimeBaseInitStruct.TIM_CounterMode TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseInit(TIM2, TIM_TimeBaseInitStruct);这段代码配置了一个10KHz的计数时钟ARR为9999所以定时周期为(99991) / 10KHz 1秒。第二步配置中断并开启光有时基还不够需要明确让定时器在“更新事件”即定时时间到时产生中断。TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 使能更新中断接着需要配置NVIC让CPU能够接收并处理这个中断。NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel TIM2_IRQn; // 中断通道 NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 1; // 抢占优先级 NVIC_InitStruct.NVIC_IRQChannelSubPriority 1; // 子优先级 NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct);优先级数字越小优先级越高。抢占优先级高的可以打断正在执行的抢占优先级低的中断。第三步编写中断服务函数并启动定时器中断服务函数的名字是固定的在启动文件startup_stm32f10x_xx.s中有定义。void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { // 检查是否是更新中断 // 这里是你的中断处理代码比如翻转一个LED灯 GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0))); TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 千万记得清除中断标志位 } }最后在主函数中启动定时器TIM_Cmd(TIM2, ENABLE); // 使能定时器计数器开始运行实操心得最容易忘记的就是清除中断标志位。如果不清除CPU会认为中断一直存在导致连续不断地进入中断服务函数程序就“卡死”在中断里了。TIM_ClearITPendingBit这句代码务必写上。3. 定时器外部时钟模式深度解析何时用怎么选前面我们用的时钟源都是来自内部的APB总线时钟。但STM32的定时器强大之处在于它可以从外部引脚获取时钟信号让定时器的计数节奏完全由一个外部信号来决定。这在很多场景下非常有用比如频率测量让定时器对外部脉冲计数。精确定时使用一个比内部晶振更精准的外部时钟源。事件同步让内部定时操作与外部事件同步。STM32的定时器主要支持两种外部时钟模式很多人容易混淆这里彻底讲清楚。3.1 外部时钟模式1最常用的“信号计数器”这是最直观、最常用的模式。其信号路径是外部引脚 - 边沿检测器 - 触发控制器 - 定时器计数器。配置要点时钟源选择必须将定时器的从模式控制器配置为“外部时钟模式1”。TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0); // 或者使用更通用的函数先配置ETR再设置触发源 TIM_ETRConfig(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0); TIM_SelectInputTrigger(TIM2, TIM_TS_ETRF); // 选择ETR作为触发源 TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_External1); // 设置从模式为外部时钟模式1引脚映射不同的定时器其外部触发输入引脚是固定的。例如TIM2的ETR引脚是PA0部分型号可能是PA15需查数据手册。你需要将该引脚配置为复用功能并使能对应GPIO的时钟。滤波器与分频TIM_ETRConfig函数中的参数可以设置对输入信号的预分频和滤波。这对于消抖或处理高频信号非常关键。工作过程一旦配置好外部引脚如PA0上的每一个上升沿或下降沿可配置都会使定时器的计数器加1。此时定时器的计数频率完全等于外部信号的频率。自动重装载寄存器ARR仍然有效当计数器计到ARR值时同样会产生更新事件和中断。典型应用旋转编码器脉冲计数、光电门通过物体计数。你不需要关心外部信号的具体频率只需要知道“来了多少个脉冲”。3.2 外部时钟模式2被忽略的“门控触发器”模式2的路径不同外部引脚 - 从模式控制器 - 使能/禁用定时器计数器。配置要点TIM_SelectInputTrigger(TIM2, TIM_TS_TI1F_ED); // 选择TI1边沿检测作为触发源 TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_Trigger); // 从模式设为触发模式 TIM_SelectMasterSlaveMode(TIM2, TIM_MasterSlaveMode_Enable); TIM_ETRConfig(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0); // 通常不使用ETR // 注意模式2通常使用TI1或TI2作为触发源而非ETR。更常见的用法是使用通道1或2的输入作为触发源实现门控。例如设置TI1为触发源从模式为门控模式TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; // 高电平时计数 TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x00; TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_SelectInputTrigger(TIM2, TIM_TS_TI1FP1); // 选择滤波后的TI1作为触发源 TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_Gated); // 从模式设为门控模式 TIM_Cmd(TIM2, ENABLE);工作过程在这种模式下外部引脚的电平控制着定时器计数器的启停。例如当TI1引脚为高电平时定时器正常计数当TI1变为低电平时定时器暂停计数。ARR依然有效。典型应用测量一个高电平脉冲的宽度。你可以启动定时器用内部高速时钟计数然后用一个外部引脚的高电平作为门控信号。高电平期间计数器的增加值就代表了脉冲宽度。这在红外遥控接收、PWM输入测量等场景非常有用。3.3 模式1 vs 模式2核心区别与选型指南为了更直观地理解我们用一个表格来对比特性外部时钟模式1外部时钟模式2核心功能将外部信号直接作为计数时钟用外部信号控制内部时钟的计数启停计数时钟源外部引脚上的边沿信号本身定时器内部的时钟如72MHz计数器动作每个有效边沿计数器1内部时钟每个周期计数器1仅在门控信号有效时依赖ARR是计到ARR产生更新是计到ARR产生更新典型应用频率较低的事件计数、转速测量高精度脉冲宽度测量、信号门控类比人手动按计数器按一下数字加一。用开关控制电表开关打开电表走字开关关闭电表停止。选型口诀要数外部发生了多少次用模式1要测外部信号持续了多久用模式2。4. 实战用外部时钟模式1实现按键长按与短按识别理解了原理我们通过一个综合案例来巩固。目标使用TIM2的外部时钟模式1将一个外部按键信号作为时钟源。通过计算定时器中断发生的次数即按键按下的次数来区分短按按下立即松开和长按按下保持超过1秒。硬件连接按键一端接GND另一端接PA0TIM2_ETR和一个上拉电阻到3.3V。默认PA0为高电平按下为低电平。软件设计思路将PA0配置为TIM2的ETR引脚采用外部时钟模式1下降沿有效因为按键按下是下降沿。配置定时器的ARR为一个较大的值比如65535我们并不真的指望它溢出而是禁用更新中断。开启另一个定时器如TIM3用内部时钟设置一个1ms的中断。在TIM3的1ms中断里去读取TIM2计数器的值。这个值就代表了从上次读取到现在按键按下的次数下降沿次数。根据这个次数的变化来判断按键状态。关键代码实现// 1. 初始化TIM2为外部时钟模式1 void TIM2_ExternalClock_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; // 开启时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 配置PA0为复用功能推挽输出实际上作为ETR输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 外部上拉配置为浮空输入亦可 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 时基初始化ARR设大PSC为0不分频 TIM_TimeBaseInitStructure.TIM_Period 0xFFFF; TIM_TimeBaseInitStructure.TIM_Prescaler 0; TIM_TimeBaseInitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseInitStructure); // 配置外部触发模式 TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_Inverted, 0); // 下降沿有效 // 注意这里使用TIM_ETRClockMode2Config但第二个参数选择的是时钟分频对于模式1通常用TIM_ETRConfig配合从模式设置更清晰。 // 更推荐的清晰配置方式 TIM_ETRConfig(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_Inverted, 0); // ETR下降沿有效无分频滤波 TIM_SelectInputTrigger(TIM2, TIM_TS_ETRF); // 选择ETR作为触发源 TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_External1); // 明确设置为外部时钟模式1 TIM_Cmd(TIM2, ENABLE); // 启动TIM2 } // 2. 初始化一个1ms中断的TIM3用于采样TIM2的计数值 volatile uint32_t g_last_counter 0; volatile uint32_t g_press_duration_ms 0; volatile uint8_t g_key_state 0; // 0:释放1:短按2:长按 void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { uint32_t current_counter TIM_GetCounter(TIM2); // 如果当前计数器值大于上次的值说明期间有按键动作下降沿 if (current_counter ! g_last_counter) { g_press_duration_ms 0; // 有新的边沿重置持续时间重新计时 g_last_counter current_counter; } else { // 计数器没变说明按键状态稳定 // 假设按键按下时PA0为低电平。这里需要结合GPIO读取来判断是按下还是释放 // 简化逻辑如果检测到过一次边沿变化g_last_counter更新过且PA0持续为低 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_RESET) { g_press_duration_ms; if (g_press_duration_ms 1000) { // 超过1000ms即1秒 g_key_state 2; // 长按 } } else { // 按键释放 if (g_press_duration_ms 0 g_press_duration_ms 1000) { g_key_state 1; // 短按 } g_press_duration_ms 0; } } TIM_ClearITPendingBit(TIM3, TIM_IT_Update); } }代码解析与避坑我们使用了两个定时器TIM2作为“事件计数器”TIM3作为“状态采样器”。TIM_GetCounter(TIM2)直接读取TIM2计数器的当前值。由于TIM2的时钟是外部按键信号所以这个值就是按键按下的累计次数下降沿数。在TIM3的中断里我们比较本次和上次的计数值。如果不同说明按键被按过我们开始计时g_press_duration_ms。如果相同且按键引脚仍是低电平则累加计时判断是否达到长按阈值。关键点直接读取计数器值来判断事件发生比在外部中断里处理更高效尤其适合可能频繁触发或需要计数的场景。而且这种方式避免了在外部中断服务函数中做延时判断是更优的设计。踩坑记录在这个案例中最大的坑在于按键抖动。机械按键在按下和释放的瞬间会产生多个边沿导致TIM2计数器在极短时间内增加多次误判为多次短按。解决方法是在TIM_ETRConfig函数中启用滤波器。例如将最后一个参数滤波器长度设置为一个非零值如0x0F可以滤除短时间内的抖动毛刺。硬件上也可以在按键两端并联一个小电容如104。