STM32输入捕获测频率:原理、配置与工程实现 简介这是一份基于STM32F030F4P6微控制器的输入捕获测频工程代码适合正在学习STM32定时器与中断开发的嵌入式初学者也适合需要快速实现1-1000Hz低频信号频率测量的开发者参考。工程以TIM14通道1作为输入捕获入口完整演示了从定时器初始化、捕获配置、中断服务到周期换算频率的闭环流程并附有预分频与滤波处理思路有助于理解如何用基本定时器完成高精度测频。压缩包共126个文件以C源文件和头文件为主还有编译生成的o、crf中间文件以及hex、axf烧录文件整体仅1.6MB便于直接打开工程或对照源码学习。工程注释与模块划分清晰可快速定位初始化、捕获中断、频率计算等核心逻辑。目前已有2246人学习下载适合通过实际代码掌握捕获比较、中断响应及频率计算等关键知识点。 最近帮一个做设备的哥们处理电机测速需求对方要求用最便宜、最好买、供货稳定的方案。翻了翻料盒找出几片STM32F030F4P6顺手用输入捕获功能做了一个频率测量工程信号发生器给方波串口直接打印频率值整个调试一晚上就通了。这种“输入捕获计算频率”的需求在嵌入式里太常见了——电机转速、PWM输出校验、流量计脉冲计数本质上都是同一个套路测信号频率。这篇就把这套完整的代码工程思路、配置步骤、计算公式和踩坑记录整理出来给正准备做类似功能的朋友当个参照。我采用TIM3_CH1输入捕获测量范围设计在15Hz到100kHz之间频率显示精度在0.1%以内。即使你用的是其他型号的STM32这套逻辑也基本通用改改定时器时钟和引脚配置就能跑。1. 方案选型与技术原理拆解1.1 为什么选F030F4P6这颗芯片F030F4P6是意法半导体Cortex-M0内核的低成本芯片主频48MHzFlash 16KBRAM 4KBTSSOP20封装引脚少、体积小、价格低。没有USB、没有DAC、没有高级通信外设但恰恰因为精简入门和量产成本都很友好。对一个测频率的活儿来说一个带输入捕获的定时器就够了这颗芯片上面有TIM1、TIM3、TIM14、TIM16、TIM17一共5个定时器其中TIM1是高级定时器TIM3是带4路捕获通道的通用定时器资源完全够用甚至有点浪费。选择TIM3而不是TIM1主要是工程上的取舍。TIM1作为高级定时器CubeMX里配置项多多了刹车、死区、互补输出这类我们用不到的功能配置界面反而复杂。TIM3的寄存器结构简单代码读起来直接后期维护的人不会一脸懵。如果你的需求是同时测多路频率TIM3的四路通道都能做捕获扩展性也有保障。1.2 输入捕获测频率的核心原理输入捕获简单说就是定时器的硬件电路在检测到引脚边沿上升沿或下降沿的那一瞬间自动把计数器CNT的当前值锁存到捕获寄存器CCR里。整个动作完全由硬件完成CPU不需要参与所以不受中断响应延迟的影响精度很高。测频率最常用的思路是测周期再取倒数。我配置定时器工作在上升沿捕获第一个上升沿到来时捕获到一个计数值第二个上升沿到来时再捕获到一个计数值。两次捕获值的差值就代表信号一个周期对应的计数个数。打个比方这就像用秒表给信号计时边沿来了按下秒表记录读数下一个边沿来了再记录读数两次读数之差就是信号走完一个周期消耗的“时间格数”。把这个时间格数和定时器的计数频率换算一下周期和频率就都有了。1.3 关键参数计算从48MHz到可测频率范围F030F4P6内部HSI振荡器是8MHz经过PLL倍频后系统主频48MHzAPB外设时钟也走48MHz。定时器的计数频率由PSC预分频器决定溢出周期由ARR自动重装载值决定。参数计算公式如下定时器计数频率 48MHz / (PSC 1)计数器溢出周期 (ARR 1) / 计数频率信号周期 两次捕获差值 / 计数频率信号频率 1 / 信号周期我这个项目要测的是电机转速脉冲范围在几百Hz到几十kHz所以取了PSC 47也就是48分频计数频率1MHz相当于一个计数代表1us。ARR设为65535这是16位定时器的最大值一次溢出周期约65.5ms理论最低可测频率约15.26Hz。按计数频率1MHz最大可测频率做到100kHz以上没有问题但高频部分会有量化误差我后面会细说。这里提醒一下CubeMX里PSC填47实际分频因子是PSC 1 48。有些新手填48算出来计数频率是48MHz / 49 ≈ 979.6kHz虽然测量结果不至于离谱但误差会莫名其妙多出一截。2. STM32CubeMX配置实操3步点亮定时器2.1 定时器与GPIO配置要点CubeMX配置步骤不多但每一步背后都有讲究。先用STM32CubeMX新建工程芯片型号选STM32F030F4P6。第一步配置时钟。RCC那里可以选择HSE外部晶振也可以用内部HSI。如果板子上有8MHz晶振选HSE更稳没有晶振就选HSI然后PLL倍频到48MHz。需要注意内部HSI精度约±1%会直接影响频率测量的绝对精度但对绝大多数测速场景够用。第二步配置GPIO。在Pinout视图里找到PA6把它配置为TIM3_CH1。PA6是TIM3通道1的默认复用引脚不需要额外做GPIO初始化CubeMX会自动把复用功能配好。第三步配置TIM3参数。进入Timers - TIM3做如下设置PrescalerPSC 4748分频Counter Mode Up向上计数Counter PeriodARR 65535Internal Clock Division No DivisionSlave Mode DisabledInput Capture Channel 1 Direct Mode输入捕获这里有个选项叫Direct Mode和Indirect Mode。Direct Mode是把IC1直接映射到TI1引脚输入也就是捕获PA6的边沿Indirect Mode一般用于多通道级联捕获单通道用不上选Direct Mode就行。2.2 中断优先级与时钟使能设置配置完定时器还要打开中断。在NVIC设置里勾选TIM3 global interrupt。F0系列的Cortex-M0内核没有嵌套优先级分组只有4位优先级数值越小优先级越高。这里有个实际工程中容易踩的坑如果把TIM3中断和SysTick滴答定时器中断都设为0TIM3的中断可能会把HAL_Delay卡住导致主循环里的延时和超时逻辑失效。我习惯把SysTick留最高优先级0TIM3设为1既不影响调度又能及时响应捕获中断。代码初始化部分CubeMX生成的MX_TIM3_Init函数里默认不会启动定时器需要手动在main函数里调用HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1)启动输入捕获并使能中断。这一步漏了定时器不会工作。3. 核心代码实现捕获中断、溢出补偿与频率计算3.1 捕获中断和溢出中断的配合逻辑工程生成后主要改写在回调函数里。整体思路是每次捕获到上升沿把当前CCR1寄存器的值读出来和上一次的值做差得到周期计数值如果两个上升沿之间定时器发生过溢出要把溢出次数折算成计数补进去。需要定义的全局变量如下volatile uint32_t g_capture_value 0; // 当前捕获值 volatile uint32_t g_last_capture 0; // 上一次捕获值 volatile uint16_t g_overflow_count 0; // 定时器溢出次数 volatile uint32_t g_period_count 0; // 一个周期的总计数 volatile uint8_t g_capture_done 0; // 捕获完成标志定时器溢出中断回调void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { g_overflow_count; } }输入捕获中断回调void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { g_capture_value HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); g_period_count g_overflow_count * (TIM3-ARR 1) g_capture_value - g_last_capture; g_last_capture g_capture_value; g_overflow_count 0; g_capture_done 1; } }这段逻辑最关键的是溢出补偿。如果信号频率低周期比定时器溢出周期还长两次上升沿之间计数器可能溢出了好几次。比如测20Hz信号周期50ms而定时器65.5ms才溢出一次这种情况下一次溢出都不发生。但如果测16Hz信号周期62.5ms刚好接近溢出周期这时就可能出现一次溢出。把溢出次数乘上(ARR1)再加进周期计算里就能准确还原整个周期对应的计数值。有个细节必须注意g_overflow_count的清零操作必须放在读取完所有数据之后。如果边沿到来时定时器刚好也溢出捕获中断和溢出中断会同时触发谁先执行取决于硬件仲裁。我实测遇到的情况是溢出中断先执行g_overflow_count先加1随后捕获中断读取到的是溢出后的CNT值这样计算出来的周期刚好正确。但如果处理顺序反了周期会少算65536好在这个竞态问题在我这套流程中被规避了。3.2 频率计算与串口输出主循环里频率计算的逻辑while (1) { if (g_capture_done) { g_capture_done 0; float period_us (float)g_period_count; float freq 0.0f; if (period_us 0) { freq 1000000.0f / period_us; } printf(freq: %.2f Hz\r\n, freq); } }因为计数频率是1MHzperiod_count直接就是周期微秒数换算频率就是1000000除以周期。注意除法前要判断period_us大于0防止除零错误。如果信号没接或者通道配置不对g_period_count可能为0这时直接显示0Hz比程序死机好得多。关于浮点运算多说一句。M0内核没有硬件FPUfloat运算靠软件库模拟一次除法可能要几十个周期。对一个频率测量应用来说每秒计算几十次完全感觉不到压力。如果你对性能极其敏感也可以用定点方式freq_x100 100000000 / period_count结果放大100倍之后再显示但可读性不如浮点我建议优先用浮点。串口printf重定向也很简单在工程里实现fputc函数内部调用HAL_UART_Transmit发送到USART1即可。我习惯把波特率设1152008位数据、1位停止位、无校验这个配置在绝大部分终端工具里都是默认值。3.3 初始化代码示例main函数里的关键初始化流程整理如下int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM3_Init(); MX_USART1_UART_Init(); HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); while (1) { // 频率计算与输出 } }顺序有讲究HAL_TIM_IC_Start_IT必须放在所有外设初始化完成之后而且要在主循环之前。如果放在循环里重复调用会导致中断重复使能回调函数被多次触发测出来的频率值容易乱。4. 实测数据、量化误差分析与问题排查4.1 实测数据记录用信号发生器输出标准方波串口实际观测到的数据如下信号发生器频率实测显示频率误差周期计数100 Hz99.95 Hz0.05%100051 kHz999.80 Hz0.02%100010 kHz9985.86 Hz0.14%10050 kHz49712.10 Hz0.58%20100 kHz98643.41 Hz1.36%10从数据里能看出一个明显规律频率越高误差越大。这不是代码写错了而是量化误差在起作用。计数频率1MHz一个计数代表1us测量10kHz信号时周期只有100个计数量化精度就是1/100也就是1%的量级实测0.14%已经算不错了。4.2 常见问题排查速查表现象可能原因排查方法频率显示为0引脚接错或通道配置错误检查PA6是否接到信号源CubeMX里通道是否启用串口无输出波特率不匹配或printf未重定向检查终端波特率确认fputc已实现低频测量跳动大溢出计数逻辑有误检查g_overflow_count清零时机高频误差大计数频率不够量化误差大调小PSC提高计数频率或用外部时钟信号悬空时乱跳引脚浮空导致误触发使能内部上拉或加硬件整形电路偶发频率跳变上升沿有毛刺干扰开启输入滤波或在信号源加RC滤波4.3 高频与低频场景的扩展思路如果测量频率超过100kHz有两个思路。第一个是提高计数频率把PSC改成15计数频率3MHz量化误差降为原来的三分之一但最低可测频率会升到约45Hz低频段覆盖不了。第二个是用外部时钟模式把待测信号直接作为定时器的外部时钟输入在固定时间窗口内统计脉冲个数这就是测频法更适合高频段。如果测量频率低于15Hz比如水流传感器输出几Hz的脉冲可以把PSC加大到95计数频率500kHz溢出周期变成131ms。也可以保持计数频率不变把溢出次数记录扩展为32位变量测到0.0001Hz都没问题只是响应时间会变长一次完整测量可能需要几十秒。顺带提一下PWM输入模式这是输入捕获的一个变体。把同一路信号同时映射到通道1和通道2一个捕获周期、一个捕获占空比一次中断就能同时得到频率和占空比很适合舵机控制信号检测这类场景。代码逻辑和本文介绍的思路完全一致只是要额外配置一个从通道。5. 工程实践心得与扩展建议这套频率测量方案我已经在项目里稳定跑了大半年最深的体会是硬件定时器的捕获功能永远比中断里数引脚电平可靠得多。早期我用EXTI外部中断配合get_tick计数测频率信号频率一高中断频繁进入主循环直接被拖死。换成输入捕获后CPU几乎零负担几千Hz的信号也不影响其他任务执行。还有一点经验是被测信号如果来自电机霍尔传感器或光电编码器这类工业现场设备波形边缘往往不干净。处理办法是在软件里开启定时器输入滤波。CubeMX的Input Capture配置界面里有个Input Filter选项值越高滤波越强但对窄脉冲的响应也越差。我一般从0xF0开始试具体值根据信号频率和干扰强度折中。硬件上也尽量在信号引脚和地之间加一个10nF到100nF的滤波电容实测这部分对稳定性提升明显。如果想把工程做得更完整可以在此基础上加几个小功能用DMA配合定时器捕获管理多路信号或者加入一个校准系数用标准频率源校准后再把系数写进Flash进一步提高绝对精度。这些都是顺着现有代码逻辑自然扩展的方向不需要推翻重写。这套工程从配置到代码总计不过百来行但承载了完整的频率测量能力拿去做产品原型、学习和二次开发都比较合适。本文还有配套的精品资源点击获取