
简介针对STM32F030F4P6微控制器的定时器输入捕获应用这份工程代码展示了如何利用TIM14通道1采集外部信号并计算频率适合正在学习STM32定时器与中断开发的嵌入式初学者也可作为低成本频率计项目的参考模板。资源包共126个文件以标准外设库的c/h源文件为主同时包含编译生成的o、crf、axf、hex及调试配置文件压缩包大小约1.6MB便于直接打开工程查看或烧录验证。目前已有2246人学习下载。工程内不仅包含定时器时基初始化、输入捕获通道配置和中断服务例程还实现了基于捕获周期计算频率的逻辑并针对1-1000Hz测量误差小于1%的要求提供了软件滤波思路。通过阅读和修改代码可以掌握捕获极性设置、预分频计算、中断处理以及浮点精度控制等关键知识点为后续开发更复杂的信号测量系统打下基础。1. 为什么测频率首选输入捕获先对比再动手上周有个做小家电控制板的朋友找我说用STM32F030F4P6测电机转速波形一上来CPU就被外部中断淹没了问我有没有更省力的办法。我给他的方案很直接别用外部中断逐边沿打时间戳改用定时器输入捕获。这篇就把这个代码工程完整拆开讲一遍从选型理由到寄存器配置再到误差处理和踩坑排查适合刚接触F0系列、或者想把测频模块做稳的开发者。在动手之前先把三种常见测频方案摆在一起看。很多人一上来就写外部中断其实是习惯了F1的玩法没想过还有更优解。方案原理精度CPU占用高频表现低频表现外部中断定时器时间戳每个边沿触发中断中断里读计数器算差值低受中断延迟影响大高频率越高中断越密集差容易中断风暴好定时器门控计数一个定时器做闸门另一个对输入脉冲计数高闸门时间越长越准低好差闸门内脉冲太少误差大输入捕获周期法硬件在边沿瞬间锁存计数器值中断里做减法中高与频率相关极低差周期太短计数值小好调整预分频即可输入捕获最大的优势在于边沿到来时当前计数器值被硬件自动锁存到捕获寄存器不需要CPU实时响应。也就是说中断延迟、主循环卡顿、高优先级任务抢占都不会影响边沿的时刻记录。CPU要做的只是在捕获中断里把两次锁存值做个减法然后继续干别的。F030F4P6这颗芯片虽然便宜、Flash只有16KB但定时器资源并不寒酸。TIM1、TIM3都在TIM3的通道1默认映射到PA6这个引脚在TSSOP20封装里几乎不会被其他外设抢占用来做频率输入非常顺手。顺带一提这种用硬件锁存代替软件打时间戳的思路不仅输入捕获能用到很多编码器接口、PWM测量场景里也是同一个底层逻辑。2. 配置代码逐段拆解寄存器比HAL更可控2.1 GPIO复用配置PA6到底要设成什么模式很多人第一坑就栽在GPIO上。输入捕获不是把引脚配成普通输入就完事必须把引脚切换到定时器的复用功能AF否则TIM3根本收不到信号。// 使能GPIOA时钟 RCC-AHBENR | RCC_AHBENR_GPIOAEN; // PA6设为复用功能模式 GPIOA-MODER ~GPIO_MODER_MODER6; GPIOA-MODER | GPIO_MODER_MODER6_1; // 选择复用功能AF1即TIM3_CH1 GPIOA-AFR[0] ~GPIO_AFRL_AFSEL6; GPIOA-AFR[0] | (1 GPIO_AFRL_AFSEL6_Pos);注意F0和F1的区别非常大。F1用的是AFIO重映射寄存器F0直接在GPIOx_AFR里选择AF编号。PA6的AF1就是TIM3_CH1这个编号查数据手册的“Alternate function mapping”表就能确认不需要死记。2.2 时基初始化预分频值到底怎么算定时器时基是输入捕获的核心预分频决定了计数时钟也就决定了测量的时间分辨率。我习惯让定时器每1微秒计一个数这样捕获寄存器里的差值直接就是信号周期微秒计算频率时非常直观。// 假设系统时钟48MHz // 计数频率 48MHz / (PSC 1) 1MHz即1us计数一次 TIM3-PSC SystemCoreClock / 1000000 - 1; TIM3-ARR 0xFFFF; // 16位计数器最大计数值65535这里最容易错的是忘了减1。预分频器的值是0基的PSC写47实际分频是48。如果系统时钟不是48MHz直接用SystemCoreClock这个全局变量算别写死。ARR设成0xFFFF意味着计数器从0数到65535后溢出。65535微秒约65.5毫秒对应最低可测频率约15Hz。如果你要测的转速信号可能低到5Hz就得把PSC调大比如让计数频率降到100kHz这样最大周期能到655.35ms对应约1.5Hz。代价是时间分辨率从1us变成10us这就是量程和精度的取舍后面第三节细说。2.3 捕获通道配置边沿、滤波、使能时基准备好之后配置通道1为输入捕获模式映射到TI1输入上升沿触发。// 通道1映射到TI1 TIM3-CCMR1 ~TIM_CCMR1_CC1S; TIM3-CCMR1 | TIM_CCMR1_CC1S_0; // 不使能输入滤波上升沿捕获 TIM3-CCMR1 ~TIM_CCMR1_IC1F; TIM3-CCER ~TIM_CCER_CC1P; TIM3-CCER | TIM_CCER_CC1E; // 使能捕获中断和更新中断 TIM3-DIER | TIM_DIER_CC1IE | TIM_DIER_UIE; // 启动计数器 TIM3-CR1 | TIM_CR1_CEN; // NVIC使能 NVIC_EnableIRQ(TIM3_IRQn); NVIC_SetPriority(TIM3_IRQn, 1);IC1F是输入滤波配置位默认是0即不滤波。如果待测信号比较干净不滤波反而更好因为滤波会引入边沿判定的延迟影响高频测量。信号有毛刺时再按需开启第四节单独讲。CC1P位是极性选择0表示上升沿捕获1表示下降沿。如果你的信号是低电平有效或者想测负脉冲把这一位置1就行。2.4 中断服务函数差值法捕获周期中断里只做一件事算出本次捕获值和上次捕获值的差这个差值就是信号周期。注意第一次捕获没有“上一次”可减需要丢弃从第二次捕获才开始产生有效周期。volatile uint16_t g_last_cc 0; volatile uint16_t g_measured_period 0; volatile uint8_t g_capture_ready 0; void TIM3_IRQHandler(void) { if (TIM3-SR TIM_SR_CC1IF) { uint16_t now TIM3-CCR1; uint16_t diff; if (now g_last_cc) { diff now - g_last_cc; } else { // 计数器回绕需要补上0x10000 diff 0x10000 - g_last_cc now; } g_last_cc now; if (diff 0) { g_measured_period diff; g_capture_ready 1; } // 清除捕获标志 TIM3-SR (uint16_t)~TIM_SR_CC1IF; } if (TIM3-SR TIM_SR_UIF) { // 溢出中断可用来标记“周期超时” TIM3-SR (uint16_t)~TIM_SR_UIF; } }这里必须强调一个原则中断里绝对不要做除法。Cortex-M0内核没有硬件除法指令软件除法会消耗几十上百个周期放在中断里会严重拖慢响应。正确做法是把原始周期值存到全局变量主循环里再算频率。另外这个差值法已经隐式处理了计数器回绕的情况不需要额外维护溢出计数。前提是一次捕获间隔内只发生了一次溢出也就是周期不超过65.5ms。如果待测信号周期可能超过这个值就得配合更新中断里的溢出计数来判断否则数据不可信。2.5 主循环读频率整数运算的精度技巧主循环里把周期值换算成频率核心公式是频率Hz 计数频率Hz / 周期计数值。if (g_capture_ready) { g_capture_ready 0; uint16_t period g_measured_period; if (period 0) { // 避免整数除法丢精度先乘100再除 uint32_t freq_x100 100000000UL / period; // 例如 freq_x100 100023表示1000.23Hz } }直接用1000000 / period也能算但整数除法会把小数部分全丢掉1kHz信号会显示1Hz完全没法用。先乘100再除就能保留两位小数实测显示效果好了不止一个数量级。想要更稳的话可以在主循环里做多次平均。我一般取4次或8次周期值求平均再换算频率能明显压低抖动。3. 频率计算公式与误差控制测出来的数能不能信3.1 从捕获值到频率的完整推导整个测量链路可以这样理解定时器以固定频率计数信号每个上升沿触发一次捕获把当时的计数值锁存。两个上升沿之间的计数差值就是信号一个周期内“走过了多少个计数节拍”。知道了每拍多长自然就得到时间间隔。周期换算时间T period / 计数频率。 频率换算f 1 / T 计数频率 / period。举例计数频率1MHz捕获差值为1000则周期为1000微秒频率为1kHz。这个公式简单到能用口算但实际工程里问题往往出在误差上。3.2 误差的三个主要来源第一个是时钟源本身的误差。F030F4P6内部HSI精度大约在±1%左右这意味着即使所有计算都完美测出来的频率也可能比真实值偏差1%。如果项目对频率精度有要求建议外部加晶振或者至少在量产时做校准。第二个是量化误差。计数时钟是离散的信号边沿落在两个计数节拍之间时捕获值会取前一个或后一个最多偏差1拍。这个误差对高频和低频的影响完全不同输入频率周期计数值1MHz计数单拍量化误差占比1kHz1000±0.1%10kHz100±1%100kHz10±10%100Hz10000±0.01%这个表说明一个反直觉的结论在输入捕获周期法里频率越低反而测出来的相对精度越高因为周期内包含的计数节拍更多。频率高了之后周期本来就短一拍的误差占比就很大。所以想用输入捕获测100kHz以上的信号必须提高计数频率或者换门控计数方案。第三个是信号本身的阻抗和边沿质量。如果输入波形上升沿很缓或者有高频噪声叠加捕获边沿的触发点会前后抖动导致测量值忽大忽小。这种误差不是算法能抹平的得从硬件端解决。3.3 提高精度的小技巧保证共地。信号发生器和开发板必须共地浮地会导致输入偏置漂移。能用内部上拉就开上拉让空闲电平稳定避免悬空噪声。多次测量取平均能有效压低随机抖动。如果系统有主循环任务切换务必保证周期值读取和计算期间不会被新中断覆盖。可以用临界区保护。我用信号发生器实测过1kHz方波输入计数频率1MHz连续取8次平均后显示值在999.8Hz到1000.2Hz之间跳动与理论量化误差基本吻合。4. 实测问题排查频率跳变的完整定位过程4.1 问题一频率值一直是0或者偶尔跳出一个大数这是最常见的现象也是排查链路最长的问题。我的排查顺序是先看GPIO复用、再看共地、最后看电平。很多人第一步就错了——把PA6配成了普通输入模式定时器根本收不到信号。先回头检查AFR寄存器PA6务必是AF1可以在调试器里直接读GPIOA-AFR[0]确认。共地问题更隐蔽。信号发生器自己有电源如果它的GND没和开发板GND连在一起示波器看起来波形正常但MCU输入的共模电压可能已经偏到二极管保护区间了。建议先用万用表量PA6对GND的电压信号为3.3V方波时应该能测到0V和3.3V两种电平。还有一个容易忽略的点信号源输出阻抗和3.3V的逻辑电平是否匹配。有的信号发生器是5V TTL输出直接灌到F0引脚上有烧毁风险最好加电平转换或串联电阻限流。4.2 问题二低频信号测不准周期值经常跑到65535附近15Hz以下信号的周期超过65.5ms16位计数器会溢出。你看到的周期值不是真实周期而是回绕补值后的残差。虽然差值法能正确处理一次回绕但如果两次捕获之间经过了超过一个满周期信息就彻底丢了。解决办法是把计数频率降下来。比如PSC改成 SystemCoreClock / 100000 - 1计数时钟变成100kHz每个计数节拍10us最大可测周期655.35ms覆盖到1.5Hz。代价是量化误差从1us变成10us不过对低频信号来说周期本来就大相对误差仍然在可接受范围。4.3 问题三高频信号捕获异常测出来频率只有实际值的一半甚至更低高频问题通常是输入滤波惹的祸或者信号本身上升沿过快、边沿抖动导致硬件触发不稳定。如果你开了ICF滤波频率高了之后滤波采样点可能跟不上边沿导致漏触发。另一个高频陷阱是想当然地提高计数频率。计数频率越高65535个计数值对应的时间越短可测的最低频率反而变高。比如用48MHz计数最大周期只有1.36ms最低频率约733Hz。所以高频场景下输入捕获不是万能药需要用定时器门控计数或者更专业的测频方案。4.4 毛刺导致触发混乱何时开启输入滤波如果信号本身带毛刺在示波器上看到上升沿附近有振铃捕获值会忽大忽小。这时可以启用ICF数字滤波让边沿必须连续稳定N个采样周期才被确认。// 以fDTS/8采样连续稳定8拍才算有效边沿 TIM3-CCMR1 ~TIM_CCMR1_IC1F; TIM3-CCMR1 | (0x07 TIM_CCMR1_IC1F_Pos);但注意滤波是有代价的它会把信号边沿的实际触发时刻延迟几个采样周期。如果信号频率本身很高延迟可能占周期的很大比例导致测量偏差。所以滤波这事要按需开信号干净就别开有毛刺时从小档位试起别一上来就拉满。5. 工程化封装与后续扩展5.1 把测频模块做成独立文件代码写了几百行之后如果全部堆在main.c里后面改起来非常痛苦。我建议把测频功能拆成freq_measure.c和freq_measure.h对外只暴露两个接口void FreqMeasure_Init(void); uint32_t FreqMeasure_ReadHz100(void); // 返回频率×100避免浮点内部实现可以是状态机初始化、等待第一次捕获、后续每次捕获都更新周期值并置标志。主循环调用ReadHz100时读的是最近一次完整周期的换算结果。这种分层设计的好处是以后想换定时器、换引脚、甚至换另一颗芯片主程序完全不用动。5.2 从F0移植到其他芯片时要注意什么我最近看到不少人在讨论PY32这类国产M0芯片它们和STM32F0的定时器外设确实非常像但移植时千万别指望直接烧录。三个地方必须重新核对时钟树APB1总线时钟频率可能不同、引脚复用AF编号AF映射表各芯片不一样、中断号TIM3_IRQn的定义可能不同。系统时钟变了PSC的计算公式也要跟着改。5.3 扩展方向PWM脉宽测量和自动量程输入捕获不止能测频率。如果配置两个通道一个捕获上升沿、一个捕获下降沿就能算出PWM的高电平时间进而得到占空比。F0的TIM3_CH1和TIM3_CH2刚好对应PA6和PA7硬件上天然支持。自动量程是个更好玩的扩展。初始化时先用较大的预分频值如果连续几次测到的周期都很小说明信号频率高就动态切到更小的预分频反过来周期快溢出时就往大预分频切。这个逻辑用状态机实现最清晰每次换挡后丢弃前两次捕获值等新量程稳定再上报频率。有一点我得提醒输入捕获测频率不是万能方案。低频精度差、高频测不了、占空比极端的信号容易波动这些都是它的边界。搞清楚边界才知道什么时候用它、什么时候换方案。这个板块我做了快两年最后沉淀下来的一句话就是硬件能做好的事绝不让CPU硬扛——输入捕获就是这句话最好的例子。本文还有配套的精品资源点击获取