
简介为STM32开发者准备的频率测量代码资源基于定时器输入捕获原理解决外部脉冲或时钟信号频率检测需求。资源提供完整工程文件涵盖TIM定时器模式选择、输入捕获通道配置、中断服务程序设计以及连续捕获时间戳差值计算周期与频率的实现方法并给出HAL库关键API调用示例如HAL_TIM_IC_Init、HAL_TIM_IC_ConfigChannel与HAL_TIM_IC_GetCapturedValue稍作适配即可移植到实际项目中。资源包为zip压缩包共125个文件以.h头文件与.c源文件为主同时包含uvproj工程配置、hex烧录文件及编译中间文件等压缩包整体约1.87MB。目前已有2143人学习下载适合具备一定STM32基础、希望快速掌握定时器测频技术的开发者也可作为学习定时器中断及信号测量机制的实战参考。 做STM32开发的人迟早都要写一次测量频率的代码。不管是给毕设做转速测量还是给工控板卡采集编码器输出甚至只是想把一个未知波形大概摸清楚stm32定时器就是你手里最趁手的工具。这篇文章不绕弯子直接从方案选型、原理、HAL库代码到常见踩坑给你捋一遍示例基于STM32F103思路通用于F4/G0系列。看完之后你不仅能写出能跑的测量频率代码还能知道为什么这么写、测出来不准时该往哪个方向排查。1. 先判断你的信号测周期还是数脉冲1.1 测量频率的两个基本思路频率的本质是单位时间内重复次数。STM32测频率绕不开两种思路一是数脉冲固定一个时间窗口比如1秒数这个窗口里来了多少个上升沿个数就是频率二是测周期捕获两次相邻上升沿的时间差得到周期T再用f1/T换算出频率。这两条路各有各的适用场景没有谁绝对更好。数脉冲的思路适合高频信号因为窗口内脉冲多量化误差就小测周期的思路适合低频信号因为周期长计时分辨率占比就高。你要是一上来不分青红皂白用捕获法去测1MHz的信号大概率会发现捕获中断根本忙不过来或者测量值跳得像过山车。1.2 三种硬件实现方案对比STM32的定时器模块很灵活同一个测频需求可以用三种方式实现定时器外部计数模式定时器由外部引脚脉冲驱动计数配一个基本定时器做时间窗口。窗口结束时读计数器得到脉冲个数。输入捕获模式配置某个通道捕获上升沿记录捕获寄存器CCR的值相邻两次捕获值之差就是周期。PWM输入模式其实是输入捕获的变种用两个通道同时捕获周期和占空比。如果只需要频率没必要用它徒增配置复杂度。这三种方式在STM32F103上都能实现但占用的定时器资源、引脚、精度特性完全不同。我把它们整理成一张表方案测量原理适合频率范围核心误差来源占用资源外部计数模式窗口内数脉冲高频1kHz窗口边界±1个脉冲2个定时器输入捕获模式相邻边沿时间差低频10kHz计时分辨率、边沿抖动1个定时器1个通道PWM输入模式捕获周期与占空比低频-中频同输入捕获1个定时器2个通道外部计数模式的精度公式很好算窗口T秒内数到N个脉冲频率fN/T。误差最多就是窗口边界处多一个少一个脉冲所以f的绝对误差是1/T。1秒窗口就是±1Hz0.1秒窗口就是±10Hz。这个特性让它天生适合高频信号信号频率越高相对误差越小。输入捕获模式的精度取决于预分频后的计时分辨率。假设定时器时钟72MHz预分频71那么计数器每1us加1。被测信号周期如果是10ms那么两次捕获差约10000误差±1us就是0.01%精度很高。但如果被测信号是100kHz周期10us计数只有10个误差直接变成10%。1.3 实际项目里的选型经验我知道很多人上来就想找“万能方案”现实是没有。我在实际项目里的做法是先粗测频率范围再决定具体方案。手里有示波器就用示波器看没有示波器就先写一个临时外部计数程序窗口设长一点把频率量级估出来。小于1kHz的走输入捕获大于1kHz的走外部计数如果有占空比测量需求再考虑PWM输入模式。工程上还有一点容易被忽略测的是干净方波还是带噪声的传感器信号。像编码器输出、霍尔传感器输出波形沿往往不够陡甚至带有抖动。这种信号直接用定时器捕获容易一次性触发两次测出来的频率偏高。后面会专门讲怎么处理波形整形的问题。2. 测频之前必须搞清楚的时钟和引脚2.1 定时器时钟从哪里来很多初学者把定时器当作一个黑盒直接在CubeMX里给Prescaler和Period填数字。但测频率这件事时钟源错了后面全白搭。STM32F103的定时器时钟分配有个特点挂在APB1上的定时器TIM2-TIM7等如果APB1预分频不为1定时器时钟是APB1的两倍如果APB1预分频是1那定时器时钟就等于APB1。举例来说系统时钟配到72MHz时APB1一般分频到36MHz这时TIM2-TIM7的时钟不是36MHz而是72MHz。你要是不了解这条规则以为定时器时钟是36MHz预分频和周期算出来的闸门时间就偏了一倍最后的频率测量结果也会跟着偏一倍。建议在代码里不要硬编码时钟数值直接调HAL_RCC_GetPCLK1Freq()或HAL_RCC_GetPCLK2Freq()再根据分频关系换算成定时器时钟。这样即使换到F4或G0系列、改了系统时钟测频代码的时钟换算部分也不会出错。2.2 引脚映射波形进不来一切白搭F103的定时器输入引脚有好几组默认引脚和重映射引脚不同。比如TIM2_CH1默认在PA0重映射后可以到PA15TIM3_CH1默认在PA6重映射后到PB4。你用CubeMX配置时它会自动分配但如果是手写寄存器或移植别人的工程就要特别注意引脚是否被复用成了普通GPIO。这里有个常见坑PA15、PB3、PB4这几个引脚在F103上默认是JTAG调试引脚上电后不是普通GPIO。你要是把它们当输入捕获引脚用必须先把AFIO的SWJ配置改为关闭JTAG、保留SWD否则引脚根本不受你控制。很多人的程序下载过一次之后第二次就报“no stm32 target found”也是这个原因后面详细说。2.3 晶振电容和频率基准的关系测频率的本质是拿被测信号和单片机内部时钟做对比。单片机内部时钟不准测得再努力也白搭。F103的外部高速晶振HSE一般是8MHz晶振旁边那两个负载电容不是随便焊上去的。匹配电容选得不对晶振起振频率会偏移虽然偏移量很小但高频测量时会被放大。无源晶振匹配电容的工程经验公式是C1 C2 2 × CL - Cstray其中CL是晶振规格书里的负载电容Cstray是芯片引脚和PCB走线的寄生电容一般取3~5pF。比如8MHz晶振规格书标CL12pF那么C1C2约等于2×12-420pF。实际项目中用12pF到22pF都常见关键是要选质量靠谱的晶振和电容别在频率基准上省钱。3. 两种核心测频代码的完整实现3.1 外部计数法1秒闸门测高频先说我最常用的外部计数方案。思路是TIM3工作在外部时钟模式1由TI1引脚默认PA6需要复用为TIM3_CH1的上升沿驱动计数器。然后TIM6配置为1秒中断一次在TIM6中断里读TIM3的计数器值这个值就是1秒内的脉冲个数数值上等于频率。CubeMX里TIM3的配置要点Clock Source选择Internal ClockSlave Mode选择External Clock Mode 1Trigger Source选择TI1FP1Prescaler0Counter Period0xFFFF。注意这里的Clock Source虽然是Internal Clock但实际上外部脉冲会通过触发源驱动计数这是CubeMX中配置外部计数模式的常规做法。关键代码如下// TIM3初始化省略重点看中断处理逻辑 volatile uint32_t overflowCount 0; volatile uint32_t measuredFreq 0; // TIM3更新中断外部脉冲导致计数器自然溢出时进入 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { overflowCount; } else if (htim-Instance TIM6) { uint32_t totalPulse overflowCount * 65536 __HAL_TIM_GET_COUNTER(htim3); overflowCount 0; measuredFreq totalPulse; // 窗口1秒脉冲数就是Hz __HAL_TIM_SET_COUNTER(htim3, 0); } }这段代码里最容易漏掉的是overflowCount。外部脉冲频率如果超过65535Hz1秒内TIM3会溢出多次你不统计溢出次数读出来的频率就会周期性归零。TIM6的1秒窗口到了之后先算总脉冲数再把overflowCount和CNT清零保证下一个窗口从零开始计。main函数里启动两个定时器HAL_TIM_Base_Start_IT(htim6); // 1秒闸门 HAL_TIM_SlaveConfigSynchro(htim3, TIM_SLAVEMODE_EXTERNAL, sSlaveConfig); HAL_TIM_Base_Start(htim3); // 外部计数启动实测下来用这个方案测10kHz以上的信号非常稳误差就是±1Hz。唯一要注意的是TIM6的窗口不要用HAL_Delay去凑中断里也不能做耗时操作否则窗口就不准了。3.2 输入捕获法低频信号测周期低频信号用数脉冲方案很不划算比如50Hz信号1秒窗口只有50个脉冲理论误差就有1Hz相对误差2%。这种情况改用输入捕获测周期精度高得多。配置TIM4_CH1默认PA6注意和前面TIM3外部计数别冲突预分频设为71这样计数器频率是1MHz计时分辨率1us。周期寄存器0xFFFF捕获通道选择上升沿触发。// TIM4捕获中断 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM4) { static uint16_t lastCapt 0; uint16_t cur __HAL_TIM_GET_COMPARE(htim4, TIM_CHANNEL_1); uint16_t diff; if (cur lastCapt) { diff cur - lastCapt; } else { diff 65535 - lastCapt cur 1; } lastCapt cur; // diff单位是1usfreq单位是Hz measuredFreq 1000000UL / diff; } }启动部分配置好捕获通道后调用HAL_TIM_IC_Start_IT(htim4, TIM_CHANNEL_1)开启捕获中断。这段代码处理了计数器回绕的情况。如果cur小于lastCapt说明计数器从65535回绕到了0周期跨越了一次回绕要把差值补回来。实际中断回调里还有更细的坑比如static变量lastCapt在系统初始化时要记得清零否则第一次捕获算出来的频率可能是错的。输入捕获方案测量50Hz信号理论频率值是1000000/2000050Hz±1us的量化误差带来约0.005%的相对误差和外部计数法的2%相比提升非常明显。但要注意如果信号频率上升到几百kHzdiff值变得很小此时要适当降低预分频提高计时分辨率或者干脆切回外部计数法。3.3 精度调整的实操心得两类方案的精度调整完全遵循一个原则让测量窗口内的计数值尽量大。外部计数法就是拉长闸门时间1秒不够就2秒代价是响应变慢输入捕获法就是提高计时分辨率预分频往小调代价是计数器溢出变快处理回绕的代码不能少。实测过程中我还发现输入捕获法测频率时测量结果在相邻几个值之间跳动是正常的这是±1量化的正常表现。如果你看到结果跳变量很大那不是量化问题大概率是信号沿不干净需要去查波形。真正要做到高精度可以考虑多次测量取平均或者用DMA批量捕获多组周期再做处理但一般应用没必要上这么复杂。4. 实测中容易踩的坑和排查速查表4.1 频率乱跳波形整形问题很多人测频率遇到的最大问题是“读数不稳定”。代码写得没问题定时器配置也对但频率值一下高一下低。我遇到最多的情况是信号沿太差导致的。传感器输出、电机编码器输出这类信号上升沿可能非常缓慢而STM32引脚的施密特触发器触发阈值是固定的信号缓慢爬升时很容易在阈值附近抖动造成一次触发变成两次。解决办法有三个层次最简单的在输入引脚和GND之间加一个10k下拉电阻把悬空噪声拉低效果更好的加一个施密特触发器芯片比如74HC14做波形整形把缓慢变化的信号变成陡峭的方波如果有条件优先处理源头看信号是不是负载太重串一个几百欧的电阻再进引脚。我在测电机编码器时遇到过这类问题加了74HC14之后读数稳定了很多。4.2 delay卡死测频程序里的经典翻车热词里有“stm32延时函数delay卡死”在测频程序里真的太常见了。很多人的第一版代码喜欢在while循环里先HAL_Delay(1000)再去读计数值。结果程序跑起来经常卡在HAL_Delay里出不来或者延时时间完全不对。原因基本出在时钟配置上。HAL_Delay依赖SysTickSysTick的时钟源在SystemClock_Config里没更新的话HAL_Delay的时间基准就是错的。更隐蔽的问题是如果某个中断优先级太高在中断服务函数里长时间占用CPUSysTick的中断无法及时响应HAL_Delay就会表现出“卡死”。我给你的建议是测频程序里不要用HAL_Delay做闸门时间老老实实开一个硬件定时器做时间窗口。这样既精确又不会跟SysTick产生耦合。如果只是调试时用也要确认SystemClock_Config正确并且不要在中断回调里放HAL_Delay。4.3 连不上调试器error: no stm32 target found!热词里有一条“error: no stm32 target found! if your product embeds debug authentication”这个报错我见得太多了。最典型的场景程序里把某个引脚配置成普通GPIO恰好这个引脚是SWDIO或SWCLK第一次烧录成功程序跑起来后占用了调试引脚第二次就再也连不上了。F103上要特别注意PB3、PB4、PA15这几个引脚的复用配置。如果软件里执行了类似“完全关闭SWJ”的操作ST-Link会彻底失去和芯片的通信通道。遇到这种情况处理方法是用stlink工具配合复位信号连接按住板子复位键不放点击烧录/连接等软件开始通信后再释放复位键。另一种方案是把BOOT0拉高进入ISP模式擦除芯片后再恢复正常模式。不管哪种方案只能算一次性的救急手段。治本的办法是工程项目里默认保留SWD调试功能只在量产固件里才关闭调试引脚。4.4 中断进一会就不进了有朋友遇到过这样的情况程序刚跑起来能测量过几秒中断就不进来了频率值停留在最后读数上。排查思路分两步走。第一看NVIC优先级配没配对定时器中断优先级如果被其他中断长期抢占回调执行会非常滞后第二更常见的是中断回调里干了太多事比如在回调里串口打印、处理浮点运算导致回调执行时间超过了被测信号周期捕获事件被不断错过。处理办法是中断回调里只记录捕获值、置一个标志位主循环里检测到标志位再做频率计算、串口打印、显示这些耗时的活。我自己的代码就是这么写的回调里就两三行绝不拖泥带水。4.5 问题排查速查表故障现象可能原因排查方向解决办法测量值偏高或乱跳信号沿抖动、引脚悬空示波器看波形沿加上下拉、施密特整形测量值周期性归零计数器溢出未统计检查溢出计数逻辑统计overflowCount低频测不准信号频率低于量化粒度检查闸门时间/分辨率改用输入捕获测周期高频双击捕获中断来不及响应看中断回调代码量回调精简只置标志HAL_Delay卡死SysTick时钟配置错误检查SystemClock_Config用硬件定时器做闸门调试器连不上SWD引脚被复用检查PA15/PB3/PB4配置复位连接或ISP擦除频率整体偏大/偏小时钟基准不对确认HSE和APB分频核对时钟树、晶振电容4.6 再补一个高频测量的扩展方案如果你要测的频率超过定时器外部时钟模式能够可靠处理的范围比如几MHz以上可以加一个外部预分频器芯片先把信号分频再送给STM32软件里把测到的频率乘回分频倍数即可。另外STM32外部时钟模式1本身也支持ETR引脚的预分频功能能够在硬件层面对输入脉冲做分频这个功能很多人在CubeMX里没注意到。启用方法是在TIMx的SlaveMode配置里找到External Clock Mode 1然后在ETR相关配置里把Prescaler设成2、4或8。说实话我在实际项目里很少直接去测超过1MHz的信号更多是把测频功能当作系统的一个模块配合按键切换量程、配合OLED显示结果。测频代码本身不复杂复杂的是把它稳定地融到整个系统里。最后再分享一个我自己的封装习惯把这套测频逻辑封装成一个独立模块对外只暴露Init、Start、GetFreq三个接口具体用哪个定时器、哪个引脚通过宏定义写在头文件顶部。这样等下次换个项目、换块板子只要改宏定义就能复用整套代码。我踩过几次“测频调好了结果换了个板子全得重写”的坑之后深深觉得正确的工作量应该花在封装上而不是每次从零开始抄寄存器。本文还有配套的精品资源点击获取