零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)

回顾

上一章节学习了PWM生成,刚好买的元器件也都到了。测试下代码,完美运行。这不又趁着周末,进行下一个环节—定时器编码器模式。
目的是为下一步PID控制做准备。

遇到的问题

周末学习编码器模式也是一波三折呀,没人指点真的是寸步难行呀。气的直咬牙。
说说啥情况:
我用上节的PWM输出控制电机旋转,用编码器采集脉冲,想把这个脉冲显示在LCD上,就这个显示功能捣鼓了一天多,最后还是没成功,最后没办法了,只能串口打印出来。就在昨天晚上准备更新博客的时候想想不能就这么算了,又去调试了几下,奇迹出现了,LCD竟然可以显示了,弄完之后十点多了,就去睡觉了。目前问题还是不确定在哪,之前的代码修改的也找不到了,晚上回家再试试改成之前的代码,找找问题的根本原因。
改天单独开个帖子说说吧,今天先学习编码器。

编码器的作用

首先需要明白的一点就是为什么要用编码器模式,而不是直接用输入捕获。
最主要的原因就是编码器采集的是两个信号,根据两个信号的高低电平来判断是正转还是反转,比输入捕获用一个信号的抗干扰能力强。如果一个信号有干扰,而另一个信号没有干扰,则计数器不会计数,
就像下图这样,图片在中文参考手册中找的
零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)_第1张图片
图中红线标注的是毛刺,当TI1有干扰的情况下,计数器是不会继续向上累加的。如果用输入捕获的话,左边红色框中有两个脉冲,也就是说累加器会加两次。这就出现了误差,本来不该加,他却加了。
这就是首选编码器模式的主要原因。知道了他的作用还需要知道原理。

编码器采集原理

1.理解框图
零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)_第2张图片
上图是定时器的框图,首先我们会从电机屁股后面的编码器引出两根信号线接到单片机的编码器引脚,如上图第一个框内,接到某定时器的TIMx_CH1和TIMx_CH2两个通道的引脚上。进来之后进行滤波和边沿检测之后输出TI1FP1和TI1FP2,之后就直接到2号框了。2号框是将TI1FP1和TI1FP2接入编码器接口,在这里判断是该向上还是向下计数,之后来到3号框,进行分频,最后来到CNT计数器进行计数。我们可以定时采集CNT里面的值,这样就能算出电机的转速和其他一些参数了。
具体配置一会看代码吧。大致了解一下定时器框图。

2.理解计数方式
框图明白了,脑海里就明白了编码器工作的大致原理了。但还需要明白一些细节方面的东西。比如它是怎么通过两路信号进行计数的,到底是向上还是向下计数等等。

零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)_第3张图片
零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)_第4张图片
其实也很简单,结合上面两张图片很好理解。简单说一下,明白一个其他都明白了。

首先假定此时是电机正转,还有一点要知道,编码器两个信号相差90°的相位角。一个周期的信号可以分为0 π/2 π 3/2*π 2π,如上图,刚好差了π/2。上图是两个通道上下边沿都检测(SMS=’011’) 。
当TI1为上升沿时时,此时TI2为低电平,对应下面的图,先看【有效边沿】,因为是看的TI1上升沿,所以第一项选择【仅在TI1计数】,之后找对应的【TI1FP1 信号项】,选择上升。因为图中TI1是上升沿,对应TI2是低电平,因此在【相对信号的电平】那一项应该选择低,对应起来就是向上计数。如下图红色部分。
理解了TI1上升沿计数之后,其他的都一样了。
零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)_第5张图片
到这里,基本上编码器模式已经掌握的差不多了,下面就开始代码了。

STM32F103ZET6生成编码器代码

