ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F407 HAL库定时器输入捕获:频率与脉宽测量实战

STM32F407 HAL库定时器输入捕获:频率与脉宽测量实战 手头正好有一块STM32F407开发板这两天在调一个转速测量的需求翻来覆去还是绕回定时器的输入捕获功能。这个东西说简单也简单说复杂也足够折腾人——网上教程大多停留在标准外设库的老写法换成HAL库之后很多细节对不上尤其是不熟悉中断回调机制的人往往卡在“明明配置没问题就是进不了中断”这种坑里出不来。这篇就把我在STM32F4系列上用HAL库配置定时器输入捕获的完整过程捋一遍从原理、CubeMX配置到测频率、测脉宽的代码实现再到实际调试中遇到的坑一次说清楚。输入捕获这个功能本质上解决的是一类高频出现的问题怎么测量一个外部信号的周期、频率或者一个脉冲的高电平持续时间。它可以被用来做按键消抖的时间计量、电机转速计算、红外遥控解码、超声波测距的回波时间测量等等。比起用GPIO外部中断加 systick 轮询输入捕获的精度更高也更省CPU资源因为时间戳的锁定完全由硬件完成不依赖中断响应延迟。这篇文章主要面向正在学习STM32定时器、准备用HAL库做项目开发的同学也适合那些从标准库迁移过来、被HAL库这套异步回调机制弄糊涂的工程师。我会按自己的实践路径来写尽量把为什么这么做也讲清楚而不只是贴代码。1. 输入捕获的原理与设计思路1.1 从定时器的“秒表”本质说起要把输入捕获讲明白得先从定时器的基本结构说起。STM32的定时器本质上就是一个不断累加的计数器时钟源来自内部的定时器时钟比如F407的APB1定时器时钟通常是84MHz经过预分频器PSC分频之后得到一个计数频率计数器CNT就按这个频率不断加1。把这个计数器想象成一块秒表它一直在走而输入捕获做的事情就是在外部信号出现指定边沿上升沿或下降沿的那一刻硬件自动把当前CNT的数值“拍照”保存到捕获/比较寄存器CCR里。因为是硬件完成的所以这个“拍照”的时点非常精确不会因为代码里中断响应延迟而出现抖动。这里有个关键认知输入捕获测的不是“信号本身怎么变化”而是“信号边沿出现时计数器走到了多少”。后续计算周期也好、脉宽也好本质上都是对两个CCR值做差再乘上计数时钟周期。明白了这一层后面所有代码逻辑都顺了。1.2 为什么HAL库的做法和标准库不一样标准外设库时代输入捕获的中断处理逻辑写在中断服务函数里捕获到边沿之后直接读CCR然后置标志位主循环轮询处理。逻辑是同步的思路非常直白。HAL库把这一套封装成了回调函数机制中断发生之后HAL库内部先做现场保护和状态管理然后调用一个我们自定义的弱函数——回调函数。所以我们不再需要在中断函数里写业务逻辑只需要在回调函数里响应就行。这套机制的好处是代码分层清晰中断函数里只做库层面的处理业务逻辑集中在回调里代码复用性好。但坑也在“回调”这两个字很多人不知道回调函数是需要自己重写的或者不知道不同捕获事件对应不同的回调函数名字导致中断发生了函数却空空如也。1.3 测频率和测脉宽的两种典型思路输入捕获的典型应用场景有两个测频率和测脉宽。这两个场景的思路是类似的但细节上有差异。测频率用的是“测周法”捕获相邻两个上升沿或下降沿得到两个CCR值差值就是信号的一个完整周期对应的计数值。然后用定时器计数频率除以计数值就得到信号频率。这个方法适合中低频信号频率越低精度越高如果信号频率太高一个周期内计数值太小误差就上去了。测脉宽则是捕获一个上升沿再捕获紧接着的下降沿两个CCR值之差就是高电平持续时间对应的计数值。这里要注意如果信号的占空比非常小或非常大需要确保定时器没有溢出。实际项目中经常把输入捕获和定时器更新中断配合起来用当定时器溢出时用一个变量做高16位的扩展这样就能测量远大于单次计数周期的信号。这一点在后面代码实现时我会详细讲。2. 实验环境与 CubeMX 配置细节2.1 硬件和软件环境准备我先交代一下我这次实验的硬件环境主控STM32F407VET6主频168MHz信号源开发板上的按键机械抖动作为脉冲源适合验证捕获边沿以及另一个定时器输出的PWM信号调试工具STM32CubeMX 6.xSTM32CubeIDE或者Keil MDK都行库版本STM32CubeF4 1.27.x建议手头准备一台示波器或逻辑分析仪调试输入捕获的时候能同时看到信号波形和计算出的频率值能帮你快速判断到底是信号问题还是代码问题。2.2 CubeMX里定时器参数的设置逻辑在CubeMX里配置定时器路径是Timers - TIM2 - Mode把通道1CH1的Mode选为Input Capture direct mode。其他通道如果要用也是类似操作。关键的是下方参数区几个配置项Prescaler预分频值PSC分频系数决定计数频率。设置成84-1时如果定时器时钟是84MHz计数频率就是1MHz对应分辨率为1微秒。Counter Period自动重装值ARR一般设为最大值65535也就是16位计数器的满量程避免计数溢出太频繁。Input Capture Channel选择要使用的通道。Polarity Selection选择捕获边沿上升沿或下降沿。IC Selection一般选Direct表示直接映射到对应的输入引脚。如果选Indirect可以借用其他通道这个在特殊场景下再用新手不建议动。Prescaler内部捕获分频这里的Prescaler指的是捕获信号的预分频也就是每几个边沿触发一次捕获。默认1分频即可也就是每个边沿都捕获。Filter输入滤波单位是时钟周期。如果信号有抖动可以设置滤波值比如设置为4或8能滤掉一些毛刺但也会引入一定的信号延迟。这四个“PSC”类参数容易搞混我实际配置的时候也经常要绕一下。简单区分Prescaler控制计数器走多快捕获的Prescaler控制每隔几个边沿才触发一次捕获。2.3 时钟树配置和中断开启定时器时钟来源要提前确认好。在CubeMX的Clock Configuration页签里APB1定时器时钟也叫Timer Clock往往和APB1外设总线时钟不同通常APB1总线时钟是42MHz时定时器时钟会翻倍成84MHzAPB2总线时钟84MHz定时器时钟168MHz。这个数值在计算频率时会用到一定要到CubeMX的时钟树页面确认实际值。别忘了在NVIC设置里勾选TIM2 global interrupt同时把中断优先级合理设置。这里有一点要提醒如果在同一个工程里用了多个定时器中断优先级错开一点避免中断互相嵌套导致逻辑混乱。3. HAL库输入捕获的代码实现与关键点拆解3.1 初始化代码与时钟使能CubeMX生成代码之后初始化的部分基本不用改。它会自动生成MX_TIM2_Init()函数并且在main()里调用。你要做的第一件事是在主循环之前启动定时器的输入捕获中断HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1);这句话的意思是启动TIM2的通道1输入捕获并且使能捕获中断。如果不加_IT后缀HAL_TIM_IC_Start()也能启动捕获但不会触发中断回调函数永远不会被调用。这个细节是我见过的最常见的“配置了却进不了中断”的原因。3.2 捕获回调函数的重写HAL库在中断处理完成后会调用一个弱函数HAL_TIM_IC_CaptureCallback()。我们要做的就是把这个弱函数重新实现加上自己的业务逻辑。uint32_t ic_val1 0, ic_val2 0; uint8_t ic_flag 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { if (ic_flag 0) { ic_val1 HAL_TIM_ReadCapturedValue(htim2, TIM_CHANNEL_1); __HAL_TIM_SET_CAPTUREPOLARITY(htim2, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); ic_flag 1; } else { ic_val2 HAL_TIM_ReadCapturedValue(htim2, TIM_CHANNEL_1); __HAL_TIM_SET_CAPTUREPOLARITY(htim2, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); ic_flag 0; } } } }这段代码的逻辑是第一次捕获上升沿记录值然后把捕获极性翻转为下降沿第二次捕获下降沿记录值再把极性翻回上升沿。这样交替捕获就得到了一个高电平的起止时间点差值就是脉宽。这里有一个很多人问过的问题为什么不在回调函数里直接算频率我的建议是不要在回调函数里做重活。回调函数运行在中断上下文如果在这里做除法、浮点运算、打印日志会明显拉长中断响应时间严重时可能导致其他中断丢失。正确做法是只在回调里保存原始CCR值和标志位回到主循环再做计算和业务处理。3.3 主循环里的计算逻辑保存好两个捕获值之后回到主循环就能做测频率和测脉宽的计算了。测脉宽的计算公式uint32_t pulse ic_val2 - ic_val1; float time_us (float)pulse / 1000000.0f; // 计数频率1MHz时单位us测频率的计算则是捕获两个相邻上升沿float freq 1000000.0f / (float)(ic_val2 - ic_val1);这里的频率单位是Hz计数频率是1MHz。如果信号周期大于65535个计数周期也就是大于65.535ms对应的频率大约是15Hz以下就会出现计数器溢出的问题测量结果会是错的。解决办法后面专门讲。3.4 使用定时器更新事件扩展量程解决溢出的标准做法是在定时器更新中断里计数溢出次数用32位变量拼接出一个扩展计数值。思路是维护一个全局变量overflow_cnt每次定时器向上计数溢出CNT从ARR回到0就在更新中断回调里给它加1。void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { overflow_cnt; } }这样真正的时间差就是uint32_t total_cnt (overflow_cnt * (TIM2_ARR 1)) ic_val2 - ic_val1;要注意的是捕获发生之后需要把溢出计数清零否则下一次测量会累加上之前的溢出次数。另外在读取overflow_cnt和ic_val的时候要防止它们在读取过程中被中断更新简单的处理是关中断读取读完再开。4. 单通道捕获的应用实践测量PWM频率与占空比4.1 单通道交替极性的测量方案用单个通道交替捕获上升沿和下降沿可以同时测量PWM信号的频率和占空比这是非常经典的做法。具体思路分四步配置为上升沿捕获捕获到第一次上升沿记录值A把极性翻转为下降沿启动一个溢出次数计数捕获到下降沿记录值B此时B - A就是高电平时间把极性翻转为上升沿捕获到第二次上升沿记录值C此时C - A就是一个完整周期频率 计数频率 / 周期计数值占空比 高电平计数值 / 周期计数值。这里的关键是在整个测量过程中溢出次数的累计要准确。如果在捕获到两个边沿之间定时器发生了溢出那么时间差就要把溢出次数折算进去。4.2 两通道同时捕获的方案对比除了单通道交替极性还可以用两个通道同时捕获CH1捕获上升沿CH2捕获下降沿。这样就不用反复切换极性时序逻辑也更干净因为两个通道各管各的。我实际对比过两种方案的优缺点单通道方案占用资源少一个通道就够了但逻辑上要处理极性切换的时机代码稍复杂一些双通道方案代码直观两个通道各自独立捕获不需要切换极性但从引脚的分配上多占用一个通道CubeMX配置时要注意选择正确的输入映射关系。如果你的信号源是标准PWM我推荐双通道方案容错率更高。但是如果你只有一个定时器通道空闲那单通道方案也完全够用。4.3 实际测量中的参数计算示例我这次实验用了TIM2计数频率设置为1MHz也就是PSC设为8384MHz / 84 1MHz。用TIM3输出一个PWM信号作为被测源设置TIM3输出频率为1kHz占空比60%。用输入捕获测出来的结果周期计数值约为1000高电平计数值约为600换算得到频率约1000Hz占空比约60%和设置值一致。如果改用10kHz的PWM周期计数值约为100此时每个计数单位代表1微秒测量精度就是周期的1%也就是0.1%的占空比误差。这说明了一个规律计数频率越高测量精度越高但计数器溢出的风险也越大。所以要根据实际被测信号合理选择PSC值。5. 常见问题与排查技巧实录5.1 问题一信号明明有回调就是不触发这是输入捕获最常见的坑。我在前面的代码里提到过可能的原因有几个按优先级排查第一确认HAL_TIM_IC_Start_IT()有没有被调用。很多人初始化完就等回调却忘了启动。第二确认NVIC中断有没有使能。CubeMX生成的代码默认会配置好但如果你手动改过中断优先级分组可能导致中断被屏蔽。第三确认信号波形是否满足输入电平要求STM32的输入引脚不能直接接5VTTL电平一般没问题但如果信号幅度过低是检测不到边沿的。另外还要确认定时器的时钟有没有使能。有些情况下CubeMX只生成了初始化结构体但你手动改了时钟配置导致定时器时钟为0计数器不走自然永远捕获不到。5.2 问题二频率测量结果跳动很大不稳定频率值跳动大多数情况和信号本身的质量有关。如果你测量的信号来自按键机械抖动或者来自接触不良的杜邦线边沿会有很多毛刺导致误触发。解决办法有几种一种是在CubeMX里配置输入滤波器Filter参数比如设置为0x0F表示8个时钟周期的滤波窗口毛刺短于这个时间会被滤掉另一种是在信号进入单片机之前加一个施密特触发器或RC低通滤波从根源上把波形整形干净。还有一种跳动原因是测量时间窗口太短。要获得稳定读数通常需要连续测量多个周期做平均。建议在代码里做一个滑动平均比如连续8次测量取平均值比单次测量值稳定得多。5.3 问题三高频信号测不准高频信号测不准的核心原因是计数器分辨率不够。一个周期内只有几个计数值量化误差占比很大。解决办法是提高计数频率。比如从1MHz提高到8MHz但这样计数器溢出得更快需要配合溢出计数。另一个办法是用“多周期测量法”不测相邻两个边沿的时间差而是测N个周期的时间差再除以N。比如测量100个上升沿经过的时间算出来的平均周期误差就小很多。这个方法在旋钮编码器测速、电机测速等场景下非常实用。5.4 问题四进入HardFault或程序卡死程序跑飞通常是不规范的中断操作引起的。比如在回调函数里调用了HAL_Delay()而HAL_Delay()依赖SysTick中断在中断上下文调用会造成死等。或者在回调函数里做了浮点运算但没有启用FPU编译器生成了软浮点代码速度慢且容易出问题。F4系列默认开启FPU但如果你用的老工程模板没开那就要注意。另外如果在回调里使用了printf()默认通过串口发送是阻塞式的在中断里执行会严重影响实时性高频捕获时会直接导致数据丢失甚至看门狗复位。5.5 排错技巧用逻辑分析仪验证信号链说一个我自己的土办法。遇到捕获值异常先别急着改代码拿一个已知频率的方波信号做测试用单片机内部另一个定时器输出PWM比如1kHz直接接到捕获输入引脚。这样排除了外部信号源的问题可以单独验证代码逻辑。如果还测不准再用逻辑分析仪或示波器看引脚上的波形确认引脚没有接错、电平正确。这套“信号源标准化”的排查方法帮我省了很多时间。6. 输入捕获在项目中的扩展应用6.1 应用到转速测量电机转速测量的基本原理是在电机转轴上装一个码盘码盘上均匀分布若干个齿或孔用光电传感器或霍尔传感器把转动信号变成脉冲再用输入捕获测量脉冲频率就能算出转速。例如一个码盘有20个齿测到脉冲频率为1000Hz转速就是 1000 / 20 50转/秒也就是3000转/分钟。这时候用上一节提到的多周期测量法能有效提高转速测量的稳定性。6.2 应用到红外遥控解码红外遥控的信号由一系列不同宽度的脉冲组成。NEC协议就是通过脉冲宽度编码560us的短脉冲加1680us的长脉冲表示不同逻辑值。用输入捕获配合高精度计数可以精确测量每个高低电平的持续时间再对照协议表解码。这个场景对捕获的精度要求比较高计数频率建议设置成4MHz以上。6.3 应用到超声波测距超声波模块HC-SR04回波信号是一个高电平脉冲宽度和距离成正比。用输入捕获测量这个高电平的持续时间除以声速再除以2就得到距离。这个应用其实只需要一次脉宽测量逻辑上非常简单但要注意测量量程对应的脉宽上限比如4米对应约23ms的脉宽计数器不能在这个时间内溢出。在F407上如果计数频率是1MHz23ms对应23000个计数远小于16位的65535所以不溢出可以直接测量。如果测量更远的距离或者使用更低的主频就要考虑溢出处理了。6.4 多通道输入捕获的设计建议如果你的项目需要同时测量多路信号的频率或脉宽可以考虑用同一个定时器的多个通道比如TIM2的CH1到CH4同时输入捕获。每个通道对应一个独立的外部引脚计数器是公共的因此测量结果天然同步非常适合需要比较多路信号时序的场景。但要注意同一个定时器的多个通道共用一个中断入口在回调函数里要区分是哪个通道触发的事件可以通过htim-Channel来判断。7. 最后再分享一点我的体会这次实验做下来我有一个很深的感受HAL库的封装虽然让代码看起来更“高级”但它并没有降低对底层原理的理解要求。如果你不懂定时器计数器的工作方式不懂边沿捕获的硬件机制光是照着CubeMX点一遍、复制几行回调代码那遇到问题时依然无从下手。所以我的建议是在做输入捕获实验的时候花点时间把参考手册里Timer那部分读一遍尤其是捕获比较通道的结构图把PSC、ARR、CCR这几个寄存器之间的关系彻底搞清楚。这些知识在以后做输出比较、PWM、编码器模式的时候都会用到是一次投入、长期回报的学习。另外调试这种偏底层的功能耐心比技巧更重要。我调试这段代码的时候第一次测出来的频率值差了整整一倍排查了半天才发现是PSC少减了1导致实际计数频率和预期不一样。像这种低级的算数错误不仔细看计算过程真的很难发现。如果你在配置过程中遇到什么奇怪的坑欢迎在留言区讨论。每个人的应用场景不同遇到的问题也五花八门互相交流往往比一个人死磕效率高得多。
返回列表