一篇搞懂 STM32 TIM 输入捕获:频率怎么测?占空比怎么测?附完整标准库代码
本文深入解析STM32定时器的输入捕获功能,重点讲解普通输入捕获测频率和PWMI模式测频率+占空比的实现原理。通过分析输入捕获的本质(边沿触发时CNT值锁存到CCR),详细介绍了测周法的实现思路和代码配置,并指出常见配置误区。文章还对比了普通输入捕获与PWMI模式的区别,PWMI通过双通道配置可同时获取周期和高电平时间,从而计算频率和占空比。最后提供了完整的标准库代码实现,并强调实践验证的重要性,
摘要
很多人学 STM32 定时器时,PWM 输出一看就会,但一到输入捕获就开始发懵:CCR 到底存了什么?为什么配置成 Reset 模式就能测频率?PWMI 又为什么能同时测出频率和占空比?本文结合 TIM 输入捕获的核心原理,把“普通输入捕获测频率”和“PWMI 测频率+占空比”一次讲透,并附上标准库完整代码,适合刚学完 STM32 定时器的同学直接上手。
目录
前言
学定时器的时候,很多人会觉得 PWM 输出很直观:
无非就是让 CNT 和 CCR 比较,然后在合适的时刻翻转电平。
但输入捕获就不一样。它不是“往外输出”,而是“把外部输入信号的时间特征记下来”。只有你把这句话理解透了,后面的测频率、测脉宽、测占空比,其实就都顺了。公开资料中对 TIM 输入捕获的描述也很一致:当输入通道检测到指定边沿时,当前 CNT 的值回被锁存在 CCR 中,然后再根据这个计数值换算出周期、频率和占空比。
一、TIM 输入捕获的本质,一句话吃透
输入捕获的本质可以直接记成下面这句话:
外部边沿一来,CNT 当前值立刻存入 CCR。
比如一个 PWM 波形进来:
-
在上升沿时捕获一次;
-
在下降沿时捕获一次;
-
在下一次上升沿再捕获一次;
这样你就拿到了一个完整周期里的关键时间点。
于是:
-
两次上升沿之差,可以算出周期
-
上升沿到下降沿之差,可以算出高电平时间
-
有了周期和高电平时间,就能算出频率和占空比
-
所以输入捕获你只要记住两个核心:
-
CCR 里存的不是电平,而是时间刻度对应的计数值。
-
频率、脉宽、占空比,都是由这些计数值换算出来的。
-
-
这也是为什么输入捕获特别适合测量 PWM 波形。公开资料同样指出,输入捕获常用来测量输入信号的频率、占空比、脉宽和脉冲间隔。
*附:输入捕获通道

二、输入捕获模式测频率基本知识

2.1 测频率的两种常见方法
对于一段 PWM 波形频率的测量,我们通常有测频法和测周法两种方式:如下图

2.1.1 测频法
这种方法是设定一个闸门时间T,在时间T内记录上升沿的次数N,用N/T就能得到PWM波形的频率。相较于PWM波形的周期,这种测量方式中闸门周期的时间较长,因此适宜测量频率较高的PWM波形以减小误差。
2.1.2 测周法
这种方法是设定一个标准频率f,在两个上升沿内以f进行计次,得到N,则频率为1/[N*(1/f)],即f/N。相较于PWM波形的周期,这种测量方式响应较快,因此适宜测量频率较低的PWM波形以减小误差。
2.1.3 中界频率
为了给定“多大算大”,“多小算小”的边界值,我们令两种测量方式中的相等,联立解出一个频率,我们称这个频率为中界频率,因此小于中界频率时,我们采用测周法;大于中界频率时,我们采用测频法。
2.2 使用测周法实现 PWM 波形频率的测量
输入捕获测频率,最常用的思路就是“测周法”。因此本文我们以“测周法”的思路实现测量PWM波形频率的代码,也欢迎读者进一步思考“测频法”对应的代码!
2.2.1 基本思路
让定时器 CNT 在内部时钟驱动下不断自增。
然后把输入捕获的触发源选到TI1FP1,再把从模式设成Reset。
这样每次上升沿到来时,硬件会自动做两件事:
-
先把当前 CNT 锁存到 CCR1
-
再把 CNT 清零,开始下一轮计数
于是 CCR1 里保存的,就是上一个周期的计数值 N。
再根据公式 fx = fc / N 计算得到待测频率。
三、输入捕获模式测频率代码实现
3.1 接线图

