ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32G0定时器中断实战:TIM2精准1秒翻转LED,HAL库从配置到代码

STM32G0定时器中断实战:TIM2精准1秒翻转LED,HAL库从配置到代码 做过嵌入式的人都知道一句话点灯是入门点灯的方式才是分水岭。用HAL_Delay()写个死循环延时灯也能闪但你永远不知道系统里有多少个“等一会儿”在排队用定时器中断把翻转 LED 的动作挂到硬件心跳上CPU 该睡觉睡觉、该干活干活灯的节奏却分毫不差。这篇文章就用 STM32G0 的 TIM2 做一个精准 1 秒翻转 LED 的完整实战全程基于 HAL 库从 CubeMX 配置、时钟计算、中断链路到回调函数写法逐行拆解最后给出可以直接抄作业的完整代码。这套东西适合谁刚接触 STM32 想搞懂定时器中断的同学被 HAL 库各种封装绕晕的新手或者已经会点灯但想摸清 TIM2 溢出周期怎么算的老哥们。看完你会明白一个核心问题为什么你写出来的 1 秒总是不准以及怎么用 TIM2 这种 32 位定时器把 1 秒算得明明白白。1. 项目拆解为什么选定时器中断为什么是 TIM21.1 定时器中断解决的三个痛点先聊最本质的问题为什么不用HAL_Delay()很多入门教程教的就是 Delay 翻转我承认它简单但它有三个硬伤。第一HAL_Delay()是忙等。CPU 在while里空转什么事都干不了。你以为你在“精准延时”实际上是把整个 MCU 锁死在一个循环里。第二HAL_Delay()依赖 SysTick 中断如果系统里别的地方把 SysTick 关了或者优先级配错了延时会直接失灵。第三延时函数的精度受中断抢占影响外部中断一多这个 1 秒可能变成 1.2 秒你根本察觉不到。定时器中断的思路完全相反TIM2 自己数时钟数到预设值就触发一次中断CPU 在中断里翻转一下 LED然后该干嘛干嘛。MCU 的核心频率、外设运算、通信处理都不受影响LED 的节奏完全由硬件定时器保证。这正是工业上做周期性采样的标准姿势也是多任务调度的雏形。1.2 为什么选 TIM232 位计数的优势STM32G0 系列里的 TIM2 是 32 位通用定时器。32 位意味着什么它的自动重载寄存器 ARR 最大可以写到 4294967295。这个数字对做“精准 1 秒”太友好了。你可以直接把计数器周期配成 999999让定时器整数溢出在 1 秒这个点上不需要软件里再计数一次。举个例子感受一下如果用的是 16 位定时器ARR 最大只有 65535在 64 MHz 时钟下即便把预分频拉到 65535计数时间也就一秒钟上下非常局促。你要么牺牲计数精度要么在中断里做软件累加。TIM2 就没这个问题32 位的计数范围让你能把“1 秒”直接写进硬件配置代码干净、逻辑直白。当然 TIM2 不是白送的它占用的中断向量和资源在 G0 内部属于通用定时器组不跟高级定时器 TIM1 冲突很适合当系统的“心跳时钟”来用。1.3 TIM2 和 SysTick 的分工千万别搞混这里有个容易踩坑的点HAL 库自己有一个时基Timebase默认用 SysTick 实现HAL_Delay()、各种超时判断都靠它。你在应用层用 TIM2不等于要把 SysTick 替换掉。正确姿势是SysTick 继续给 HAL 库当心跳TIM2 专心做你的应用时基。两者各管一段互不干扰。如果你非要手贱去把 HAL 的时基改成 TIM2那代码里所有依赖HAL_Delay()的地方都会乱套排查起来极其痛苦。我见过有人为了省一个定时器把HAL_InitTick指到 TIM6结果整个工程的超时逻辑全部失效当场血压拉满。2. 环境准备与 CubeMX 配置的关键参数2.1 开发环境与硬件清单先交代一下我这边的环境你照着配大概率不会出问题。开发板Nucleo-G071RB板载 STM32G071RB主频最高 64 MHz开发环境STM32CubeIDE 1.13 以上版本自带 CubeMX 图形化配置HAL 库版本随固件包自动拉取我用的是 STM32Cube FW_G0 V1.6.1板载 LEDPA5低电平点亮Nucleo 板上 LD2 默认就是这么接的如果没有 Nucleo 板用市面上任意一块 STM32G0 核心板也能复现只要把 LED 引脚改成你自己的 GPIO 就行。我的配置是 PA5 推挽输出这个后面会写清楚。2.2 CubeMX 里 TIM2 的图形化配置打开 CubeMX新建项目选对芯片型号然后分三步配置。第一步配置 RCC。在 System Core 的 RCC 选项里HSE 选Crystal/Ceramic Resonator或BYPASS Clock Source。如果你的板子上没有外部晶振也可以直接用内部高速时钟 HSI16后面配合 PLL 锁到 64 MHz。Nucleo 板上默认有外部晶振所以 HSE 直接选 Crystal。第二步配置时钟树。这是整个工程精度最关键的一环。确保 SYSCLK 拉到 64 MHz然后看 APB 总线分频。这里有个容易被忽略的知识点STM32 的定时器时钟并不总是等于总线时钟当 APB 预分频器大于 1 时定时器内部时钟会翻倍。G0 上如果你把 APB1 设置成 DIV1那么 TIM2 的时钟就等于 64 MHz干干净净。第三步配置 TIM2 参数。在 Timers 列表里选中 TIM2勾选Activated然后设置以下参数参数数值说明Prescaler (PSC)6364 分频得到 1 MHz 计数时钟Counter ModeUp向上计数模式Counter Period (ARR)999999计满 100 万个脉冲触发中断Auto Reload PreloadEnable使能自动重载预装载NVIC 中断TIM2 global interrupt勾选并给一个合适的优先级这个配置的含义是64 MHz 先除以 64得到 1 MHz也就是计数器每 1 微秒走一步再走 100 万个步数就是整整 1 秒。公式随手一算$$中断频率 \frac{64\text{ MHz}}{(631) \times (9999991)} 1 \text{ Hz}$$用 CubeMX 的 Clock Configuration 页面把 SYSCLK 调到 64 MHz 后你可以在 TIM2 的配置界面里直接看到这个计算结果。如果参数填错了CubeMX 甚至会弹警告这个细节对新手很友好。2.3 时钟源选择内部时钟配置到底怎么回事有同学问过“TIM2 为什么是 internal clock”这类问题这里统一解释一下。在 CubeMX 的 Clock Configuration 视图里TIM2 的时钟源会显示为“Internal Clock”前缀是灰色锁定的。它指的是定时器的计数时钟不来自外部引脚而是来自 APB 总线提供的内部时钟。这是绝大多数场景下的默认配置也正是我们这个项目需要的让定时器跟着系统主频走精度可预测、可计算。TIM2 其实也支持外部时钟模式可以从外部引脚输入脉冲用在测频、编码器计数这些场景。普通应用根本不用碰这个选项你就记着项目里只做定时、点灯、周期采集选 Internal Clock 就够了。配置完成后点击GENERATE CODE生成的工程里会看到TIM2初始化和中断向量但 HAL 库的核心逻辑还没有自动启动我们得在main.c里手动开启中断。3. HAL 库定时器中断的完整链路3.1 HAL 库的封装结构从硬件中断到回调函数很多人学 HAL 库时觉得“像个黑盒”因为 CubeMX 生成了几个函数一时看不懂背后干了什么。其实链路非常简单就三层。第一层是初始化。CubeMX 生成的MX_TIM2_Init()会把htim2这个句柄填好包括我们在图形界面里配的 PSC、ARR、计数模式这些字段然后调用HAL_TIM_Base_Init()把参数写进 TIM2 的硬件寄存器。这个函数在stm32g0xx_hal_tim.c里是整个定时器驱动的地基。第二层是中断服务函数。当你使能 NVIC 后TIM2 溢出时会发生一次中断CPU 跳转到stm32g0xx_it.c里的TIM2_IRQHandler()。这个函数什么都没干就调用了HAL_TIM_IRQHandler(htim2)。HAL 库在这里检查是哪种事件更新、捕获、比较等然后进入第三层。第三层就是你自己写的回调函数HAL_TIM_PeriodElapsedCallback()。HAL 库在中断上下文里会帮你调用这个函数你不需要去操作任何寄存器也不需要看中断状态位直接在回调里翻转 LED 就行。理解了这三层你就知道 HAL 库多出一个“句柄 回调”的抽象层是为了让你把业务逻辑跟硬件细节解耦。代价是中断响应多了几个函数调用但我们这个是 1 秒级别的低速中断完全无所谓。3.2 回调函数的覆盖时机千万不要乱加东西HAL_TIM_PeriodElapsedCallback()是弱函数weakHAL 库默认给了个空实现。你要在自己的用户代码区重写这个函数这样链接时你的版本就能覆盖掉默认版本。这里有个细节这个回调是所有定时器共用的。如果你的工程里同时用了 TIM2 和 TIM6中断触发后都会进同一个回调函数。所以函数开头一定要判断句柄是谁void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } }不看句柄直接翻转 LED 的写法在只有一个定时器的时候没问题但一旦以后加了别的定时器就会出奇奇怪怪的逻辑错误。养成判断Instance的习惯是资深从业者的基本素养。3.3 要精细控制就用 LL 库别急着下结论很多老工程师喜欢在中断场景用 LL 库理由是 LL 更接近寄存器没有回调封装代码执行更快。我不否认LL 库确实轻量比如用 LL 库写定时器溢出中断直接在中断服务函数里判断标志位即可少了两层函数调用。但做项目不是只考虑速度还要考虑可维护性和团队协作。HAL 库的封装虽大但逻辑统一换芯片平台时大部分代码可以平移。而且到了 1 秒、甚至 1 毫秒这个量级的中断HAL 库多出来的那几微秒开销根本感知不到。真到了对时序极端苛刻的场景比如高速 PWM 或高频采样那时候再混用 LL 调整关键寄存器也不迟。HAL 和 LL 不是二选一的仇人GE 工程里完全可以共存网上那些“HAL 垃圾”的说法听听就好。4. 代码实现与逐行解析4.1 初始化代码背后的事CubeMX 生成的初始化流程里main()会依次调用HAL_Init()、SystemClock_Config()、MX_GPIO_Init()、MX_TIM2_Init()。这里面有个很多人忽略的函数HAL_Init()里最关键的动作就是配置 SysTick并启动了一个 1 ms 的时基中断。HAL_StatusTypeDef HAL_Init(void) { HAL_InitTick(); ... }如果你把这条链捋顺了就不会问“为什么我什么都没干SysTick_Handler 也在跑”这种问题。因为 HAL 库本身就需要一个节拍。TIM2 的初始化在这里只是个“配寄存器”真正开始计时还得靠我们手动调HAL_TIM_Base_Start_IT()。启动函数放哪里最稳妥的位置是在main()里所有外设初始化完之后int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); HAL_TIM_Base_Start_IT(htim2); // 启动TIM2定时器中断 while (1) { // 主循环里什么都不用干LED翻转全部交给中断 // 想看效果就加个低功耗模式或者做别的事 } }4.2 中断回调中的 LED 翻转回调函数我放在main.c的 USER CODE 区域CubeMX 重新生成代码时不会覆盖。翻转 GPIO 用 HAL 库的HAL_GPIO_TogglePin()即可参数就是我们配好的LED_GPIO_Port和LED_Pin。完整回调void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } }有人会问直接操作寄存器翻转是不是更快比如GPIOA-ODR ^ GPIO_PIN_5;。确实快一点但 HAL 库里HAL_GPIO_TogglePin内部也只是做异或操作差别微乎其微。优先用库函数代码更统一真到了要优化的阶段再考虑寄存器操作不迟。4.3 完整代码清单与编译验证下面把核心代码完整贴出来基于 CubeMX 生成的项目我只额外加了三处第一处配置 TIM2 的 PSC 为 63、ARR 为 999999这个在图形界面完成生成的代码会对应如下static void MX_TIM2_Init(void) { htim2.Instance TIM2; htim2.Init.Prescaler 63; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 999999; htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_Base_Init(htim2) ! HAL_OK) { Error_Handler(); } }第二处在main()里加启动函数HAL_TIM_Base_Start_IT(htim2);第三处在用户代码区加回调函数void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } }编译下载后你会看到 PA5 上的 LED 以 1 Hz 频率翻转亮 1 秒、灭 1 秒。用示波器量 PA5 引脚可以看到一个精确的方波周期 2 秒高电平 1 秒低电平 1 秒。如果你只有逻辑分析仪也能看到这个规律。5. 精准计时的实战经验常见问题与排查技巧5.1 为什么你写的 1 秒总是不准很多人在这个例程上翻车原因五花八门最集中的几个我列一下。第一时钟树配错了。CubeMX 默认生成的工程 SYSCLK 不一定是 64 MHz。如果用默认的 HSI16 而没有配置 PLL主频只有 16 MHz那你的 TIM2 即使 PSC63计数时钟也只有 250 kHz1 秒的周期会变成 4 秒。改完时钟树一定要看 Clock Configuration 页面的确认确保 SYSCLK 显示 64 MHz。第二APB 分频器配高了。前面提过STM32 定时器时钟在 APB 预分频大于 1 时会自动乘 2。如果你在这里搞混了计数器时钟就不是你以为的那个数周期自然不准。有个土办法验证直接在 CubeMX 的 TIM2 配置界面右键能看到计算后的溢出频率填完参数瞄一眼就心里有数了。第三用了 16 位计数思路。有人把 TIM2 当 16 位定时器用看到 ARR 填 999999 就担心溢出了。其实 TIM2 在这个系列的 G0 上是 32 位定时器放心填。但如果换到 TIM3就未必是 32 位需要查数据手册确认。5.2 中断服务函数里的隐形杀手再来谈谈质量隐患。定时器中断进入后代码执行时间越短越好这是铁律。很多新手会在回调函数里写HAL_Delay()想着“翻转后等 500 毫秒再来一次”。这是一个典型的死锁陷阱。因为HAL_Delay()依赖 SysTick 中断而你的程序现在正处在 TIM2 中断上下文里。如果 SysTick 中断优先级和 TIM2 相同或更高倒还有可能正常运行一旦优先级配置不当或者延迟时间跨过 SysTick 的中断周期整个调度就会乱成一锅粥轻则闪烁异常重则直接卡死。正确思路是中断里只做“标记”和“耗时极短的操作”比如翻转 LED、置一个标志位。真正复杂的逻辑放到主循环里处理。我们这次的例程简单回调里直接翻转没问题。以后你要是拿定时器做按键扫描、数据处理一定把大部分工作挪出中断。5.3 排查手法示波器、变量监视与逻辑分析仪如果 LED 完全不动先看三个方向。第一HAL_TIM_Base_Start_IT有没有调用光有 CubeMX 生成的初始化不够没启动定时器就不会产生中断。第二NVIC 里有没有勾选 TIM2 global interruptCubeMX 其实已经帮你配好了但手动改代码时容易丢。第三回调函数有没有被正确覆盖如果你的回调函数名打错了一个字母HAL 库的默认空实现就会接管你的代码永远执行不到。如果 LED 在动但周期不对直接用示波器量 PA5看波形周期是多少。测出来不是 2 秒就用调试器在回调函数里打断点看htim2.Instance和当前计数值__HAL_TIM_GET_COUNTER(htim2)。这个方法能快速锁定是配置问题还是时钟问题。我个人调试这类东西习惯在回调里加一个volatile uint32_t tick每次进来tick然后在调试器里观察这个变量每秒增量是否等于 1。如果等于 1说明硬件没问题是 LED 或者其他外设的锅。这个小技巧能帮你把问题隔离得很干净。6. 扩展一个定时器能玩出多少花样6.1 1 ms 节拍计数实现多任务时基有朋友看完直接照抄上面的回调然后问我现在想做一个“按键按下后 3 秒后执行动作”怎么做简单把 TIM2 改成 1 ms 中断一次PSC 保持 63ARR 改成 999。这样每秒进 1000 次中断每次进去只做一个tick主循环里判断 tick 的差值实现非阻塞延时。volatile uint32_t tick_ms 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { tick_ms; } }然后随便写个超时判断uint32_t start tick_ms; while ((tick_ms - start) 3000) { // 这段循环不会卡死其他逻辑 }这个模式本质上是把定时器升级成软件的“心跳”后面做状态机、事件调度都用得上。6.2 定时器触发 ADC 采集与 DMA 配合TIM2 还有一个隐藏技能作为 ADC 的触发源。ADC 的采样需要固定间隔如果用软件轮询采样时间容易抖动用 TIM2 输出触发事件ADC 就能在完全确定的时间点启动转换。配合 DMA可以实现“定时器触发 ADC → DMA 搬运结果 → CPU 处理”的整套数据流。网上常说的“定时器中断触发 ADC HAL”大方向就是这个思路。操作上CubeMX 里把 TIM2 的Trigger Output (TRGO)配置成Update Event然后在 ADC 配置里把触发源选成TIM2 Trigger Out event。生成代码后启动顺序变成先启动 ADC再启动 TIM2TIM2 每次溢出更新就自动触发一次 ADC 采样。整个过程不需要 CPU 干预采样频率极其稳定。这就是定时器的威力它不只是点灯工具它是整个系统的节拍器。6.3 把定时器搬到其他芯片上要注意什么最后提一句迁移问题。同样是 STM32 系列F1、F4、G0 的 HAL 库 API 基本兼容但定时器资源差异很大。比如 F103 的 TIM2 叫通用定时器但主频、时钟树、APB 分频规则都不同。你在这篇例程里写的 PSC63、ARR999999换到 72 MHz 的 F103 上周期就会变成 0.89 秒左右必须重新计算。所以凡是涉及“精准”两个字的项目都要养成果断检查时钟树的习惯。芯片型号一变时钟频率一变所有定时参数都得重新推导。这也是嵌入式开发和纯软件最大的不同你的代码不是跑在一台理想机器上而是跑在一个有具体时钟、具体寄存器的裸机上。我自己做的项目多了以后最大的体会就是定时器中断不是什么高深技巧但它考验的是你对时钟、外设、中断优先级、HAL 库封装逻辑的整体理解。把这 1 秒 LED 做透了后面做 PWM、捕获、编码器、ADC 触发都是顺理成章的事。你在这个例程里养成的每一步排查习惯会在今后的每一个 Bug 现场救你一命。
返回列表