资讯动态

STM32 TIM4定时器中断从原理到实战:告别HAL_Delay死等延时

发布时间:2026/10/5 1:10:37 来源:尧图企业网站定制
别的不说先问一句你第一次用STM32点灯的时候是不是用HAL_Delay()让LED闪烁的那时候觉得挺简单对吧但一旦你开始接触按键扫描、数码管动态刷新、多任务轮流处理就会立刻发现HAL_Delay()这种死等延时简直是在浪费CPU。这时候定时器中断就该登场了。本篇就围绕TIM4定时器中断从原理到CubeMX配置、从标准库到HAL库的代码写法、再到实际项目里那些容易踩的坑完整走一遍。适合正在学STM32定时器的新手也适合已经会点灯但还没系统梳理过中断机制的朋友。文章用到的芯片以STM32F103系列为主其他系列思路完全一致只是外设时钟源和中断号略有差异。1. 为什么是定时器中断先搞懂它和延时函数的本质区别1.1 死等延时到底浪费了多少CPUHAL_Delay(500)或者标准库里的Delay(500)本质上是让CPU在一个循环里不断递增一个计数器直到计数值到达目标就退出。这个过程中CPU什么事情都干不了按键按下去没反应串口数据来了也顾不上收整个芯片就像被人按住了暂停键。实测一组数据最能说明问题STM32F103C8T6主频72MHz执行一次空循环大约需要几个时钟周期。如果你在主循环里调用了10次HAL_Delay(50)那就有500毫秒的时间CPU完全被占用。表面上看代码逻辑没问题但只要你需要同时处理按键、显示、通信就会深刻体会到什么叫卡顿。定时器中断就不一样了。TIM4是一个独立的硬件外设它自己计数、自己比较、到点了自己触发中断。CPU只需要在中断发生的那一刻去执行一小段处理函数其余时间都在正常跑主流程。这就好比你在厨房做饭死等延时是你守在锅边什么也不干定时器中断是你定个闹钟闹钟响之前你还能洗菜切菜。1.2 那么多定时器为什么常用TIM4STM32F103系列通常有TIM1到TIM8共8个定时器。TIM1和TIM8是高级定时器带互补输出和死区控制主要用来驱动电机、逆变电源这类需要PWM互补输出的场景。TIM2、TIM3、TIM4、TIM5是通用定时器功能已经非常够用。TIM6和TIM7是基本定时器只有最基础的定时功能连输入捕获和输出比较都没有。TIM4被大量教程和项目选中的原因很朴素TIM2和TIM3经常被PWM输出或者编码器模式占用了TIM4作为剩下功能完整的通用定时器拿来做定时中断、输入捕获、输出比较都很合适。尤其是做四路PWM输出的项目TIM2和TIM3各有四个通道正好分配掉TIM4就专门负责时基比如1ms进一次中断做系统心跳。1.3 72MHz主频下TIM4的时钟来源这是很多人配置定时器时栽跟头的地方。STM32F103的定时器时钟不是直接等于72MHz的。在默认配置下APB1总线的最高频率是36MHz挂在APB1上的TIM2、TIM3、TIM4、TIM5、TIM6、TIM7当APB1预分频系数不等于1时定时器时钟会被强制倍频到72MHz。CubeMX默认生成的代码里APB1分频系数一般是2所以定时器时钟就是72MHz。如果你用的是标准库自己配置时钟务必检查RCC配置里APB1的分频值。很多人的定时时间算出来和实际差一倍九成是这里出了问题。2. 定时器中断的底层逻辑从预分频到更新事件一次说清2.1 计数器的三个核心寄存器所有通用定时器的时基单元都由三部分组成预分频寄存器PSC、自动重装载寄存器ARR、计数器CNT。这三者配合决定了定时器多久触发一次中断。拿大家熟悉的分频概念来类比。PSC预分频器就是把72MHz的时钟信号进行分频比如PSC设为71那实际送给计数器的时钟频率就是72MHz除以72等于1MHz也就是每1微秒CNT就加1。ARR自动重装载寄存器指定了计数器要数到多少才归零。比如ARR设为999那计数器从0数到999共1000次正好耗时1毫秒。每次计数器溢出归零时就会产生一次更新事件更新事件如果开启了中断就会触发TIM4的中断服务函数。用公式表达就是定时时间 (PSC 1) × (ARR 1) ÷ 定时器时钟频率比如72MHz时钟下想要1ms进一次中断可以设PSC71、ARR999即72 × 1000 ÷ 72000000 0.001秒完美对应。2.2 更新事件和更新中断的区别更新事件是定时器溢出时硬件产生的一个脉冲信号它可以触发DMA、可以触发ADC采样、也可以触发中断。更新中断只是这个事件的一种响应方式。这里有个常见的误区很多人以为只要设置了PSC和ARR定时器就会自动产生中断。不对你还得显式地开启更新中断使能位也就是TIM4-DIER | TIM_DIER_UIE以及NVIC里使能TIM4的中断通道。两个开关缺一个中断都不会执行。另一个细节是更新事件还会产生一个标志位UIF。中断服务函数执行完毕后需要在软件里手动将UIF清零。如果忘了清标志中断服务函数就会退出后立刻再次进入表现就是主循环完全被中断占据程序看起来像卡死了。2.3 递增模式与中央对齐模式的对比TIM4可以工作在递增计数、递减计数、中央对齐三种模式。常规定时中断用的是递增模式计数器从0加到ARR溢出归零产生更新事件。中央对齐模式则是计数器先从0加到ARR再从ARR减到0两个方向都产生更新事件所以中断频率是递增模式的两倍。这个模式主要用于PWM死区控制这类特殊场合定时中断基本用不到。刚上手的朋友不要被向上计数向下计数中心对齐搞晕定时中断只需要记住一句话CNT从0数到ARR到顶了产生中断然后重新从0开始数。3. 配置TIM4中断标准库和HAL库两条路线全流程3.1 标准库实现TIM4定时中断的完整步骤标准库虽然老但逻辑直白很适合理解定时器原理。第一步先开时钟TIM4挂在APB1上同时需要开启GPIO时钟的话一并处理RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);第二步配置时基结构体。TIM_TimeBaseStructure里五个字段常用到的是前三个TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Prescaler 71; // 72MHz / 72 1MHz即1us数一次 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 999; // 数1000次溢出周期1ms TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_RepetitionCounter 0; // 仅高级定时器有效通用定时器填0 TIM_TimeBaseInit(TIM4, TIM_TimeBaseStructure);第三步打开更新中断并配置NVICTIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel TIM4_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure);第四步写中断服务函数。注意STM32标准库所有中断函数的名称是启动文件里写死的必须叫TIM4_IRQHandler随便改名的话中断会进不去。函数内部先判断标志位再清标志最后执行自己的逻辑void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM4, TIM_IT_Update); // 这里写你自己的处理代码 } }最后在主函数里调用TIM_Cmd(TIM4, ENABLE)启动定时器。这句话别忘了很多人配好了所有寄存器就是没启动定时器压根不工作。3.2 HAL库实现方式与回调函数机制HAL库的写法和标准库差异巨大但更模块化。CubeMX里的配置界面做得很直白预分频器、计数模式、自动重装载值、中断优先级图形化选一下就生成初始化代码。CubeMX配置TIM4时有个容易被忽略的细节是NVIC Settings标签页。很多人在Parameter Settings里设好了PSC和ARR却在NVIC里面没勾选TIM4 global interrupt的Enabled选项导致生成的代码里根本没有HAL_TIM_Base_Start_IT(htim4)中断自然不会触发。这个勾选动作对应的就是标准库里的NVIC配置和TIM_ITConfig。HAL库的中断服务函数是TIM4_IRQHandler但这个函数里会调用HAL_TIM_IRQHandler(htim4)做通用处理。真正需要你修改的是回调函数HAL_TIM_PeriodElapsedCallback也就是更新事件产生后HAL库帮你调用的钩子函数void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM4) { // 这里写你自己的处理代码 HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); } }回调函数不是必须放在中断服务函数里的你可以在main.c里定义一个这样的函数保持回调名完全一致即可。注意HAL库所有定时器的更新事件回调都是同一个函数名所以一定要在函数内部判断htim-Instance是谁触发的。如果同时用了TIM3、TIM4、TIM5做中断不加判断就会全部挤进同一个回调。还有个常见坑在CubeMX里配置好了也调用了HAL_TIM_Base_Start_IT(htim4)结果发现中断只触发了一次就不再进入。检查一下是不是在回调函数里又调用了HAL_TIM_Base_Start_IT。HAL库的PeriodElapsedCallback是在中断上下文中执行的重复调用启动函数可能导致标志位被清理时机不对表现为后续中断丢失。正确的做法是在主流程里调用一次HAL_TIM_Base_Start_IT之后每次溢出HAL库会自动重新装载并继续触发中断。3.3 寄存器版思路一行代码都不依赖库如果你是从寄存器层面理解STM32的爱好者TIM4中断的最小化代码只需要四步操作寄存器// 1. 开启TIM4时钟RCC的APB1外设时钟使能寄存器 RCC-APB1ENR | RCC_APB1ENR_TIM4EN; // 2. 配置PSC和ARR TIM4-PSC 71; // 预分频 TIM4-ARR 999; // 自动重装载 // 3. 清更新标志位使能更新中断 TIM4-SR ~TIM_SR_UIF; TIM4-DIER | TIM_DIER_UIE; // 4. 使能定时器 TIM4-CR1 | TIM_CR1_CEN;NVIC的配置需要操作NVIC_ISER寄存器TIM4的中断号是30。如果只是实验可以用标准库或者HAL库生成NVIC初始化代码寄存器操作专注在定时器本身的逻辑上。寄存器最大的优势是省掉所有库函数的封装跳转代码执行效率更高而且能强迫你真正理解每个位的作用。缺点是移植性差、可读性因人而异。我的建议是新手至少用寄存器法点亮一次定时器中断哪怕最后项目里不用也能在排查HAL库问题时多一条思路。4. 定时时间的计算与实测误差从哪里来怎么消除4.1 从需求反推PSC和ARR的取值原则拿到一个定时需求比如每50us进一次中断做ADC采集或者每500ms翻转一次LED第一步不是直接填参数而是先算时钟。默认情况下F103的TIM4输入时钟是72MHz。先确定PSC的思路是让计数频率尽量规整。如果直接设PSC0计数器时钟是72MHz那么ARR要等于3599才是50us72 × 3600 259200实际算一下是 3600 / 72000000 50us。看起来也行但问题在于72MHz的计数器时钟意味着CNT的变化粒度是13.8ns除非你有非常精确的相位需求否则把计数频率压低到1MHz或1kHz数值计算起来更直观。我通常的做法是先设PSC让计数器频率为1MHz也就是计数粒度1us然后ARR直接填定时时间对应的微秒数减1。比如定时50us就设ARR49定时1ms就设ARR999定时500ms就设ARR499999。注意ARR是16位的寄存器最大值65535这也就是为什么PSC71时ARR最大只能到65535对应65.535ms。如果你想一次定时500ms必须增大PSC以降低计数频率比如PSC7199得到10kHz计数时钟ARR4999就是500ms。或者更常用的做法让中断每1ms触发一次在主程序里用一个变量累加500次自然就是500ms。后面讲定时器做任务调度的时候会细说这个思路。4.2 实测示例示波器验证中断周期代码写好后用示波器验证是最直接的手段。我这里用TIM4中断翻转一个GPIO引脚观察方波周期来判断定时时间是否准确。测试环境是STM32F103C8T6最小系统板CubeMX配置TIM4PSC71ARR999启用更新中断。GPIOA的Pin0配置为输出。中断回调里只做一件事HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0)。示波器探头夹在PA0上地线接GND出来的波形应该是频率500Hz的方波。为什么是500Hz因为周期1ms中断一次每次翻转一次电平一个完整的方波周期需要两次翻转所以方波频率是中断频率的一半。如果你想验证自己的配置对不对记住这个关系示波器上看到的频率 中断频率 ÷ 2。实测过程中发现如果示波器上看到的频率是1kHz也就是比预期快了一倍那基本可以确定PSC设的是0或者你之前配置过别的定时器改变了APB1分频系数。反过来如果看到250Hz说明中断周期实际是2ms多半是PSC或者ARR里某个值多算了1。4.3 为什么中断里的代码不能写太久这是个非常关键但又容易被忽视的问题。假设你的中断周期是1ms中断服务函数里跑了一段耗时800us的代码看起来好像还能工作但一旦后续又加入了其他中断或者主循环里有临界区操作时序就直接乱了。最直观的问题是中断执行期间如果有其他中断请求到达它们会排队等待。如果TIM4的中断优先级比它们高那么TIM4中断执行完后会再次进入——因为这期间又产生了新的更新事件。极端情况下TIM4的中断服务函数可能连续执行多次主循环完全被饿死。我自己做一个电机控制项目时踩过这个坑。在1ms的TIM4中断里写了PID运算、编码器读取、串口打印调试信息结果中断执行时间达到了2.3ms远超中断周期。表现就是电机转速完全不对串口数据卡顿系统像喝醉了一样。后来把所有调试信息的发送移到主循环中断里只保留必要的时间敏感操作问题立刻消失。如果你需要在中断里处理大量数据合理做法是中断里只置一个标志位或者往环形缓冲区里丢数据主循环检测到标志后再去处理。5. 标准库和HAL库中断框架对比选择困难症的解法5.1 两种框架下的代码结构差异对初学者来说标准库和HAL库最大的区别在于谁在帮你管状态。标准库的逻辑是你自己调用TIM_GetITStatus查标志、自己调TIM_ClearITPendingBit清标志一切都在你掌控之中。优点是流程透明寄存器手册和代码能对应上。缺点是每次写一个新外设都要重复类似的操作流程代码行数偏多。HAL库的逻辑是HAL_TIM_IRQHandler这个统一入口帮你做了标志位判断和清除你的业务代码塞进回调函数就行。优点在于写业务逻辑时不用关心底层寄存器细节缺点是当出问题时你得追着源码层层看对新手来说容易懵。我自己经常遇到的情况是HAL库初始化没问题但中断进不了回调。排查半天发现是CubeMX生成代码里原本应该在main()函数的HAL_TIM_Base_Start_IT被我删掉了或者被注释掉了。标准库里如果你漏了TIM_Cmd(TIM4, ENABLE)一样好排查因为就那一行。HAL库则是分散在多个文件里溯源路径更长。5.2 中断优先级分组的设计考量STM32的NVIC支持中断嵌套优先级分为抢占优先级和子优先级两部分。优先级分组指的是你怎么分配这两部分的位数CubeMX默认用的是Group4即所有位都是抢占优先级没有子优先级。我调试项目时习惯用Group2。为什么因为Group2把优先级分成2位抢占优先级 2位子优先级既能满足嵌套需求又能对同一抢占优先级内的中断做个先后排序。比如TIM4用于时基抢占优先级设为2串口接收中断设为1那串口可以去打断TIM4的中断。反过来如果你希望TIM4绝对准时不被其他中断打扰就把TIM4的抢占优先级设成最高也就是数值最小的那个比如0。这里要特别强调一下STM32的优先级数值越小优先级越高。很多从51单片机转过来的朋友会天然以为优先级3比0高结果配置反了中断嵌套完全乱套。关于优先级分组还有一个容易踩的坑CubeMX里不同的外设配置页面NVIC优先级数值你填了但如果在两个外设上用了相同的抢占优先级那就需要子优先级来区分先后。系统初始化的时候HAL_NVIC_SetPriorityGrouping必须在整个工程里只调用一次。如果在两个地方分别设置了不同的分组方式后设置的会覆盖前者所有中断的编号含义就全变了。5.3 在通用工程中如何规划TIM4的角色实际做项目时TIM4通常承担的是系统心跳角色。做法是配置PSCARR组合让中断周期正好1ms然后在中断回调里累加一个全局变量volatile uint32_t uwTick_ms 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM4) { uwTick_ms; } }每过1msuwTick_ms就加1。主循环里就能实现多任务的非阻塞延时。比如想每100ms扫描一次按键if ((uwTick_ms - last_key_scan_time) 100) { last_key_scan_time uwTick_ms; KeyScan(); }这种写法不用HAL_Delay主循环随时可以响应其他任务。但要小心uwTick_ms的溢出问题。32位无符号整数在连续运行49.7天后会溢出归零如果只是个人学习项目无所谓但工业环境下还是要考虑。做法是判断差值时用无符号减法也就是上面代码里的写法它对溢出是免疫的只要两次采样间隔不超过49.7天。6. 排查实录TIM4中断不触发、运行一次就停的完整定位链路6.1 中断完全不触发的排查路径很多人配置完TIM4后发现程序运行了但中断就是不进。这时候别急着怀疑芯片坏了按照下面这个顺序排查九成问题在几分钟内能定位。第一步确认RCC时钟是否开启。标准库里检查有没有调用RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE)HAL库里检查CubeMX初始化代码里__HAL_RCC_TIM4_CLK_ENABLE()有没有被执行。第二步确认NVIC配置。用调试器查看NVIC-ISER寄存器的值TIM4对应的中断号是30那么ISER寄存器的bit30应该是1。如果这里不是1说明NVIC没有使能TIM4中断。标准库查NVIC_InitHAL库查CubeMX的NVIC Settings是否勾选。第三步确认定时器是否启动。标准库有没有调用TIM_Cmd(TIM4, ENABLE)HAL库有没有调用HAL_TIM_Base_Start_IT(htim4)。这两行是开始干活的开关很多人配好了一切寄存器忘了这一步。第四步确认中断函数名称是否正确。标准库是TIM4_IRQHandlerHAL库是TIM4_IRQHandler加内部的HAL_TIM_IRQHandler调用。如果函数名拼错链接器不会报错但中断向量表里指向的是启动文件定义的弱函数你的代码永远不会被执行。用调试器在中断服务函数里打断点是最好的验证方式。第五步检查是不是有代码把中断优先级分组或者NVIC配置覆盖了。比如在某个HAL_MspInit或者自定义的初始化函数里又调用了HAL_NVIC_SetPriorityGrouping改变了分组方式。6.2 中断只执行一次的排查思路如果程序上电后中断确实执行了一次然后就没有然后了这基本能锁定是标志位没有清除。标准库的写法是if (TIM_GetITStatus(TIM4, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM4, TIM_IT_Update); // 业务代码 }如果你写了判断标志位的if却忘了在if内部调用TIM_ClearITPendingBit就会陷入执行一次→标志位保持置1→中断返回→由于标志位依然置1硬件再次触发中断→又执行一次→返回的死循环。表现是什么主循环完全卡死连LED都不闪了看起来像程序死在中断里。还有一种情况是标志位清了但在中断服务函数内部又主动清零了全局状态导致下一次判断条件不满足。比如你在中断里把控制标志置0而这个标志是主循环里某段代码用来使能中断的那么中断执行一次后主循环执行到那里又把中断关了。这类逻辑问题比较隐蔽建议在中断里加上调试引脚翻转观察到底是不是中断没进还是进了但业务逻辑没生效。我用过一个非常土的排查方法在中断服务函数最开头翻转一个LED引脚中断执行时LED闪一下。如果LED正常闪烁说明中断本身没问题问题在业务代码。这个办法屡试不爽尤其适合没有示波器的场景。6.3 定时时间比预期长一倍的奇葩原因这是个很容易被忽视的时钟树问题。前面提到过STM32F103的APB1预分频系数决定定时器时钟是否倍频。CubeMX如果在系统时钟配置页面把APB1分频设为了4那么定时器时钟就不再是72MHz而是36MHz。在PSC和ARR不变的情况下定时时间会翻倍。为什么APB1会影响定时器时钟这是STM32的一个设计特性当APB1的分频系数大于1时定时器时钟会自动获得一个2倍的倍频。如果APB12那么定时器时钟APB1×236MHz×272MHz。如果APB14定时器时钟APB1×218MHz×236MHz。标准库代码里SystemInit函数会根据外部晶振和倍频配置设置RCC_CFGR的PPRE1位。如果你在项目里手动修改了时钟配置或者在CubeMX里改了HCLK频率一定要回头确认APB1的分频值。这个坑非常隐蔽因为程序能跑、中断能进、看起来一切正常就是时间不对。6.4 TIM4和系统滴答定时器的关系有人会问既然有了SysTick为什么还要用TIM4做定时SysTick是ARM内核自带的24位向下计数器常用于操作系统的时基被HAL_Delay和uwTick占用。如果你细看过HAL库的源码会发现SysTick的优先级被特意设为最低目的就是不干扰其他中断。当你既要用HAL_Delay又要用TIM4做精确定时完全没有冲突它们是两个独立的外设。常见做法是SysTick负责操作系统心跳和HAL_DelayTIM4负责精确控制某个时间敏感任务。比较特殊的情况是如果你在中断回调里使用了HAL_Delay而SysTick优先级低于TIM4那么HAL_Delay依赖的SysTick中断无法打断当前TIM4中断延时就不准确。我在调试时发现HAL_Delay在TIM4中断里永远比设定值长就是这个原因。解决方法是尽量减少在中断里调用其他依赖中断的函数尤其是HAL_Delay这种。7. 案例实战用TIM4构建一个非阻塞的任务调度器7.1 任务调度器的基本设计思路在裸机开发中最常见的架构是主循环中断。主循环不断轮询中断负责闹钟功能。基于TIM4的1ms中断维护一个时间戳变量主循环检查时间戳差值各种任务就能各自按不同的周期运行互不阻塞。具体来说先定义各任务的时间片#define TASK_LED_PERIOD 500 // LED翻转周期单位ms #define TASK_KEY_PERIOD 10 // 按键扫描周期单位ms #define TASK_UART_PERIOD 1000 // 串口发送周期单位ms volatile uint32_t uwTick_ms 0; uint32_t last_led_time 0; uint32_t last_key_time 0; uint32_t last_uart_time 0;在主循环里每个任务只需要搞定三件事判断时间到没到、执行任务、记录本次执行时间。while (1) { if ((uwTick_ms - last_led_time) TASK_LED_PERIOD) { last_led_time uwTick_ms; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } if ((uwTick_ms - last_key_time) TASK_KEY_PERIOD) { last_key_time uwTick_ms; KeyScan(); } if ((uwTick_ms - last_uart_time) TASK_UART_PERIOD) { last_uart_time uwTick_ms; UartSendStatus(); } }7.2 为什么使用差值判断而不是等值判断新手喜欢这么写if (uwTick_ms last_led_time 500)。这个写法看着没问题但有个致命隐患如果那次主循环刚好在处理一个较长的任务错过了uwTick_ms等于目标值的那一瞬间那么这个任务就永远等不到执行了直到uwTick_ms溢出重新绕回这个值而这可能已经是几十秒甚至几十分钟之后的事。使用if ((uwTick_ms - last_led_time) TASK_LED_PERIOD)就能规避这个问题。无符号整数减法自带溢出回绕处理即使uwTick_ms从0xFFFFFFFF回到0x00000000只要差值按无符号计算结果依然是正确的时间间隔。这是嵌入式里非常经典的处理手法一句话判断时间差比判断相等更健壮。7.3 中断里放什么、主循环里放什么这个调度器跑通之后你还会面对一个问题哪些任务放中断里哪些放主循环我个人的分配原则是放中断里的时间要求极度严格的操作比如电机PWM输出的比较值更新需要精确记录的输入捕获时间戳需要立即响应的外部信号。放主循环里的按键扫描、数码管显示、串口数据解析、LCD刷新、传感器数据轮询、PID运算的慢速部分。这些任务对时间精度要求不高几毫秒的延迟完全可以接受。按这个原则TIM4中断回调只做一件事uwTick_ms。它足够短不会造成中断堆积也不会影响其他中断的响应。主循环里根据时间戳调度所有任务代码清爽逻辑清晰而且扩展新任务只需要加一个周期宏时间戳变量判断逻辑非常方便。我在一个实际项目中用这套架构同时管理了4路按键、2路编码器读数、1路OLED刷新、串口遥控指令解析。主循环循环一轮大约是2-3毫秒但对于这些任务来说完全没有问题。核心的电机控制放在另一个高级定时器的中断里和TIM4的时基互不干扰。8. 最后的两个实战建议8.1 学会读参考手册的时基单元章节网上关于定时器的教程很多但大部分只告诉你填什么值、调什么函数没有人讲清楚为什么这个值能算出正确时间。如果你真的想在定时器上不吃亏建议花一晚上读一下STM32F103参考手册RM0008里通用定时器那一章重点看时基单元、计数模式、更新事件这三个小节。手册里的时序图对理解CNT和ARR的关系帮助极大配合示波器实测比刷一百篇博客都管用。对于已经能用CubeMX完成配置的朋友我建议找一个下午把CubeMX生成的代码和参考手册对照着看一遍。HAL_TIM_Base_Init里的每一条赋值你在手册里都能找到对应寄存器的描述。这个过程有点枯燥但做完之后你对定时器的理解会上升一个台阶。8.2 调试定时器中断的黄金法则最后分享一个调试定时器相关的黄金法则随手准备一个空闲GPIO专门用来在中断里翻转电平。这个GPIO不接任何实际负载纯粹给示波器或者逻辑分析仪观察用。判定方法很简单如果程序跑飞了或者卡死了先看这个GPIO还在不在翻转。如果在翻转说明定时器和中断链路是通的问题出在你的业务代码如果不在翻转说明定时器配置或者中断使能出问题了。这个隔离法能把你从一堆看似毫不相关的Bug里快速解脱出来。我的桌面上现在都常备一块STM32F103最小系统板上面只跑了TIM4中断翻转LED的程序。每当新项目用到定时器我都会先在它上面验证一下时钟配置确认没问题了再移到目标板上调业务。表面上看多花了几分钟实际上省下了后面调试时序的大把时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