【江协STM32】6-5 TIM输入捕获、输入捕获模式测频率&PWMI模式测频率占空比
1. 输入捕获
1.1 输入捕获简介

1.2 输入捕获通道
1.3 主从触发模式
如果想让TI1FP1信号自动触发CNT清零,那么触发源选择就选中TI1FP1,从模式执行Reset操作,实现硬件的全自动测量。
1.4 输入捕获基本结构
经过预分频后的时钟频率,就是驱动CNT的标准频率fc = 72M / ( PSC + 1)。
测周法流程:
GPIO输入的信号经过滤波器和边沿检测,选择TI1FP1为上升沿触发,之后输入选择直连的通道,分频器选择不分频。当TI1FP1出现上升沿后,CNT的当前计数值转运到CCR1,同时触发源选择选中TI1FP1为触发信号,从模式选择Reset。(先转运值,再触发CNT清零)
GPIO输入方波信号:出现上升沿,CNT的值转运到CCR1(CCR1 = CNT),这是输入捕获自动执行的。然后CNT = 0,清零计数器,这是从模式自动执行的。
注意事项:

1.5 PWMI基本结构
PWMI模式使用两个通道同时捕获一个引脚,可以同时测量周期和占空比。
TI1FP1配置上升沿触发,触发捕获和清零CNT,用于捕获周期。TI1FP2配置下降沿触发。
GPIO输入方波信号:开始,上升沿,CCR1捕获,同时清零CNT,之后CNT++。在下降沿时刻,触发CCR2捕获,不触发CNT清零,CNT的值保存到CCR2,此时的CNT值就是高电平期间的CNT值,之后CNT++。直到下一次上升沿,CCR1捕获周期,CNT清零。CCR2/CCR1即为占空比。

