
做STM32F103C8T6开发定时器中断几乎是绕不开的一道坎。我见过不少刚入门的朋友照着网上的例程把定时器初始化代码抄过来编译下载后发现要么中断完全不进要么进了中断程序就卡死要么用示波器一测发现定时时间快了整整一倍。问题出在哪里大多数时候不是代码写错了而是没搞明白F103的时钟树、定时器计数链路和中断响应机制这三者之间的关系。这篇文章我把“stm32f103c8t6 定时器中断”从内部原理到标准库、HAL库两套实现再到实际调试中的排查方法和进阶用法完整捋一遍适合已经能点灯、想进一步做精确时序控制的开发者阅读。1. 定时器中断到底能干什么从轮询到中断的价值跃迁1.1 轮询方式的局限与痛点先看一个很常见的需求每1ms扫描一次按键每10ms刷新一次数码管每100ms采集一次ADC每500ms翻转一次LED。如果用主循环加delay来做代码大致长这样先delay到1ms扫描按键再delay到10ms刷新数码管。问题是delay是串行阻塞的1ms的delay还没结束10ms的任务就已经到期了。你为了让所有任务都“差不多按时”执行只能把周期缩短用主循环不断判断时间差结果是代码嵌套混乱实时性还差。更麻烦的是主循环里一旦出现较长耗时的操作比如往屏幕写一段数据或者处理一条串口协议其他所有周期任务都会被推迟。对于按键扫描这种容忍几十毫秒抖动的场景还好但对于需要严格间隔的脉冲输出、PWM周期同步、通信超时判断这种方式根本没法用。1.2 中断的“水壶哨子”模型定时器中断的思路完全不同。硬件定时器独立于CPU运行内部计数器按固定时钟频率自增加到设定值后自动归零并产生一个事件。这个事件如果使能了中断就会向CPU发出请求CPU暂停当前任务跳去执行中断服务函数执行完再回到原来断掉的地方。用生活里的例子类比轮询方式烧水你得一直盯着壶看定时器中断等于给水壶装了个哨子水开了它自己响你去关火就行。关键区别在于哨子响之前你该干嘛干嘛不需要把注意力耗在看水上面。定时器的计数时钟来自晶振或内部RC不依赖CPU的执行节奏所以哪怕主循环里在忙着做别的事定时器到点了照样触发中断精确性和实时性都远高于纯软件delay。1.3 典型应用场景清单定时器中断在F103C8T6上能做的事情非常多这里列几个最常见的周期采集传感器数据比如每10ms读一次温湿度或六轴姿态按键消抖、短按长按检测、双击判定作为操作系统的系统心跳tick或者自己实现一套软件定时器心跳指示灯闪烁替代delay阻塞方式配合输入捕获测量外部PWM信号的周期和占空比通信超时判断比如串口帧与帧之间的间隔超时看门狗喂狗在指定周期内重置计数器你会发现这些功能都有一个共同点都需要一个稳定的时间基准而且这个基准不能阻塞主循环。学明白定时器中断后面做外部中断、输入捕获、PWM输出、编码器测速都会顺很多它是F103里最核心的外设之一。2. 先搞清楚内部机制从时钟树到NVIC的完整链路2.1 F103C8T6上有哪些定时器F103C8T6属于中等容量型号片内可用的定时器主要是TIM1、TIM2、TIM3、TIM4另外还有TIM6和TIM7。很多刚接触的人以为所有定时器都一样其实它们的定位差别很大定时器类型挂在哪条总线主要特点TIM1高级控制定时器APB216位支持PWM互补输出、死区控制、刹车输入TIM2/TIM3/TIM4通用定时器APB116位支持输入捕获、输出比较、PWM、编码器接口TIM6/TIM7基本定时器APB116位只能定时没有外部IO引脚实际项目里TIM2、TIM3、TIM4是最常用的。它们既能做普通定时中断也能输出PWM、测量外部信号是一个综合性的“多功能工具”。TIM1因为带互补输出和死区通常用在电机控制这类需要三相全桥驱动的场合日常点灯和采集用不上。TIM6/TIM7结构最简单只提供时基功能适合纯粹做周期中断的极简场景。每个通用定时器内部的核心部件就三个预分频器PSC、计数器CNT、自动重载寄存器ARR。PSC把定时器时钟分频成计数频率CNT按这个频率往上加加到ARR的值就归零并产生更新事件。理解这三者的关系是配置定时中断的关键。2.2 定时器时钟从哪里来最容易算错的一环F103C8T6的系统时钟默认是72MHz由外部8MHz晶振经过PLL锁相环倍频得到。系统时钟经过AHB预分频后给各总线APB1上的外设最大只能跑36MHzAPB2上的外设可以跑72MHz。关键点来了TIM2、TIM3、TIM4都挂在APB1总线上但它们的时钟不是直接等于APB1的外设时钟。F103的设计是如果APB1预分频系数不等于1那么定时器时钟会强制翻倍。默认配置下APB1分频系数为2APB1外设时钟为36MHz而挂在APB1上的TIM2、TIM3、TIM4实际拿到的是72MHz。APB1预分频系数APB1外设时钟APB1定时器时钟136MHz36MHz2、4、8、1636MHz的一半、四分之一等两倍于APB1时钟举个例子如果SystemInit之后APB1是36MHz有人按36MHz去算PSC和ARR想配置1ms结果实际定时器时钟是72MHz最终定时时间只有0.5ms。这就是示波器上一测发现在翻倍闪的原因。所以写代码前先确认你的定时器时钟到底是多少再往下配。2.3 定时时间的计算公式与实例定时中断的时间由两个寄存器决定预分频PSC和自动重载ARR。它们的关系是这样的T (PSC 1) × (ARR 1) / TIMxCLK意思很直白PSC1决定了计数器每增加1需要几个时钟周期也就是计数频率ARR1决定计数器要跑多少个数才溢出一次。两者相乘再除以时钟频率就是一次完整中断的周期。以定时器时钟72MHz为例想得到1ms的中断周期先让计数频率变成1MHz需要PSC 72 - 1 71此时每个计数tick是1微秒。再让计数器累加1000个数后溢出需要ARR 1000 - 1 999。这样CNT从0加到999正好1000微秒也就是1ms。如果你把PSC设成0计数频率就是72MHz想定1ms需要ARR 71999这个值小于65535其实也能用。但为什么不推荐因为PSC越小计数分辨率越高但ARR也相应变大对于固定1ms周期PSC71是更稳定的中间选择。理解PSC和ARR的配合逻辑比死记配置代码重要得多。2.4 更新事件如何变成一次中断定时器计数溢出后会做什么取决于你开了哪些开关。首先计数器溢出时SR状态寄存器里的UIF位会被硬件置1这就是更新事件标志。UIF置上之后如果TIMx_DIER寄存器里的UIE中断使能位也为1定时器就会发出一个中断请求信号。这个请求信号接着送到NVIC。NVIC是Cortex-M3内核的中断控制器它负责两件事这个中断在系统里有没有被使能以及它的优先级能不能打断当前正在执行的代码。如果NVIC里对应的中断通道是关着的定时器请求再强烈也没用程序也不会跳走。配置NVIC时要设置中断通道号、抢占优先级和子优先级这一层往往被初学者忽略。当NVIC允许响应后CPU会跳转到启动文件中预先定义好的中断向量执行对应的中断服务函数。标准库里TIM2的中断服务函数名是TIM2_IRQHandler函数名必须和启动文件里的向量表严格一致否则编译能过但中断触发后会跑飞。到这里一条完整的链路才算闭合定时器溢出产生事件SR置位经过中断使能、NVIC使能最终CPU进入你的处理函数。3. 标准库与HAL库两种实现方式的完整代码拆解3.1 标准库还是HAL库怎么选F103C8T6的资料和例程几乎都是标准库和HAL库两套并行。标准库封装得比较薄函数名直接对应寄存器操作代码编译出来体积小运行效率高调试时能直观看到每个寄存器状态适合理解原理。HAL库封装层更厚提供了MSP初始化和回调机制从别的芯片型号迁移代码更方便但二进制体积偏大函数调用层级多底层细节被隐藏出问题后不好定位。我的建议是如果目的是搞清楚定时器到底怎么工作先玩标准库如果是要快速搭一个产品原型或者习惯了HAL库的工程结构直接用HAL库也没问题。两者实现同一个1ms定时中断差异主要在初始化流程和中断处理方式上下面分别给完整代码。3.2 标准库实现一步步配置标准库的初始化流程大概是开定时器时钟配置时基结构体配置NVIC使能更新中断最后启动定时器。以TIM2为例#include stm32f10x.h void TIM2_Init_Std(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1. 开启TIM2时钟TIM2挂在APB1总线上 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 2. 配置时基72MHz计数时钟PSC71得到1MHzARR999得到1ms TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 向上计数 TIM_TimeBaseStructure.TIM_Period 999; // ARR TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 3. 配置NVIC中断通道 NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 4. 清一次标志位避免历史遗留的UIF导致一开中断就立刻进入 TIM_ClearFlag(TIM2, TIM_FLAG_Update); // 5. 使能更新中断然后启动定时器 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); }每一步都有明确目的第1步不开时钟写寄存器没有任何反应第2步决定中断周期数值怎么算参考上一节第3步是很多人漏掉的关键只配置定时器不配NVIC中断根本进不来第4步清一次标志能保证启动瞬间的干净第5步里TIM_ITConfig和TIM_Cmd是两回事前者开中断请求后者真正让计数器跑起来。3.3 标准库中断服务函数的写法定时器启动后每当计数溢出就会进入TIM2_IRQHandler。标准库推荐在函数里先判断中断标志然后处理逻辑最后清标志void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { // 先清标志防止处理逻辑期间再次触发 TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 这里放你的周期任务比如翻转PB0用于示波器验证 GPIO_WriteBit(GPIOB, GPIO_Pin_0, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_0))); } }关于清标志的时机标准库例程里有先处理再清、也有先清再处理的。我的实践是如果处理逻辑很短两种都行如果逻辑比较长比如有多个传感器读取建议先清标志再处理逻辑这样即便处理过程超过一个周期也不会因为标志未清导致中断反复重入时序不会无限制地漂移。3.4 HAL库实现从初始化到回调HAL库的写法分层更清晰。定时器句柄定义、时基初始化、中断回调分别在几个不同的函数里TIM_HandleTypeDef htim2; void TIM2_Init_HAL(void) { __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance TIM2; htim2.Init.Prescaler 71; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 999; htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_DISABLE; HAL_TIM_Base_Init(htim2); // 启动定时器并开启更新中断 HAL_TIM_Base_Start_IT(htim2); } // HAL_TIM_Base_Init内部会调用这个MSP回调NVIC配置放在这里 void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn); } }中断函数写在中断向量文件里通常命名为stm32f1xx_it.c它本身只调HAL库的中断入口函数void TIM2_IRQHandler(void) { HAL_TIM_IRQHandler(htim2); }具体的周期任务放到回调函数里void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { // 1ms周期的业务逻辑写在这里 } }HAL库的启动顺序里HAL_TIM_Base_Start_IT是开启中断模式运行的开关如果误调用成HAL_TIM_Base_Start定时器虽然也在跑但不会产生中断这一点在实际开发中非常容易踩。3.5 两套库使用中最常见的配置错误标准库最容易出的错误是NVIC_IRQChannel写错通道号以及启动文件选错。F103C8T6是中等容量芯片启动文件要用startup_stm32f10x_md.s如果用成ld或者hd的向量文件中断函数名对不上编译能过但进不了中断。HAL库最容易出的错误是忘记在自己的MSP回调里配NVIC或者中断回调里忘了判断htim-Instance导致一个定时器的事件被当成另一个定时器的事件处理。另外无论是标准库还是HAL库开定时器时钟这一步都不要省。有些时候你觉得“代码明明照着文档写的”但定时器没反应第一件事永远是检查RCC对应位有没有置1。4. 中断服务函数里最容易踩的坑标志位、回调与嵌套4.1 标志位没清引发的“中断风暴”定时器中断最常见也最“致命”的现象是程序一运行看起来像是卡死了主循环完全不动单步调试时发现一直在中断服务函数里打转。罪魁祸首基本就是UIF标志位没清。原理是这样的计数溢出后SR寄存器的UIF变成1只要这个位不归零中断请求信号就会一直保持有效。CPU每响应一次中断返回主程序之前检查到请求还在又立刻跳进中断函数形成一个无限循环。主循环在这种疯狂的中断请求下根本得不到执行机会表现就是“死机”。所以中断服务函数里一定要调用TIM_ClearITPendingBit或者HAL库内部对应的清标志操作在中断末尾或开头把UIF清零。标准库中如果先判断标志再清一旦判断和处理中间又发生了新的溢出UIF会被再次置位处理结束后会再进一次中断这种偶发性重复触发在周期精度要求高的场景中会有影响。我的做法是进入中断后立刻清标志把实际业务放到清标志之后让每次中断事件尽量干净。4.2 不要在中断里做耗时操作中断服务函数应该保持“短小精悍”。如果在中断里调用库函数printf往串口打印调试信息一个1ms周期任务可能被执行几十毫秒期间其他定时器中断、外部中断全部被拖住主循环也基本停摆。更严重的如果中断里用了带阻塞等待的延时函数理论上可能造成不可预测的响应延迟。推荐的模式是“中断置位标志主循环处理业务”。例如在1ms中断里只把递增计数和置位标志做掉volatile uint8_t flag_1ms 0; volatile uint32_t tick_1ms 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); tick_1ms; flag_1ms 1; } } int main(void) { while (1) { if (flag_1ms) { flag_1ms 0; // 耗时任务放到这里做 } } }这个模式本质上是把定时器当成精确的“闹钟”到点了只把消息放到邮箱主循环空闲时再取出来处理。用在这种模式下中断函数里只有几条指令既保证了周期性又不影响主循环整体的响应速度。4.3 中断优先级与嵌套的影响Cortex-M3的NVIC支持抢占优先级和子优先级两级调度。F103可以分成最多4个优先级组比如最常用的分组2就是2位抢占优先级、2位子优先级。抢占优先级数值越小越优先高抢占优先级的中断可以打断低抢占优先级的中断形成嵌套相同抢占优先级的中断不能互相打断只能按子优先级排队。实际项目中如果多个定时器同时开中断优先级设计不合理会出现时序抖动。比如TIM2和TIM3都开了1ms中断抢占优先级又完全相同当一个中断正在执行时另一个中断请求只能挂起等待处理时间会多出几微秒到几十微秒。如果某个外设对采样间隔要求极严格这种抖动就不可忽略。我的建议是把要求最严格的任务放到最高的抢占优先级给一个0普通周期任务给2或3。这样可以保证关键时基不被其他逻辑干扰。4.4 HAL库回调函数的“多源分流”HAL库把所有定时器更新中断统一汇总到HAL_TIM_PeriodElapsedCallback这一个回调函数里。如果你的工程里同时用了TIM2和TIM3的更新中断回调函数里必须判断当前是哪个定时器产生的void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { // TIM2的周期任务 } else if (htim-Instance TIM3) { // TIM3的周期任务 } }同理一个定时器内部的不同事件比如更新中断和输入捕获中断最终也会汇聚到不同的回调函数HAL_TIM_IC_CaptureCallback负责捕获事件这个函数内部同样要区分通道。如果不做分流某个定时器的事件会误触发另一个定时器的业务逻辑排查起来相当隐蔽。HAL库内部的中断入口函数HAL_TIM_IRQHandler会读取状态寄存器并清除对应标志然后才调用回调所以在回调里不需要也不应该再手动清一次定时器标志清重复了反而可能把状态搞乱。4.5 共享变量的volatile声明中断服务和主循环共享的变量比如上面代码里的tick_1ms和flag_1ms必须加volatile修饰。不加的话编译器在开启优化后可能把普通变量缓存在寄存器里主循环每次读到的都是旧值中断里明明已经更新了主循环却“看不到”。这个问题的本质是volatile告诉编译器“这个变量可能在外部被修改所有读操作都要从内存重新取不要做寄存器缓存优化”。对于中断共享的计数、标志、缓存数组下标这条规则都要遵守否则排查起来会怀疑人生。5. 实测中的定时误差与排查思路从示波器到逻辑分析仪5.1 定时误差的来源定时中断的周期真的就是配置里的那个数吗不一定。首先是时钟源本身有精度误差F103常用的8MHz晶振精度通常在20ppm到50ppm也就是说1秒钟会偏差20到50微秒对大多数应用足够但拿来做长时间定时累加就不太合适。其次是配置理解错误比如前面说的APB1定时器时钟翻倍问题会让实际时间缩半。最后是中断处理本身带来的相位抖动中断请求发出到CPU真正进入服务函数有几微秒的响应延迟如果前面有一个高优先级中断正在执行这个延迟会更大。现象实际定时时间可能原因按72MHz算定时1ms实测0.5ms0.5ms实际定时器时钟按36MHz理解或配错定时1ms实测2ms2msPSC或ARR按2倍设了比如ARR设成1999定时1ms偶发多进中断周期不稳中断里清标志太晚或优先级冲突5.2 GPIO翻转法验证定时精度判断定时中断准不准靠肉眼闪LED不靠谱最直接的办法是用一个GPIO翻转在中断里每次翻转一次输出电平然后用示波器或逻辑分析仪测量方波的周期。翻转一次代表两个中断周期所以实际中断周期等于测量到的方波周期除以2。测试代码只要在刚才的TIM2_IRQHandler里加一行GPIO翻转然后接好逻辑分析仪看波形。如果配置的是1ms定时PB0输出方波的周期应该是2ms也就是频率500Hz。如果测出来是1ms说明定时时间实际只有0.5ms基本可以断定是时钟源算错了如果测出来是4ms那就要检查ARR是不是多设了1000。逻辑分析仪的优势是能长时间记录方便观察是否有漏中断或额外多出的脉冲这是单纯看LED闪烁完全做不到的。5.3 定时器不工作的标准排查链路遇到定时器中断不工作不用乱猜按下面的链路一步步查绝大多数问题都能定位查RCC时钟有没有开启查定时器有没有启动调试模式下看CNT计数器是否在走看SR里的UIF有没有置位查NVIC中断通道有没有使能查中断服务函数名和启动文件是否匹配进中断后有没有清标志第一步到第四步用调试器很容易确认。程序停在断点处打开外设寄存器视图一边全速运行一边观察CNT如果CNT从0涨到ARR然后归零说明定时器硬件本身在工作。第四步如果UIF置位却没有进中断函数问题出在NVIC这一层如果根本看不到UIF说明计数器没跑起来回去查时钟和定时器使能。第六步很多人会忽略特别是自己手动建工程时启动文件移植出错症状和NVIC关闭一样。5.4 偶发早触发或漏触发的排查思路定时中断偶尔早一点、偶尔晚一点这种问题最让人头疼。原因往往不在定时器本身而在代码的全局影响。比如某个中断服务函数里花了几十微秒做浮点运算这个时间段里定时器又有溢出事件产生低优先级的定时器中断就只能等表现就是这次中断比预期晚来几十微秒。另外如果代码里某个驱动函数不小心修改了定时器的PSC或ARR寄存器也会导致周期突变。解决思路是缩小嫌疑范围先把所有其他外设中断关闭只保留定时器中断看现象是否消失如果消失就把其他中断逐个打开找出优先级冲突的根源。还可以在中断函数入口加一个GPIO翻转测量实际中断响应到执行之间的延迟用数字一对比就能看出哪个中断在捣乱。6. 多定时器协同与进阶用法从1ms心跳到软件定时器架构6.1 想定1秒以上怎么办溢出累加与低频计数F103的定时器计数器是16位ARR最大只能写到65535。如果计数频率仍然是1MHz单次最大定时只能到65.535ms想直接配ARR来定时1秒是不可能的。有两种绕过办法。第一种是降低计数频率比如把PSC设成7199计数频率变成10kHz一个计数tick是0.1msARR设成9999时定时就是1秒精度能到0.1ms量级。第二种是维持1MHz计数频率在1ms中断里用软件累加计数累加到1000就是1秒volatile uint32_t ms_count 0; volatile uint32_t seconds 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); ms_count; if (ms_count 1000) { ms_count 0; seconds; } } }第二种方式更通用因为1ms是整数基准后面想做500ms、3秒、10秒都方便只要用ms_count模上对应数值就行。第一种方式的优点是不占用中断次数但扩展性差一些。6.2 基于1ms心跳的软件定时器雏形系统里经常需要多个不同周期的任务不可能开一堆硬件定时器。一个很实用的做法是一个硬件定时器提供1ms心跳软件里维护多个计数变量每个变量到期后置一个标志位volatile uint32_t tick_1ms 0; volatile uint32_t timer_10ms_count 0; volatile uint8_t flag_10ms 0; volatile uint32_t timer_500ms_count 0; volatile uint8_t flag_500ms 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); tick_1ms; if (timer_10ms_count 10) { timer_10ms_count 0; flag_10ms 1; } if (timer_500ms_count 500) { timer_500ms_count 0; flag_500ms 1; } } }主循环里轮询这些标志位各自执行对应任务。这套结构本质上就是最简版本的软件定时器调度实际项目中可以再封装成一个结构体数组每个定时器记录周期、当前计数和到期回调使用体验和OS里的软件定时器很接近但代码量小得多非常契合F103C8T6这种资源有限的芯片。6.3 用定时器中断配合输入捕获测PWM定时器中断还能配合输入捕获做更复杂的测量。比如用TIM2的CH1捕获外部PWM的上升沿每次捕获事件到来时读CCR1寄存器的值相邻两次捕获值的差值乘以计数频率的倒数就是PWM周期。这个过程中如果两次捕获之间计数器发生了溢出直接用差值算出来的结果就是错的必须靠更新中断里的溢出次数来修正所以输入捕获和更新中断经常会同时打开。具体思路是更新中断记录共溢出了多少个ARR1个计数捕获中断读取当前计数值两者拼出一个完整的、不歧义的时间戳。这个方法能粗略测量PWM频率和占空比读出来之后还能继续做调速控制或信号分析是定时器从“定时”走向“测量”的典型进阶用法。6.4 多定时器分工与优先级分配参考一个工程里多个定时器各司其职是很常见的。以一个小型电机控制带按键交互的项目为例可以这样分配定时器职责中断优先级说明TIM21ms系统心跳软件定时器基座抢占0最高优先级保证时间基准稳定TIM3输入捕获测编码器或PWM反馈抢占1需要及时读取捕获值TIM4按键扫描和消抖周期抢占2延迟几毫秒无影响这样划分之后高优先级的中断不会被低优先级任务拖累低优先级任务即使偶尔被高优先级打断也不会影响关键时序。优先级分配和任务职责是强相关的不能所有中断都堆在同一个优先级上。这套多定时器协同的思路是F103C8T6从“会点灯”走向“能独立做小系统”的重要一步。我自己在实际项目里习惯把TIM2永远固定成1ms心跳所有周期逻辑都从它派生业务代码只认软件定时器接口硬件定时器寄存器尽量少碰长期维护下来非常省心。如果你刚开始调定时器中断建议先拿GPIO翻转法把中断周期测准确认硬件链路通了再往里面填业务逻辑这个顺序能避开绝大多数入门阶段的地雷。