#include "PWM.h"
//通用定时器TIM3   PA6
void PWM_Init(void)
{
    //基本定时器初始化部分
    
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;
    //初始化定时器
    TIM_DeInit(TIM3);
    
    //GPIO初始化部分
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    
    //使能定时器时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
    
    //死区时间时钟源,该例程只进行PWM输出,没有用到死区,因此不设定
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    //向上计数模式
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    //自动重装载的值设定为1000
    TIM_TimeBaseInitStructure.TIM_Period = (1000 - 1);
    //定时器周期分频数
    TIM_TimeBaseInitStructure.TIM_Prescaler = (72 - 1);
    //重复计数器的值,只有高级定时器1和8有重复计数功能,其他都没有,因此设定为0
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    
    //初始化定时器3
    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
    
    
    
    
    
    //PWM初始化部分
    //pwm1模式
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    //初始化电平
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    //使能输出比较
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    //初始化PWM占空比为0
    TIM_OCInitStructure.TIM_Pulse = 1000;
    //初始化pwm
    TIM_OC1Init(TIM3,&TIM_OCInitStructure);
    
    //使能TIM3在CCR2上的预装载寄存器
    TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); 
    //使能定时器3
    TIM_Cmd(TIM3,ENABLE);    
}
//编码器模式  通用定时器2  PA0  PA1
void Advance_TIM_Init(void)
{
    //GPIO初始化
    
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    TIM_ICInitTypeDef  TIM_ICInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure; 
    
    
    // 设置中断组为0
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);		
		// 设置中断来源
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; 	
		// 设置抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;	 
	  // 设置子优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;	
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    
    
    //基本定时器初始化部分
    //使能定时器时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
    TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);//设置缺省值
    // 自动重装载寄存器的值,累计TIM_Period+1个频率后产生一个更新或者中断
	TIM_TimeBaseStructure.TIM_Period = (359*4);	
	// 驱动CNT计数器的时钟 = Fck_int/(psc+1)
	TIM_TimeBaseStructure.TIM_Prescaler = (72 - 1);	
	// 时钟分频因子 ,配置死区时间时需要用到
	TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;		
	// 计数器计数模式,设置为向上计数
	TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;		
	// 重复计数器的值,没用到不用管
	TIM_TimeBaseStructure.TIM_RepetitionCounter=0;	
	// 初始化定时器
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
    
    //编码器模式
    TIM_EncoderInterfaceConfig(TIM2,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
    TIM_ICStructInit(&TIM_ICInitStructure);
    TIM_ICInitStructure.TIM_ICFilter = 10;
    TIM_ICInit(TIM2, &TIM_ICInitStructure);
    /*
    //输入捕获初始化部分
    //选择捕获通道
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    //滤波设置
    TIM_ICInitStructure.TIM_ICFilter = 0x0;
    //设置捕获的边沿  上升沿
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising ;
    // 1分频,即捕获信号的每个有效边沿都捕获
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1 ;
    // 设置捕获通道的信号来自于哪个输入通道,有直连和非直连两种
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
    // 初始化PWM输入模式,不能用下面这个函数进行初始化输入捕获模式,会出错。
    //普通的输入捕获模式用TIM_ICInit函数,特殊的PWM捕获用TIM_PWMIConfig函数
    //TIM_ICInit(TIM2,&TIM_ICInitStructure);
    TIM_PWMIConfig(TIM2, &TIM_ICInitStructure);
    
    
    // 选择输入捕获的触发信号
    TIM_SelectInputTrigger(TIM2, TIM_TS_TI1FP1);
    
    // PWM输入模式时,从模式必须工作在复位模式,当捕获开始时,计数器CNT会被复位
    TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_Reset);
    TIM_SelectMasterSlaveMode(TIM2,TIM_MasterSlaveMode_Enable); 
    */
    //使能中断
    TIM_ITConfig(TIM2,TIM_IT_Update, ENABLE);	
    
    // 清除中断标志位
	TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    
    // 使能高级控制定时器,计数器开始计数
    TIM_Cmd(TIM2, ENABLE);
}

/*单位时间编码器计数 输入定时器 输出速度值*/
int Read_Encoder(u8 TIMX)
{
	int Encoder_TIM;    
	switch(TIMX)
	{
		case 2:  Encoder_TIM= (short)TIM2 -> CNT;  TIM2 -> CNT=0;break;
		case 3:  Encoder_TIM= (short)TIM3 -> CNT;  TIM3 -> CNT=0;break;	
		case 4:  Encoder_TIM= (short)TIM4 -> CNT;  TIM4 -> CNT=0;break;	
		default:  Encoder_TIM=0;
	}
	return Encoder_TIM;
}

1.PWM输出
较上一节讲得做了引脚变动,这次使用定时器3,PA6引脚作为PWM输出
具体配置和上一节差不多,不在啰嗦。
2.编码器采集
编码器采集用的是通用定时器2的PA0,PA1引脚,程序已经详细的注释了。讲几个重要的参数配置、其中用

/*   */ 

