官方STM32CUBEMX下载

解决的需求


1、PWM输入捕获: 获取未知PWM信号的脉宽和周期,也可以换算为占空比和频率
2、编码器模式 : 读取编码器的传感数据,结合编码器原理,利用其物理公式计算电机的转速。

具体的时基部分讲解请参考我之前的文章:
基本定时器编写延时函数定时器TIM的PWM输出
本文章对时基部分的解析进行略过。
在这里插入图片描述

本篇编码器模式以直流减速电机应用为例进行论述。

定时器的输入捕获

在这里插入图片描述
黄色框:
TIx(Trigger Input x)是我们的触发输入信号,来源是我们单片机引出的外部引脚 TIMx_CH1/2/3/4 ,通常叫 TI1/2/3/4,检测的信号类型一般是PWM信号。
红色框:
①TImFPn,是TIx经过输入滤波和边沿检测后形成的中间信号。
它的m与TIx中x的值等同,表示的是输入通道号;n则表示经过处理后的信号去向。
比如:TI1FP2则表明输入通道1的输入信号经过处理后去往捕获通道2
同理:TI1FP1则表明输入通道1的输入信号经过处理后去往捕获通道1

②TRC:
不经过输入滤波器和边沿检测器,通过选择器选择TIx边沿与内部触发后,发出的触发信号。
主要用于配置从模式和外部时钟源选择。本文不细讲。
③ICx: 捕获通道。
用户选择自己想要捕获的边沿(上升沿/下降沿),在这里检测,捕获到该边沿时CNT计数到的值,存到捕获寄存器CCRx中。
蓝色框:
预分频器:ICx 的输出信号会经过一个预分频器,用于决定检测到多少个目标边沿时进行一次捕获。如果希望捕获信号的每一个目标边沿,则不分频。
关键寄存器的读值
捕获寄存器CCRx:检测到想要的边沿时,捕获到的CNT的值会存到此处,同时产生一个中断或DMA请求,供用户读取。
具体的脉宽和周期计算,需要用户对自己的时基配置非常了解,同时对PWM输入的过程以及数据处理过程有很清晰的了解,这便是本文需要重点讲述的内容。

一、输入捕获实验

1.设计思想

高级定时器TIM我们获得的CK_INT是72M,经过72分频后CK_CNT是1M,CNT计数器计数一次为1us
在这里插入图片描述
<1>非溢出理想假设:刚开始配置上升沿捕获,第一次捕获上升沿的值记为value1,第二次捕获到的上升沿的值则是value3,那么
value3-value1就是未知方波一个周期对应我们CNT的计数差值,则周期=1us*(value3-value1)
显然:检测未知方波时,如果知道它大概的周期范围,捕获边沿的配置不变,相邻两次的捕获对应的就是未知方波的一个周期。
<2>非溢出理想假设:刚开始配置上升沿捕获,当发生一次上升沿捕获时,读取捕获值value1,再配置成下降沿捕获,等捕获到下降沿的时候,捕获到的值为value2,那么(value2-value1)对应的就是一个脉宽,脉宽=1us*(value2-value1)
<3>如果有溢出(假设CNT是向上计数):
前提:你要测量的PWM信号肯定是未知的,你永远不知道要测量的方波周期和脉宽大概是什么情况。
这里很可能有三种情况:
在这里插入图片描述

这里我们拿最复杂的第三种情况来举例,同时处理溢出后的value2,value3。

2.CubeMX配置

高级定时器TIM1使能输入捕获模式,CNT的计数周期为100us,计数一次1us
在这里插入图片描述
通用定时器TIM3产生PWM信号,让TIM1捕获
在这里插入图片描述

使能高级定时器TIM1的捕获中断和上溢/下溢中断:
在这里插入图片描述
在这里插入图片描述
更新溢出中断优先级必须高于输入捕获中断的优先级,并且能打断输入捕获中断服务函数,因为高电平或低电平期间的溢出次数必须时时刻刻都能更新,才能无误算出PWM信号的信息。

