拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32 HAL库基本定时器(TIM6/TIM7)入门:从时钟链到中断实战

1. 项目概述为什么从基本定时器开始如果你刚开始接触STM32的HAL库面对一堆外设和复杂的配置选项可能会有点无从下手。我的建议是别急着去搞什么PWM、输入捕获这些高级玩法先把基本定时器这个最纯粹、最核心的模块吃透。这就像学武功先扎马步学编程先理解变量和循环一样。基本定时器特别是TIM6和TIM7是STM32定时器家族里结构最简单、功能最单一的成员它们不负责输出复杂的波形也不去测量外部信号的频率它们唯一且最重要的任务就是精准地计时并产生中断。听起来很简单对吧但恰恰是这份“简单”让它成为了理解STM32定时器乃至整个HAL库中断、时钟体系的最佳切入点。很多朋友在调试PWM时发现波形不对或者输入捕获时读数飘忽不定根源往往是对定时器最基础的计数、重装载、时钟预分频这些概念理解不透。通过搞定基本定时器你不仅能掌握如何配置HAL库的定时器结构体更能深刻理解“时钟源-预分频器-计数器-自动重载寄存器”这条数据流以及中断回调函数是如何被触发的。当你把这些底层逻辑刻在脑子里再去玩转高级定时器、通用定时器就会有一种“降维打击”的感觉很多问题看一眼配置就能猜到原因。所以这篇内容我们不谈花哨的应用就聚焦于STM32 HAL库下的基本定时器TIM6/TIM7我会带你从CubeMX配置开始手把手写代码并深入讲解每一个参数背后的意义最后分享几个我调试时踩过的坑和总结的技巧。无论你是刚拿到一块STM32开发板的新手还是已经用过标准库想转HAL库的开发者相信都能从中获得扎实的收获。2. 核心概念与硬件原理拆解2.1 基本定时器TIM6/TIM7的定位与特点在STM32庞大的定时器家族中定时器被分成了高级定时器如TIM1, TIM8、通用定时器如TIM2-TIM5和基本定时器TIM6, TIM7。你可以把它们想象成汽车高级定时器是功能齐全的越野车有四驱互补PWM、差速锁刹车功能、各种驾驶模式编码器接口等通用定时器是家用轿车能开能跑配置灵活输入捕获、输出比较、PWM都有而基本定时器就是最基础的卡丁车它只有一个目标——跑圈计数并在固定的圈数自动重载值给你一个信号更新中断。TIM6和TIM7在结构上几乎一模一样它们的主要特点包括16位自动重装载递增计数器计数器从0开始每个时钟脉冲加1直到达到你设定的重装载值ARR然后产生一个更新事件UEV计数器清零重新开始如此循环。16位可编程预分频器用于对输入时钟进行分频。这是定时器精度的关键。如果系统主频是72MHz你希望定时器每1ms计数一次就需要通过预分频器把72MHz的时钟“降速”。触发DAC的专用同步电路这是TIM6/TIM7一个特别的“兼职”它们可以内部连接到DAC为数据转换提供精确的触发时钟。不过在我们入门阶段可以先专注于其定时核心功能。在更新事件时产生中断/DMA请求这是基本定时器最常用的功能。当计数器溢出即达到ARR值时会产生一个更新事件这个事件可以触发中断让你在中断服务函数里执行特定代码比如翻转一个LED灯或者触发DMA进行数据传输。注意基本定时器没有外部时钟输入引脚、没有输入捕获/输出比较通道、也不能产生PWM波形。它的纯粹性正是我们学习的优势。2.2 定时器核心时钟链从HCLK到CK_CNT理解定时器必须理清时钟的来龙去脉。这是很多初学者配置后定时不准的根源。对于基本定时器TIM6/TIM7它们的时钟源CK_PSC通常来自于APB1总线时钟PCLK1。这里有一个关键点在STM32的时钟树中如果APB1的预分频系数不为1即1分频那么连接到定时器的时钟会是APB1时钟的2倍。这是硬件设计上的一个“福利”目的是在总线时钟较低时仍能为定时器提供较高的时基精度。假设我们的系统主频SYSCLK是72MHzAPB1预分频器设置为2分频那么PCLK1 SYSCLK / 2 36MHz。因为APB1预分频系数是2不等于1所以定时器时钟CK_PSC PCLK1 * 2 72MHz。这个72MHz的CK_PSC就是预分频器的输入时钟。预分频器PSC是一个16位的寄存器你可以设置一个0到65535之间的值。预分频器实际的分频系数是PSC 1。也就是说如果你设置PSC71那么分频系数就是72。那么驱动计数器CNT递增的最终时钟CK_CNT的频率就是CK_CNT CK_PSC / (PSC 1)继续上面的例子CK_PSC72MHzPSC71则CK_CNT 72MHz / 72 1MHz。这意味着计数器每1微秒1/1MHz加1。2.3 定时时间的计算ARR与PSC的配合定时器一次完整的计数周期从0计数到ARR产生更新事件所花费的时间就是我们常说的“定时周期”。定时周期 T (ARR 1) * (PSC 1) / CK_PSC这里ARR1是因为计数器从0计数到ARR总共经历了ARR1个计数脉冲。同理PSC1是预分频系数。举个例子我们想用TIM6实现一个1ms0.001秒的定时中断。已知CK_PSC 72MHz。计算总计时脉冲数 T * CK_PSC 0.001s * 72,000,000 Hz 72,000。我们需要将72,000这个数分解为(ARR1) * (PSC1)的形式。为了计算方便我们通常先设定PSC的值来得到整数MHz级别的CK_CNT。若令 PSC 71则 PSC1 72。那么 CK_CNT 72MHz / 72 1MHz。此时1ms需要的计数脉冲数 1ms * 1MHz 1000。所以 ARR 1000 - 1 999。验证T (9991) * (711) / 72MHz 1000 * 72 / 72,000,000 0.001s。完美。在代码中我们设置的就是PSC寄存器的值71和ARR寄存器的值999。HAL库的初始化函数会帮我们处理好这些关系。3. 开发环境搭建与CubeMX配置详解3.1 工程创建与时钟树配置我们以STM32F103C8T6蓝色战舰核心板为例使用STM32CubeIDE它集成了CubeMX进行配置。创建新工程选择对应的芯片型号。在Project Manager标签页给工程起个名字比如TIM6_Basic_Demo选择好工程路径和工具链默认MDK-ARM或STM32CubeIDE均可。配置系统核心SYS在SYS标签下将Debug改为Serial Wire。这非常重要否则芯片可能被锁死无法再次下载程序。配置时钟RCC将High Speed Clock (HSE)选择为Crystal/Ceramic Resonator。这表示我们使用外部8MHz晶振。进入Clock Configuration标签页这是整个系统时钟的核心。对于F103常见的配置是在PLL Source选择HSE。将PLL Multiplication Factor设置为9倍频因为HSE8MHz9倍后为72MHz。将System Clock Mux选择为PLLCLK。将APB1 Prescaler设置为2分频这样PCLK136MHz定时器时钟自动倍频为72MHz。最终HCLK系统时钟显示为72MHz。这个72MHz就是我们的SYSCLK也是定时器时钟CK_PSC的来源。3.2 TIM6参数化配置实战在Pinout Configuration标签页左侧找到Timers点击TIM6。时钟源Clock Source保持为Internal Clock。基本定时器只有内部时钟源。参数配置Parameter SettingsPrescaler (PSC - 16 bits value)这是我们计算中的PSC寄存器值。输入71。注意CubeMX这里填的就是寄存器值硬件会自动加1。Counter Mode保持Up递增模式。基本定时器只支持递增。Counter Period (AutoReload Register - 16 bits value)这是我们计算中的ARR寄存器值。输入999。同样硬件操作的是ARR但计数周期是ARR1。auto-reload preload选择Enable。这个功能非常重要它意味着对ARR或PSC的修改会在下一次更新事件时才生效而不是立即生效。这可以避免在定时器运行中途修改周期导致计数器错乱产生毛刺。强烈建议始终启用。中断配置NVIC Settings勾选TIM6 global interrupt下的Enabled复选框。这样当定时器更新事件发生时才会触发中断执行我们的中断服务程序。配置完成后点击右上角的GENERATE CODE生成工程代码。CubeMX会帮我们初始化好时钟、GPIO如果有用到、以及TIM6的外设和中断。3.3 生成的代码结构解析打开生成好的工程进入main.c我们可以看到CubeMX/HAL库风格的代码。SystemClock_Config()这个函数包含了我们刚才在图形界面配置的所有时钟树设置将系统时钟初始化为72MHz。MX_TIM6_Init()这是TIM6的初始化函数。核心是填充一个TIM_HandleTypeDef结构体htim6并调用HAL_TIM_Base_Init()。htim6.Instance TIM6; htim6.Init.Prescaler 71; htim6.Init.CounterMode TIM_COUNTERMODE_UP; htim6.Init.Period 999; htim6.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE;这个结构体包含了定时器的所有配置信息是HAL库操作外设的核心。在main()函数中初始化完成后我们需要手动启动定时器并开启其更新中断。HAL_TIM_Base_Start_IT(htim6); // 启动定时器并开启中断4. 核心代码编写与中断处理4.1 启动定时器与使能中断仅仅初始化htim6定时器并不会开始工作。必须在main()函数的初始化部分之后显式地启动它。/* 在初始化所有外设之后进入主循环之前 */ if (HAL_TIM_Base_Start_IT(htim6) ! HAL_OK) { /* 启动错误处理 */ Error_Handler(); }HAL_TIM_Base_Start_IT()这个函数做了两件事1. 使能定时器计数器TIMx-CR1寄存器的CEN位置1。2. 使能定时器的更新中断TIMx-DIER寄存器的UIE位置1。4.2 重写更新中断回调函数HAL库采用了回调函数Callback机制来处理中断。我们不需要直接去修改stm32f1xx_it.c中的TIM6_IRQHandler函数那个函数里已经调用了HAL库的通用中断处理函数HAL_TIM_IRQHandler()。这个通用函数会判断中断类型然后调用对应的弱定义__weak的回调函数。我们需要做的是在main.c或者自己的用户文件中**重写Override**这个弱定义的回调函数。/* 在main.c的 /* USER CODE BEGIN 4 */ 和 /* USER CODE END 4 */ 之间添加 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { /* 判断是哪个定时器触发的更新中断 */ if (htim-Instance TIM6) { /* 用户代码区这里执行每1ms需要做的事情 */ // 例如翻转一个LED灯 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); /* 也可以在这里进行软件计时 */ static uint32_t time_ms 0; time_ms; if(time_ms 1000) // 每1000次中断即1秒 { time_ms 0; // 执行每秒一次的任务 } } /* 如果你的工程有多个定时器可以继续用else if判断 */ }关键点这个函数的名字是固定的HAL_TIM_PeriodElapsedCallback不能写错。传入的参数是一个指向定时器句柄的指针。通过判断htim-Instance是TIM6还是TIM7来区分中断源。在这个函数里执行的操作必须尽可能短小精悍。中断服务函数的原则是“快进快出”长时间的操作如HAL_Delay、打印大量信息会阻塞其他中断和主程序导致系统响应异常。4.3 一个完整的1ms定时LED闪烁示例假设我们已经在CubeMX中配置好了PC13引脚板载LED为推挽输出模式并命名为LED。完整的main.c用户代码部分如下/* Private variables ---------------------------------------------------------*/ TIM_HandleTypeDef htim6; /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_TIM6_Init(void); int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM6_Init(); /* 启动TIM6的基础定时中断模式 */ if (HAL_TIM_Base_Start_IT(htim6) ! HAL_OK) { Error_Handler(); } while (1) { /* 主循环可以处理其他非实时任务 */ // 例如检测按键处理通信等 } } /* 定时器更新中断回调函数 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 每1ms翻转一次LED } }编译下载后你会看到LED灯以极高的频率闪烁1ms亮1ms灭由于人眼的视觉暂留你看到的可能是一个亮度减半的常亮灯。为了看到明显的闪烁我们可以修改逻辑在回调函数里进行计数累计1000次即1秒才翻转一次LED。void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { static uint16_t count 0; count; if(count 1000) { count 0; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 每1秒翻转一次LED } } }5. 进阶应用与调试技巧5.1 动态修改定时周期有时我们需要在程序运行中改变定时器的定时间隔。比如根据传感器数据动态调整采样频率。切记不能直接修改htim6.Init.Period或htim6.Instance-ARR正确的做法是使用HAL库提供的API。停止定时器后修改适用于可以接受定时短暂停止的场景HAL_TIM_Base_Stop_IT(htim6); // 先停止定时器和中断 __HAL_TIM_SET_AUTORELOAD(htim6, 1999); // 修改ARR为1999此时周期变为2ms __HAL_TIM_SET_PRESCALER(htim6, 35999); // 修改PSC为35999此时CK_CNT72MHz/360002KHz HAL_TIM_Base_Start_IT(htim6); // 重新启动注意直接修改PSC或ARR寄存器可能会导致计数器立即重置或产生不期望的更新事件。使用__HAL_TIM_SET_xxx宏是更安全的方式它们会操作底层寄存器。在停止定时器后修改是最稳妥的。利用自动重装载预装载功能适用于需要平滑过渡的场景 如果我们初始化时已经使能了AutoReloadPreloadCubeMX默认使能那么我们可以通过API在运行时设置新的ARR值这个新值会先被写入预装载寄存器等到下一次更新事件发生时才会真正生效并覆盖当前的ARR寄存器。这避免了周期突变。// 定时器仍在运行 __HAL_TIM_SET_AUTORELOAD(htim6, 1999); // 新ARR值写入预装载寄存器 // 当下一个更新事件到来时定时器周期自动变为2ms过程无毛刺5.2 高精度延时函数实现虽然HAL库提供了HAL_Delay()但它依赖于SysTick中断精度通常为1ms且在中断中调用可能导致死锁。我们可以用基本定时器实现一个更灵活、可中断的微秒级延时函数。首先初始化一个定时器比如TIM7用于延时配置其时钟为1MHzPSC71ARR设置为最大值65535。// tim_delay.c static TIM_HandleTypeDef *s_delay_tim; // 用于延时的定时器句柄指针 void Delay_TIM_Init(TIM_HandleTypeDef *htim) { s_delay_tim htim; HAL_TIM_Base_Start(s_delay_tim); // 启动但不开启中断 } void Delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(s_delay_tim, 0); // 计数器清零 while(__HAL_TIM_GET_COUNTER(s_delay_tim) us); // 等待计数值达到目标微秒数 } void Delay_ms(uint16_t ms) { for(uint16_t i0; ims; i) { Delay_us(1000); // 注意1000us循环会有少量误差 } }在main.c中初始化并调用// 初始化TIM7PSC71, ARR65535 MX_TIM7_Init(); Delay_TIM_Init(htim7); // 使用 Delay_us(500); // 延时500微秒这个延时函数是阻塞式的但在延时期间不影响中断。它的精度取决于定时器时钟的精度1MHz时理论精度为1us。注意Delay_ms函数由于循环调用会有函数调用开销误差比Delay_us大对于长延时建议直接用定时器中断累加计数的方式实现非阻塞延时。5.3 使用DMA进行定时数据搬运基本定时器的更新事件可以触发DMA请求。这在一些需要定期、精确搬运数据的场景非常有用比如定期刷新DAC输出形成波形或者定期从ADC读取数据到缓冲区而完全不需要CPU干预。配置步骤以TIM6触发DMA搬运数据到DAC为例CubeMX配置配置TIM6例如1ms周期。在DMA Settings标签页点击Add选择TIM6_UP作为DMA请求。选择方向为Memory To Peripheral。数据宽度根据DAC数据寄存器宽度选择如Word。模式选择Circular循环模式这样DMA会一直自动搬运。代码编写在main.c中定义一个数据缓冲区比如uint32_t dac_data_buffer[100];并填充好波形数据。使用HAL_DMA_Start_IT()或HAL_DMA_Start()将DMA与定时器更新事件关联起来。启动定时器HAL_TIM_Base_Start(htim6);(注意这里用Start而不是Start_IT因为中断由DMA完成)。之后每当TIM6计数溢出DMA就会自动将缓冲区中的下一个数据搬运到DAC的数据寄存器中CPU可以完全去处理其他任务。6. 常见问题排查与实战心得6.1 定时不准从时钟树开始检查这是最常见的问题。症状是设定的1ms中断实际用逻辑分析仪测量可能是1.1ms或0.9ms。第一步确认系统时钟SYSCLK。在main()函数初始化后调用SystemCoreClock变量或者通过HAL_RCC_GetSysClockFreq()函数打印出来看是不是你预期的72MHz或其他值。第二步确认APB1时钟和定时器时钟。查看RCC-CFGR寄存器或者计算SystemCoreClock / (APB1预分频系数)得到PCLK1。记住如果APB1预分频系数不为1定时器时钟是PCLK1的2倍。第三步核对CubeMX配置。双击打开.ioc文件再次检查TIMx的Prescaler和Counter Period值确保计算正确。一个快速验证公式定时周期(秒) (Period1)*(Prescaler1) / TIMx时钟频率(Hz)。第四步检查中断负担。如果中断服务函数HAL_TIM_PeriodElapsedCallback里执行的操作太耗时可能导致下一次中断被延迟响应。确保中断函数简洁。6.2 中断不触发NVIC与启动顺序是关键定时器配置好了代码也写了但LED就是不闪回调函数好像从来没进去过。检查CubeMX的NVIC配置确保TIMx global interrupt已经勾选Enabled。生成的代码会在MX_TIMx_Init()函数末尾自动调用HAL_NVIC_SetPriority()和HAL_NVIC_EnableIRQ()。检查启动函数你调用HAL_TIM_Base_Start_IT(htimx)了吗这个函数必须在初始化之后主循环之前调用。只初始化不启动定时器是不会工作的。检查回调函数名和判断条件回调函数必须是void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)。里面的判断条件if(htim-Instance TIMx)是否正确曾经有朋友把TIM6错写成htim6导致判断永远不成立。使用调试器设置断点在TIMx_IRQHandler中断服务函数在stm32f1xx_it.c里和你的回调函数里都打上断点。如果连TIMx_IRQHandler都没进去说明中断没触发问题在NVIC或启动如果进了TIMx_IRQHandler但没进你的回调说明回调函数没被正确重写可能是函数名拼错或者放在了不被链接的源文件里。6.3 如何测量定时器中断的实际执行时间想知道你的中断服务函数到底花了多长时间一个简单的方法是使用一个空闲的GPIO引脚。在回调函数一开始将某个GPIO引脚比如PA0置高。在回调函数结束前将PA0置低。用示波器或逻辑分析仪测量这个引脚高电平的脉宽就是中断服务函数的执行时间。void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // 开始计时 // ... 你的中断处理代码 ... HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // 结束计时 } }如果这个时间接近甚至超过你的定时周期那系统就危险了必须优化中断服务函数的代码。6.4 HAL库与标准库配置的思维转换从标准库StdPeriph转过来的朋友可能会对HAL库的结构体初始化方式感到不习惯。记住一个对应关系标准库的TIM_TimeBaseInitTypeDef对应 HAL库的TIM_HandleTypeDef结构体中的Init成员。标准库直接操作TIMx-ARR等寄存器HAL库则通过htimx.Instance-ARR来访问但更推荐使用__HAL_TIM_SET_AUTORELOAD()这样的宏来保证安全。标准库的中断服务函数里需要手动清除标志位TIM_ClearITPendingBit()而HAL库在HAL_TIM_IRQHandler()内部已经帮你处理好了这些清理工作你只需要关心回调函数。我个人最深刻的体会是HAL库的“句柄”思维。每个外设TIM、UART、SPI都有一个对应的HandleTypeDef结构体句柄如htim6。这个句柄贯穿了该外设的整个生命周期初始化、启动、中断回调、状态获取、停止、反初始化。所有针对这个外设的操作都通过向HAL库函数传递这个句柄的指针来完成。这种封装提高了代码在不同STM32系列间的可移植性虽然牺牲了一点效率和代码体积但对于大多数应用和快速开发来说利大于弊。理解并适应这种“面向句柄”的编程模式是玩转HAL库的关键。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门