3.2 PWM.c
这里我们使用PA0输出一段1kHz的PWM波形,使用PA6捕获,并测量对应的PWM频率
在PWM.c中,配置好PA0的参数,同时将测量频率和测量占空比的模块分别封装成对应的函数,以便主函数中进行调用。
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0; //CCR
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);
}
void PWM_SetPrescaler(uint16_t Prescaler)
{
TIM_PrescalerConfig(TIM2, Prescaler, TIM_PSCReloadMode_Update);
}
3.3 PWM.h
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);
void PWM_SetPrescaler(uint16_t Prescaler);
#endif
3.4 IC.c
配置PA6输入捕获时基本有以下步骤:
开启时钟(TIM3,GPIOA) → GPIO初始化PA6 → 选择内部时钟 → 初始化时基单元 → 初始化输入捕获单元 → 选择从模式触发源:TI1FP1 → 选择从模式:Reset,每次触发后,CNT自动清零 → 启动定时器
最后再将频率的测量值封装成函数,便于主函数调用
#include "stm32f10x.h" // Device header
void IC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter = 0xF;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
TIM_Cmd(TIM3, ENABLE);
}
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}
*附:主从触发模式

3.5 IC.h
#ifndef __IC_H
#define __IN_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
#endif
3.6 main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "PWM.h"
#include "OLED.h"
#include "IC.h"
uint8_t i;
int main(void)
{
OLED_Init();
PWM_Init();
IC_Init();
OLED_ShowString(1, 1, "Freq:00000Hz");
PWM_SetPrescaler(720 - 1); //Freq = 72M / (PSC + 1) / 100
PWM_SetCompare1(50); //Duty = CCR / 100
while (1)
{
OLED_ShowNum(1, 6, IC_GetFreq(), 5);
}
}

四、PWMI模式测频率和占空比基本知识

PWMI(PWM Input)你可以把它理解成:
输入捕获的“加强版双通道方案”
它本质上仍然是输入捕获,只不过同时启用了两路通道去分析同一个输入信号。
4.1 它到底比普通输入捕获多了什么?
普通输入捕获,通常只盯一种边沿。
而 PWMI 会把一个通道配置成上升沿,另一个通道配置成下降沿。
于是它就能同时得到:
-
一个完整周期
-
高电平持续时间
这样就能直接计算:
频率 = 计数器时钟 / 周期计数值
占空比 = 高电平时间 / 周期时间
所以 PWMI 最大的优势就是:一次配置,频率和占空比一起拿下。
五、PWMI模式测频率和占空比代码实现
5.1 接线图

5.2 IC.c
这里我们需要将TI1FP2通道也进行初始化,并且和TI1FP1通道的参数是相反的,在这里,ST公司提供了一个简便的函数TIM_PWMIConfig,只需要填1和2(注意不要填3和4进来)其中一个通道的参数,另外一个通道就能自动被初始化为该通道相反的参数。
同时我们将测量占空比(CCR2 / CCR1)的函数模块也进行封装,便于主函数中直接调用。
#include "stm32f10x.h" // Device header
void IC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter = 0xF;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_PWMIConfig(TIM3,&TIM_ICInitStructure);//初始化另外一个通道为相反的配置
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
TIM_Cmd(TIM3, ENABLE);
}
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}
uint32_t IC_GetDuty(void)
{
return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1);
}
5.3 IC.h
#ifndef __IC_H
#define __IN_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
uint32_t IC_GetDuty(void);
#endif
5.4 main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "PWM.h"
#include "OLED.h"
#include "IC.h"
int main(void)
{
OLED_Init();
PWM_Init();
IC_Init();
OLED_ShowString(1, 1, "Freq:00000Hz");
OLED_ShowString(2, 1, "Duty:00%");
PWM_SetPrescaler(720 - 1); //Freq = 72M / (PSC + 1) / 100
PWM_SetCompare1(50); //Duty = CCR / 100
while (1)
{
OLED_ShowNum(1, 6, IC_GetFreq(), 5);
OLED_ShowNum(2, 6, IC_GetDuty(), 2);
}
}