这就意味着两者的主优先级不能相同。

3.单通道实现PWM信号完整捕获的设计

这里特地选取有溢出现象的PWM信号捕获来讲,以下是核心代码逻辑:

变量区:

volatile char capture_flag=0;				//捕获状态标记变量,0x80最高位标记捕获完一个周期,0x40表示捕获到了上升沿
volatile uint8_t OverflowCount_high=0;		//高电平期间溢出次数
volatile uint8_t OverflowCount_low=0;		//低电平期间溢出次数
volatile uint32_t value1,value2,value3;	    //下图中三个边沿中的值
volatile uint32_t Pulse_Width=0;		    //脉宽
volatile uint32_t PWM_Period=0;				//周期

输入捕获逻辑区:
以上升沿为捕获的第一个边沿(当然也可以下降沿,随自己的喜好选择,本例的代码第一次捕获边沿为上升沿),那么等第二次(偶数次)捕获到上升沿时表示捕获完了一个PWM周期。
期间捕获完第一个边沿以后,马上改变捕获的极性,改为下降沿捕获,捕获到的值就是value2,然后再改成上升沿捕获,再捕获到value3,以如此反复。
在这里插入图片描述

/**
  * @brief (高级定时器TIM特有)输入捕获中断函数
  */
void TIM1_CC_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim1);
}
/**
  * @brief 输入捕获回调函数
  * @retval None
  */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
	static uint8_t RisingEdge_count=0;
	if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
	{
	  if(capture_flag&0x40)										//0X40是0100 0000,高电平期间捕获到下降沿																  							
	  {		
	    capture_flag&=0x3F;										//0X3F是0011 1111,清除捕获到上升沿的标记位和捕获完成的标记位																																
		value2=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);	//获取当前的捕获值
																	  										
		__HAL_TIM_DISABLE(htim);        						//关闭定时器
		__HAL_TIM_SET_COUNTER(htim,value2);						//以value2为基准重新计数
		TIM_RESET_CAPTUREPOLARITY(htim,TIM_CHANNEL_1);      	//复位极性选择才能进行下行配置
        TIM_SET_CAPTUREPOLARITY(htim,TIM_CHANNEL_1,TIM_ICPOLARITY_RISING);	//下次上升沿捕获
		__HAL_TIM_ENABLE(htim);									//重开定时器															
	   }
	  else	            								 		//捕获到上升沿       																																																				
	  {
		capture_flag|=0x40;										//0X40是0100 0000,标记捕捉到了一次上升沿	
																												
		RisingEdge_count++;
		if((RisingEdge_count%2==0))								//每捕获两次相同跳变沿表示过了一个PWM周期				  														
	    {
		  value3=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);//检测完一个周期的那个上升沿为value3
		  capture_flag|=0x80;								   //标记捕获完了一个周期
		}
		else				 								   //正在检测PWM信号的第一个上升沿,意味着下次捕获下降沿
		{
		  capture_flag&=0x7F;								   //0X7F是0111 1111,清除PWM捕获完成标志,开始新一轮PWM周期捕获
		  value1=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);//第一个上升沿是value1
		}


		__HAL_TIM_DISABLE(htim);
		__HAL_TIM_SET_COUNTER(htim,value1);					//以value1为基准重新计数
		TIM_RESET_CAPTUREPOLARITY(htim,TIM_CHANNEL_1);  	//复位极性选择才能进行下行配置   
        TIM_SET_CAPTUREPOLARITY(htim,TIM_CHANNEL_1,TIM_ICPOLARITY_FALLING);	//下次下降沿捕获
		__HAL_TIM_ENABLE(htim);								//重开定时器
	  }
   }
}

更新溢出逻辑区:
在这里插入图片描述

/**
  * @brief (高级定时器TIM特有)更新溢出中断函数
  */
