数字电位器(旋转编码器)就是下面这个,示波器等仪器上用的很多,手感很好。
这种编码器的原理和电机的AB相增量式编码器原理是一样的,参考之前文章《【机械自动化】旋转编码器》。
这种编码器的检测不需要在意换向(旋转方向改变)时的抖动问题,这里只要检测到上图表中的A相跳变的情况,也就是第1/3/6/8四栏中的情况即可,为了不会漏检,可以使用中断检测A相的上升下降沿,同时判断B相的电平状态,如下程序为测试程序。需要注意的是这种数字电位器(旋转编码器)的每相信号的跳变是有抖动的(不是指换向过程的抖动,换向的抖动是正常抖动,这里的抖动是物理电气特性导致的无用甚至有害的“杂波”),和按键抖动一样,需要进行去抖操作,如下图。
程序中GPIO_Pin_5为A相,GPIO_Pin_6为B相,设置GPIO_Pin_5为上升下降沿中断触发,进入中断程序之后先进行去抖动,然后先判断A相是否有变化(上升沿或下降沿),然后在进行具体情形的检测。不需要检测B相的上升沿或下降沿,同时检测A相和B相的变化是为了检测到换向的抖动,多用于电机编码器,这里没必要。
void EXTI4_15_IRQHandler(void)
{
if(EXTI->PR & EXTI_Line5)
{
//每一相脉冲都可能有抖动,类似于按键的抖动
//需要在软件上将抖动过滤掉,延时放在最前面。
delay_us(1000); //去抖动,放在中断处理的最前面!
// printf("r");
now = GPIOA->IDR & (GPIO_Pin_5 | GPIO_Pin_6);
if((last & GPIO_Pin_5) != (now & GPIO_Pin_5)) //A相变化
{
// printf("s");
if((now & (GPIO_Pin_5 | GPIO_Pin_6)) == B00100000)
{
// printf("a");
count++;
}
else if((now & (GPIO_Pin_5 | GPIO_Pin_6)) == B01100000)
{
// printf("b");
count--;
}
else if((now & (GPIO_Pin_5 | GPIO_Pin_6)) == B01000000)
{
// printf("c");
count++;
}
else if((now & (GPIO_Pin_5 | GPIO_Pin_6)) == B00000000)
{
// printf("d");
count--;
}
last = now;
// printf("%d\r\n",count);
}
EXTI->PR = EXTI_Line5;
}
else if(EXTI->PR & EXTI_Line6)
{
//B相不用检测,因为这里和电机编码器不同
//不需要关心换向时的抖动,有点错误没关系
EXTI->PR = EXTI_Line6;
}
}
为了实现速率控制(velocity control),也就是转的越快,增长(降低)速度越快,绝大部分示波器都有这个功能,可以检测两次脉冲间的时间间隔,如果时间间隔较大,则增长速率为1,如果时间间隔较小,则可以根据时间间隔大小适当调整增长速率,如下测试程序中,使用一个1ms的定时器产生一个时钟计数器,定时器中断中简单地增加计数即可,不用管溢出情景,因为溢出对这里没什么大影响。
void EXTI4_15_IRQHandler(void)
{
if(EXTI->PR & EXTI_Line5)
{
//每一相脉冲都可能有抖动,类似于按键的抖动
//需要在软件上将抖动过滤掉,延时放在最前面。
delay_us(1000); //去抖动,放在中断处理的最前面!
now = GPIOA->IDR & (GPIO_Pin_5 | GPIO_Pin_6);
if((last & GPIO_Pin_5) != (now & GPIO_Pin_5)) //A相变化
{
int delta = syscount - syscount_last; //计算两次有效脉冲的时间间隔
if(delta > 10) //根据时间间隔设置增长速率
delta = 1;
else
delta = 20 - delta; //修改这里的速率和时间间隔关系改变体验
if((now & (GPIO_Pin_5 | GPIO_Pin_6)) == B00100000)
{
count += delta;
}
else if((now & (GPIO_Pin_5 | GPIO_Pin_6)) == B01100000)
{
count -= delta;
}
else if((now & (GPIO_Pin_5 | GPIO_Pin_6)) == B01000000)
{
count += delta;
}
else if((now & (GPIO_Pin_5 | GPIO_Pin_6)) == B00000000)
{
count -= delta;
}
last = now;
syscount_last = syscount;
}
EXTI->PR = EXTI_Line5;
}
else if(EXTI->PR & EXTI_Line6)
{
//B相不用检测,因为这里和电机编码器不同
//不需要关心换向时的抖动,有点错误没关系
EXTI->PR = EXTI_Line6;
}
}
//1ms中断定时器
void TIM16_IRQHandler(void)
{
if (TIM16->SR & TIM_IT_Update)
{
syscount++;
}
// TIM16->SR = (uint16_t)~TIM_FLAG_Update;
TIM16->SR = (uint16_t)0; //清除所有标志位
}
测试主函数可以显示增长速度。
int last = count;
while(1)
{
if(last != count)
{
printf("%d,%d\r\n",count,count-last);
last = count;
}
}
测试结果:
但是,这种检测方式还是比较复杂的,在中断中需要进行延时去抖动,还需要判断多种情形,这样会使得中断频率过快的时候丢步。为了简化检测程序,只需要检测在A相的上升沿中断产生的时候,判断B相的电平状态即可,这种做法虽然会丢失一些情况的检测,但是处理速度快,反而会提高检测速度,示例程序:
void EXTI0_1_IRQHandler(void)
{
//每一相脉冲都可能有抖动,类似于按键的抖动
//需要在软件上将抖动过滤掉,延时放在最前面。
// delay_us(1000); 加上0.1uF滤波电容可以去掉抖动了//去抖动,放在中断处理的最前面!
//尽量减少操作,减少丢步
if(ENCODER_AB_PORT->IDR & ENCODER_B_PIN)
encoder_count --;
else
encoder_count ++;
EXTI->PR = EXTI_Line0;
}
速率检测部分的代码也可以不在中断中做,可以在主程序中计算相邻两次计数值变化的时间差来推导速率。另外在编码器的A、B相和地端接上一个陶瓷电容,用于去除抖动,这样就无需在中断中做延时这样耗CPU的工作了,简化A、B电路为按键电路示意如下(使用的单片机内部上拉电阻约为40K):
C1取0.1uF,示波器测量IO的波形:
放大下降边沿波形,可以看出没有抖动了,而且下降时间也很短,因为按下按键是直接短路电容,放电时间短:
放大上升边沿波形,典型的RC充电波形:
RC = 40K * 0.1uF = 4ms;
当t= RC时,电容电压=0.63E;
当t= 2RC时,电容电压=0.86E;
当t= 3RC时,电容电压=0.95E;
当t= 4RC时,电容电压=0.98E;
当t= 5RC时,电容电压=0.99E;
所以当松开按键4ms之后,电容电压应该为 0.63E = 0.63 * 3.16 = 1.99V,如下图,△V = 1.96V,由于有抖动,可能会造成实际值和计算值有误差,但是这里差距很小了。
改变电容C1为1uF,测量波形,RC = 40K * 1uF = 40ms,测量结果很接近:
但是这个波形使用逻辑分析仪抓取出来的结果很奇怪:
上升沿尽然有毛刺出现:
可能是逻辑分析仪的采样方式和示波器不同导致RC充电曲线被逻辑分析仪误认为是毛刺信号,这时候看出来示波器的好处了。
如果C1的取值过大(或者内部上拉电阻过高),导致放电时间过长,这时候会导致连续按按键会检测不到,因为电容还没有充电至高电平电压的时候又按下按键,这时候并不会产生一个有效的下降沿中断,所以按键检测就会丢失。如果C1的取值过小,RC时间和按键抖动时间处于同一数量级的话,会导致抖动过滤效果不佳。