正交编码器,旋转编码器的一种(增量式编码器),用来确定机械行程的位移量与方向。通过监控脉冲数目和两个脉冲的相对相位,可以跟踪旋转的位置,旋转 和速度。比如电机转速。
LM3S8962包含两个正交编码器接口,这对于电机驱动与监控足够,不过编码器还常常被用作数据输入设备,比如仪器上的参数设置旋钮就有很多是采用的编码器,鼠标的滚轮也是一只小型的编码器。这时候,8962的编码器资源就显得不足了,用软件解码正交编码器就很必要了。
不过还好,正交编码器的解码并不复杂,很简短的语句就可以实现。在编写编码器解码程序之前,我们先看一下编码器的工作原理。
正交编码器在匀速旋转时会输出一组相位相差90度的两个通道的方波,如图1。当然,不匀速旋转时也一样可以工作,只是此时输出的不是方波,而且在一个周期内的速度测量已经无法保证准确了。
这组方波相位关系有两种模式,A通道领先B通道 90度或B通道领先A通道90度。当其顺时针旋转时,A通道领先B通道,反之,B通道领先A通道。
关于其详细的解码规范可以参考网上资料,我这里只讲述一种简易的解码规范,也许解码没有那么精确,不过方法及其简单。
大家仔细观察一下上面两组波形,选择其中一个通道的一个边沿作为参照的话,可以看到,在旋转方向不同时,该边沿对应另一通道的电平也是不同的。
基于这一现象,可以选择其中一个通道来触发单片机的中断,在中断中读取另一通道当前的状态,以此来判断当前的旋转方向。其代码如下:只有三行。
-
void QEI_GPIOInt_ISR(void)
{
GPIOPinIntClear(GPIO_PORTE_BASE,0x01);
Direction=GPIOPinRead(GPIO_PORTE_BASE,0x02);
if(Direction) g_StepCount++; else g_StepCount--;
}
复制代码
这里将8962的PE0连接相位A输入,并作为中断源,下降沿触发中断,PE1输入相位B。
程序中Direction表示当前旋转方向,0为逆时针旋转,非0为顺时针旋转。当然,如果大家想为顺时针旋转也确定一个固定的值的话可以改成下面方式,也是三行:
-
void QEI_GPIOInt_ISR(void)
{
GPIOPinIntClear(GPIO_PORTE_BASE,0x01);
Direction=(!GPIOPinRead(GPIO_PORTE_BASE,0x02));
if(!Direction) g_StepCount++; else g_StepCount--;
}
复制代码
这样写的好处是,对读取的数据做以此逻辑运算,不论读取的是哪个管脚,都可以变成0或者1两个值之一。在整个PE端口内都可以采用这段程序中的运算来实现,只需要更改一下引脚编号即可。
g_StepCount中保存的是当前位置的累计值。编码器每输出一个脉冲,该值加1。
简单吧,用它来作为连续数据输入装置是不是很酷呢?做一个掉电记忆的数控电源,设定好每步的步进值,设定好后自动保存,断电后也不怕别人去乱拧你的旋钮,下次开机时,还是原来的输出电压,比电位器安全多了。而且没有精度限制,软件中可以任意设置步进精度,只要你不怕累,可以将调节精度设置到小数点后20位,呵呵。完整工程文件在此下载。
QEI1.rar
(55.69 KB, 下载次数: 69)
上面说了,只要你不怕累,你可以将调节精度设置到小数点后的20位,但是有时候我们既想做的精细点,但是又不想跟转风车似的去拧旋钮,鱼和熊掌可以兼得吗?在编码器这里可以兼得的。
上面这个程序只是实现了编码器最基本的功能,位置计数和判断方向,编码器还有一个速度参数可以使用的。我们可以在程序中设置数值增长的步进值和速度的平方成正比,或者成指数关系,等等。这样当我们快速的旋转编码器的时候,数值变化是飞快的,当我们慢下来准备细调的时候,程序也可以很善解人意的陪我们慢慢调节,甚至拿起放大镜,一个单位一个单位的调节。
那么如何来测量编码器的速度呢?测脉宽,用定时器测量一个脉冲的宽度,进而计算出编码器的速度。
带有速度测量功能的编码器解码方法是这样的(一定范围内的匀速旋转):
输入脉冲中断采用双边沿中断,在下降沿开启定时器,在上升沿计算定时器计数值。速度的解码在主程序中根据该值计算,方法自定,呵呵,毕竟应用不同,会有不同的计算方式。不过既然有了周期,计算速度还不是定义一个函数关系的事。
带有速度测量功能的编码器解码程序如下:
-
void QEI_GPIOInt_ISR(void)
{
TimerEnable(TIMER0_BASE, TIMER_A);
g_timeValue1=TimerValueGet(TIMER0_BASE,TIMER_A);
GPIOPinIntClear(GPIO_PORTE_BASE,0x01);
if(GPIOPinRead(GPIO_PORTE_BASE,0x01))
{
TimerDisable(TIMER0_BASE,TIMER_A);
TimerLoadSet(TIMER0_BASE, TIMER_A, 0xffffffff);
g_timeStep=g_timeValue2-g_timeValue1;
g_Direction=(!GPIOPinRead(GPIO_PORTE_BASE,0x02));
if(g_Direction) g_StepCount++; else g_StepCount--;
g_readState=1;
}
else
{
g_timeValue2=g_timeValue1;
}
}
复制代码
首先两行:
TimerEnable(TIMER0_BASE, TIMER_A);
g_timeValue1=TimerValueGet(TIMER0_BASE,TIMER_A);
这里没有经过任何判断直接开启定时器,然后是一个定时器读取。因为当定时器已经开启时,再执行一次开启操作对其没有影响。而且还要保证在定时器计数状态下,两次读取定时器值的时间相对于响应中断时间尽量一致,以减小代码执行时间带来的误差。
在后面的判断语句中,所有的计算都是在上升沿中断进行的,下降沿中断只是将当时定时器的值(开启时间值)转存,防止下次中断将其覆盖掉。
在上升沿中断中,首先关掉定时器,此时已经不需要它了。然后再进行一次初值装载操作,只有在对计数初值寄存器有过写入操作后,开启定时器时才会重新装载计数初值,不管写入的这个数据有没有改变寄存器的值,这样是为下一周期计数做准备。
然后进行步长计算,方向判断以及当前位置累计值的更新。
最后再说一下最后设置的这个g_readState,这个值表示当前数据已经更改过了,可以用来作为读取数值的标志,也可以用来防止读取过程中更改数据,保证读取数据的一致性。
同时它还有另外一个用途。上面的速度计算只在一定范围内有效,当速度特别慢时,其计算值就已经没有什么意义了,所以,在应用中可以设定一个速度的下限,当速度低于这个下限时,就根据g_readState和g_Direction的值来作步进计算,相当于是最细微的微调了。
好了,说了这么多的g_readState,我们来看一个实例吧:
-
signed long Ger_GPIOQEI_StpTime()
{
signed long m_StpTime;
while(1)
{
g_readState=0;
m_StpTime=g_timeStep;
if (g_readState==0) break;
}
return(m_StpTime);
}
复制代码
在这里,不需要关心读取前g_readState是0还是1,只关心其读取前后是否从0变成了1。如果改变了,则重新读取该值。
在读取多个值的时候一定要注意,在读取之前清零g_readState,全部读取完毕后再检测g_readState,这样才能保证其读取的值是同一状态下的。
好了,关于编码器今天就聊这么多吧,第二个程序在这里
QEI2.rar
(58.32 KB, 下载次数: 54)
,匆忙写就,难免遗漏,如有问题,欢迎大家指正。