void TIM1_UP_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim1);
}
/**
  * @brief 更新溢出回调函数
  * @retval None
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if((capture_flag&0X80)==0)			//PWM的一个周期没检测完
	{
	  if(capture_flag&0x40)				//在高电平期间溢出M次
	  {
	    OverflowCount_high++;
	  }
	  else								//在低电平期间溢出N次
	  {
		OverflowCount_low++;
	  }
	}
	else								   //PWM的一个周期检测完了
	{
	  OverflowCount_high=0;
	  OverflowCount_low=0;
	}
}

主函数打印信息:
在这里插入图片描述

int main(void)
{
  ... //系统生成的代码省略
	/* 使能PWM输出 */
  HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1);
	/* 清零中断标志位 */
  __HAL_TIM_CLEAR_IT(&htim1,TIM_IT_UPDATE);
  /* 使能定时器的更新事件中断 */
  __HAL_TIM_ENABLE_IT(&htim1,TIM_IT_UPDATE);
	/* 使能输入捕获 */
  HAL_TIM_IC_Start_IT(&htim1,TIM_CHANNEL_1);
  
  while (1)
  {
	if(capture_flag&0x80)
	{		
		/*输入捕获功能的重配与开启,硬件启动会产生几个时钟的延迟*/
		Pulse_Width=value2+OverflowCount_high*IC_CNT_Period-value1+5;	//经验值:再进行5个时钟的补偿
		PWM_Period=value3+OverflowCount_high*IC_CNT_Period+OverflowCount_low*IC_CNT_Period-value1+5;
		printf("/********************/\r\n");
		printf("脉宽为: %d us\r\n",Pulse_Width);
		printf("周期为:	%d us\r\n", PWM_Period);
		printf("/********************/\r\n");	
	}
  }
}

至于这里为什么不写(value2+1)与(value3+1)呢?是因为在与(value1+1)相减过程中,1已经被消去。
演示效果:
在这里插入图片描述

二、特例一:PWM输入

单路输入捕获同时捕捉PWM信号的脉宽和周期有一个致命的缺点,就是错误率高,当用作输入捕获功能的定时器CNT计数周期越小,而PWM信号的脉宽或周期越大,单路输入捕获几乎获取不了该PWM的正确信息。所以单路输入捕获一般只用来检测脉宽或周期,二者其一。
而PWM输入模式则可以完美解决这个问题,但是也多牺牲了一个捕获通道。

1.硬件执行过程

在这里插入图片描述

在这里插入图片描述
触发源只能是TI1FP1或TI2FP2,即触发输入通道的中间信号TIxFPx,而TIxFPx去往ICx直接捕获,TIxFP(x+1)去往IC(x+1)间接捕获。
下图假设TI1FP1是触发源:

在这里插入图片描述
触发源的直接捕获通道,要捕获什么边沿,可以由用户设置(上图举例上升沿)。当直接捕获通道捕获到目标边沿时,CNT会自动硬件清0,直接捕获通道捕获周期,间接捕获通道捕获脉宽。
同理,若触发源的直接捕获通道捕捉下降沿,那么间接捕获通道捕获的就是脉宽-,脉宽+就是周期减去脉宽-

2.CubeMX配置

这里就不特地做溢出的演示了,具体的实现逻辑可以参考单路捕获,我们的目的是快速实现PWM输入的功能。
同时缺点也显而易见,PWM输入模式占用了两个捕获通道,优点是缩减了代码的编写难度。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

3.主干部分逻辑代码

变量区

volatile float duty_p=0,duty_m=0;		//经过计算获得的PWM占空比
volatile uint32_t pwm_period=0;			//经过计算获得的PWM周期
volatile uint16_t IC1_VAL,IC2_VAL;		//记录寄存器CCR1与CCR2的值

中断处理逻辑:

/**
  * @brief This function handles TIM1 capture compare interrupt.
  */
