目录
编码器接口的初始化步骤
Tim.h库函数
TIM_EncoderInterfaceConfig
代码实现
Encoder.c
初始化
三种输入模式的选择原则
获取编码器的增量值的函数
调整极性
闸门时间调整
Encoder.h
main.c
声明:本专栏是本人跟着B站江科大的视频的学习过程中记录下来的笔记,我之所以记录下来是为了方便自己日后复习。如果你也是跟着江科大的视频学习的,可以配套本专栏食用,如有问题可以QQ交流群:963138186
接下来我们就来写一下编码及接口测速的代码
编码器接口测速
接线图:
A相可以接PA6或者PA7,这个可以随便交换,但是必须用PA6和PA7这两个引脚,因为我们计划用定时器3来接编码器,PA6和PA7对应的是定时器3的通道一和通道二。
复制定时器定时中断节的工程并改名
现在来封装一下编码器接口的代码
编码器接口的初始化步骤
编码器接口的初始化步骤看这个结构图来配置
第一步,RCC开启时钟,开启GPIO和定时器的时钟。
第二步,配置GPIO,这里需要把PA6和PA7配置成输入模式。
第三步,配置时基单元,这里预分频器,一般选择不分频。自动重装一般给最大65535,只需要个CNT执行计数就行了。
第四步,配置输入捕获单元,不过这里输入捕获单元只有滤波器和极性这两个参数有用,后面的参数没有用到,与编码器无关。
第五步,配置编码器接口模式,这个直接调用个库函数就可以了。
第六步,启动定时器,调用TIM_Cmd启动定时器。
电路初始化完成之后,CNT就会随着编码器旋转而自增自减。如果想要测量编码器的位置,直接读出CNT的值就行了。如果想测量编码器的速度和方向,就需要每隔一段固定的闸门时间取出一次CNT,然后再把CNT清零,这样就是测频法测量速度了。
Tim.h库函数
在这里找一下库函数,本节我们需要新学习的扩函数比较少,只有这一个。
TIM_EncoderInterfaceConfig
TIM_EncoderInterfaceConfig配置编码器接口的函数。第一个参数选择定时器,第二个参数选择编码器模式,后面两个参数分别选择通道一和通道二的电平极性。
代码实现
Encoder.c
初始化
首先前面几步和之前的代码基本一样,我们直接复制输入捕获的代码,然后修改一下。
GPIO初始化,我们使用的是PA6和PA7,把PA6和PA7配置成上拉输入模式。
上拉和下拉如何选择?
三种输入模式的选择原则
我们一般可以看一下接在这个引脚的外部模块输出的默认电平。如果外部模块空闲默认输出高电平,我们就选择上拉输入,默认输入高电平。如果外部模块默认输出低电平,我们配置下拉输入,默认输入低电平,和外部模块保持默认状态一致,防止默认电平打架,这是上拉和下拉的选择原则。
不过一般来说,默认高电平是一个习惯的状态,所以一般上拉输入用的比较多。
然后如果不确定外部模块输出的默认状态或者外部信号输出功率非常小,这时就尽量选择浮空输入,浮空输入没有上拉电阻和下拉电阻去影响外部信号。但是缺点就是当引脚悬空时没有默认的电平了,输入就会受噪声干扰,来回不断的跳变。
这就是三种输入模式的选择原则。
这一行就不需要了
因为编码器接口会托管时钟,编码器接口就是一个带方相控制的外部时钟,所以这个内部时钟就没有用了。
接下来是时基单元配置。
计数器模式这个参数目前也是没有作用的
因为计数方相也是被编码器接口托管的。
自动重装值还是给65536-1,也就是满量程计数,这样计数的范围是最大的,而且方便换算为负数。
预分频器这里改成1-1,预分频给零,就是不分频,编码器的时钟直接驱动计数器
这就是时基部分的配置。
下一步就是输入捕获单元的配置
后面这两个参数与编码器无关,我们可以直接删掉。
删掉之后,目前结构体的配置是不完整的。为了防止结构体中出现不确定值可能会造成的问题,我们最好用StructInit给结构体赋一个初始值,也是提醒一下我们结构体并没有配置完整。在定义结构体定义好之后,我们来一个结构体初始化,把这个结构体的地址传进去,赋一个初始值。
接着看,电平极性为上升沿,上节我们说过,这里的上升沿并不代表上升沿有效。因为编码器接口时钟都是上升沿,下降沿都有效的。这里的上升沿参数代表的是高低电平极性不反转。对应通道,给上升沿就是不反相,给下降沿就是反相这个意思。
其实这里的这个极性参数,等会儿我们配置编码器接口的时候也有,属于重复配置的。这里这个其实也可以删掉。
我们可以等会儿对比一下之后再删。
通道一的我们配置好了,然后还有通道二,我们复制修改一下,把通道1改成通道二。
到这里两个通道的滤波器和极性就都配置好了。
下一步配置编码器。
我们只需要调用一个函数就行了,到tim.h里复制这个函数。
第一个参数是TIM3。
第二个编码性模式,可以选择下面三个参数之一。
第一个TI1就是仅在TI1计数,第二个TI2就是仅在TI2计数。第三个TI12就是TI1和TI2都计数。对应这三个模式。
我们一般使用TI1和TI2都计数。
第三和第四个参数就是IC1的极性和IC2的极性,取值列表一样
rising就是这个通道不反相选择,falling就是这个通道反相。这个可以根据实际情况来配置,我们目前两个参数都可以先选择rising。
这样参数就配置好了。
注意:这里后两个参数和上面输入捕获单元的两个参数是一样的。
实际的效果确实就是一样的,这两个地方的参数其实都配置的是同一个寄存器,属于重复配置的,后配置的参数会覆盖前面的参数。所以可以把这前面输入捕获单元的极性参数也删掉,只使用后面这个函数来配置极性。
不过要注意,这时一定要保证这个编码器接口配置的函数位于输入捕获初始化函数的下面,否则的话就是TIM_ICInit覆盖TIM_EncoderInterfaceConfig函数的配置。
到这里,我们整个电路就配置完成了,
最后我们再调用一个TIM_Cmd开启定时器。
这样初始化配置就结束了。
调用一下TIM_EncoderInterfaceConfig函数,编码器旋转就能控制CNT自增自减了。
获取编码器的增量值的函数
我们接下来再写个获取编码器的增量值的函数
在里面,我们暂时先直接返回CNT的值测试一下看看
测试结果就是右转一下编码器可以看到值变成4。这个编码器是有这个段落感的(如果是电机的编码器就不会有段落感),每转一格它输出的波形其实是。a相产生一个低电平脉冲,b相产生一个相位差九十度的脉冲。
提前还是滞后取决于正转还是反转。
编码器转动一格,ab相各出现了一个下降沿和上升沿。所以计次总共加了4次。
然后我们继续往右转CNT就继续自增,往左转CNT就自减。
向左一直转到零之后是什么情况?
我们转到零,再继续转可以看到零再自减,计数器溢出,回到自动重装值65535,然后继续往下减。
这就是目前我们使用uint16_t数据的现象。
如果我们想要0之后变为-1,就直接把uint16_t类型强制转换成int16_t就行了。
这里函数的返回值,直接换成int16_t。
然后函数声明也别忘了改一下,在主循环里改成
这样就能显示负数了。
现在是0
向左转数值变成负数
这就是借用补码的特性快速完成负数转换的小技巧。
我们再来研究一下极性的问题。
调整极性
如果方向和你想要的不一致的话,可以修改一下极性。
在硬件上面,我们可以把ab相两根线换一下。
在软件层面,我们可以修改这里的两个输入通道的极性:
把任意一个极性反转一下,方向就会反过来。如果两个极性都反转,极性还是保持不变。
这就是极性的问题。
目前我们这个代码是编码器测量位置,如果需要测量位置的话,就这样直接get counter就行了
如果我们想用这个编码器来测速的话,就可以在固定的闸门时间读一次CNT,然后把CNT清零。
我们修改一下这个Encoder_Get函数,要求读完后清零CNT,所以我们要定义一个临时变量temp。
要先读取后清零,所以需要用temp缓存一下。
在主循环里每隔一段时间get一次。所以下面可以给一个delay一千毫秒。因为我们人手转比较慢,所以闸门时间就给一秒。
如果是电机飞速旋转的话,闸门时间就可以给短的,这样可以提高速度刷新的频率,而且防止计数器溢出。
这样每一秒都会清零,CNT最多是一秒内的值,就是速度。也即CNT的值就代表速度,单位是脉冲个数每秒。
结果就是我们向右慢速转,速度是正数比较小。快速转,正数比较大。向左慢速转,速度负数比较小。快速转负数比较大。
当然我们目前只是用这个旋钮模拟的测速。如果你有编码电机的话,可以实际接电机的编码器试试看现象都是一样的。不过要注意把这个闸门时间弄短点,防止计数器溢出。
闸门时间调整
目前我们是直接通过delay实现的闸门时间,如果主程序没有其他东西的话,可以这样来做。但是如果有其他东西的话,最好就不要在主循环加入过长的delay,这样会阻塞主循环的执行。
比较好的方法就是用定时中断了。
目前是每隔一秒执行一次,可以通过修改定时中断的时间,来调整闸门时间。
之后再定义一个全局变量
然后在定时中断里执行每隔一秒读取一下速度,存在speed的变量里,然后主循环就可以快速刷新显示speed。这样delay可以删掉,这样就不会阻塞主循环了。
Encoder.c
#include "stm32f10x.h" // Device header
/**
* 函 数:编码器初始化
* 参 数:无
* 返 回 值:无
*/
void Encoder_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //开启TIM3的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA6和PA7引脚初始化为上拉输入
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM3的时基单元
/*输入捕获初始化*/
TIM_ICInitTypeDef TIM_ICInitStructure; //定义结构体变量
TIM_ICStructInit(&TIM_ICInitStructure); //结构体初始化,若结构体没有完整赋值
//则最好执行此函数,给结构体所有成员都赋一个默认值
//避免结构体初值不确定的问题
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择配置定时器通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; //输入滤波器参数,可以过滤信号抖动
TIM_ICInit(TIM3, &TIM_ICInitStructure); //将结构体变量交给TIM_ICInit,配置TIM3的输入捕获通道
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; //选择配置定时器通道2
TIM_ICInitStructure.TIM_ICFilter = 0xF; //输入滤波器参数,可以过滤信号抖动
TIM_ICInit(TIM3, &TIM_ICInitStructure); //将结构体变量交给TIM_ICInit,配置TIM3的输入捕获通道
/*编码器接口配置*/
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
//配置编码器模式以及两个输入通道是否反相
//注意此时参数的Rising和Falling已经不代表上升沿和下降沿了,而是代表是否反相
//此函数必须在输入捕获初始化之后进行,否则输入捕获的配置会覆盖此函数的部分配置
/*TIM使能*/
TIM_Cmd(TIM3, ENABLE); //使能TIM3,定时器开始运行
}
/**
* 函 数:获取编码器的增量值
* 参 数:无
* 返 回 值:自上此调用此函数后,编码器的增量值
*/
int16_t Encoder_Get(void)
{
/*使用Temp变量作为中继,目的是返回CNT后将其清零*/
int16_t Temp;
Temp = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3, 0);
return Temp;
}
Encoder.h
#ifndef __ENCODER_H
#define __ENCODER_H
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "Encoder.h"
int16_t Speed; //定义速度变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Timer_Init(); //定时器初始化
Encoder_Init(); //编码器初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Speed:"); //1行1列显示字符串Speed:
while (1)
{
OLED_ShowSignedNum(1, 7, Speed, 5); //不断刷新显示编码器测得的最新速度
}
}
/**
* 函 数:TIM2中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断是否是TIM2的更新事件触发的中断
{
Speed = Encoder_Get(); //每隔固定时间段读取一次编码器计数增量值,即为速度值
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIM2更新事件的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
运行结果:
STM32-编码器接口测速
到这里,编码器接口测速的代码就写完了,下节继续!
QQ交流群:963138186
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