注释的是普通的输入捕获模式、
3.编码器配置中的主要参数讲解
①中断优先级
不用问,问就是最高优先级,再平衡小车系统中,电机的速度和位置是至关重要的,而采集编码器数据就是作为计算速度和位置的重要参数、
②TIM_TimeBaseStructure.TIM_Period = (359 * 4);
这行代码是配置自动重装载的值,在这里设置359 * 4其实意义不大,因为我们做的是实验,而且转速不是很快,编码器一圈才11个脉冲,因此采样值很小,满占空比才到达50左右,因此该数值只要大于60就行。网上很多都是一圈300脉冲的那种高精度编码器,因此该值需要大于在采样周期能采样的最大值,再留一些余量就行了。假设你编码器1s采样一次,一次采样500个脉冲,而你设定ARR=400,这肯定是不行的,因此该值应该根据自己的实际情况进行设定。
③TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;
在编码器模式中,该配置不起作用,正反项计数是根据两个信号的前后顺序来决定的,亲测该配置对计数无作用。
④TIM_EncoderInterfaceConfig(TIM2,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
重中之重
这个函数明白了就好理解,不要被表象给迷惑到就行。
1.这个函数是开启编码器模式的,第一个参数是选择定时器,在这里我们选择定时器2;
2.第二个参数是选择计数器模式的,进入到函数体中可以找到具体配置的是TIMx_SMCR寄存器中的SMS【2-0】位。具体说明如下:
零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)_第6张图片
因此选择的是编码器模式3,根据另一个信号的电平来决定怎么计数,上面在将编码器计数原理的时候也说过了,向上向下计数是要看另一路信号的电平的。
3.最后两个参数也是最不容易理解的,主要是好多都是根据字面意思讲解,没有到函数体中看到底是配置的哪一个寄存器。当你真正的进去看函数体的时候你就明白了。
这两个参数从下面这张图中还以为是配置上升沿、下降沿还是双边沿检测的,其实不是,
零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)_第7张图片
其实不是这样,从函数体中找到这两个参数,其实是配置CCER寄存器中的CC1E、CC1P、CC2E以及CC2P,CC1E和CC2E是使能的,CC1P、CC2P是配置极性的,具体看中文手册,我也给你截好图了
零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)_第8张图片
反向和不反向的意思就是从0到ARR计数还是从ARR到0 计数、而该位正是配置这一点的,在一个TIM_ICPolarity_BothEdge这个定义并不能在这个函数的形参中使用。该函数只能配置反向和不反向,下图红色框中需要注意下。
零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)_第9张图片
4.TIM_ICInitStructure.TIM_ICFilter = 10;
这个是设定滤波的,设定0的时候不是很稳。具体参考中文手册如下图:
零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)_第10张图片

写到这里,输入捕获的配置基本完后了。下面就是采样了,我采用系统滴答定时器进行1s采样一次,这个值是输入捕获1s中采样的编码器的值。并把它显示到屏幕上或者串口打印出来。

int Read_Encoder(u8 TIMX);
这个函数参考网上的代码写的,把这放到滴答定时器中断里。如下图

void SysTick_Handler(void)
{
    flag ++;
    if(flag >= 1000)
    {
        flag = 0;
        Frequency1 = Read_Encoder(2);
    }
}

之后主函数一直循环显示即可。这个是显示在屏幕上的。

int main(void)
 {	
	char aa[10];
     
    delay_init();	    	 //延时函数初始化	
    LCD_Fill(30,130,239,130+16,WHITE);
    LCD_Init();
    //uart_init(115200);	 //串口初始化为115200
    SysTick_Config(SystemCoreClock / 1000);//1ms

    Advance_TIM_Init();   
     
    PWM_Init();
    
    LCD_ShowString(30,50,16,"Show Frequency1",0);
    while(1)
	{	
        sprintf(aa,"%d",Frequency1);
        LCD_ShowString(30,70,16,aa,0); 	 
        //printf("count = %d\r\n",Frequency1);
        //delay_ms(10);//每隔1s打印一次编码器角度,用手去拨动编码器  使其慢速旋转
	}
 }

总结

基本都是老套路了、
①定义函数结构体
②使能GPIO时钟
③配置GPIO
④配置中断优先级
⑤使能定时器时钟
⑥配置基本定时器
⑦配置编码器模式
⑧初始化输入捕获
⑨使能定时器

最主要的还是理解其中的一些重要的参数,其实也不难(不过对于新手确实难,弄过一遍回过头才觉得简单),进入到函数体中仔细看看函数,在对着手册看寄存器内容说明也就明白啦。

感悟

这个帖子已经写了好几天了,7月3号就开始写,写到现在才弄完。中间过程很艰苦,不过一旦走出来,满满的成就感。

LCD相关的函数操作直接找的别人的代码,我还没有仔细研究,先拿来用吧。还是先把项目完成吧,拖得时间长了就没信心了。

一个人搞软件有时候真的是累,不是那种身体劳累,是心累。搞了几天还没把一个问题解决的那种心情,真的是烦躁,恨不得把电脑砸了。他不像干体力活,加把劲就干完了,这种加把劲都不到往哪加。就像这个lcd显示采集到的编码器的值,就这一个问题弄了两三天。各种百度,同样代码别人就行,到我这就不行了。到目前为止还没有真正找到答案、-_-||

源码

代码放到公众号了,后续我会将之前的代码也都放到公众号上,欢迎小伙伴们交流学习哈。关注公众号回复
“编码器模式”
获取源码。
零基础制作平衡小车【连载】4---STM32定时器编码器模式(附源码)_第11张图片

你可能感兴趣的