void TIM1_CC_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim1);
}
/**
  * @brief 输入捕获回调函数
  */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1)
	{
		IC1_VAL=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
		IC2_VAL=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
		if(IC1_VAL!=0)
		{
			duty_p=(IC2_VAL+1)*100/(IC1_VAL+1);
			duty_m=(IC1_VAL-IC2_VAL)*100/(IC1_VAL+1);
			pwm_period=(IC1_VAL+1);
		}
	}
}

主函数打印:

int main(void)
{
    ...
	HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1);
	HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1);
	HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_2);
  while (1)
  {
	if(IC1_VAL!=0)					//捕获完了一个周期
	{
	  printf("占空比+:%f%\r\n占空比-:%f%\r\n周期:%d us\r\n",duty_p,duty_m,pwm_period);
	}
	HAL_Delay(500);
  }
}

演示效果:
几乎没有误差,还不用做时钟补偿,优点还是显而易见的。
在这里插入图片描述

有坑勿踩

在这里插入图片描述

在这里插入图片描述
编码器模式和PWM输入模式,只有CH1与CH2才有其功能。

三、特例二:正交编码器模式

1.闭环控制中的检测装置

在这里插入图片描述
编码器的作用: 主要用于检测电机的转速。

2.增量式旋转编码器

旋转编码器的工作原理:
旋转编码器的光电码盘在旋转过程中,会根据码盘自身的物理特性,会周期性产生脉冲,产生的脉冲会按照一定规则,促使定时器的CNT计数器向上计数或向下计数,利用物理公式,对数据处理就可以得到电机转速。
在这里插入图片描述
增量式旋转编码器:它内部机械结构的运动方式是旋转,将设备旋转时的位移信息变成连续的脉冲信号,脉冲个数表示位移量的大小。只有当设备旋转的时候增量式编码器才会输出信号。这类编码器一般会把这些信号分为通道 A和通道 B 两组输出,并且这两组信号间有 90° 的相位差(下面细讲)。同时采集这两组信号就可以知道设备的转速和转动方向。

电赛常用的增量式旋转编码器类型:
这两种编码器一般5V供电。
在这里插入图片描述

在这里插入图片描述

3.编码器的基本参数与信号形式

<1>线数(分辨率)
表示编码器转过一圈,会产生多少脉冲,即脉冲数/转 (Pulse Per Revolution 或 PPR)。
在这里插入图片描述
编码器线数就是它的分辨率,在公式中常用C表示,单位是ppr。
这个参数至关重要,一般由厂家提供。
<2>精度:
指编码器每个读数与转轴实际位置间的最大误差,通常用角度、角分或角秒来表示。
<3>最大响应频率
指编码器每秒输出的脉冲数,单位是 Hz。
假设电机的轴转速是x圈每分钟,编码器的线数是C个脉冲每圈,那他一分钟就产生了Cx个脉冲,一秒就是Cx/60个脉冲。
<4>电源输入与信号输出形式
本例中的增量式旋转编码器,有四个接口:
在这里插入图片描述
两个输入口,主要用于自身供电;两个输出口,主要用于A相和B相脉冲的输出,供单片机定时器检测,A相和B相的信号是有90°差的信号。
对于增量式编码器,每个通道的信号独立输出,输出电路形式通常有集电极开路输出、推挽输出、差分输出等。

4.编码器的物理公式解析

<1>M法测速(高速测量):
假如经过了时间T0,编码器输出了M0个脉冲,那么(M0/C)就是它转过的圈数,(圈数/T0)就是转速。
STM32定时器编码器模式检测时,一般是2倍频或者4倍频,此时读取到的脉冲数就是2M0或者4M0
在这里插入图片描述
如果测量的电机转速较小,那么由于编码器自身的机械结构,这个误差值的总体占比就会越来越大,因此M法不适合测量低速。
<2>T法测速(低速测量)
T法的测量思想和M法是一样的。先从M法公式的形式去看T法公式,M0=1,显然T法捕获的是编码器的一个脉冲,要求得的未知量,是这一个脉冲期间的时间间隔TE。
这么短的时间间隔TE,不能直接由M法求得,那样误差太大了,T法的思想是:引入一个频率为F0的外来高频信号,当捕获到完整一个脉冲时,停止期间对高频信号的计数,记计数值为M1。
频率为F0,说明1s可以产生F0次脉冲,那么M1/F0,就是说数了M1次时,经过了多少秒,仔细理解一下,这个就是TE,所以(1/TE)=(F0/M1)
在这里插入图片描述

