FEATURED · 精选文章

STM32 HAL库定时器实战:中断与PWM配置详解

发布时间 / 2026/8/27 19:22:06
来源 / 创域科博编辑部
栏目 / 资讯中心
STM32 HAL库定时器实战:中断与PWM配置详解 1. 项目概述从寄存器到HAL定时器的进阶之路搞嵌入式开发尤其是STM32定时器TIM绝对是个绕不开的核心外设。我记得刚入行那会儿对着数据手册和标准库一个寄存器一个寄存器地配置虽然能跑起来但心里总有点发虚生怕哪个位没配对。后来ST推出了HAL库一开始大家还觉得封装得太厚效率不行但用久了才发现在项目快速迭代和代码可维护性上它带来的便利是实实在在的。今天我就结合自己踩过的坑和积累的经验来聊聊如何用STM32的HAL库玩转定时器中断和PWM输出这两个最常用、也最考验基本功的功能。简单来说这个内容就是教你如何用HAL库让STM32的定时器按照你设定的时间规律精准地“打断”CPU去执行特定任务中断同时还能输出频率和占空比都可调的方波信号PWM。无论是用来做LED呼吸灯、控制舵机、驱动电机还是为其他外设提供精确的时基都离不开它。无论你是刚接触HAL库的新手还是想从标准库迁移过来的老鸟这篇内容都能帮你理清思路避开那些手册上不会写的“暗坑”。2. 核心思路与方案选型为什么是HAL库的TIM在开始动手写代码之前我们得先想明白几个关键问题用哪个定时器中断和PWM分别怎么实现HAL库的驱动模型是怎样的把这些搞清楚了后面的配置才能有的放矢。2.1 定时器资源盘点与选型考量STM32的定时器家族非常庞大从基本定时器TIM6, TIM7通用定时器TIM2-TIM5, TIM9-TIM14到高级定时器TIM1, TIM8功能强弱和复杂度依次递增。对于绝大多数应用通用定时器就足够了。选型时我主要考虑以下几点通道数量需要输出几路PWM通用定时器通常有4个独立的通道CH1-CH4可以同时输出4路PWM。如果你的项目需要控制多个舵机或LED就要选通道足够的定时器。时钟源与计数范围定时器的时钟来源于APB总线经过可能的倍频。计数器的位数16位或32位决定了最大定时周期。例如72MHz系统时钟下16位计数器最大可计到65535那么最长的定时周期就是 (655351)/72MHz ≈ 0.91ms。如果需要更长的定时时间就需要使用分频器PSC来降低计数频率。是否与其他功能冲突有些定时器的通道可能和高级功能如互补输出、刹车输入绑定或者被其他外设如串口、SPI的引脚复用占用。在CubeMX中配置引脚时一定要检查清楚。实操心得对于新手我建议从TIM2或TIM3开始它们是资源最丰富、例子最多的通用定时器。在CubeMX里看哪个定时器对应的GPIO引脚位置最方便你的硬件布线就选哪个能减少很多硬件上的麻烦。2.2 HAL库驱动模型解析回调函数与状态机HAL库和标准库最大的不同在于它采用了一套基于状态机和回调函数Callback的驱动模型。理解这个模型是用好HAL库的关键。状态机每个外设如TIM都有一个状态变量比如htim.Instance-SR寄存器映射和HAL库管理的状态标志。HAL库函数在执行操作如启动定时器、使能中断前会先检查状态防止重复操作导致错误。这增加了鲁棒性但有时也需要我们在代码中主动查询或清除某些状态标志。回调函数这是HAL库的精华所在。当定时器更新事件溢出发生时硬件触发中断HAL库的中断服务函数如TIMx_IRQHandler会自动被调用。这个服务函数会处理一堆状态位然后调用一个弱定义的Weak回调函数比如HAL_TIM_PeriodElapsedCallback()。我们用户要做的不是去修改中断服务函数而是在自己的主程序文件里重写Override这个回调函数在里面放入我们想要中断后执行的代码。这种设计让中断处理逻辑变得非常清晰和模块化。方案确定我们的目标就是利用HAL库的MX_TIMx_Init()函数完成定时器的基础结构配置通过计算和设置预分频器PSC和自动重载值ARR来确定定时周期。然后分别使能更新中断和PWM输出通道。最后在对应的回调函数和主循环中实现我们的业务逻辑。整个流程CubeMX可以帮我们完成80%的初始化代码生成工作。3. 关键参数计算与CubeMX图形化配置理论说得再多不如动手配一遍。这里我以STM32F103C8T6蓝色药丸板的TIM3为例目标是实现一个1kHz的PWM输出占空比后续可调同时让定时器每500ms产生一次更新中断在中断里翻转一个LED灯。3.1 定时器参数计算ARR与PSC的设定这是最核心的一步决定了定时器的“心跳”节奏。公式很简单但一定要理解定时器时钟频率 (TIM_CLK) 查看芯片数据手册和时钟树。在标准72MHz系统时钟配置下APB1总线时钟是36MHz但连接到定时器时如果APB1预分频系数≠1则TIM时钟会翻倍。对于F103默认配置下APB1 prescaler 2所以TIM_CLK APB1 clock * 2 36MHz * 2 72MHz。这个信息在CubeMX的Clock Configuration标签页里可以直观看到。计数频率 (CK_CNT) 这是计数器实际累加的频率。CK_CNT TIM_CLK / (PSC 1)。PSC是16位预分频寄存器值。定时周期 (T) 计数器从0计数到ARR自动重载值一次所花的时间。T (ARR 1) / CK_CNT (ARR 1) * (PSC 1) / TIM_CLK。我们的目标1500ms更新中断假设我们让定时器每计数50000次产生一次更新ARR 49999。那么为了达到500ms的周期计数频率应为CK_CNT (ARR 1) / T 50000 / 0.5s 100,000 Hz。 接着计算PSCPSC TIM_CLK / CK_CNT - 1 72,000,000 / 100,000 - 1 720 - 1 719。 所以中断配置为PSC 719 ARR 49999。这样更新事件频率就是100kHz每50000次计数正好0.5秒。我们的目标21kHz PWM输出PWM频率由定时器的整个计数周期决定即Fpwm CK_CNT / (ARR 1)。注意一个定时器只有一个ARR它同时决定了中断周期和PWM频率。所以如果我们用上面算出的ARR49999那么PWM频率Fpwm 100,000 Hz / 50000 2 Hz这太慢了不是我们想要的1kHz。这说明当我们需要用同一个定时器同时做不同周期的任务时会产生冲突。解决方案有两种使用不同的定时器一个TIM专门用于产生中断另一个TIM专门输出PWM。这是最干净的方法。使用同一个定时器但以PWM频率为主这是我们演示的方法。将ARR设置为产生1kHz PWM所需的值然后通过软件计数的方式在更新中断里实现更长的延时如500ms。重新计算以PWM为主Fpwm 1kHz 1000 Hz。ARR 1 CK_CNT / Fpwm。我们需要先确定CK_CNT。通常为了让PWM分辨率更高占空比调节更精细我们希望ARR尽可能大。设ARR7199一个常用值则CK_CNT Fpwm * (ARR 1) 1000 * 7200 7,200,000 Hz。 那么PSC TIM_CLK / CK_CNT - 1 72,000,000 / 7,200,000 - 1 10 - 1 9。 所以最终基础配置为PSC 9 ARR 7199。此时PWM频率为1kHz更新中断的频率也是1kHz。如何在1kHz中断里实现500ms延时我们可以在更新中断回调函数里设置一个软件计数器。每进入一次中断计数器加1当加到500时因为1kHz中断1000次/秒500次就是0.5秒执行一次LED翻转然后计数器清零。这样就实现了用高频中断模拟低频任务。3.2 CubeMX工程配置实战打开CubeMX选择你的芯片型号。配置时钟树在Clock Configuration标签页确保HCLK系统时钟设置为72MHz。确认APB1 Timer Clocks显示为72MHz。这是正确计算的基础。配置TIM3在Pinout Configuration标签页左侧找到Timers-TIM3。Clock Source选择Internal Clock内部时钟源。在Configuration下方的Parameter Settings中Prescaler (PSC - 16 bits value)填入9。Counter ModeUp向上计数。Counter Period (AutoReload Register - 16 bits value)填入7199。Internal Clock Division (CKD)No Division。auto-reload preloadEnable推荐使能可以避免更新ARR时产生毛刺。切换到NVIC Settings勾选TIM3 global interrupt使能全局中断优先级可以先用默认值。配置PWM输出通道在TIM3的配置页面找到Channel1选择PWM Generation CH1。这会自动将TIM3的通道1对应引脚对于F103C8T6的TIM3_CH1是PA6设置为复用输出模式。此时下方会多出一个PWM Generation Channel 1的子菜单。点进去ModePWM mode 1这是最常用的模式计数器小于比较值时输出有效电平。Pulse (16 bits value)这是比较寄存器CCR的值决定占空比。初始值可以设为(ARR1)/2 3600即50%占空比。Fast ModeDisable常规应用禁用即可。CH PolarityHigh有效电平为高即占空比期间输出高电平。这个根据你的驱动电路需求来定。配置GPIO可选如果你计划用中断控制一个LED比如用PC13板载LED需要在Pinout视图找到PC13设置为GPIO_Output。生成代码点击Project Manager设置好项目名称、路径、IDE如MDK-ARM V5。在Code Generator里勾选Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral这样代码结构更清晰。最后点击GENERATE CODE。4. 代码实现与解析从初始化到业务逻辑CubeMX生成的代码搭建了坚实的框架我们只需要在它划定的“安全区”内填充自己的逻辑即可。4.1 初始化代码分析在生成的main.c中我们可以看到MX_TIM3_Init()函数。HAL库的初始化是分层的static void MX_TIM3_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; TIM_OC_InitTypeDef sConfigOC {0}; htim3.Instance TIM3; htim3.Init.Prescaler 9; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 7199; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_Base_Init(htim3) ! HAL_OK) { Error_Handler(); } // 配置时钟源内部时钟 sClockSourceConfig.ClockSource TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(htim3, sClockSourceConfig) ! HAL_OK) { Error_Handler(); } // 配置PWM模式 if (HAL_TIM_PWM_Init(htim3) ! HAL_OK) { Error_Handler(); } // 配置主模式本例中未使用输出触发可忽略其具体作用 sMasterConfig.MasterOutputTrigger TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(htim3, sMasterConfig) ! HAL_OK) { Error_Handler(); } // 配置输出比较通道即PWM通道 sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 3600; // 初始占空比50% sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; if (HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1) ! HAL_OK) { Error_Handler(); } // 在初始化后立即启动PWM输出也可在main中启动 HAL_TIM_MspPostInit(htim3); }关键点在于最后调用的HAL_TIM_MspPostInit它是在外设初始化之后由HAL库调用的一个弱函数其实现通常在stm32f1xx_hal_msp.c文件中。这里面完成了GPIO的复用功能配置。我们一般不需要修改这个文件除非有特殊的引脚或时钟需求。4.2 主程序与中断回调函数编写现在我们在main.c的用户代码区/* USER CODE BEGIN */和/* USER CODE END */之间添加我们的逻辑。首先定义软件计数器变量。/* USER CODE BEGIN PV */ uint32_t timer3_counter 0; // 用于500ms计数的软件计数器 /* USER CODE END PV */在main函数中在初始化之后、主循环之前启动定时器的基本功能、PWM输出和更新中断。/* USER CODE BEGIN 2 */ // 启动定时器基础驱动时钟等 HAL_TIM_Base_Start(htim3); // 启动PWM输出通道1 HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); // 启动定时器更新中断 HAL_TIM_Base_Start_IT(htim3); /* USER CODE END 2 */接下来重写定时器更新中断的回调函数。这个函数会在每次定时器溢出更新事件时被自动调用。/* USER CODE BEGIN 4 */ /** * brief 定时器周期结束回调函数非阻塞式 * param htim: 定时器句柄 * retval None */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { // 判断是哪个定时器触发的回调 if (htim-Instance TIM3) { timer3_counter; // 每次中断计数器加1 // 判断是否达到500次对应500ms 1kHz中断 if (timer3_counter 500) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 翻转LED状态 timer3_counter 0; // 计数器清零 } } } /* USER CODE END 4 */最后在主循环中我们可以添加其他任务或者动态改变PWM的占空比。例如通过按键或串口指令来调整。/* USER CODE BEGIN WHILE */ while (1) { // 示例在主循环中逐渐改变PWM占空比实现呼吸灯效果简易版 static uint16_t pwm_pulse 0; static int8_t dir 1; // 方向1为增加-1为减少 pwm_pulse dir * 10; // 每次步进10 if (pwm_pulse htim3.Init.Period) // 达到最大值 { pwm_pulse htim3.Init.Period; dir -1; } else if (pwm_pulse 0) // 达到最小值 { pwm_pulse 0; dir 1; } // 更新比较寄存器的值即改变占空比 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, pwm_pulse); HAL_Delay(10); // 延时10ms控制呼吸速度 /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */重要提示在主循环中频繁调用__HAL_TIM_SET_COMPARE是可行的但更严谨的做法是检查定时器是否处于运行状态或者使用HAL库提供的__HAL_TIM_SET_AUTORELOAD等宏时要注意如果定时器正在运行且没有使能预装载AutoReloadPreload直接修改ARR可能会导致当前计数周期错乱。对于CCR比较寄存器的修改在PWM模式1或2下通常可以立即生效或在下个周期生效是相对安全的。5. 调试技巧与常见问题排查代码写完了下载到板子可能一次成功也可能遇到各种问题。下面是我总结的几个常见坑点和调试方法。5.1 PWM无输出或波形不对检查引脚配置首先确认CubeMX里是否正确配置了PWM通道并且生成的代码里HAL_TIM_MspPostInit正确配置了GPIO的复用功能。用万用表或示波器测量对应引脚看是否有输出。检查时钟和定时器是否启动确保HAL_TIM_PWM_Start被调用。我遇到过在MX_TIMx_Init后忘记启动PWM的情况自然没有输出。检查ARR和Pulse值如果Pulse值大于或等于ARR值在PWM模式1下占空比会是100%或0%。确保Pulse值在0到ARR之间。检查极性如果CH Polarity设为Low那么有效电平是低你看到的波形可能是反的。根据驱动电路要求调整。使用示波器测量这是最直接的方法。看波形频率是否为预期的1kHz占空比是否正确波形是否干净无毛刺。5.2 中断不触发检查NVIC配置在CubeMX的NVIC Configuration中确认定时器的全局中断已使能并且优先级合理不要被更高优先级的中断屏蔽。检查中断启动函数是否调用了HAL_TIM_Base_Start_IT只调用HAL_TIM_Base_Start是不会产生中断的。在回调函数中打断点或点灯在HAL_TIM_PeriodElapsedCallback函数里第一行设置断点或者添加一个GPIO翻转语句用另一个未使用的引脚下载后运行看程序是否能停在此处或引脚是否有变化。这是判断中断是否进入的最简单方法。检查软件计数器逻辑如果中断能进但LED不闪检查你的软件计数器timer3_counter的逻辑特别是判断条件500和清零操作。5.3 定时时间不准核对时钟树这是最常见的原因。务必在CubeMX的Clock Configuration里确认系统时钟SYSCLK、APB1/APB2时钟以及定时器时钟TIMxCLK是否与你的计算一致。STM32F1系列APB1预分频不为1时定时器时钟会翻倍这个细节很容易被忽略。理解ARR和PSC的生效时机如果AutoReloadPreload未使能当你修改ARR值时新值可能立即生效打断当前的计数周期导致时间间隔出现一次异常。对于需要高精度定时的场合务必使能自动重载预装载并在修改ARR后通过软件触发一次更新事件__HAL_TIM_GENERATE_SW_EVENT或等待下次更新让新值在下一个周期生效。中断响应时间中断处理函数包括HAL库的中断服务和你的回调函数的执行需要时间。如果中断频率非常高比如100kHz中断处理代码又很长就可能丢失中断或导致定时不准。对于高频需求要考虑使用DMA或降低中断处理复杂度。5.4 资源冲突与优化建议一个定时器多个通道TIM3有4个通道可以同时输出4路同频率但占空比独立的PWM非常适合控制多路舵机或LED。只需在CubeMX中使能多个通道并在代码中分别启动HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_x)和设置比较值即可。使用不同定时器如果中断任务和PWM任务频率要求差异很大或者都需要高精度最好分配给两个定时器。比如用基本定时器TIM6做精确延时中断用通用定时器TIM3输出PWM。减少中断处理时间在中断回调函数里只做最必要、最快速的操作比如设置标志位、复制数据。耗时的运算如浮点计算、复杂逻辑判断、HAL_Delay应放到主循环中根据标志位来处理。利用DMA对于需要产生非常精确、连续变化的PWM序列如复杂调光曲线可以考虑使用定时器的DMA突发模式将预先计算好的CCR值数组通过DMA自动搬运到定时器寄存器完全解放CPU。通过以上步骤你应该能够顺利地在STM32上使用HAL库配置出稳定的定时器中断和PWM输出。这套流程和排查思路对于STM32的其他系列如F4、H7等也是基本通用的只是具体的时钟树和寄存器细节略有差异。关键是多动手多思考每个参数背后的意义遇到问题善用调试工具和芯片参考手册。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