STM32F407实现1Hz-3MHz高精度频率计:M法T法分段测量与误差分析

发布时间:2026/9/1 21:43:39
STM32F407实现1Hz-3MHz高精度频率计:M法T法分段测量与误差分析 简介基于STM32F407实现1Hz~3MHz宽范围频率测量误差控制在2%以内是面向嵌入式开发与单片机学习者的完整工程资源。内容围绕高级/通用定时器、外部中断及计数法测量展开涵盖信号滤波、定时器时钟配置、预分频与中断优化等关键点可直接作为频率计、测速仪等仪器类项目的设计参考。压缩包共184个文件约5.18MB以C语言源文件与头文件为主同时包含编译生成的o、d、crf等中间文件以及hex、axf、map等烧录与调试输出便于二次编译和功能裁剪。工程配有LCD显示相关代码并提供标准外设库驱动适合ST官方STM32F4系列标准外设库开发方式。资源已有3662人学习使用对于需要快速掌握STM32定时器测频原理、减少测量误差的开发者来说是一份具有实操价值的参考资料。 STM32F407做频率测量题目本身看着不新鲜但要求把1Hz~3MHz这个量程完整覆盖还要把误差压在2%以内这里面的门道就多了。我最近把整套方案从头做了一遍包括测频和测周方法怎么选、硬件前端怎么整形、定时器怎么配、误差从哪来一次性整理出来。如果你也要在STM32F407上做频率计或者只是想弄明白M法、T法到底该怎么选这篇应该能帮你少走不少弯路。1. 需求拆解为什么1Hz~3MHz这个范围必须分开测1.1 两种基础测量思路测频法M法与测周法T法频率测量最常用的两种方法是测频法和测周法它们本质上是一对互逆的思路。测频法M法是在固定闸门时间T内对输入脉冲个数N进行计数频率f N/T。它的优点是频率越高N越大误差越小。缺点是信号频率低的时候比如1Hz信号1秒闸门内只能数到1个脉冲如果计数刚好多一个或少一个相对误差就是50%直接没法用。测周法T法反过来是用一个已知的高频标准时钟去测量一个输入周期的时长。假设标准时钟是84MHz输入信号周期内数到N个标准时钟脉冲那么频率f 84MHz / N。低频信号周期长N很大误差极小高频信号周期短N很小误差会被放大。比如3MHz信号周期约0.333微秒84MHz时钟在每个周期内只能数到28个脉冲±1计数就是3.6%误差已经超过2%的指标。这里打个比方测频法像在商场门口统计单位时间人流量人越多统计越准测周法像量每个人走一步的步幅再反推走路速度步子越慢反而量得越准。所以没有哪种方法能同时覆盖1Hz和3MHz两端工程上必须分段处理。1.2 误差公式推导中界频率到底怎么算既然要分段就得找一个理论上的分界点也就是中界频率。在这一点上测频法和测周法的误差相等。测频法的相对误差表达式是δ_M 1 / (f × T_gate)因为固定闸门内计数最多偏差1个N f × T_gate。测周法的相对误差表达式是δ_T f / f_clk因为一个周期内计数N f_clk / f偏差1个相对误差就是1/N f / f_clk。令两者相等1 / (f × T_gate) f / f_clk得到f0 sqrt(f_clk / T_gate)在本项目中标准时钟取TIM2的84MHz闸门时间取1秒那么f0 sqrt(84MHz / 1s) ≈ 9165Hz也就是说频率低于9.2kHz左右用测周法更优高于9.2kHz左右用测频法更优。3MHz用测频法1秒闸门内计数300万个误差只有0.000033%1Hz用测周法一个周期内计数8400万个误差可以忽略。两端都有极大的余量2%这个指标可以说非常宽裕。测量方法计算公式相对误差适合频段测频法M法f N / T_gate1 / (f×T_gate)约9.2kHz以上测周法T法f f_clk / Nf / f_clk约9.2kHz以下这里有一个关键点必须说清楚有些人拿到题目直接配个输入捕获就开测结果3MHz附近误差明显超标就是因为3MHz信号用测周法时计数太少每个周期只有28个标准时钟脉冲误差3.6%天然超限。这个不是代码能解决的是方法论选错了。我建议无论你的目标精度是多少动手前先把这两条误差曲线画出来再决定分界点。2. 硬件前端设计摸清信号类型再决定怎么接入2.1 三种常见输入信号的处理方式频率测量系统最先面对的不是STM32而是被测信号本身。我踩过的第一个坑就是拿开发板的杜邦线直接去戳信号发生器结果3MHz正弦波进来PA0电平判断乱七八糟计数完全不对。被测量信号大概分三种情况0~3.3V的方波/逻辑电平信号比如另一个MCU的PWM输出这种情况最简单信号可以直接进STM32引脚串联一个1kΩ电阻限流即可不需要额外整形。0~5V的TTL电平信号。STM32F407的IO最高耐压虽然能到5V但长时间直接接5V逻辑信号不推荐最好做一下电平转换。最简单的做法是串联1kΩ电阻再在引脚对地并联一个3.3V稳压管或者用两个1N4148二极管做钳位。正弦波、三角波、任意波形甚至幅度不可控的传感器输出这类信号必须经过整形电路转换成干净的方波再进MCU。直接用GPIO判断模拟波形是灾难电压在阈值附近徘徊会产生大量毛刺。2.2 一个可靠的施密特整形输入电路我最终的硬件前端用的是一颗74HC14施密特触发器反相器这个东西不到一块钱但作用非常大。信号先进入74HC14的输入端经过施密特整形后从输出端引出再串联1kΩ电阻接到STM32F407的PA0。74HC14直接用5V供电输出端串电阻的目的是限流PA0处加3.3V钳位保护避免5V电平直接打坏引脚。如果被测信号是0~3.3V方波同样建议过74HC14整形因为施密特触发器自带回差电压能有效抑制边沿抖动。有人会问为什么不用LM393这类比较器LM393响应时间比较慢电平翻转延迟能达到1.3微秒左右测3MHz信号时一个周期才333纳秒比较器根本反应不过来。74HC14的传输延迟大约是十几纳秒对付3MHz绰绰有余。更快的可以用74LVC1G14或者专用比较器TLV3501但对这个项目来说74HC14足够了。这里还要强调一点输入端不能悬空。如果信号源不是始终接在端子上建议在74HC14输入端对地接一个10kΩ下拉电阻。否则悬空时施密特触发器会因环境噪声随机翻转计数器会疯狂跳动看起来就像频率计坏了。3. STM32F407定时器配置与代码实现3.1 时钟树84MHz从哪来STM32F407的频率测量精度很大程度取决于定时器时钟源配置。以正点原子探索者开发板为例外部晶振8MHz系统主频168MHzAHB总线168MHzAPB1总线42MHzAPB2总线84MHz。这里有个容易搞错的点TIM2、TIM3、TIM4、TIM5挂在APB1上但APB1分频系数不为1时定时器时钟是APB1的两倍。也就是说APB142MHz时TIM2的时钟实际是84MHz。TIM1和TIM8挂在APB2上APB284MHzTIM1时钟就是168MHz。我最初用测周法计算频率时直接拿42MHz当分母结果所有读数都偏大一倍查了半天才发现是时钟树的问题。在代码中可以通过HAL库函数动态获取uint32_t get_timer_clock(void) { uint32_t pclk1 HAL_RCC_GetPCLK1Freq(); if ((RCC-CFGR RCC_CFGR_PPRE1) ! RCC_CFGR_PPRE1_DIV1) return pclk1 * 2; else return pclk1; }实测下来按照SystemClock_Config默认生成的168MHz主频配置TIM2时钟就是84MHz。这个值作为测周法公式的分母是整个项目的核心参数。3.2 测频法M法外部时钟模式计数测频法的硬件逻辑是让外部方波直接作为定时器的计数脉冲1秒闸门结束后读取计数器的值就是频率数值。STM32F407的TIM2支持外部时钟模式1可以把TI1FP1引脚上的边沿当作计数时钟。关键配置如下void TIM2_ExtCount_Init(void) { TIM_SlaveConfigTypeDef sSlaveConfig {0}; htim2.Instance TIM2; htim2.Init.Prescaler 0; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFFFFFF; // 32位计数器足够3MHz计1秒 HAL_TIM_Base_Init(htim2); sSlaveConfig.SlaveMode TIM_SLAVEMODE_EXTERNAL1; sSlaveConfig.InputTrigger TIM_TS_TI1FP1; sSlaveConfig.TriggerPolarity TIM_INPUTCHANNELPOLARITY_RISING; sSlaveConfig.TriggerFilter 0; HAL_TIM_SlaveConfigSynchro(htim2, sSlaveConfig); HAL_TIM_Base_Start(htim2); }闸门1秒的实现我推荐用TIM6基本定时器产生中断而不是HAL_Delay。HAL_Delay受SysTick中断和其他中断影响精度不够稳定虽然对2%目标也能凑合但作为一个严谨的频率计闸门不准就是系统误差。TIM6定时1秒中断回调里把完成标志位置1即可。void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) gate_done 1; }主程序测频流程是清零TIM2计数器开启TIM6定时1秒等待gate_done标志关掉TIM6读取TIM2计数器值这个值就是频率。3.3 测周法T法输入捕获测量单个周期测周法的思路是测量两个相邻上升沿之间的时间差。TIM2的输入捕获模式在上升沿到来时把计数器的值锁存到捕获寄存器并产生中断。两次捕获值相减就是周期对应的84MHz时钟计数。void TIM2_InputCapture_Init(void) { TIM_IC_InitTypeDef sConfigIC {0}; htim2.Instance TIM2; htim2.Init.Prescaler 0; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFFFFFF; HAL_TIM_IC_Init(htim2); sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler TIM_ICPSC_DIV1; sConfigIC.ICFilter 0; HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1); }中断回调里记录两次捕获值void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2 htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { if (cap_count 0) cap1 HAL_TIM_ReadCapturedValue(htim2, TIM_CHANNEL_1); else cap2 HAL_TIM_ReadCapturedValue(htim2, TIM_CHANNEL_1); cap_count; } }获取两个上升沿之后频率计算float calc_freq_by_period(void) { uint32_t ticks; if (cap2 cap1) ticks cap2 - cap1; else ticks (0xFFFFFFFF - cap1) cap2 1; return 84000000.0f / (float)ticks; }注意1Hz信号周期是1秒84MHz计数8400万个远没有超过32位计数器上限所以不做溢出扩展也能满足本项目最低1Hz的要求。如果你想把下限再往下探就得在更新中断里加溢出计数把计数器回绕的次数累加起来。3.4 主程序判断逻辑低频测周期高频测频率这里我给出一套最稳妥的判断流程先用测频法测1秒如果计数超过9200个脉冲约9.2kHz以上直接用测频法结果如果计数不超过9200再切到测周法精确测量低频。float measure_frequency(void) { uint32_t cnt; // 测频法1秒闸门 TIM2_ExtCount_Init(); __HAL_TIM_SET_COUNTER(htim2, 0); gate_done 0; HAL_TIM_Base_Start_IT(htim6); while (!gate_done); HAL_TIM_Base_Stop_IT(htim6); cnt __HAL_TIM_GET_COUNTER(htim2); HAL_TIM_Base_Stop(htim2); if (cnt 9200) return (float)cnt; // 高频段直接读出频率 // 低频段切测周法 HAL_TIM_IC_DeInit(htim2); cap_count 0; TIM2_InputCapture_Init(); uint32_t timeout HAL_GetTick(); while (cap_count 2) { if (HAL_GetTick() - timeout 3000) // 3秒超时保护 break; } HAL_TIM_IC_Stop_IT(htim2, TIM_CHANNEL_1); if (cap_count 2) return calc_freq_by_period(); else return 0; // 无信号或异常 }这里注意一个细节测频法和测周法共用TIM2和PA0所以从外部时钟模式切换到输入捕获模式时必须把TIM2的从模式设置清掉本文还有配套的精品资源点击获取