<3>M/T法测速:
M/T法的思想就是两者结合,这里我们要求的,不是一个脉冲的时间间隔TE,而是M0个脉冲的时间间隔T0
然后引入一个频率为F0的外来高频信号,当捕获完M0个脉冲时,记下期间对高频信号产生的计数值M1,来求得T0。
由<2>推导(1/T0)=(F0/M1),那么n=[M0/(CT0)]=下式:
在这里插入图片描述

5.CubeMX的配置

编码器模式与PWM输入一样,属于STM32定时器输入捕获的特例:
在这里插入图片描述
在这里插入图片描述

CubeMX的配置:
在这里插入图片描述
注意在编码器模式中,配置的Polarity极性并不是指它的捕获边沿,而是不反相(Rising Edge)或反相(Falling Edge)
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

6.细说编码器模式的CNT计数方式

<1>非反相与反相
在这里插入图片描述
在这里插入图片描述
所以对于TI1FP1/TI2FP2,如果TI1/TI2发生了上升沿,没有设置反相,TI1FP1/TI2FP2捕获到的就是上升沿;
如果TI1/TI2发生了上升沿,设置了反相,TI1FP1/TI2FP2捕获到的就是下降沿;

这里再重复强调一遍:编码器模式的Rising Edge和Falling Edge并不是配置边沿的意思,而是不反相和反相,存在CubeMX一个标识错误的地方。
在这里插入图片描述
<2>计数方向与编码器信号
TI1FP1/TI2FP2捕获的是边沿,如果设置了反相,TI1/TI2发生上升沿则会捕获到下降沿,以此类推。
一般我们不反相,TI1/TI2发生什么边沿我们就捕获到什么边沿。
当TI1FP1/TI2FP2捕获到边沿时,STM32的定时器就会对比相反信号TI2/TI1的电平状态,决定CNT的计数方式,具体如下:
在这里插入图片描述
关于两对相反信号,特地画出来给大家看一下:

在这里插入图片描述
在这里插入图片描述
<3>二倍频编码器模式——一个脉冲,计数两次
假设不反相,TI1发生什么边沿,TI1FP1就捕获到什么边沿,这里把TI1FP1看作TI1
TI1超前TI2九十度:
在这里插入图片描述超前向上计数
TI1滞后TI2九十度:
在这里插入图片描述滞后向下计数
显然记录一次脉冲,就会计数2次,这就是2倍频的原理,在选择2倍频模式时,得到的计数差值要除以2,才是正确的脉冲数。
<4>TI1和TI2都采集——四倍频
在这里插入图片描述
TI1和TI2两个脉冲,计数4次,并且这两个脉冲存在相位差,这就是4倍频的原理,在选择4倍频模式时,得到的计数差值要除以4,才是正确的脉冲数。这里是TI1超前TI2 90°,所以是向上计数。

7.代码逻辑实现

<1>核心代码逻辑
宏定义

#define ENCODER_MODE								TIM_ENCODERMODE_TI12//TI12都计数,是四倍频模式

#define ENCODER_RESOLUTION							500					//光电编码器线数500
#if	(ENCODER_MODE==TIM_ENCODERMODE_TI12)
	#define	ENCODER_READ_RESOLUTION					(ENCODER_RESOLUTION*4)//本例采用四倍频模式
