之前项目上用到一个使用定时器捕获输入采集风扇波形频率得到风扇转速的模块,作为笔记简单记录以下当时的逻辑结构和遇到的问题,有需要参考源码、有疑问或需要提供帮助的可以留言告知 。
提示: 测试基于GD32F103CBT6硬件平台,标准的108MHz系统时钟, 使用标准库GD32F10x_Firmware_Library_V1.0.0提示:(提示:此库坑多,外设编号从1开始,与用户手册略有出入、慎用!)
定时器输入捕获模式可以用来测量脉冲宽度或者测量频率,我们以测量频率为例,用一个简图来说明输入捕获的原理:
如图示,斜线表示向上计数的定时器的计数值,ARR表示定时器的自动重装载值,定时计数器由0递加到这里就会发生溢出,并重0重新开始计数。将输入捕获配置为上升沿捕获,当检测到一个波形的上升沿时候,触发第一次捕获中断,T1时刻会采集计数器当前CNT值并保存记为CCRx1,当再次出现上升沿时触发第二次捕获,T2时刻会再次采集计数器当前CNT值并保存记为CCRx2,理想状态波形周期就是T2 -T1。但是如果波形较长可能产生 N 次定时器溢出,这就要求我们对定时器溢出,做处理,防止高电平太长,导致数据不准确。 T1~T2之间CNT计数的次数等于: (ARR - CCRx1)+( N * ARR)+ CCRx2,有了这个计数次数,再乘以 CNT 的计数周期,即可得到 T1 -T2的时间长度,即波形周期以及频率。
GD32的定时器,我们使用通用定时器Timer1的,使用的GPIO为PB10和PB11,根据用户手册,需要进行功能重映射才能使其分别对应Timer1的通道2和通道3。在定时器初始化中使用函数GPIO_PinRemapConfig()即可;
使用的主时钟频率为108MHz,定时器的重装载值寄存器为16位,最大为65535,当定时器时钟分频系数为108分频时候,相当于定时器每65.535ms会溢出一次,如下是整个定时器PWM输入捕获的配置方式,目的是分别对通道3和通道4上的两个风扇进行脉冲周期数据采集和计算,详细内容参见代码注释:
void FanPwm_Input_Init(void)
{
TIMER_BaseInitPara sTIM_TimeBaseStructure;
NVIC_InitPara NVIC_InitStructure;
TIMER_ICInitPara sTIM_ICInitCaptureStructure;
/*初始化Timer2输入捕获*/
RCC_APB1PeriphClock_Enable(RCC_APB1PERIPH_TIMER2, ENABLE); //实际是复位Timer1(手册与固件库有误)
TIMER_DeInit(TIMER2); //复位
GPIO_PinRemapConfig(GPIO_FULL_REMAP_TIMER2,ENABLE); //PB10和PB11是全映射复用
sTIM_TimeBaseStructure.TIMER_Period = 65535; //计数器自动重装值
sTIM_TimeBaseStructure.TIMER_Prescaler = 107; //计数器时钟预分频值,计数器时钟等于 PSC 时钟除以 (PSC+1)
sTIM_TimeBaseStructure.TIMER_ClockDivision = TIMER_CDIV_DIV1; //设置时钟分割:fDTS = fTIMER_CK
sTIM_TimeBaseStructure.TIMER_CounterMode = TIMER_COUNTER_UP; //TIM向上计数模式
TIMER_BaseInit(TIMER2, &sTIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
sTIM_ICInitCaptureStructure.TIMER_ICSelection = TIMER_IC_SELECTION_DIRECTTI; //通道x配置为输入, ISx 映射在 CIxFEx 上
sTIM_ICInitCaptureStructure.TIMER_ICPrescaler = TIMER_IC_PSC_DIV1; //时钟分频
sTIM_ICInitCaptureStructure.TIMER_ICPolarity = TIMER_IC_POLARITY_RISING; //上升沿捕获
sTIM_ICInitCaptureStructure.TIMER_ICFilter = 0x00; //不滤波
sTIM_ICInitCaptureStructure.TIMER_CH = TIMER_CH_3; //PB10对应Timer2的通道3(手册是通道2,标准库有错位)
TIMER_ICInit(TIMER2,&sTIM_ICInitCaptureStructure);
sTIM_ICInitCaptureStructure.TIMER_CH = TIMER_CH_4; //PB11对应Timer2的通道4(手册是通道3,标准库有错位)
TIMER_ICInit(TIMER2,&sTIM_ICInitCaptureStructure);
NVIC_InitStructure.NVIC_IRQ = TIMER2_IRQn; //TIM2中断
NVIC_InitStructure.NVIC_IRQPreemptPriority = 0; //Q抢占优先级优先级0级
NVIC_InitStructure.NVIC_IRQSubPriority = 5; //副优先级2级
NVIC_InitStructure.NVIC_IRQEnable = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure);
TIMER_INTConfig(TIMER2,TIMER_INT_UPDATE,ENABLE); //使能定时器溢出中断,暂不使能通道捕获中断
TIMER_Enable(TIMER2, ENABLE); //使能定时器外设,暂不使能中断
}
配置完成后,会分别轮询打开两个通道的输入捕获中断,当脉冲的第一个上升沿触发对应通道的输入捕获中断时,会捕获第一个定时器计数值并将变量WaveEgde 置为1 ,说明此时已经采集到第一个上升沿。当WaveEgde = 1的情况下,当再次触发中断时候会判断是定时器溢出中断还是上升沿触发的输入捕获中断,如果是定时器溢出,说明定时器重新计数了,会将录有效溢出次数+1;如果发生上升沿捕获中断,则说明第二个上升沿到来,会捕获第一个定时器计数值且已采集到一个完整波形周期。数据采集结束,关闭该通道的输入捕获中断,并将捕获完成标记位置位,变量WaveEgde 置为0(无采集状态)。然后根据是否有有效溢出次数,选择计算方式计算两次捕获总的时间差。整个采集流程如图所示:
以上是一个通道的采集流程,使用多个通道采集时采集流程基本原理一致,但是如果同时打开多个通道的捕获中断,会导致某些通道的数据差异非常大,这个差异随着增加的通道数量越多而变得明显。因为如果通道数量太多,会在集中一段时间内频繁进中断导致数据采集不准确,所以我们使用轮询方式打开通道的捕获中断,轮询间隔时间设置为200ms,这段逻辑代码如下所示:
定义了捕获数据结构体,所有的数据都保存在此结构体中
typedef struct
{
uint8_t WaveEgde; //Eegd = 0,表示当前处于低电平,Egde = 1,表示当前处于高电平
uint8_t ucFinishFlag; //捕获结束标记位
uint16_t ucCaptureRisingVal[2]; //输入捕获值,[0]:第1次触发捕获值,[1]:第2次触发捕获值
uint32_t ucUpdateCnt; //记录溢出次数
uint32_t ulFanSpeed; //风扇转速
uint32_t ulFrequency; //输入波形频率
}Capture_DateType;
Capture_DateType WaveCap[FAN_Count_Num]; //定义两个风扇的捕获数据结构体
这里只给出了Timer2的中断回调函数中的逻辑结构
/*********************************************************************
*1-函数名:Fan_PwmI_IRQFunction
*2-函数功能:Timer2的中断回调函数
*3-输入参数:无
*4-返回值:无
*5-输入全局变量:无
*6-输出全局变量:无
*7-创建者与创建日期: Awen_ 2023-9-24
**********************************************************************/
void Fan_PwmI_IRQFunction(void)
{
if(TIMER_GetIntBitState(TIMER2,TIMER_INT_UPDATE) != RESET)
{
/*定时器溢出中断*/
TIMER_ClearIntBitState(TIMER2,TIMER_INT_UPDATE);
PwmInput_Timer_Update_Handler();
}
if(TIMER_GetIntBitState(TIMER2,TIMER_INT_CH3) != RESET)
{
/*风扇1通道输入捕获中断*/
TIMER_ClearIntBitState(TIMER2,TIMER_INT_CH3);
PwmInput_Capture(FAN_NO_0);
}
if(TIMER_GetIntBitState(TIMER2,TIMER_INT_CH4) != RESET)
{
/*风扇2通道输入捕获中断*/
TIMER_ClearIntBitState(TIMER2,TIMER_INT_CH4);
PwmInput_Capture(FAN_NO_1);
}
else
{
}
}
/*********************************************************************
*1-函数名:PwmInput_Timer_Update_Handler
*2-函数功能:判断是否是有效溢出
*3-输入参数:无
*4-返回值:无
*5-输入全局变量:无
*6-输出全局变量:无
*7-创建者与创建日期:Awen_ 2023-9-24
**********************************************************************/
static void PwmInput_Timer_Update_Handler(void)
{
uint8_t Index = 0;
for(Index = 0;Index < FAN_Count_Num;Index++) //FAN_Count_Num:总风扇个数
{
if(WaveCap[Index].WaveEgde == HIGH_LEVEL) //是否是高电平状态
{
WaveCap[Index].ucUpdateCnt++; //有效溢出
}
else
{
}
}
}
/*********************************************************************
*1-函数名:PwmInput_Capture
*2-函数功能:输入捕获中断中的处理
*3-输入参数:无
*4-返回值:无
*5-输入全局变量:无
*6-输出全局变量:无
*7-创建者与创建日期:Awen_ 2023-9-24
**********************************************************************/
static void PwmInput_Capture(uint8_t Channel)
{
/*是否是第一次捕获*/
if(WaveCap[Channel].WaveEgde == LOW_LEVEL)
{
/*第一次捕获*/
WaveCap[Channel].ucCaptureRisingVal[0] = FanPwm_Input_GetCapture(Channel); //第一次捕获CNT
WaveCap[Channel].WaveEgde = HIGH_LEVEL; //高电平状态
}
else if(WaveCap[Channel].WaveEgde == HIGH_LEVEL)
{
/*第二次捕获*/
WaveCap[Channel].ucCaptureRisingVal[1] = FanPwm_Input_GetCapture(Channel); //第二次捕获CNT
FanPwm_Input_EnableINT(Channel, DISABLE); //关闭中断
WaveCap[Channel].WaveEgde = LOW_LEVEL; //恢复到低电平状态
WaveCap[Channel].ucFinishFlag = TRUE; ///捕获完成标记位
}
else
{
WaveCap[Channel].WaveEgde = LOW_LEVEL;
FanPwm_Input_EnableINT(Channel, DISABLE);
}
}
最后频率的计算方式
static void FanPwmI_SpeedCalcul()
{
uint8_t Index = 0;
uint32_t Freq_vallue;
for(Index = 0;Index < FAN_Count_Num;Index++)
{
if(WaveCap[Index].ucFinishFlag == TRUE) //判断采集完成标记位
{
if(WaveCap[Index].ucUpdateCnt > 0) //是否有有效溢出
{
/*算式1*/
Freq_vallue =(0xFFFF - WaveCap[Index].ucCaptureRisingVal[0]) + ((WaveCap[Index].ucUpdateCnt - 1) * 0xFFFF) + WaveCap[Index].ucCaptureRisingVal[1];
}
else
{
/*算式2*/
Freq_vallue =WaveCap[Index].ucCaptureRisingVal[1] - WaveCap[Index].ucCaptureRisingVal[0];
}
WaveCap[Index].ulFrequency = 1000000 / (Freq_vallue + 1); //频率计算
WaveCap[Index].ulFanSpeed = WaveCap[Index].ulFrequency * 30; //根据风扇手册计算转速
}
else
{
}
WaveCap[Index].ucFinishFlag = FALSE; //采集完成标记位复位
WaveCap[Index].ucUpdateCnt = 0; //有效溢出计数复位
}
}
输入捕获还可采集波形占空比,其原理相同,只需要在第一次捕获之后改为下降沿触发,采集到第一个上升沿到第一个下降沿的CNT值,然后再设置为上升沿触发,采集一个完整波形周期CNT值,然后计算得到占空比。比较高效的做法是:硬件上使用Timer的两个通道的GPIO同时连到需要采集的波形管脚上,一个通道采集上升沿得到整个波形的周期,另一个通道采集下降沿得到波形高电平时长,再计算占空比。