简介面向嵌入式开发者的STM32F407频率测量工程资料包围绕一赫兹至三兆赫兹信号频率测量误差控制在百分之二以内适用于工业控制、物联网及实时测频场景。包内提供完整可编译的Keil工程源码包含四十八个C源文件与四十八个头文件覆盖定时器中断、外部中断、LCD显示、时钟配置等模块附带工程配置、十六进制与可执行映像、链接映射和列表文件便于直接烧录与静态分析。压缩包共一百八十四个文件体积约五点一八兆字节目录结构清晰便于定位定时器与中断相关代码。配套说明详述外部中断捕获信号沿、定时器中断服务函数处理脉冲、计数法计算频率以及通过稳定时钟源、缩短中断服务时间、滤波降噪减小误差的技巧并给出低频率长时间采样和高频率快速响应的优化思路。已有三千六百余人学习适合需要实现宽范围频率测量或深入理解STM32定时器与中断协同工作的开发者。1. 用 F407 做 1Hz~3MHz 频率测量真正的难点不在高频而在低频“基于STM32F407(1HZ~3MHZ)频率测量2%误差测量”这个题目听起来不算难真动手做才会发现高频并不可怕低频才是让你加班的地方。3MHz 用 1 秒闸门计数能轻松到 0.001% 分辨率而 1Hz 下要求 2% 误差意味着你要把读数稳定到 0.02Hz 以内随便一点边沿抖动、引脚悬空或者时基偏差误差就直接超标。对做仪器仪表、工业测速、频率采集的开发者来说F407 的定时器资源完全够用关键是选对测频方法并把输入信号调理干净。这篇笔记就把从原理、实现到踩坑的完整路线说清楚。2. 频率测量原理与误差来源为什么 1 秒闸门对 1Hz 信号不够用2.1 频率法、周期法与等精度法的取舍频率测量最直接的做法是固定闸门数脉冲。设输入频率为 fx闸门时间为 Tg在 Tg 内数到 N 个上升沿则 fx N / Tg。这叫频率法工程上也叫 M 法。另一种思路相反测相邻两个上升沿之间的时间差 Ts再算 fx 1 / Ts这叫周期法也叫 T 法。这两种方法各有短处。M 法在低频时表现很差因为闸门内只有很少的脉冲1Hz 信号配上 1 秒闸门N 可能数到 1 也可能数到 2误差几乎是一倍。T 法在高频时表现差因为一个周期内高精度时基能数到的计数太少。F407 的定时器时钟最高到 84MHz3MHz 信号一个周期只占 28 个计数单位只要 1 个计数的捕获抖动相对误差就超过 3.5%直接突破 2% 红线。所以实际仪表很少只用一种方法。普遍做法是高频段用 M 法低频段用 T 法交叉区域用等精度法过渡。等精度法的核心思路是让闸门与被测信号边沿同步保证一次测量覆盖整数个被测周期再把参考时基的计数作为分母。这样做高频和低频都能拿到不错的分辨率代价是要多占用一个定时器并处理边沿触发逻辑。2.2 不同频段的主要误差与适合方法先给一张对照表方便你对照自己的量程选方案。频段典型方法主要误差来源2% 指标下要特别注意的点1Hz 附近周期法时基精度、触发抖动要测多个周期平均不能只测一个边沿100Hz 附近周期法或 1 秒闸门计数量化误差 1/N1 秒闸门 N≈100误差在 1% 边缘建议进入高频段阈值设 200Hz1kHz~100kHz闸门计数更简单量化误差随 N 增大迅速下降闸门时间不用太长100ms 已够3MHz 附近闸门计数信号整形和倍频毛刺真正风险不是算法是输入信号是否干净这张表反映了一个容易被忽略的事实2% 这个指标对中高频段非常宽松真正的约束在 100Hz 以下。只要 1Hz 端做成周期测量中高频用闸门计数整个量程就能把误差压在 2% 以内。2.3 2% 误差预算量化误差、时基误差、触发抖动误差预算至少要分成三项量化误差来自“计数不能数半个边沿”。M 法最大相对误差是 1/NT 法最大相对误差是 1/(fx × Tg)。所以低频要用周期法和多周期平均高频要让 N 足够大。时基误差来自 F407 的时钟源。如果直接用内部 HSI出厂校准精度大约在 1% 左右温度变化后还会再漂这个误差是整体乘性误差低频高频都受影响。2% 的指标看似宽但 HSI 一漂加上量化误差和触发误差很容易超标。工程上必须用 HSE 外部晶振。触发误差来自信号边沿叠加噪声表现为实际触发边沿前后抖动。低频信号如果边沿很缓噪声会在阈值附近来回触发产生多个虚假周期。进入定时器之前必须做整形必要时还要加滤波。另外要留意中断延迟。用 EXTI 读取时间戳时主循环和中断优先级会导致微秒级延迟但周期法看的是两个边沿之间的差值起点和终点同时被推迟大部分延迟会被抵消所以 2% 指标下不会致命。2.4 F407 硬件条件32 位定时器、ETR 外部时钟与外部晶振F407 的 TIM2 和 TIM5 是 32 位定时器这对频率计非常重要。TIM2 可以直接作为外部脉冲计数器1 秒闸门内数 3MHz 信号只累计到 300 万远不会溢出TIM5 作为 84MHz 时基1Hz 信号的一个周期约 8400 万计数32 位也装得下。如果用的是 TIM3、TIM4 这种 16 位定时器低频周期测量就必须做溢出扩展代码会复杂不少。F407 的定时器外部时钟模式 2 也很适合做频率计。开启后外部脉冲直接作为计数器的时钟每来一个有效边沿计数器加一CPU 完全不用参与逐边沿处理。配合 ETR 输入引脚中高频段几乎是零开销计数。剩下要做的只是用一个内部定时器做固定闸门在闸门结束时读一次计数器。这套组合比“每个脉冲进一次中断”的方案可靠得多也更容易把 2% 误差控制在预算内。3. 可复现的 F407 频率测量实现高频闸门计数 低频周期测量3.1 硬件连接与引脚选择我一般建议把待测信号整形后同时接到两个引脚PA0 作为 TIM2_ETR用于高频闸门计数PA1 作为 EXTI1用于低频周期测量。分开接的好处是复用配置简单调试时能独立观察两路输入不用纠结 F407 的 AF 复用与 EXTI 配置是否冲突。输入级先串一个 1kΩ 限流电阻再用带迟滞的比较器把任意波形整成 0~3.3V 方波。比较器的输出直接进 PA0 和 PA1。注意两个引脚之间不要加电容电阻分压否则 3MHz 方波会被 RC 滤波吃掉边沿变缓后触发抖动变大。如果被测信号本身是 5V 逻辑电平建议先用电平转换电路降到 3.3V不要靠 F407 的引脚钳位硬扛。占空比特别窄的信号比较器之后加一级单稳态展宽是更稳妥的做法。时钟方面系统必须用 HSE 外部晶振。F407 的 HSI 内部 RC 低频误差在 1% 左右高频时温度漂移更明显做 2% 频率计会非常勉强。3.2 高频段测频TIM2_ETR 外部计数 1 秒闸门高频段我用 TIM2 的外部时钟模式 2。初始化代码可以按下面这种写法组织。#include main.h TIM_HandleTypeDef htim2; TIM_HandleTypeDef htim3; TIM_HandleTypeDef htim5; void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); /* PA0: TIM2_ETR接整形后的待测信号 */ GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF2_TIM2; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /* PA1: EXTI1低频周期测量 */ GPIO_InitStruct.Pin GPIO_PIN_1; GPIO_InitStruct.Mode GPIO_MODE_IT_RISING; GPIO_InitStruct.Pull GPIO_PULLDOWN; GPIO_InitStruct.Alternate 0; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_NVIC_SetPriority(EXTI1_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI1_IRQn); } void MX_TIM2_Init(void) { __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance TIM2; htim2.Init.Prescaler 0; /* 外部脉冲不分频一个边沿计一次 */ htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFFFFFF; /* 32 位计数器闸门内不会溢出 */ HAL_TIM_Base_Init(htim2); /* ECE1 开启外部时钟模式2ETR 有效边沿驱动计数器 */ TIM2-SMCR | TIM_SMCR_ECE; /* ETF 滤波器先关闭高频段保持边沿完整 */ TIM2-SMCR ~TIM_SMCR_ETF_Msk; }逻辑说明Prescaler 0让每个外部脉冲都直接让 TIM2-CNT 加一任何分频都会损失分辨率。Period 0xFFFFFFFF配合 32 位计数器0.1 秒闸门内就算输入 3MHz 也只有 30 万计数不需要处理溢出。TIM_SMCR_ECE是外部时钟模式 2 的总开关置位后 TIM2 不再依赖内部时钟计数而是由 PA0 的 ETR 信号驱动。参数上要注意 GPIO 速度别设太低3MHz 方波需要足够快的边沿翻转能力。接下来是 1 秒闸门。我直接用 TIM3 产生 1 秒更新中断在中断回调里读 TIM2-CNT。#define GATE_TIMER_PERIOD_MS 1000 void MX_TIM3_Init(void) { __HAL_RCC_TIM3_CLK_ENABLE(); htim3.Instance TIM3; /* 定时器时钟 84MHz8400 分频后得到 10kHz 计数 */ htim3.Init.Prescaler 8399; /* ARR999910kHz * 10000 1 秒溢出一次 */ htim3.Init.Period 9999; htim3.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_Base_Init(htim3); HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM3_IRQn); } typedef enum { FM_IDLE, FM_RUNNING, FM_FINISHED } FM_State; static volatile FM_State fm_state FM_IDLE; static volatile uint32_t fm_pulses 0; void fm_start_gate_measure(void) { TIM2-CNT 0; /* 清零脉冲计数器 */ __HAL_TIM_SET_COUNTER(htim3, 0); fm_state FM_RUNNING; HAL_TIM_Base_Start_IT(htim3); /* 启动 1 秒闸门 */ } float fm_get_gate_frequency(void) { if (fm_state FM_FINISHED) { fm_state FM_IDLE; return (float)fm_pulses / 1.0f; /* 闸门 1 秒Hz 值等于脉冲数 */ } return -1.0f; } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3 fm_state FM_RUNNING) { HAL_TIM_Base_Stop_IT(htim3); fm_pulses TIM2-CNT; /* 立即读外部脉冲计数 */ fm_state FM_FINISHED; } }逻辑说明闸门开始前先把 TIM2 清零闸门结束时在 TIM3 中断里读回脉冲总数。因为高频段只处理 200Hz 以上的信号1 秒闸门内最少也有 200 个脉冲1 个脉冲的量化误差控制在 0.5% 以内再叠加时基误差总误差也能压在 2% 内。参数上最关键的是Prescaler 8399和Period 9999。这两个值是为 84MHz 定时器时钟设计的先分频得到 10kHz再数 10000 次得到 1 秒。如果系统时钟配置不同这两个参数必须一起改否则闸门时间不是 1 秒所有读数都会成比例偏移。3.3 低频段周期测量TIM5 时间戳 EXTI 边沿捕获低频段使用周期法。TIM5 以 84MHz 计数每个上升沿 EXTI 中断读一次 TIM5-CNT相邻两次读数的差值就是一个周期对应的时钟计数。static volatile uint32_t last_timestamp 0; static volatile uint32_t period_ticks_84m 0; static volatile uint8_t period_valid 0; void MX_TIM5_Init(void) { __HAL_RCC_TIM5_CLK_ENABLE(); htim5.Instance TIM5; htim5.Init.Prescaler 0; /* 84MHz 直接计数 */ htim5.Init.CounterMode TIM_COUNTERMODE_UP; htim5.Init.Period 0xFFFFFFFF; /* 32 位计数51 秒才回绕 */ HAL_TIM_Base_Init(htim5); HAL_TIM_Base_Start(htim5); } void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { uint32_t now; if (GPIO_Pin ! GPIO_PIN_1) return; now TIM5-CNT; if (last_timestamp ! 0) { period_ticks_84m now - last_timestamp; /* 无符号减法回绕一次也能算对 */ period_valid 1; } last_timestamp now; } float fm_get_period_frequency(void) { if (period_valid) { period_valid 0; return 84000000.0f / (float)period_ticks_84m; } return -1.0f; }逻辑说明now - last_timestamp用无符号 32 位减法即使 TIM5 在两次边沿之间回绕了一次结果依然是正确的周期长度。TIM5 的 32 位计数在 84MHz 下约 51 秒才回绕1Hz 信号完全够用。如果完全不处理回绕用 16 位定时器等不到 1 秒就会溢出这也是我坚持用 TIM5 的原因。这套周期法最怕两个边沿之间发生抖动触发。软件层可以加一个最小周期保护比如前后两次时间差小于 1000 个计数单位就丢弃防止毛刺被当成一个真实周期。3.4 模式切换阈值与滞回两段方法要配合阈值切换。我常用 200Hz 作为进入高频段的阈值150Hz 作为退回低频段的阈值中间形成滞回避免信号在临界点反复横跳。#define HYST_ENTER_GATE_HZ 200.0f #define HYST_LEAVE_GATE_HZ 150.0f typedef enum { MODE_PERIOD, MODE_GATE } FmMode; FmMode fm_mode MODE_PERIOD; void fm_update_mode(float current_freq) { if (fm_mode MODE_PERIOD) { if (current_freq HYST_ENTER_GATE_HZ) { fm_mode MODE_GATE; } } else { if (current_freq HYST_LEAVE_GATE_HZ) { fm_mode MODE_PERIOD; last_timestamp 0; /* 重新积累边沿时间戳 */ } } }逻辑说明进入高频段的条件是频率超过 200Hz退回低频段的条件是频率低于 150Hz。如果只有一个 200Hz 的阈值信号在 200Hz 附近波动时两套方法会来回切换既浪费 CPU 又会让显示值抖动。滞回之后状态切换变得稳定。低频退回时要把last_timestamp清零否则第一次周期计算会把低频模式启动前遗留的时间戳带进来产生一个大周期误判。这是切换逻辑里最容易漏掉的一步。4. 把误差压进 2%时钟校准、输入整形与平均窗口设置4.1 时钟源与 PLL 配置对时基误差的影响时基误差是乘性误差。假设 84MHz 时基实际只有 83MHz那 3MHz 信号会显示成 3.036MHz误差 1.2%1Hz 信号会显示成 1.012Hz误差也接近 1.2%。这个误差方向固定不会因为算法改进而减小。F407 的 HSE 配合 PLL 是标准做法。常见配置是8MHz 外部晶振PLLM8PLLN336PLLP2得到 168MHz 系统时钟。APB1 预分频为 4 时PCLK142MHz定时器时钟自动加倍到 84MHz。这个 84MHz 就是 TIM2 和 TIM5 的计数基准。如果板子上没有外部晶振而用了 HSI系统时钟精度全看内部 RC。虽然也能跑出数据但低温、高温和长时间老化后可能漂过 2%。做稳定的频率计HSE 是必选。换晶振之后要重新用高精度信号源验证一下PLL 参数不同会直接影响闸门时间和周期法时间戳必须在代码里保持一致。4.2 输入整形边沿抖动、占空比与内部滤波频率计测的是“边沿”不是“波形”。输入信号只要过零缓慢阈值附近的噪声就会变成多个虚假边沿。低频段 1Hz 正弦波直接接 GPIO周期法会频繁触发读数在 0.9Hz 和 1.1Hz 之间乱跳。先整形成陡峭的方波这个问题就解决一大半。整形电路推荐施密特结构。比较器加正反馈产生回差比如阈值设为 1.2V 和 1.8V上升沿在 1.8V 触发下降沿在 1.2V 触发两者之间 0.6V 的迟滞能把大部分噪声挡在外面。对 3MHz 信号比较器输出要选择高摆率类型避免输出边沿本身过缓。F407 定时器的 ETR 引脚内部还带一个可编程滤波器能按采样方式和连续有效次数过滤毛刺。我在高频段默认关闭滤波因为 3MHz 周期只有 333ns滤波太强会直接把真实边沿滤掉如果确认输入信号有振铃毛刺可以从最弱的滤波挡位开始试。低频段可以通过代码判断周期异常并丢弃不一定依赖硬件滤波。占空比特别小的信号比如脉宽只有 50nsF407 的 ETR 可能来不及正确识别。先把窄脉冲展宽到几百纳秒以上再进计数器。展宽之后频率不会变但每个上升沿都稳定可识别。4.3 平均窗口与异常值剔除2% 误差是稳定状态下的指标。如果只测一个周期1Hz 信号的自然相位抖动会直接显示成频率变化看起来误差很大其实信号本身是合格的。所以要做平均。低频段我推荐做多周期平均值连续记录 10 个周期把 10 个周期的总时间差累加再计算频率。这比先算 10 个频率再取平均更合理因为频率和周期不是线性关系对周期求平均不会引入数学偏差。高频段闸门计数本身就是 1 秒窗口长时间序列可以在显示侧做滑动平均。比如保存最近 5 次闸门结果去掉最大值和最小值后取中间 3 次平均基本能压掉偶尔的毛刺脉冲。要注意滑动平均不能太长否则频率真的变化时需要很久才能跟上现场会误以为设备卡死。异常值剔除还要管住量程边界。如果频率超过 3.5MHz明显超出题目要求界面要显示超量程而不是一个静止的大数字。低于 0.5Hz 时周期法需要 2 秒以上才能出一次数据也要给出合理显示策略不能一直显示上一次结果。提示平均窗口会引入显示滞后。如果后面要接 PID 或测速闭环建议输出原始值只在人机界面上用平均后的频率。5. 频率测量避坑F407 调试中 5 个最容易翻车的地方5.1 现象1Hz 读数稳定不了总在 0.95Hz 到 1.05Hz 之间跳原因PA1 浮空时工频电场耦合会在引脚上叠加噪声。周期法的边沿触发时机被噪声推来推去单个周期的测量结果自然不稳定。另一个常见原因是 HSI 内部 RC 时钟漂移时基不准会直接影响 84MHz 时间戳。解决输入级加 10kΩ 下拉电阻让信号断开时引脚保持低电平避免悬空。周期法里加最小周期保护把小毛刺产生的短时间差丢弃。时基切换到 HSE并用高精度信号源做整体校准。5.2 现象3MHz 附近读数变成 6MHz 或者无规律跳变原因高频段出现倍频毛刺。输入波形上升沿如果有振铃会在一个真实周期内产生两个上升沿计数器把毛刺也算进去。另一个原因是 GPIO 速度等级太低边沿没有被正常识别触发位置偏移导致读数不稳。高频下 F407 本身跑 3MHz 没有压力问题基本都在信号通路。解决检查比较器输出波形是否干净导线上有没有明显振铃。开启 TIM2 ETR 最低一档内部滤波把亚微秒级毛刺滤掉。再用短线连接避免长跳线形成反射。5.3 现象测量结果随供电电压变化原因供电电压跌落会让比较器输出摆率变化边沿到达 PA0 的时间点也随之移动。周期法对相位移动非常敏感电压低时边沿变慢读数会偏大或偏小。解决比较器供电尽量单独用 LDO 稳到 3.3V不要和电机、继电器共用电源。输入信号整形后边沿应该不受供电小幅波动影响。如果还出现随电压变化多半是信号源本身内阻太高需要加缓冲器。5.4 现象串口一打印测量值偶尔掉到一半原因串口发送中断占用了 CPU导致高频闸门开启和停止的时间点被推迟闸门实际时长不是 1 秒。读取 TIM2-CNT 的时机被中断拖住脉冲计数和闸门时间对不上。解决不要把打印函数放在测量中断里。主循环只计算和显示串口输出用 DMA不让发送过程长时间占用 CPU。高频闸门的状态机必须由 TIM3 中断推进不能在阻塞延时里读计数器。5.5 现象手捏杜邦线测到 0Hz 或者乱码原因接触不良和人体耦合双管齐下。杜邦线摩擦会产生瞬间开路F407 输入高阻状态下一个悬空引脚就会收到随机噪声。解决测量地线一定要和信号源地共地。信号线用屏蔽线屏蔽层单端接地。前端加 10kΩ 下拉电阻让断开时电平明确为低。手需要动到测试点时优先用鳄鱼夹固定而不是捏着杜邦线。6. 用高精度信号源做验证10 点回归测试与校准系数做完功能最后要验证 2% 误差。最有效的办法是把信号源分别设置到典型频点记录测量值做成一张回归表。信号源设定频率F407 显示频率校准前误差校准后误差1Hz记录值(显示值-设定值)/设定值修正后重新计算10Hz记录值同上同上100Hz记录值同上同上1kHz记录值同上同上100kHz记录值同上同上1MHz记录值同上同上3MHz记录值同上同上如果每个点都在 2% 以内指标达成。如果整体偏大或偏小而且偏差接近线性说明时基有固定误差。最后一步校准很简单用 100kHz 这一点的偏差计算修正系数乘到所有频率上。static volatile float g_cal_coef 1.0f; void cal_update_from_reference(float ref_freq, float raw_freq) { if (raw_freq 0.0f) { g_cal_coef ref_freq / raw_freq; } } float cal_apply(float raw_freq) { return raw_freq * g_cal_coef; }这个修正系数只对时基误差有效量化误差和触发抖动是修正不掉的。所以校准前先确认读数稳定再用稳定后的平均值做计算。修正系数记得存到 Flash断电后要能恢复。我每次做频率计都会先保证 1Hz 端读数能用周期法稳定输出再追求 1s 闸门的高频精度。低频做稳高频反而很少出问题如果先调 3MHz 再回头看 1Hz会遇到很多重复踩坑。这算是一条血泪经验。希望帮到你。本文还有配套的精品资源点击获取