官方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输入的过程以及数据处理过程有很清晰的了解,这便是本文需要重点讲述的内容。
高级定时器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。
高级定时器TIM1使能输入捕获模式,CNT的计数周期为100us,计数一次1us:
通用定时器TIM3产生PWM信号,让TIM1捕获
使能高级定时器TIM1的捕获中断和上溢/下溢中断:
更新溢出中断优先级必须高于输入捕获中断的优先级,并且能打断输入捕获中断服务函数,因为高电平或低电平期间的溢出次数必须时时刻刻都能更新,才能无误算出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信号的脉宽和周期有一个致命的缺点,就是错误率高,当用作输入捕获功能的定时器CNT计数周期越小,而PWM信号的脉宽或周期越大,单路输入捕获几乎获取不了该PWM的正确信息。所以单路输入捕获一般只用来检测脉宽或周期,二者其一。
而PWM输入模式则可以完美解决这个问题,但是也多牺牲了一个捕获通道。
触发源只能是TI1FP1或TI2FP2,即触发输入通道的中间信号TIxFPx,而TIxFPx去往ICx直接捕获,TIxFP(x+1)去往IC(x+1)间接捕获。
下图假设TI1FP1是触发源:
触发源的直接捕获通道,要捕获什么边沿,可以由用户设置(上图举例上升沿)。当直接捕获通道捕获到目标边沿时,CNT会自动硬件清0,直接捕获通道捕获周期,间接捕获通道捕获脉宽。
同理,若触发源的直接捕获通道捕捉下降沿,那么间接捕获通道捕获的就是脉宽-,脉宽+就是周期减去脉宽-
这里就不特地做溢出的演示了,具体的实现逻辑可以参考单路捕获,我们的目的是快速实现PWM输入的功能。
同时缺点也显而易见,PWM输入模式占用了两个捕获通道,优点是缩减了代码的编写难度。
变量区
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);
}
}
演示效果:
几乎没有误差,还不用做时钟补偿,优点还是显而易见的。
旋转编码器的工作原理:
旋转编码器的光电码盘在旋转过程中,会根据码盘自身的物理特性,会周期性产生脉冲,产生的脉冲会按照一定规则,促使定时器的CNT计数器向上计数或向下计数,利用物理公式,对数据处理就可以得到电机转速。
增量式旋转编码器:它内部机械结构的运动方式是旋转,将设备旋转时的位移信息变成连续的脉冲信号,脉冲个数表示位移量的大小。只有当设备旋转的时候增量式编码器才会输出信号。这类编码器一般会把这些信号分为通道 A和通道 B 两组输出,并且这两组信号间有 90° 的相位差(下面细讲)。同时采集这两组信号就可以知道设备的转速和转动方向。
电赛常用的增量式旋转编码器类型:
这两种编码器一般5V供电。
<1>线数(分辨率)
表示编码器转过一圈,会产生多少脉冲,即脉冲数/转 (Pulse Per Revolution 或 PPR)。
编码器线数就是它的分辨率,在公式中常用C表示,单位是ppr。
这个参数至关重要,一般由厂家提供。
<2>精度:
指编码器每个读数与转轴实际位置间的最大误差,通常用角度、角分或角秒来表示。
<3>最大响应频率
指编码器每秒输出的脉冲数,单位是 Hz。
假设电机的轴转速是x圈每分钟,编码器的线数是C个脉冲每圈,那他一分钟就产生了Cx个脉冲,一秒就是Cx/60个脉冲。
<4>电源输入与信号输出形式
本例中的增量式旋转编码器,有四个接口:
两个输入口,主要用于自身供电;两个输出口,主要用于A相和B相脉冲的输出,供单片机定时器检测,A相和B相的信号是有90°差的信号。
对于增量式编码器,每个通道的信号独立输出,输出电路形式通常有集电极开路输出、推挽输出、差分输出等。
<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)]=下式:
编码器模式与PWM输入一样,属于STM32定时器输入捕获的特例:
CubeMX的配置:
注意在编码器模式中,配置的Polarity极性并不是指它的捕获边沿,而是不反相(Rising Edge)或反相(Falling Edge)
<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°,所以是向上计数。
<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; //获取上一次的值