
简介基于STM32F103微控制器的实时频率跟踪系统完整工程面向单片机学习者与嵌入式开发者可用于测量外部信号频率并通过LED屏实时显示。系统采用定时器输入捕获与中断机制实现周期测量与频率换算适合电子实验室、教学实训及简单自动化监测场景。包体共146个文件压缩包约2.83MB以.c源文件和.h头文件为主涵盖STM32标准外设库驱动、LCD显示驱动另有Keil工程文件.uvprojx、编译目标文件.o、可烧录的hex/axf文件及批量清理脚本等结构完整便于直接编译与烧录验证。已有2008人浏览学习对于希望掌握STM32定时器输入捕获、中断处理及外设驱动整合的读者可对照工程代码厘清频率测量链路并借鉴LCD驱动与频率平滑处理思路工程中还涉及TIM、RCC、ADC、I2C等多个外设的初始化配置能辅助系统理解STM32标准库开发流程。若需快速开展频率测量类实验或二次开发可将其作为较完整的参考起点。1. 实时频率跟踪的工程起点——为什么测频不只是“数脉冲”测量频率和实时跟踪一个变化中的频率完全是两回事。固定闸门测频法给工频信号做显示绰绰有余但电机转速、光电码盘、PWM 反馈回路这类场景信号频率可能在几十毫秒内跳一个数量级测频策略直接决定整个闭环的响应带宽。STM32F103 在频率测量项目里被反复选中不是因为它有多强的测量外设而是 8 个定时器、4 路输入捕获、DMA 和中断配合起来能在 1ms 甚至 200μs 内完成一次有效频率估值外围成本还压得下来。这篇文章按“原理—实现—调参—进阶”顺序推进先把测频法、测周法和多周期同步法的误差边界讲透再给一套基于输入捕获与定时器级联、可以直接移植的频率跟踪代码最后落到多路捕获、自适应闸门和串口时序这些工程里绕不开的硬问题。2. STM32F103 频率测量的三条技术路线误差公式与适用边界2.1 测频法固定闸门计数的短板在低频段测频法的思路是开一个固定时间窗口 T在窗口内对被测信号的上升沿计数得到频率 f N / T。误差来源是窗口边界上的 ±1 计数误差相对误差为 1/N 1/(f·T)。这个误差和信号频率无关只取决于闸门时间长短。也就是说闸门越长越准低频信号想要同样精度需要等更久。用 STM32F103 实现测频法常见做法是把一个定时器配置成外部时钟模式把被测信号接到定时器的输入引脚让硬件自动计数再用另一个定时器定时读取计数值/* 测频法TIM1 外部时钟模式计数TIM3 定时 1s 读取 */ TIM1-SMCR | TIM_SMCR_SMS_0; // 外部时钟模式1引脚边沿驱动计数 TIM1-CCER | TIM_CCER_CC1E; // 使能 TI1 输入 TIM1-CCER ~TIM_CCER_CC1P; // 上升沿计数 TIM1-ARR 0xFFFF; // 16 位计数器 TIM1-CR1 | TIM_CR1_CEN;这段配置把被测信号当作计数器时钟CPU 不用参与每个脉冲的处理但窗口结束前拿不到任何结果。下面这张表能直观看出低频场景下的问题闸门 1s 时1kHz 信号的相对误差是 0.1%换个思路算一下用测周法在 1kHz 下配合若干 MHz 的计数时钟误差远小于这个值。测频法更适合 MHz 级高频信号以及那些对更新率不敏感、只要求长时间内稳定的测量场景。闸门时间100Hz 时相对误差1kHz 时相对误差10kHz 时相对误差100ms10%1%0.1%1s1%0.1%0.01%10s0.1%0.01%0.001%2.2 测周法把低频测量变成高频测量测周法与测频法正好反过来用高频时钟对被测信号的单个周期计数得到周期的精确时间再换算成频率 f f_clk / diff其中 diff 是两个相邻上升沿捕获值的差。误差来源是高频时钟边沿与被测信号边沿不同步带来的 ±1 计数误差相对误差等于 1/diff f_signal / f_clk。这个公式说明一件事被测频率越低一个周期内的高频时钟计数越多相对误差越小。所以测周法天然适合低频高频段反而吃亏——比如 10kHz 信号、计数时钟 1MHz 时相对误差是 1%这在实时控制系统里很难接受。解决办法是提高计数时钟或者干脆在程序里判频段切换策略。STM32F103 的输入捕获硬件就是为测周法准备的定时器自由运行输入引脚每个上升沿会把当前 CNT 锁存到 CCR 寄存器同时触发中断。CPU 每次读到的值是连续上升沿的时间戳两个时间戳之差就是信号周期。这种模式中断频率等于信号频率1kHz 信号对应 1ms 中断一次对实时跟踪系统非常友好这也正是本文后续主题“实时频率跟踪系统”的实现基础。2.3 多周期同步测频更公平但实时性打折扣多周期同步法也叫等精度测频法是为了消除被测信号边沿带来的 ±1 误差。它不固定闸门时间而是等被测信号上升沿到来时才同时打开两个计数器一个对被测信号计数另一个对参考时钟计数直到下一个上升沿同时关闭。因为闸门开关都与被测信号同步被测信号这边没有边界误差只剩参考时钟对实际闸门时间的 ±1 误差相对误差表达为 1/(f_clk·T_gate)。这种方法在全频段表现均衡但代价是闸门时间不固定更新间隔不可预测。对 STM32F103 来说等精度测频需要两个定时器联动或者在中断里手动同步开关状态机代码量和调试成本明显上去而带来的精度提升在低频段并不明显。实测做 100Hz100kHz 的信号跟踪时测周法加上软件平滑已经能满足大多数控制系统需求。对频率覆盖范围跨越四个数量级、且每个数量级都要求 0.01% 精度的设备再考虑上等精度测频也不迟。方法误差公式适合频段更新率实现成本测频法固定闸门1/(f·T)高频、稳定信号一个闸门一次低测周法输入捕获f_signal/f_clk低频至中频每个信号周期一次低多周期同步法1/(f_clk·T_gate)全频段取决于实际闸门时长高3. 用输入捕获搭一套 STM32F103 实时频率跟踪的最小系统3.1 硬件接线先解决信号的“干净”问题STM32F103 最小系统上做频率测量信号进入 MCU 前要先确认电平范围。被测信号如果是 3.3V 逻辑方波串一个 1kΩ 电阻直接进 PA0TIM2_CH1就能跑如果是传感器开漏输出必须加上拉电阻如果来自电机编码器或继电器触点建议先过施密特触发器整形再进捕获引脚。PA0 属于 TIM2_CH1是频率测量最常用的入口。要注意 F103 的 PA11/PA12 在部分封装上与 USB D-/D 共用引脚如果你既要做输入捕获又要用 USB 调试把捕获通道安排在 PA0/PA1 或 PA6TIM3_CH1上可以避开这个冲突。另外BOOT0 和 BOOT1 跳线要在下载前确认状态F103 在 BOOT1 悬空或接错时经常出现 DAP 连接不稳定、下载失败的现象这个问题和频率测量本身无关但现场排查时很容易干扰判断。3.2 寄存器级最小代码TIM2_CH1 捕获上升沿时间戳用标准外设库或者 HAL 库都能完成配置但想看清楚原理寄存器写法最直接。下面这套初始化把 TIM2 的计数时钟设为 6MHz即 PSC 11实际计数频率为 72MHz / (111) 6MHz单个计数的分辨率为 166.7ns。void TIM2_CH1_Capture_Init(void) { RCC-APB1ENR | RCC_APB1ENR_TIM2EN; // 打开 TIM2 时钟 TIM2-PSC 11; // 72MHz / 12 6MHz TIM2-ARR 0xFFFF; // 16 位65535 回绕 TIM2-CCMR1 TIM_CCMR1_CC1S_0; // IC1 映射到 TI1 TIM2-CCMR1 | TIM_CCMR1_IC1F_0 | TIM_CCMR1_IC1F_1; // IC1F 0011采样 fCK_INT连续 8 次一致才判定有效 TIM2-CCER TIM_CCER_CC1E; // 上升沿捕获CC1P 默认 0 TIM2-SR 0; // 清中断标志避免复位后假触发 TIM2-DIER TIM_DIER_CC1IE; // 使能捕获比较中断 NVIC_EnableIRQ(TIM2_IRQn); TIM2-CR1 | TIM_CR1_CEN; // 启动计数 }IC1F 滤波位在这里设成了 0011硬件会在 72MHz 采样时钟下连续检测到 8 次有效电平才确认边沿能滤掉大部分窄毛刺代价是信号边沿最多延迟约 111ns。对 100kHz 以下的信号这个延迟可以忽略但如果你测的是 1MHz 以上的高频滤波反而会引入系统误差建议关掉或降低采样频率。中断处理代码是整个频率跟踪系统的核心每次上升沿读取 CCR1 的差值换算成频率volatile uint32_t g_last_ccr 0; volatile float g_freq_hz 0.0f; volatile uint32_t g_cap_cnt 0; void TIM2_IRQHandler(void) { uint32_t ccr_now, period; if (TIM2-SR TIM_SR_CC1IF) { TIM2-SR ~TIM_SR_CC1IF; ccr_now TIM2-CCR1; /* 16 位回绕处理减法后与 0xFFFF 相与结果排列正确 */ period (ccr_now - g_last_ccr) 0xFFFF; g_last_ccr ccr_now; if (period ! 0) { g_freq_hz 6000000.0f / (float)period; } else { g_freq_hz 0.0f; /* 无效数据交由上层过滤 */ } g_cap_cnt; } }period 计算这行很关键两个 CCR 值必然在 065535 之间分布直接相减会有负数换成 uint32 减法后与0xFFFF做按位与相当于自动处理了计数器回绕。你不需要在中断里判断“是否发生了溢出后再补 65536”这种写法更简洁也不会漏。CCR1 寄存器还有一个特性捕获事件发生时计数器当前值会被硬件锁存即使中断响应稍慢读到的仍然是本次触发时刻的值而不是你中断里读寄存器的时刻的值这个机制保证了时间戳的准确性。3.3 从“测周期”到“实时跟踪”平滑与跳变检测缺一不可中断里直接拿单次周期算出来的频率会抖动原因是信号沿存在触发噪声和量化误差。比如 6MHz 计数时钟下测 1kHz 信号单个周期约 6000 个计数±1 个计数的量化误差对应频率波动约 0.17Hz这个数字看起来不大但电机编码器在低速段的占空比抖动可能远大于此。实时跟踪系统不能只做简单平均因为平均窗口太长会让真实频率跳变被“磨平”响应变慢。常见做法是滑动平均配合跳变检测正常情况输出平滑后的频率一旦发现新频率与当前平滑值的相对偏差超过阈值立刻把新值作为输出相当于跳过窗口延迟#define SMOOTH_N 8 #define JUMP_THRESHOLD 0.10f float freq_track_update(float f_new) { static float ring[SMOOTH_N]; static uint8_t idx 0; static uint8_t filled 0; static float ring_sum 0.0f; float avg; if (filled SMOOTH_N) { ring_sum f_new; ring[filled] f_new; avg ring_sum / filled; } else { ring_sum f_new - ring[idx]; ring[idx] f_new; avg ring_sum / SMOOTH_N; } idx (idx 1) % SMOOTH_N; /* 跳变检测偏差超过 10%直接跟进不等窗口更新 */ if (fabsf(f_new - avg) JUMP_THRESHOLD * avg) { return f_new; } return avg; }窗口大小 SMOOTH_N 取 8 是个折中对 1kHz 信号窗口对应 8ms跟踪系统在 8ms 内能把真实变化反映出来对 100Hz 信号窗口对应 80ms但低频本身响应慢80ms 通常可以接受。跳变阈值 JUMP_THRESHOLD 的取值跟信号类型关系很大转速测量建议 5%10%温度或压力这种缓变量可以放到 20%避免误跳。这段代码放在中断里调用时要确保计算时间短fabsf 属于软浮点运算在 F103 上大约几十微秒能接受。如果后续要移植 FreeRTOS建议把这个滤波逻辑挪到任务里中断只负责更新 g_freq_hz任务轮询处理道理一样。4. 频率跟踪系统的 4 个必调参数与常见误配置4.1 PSC 预分频怎么选分辨率和低频下限是同一枚硬币的两面PSC 的值决定了计数时钟频率也决定了 16 位计数器的溢出周期。计数时钟越高单计数时间分辨率越好但现在高频率信号下周期计数值反而小量化误差变大同时 16 位计数器只能覆盖更短的时间范围测不了低频。PSC计数时钟单计数分辨率16 位最大周期理论最低可测频率318MHz55.6ns3.64ms274Hz116MHz166.7ns10.92ms91.5Hz352MHz500ns32.77ms30.5Hz711MHz1μs65.54ms15.3Hz把频范围被要求覆盖 100Hz20kHz 时PSC 11 是常见选择91.5Hz 的最低可测频率包含 100Hz166.7ns 的速率在 20kHz 下的相对量化误差约 0.33%对多数反馈控制系统足够。如果你的实际最低信号是 10Hz那就得把 PSC 加大到 71但这时 20kHz 信号的量化误差会到 2%属于不可接受的水平。这种情况下应该考虑第 4.3 节的定时器级联方案而不是单纯改 PSC。4.2 IC1F 输入滤波防抖与延迟的取舍IC1F 这 4 个位看起来简单设错了直接影响测量精度。不同取值对应不同的采样频率和有效判定次数判定次数越多越抗干扰但单位延迟也越大。IC1F 值内部采样频率有效判定次数理论最大延迟0000不滤波100011fCK_INT 72MHz8约 111ns0110fCK_INT / 4 18MHz6约 333ns1001fCK_INT / 8 9MHz8约 889ns信号源类型决定滤波强度实验室信号发生器输出本身就很干净IC1F 设为 0000 没问题继电器触点或电机碳刷产生的多次抖动可能持续几十微秒这时候必须上 0110 或 1001。一个工程判断标准是滤波延迟不得超过被测信号周期的 0.1%否则精度损失超过了滤波带来的稳定性收益。1kHz 信号周期 1ms0.1% 对应 1μs1001 的 889ns 延迟还可以接受但测 100kHz 信号时周期只有 10μs889ns 的延迟已经占接近 9%应该果断把滤波降档或关掉。4.3 定时器级联扩展量程32 位捕获的正确打开方式16 位计数器在低频测量上是个绕不过去的墙。刚才算过6MHz 计数时钟下只能覆盖到 91.5Hz想测 20Hz 信号必须另想办法。一个常用方案是把溢出事件当作高位扩展TIM2 每溢出一次在一个全局变量里加 1捕获事件读取这个扩展计数配合 CCR1 的 16 位值拼出一个 32 位时间戳。volatile uint32_t g_tim_ovf 0; volatile uint32_t g_last_cap_ext 0; volatile uint32_t g_cap_ext 0; volatile uint32_t g_period_ext 0; void TIM2_IRQHandler(void) { if (TIM2-SR TIM_SR_UIF) { TIM2-SR ~TIM_SR_UIF; g_tim_ovf; } if (TIM2-SR TIM_SR_CC1IF) { TIM2-SR ~TIM_SR_CC1IF; /* 先读 CCR1再读溢出计数顺序不能反过来 */ uint32_t ccr TIM2-CCR1; uint32_t ovf g_tim_ovf; g_cap_ext (ovf 16) | ccr; g_period_ext g_cap_ext - g_last_cap_ext; g_last_cap_ext g_cap_ext; } }顺序问题值得细讲如果先读溢出计数再读 CCR1捕获中断和溢出中断之间一旦插入一次新的溢出CCR1 还是旧值溢出计数已经加了 1时间戳就会错一个 65536 计数。先读 CCR1 再读溢出计数虽然也不能完全避免捕获出现在两次溢出之间的竞态但出错概率低一个数量级。要求更高时可以用 TIM2 TIM3 的级联硬件扩展TIM3 作为 TIM2 时钟的手动扩展部分这是 F103 手册里具备的同步机制但工程上手动计数配合中断已经足够可靠代码也可维护。4.4 串口 1 与串口 3 的差异数据输出环节的时钟分频陷阱频率数据最终要送出去串口是首选。STM32F103 的 USART1 挂在 APB2USART2/3 挂在 APB1这两条总线的外设时钟默认不同导致波特率分频计算基准完全不同。串口所在总线默认外设时钟波特率计算基准USART1APB272MHzBRR 72000000 / BaudUSART2 / USART3APB136MHzBRR 36000000 / Baud常见误配置是把 USART1 的初始化代码原样复制给 USART3波特率按 72MHz 算但硬件是 36MHz实际波特率只有一半串口助手看着全是乱码。更隐蔽的是 36MHz 除以 115200 得到 312.5除不尽USART3 在 115200 波特率下本身就有约 0.16% 的偏差某些要求严格的接收端会偶发丢帧。解决方法是把波特率换成 110592 或 921600 这类能整除的值或者用 CubeMX 生成代码避免手算。频率跟踪系统如果每 1ms 输出一次 float 型频率值每次 4 字节115200 波特率下串行发送耗时约 425μs占每周期近一半的时间必须把发送放到 DMA 里中断里只做数据写入和一帧置位CPU 不用等发送完成。4.5 多路捕获同一定时器不同通道的共享中断处理“stm32f103 多路捕获”是多通道系统最常见的需求原理上和单路完全一致TIM2 的 CH1 和 CH2 分别对应 PA0 和 PA1可以同时配置为输入捕获它们触发同一个 TIM2_IRQHandler。判断哪个通道触发靠读 SR 寄存器里的 CC1IF 和 CC2IF 两个标志位。两路通道的时间基准完全独立各算各的差值就行。需要注意两点一是两路信号频率接近时中断处理里要保证先处理完第一路再处理第二路CPPU 负载接近两倍二是 SPI 等外设如果和 TIM2 的引脚存在复用关系要查重映射。很多时候多路捕获的需求可以用“复用同一路信号”简化比如电力监控里只测一相频率另两相依此推算能省掉一路中断开销这也是实时频率跟踪系统常见的工程做法。5. 自适应闸门让频率跟踪系统在实时性与精度之间自动平衡5.1 固定窗口为什么不够用第 3.3 节的平滑窗口 SMOOTH_N 是固定值。固定窗口带来的矛盾很直接窗口大抗噪能力强但响应慢窗口小响应快但输出抖动。实时跟踪系统被测信号往往不是匀速变化电机从静止到额定转速频率可能跨两个数量级一个固定的窗口参数不可能同时照顾好低速和高速两个阶段。自适应闸门的思路是利用系统已知的当前频率估值动态调整参与平均的周期数量频率高时缩短窗口频率低时拉长窗口。5.2 按当前频率自动调整窗口大小代码里维护一个可变长度的滑动窗口新的频率值进来时根据当前频率重新选择窗口大小uint8_t auto_window_size(float f_cur) { if (f_cur 50000.0f) return 2; if (f_cur 10000.0f) return 4; if (f_cur 1000.0f) return 8; if (f_cur 100.0f) return 16; return 32; }高频段窗口小因为信号周期本身短几个周期的数据已经足够代表当前状态响应延迟也小低频段窗口大但低频信号每个周期时间长窗口长度换算成毫秒并不长。比如 50Hz 信号配 32 窗口对应 640ms 的时间常数对于 50Hz 工频这种变化缓慢的信号完全合适而对 50kHz 信号配 2 窗口对应 40μs跟得上绝大部分动态过程。调用时把 freq_track_update 里的 SMOOTH_N 换成这个动态函数返回值就能实现“频率越高响应越快、频率越低越稳定”的自动平衡。这个技巧在转速波动测量和电网频率跟踪里是常用方案但要注意窗口长度切换瞬间累积和 ring buffer 内容要清零否则新旧窗口的数据混在一起会出现短暂跳变。5.3 阶跃验证法用信号发生器量化跟踪延迟频率跟踪系统好不好不能用“看起来挺稳”来评价要实测量化。准备一台带方波输出和频率扫描功能的信号发生器接 PA0步骤如下信号发生器输出 1.000kHz 方波通过串口记录稳定后的频率值和波动范围验证静态精度。设置频率从 1.000kHz 突变到 2.000kHz触发信号发生器的阶跃或通过外部开关控制记录系统输出第一次越过“1.9kHz”的时间戳和最终稳定到 2kHz±0.5% 的时间戳。用这个差值对比不同窗口参数下的响应时间判断自适应窗口是否真的比固定 8 点窗口更快进入稳态。对低频段重复比如 50Hz 跳到 100Hz观察窗口大小变化对系统是否造成额外延迟。这个实验能同时验证三件事静态精度是否满足需求、跳变响应时间是否在系统控制周期内、以及多路捕获通道之间是否存在串扰。如果静态精度差检查 IC1F 滤波和 PSC 是否匹配如果响应慢优先调整跳变阈值而不是盲目减小窗口。频率跟踪系统的最终评价标准就是两条——稳态时输出抖得小动态时跟得快这两条指标在自适应闸门方案里可以通过两个独立参数分别调节窗口大小管抖动跳变阈值管加速度。实际参数要拿到目标信号源上反复压测没有一个理论值能同时适配所有现场。本文还有配套的精品资源点击获取