ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

STM32测频率全攻略:输入捕获、PWM模式与门控模式详解

STM32测频率全攻略:输入捕获、PWM模式与门控模式详解 简介面向需要测量脉冲、时钟等外部信号频率的嵌入式开发者STM32定时器输入捕获频率测量工程提供了完整可编译的Keil源码。资源基于HAL库实现定时器时基配置、输入捕获通道选择、中断服务处理与周期/频率换算包含初始化函数和中断回调可直接移植到实际项目。压缩包共125个文件、约1.87MB以C源码和头文件为核心并带有Keil工程配置文件、编译生成的axf/hex文件、备份文件及少量txt说明目录结构典型便于对照学习和调试。已有2145人学习下载。工程覆盖定时器配置、捕获中断、连续采样的平均值计算等关键环节同时保留编译中间产物和备份文件适合初学者理解底层捕获机制也适合工程师快速集成或重新验证。通过HAL_TIM_IC_GetCapturedValue等API完整展示了捕获寄存器的读取与周期计算流程。1. 拿到一块 STM32 想测方波频率为什么先别急着翻电平有次我手头接了一个输出方波的传感器信号频率大约 100 Hz第一反应是在 GPIO 外部中断里数电平翻转次数结果读数在 98 和 103 之间跳怎么调都稳不下来。换成定时器输入捕获再花十分钟把分频和滤波配好读数才真正贴住信号源。这段经历基本代表了一类问题STM32 测量频率的难点不在读引脚而在选对定时器的测量方式。同一个引脚测 1 Hz 和测 1 MHz 用的几乎是两套代码选错方法要么溢出要么被中断延迟拖垮。这篇文章按原理、最小工程代码、量程参数、避坑记录和进阶做法的顺序把用 STM32 测量频率的一条完整路径讲清楚适合正在做电机测速、传感器脉冲采集或设备状态监测的开发者。2. 测频率的三种路数输入捕获、中断计时与 PWM 捕获模式的取舍2.1 输入捕获把 CNT 锁存进 CCR两次锁存之差就是周期输入捕获的原理一句话就能说清定时器的 CNT 寄存器一直按内部时钟递增外部信号接到捕获通道上当检测到指定边沿时硬件把当前 CNT 值锁存到捕获寄存器 CCR同时触发一次中断。两次相邻上升沿的 CCR 差值就是被测信号一个周期的长度。反向换算一下就得到频率。这个方案的决定性优势是测量时刻由硬件决定不经过软件判断也不受中断响应延迟影响。比如用 72 MHz 计数频率去测 1 kHz 方波两次捕获之间 CNT 大概走了 72000那么周期就是 72000 个 tick频率算出来就是 1 kHz误差来源基本只剩 1 个 tick 的量化误差和一个可能被忽略的 CNT 回绕问题。我一般把这个方法作为测频率的默认选项因为它在几十 Hz 到几百 kHz 的宽范围内都能用而且中断频率等于信号频率比每个边沿都进中断的做法负载低得多。2.2 中断计时法代码最简单为什么高频和抖动下先翻车另一条常见路线是 GPIO 外部中断加一个微秒级计时器上升沿进中断读一下当前时间下次上升沿再读一下差值就是周期。它的优点是引脚选择自由、代码直观适合对精度完全没要求的场景。但中断计时法有三个天生的软肋。一是中断响应延迟从硬件置位中断标志到软件真正读到计数器中间要经过压栈、跳转、读寄存器这个延迟不是固定值代码分支不同、编译优化不同都会带来几十到几百纳秒的偏差。二是抖动如果信号沿带毛刺中断可能在同一个周期里触发两次读数直接跳变。三是吞吐上限高频时中断频繁进入主循环被挤占丢一次中断整个读数就废了。我的判断标准是只用来测几 kHz 以下的低频脉冲或者是按键、限位开关这种不在乎精度的计数场景一旦要正经测频率不要用它。2.3 PWM 输入模式硬件复位定时器周期和占空比一次拿全PWM 输入模式本质是「从模式复位 两路输入捕获」的组合。把被测信号接到 TI1配置从模式复位让上升沿到来时 CNT 直接清零同时通道 1 捕获触发值作为周期通道 2 配置为下降沿捕获得到脉宽。这样一次测量同时拿到周期和占空比全程由硬件完成捕获顺序不需要软件判断哪个边沿先到。TIM_SlaveConfigTypeDef sSlaveConfig {0}; sSlaveConfig.SlaveMode TIM_SLAVEMODE_RESET; sSlaveConfig.InputTrigger TIM_TS_TI1FP1; sSlaveConfig.TriggerPolarity TIM_TRIGGERPOLARITY_RISING; sSlaveConfig.TriggerFilter 0x0F; HAL_TIM_SlaveConfigSynchro(htim, sSlaveConfig);这段配置的含义是从模式复位让 CNT 在触发边沿清零触发源选 TI1FP1 也就是 TI1 经过滤波后的信号极性设为上升沿触发滤波取 0x0F 用来滤掉窄毛刺。之后还要分别使能通道 1 和通道 2 的输入捕获把通道 2 的极性设成下降沿否则读到的 CCR2 不是脉宽。这个方案适合被测对象本身是 PWM 信号、需要同时关心频率和占空比的场合比如电机驱动器的使能反馈、开关电源的开关频率监控。代价是占用了定时器的两个捕获通道比单纯测频率多占一路资源。2.4 三种方案怎么选一张表说清量程、误差与资源方案典型可测量程72 MHz 主频主要误差来源中断压力适用场景输入捕获几十 Hz 到几百 kHz1 个 tick 量化、CNT 回绕每周期一次PWM 信号、编码器测速、通用频率计中断计时几 Hz 到几 kHz中断延迟、边沿抖动每边沿一次高频扛不住低频脉冲、按键计数、粗略测速PWM 输入模式几百 Hz 到几百 kHz触发抖动、滤波参数每周期一次需要同时测频率和占空比的 PWM 场景选型时我先回答两个问题被测信号是纯方波还是带着噪声的传感器输出要不要同时读占空比答案基本就指向输入捕获或 PWM 输入模式。中断计时只用于临时验证引脚有没有信号不适合做成产品里的正式测量路径。3. 用 HAL 库跑通输入捕获测频率最小工程代码与回绕处理3.1 引脚与定时器怎么配先看时钟树再选通道写代码之前先花两分钟确认定时器挂在哪条总线上。以常见的 F103 为例主频 72 MHzAPB1 分频为 2 时挂在 APB1 上的 TIM3、TIM4 等定时器时钟反而是 72 MHz而不是 APB1 外设时钟的 36 MHz因为定时器时钟在 APB1 预分频系数不等于 1 时会自动翻倍。这个细节无数人栽过在 CubeMX 里把外设时钟设成 36 MHz心想定时器也是 36 MHz结果 Prescaler 全部按一倍算错。通道选择上TIM3_CH1 默认复用引脚是 PA6TIM4_CH1 是 PB6TIM2_CH1 是 PA0。我习惯优先用 TIM3_CH1因为 PA6 不占调试口也不和其他常用功能冲突。要注意的是 F103 上还有 AFIO 重映射如果用默认复用引脚不需要额外配置如果换到重映射引脚必须开启 AFIO 时钟并设置重映射否则中断永远不触发。3.2 定时器与 GPIO 初始化代码每一个参数都说明白下面是 CubeMX 生成习惯的 HAL 写法我在此基础上加了逐项注释。先初始化定时器本身TIM_HandleTypeDef htim3; void MX_TIM3_Init(void) { TIM_IC_InitTypeDef sConfigIC {0}; htim3.Instance TIM3; htim3.Init.Prescaler 71; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 65535; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_IC_Init(htim3); sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; sConfigIC.ICSelection TIM_IC_SELECTION_DIRECTTI; sConfigIC.ICPrescaler TIM_ICPSC_DIV1; sConfigIC.ICFilter 0; HAL_TIM_IC_ConfigChannel(htim3, sConfigIC, TIM_CHANNEL_1); }参数含义逐一解释。Prescaler 71 表示 72 MHz 定时器时钟分频 72 倍CNT 每 1 微秒加一分辨率对大多数低频场景足够。Period 65535 是 ARR 上限CNT 从 0 计到 65535 再回绕到 0。一个计数周期覆盖 65.535 毫秒意味着测低于 15 Hz 的信号必须靠溢出计数后面会讲。ClockDivision 影响数字滤波器的采样时钟分频这里选 DIV1。ICPolarity 上升沿表示在信号上升沿捕获。ICPrescaler 选 DIV1 表示每个事件都捕获如果选 DIV2 会每两个事件才捕获一次中断压力减半但会丢细节测频率不推荐。ICFilter 先设 0让基础通路先跑通遇到毛刺问题再开滤波。GPIO 这边按复用推挽配置void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); }Mode 必须是 AF_PP也就是复用推挽输出这表示引脚交给片上外设控制而不是普通 GPIO。Pull 选 NOPULL因为输入捕获本身不依赖上下拉如果信号源是开漏输出这里要改成上拉。Speed 选 HIGH保证高频信号边沿不会被驱动能力拖慢太多。在 F4 系列上还要给 GPIO_InitStruct.Alternate 赋定时器复用编号F1 用默认复用不需要这一步。启动捕获和中断的代码放在主循环之前HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); __HAL_TIM_ENABLE_IT(htim3, TIM_IT_UPDATE); HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM3_IRQn);第一行使能通道 1 的捕获并打开捕获中断第二行打开更新中断用来记录 CNT 溢出第三、四行设置 TIM3 的中断优先级。注意这里更新中断和捕获中断共用同一个 TIM3 中断向量优先级设置只是给整个外设用的。3.3 在捕获回调里把两次边沿换算成周期顺带处理 CNT 回绕如果不管 CNT 回绕代码在低频时可能看起来正常频率一高就乱跳。原因是 ARR 设为 65535 后CNT 会周期性地从 65535 回绕到 0如果两次捕获之间发生过回绕直接做减法得到的是负值或错误的小值。最小工程里的做法是开一个溢出计数器在捕获时把溢出次数一起算进去。volatile uint32_t capture_last 0; volatile uint32_t overflow_cnt 0; volatile uint64_t period_ticks 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { overflow_cnt; } } void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { uint32_t now HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); uint64_t delta (uint64_t)overflow_cnt * (TIM3-ARR 1) now - capture_last; capture_last now; overflow_cnt 0; if (delta 0) { period_ticks delta; } } }逻辑说明每个定时器溢出周期PeriodElapsedCallback 里对 overflow_cnt 加一捕获事件到来时从上次捕获到本次捕获之间一共走了多少 tick等于溢出次数乘以 ARR 加一再加上本次捕获值减上次捕获值。算完立即清零溢出计数确保下一次捕获的统计区间正确。这个写法把周期值直接放进全局变量 period_ticks主循环只负责读。要注意 HAL 的中断入口里更新事件先被检查溢出计数会先登记再进捕获回调所以这个差值公式的顺序是成立的如果自己写寄存器版中断务必把溢出判断放在读取捕获值之前。3.4 主循环里读出频率整数除法、浮点和刷新率怎么权衡主循环里不需要重新计算差值只做频率换算uint32_t timer_clock 72000000 / (71 1); float freq 0.0f; while (1) { uint64_t ticks; __disable_irq(); ticks period_ticks; __enable_irq(); if (ticks 0) { freq (float)timer_clock / (float)ticks; } HAL_Delay(200); }读 period_ticks 时临时关中断避免读到一半被捕获中断改写成中间状态这是多字节变量必然要做的保护。timer_clock 等于 1 MHz对应前面 Prescaler 71。换算频率用浮点是为了低频精度假设实际频率是 100.4 Hzticks 约 9960整数除法会直接截成 100 Hz浮点能保住小数位。刷新率由 HAL_Delay(200) 控制每秒 5 次。测电机转速够用。如果做快速反馈把延时缩到 20 毫秒但要确认此时的捕获中断和更新中断不会因为主循环频繁抢占而互相拖累。4. 量程与参数设计把定时器当成一把有刻度的尺子4.1 先定分辨率再定上限Prescaler 与 ARR 的配合公式配定时器参数的顺序我习惯反过来先定分辨率再倒推 Prescaler最后看 ARR 能不能覆盖目标最低频率。计数频率 f_tick 等于定时器时钟除以 Prescaler 加一一个 tick 的时间是 1/f_tick测量周期的量化误差就是 1 个 tick。比如你要测的信号的周期是 10 ms用 1 us 的 tick相对误差是 0.01%如果被测周期只有 10 us同样 1 us 的 tick 误差就到了 10%。所以高频段必须先提高计数频率也就是调小 Prescaler否则量化误差直接吞掉测量精度。目标频段建议 Prescaler1 tick 时长需要特别注意1 Hz ~ 100 Hz71910 us必须配合溢出计数否则 CNT 回绕漏计100 Hz ~ 1 kHz711 us默认参数分辨率与中断压力均衡1 kHz ~ 100 kHz0约 13.9 nsARR 回绕频繁溢出中断要及时处理100 kHz ~ 1 MHz0约 13.9 ns量化误差开始变大建议考虑门控模式计满一个 ARR 周期的时间等于 ARR 加一乘以 tick 时长。以 Prescaler 71、ARR 65535 为例一个计数周期是 65.535 ms对应最低可测频率大约 15 Hz。想测 1 Hz要么拉大 ARR 到接近 7 万要么加溢出计数后者不用动 ARR 更省事。还有一点容易算错F103 的 TIM3 挂在 APB1 上APB1 分频为 2 时定时器时钟是 72 MHz不是外设总线看到的 36 MHz计算 Prescaler 前先把这个数字确认清楚。4.2 输入滤波和信号整形传感器信号不是直接能进定时器的内部 ICFilter 的作用是让触发信号必须连续采样到若干个有效电平才被认为是一次边沿从而把窄毛刺滤掉。ICFilter 的取值范围是 0 到 15对应 2 到 32 次连续采样。判断毛刺宽度的方法很简单先用示波器看毛刺宽度小于被测信号周期的 10%就可以放心设滤波如果毛刺已经接近正常脉冲宽度内部滤波会连真信号一起滤掉这时候要从硬件层面处理。我会在代码里先保持 ICFilter 0 跑通再用示波器确认信号形状最后根据毛刺定参数。毛刺只有几百纳秒设 0x5 左右基本能压住毛刺达到微秒级内部滤波的采样窗口就不够用了。更常见的问题其实是电平调理24 V 传感器输出、继电器触点、带振铃的差分信号都不能直接接进 3.3 V 引脚。前级要么用电阻分压加钳位要么用比较器整形内部滤波只负责补最后一段它替代不了电平转换。输入引脚的上下拉也属于这一类问题开漏信号不配上拉捕获到的沿会非常不稳定。4.3 测量窗口与刷新率测转速和测音频的更新要求完全不同输入捕获天然是单周期测量单个周期受抖动影响大。想读数稳定可以做滑动平均也可以固定一个时间段内统计本质上都是增加测量窗口。窗口越长越稳但刷新率会下降。测转速时传感器每圈输出几个脉冲转速变化率低窗口取 0.5 到 1 秒完全没问题测音频信号这类快速变化量窗口必须控制在 20 毫秒左右否则读数拖影严重。我的经验值是测量窗口至少覆盖 10 个被测周期低于这个比例平均值没有意义。比如测 100 Hz 信号窗口 100 毫秒覆盖 10 个周期刷新率 10 Hz平衡点可以接受。窗口内还可以做中值滤波而不是平均滤波因为毛刺引起的异常值会把平均值拉偏中值能直接扔掉最离谱的那几个点。如果窗口很长、溢出中断压力大就把第 3 章的溢出判断改成只在窗口结束后读一次 CNT配合 DMA 搬运这是后面要讲的门控思路的雏形。5. 测量频率的避坑记录五个看过示波器才会明白的问题5.1 读数突然变成 0 或跳满量程CNT 回绕没有进溢出中断现象低频信号测得好好的频率一提高读数开始随机变成 0 或者某个很大的数。原因ARR 65535 时 CNT 回绕到 0而代码里没有登记溢出两次捕获差值变成负值或错误的差值。解决打开更新中断在 PeriodElapsedCallback 里维护溢出计数让差值公式把每次溢出折算成 ARR 加一。注意更新事件和捕获事件在同一个中断入口判断顺序一定是先处理溢出再读捕获值顺序反了溢出就漏计这也是初学者最容易踩的坑。加完溢出计数后用 50 kHz 方波验证读数应该不再跳。5.2 信号稳定读数却乱跳毛刺、极性和滤波三个环节一起查现象信号发生器输出标准的 1 kHz 方波示波器看波形很干净板子读数却在 990 到 1010 之间来回跳。原因多半是触发边沿上有亚微秒级毛刺或者是信号源与板子之间存在地环路噪声导致沿的位置在抖动。解决先检查极性配置确认捕获的是上升沿还是下降沿和信号的跃迁方向一致。然后把 ICFilter 逐步加大观察读数是否收敛同时用短跳线直接连接信号源和板子排除长线的耦合。碰上继电器触点这类接触抖动内部滤波压不住要在外部加 RC 消抖电路。5.3 PWM 占空比和示波器对不上从模式复位触发的捕获顺序反了现象PWM 输入模式下频率读数正确占空比却和示波器差几个百分点甚至完全反过来。原因两个通道的捕获顺序不对。通道 1 上升沿复位并捕获周期通道 2 下降沿捕获脉宽如果通道 2 极性漏设成下降沿它会在通道 1 的下降沿触发读到的值根本不是脉宽。解决先把通道 2 的 ICPolarity 改为 FALLING再检查 CCR1 和 CCR2 哪个更大。正常情况下 CCR2 小于 CCR1如果反了说明触发极性整体错了。滤波值设置过大也会导致窄脉冲被吃掉占空比偏小这种现象在占空比低于 10% 时尤其明显。5.4 引脚复用没配对代码烧进去采不到边沿现象代码逻辑看着没问题中断就是不触发示波器点引脚上有方波。原因GPIO 的模式没设成复用推挽或者用了非默认复用引脚但没有配置重映射。F1 上很多人直接照抄普通 GPIO 输入代码Mode 设成 INPUT引脚被当成普通 IO定时器收不到信号。解决把 GPIO_InitStruct.Mode 改成 GPIO_MODE_AF_PP如果用的不是默认复用脚在代码里开启 AFIO 时钟并调用重映射函数。F4 上对应的坑是 Alternate 参数没赋定时器复用编号。这类问题最有效的排查方式是读一遍引脚复用表不要靠猜。5.5 示波器看信号好好的接到板子一路为零共地接好了吗现象示波器探头怎么点都有波形杜邦线接到开发板后频率显示 0 或乱跳。原因示波器探头的地夹在信号源地线上而开发板的地没有和信号源共地两者参考电位不一致捕获触发始终不满足条件。解决先把信号源的地和板子的 GND 用一根线连通再串一个 100 到 200 欧的电阻接到捕获引脚防止振铃。这个环节看着像玄学其实是多数「代码没问题但读数不对」的真实原因。我之前花了一下午改滤波参数最后发现就是地线没接从那以后接任何信号第一件事就是确认共地。6. 再进一步用定时器门控模式替代中断测频率更稳6.1 门控模式的思路让被测信号当闸门计数交给另一个定时器第 3 章的思路是每个周期进一次捕获中断靠中断完成测量。信号频率很高时这个中断压力会先撑不住。门控模式把思路反过来让一个定时器负责管理「窗口」另一个定时器在窗口打开期间自由计数。窗口由被测信号的 N 个周期构成窗口内从定时器计的 tick 总数除以 N就是平均周期。中断频率从每周期一次降到每窗口一次而且抖动被平均掉了。代价是需要两个定时器做主从互联代码复杂度上升。6.2 主从定时器互联的最小配置与读数方法从定时器开启门控模式的代码比较短TIM_SlaveConfigTypeDef sSlaveConfig {0}; sSlaveConfig.SlaveMode TIM_SLAVEMODE_GATED; sSlaveConfig.InputTrigger TIM_TS_ITR0; sSlaveConfig.TriggerPolarity TIM_TRIGGERPOLARITY_NONINVERTED; sSlaveConfig.TriggerFilter 0; HAL_TIM_SlaveConfigSynchro(htim2, sSlaveConfig);SlaveMode 设为 GATED 表示计数器只在触发输入有效期间递增InputTrigger 选 ITR0 表示触发源来自主定时器的 TRGO极性非反相表示触发高电平期间计数低电平停止。实际工程里需要先按参考手册的触发连接表确认 ITR 编号对应哪个主定时器再配置主定时器把外部信号转换成窗口输出。测量结束后去读从定时器的 CNT就能算出平均周期。窗口边沿处最多有 2 个 tick 的量化误差窗口越长误差占比越低。这个方法特别适合高频、抖动大的信号代价是占两个定时器。用这套方案处理之前那个 100 Hz 传感器时我把捕获中断全部拿掉了只留窗口结束后的读取读数稳定后就没再动过。门控模式的配置比输入捕获繁琐但它把测量从「每个边沿抢时间」变成了「把一个窗口算清楚」工程上省心很多。也因为这个经历我现在拿到任何测频需求都会先问一句被测信号多少赫兹、要不要同时看占空比再决定用哪套方案。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表