#else
	#define	ENCODER_READ_RESOLUTION					(ENCODER_RESOLUTION*2)//二倍频模式
#endif
/* 本例减速电机减速比 */
#define REDUCTION_RATIO              				30

变量区

volatile int16_t Encoder_Overflow_Count=0;//(必须是有符号型,用正负区分正反转)CNT计数器有效上溢次数
volatile char Motor_Direction=0;		  //电机转向状态
volatile int32_t Capture_Count=0;		 //(必须是有符号型)捕获值
volatile int32_t Last_Count=0;			 //(必须是有符号型)
volatile float Shaft_Speed;				 //轴转速

主函数初始化编码器:

int main(void)
{
  ...
  HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1);
	/* 清零中断标志位 */
  __HAL_TIM_CLEAR_IT(&htim4,TIM_IT_UPDATE);
  /* 使能定时器的更新事件中断 */
  __HAL_TIM_ENABLE_IT(&htim4,TIM_IT_UPDATE);
	/* 使能编码器接口 */
  HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);
  while (1)
  {
  }
}

编码器模式计数溢出处理:

void TIM4_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim4);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
 	/* 发生了一次更新操作,判断是下溢还是上溢 */
	/*判断TIM4是否在向下计数,为真则是在向下计数,为假是在向上计数*/
	if(__HAL_TIM_IS_TIM_COUNTING_DOWN(htim))
	{
		Encoder_Overflow_Count--;//反转过程中溢出
	}
	else	
	{
		Encoder_Overflow_Count++;//正转过程中溢出
	}
}

滴答定时器中断打印信息:

void SysTick_Handler(void)
{
  	HAL_IncTick();				//增加心跳
	HAL_SYSTICK_IRQHandler();	//中断处理函数
}
/***************************** M法 ***************************/
void HAL_SYSTICK_Callback(void)
{
	static uint16_t i=0;
	++i;
	
	if(i==100)					//100ms输出一次信息
	{
		Motor_Direction= __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim4);
		
		Capture_Count=__HAL_TIM_GetCounter(&htim4)+(Encoder_Overflow_Count*65535);
		
		Shaft_Speed=((float)(Capture_Count-Last_Count)/ENCODER_READ_RESOLUTION*10);
		
		printf("/***********/");
		printf("电机转动方向: %s\r\n",Motor_Direction?"反转":"正转");
		printf("电机转轴处转速: %.2f 转/秒\r\n",Shaft_Speed);
		printf("电机输出轴处转速: %.2f 转/秒\r\n",Shaft_Speed/30);
		Last_Count=Capture_Count;
		i=0;
	}
}

<2>数学关系
上述的例程,用的是M法。
①通过调用这个API,可以知道CNT在向下计数(函数返回1)还是向上计数(函数返回0)

Motor_Direction= __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim4);

②滴答定时器每0.1s中断打印一次数据,所以T=0.1s,1/T=10为代码中的系数
同时因为采用4倍频采集,分母要乘上4
线数是厂家提供的,线数C=ENCODER_RESOLUTION=500
当前CNT计数值与上次采集的CNT计数值的值差就是M0

#define	ENCODER_READ_RESOLUTION					(ENCODER_RESOLUTION*4)
Capture_Count=__HAL_TIM_GetCounter(&htim4)+(Encoder_Overflow_Count*65535);
		
Shaft_Speed=(float)((Capture_Count-Last_Count)/ENCODER_READ_RESOLUTION*10);

<3>
电机减速比为30,这个数值也是厂家提供的,具体看自己买的减速电机型号。
电机输出轴转速=电机转轴处转速/减速比

	printf("电机输出轴处转速: %.2f 转/秒\r\n",Shaft_Speed/30);//Shaft_Speed是输出轴转速
	Last_Count=Capture_Count;								//获取上一次的值

<4>演示效果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.注意事项

在这里插入图片描述
只有CH1与CH2才有编码器模式。

附加内容:市面上主流的直流减速电机参数举例

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