六、输入捕获最容易踩的 5 个坑
6.1 只配了输入捕获,没配从模式 Reset
这会导致你读到的 CCR 只是某个绝对时间点,不是一个完整周期的计数值,频率当然就算不准。其中TI1FP1 + Reset是测频方案的关键步骤。
6.2 预分频器没算明白
你写的是:
return 1000000 / (TIM_GetCapture1(TIM3) + 1);
那前提必须是你的 CNT 计数频率真的就是 1MHz。
否则公式一定错。
6.3 输入滤波器乱配
滤波器不是越大越好。
滤波过重,可能会把高频边沿“滤没”;滤波太小,又容易把毛刺当成有效边沿。
6.4 标准频率选择不当
如果被测信号频率很低,而 ARR 又太小,就可能在一个周期还没结束时 CNT 已经溢出。这时可以适当增大 PSC 的值,降低标准频率的值。
如果被测信号频率很高,甚至高于标准频率,则此时得到的是一个无效的数值,可以适当减小 PSC 的值,增加标准频率的值。当然,如果频率非常高,甚至高于中界频率,这是优先考虑测频法。
6.5 PWMI 和普通输入捕获混着理解
普通输入捕获更适合“只测一个量”;
PWMI 更适合“频率和占空比一起测”。
别把两种初始化写法混在一个定时器里同时开,初学时最容易把自己绕晕。
七、这篇文章你真正该记住的 3 句话
第一句:
输入捕获不是测电平,而是记时间。
第二句:
普通输入捕获测频率,本质是测一个周期对应多少个计数。
第三句:
PWMI 本质是双通道输入捕获,所以它能同时算出频率和占空比。
你只要把这三句话记住,TIM 输入捕获这部分基本就算打通了。
八、总结与源码分享
很多人觉得输入捕获难,不是因为它复杂,而是因为一开始没有抓住本质。
一旦你明白 “边沿到来 → CNT 锁存到 CCR → 再根据计数值换算结果” 这个链条,普通输入捕获和 PWMI 就都会变得非常清晰。进而配置 GPIO、时基、输入捕获通道,再通过触发源和从模式决定“怎么记一次完整的时间”。
6-6 输入捕获模式测频率
https://gitee.com/Li-Cheng-Ze/stm32/tree/master/6-6%20%E8%BE%93%E5%85%A5%E6%8D%95%E8%8E%B7%E6%A8%A1%E5%BC%8F%E6%B5%8B%E9%A2%91%E7%8E%87PWMI模式测频率占空比
https://gitee.com/Li-Cheng-Ze/stm32/tree/master/6-7%20PWMI%E6%A8%A1%E5%BC%8F%E6%B5%8B%E9%A2%91%E7%8E%87%E5%8D%A0%E7%A9%BA%E6%AF%94如果你也是刚学到 STM32 定时器,建议你一定亲手做一遍这个实验:
-
先自己输出一路 PWM
-
再用输入捕获把它测回来
-
最后观察频率和占空比是否一致
当你把“发出去”和“测回来”都做通时,TIM 这部分你就真的入门了。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注。
作者:Auto_Dev_06
开发环境:Keil MDK-ARM + STM32F103C8T6
参考资料:江协科技STM32入门教程系列、STM32F10x参考手册
版权声明:本文为CSDN原创文章,转载请注明出处
发布时间:2026年4月16日
更多推荐



所有评论(0)