STM32定时器原理与应用:从基础定时到外部脉冲捕获实战

发布时间:2026/7/30 8:10:22
STM32定时器原理与应用:从基础定时到外部脉冲捕获实战 1. 项目概述从“定时”与“计数”两个维度理解STM32的脉搏如果你刚开始接触STM32可能会觉得定时器Timer是个有点抽象的概念。它不像GPIO点灯那样直观也不像串口通信那样有来有回。但我要说定时器是STM32这类微控制器真正的“心脏”和“节拍器”是构建复杂、精准、实时应用的基础。简单理解定时器就是芯片内部一个可以自动、精确计数的模块。这个“计数”的能力衍生出了两大核心应用场景定时和计数。所谓“定时”就是让定时器按照我们设定的固定时间间隔产生一个信号比如中断告诉我们“时间到了”。这就像你设置了一个闹钟每隔一小时响一次提醒你该休息了。在STM32里我们可以用这个“闹钟”来周期性地执行某个任务比如每1毫秒采集一次传感器数据、每10毫秒刷新一次显示屏、或者每1秒通过串口发送一次系统状态。没有定时器你的程序就只能靠delay函数傻等效率极低且无法处理其他事情。而“计数”则是让定时器去数外部发生的事情。比如连接一个光电传感器每通过一个物体就产生一个脉冲定时器就负责数这些脉冲的个数从而实现测速、计件等功能。或者去测量一个外部脉冲信号的频率、占空比这就是输入捕获模式或者产生特定频率和占空比的波形这就是PWM输出模式。所以定时器不仅仅是“定时”它更是一个强大的“事件处理器”和“信号发生器”。这次我们聚焦于STM32标准库Standard Peripheral Library环境下的定时器基础应用。标准库虽然已不是ST主推的HAL/LL库但对于理解硬件原理、学习嵌入式底层知识而言它依然是无可替代的经典。它的寄存器封装直观代码流程清晰能让你清楚地知道每一个配置项最终是如何落实到寄存器上的。很多遗留项目、教学资料以及追求极致可控性的场合依然在大量使用标准库。掌握了标准库的定时器再去看HAL库你会觉得豁然开朗。2. 定时器整体架构与核心思路解析STM32的定时器家族非常庞大从基本定时器TIM6, TIM7通用定时器TIM2-TIM5, TIM9-TIM14到高级定时器TIM1, TIM8功能由简到繁。对于入门“定时”和“计数”我们通常从通用定时器入手比如最常用的TIM2、TIM3、TIM4。它们的结构是理解所有定时器功能的基础。你可以把一个通用定时器想象成一个带有精密齿轮和指针的机械钟表但它是数字化的且功能强大得多。其核心部件和工作流程如下核心时钟源定时器要“走时”首先得有“动力”。这个动力就是时钟源。最常见的是来自系统内部的APB总线时钟。这里有个关键点当APB预分频系数不为1时比如系统时钟72MHzAPB1分频后是36MHz定时器的时钟会倍频x2以确保定时器有足够的精度。例如在常见的72MHz系统时钟、APB1预分频系数为2的配置下挂载在APB1上的TIM2~TIM4实际得到的时钟是72MHz而不是36MHz。这一点在计算定时时间时至关重要标准库函数RCC_GetClocksFreq可以帮助我们获取准确的时钟频率。时基单元这是定时器的“心脏”决定了计数的节奏。它主要由三个寄存器构成预分频器PSC这是一个16位的分频器可以对输入的时钟源进行分频。PSC的值可以是0~65535。如果CK_PSC是72MHzPSC设为7199那么分频后的时钟CK_CNT CK_PSC / (PSC 1) 72MHz / 7200 10KHz。这个CK_CNT就是计数器实际累加的频率。计数器CNT这是一个16位或32位如TIM2的寄存器在CK_CNT的每个脉冲下进行加1或减1计数。它是定时器当前计数值的实时反映。自动重装载寄存器ARR这也是一个16位/32位的寄存器它设定了计数器的周期。在向上计数模式下当CNT的值从0累加到ARR时就会产生一个“更新事件”并且CNT会被硬件自动清零重新开始计数。ARR的值决定了定时器产生周期性事件的频率。定时时间的计算这是最核心的公式。定时器产生一次更新事件比如中断的时间T (ARR 1) * (PSC 1) / Tclk。Tclk定时器时钟源频率如72MHz。PSC预分频器值。ARR自动重装载值。 例如要实现1ms的定时中断Tclk72MHz 设PSC71则CK_CNT 72MHz / (711) 1MHz即每个计数周期是1us。要实现1ms就需要计数1000次所以ARR 1000 - 1 999。代入公式T (9991)*(711)/72MHz 1000*72/72MHz 0.001s 1ms。计数模式主要分为向上计数、向下计数和中央对齐计数。最常用的是向上计数即CNT从0开始每个CK_CNT脉冲加1直到ARR后溢出归零同时产生更新事件。中断与事件当计数器溢出更新事件时可以触发一个“更新中断”。我们需要在NVIC嵌套向量中断控制器中配置这个中断的优先级并在中断服务函数里编写我们想要周期性执行的代码比如翻转一个LED灯。注意在标准库中修改PSC或ARR的预装载值即运行中改变定时周期时需要小心处理。新的值会在下一个更新事件时才生效。如果想立即生效可以手动产生一个软件更新事件TIM_GenerateEvent(TIMx, TIM_EventSource_Update)或者在初始化后先设置TIMx-EGR寄存器的UG位。3. 标准库定时器配置的核心细节与实操要点理解了架构我们来看如何用标准库的代码一步步实现它。标准库的配置遵循一个清晰的“初始化结构体 - 配置参数 - 使能”的流程。这里我们以实现一个1ms定时中断为例详细拆解每一个步骤。3.1 时钟使能打开定时器的电源开关任何外设使用前必须先开启其时钟。STM32的功耗控制很严格默认外设时钟是关闭的。// 假设我们使用 TIM3 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);这一步相当于给TIM3这个模块通了电。没有这一步后续的所有配置都不会生效。务必确认你使用的定时器挂在哪个总线APB1还是APB2上。TIM2~TIM7在APB1TIM1、TIM8、TIM9~TIM11等在APB2。3.2 时基单元初始化设定定时器的“心跳”这是配置的核心对应我们上面讲的PSC、ARR、计数模式等。TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; // 1. 自动重装载值决定计数周期 TIM_TimeBaseInitStructure.TIM_Period 1000 - 1; // ARR 999 // 2. 预分频器决定计数频率 TIM_TimeBaseInitStructure.TIM_Prescaler 72 - 1; // PSC 71 // 3. 时钟分频因子用于数字滤波通常不分频 TIM_TimeBaseInitStructure.TIM_ClockDivision TIM_CKD_DIV1; // 4. 计数模式向上计数 TIM_TimeBaseInitStructure.TIM_CounterMode TIM_CounterMode_Up; // 5. 初始化定时器 TIM_TimeBaseInit(TIM3, TIM_TimeBaseInitStructure);参数详解与避坑TIM_Period这就是ARR。如果你想要计数器从0计数到N那么这里就填N。注意它存储的是“计数值上限”所以如果我们要计1000个数0~999这里就填999。TIM_Prescaler这就是PSC。计算时是(PSC1)分频。我们的目标是CK_CNT 72MHz / 72 1MHz所以PSC 71。TIM_ClockDivision这个与定时器输入捕获功能中的数字滤波器有关用于在外部时钟源模式下降低采样频率以抗干扰。在单纯的内部时钟定时模式下设置为TIM_CKD_DIV1即可表示不分频。TIM_CounterMode除了TIM_CounterMode_Up还有Down向下和CenterAligned中央对齐用于高级PWM。一个关键技巧如何验证你的定时计算是否正确你可以在初始化完成后通过TIM_GetPrescaler()和TIM_GetAutoreload()函数读回配置的值或者在调试模式下直接查看TIMx-PSC和TIMx-ARR寄存器的值。更直观的方法是在中断服务函数里翻转一个GPIO然后用示波器或者逻辑分析仪测量这个引脚波形周期看是否是预期的1ms。3.3 中断配置让定时器“开口说话”配置好时基定时器已经能周期运行了但它还不会通知CPU。我们需要开启它的更新中断并配置NVIC。// 1. 使能TIM3的更新中断 TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); // 2. 配置NVIC嵌套向量中断控制器 NVIC_InitTypeDef NVIC_InitStructure; // 设置中断分组整个系统通常只设置一次例如在主函数开头 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2位抢占2位响应 // 配置TIM3中断通道 NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; // 中断号 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; // 子优先级 NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure);中断优先级理解NVIC_PriorityGroup_2表示将4位优先级位分成2位抢占优先级和2位响应优先级。抢占优先级高的可以打断低的响应优先级用于同时发生中断时的排队顺序。对于简单的定时中断优先级可以设得低一些数字大优先级低避免影响更紧急的中断如外部紧急按键。3.4 编写中断服务函数定时到了做什么这是用户代码的入口。函数名是固定的在启动文件startup_stm32f10x_xx.s中有定义。void TIM3_IRQHandler(void) { // 1. 检查是否是更新中断标志 if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { // 2. 清除中断标志位非常重要否则会连续进入中断 TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 3. 用户代码区这里写每隔1ms要执行的任务 // 例如翻转LED灯 GPIO_WriteBit(GPIOB, GPIO_Pin_5, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_5))); // 或者让一个软件计数器累加 static uint32_t ms_count 0; ms_count; if (ms_count 1000) { // 每1000ms即1秒执行一次 ms_count 0; // 执行每秒任务比如打印信息 // printf(One second passed.\r\n); } } }致命陷阱务必清除中断标志位TIM_ClearITPendingBit这行代码绝对不能少。硬件在产生中断时会置位一个标志位。CPU响应中断后必须由软件手动清除这个标志否则中断会一直挂着导致CPU反复进入中断服务函数程序就卡死在这里了。这是新手最容易犯的错误之一。3.5 启动定时器让心脏开始跳动最后一步启动定时器的计数器。TIM_Cmd(TIM3, ENABLE);至此一个完整的1ms定时中断就配置完成了。将以上代码片段按顺序放入你的工程初始化部分通常在main()函数的while(1)循环之前编译下载就能看到LED以1ms的间隔闪烁由于人眼视觉暂留你看到的是常亮或微闪用示波器看才是方波。4. 从“定时”到“计数”外部脉冲计数实战定时器之所以强大在于它不仅能“自己数时间”还能“数外面的事”。这就是计数功能通常使用外部时钟模式或输入捕获模式。这里我们先讲更直接的外部时钟模式用于测量脉冲数量。假设我们要用TIM2的通道1PA0引脚来计数外部按钮按下的次数假设按钮已消抖每按一次产生一个干净脉冲。4.1 硬件与模式理解在这种模式下定时器不再使用内部的CK_PSC时钟而是将一个外部引脚如TIMx_CH1上的信号作为计数时钟CK_CNT。计数器在每个外部脉冲的边沿上升沿或下降沿加1。ARR寄存器依然有效当计数到ARR时会产生更新事件。4.2 标准库配置步骤配置流程和定时类似但关注点不同。// 1. 开启时钟TIM2和GPIOA RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置GPIO PA0 为浮空输入或上拉输入接收外部脉冲 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 浮空输入适用于外部已有确定电平 // 或者 GPIO_Mode_IPU (上拉输入)如果外部是开集输出内部上拉到高电平 GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 初始化时基结构体此时PSC和ARR的意义有所变化 TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_TimeBaseInitStructure.TIM_Period 65535; // ARR设为最大值因为我们想数很多次避免频繁溢出 TIM_TimeBaseInitStructure.TIM_Prescaler 0; // 对外部时钟不分频1个脉冲计1个数 TIM_TimeBaseInitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseInitStructure); // 4. 关键配置定时器为外部时钟模式 TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0); // 参数解释 // TIM_ExtTRGPSC_OFF: 外部触发预分频关闭即不对输入信号分频。 // TIM_ExtTRGPolarity_NonInverted: 上升沿有效。如果改为TIM_ExtTRGPolarity_Inverted则是下降沿有效。 // 最后一个参数是外部触发滤波0表示不滤波。 // 另一种更常用的模式是将通道配置为输入并选择TI1作为时钟源 // TIM_TIxExternalClockConfig(TIM2, TIM_TIxExternalCLK1Source_TI1, TIM_ICPolarity_Rising, 0); // 这种方式更灵活可以选择不同的通道和边沿。 // 5. 如果需要可以开启更新中断在计数到ARR时做处理例如清零计数器并记录溢出次数 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // ... 配置NVIC同上略 // 6. 启动定时器计数器 TIM_Cmd(TIM2, ENABLE);4.3 读取计数值与数据处理配置完成后计数器CNT就会随着PA0引脚上的每个上升沿自动加1。我们在主循环中可以定期读取这个值。uint16_t pulse_count 0; uint32_t total_count 0; uint16_t overflow_times 0; // 溢出次数 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); overflow_times; // 记录溢出次数 } } int main(void) { // ... 初始化代码 ... while(1) { // 读取当前计数值 pulse_count TIM_GetCounter(TIM2); // 计算总脉冲数 溢出次数 * (ARR1) 当前计数值 total_count (uint32_t)overflow_times * 65536UL pulse_count; // 将total_count显示到数码管、LCD或通过串口发送 // ... Delay_ms(100); // 每100ms读取一次避免过于频繁 } }注意事项抗干扰外部脉冲计数对噪声敏感。如果信号有毛刺会导致误计数。除了硬件上增加RC滤波电路软件上可以利用定时器的输入滤波和边沿检测功能。上面配置中的TIM_ETRClockMode2Config最后一个参数就是设置滤波器的。频率上限外部时钟的频率不能超过定时器内部时钟CK_INT的1/4否则可能无法可靠计数。例如CK_INT72MHz则外部信号最高频率约18MHz。溢出处理对于高速连续计数CNT会很快溢出从65535回到0。必须结合溢出中断来扩展计数范围如上例所示形成一个32位甚至64位的计数器。5. 进阶应用输入捕获测量脉冲宽度与频率“计数”功能的另一个高级应用是输入捕获它可以精确测量外部脉冲的高电平时间、低电平时间、周期和频率。这是实现编码器读数、红外遥控解码、PWM输入分析等功能的基础。其原理是当检测到指定引脚如TIMx_CH1上出现设定的边沿如上升沿时定时器当前的计数值CNT会被瞬间锁存到对应的**捕获/比较寄存器CCR1**中。我们可以读取这个锁存值。通过捕获两个不同边沿如一个上升沿和一个下降沿时的CNT值它们的差值就是这两个边沿之间的时间再乘以计数周期就得到了脉冲宽度。5.1 输入捕获标准库配置详解我们以TIM5的通道1PA0测量高电平宽度为例。// 1. 时钟和GPIO初始化同上略 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; // 下拉输入默认低电平适合测量高电平脉冲 GPIO_Init(GPIOA, GPIO_InitStructure); // 2. 初始化时基单元设定时基决定时间测量的“尺子”精度 TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_TimeBaseInitStructure.TIM_Period 0xFFFF; // ARR最大让CNT自由计数 TIM_TimeBaseInitStructure.TIM_Prescaler 72 - 1; // 72分频CK_CNT 1MHz1个计数1us TIM_TimeBaseInitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM5, TIM_TimeBaseInitStructure); // 3. 初始化输入捕获参数 TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel TIM_Channel_1; // 通道1 TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; // 上升沿捕获 TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; // 映射到TI1输入 TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; // 捕获预分频每个边沿都捕获 TIM_ICInitStructure.TIM_ICFilter 0x0; // 不滤波 TIM_ICInit(TIM5, TIM_ICInitStructure); // 4. 使能捕获中断当上升沿到来时进入中断读取CCR1 TIM_ITConfig(TIM5, TIM_IT_CC1, ENABLE); // 5. 配置NVIC略 // 6. 启动定时器和捕获 TIM_Cmd(TIM5, ENABLE);5.2 中断服务函数与脉宽计算逻辑测量高电平宽度需要捕获一个上升沿和一个下降沿。我们可以设置两个中断或者在同一个中断里切换捕获边沿。volatile uint8_t capture_stage 0; // 捕获阶段0-等待上升沿1-已捕获上升沿等待下降沿 volatile uint32_t capture_rise_val 0; volatile uint32_t capture_fall_val 0; volatile uint32_t high_level_us 0; // 高电平宽度单位微秒 void TIM5_IRQHandler(void) { if (TIM_GetITStatus(TIM5, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM5, TIM_IT_CC1); switch(capture_stage) { case 0: // 第一次捕获上升沿 capture_rise_val TIM_GetCapture1(TIM5); // 读取上升沿时刻的CNT // 切换为下降沿捕获 TIM_OC1PolarityConfig(TIM5, TIM_ICPolarity_Falling); capture_stage 1; break; case 1: // 第二次捕获下降沿 capture_fall_val TIM_GetCapture1(TIM5); // 读取下降沿时刻的CNT // 计算高电平脉宽 if (capture_fall_val capture_rise_val) { high_level_us capture_fall_val - capture_rise_val; } else { // 处理CNT溢出情况差值 (ARR - rise_val) fall_val 1 high_level_us (0xFFFF - capture_rise_val) capture_fall_val 1; } // 切换回上升沿捕获准备下一次测量 TIM_OC1PolarityConfig(TIM5, TIM_ICPolarity_Rising); capture_stage 0; // 这里可以进一步计算频率、占空比等 break; } } // 也要处理更新中断防止CNT溢出导致计算错误略 }计算要点因为CNT是16位的它会从0计数到65535然后溢出归零。在计算两个捕获值的差值时必须考虑溢出。上面的if-else语句就是处理溢出的经典方法。得到计数值差值后乘以计数周期就是时间。我们之前设置了PSC71CK_CNT1MHz所以1个计数对应1us。high_level_us变量的值就是高电平的微秒数。要测量周期可以连续捕获两个上升沿其差值就是周期对应的计数值进而算出频率Freq 1 / (Period * 1us)。5.3 输入捕获的滤波与分频配置结构体中的TIM_ICFilter和TIM_ICPrescaler是两个重要参数ICFilter输入滤波器这是一个数字滤波器通过连续采样来确认一个有效的边沿。其值范围是0x0到0xF。设置越大抗噪能力越强但会引入延迟降低能响应的最高信号频率。对于有噪声的信号如电机编码器适当设置滤波如0x4或0x8非常必要。ICPrescaler输入预分频器可以设置为每N个事件捕获一次。例如TIM_ICPSC_DIV4表示每4个边沿捕获一次。这可以用于降低中断频率测量频率较低的信号时有用但会降低分辨率。6. 常见问题排查与调试技巧实录在实际操作中你几乎一定会遇到定时器不工作、中断进不去、时间不准等问题。下面是我踩过坑后总结的排查清单。6.1 定时器完全不工作无中断计数器不递增时钟未使能这是最常见的原因。反复检查RCC_APBxPeriphClockCmd是否调用且参数是否正确。用调试器查看外设寄存器如果时钟未开启寄存器值可能是0或不可写。定时器未启动确认调用了TIM_Cmd(TIMx, ENABLE)。ARR或PSC设置为0如果ARR0计数器从0到0更新事件会以极高的频率发生可能导致程序异常。PSC0表示1分频。中断未正确配置如果依赖中断检查TIM_ITConfig和NVIC配置是否正确。特别是NVIC分组NVIC_PriorityGroupConfig是否在程序早期统一设置过。6.2 能进中断但定时时间不准时钟源频率算错这是最可能的原因。务必确认系统时钟SYSCLK、APB总线时钟PCLK以及定时器实际时钟TIMxCLK是多少。使用SystemCoreClock变量在标准库中它会在系统时钟初始化后更新或RCC_GetClocksFreq函数来获取准确值。PSC和ARR计算公式错误牢记公式Tout ((ARR1)*(PSC1)) / Tclk。ARR和PSC都是16位寄存器最大值65535不要超限。中断服务函数执行时间过长如果你的中断服务函数里做了非常耗时的操作如浮点运算、大量循环可能导致本次中断还没执行完下一次中断又来了造成“丢失”中断或定时变慢。中断服务函数应尽可能短小精悍只做标志位设置、数据搬运等简单操作复杂的处理放到主循环中根据标志位进行。其他更高优先级中断的干扰如果系统中有更高优先级且执行时间很长的中断它会抢占你的定时器中断导致定时不准。合理规划中断优先级。6.3 输入捕获或外部计数不准确GPIO模式配置错误用于输入的GPIO必须配置为输入模式GPIO_Mode_IN_FLOATING浮空输入、GPIO_Mode_IPU上拉输入、GPIO_Mode_IPD下拉输入。如果配置成了输出模式肯定无法捕获外部信号。信号质量问题使用示波器观察输入引脚上的波形。是否有毛刺边沿是否陡峭电压幅值是否在标准范围内对于STM32高电平要高于2V低电平要低于0.8V信号质量问题会导致误触发。未使用滤波对于有噪声的信号必须配置TIM_ICFilter。可以先设置一个较小的值如0x4试试。信号频率超限外部时钟频率不能超过TIMxCLK/4。例如72MHz下最高约18MHz。超过此频率的脉冲可能无法被可靠计数。未处理计数器溢出在输入捕获计算时间差时或者在外部计数累计时如果CNT溢出而你的代码没有考虑计算结果会完全错误。务必使用溢出计数进行扩展。6.4 调试技巧软件仿真在Keil或IAR的仿真环境下可以单步运行查看定时器控制寄存器CR1、状态寄存器SR、计数寄存器CNT、捕获比较寄存器CCRx的值是否按预期变化。这是理解定时器工作过程的最佳方式。硬件调试与GPIO翻转在中断服务函数的开头和结尾或者关键逻辑分支处添加一句GPIO_SetBits/GPIO_ResetBits来翻转一个空闲的GPIO引脚。然后用逻辑分析仪或示波器观察这个引脚你可以清晰地看到中断是否被触发、触发的间隔是否准确、中断服务函数的执行时间有多长。这是最直接、最有效的硬件调试方法之一。使用printf调试在中断里通过串口打印关键变量如捕获值、计数值但要注意串口打印本身很耗时会严重影响中断性能和定时准确性只能用于低速调试或偶尔采样且最好使用非阻塞式的串口发送。最后STM32的定时器功能极其复杂和强大本文仅涵盖了最基础的定时、外部计数和输入捕获。还有PWM输出、互补输出、死区插入、编码器接口、主从模式等高级功能等待探索。我的建议是从本文的基础功能扎实做起用一个定时器实现精准的1ms延时用另一个定时器去捕获一个按键的按下时长把这些基础动作练熟。当你彻底理解了时基单元、中断流程和捕获比较寄存器的玩法后再去查阅参考手册中那些更高级的功能章节你会发现它们都是建立在这些基础概念之上的组合与扩展。