摘要

很多人学 STM32 定时器时,PWM 输出一看就会,但一到输入捕获就开始发懵:CCR 到底存了什么?为什么配置成 Reset 模式就能测频率?PWMI 又为什么能同时测出频率和占空比?本文结合 TIM 输入捕获的核心原理,把“普通输入捕获测频率”和“PWMI 测频率+占空比”一次讲透,并附上标准库完整代码,适合刚学完 STM32 定时器的同学直接上手。

目录

前言

一、TIM 输入捕获的本质,一句话吃透

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

2.1 测频率的两种常见方法

2.1.1 测频法

2.1.2 测周法

2.1.3 中界频率

2.2 使用测周法实现 PWM 波形频率的测量

2.2.1 基本思路

三、输入捕获模式测频率代码实现

3.1 接线图

3.2 PWM.c

3.3 PWM.h

3.4 IC.c

3.5 IC.h

3.6 main.c

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

4.1 它到底比普通输入捕获多了什么?

五、PWMI模式测频率和占空比代码实现

5.1 接线图

5.2 IC.c

5.3 IC.h

5.4 main.c

六、输入捕获最容易踩的 5 个坑

6.1 只配了输入捕获,没配从模式 Reset

6.2 预分频器没算明白

6.3 输入滤波器乱配

6.4 标准频率选择不当

6.5 PWMI 和普通输入捕获混着理解

七、这篇文章你真正该记住的 3 句话

八、总结与源码分享


前言

学定时器的时候,很多人会觉得 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。

这样每次上升沿到来时,硬件会自动做两件事:

  1. 先把当前 CNT 锁存到 CCR1

  2. 再把 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 定时器,建议你一定亲手做一遍这个实验:

  1. 先自己输出一路 PWM

  2. 再用输入捕获把它测回来

  3. 最后观察频率和占空比是否一致

当你把“发出去”和“测回来”都做通时,TIM 这部分你就真的入门了。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注。

作者:Auto_Dev_06
开发环境:Keil MDK-ARM + STM32F103C8T6
参考资料:江协科技STM32入门教程系列、STM32F10x参考手册
版权声明:本文为CSDN原创文章,转载请注明出处
发布时间:2026年4月16日

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