2. 频率测量
STM32测频率只能测量数字信号。如果测量正弦波,还需要信号预处理电路,把正弦波转换为数字信号。
适合测量高频信号,当待测信号频率大于中界频率时,测频法误差更小,选用测频法更合适。
用之前学过的外设可以实现(对射式红外传感器计次、定时器外部时钟)
适合测量低频信号,当待测信号频率小于中界频率时,测周法误差更小,选用测周法更合适。
使用测周法测量频率,每次捕获后,CNT都需要清零,这个步骤可以用主从触发模式自动完成。
3. 输入捕获模式测频率
3.1 接线图
测量信号的输入引脚是PA6,待测的PWM信号是STM32自己生成的,输出引脚是PA0。如果接信号发生器,记得共地。
3.2 代码
输出待测PWM信号:
需要在原有PWM生成函数中增加一个用来便捷调节PWM频率的函数,调节ARR会同时影响频率和占空比,调节PSC只影响频率。因此计划固定ARR为100-1,通过调节PSC来改变PWM频率。
需要用到单独写入PSC的函数:TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode)
PWM.c
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
// 1、初始化时钟RCC TIM2
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);// TIM2是APB1总线的外设
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 4、配置GPIO
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);// 因为定时器上电后默认就是使用内部时钟,所以此行可省略
// 2、配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;// 指定时钟分频,设置的是输入滤波的采样频率,与时基单元关系不大。这里随便选一个
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;// 计数器模式,选择向上计数
// 计数器溢出频率:CK_CNT_OV=CK_CNT/(ARR+1)=CK_PSC/(PSC+1)/(ARR+1)=72M/(PSC+1)/(ARR+1)
// 注意:PSC和ARR的取值都要在0~65535之间
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;// 自动重装器(ARR)的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;// 预分频器(PSC)的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;// 重复计数器的值(高级定时器才有,不是CNT),不需要用,直接给0
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);// 刚初始化完会生成一次更新事件,会使Num刚复位就显示1
// 手动清除一次更新事件,避免Num从1开始
TIM_ClearFlag(TIM2, TIM_FLAG_Update);// 手动清除更新中断标志位,避免刚初始化完就进中断
// 3、配置输出比较单元
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);// 给结构体赋初始值。防止修改为高级定时器时,由于参数设置不完整出现异常。
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;// 输出比较极性。高极性,就是极性不翻转,REF波形直接输出
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;// 输出状态。使能
TIM_OCInitStructure.TIM_Pulse = 50;// 设置CCR寄存器的值
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
// 5、运行控制
TIM_Cmd(TIM2, ENABLE);// 启动定时器
}
// 通过运行中更改CCR值来调节占空比,使LED呈现呼吸灯效果
// 注意:设置CCR的值不是直接改变占空比,占空比是CCR和ARR+1共同决定的。因为此时ARR+1=100,所以CCR的值等于占空比
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);// 调节CCR
}
// 调节PSC。用来调整频率
void PWM_SetPrescaler(uint16_t Prescaler)
{
TIM_PrescalerConfig(TIM2, Prescaler, TIM_PSCReloadMode_Immediate);
}
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
输入捕获初始化步骤:
- RCC开启GPIO和TIM时钟
- GPIO初始化,配置成输入模式(一般选择上拉输入或浮空输入)
- 配置时基单元,使CNT计数器在内部时钟的驱动下自增运行
- 配置输入捕获单元,包括滤波器、极性、直连通道还是交叉通道、分频器
- 选择从模式TRGI触发源(TI1FP1)
- 选择触发后执行的操作(Reset)
- 调用TIM_Cmd函数,开启定时器
查阅引脚定义表,TIM3的通道1、通道2、通道3和通道4,分别对应PA6、PA7、PB0和PB1
初始化捕获单元有两个函数:TIM_ICInit(配置一个通道)和TIM_PWMIConfig(配置两个通道)
根据前面的分析,ARR设置最大值65536-1,防止计数溢出。PSC决定了测周法的标准频率fc
读取CCR值的函数: TIM_GetCapture1(TIM_TypeDef* TIMx)
PWM.c
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
// 1、初始化时钟RCC TIM2
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);// TIM2是APB1总线的外设
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 4、配置GPIO
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);// 因为定时器上电后默认就是使用内部时钟,所以此行可省略
// 2、配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;// 指定时钟分频,设置的是输入滤波的采样频率,与时基单元关系不大。这里随便选一个
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;// 计数器模式,选择向上计数
// 计数器溢出频率:CK_CNT_OV=CK_CNT/(ARR+1)=CK_PSC/(PSC+1)/(ARR+1)=72M/(PSC+1)/(ARR+1)
// 注意:PSC和ARR的取值都要在0~65535之间
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;// 自动重装器(ARR)的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;// 预分频器(PSC)的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;// 重复计数器的值(高级定时器才有,不是CNT),不需要用,直接给0
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);// 刚初始化完会生成一次更新事件,会使Num刚复位就显示1
// 手动清除一次更新事件,避免Num从1开始
TIM_ClearFlag(TIM2, TIM_FLAG_Update);// 手动清除更新中断标志位,避免刚初始化完就进中断
// 3、配置输出比较单元
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);// 给结构体赋初始值。防止修改为高级定时器时,由于参数设置不完整出现异常。
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;// 输出比较极性。高极性,就是极性不翻转,REF波形直接输出
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;// 输出状态。使能
TIM_OCInitStructure.TIM_Pulse = 50;// 设置CCR寄存器的值
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
// 5、运行控制
TIM_Cmd(TIM2, ENABLE);// 启动定时器
}
// 通过运行中更改CCR值来调节占空比,使LED呈现呼吸灯效果
// 注意:设置CCR的值不是直接改变占空比,占空比是CCR和ARR+1共同决定的。因为此时ARR+1=100,所以CCR的值等于占空比
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);// 调节CCR
}
// 调节PSC。用来调整频率
void PWM_SetPrescaler(uint16_t Prescaler)
{
TIM_PrescalerConfig(TIM2, Prescaler, TIM_PSCReloadMode_Immediate);
}
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
IC.c
#include "stm32f10x.h" // Device header
void IC_Init(void)
{
// 1、初始化时钟RCC TIM3
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);// TIM2是APB1总线的外设
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2、配置GPIO
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);// 因为定时器上电后默认就是使用内部时钟,所以此行可省略
// 3、配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;// 指定时钟分频,设置的是输入滤波的采样频率,与时基单元关系不大。这里随便选一个
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;// 计数器模式,选择向上计数
// 计数器溢出频率:CK_CNT_OV=CK_CNT/(ARR+1)=CK_PSC/(PSC+1)/(ARR+1)=72M/(PSC+1)/(ARR+1)
// 注意:PSC和ARR的取值都要在0~65535之间
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;// 自动重装器(ARR)的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;// 预分频器(PSC)的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;// 重复计数器的值(高级定时器才有,不是CNT),不需要用,直接给0
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);// 刚初始化完会生成一次更新事件,会使Num刚复位就显示1
// 4、初始化输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;// 配置哪个通道
TIM_ICInitStruct.TIM_ICFilter = 0xF;// 配置输入捕获的滤波器
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;// 边沿检测,选择极性。上升沿触发
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;// 触发信号分频器。不分频就是每次触发均有效,2分频就是每隔一次有效一次
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;// 选择触发信号从哪个引脚输入,可以选择直连通道或交叉通道
TIM_ICInit(TIM3, &TIM_ICInitStruct);
// 5、配置TRGI触发源为TI1FP1
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
// 6、配置从模式为Reset
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
// 7、调用TIM_Cmd函数,开启定时器
TIM_Cmd(TIM3, ENABLE);
}
// 计算频率
uint32_t IC_GetFreq(void)
{
return 1000000 / TIM_GetCapture1(TIM3);// fx=fc/N,fc=72M/(PSC+1)
}
IC.h
#ifndef __IC_H
#define __IC_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
#endif
main.c
#include "stm32f10x.h" // Device
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
uint8_t i;
int main(void)
{
OLED_Init();
OLED_ShowString(1,1,"Freq:00000Hz");
IC_Init();
PWM_Init();
// 输出待测信号至PA0
PWM_SetPrescaler(720 - 1);// Freq = 72M / (PSC+1) / 100
PWM_SetCompare1(50);// Duty = CCR / 100
while(1)
{
OLED_ShowNum(1, 6, IC_GetFreq(), 5);
}
}
其他引用的头文件和c代码可在此处查阅:OLED.h(【江协STM32】4 OLED调试工具,第5节)、 Delay.h(【江协STM32】3-2 LED闪烁&LED流水灯&蜂鸣器,第1.3节)
程序下载后测量频率为1001Hz,多了1Hz,可能是因为计数刚到1000Hz的那个数时,信号刚好跳变,由于电路结构或其他原因,导致这一个数刚好没计到,产生了误差。可以在程序补上误差,把IC.c中的IC_GetFreq函数改为:
// 计算频率
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1);// fx=fc/N,fc=72M/(PSC+1)
}
4. PWMI模式测频率占空比
4.1 接线图
4.2 代码
对上一个程序进行修改。
第4步配置输入捕获单元,需要配置成两个通道同时捕获同一个引脚的模式,可以使用TIM_PWMIConfig函数。使用该函数,比如配置通道1,直连,上升沿,函数会自动配置通道2,交叉,下降沿。此函数只用于通道1和通道2的配置,不能操作通道3和通道4。
IC.c
#include "stm32f10x.h" // Device header
void IC_Init(void)
{
// 1、初始化时钟RCC TIM3
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);// TIM2是APB1总线的外设
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2、配置GPIO
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);// 因为定时器上电后默认就是使用内部时钟,所以此行可省略
// 3、配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;// 指定时钟分频,设置的是输入滤波的采样频率,与时基单元关系不大。这里随便选一个
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;// 计数器模式,选择向上计数
// 计数器溢出频率:CK_CNT_OV=CK_CNT/(ARR+1)=CK_PSC/(PSC+1)/(ARR+1)=72M/(PSC+1)/(ARR+1)
// 注意:PSC和ARR的取值都要在0~65535之间
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;// 自动重装器(ARR)的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;// 预分频器(PSC)的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;// 重复计数器的值(高级定时器才有,不是CNT),不需要用,直接给0
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);// 刚初始化完会生成一次更新事件,会使Num刚复位就显示1
// 4、初始化输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;// 配置哪个通道
TIM_ICInitStruct.TIM_ICFilter = 0xF;// 配置输入捕获的滤波器
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;// 边沿检测,选择极性。上升沿触发
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;// 触发信号分频器。不分频就是每次触发均有效,2分频就是每隔一次有效一次
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;// 选择触发信号从哪个引脚输入,可以选择直连通道或交叉通道
//TIM_ICInit(TIM3, &TIM_ICInitStruct);// 只初始化通道1
TIM_PWMIConfig(TIM3, &TIM_ICInitStruct);// 对于同一个CH1输入,同时初始化捕获通道1和2
// 5、配置TRGI触发源为TI1FP1
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
// 6、配置从模式为Reset
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
// 7、调用TIM_Cmd函数,开启定时器
TIM_Cmd(TIM3, ENABLE);
}
// 计算频率
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1);// fx=fc/N,fc=72M/(PSC+1)
}
// 计算占空比
uint32_t IC_GetDuty(void)
{
return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1);// CCR2 / CCR1
// 显示范围为0~1,如果想显示整数,可以将其扩大100倍
// 经过实测,CCR总是少一个数,所以加1补上误差
}
IC.h
#ifndef __IC_H
#define __IC_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
uint32_t IC_GetDuty(void);
#endif
main.c
#include "stm32f10x.h" // Device
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
uint8_t i;
int main(void)
{
OLED_Init();
OLED_ShowString(1,1,"Freq:00000Hz");
OLED_ShowString(2,1,"Duty:00%");
IC_Init();
PWM_Init();
// 输出待测信号至PA0
PWM_SetPrescaler(7200 - 1);// Freq = 72M / (PSC+1) / 100
PWM_SetCompare1(87);// Duty = CCR / 100
while(1)
{
OLED_ShowNum(1, 6, IC_GetFreq(), 5);
OLED_ShowNum(2, 6, IC_GetDuty(), 2);
}
}
测频率的范围:目前的标准频率fc = 72M / (PSC + 1) = 1MHz,计数器最大计数到65535,所以测量的最低频率是1M / 65535 ≈ 15Hz。如果想要降低最低频率,可以增大PSC。
作者:冰糖雪莲IO