Arduino小车PID调速前期准备——码盘测速精度的提高

要对一个控制系统进行pid调节,一个必要条件即有可靠的输入量。对于进行PID调速的Arduino小车而言,采取一种可靠的方式进行测速,是后续实现较好PID调速效果的先决条件。

硬件结构

  • 测速模块:光码盘
  • 轮子直径:80mm
  • 码盘齿数:100齿/圈
  • 控制器:Arduino nano ATmega328P

由此可知,码盘每经过一个齿,小车的距离将变化 80π/1002.5133mm 的距离

传统测速的尝试

以光码盘作为测速模块,最基本的思想就是通过将一段时间内码盘采样脉冲数除以时间,便可得到单位时间内小车走过的码盘齿数,再乘以每个齿对应小车距离变化量,便可得到小车的速度

码盘脉冲计数常用外部中断实现,将光耦开关接在控制板的一个可实现外部中断的引脚上,调用Arduino自带的设置外部中断的函数attachInterrupt(),分别输入中断通道编号、要调用的中断函数以及触发中断的方式三个参数,并在中断函数里写入码盘脉冲计数的代码,即可实现码盘脉冲计数功能,具体如何编写不再赘述。

在设置外部中断时,Arduino中有五个触发中断模式可选

MODE描述
LOW低电平触发
CHANGE电平变化触发
RISING上升沿触发
FALLING下降沿触发
HIGH高电平触发

当给小车电机一个固定的PWM信号,即匀速时,理论上光耦开关会输出一个等间距的矩形脉冲信号,因此在许多码盘测速程序中可看到采用下降沿触发脉冲计数的方式,笔者起初也是采用的这种方式来计数的。

笔者的小车使用的电机为较常见的130直流减速电机,当给电机最大的PWM值,让其达到最快速度时,通过观察可得小车速度大约为三百多毫米每秒。当采用下降沿触发脉冲计数的方式时,这意味着小车每秒钟触发的脉冲数约为 300/2.5133120 个。而为了达到较为及时的速度输出效果,需要设定一个合适的采样周期,笔者设定的为每100ms采样一次,即每秒钟采样10次,因此在每个采样周期内,小车触发的脉冲数约为12个。由于在绝大多数情况下,一个采样周期内不会是恰好经过整的脉冲个数,会出现在一个码盘齿中间某处尚未触发下降沿脉冲信号时,便已结束一次测速采样,进行下一次测速。这造成了脉冲计数不准的情况发生,一个采样周期内会与实际值有所偏差。在以上条件下的速度计算过程中,误差范围约为0~25mm/s,这对于将来要进行PID调节来说,是非常不利的。通过在小车上实际应用,可发现即使在空转的情况下,给一个固定的PWM数值,测得的速度也常常以相差25mm/s的值波动,而实际电机的速度并没有发生任何改变。因此需要想办法提高小车测速的精度。

笔者想到的第一个办法是改变脉冲触发方式。Arduino提供的设置中断的函数里可选的触发方式除了FALLING,还有CHANGE,即只要发生电平的变化,就触发中断。将触发方式由FALLING改成CHANGE,则小车行进一圈,触发的脉冲个数由100个变为200个,从理论上将小车码盘采样的精度提高一倍。这时的误差范围约为0~12mm/s,但这还不足实现可靠的测速,满足进行PID调速的要求。

非完整矩形波计数法

小车码盘齿的个数由其机械结构决定,而能产生的最大脉冲数量也由把触发脉冲技术方式由FALLING改成CHANGE而达到了极限,无法从增加单位长度脉冲个数的方式提高精度。此时要想提高精度,便需要从误差源——不完整的矩形波脉冲上来入手思考。

幸运的是,笔者在网上找到了一篇由嵌入之梦写的低分辨率码盘测速(网址)的博客,其主要思想是“基于正常转动的特征,相邻的两个脉冲的周期不会发生突变……因此可以采用计数和测周期两种方式组合,实现倍频,以提高分辨率。“其基本的实现方式是在一个测速周期内,用产生的最后一个不完整的矩形波的时间除以产生前一个完整的矩形波的时间,便可估算出最后一个不完整的矩形波大约是完整矩形波的几分之几,从而在计算速度时大大提高了精度。由于笔者水平有限,且原博客博主已将这种非完整脉冲技术法用于提高低分辨率码盘的思想讲解的十分清楚了,所以读者可转到原博下更详细地了解。

测速程序设计

为实现每隔一段固定的时间进行一次测速,笔者采用了Arduino的MsTimer2定时器中断库,可从此超链接下载和学习库的使用。采用定时器的方式测速,相对于主函数内其他程序较为独立,避免了在主函数中设置循环和延时,产生不必要的麻烦。

以下是一个仅包含测速功能的程序代码

#include <YM3Wheel.h>           //小车轮式驱动单元库
#include <Servo.h>              //舵机库
#include <MsTimer2.h>           //定时器中断库

#define SAMPLE_DELAY 100        //测速周期,设置为100ms

//板载LED指示灯设置
int LED = 13;              
boolean LED_On;                 
//int FlashTime = SAMPLE_DELAY; //LED灯量灭闪烁时间为500ms

//轮式驱动单元设置
YM3_Motor YM3(MOTORPWM_1KHZ);  

//转向舵机设置
int ServoPWM = 10;                      
Servo myServo;                    

//定义测速代码中用到的变量
int pulseInterval = 0;          //一个定时测速周期内,采集得完整脉冲矩形波的个数
float speed = 0;                //小车速度
float tempTime = 0;             //存储产生上一个脉冲时的系统时间
float halfPulse = 0;            //不完整矩形波换算成0.xx个完整矩形波
float PulseTemp = 0;
float pulseTimeInterval = 0;          //
float halfPulseTime = 0;        //最后一个不完整矩形波占用的时间
float halfPulseBefore = 0;            //存储上一次的半个脉冲数


void setup() {          
  attachInterrupt(ENCODE_INT,pulseSample, CHANGE);  //码盘脉冲计数外部中断中断
  MsTimer2::set(SAMPLE_DELAY, speedTest);           //测速程序定时器中断
  MsTimer2::start();

  //舵机和轮式驱动单元设置                              
  myServo.attach(ServoPWM);                   //舵机对象myServo的接口为ServoPWM(pin10引脚)
  myServo.write(98);                          //舵机初始方向
  YM3.run(FORWARD, 200);                      //舵机初始运行方式

  pinMode(LED, OUTPUT);   

  Serial.begin(9600);
}

void loop() {
  //主程序段,因本程序仅为测试测速功能,故此处暂空缺
  }

//码盘脉冲计数的中断函数
void pulseSample(void)                      
 {
   YM3.pulseSample();                           /*调用轮式驱动单元库中的码盘脉冲计数函数
                                                  每触发一次外部中断,脉冲总数+1*/
   pulseTimeInterval = micros() - tempTime;     //一个脉冲所用时间 = 此刻产生脉冲时的系统时间 - 上次产生脉冲时的系统时间
   tempTime = micros();                         //记录本次脉冲触发中断的系统时间,用于下次触发中断时计算
 }

//测速定时器中断函数
 void speedTest(void)                           
 {
    halfPulseTime = micros() - tempTime;        //最后一个不完整矩形波时间 = 触发定时器中断的系统时间 - 上一个完整矩形波的系统时间
    halfPulse = halfPulseTime / pulseTimeInterval;//不完整矩形波换算成0.xx个完整矩形波 = 不完整矩形波时间/完整矩形波时间
    pulseInterval = YM3.pulseSample() - pulseInterval;//一个定时测速周期内,采集得完整脉冲矩形波的个数
    //速度 = (上次不完整矩形波剩余部分+完整脉冲数+最后一个不完整波数)*每个脉冲对应的距离/测速毫秒时间*1000
    speed = (1 - halfPulseBefore + (pulseInterval-1) + halfPulse)* (2.5133 / 2) / SAMPLE_DELAY * 1000;

    halfPulseBefore = halfPulse;                //存储本次测速的最后一个不完整波数
    pulseInterval = YM3.pulseSample();          //记录本次测速周期结束时脉冲总数
    digitalWrite(LED, LED_On);                  
    LED_On = !LED_On;

    Serial.println(speed);          
 }

测试

传统测速方式

采用传统的测速方式,即不考虑不完整矩形波,会产生较大误差,不利于后续进行PID调节

传统测速

非完整矩形波计数法

考虑了非完整矩形波的情况下,给电机一个固定PWM信号,实际转速恒定时,测量得的速度波形波动范围明显减小,突破了码盘齿数的限制,精度得到提高。

非完整矩形波计数法

不足与改进

本文中采用的非完整矩形波计数方法从本质上是利用MCU的高精度时钟来提升计数精度的,但是是建立在相邻两脉冲产生时间基本上相同的假设上,并且忽略了中断响应时间、中断退出时间以及在中断函数中进行计算所耗费的时间,这可能会造成测量速度比实际速度大。同时,传感器工作时可能会存在各种干扰,若要达到更可靠的脉冲计数,可考虑进行数字滤波。

本文的代码中,在中断函数里进行了一些浮点计算。浮点计算本身就比整型数字计算慢,因此可将一个完整脉冲矩形波进行倍频处理,从而将浮点计算化为整型数字计算,详见博客FIRA小车“习武记”之八:把握分寸(低分辨率码盘测速)。另外,系统调用中断后,系统时间将停止增长,这也是不利于进行可靠测速的。本着中断函数中内容尽量简洁的原则,可以定义一个布尔型标志speed_Flag,每当触发一次定时器中断测速时,则将该标志置1.在void loop(){}主函数内,设定一个检测speed_Flag标志状态的if条件语句,当触发了一次定时器中断,则在主函数内执行相应的测速代码,并再次将speed_Flag标志位置0,等待下一次定时器中断触发。这样做可以减轻定时器中断函数的负担,并且经过测试,两种中断处理方式得到的测速结果没有太大的偏差,是可行的。

参考

  1. 嵌入之梦-113190 FIRA小车“习武记”之八:把握分寸(低分辨率码盘测速)

新手的尝试,若有不正确之处,还望各位前辈多多指教!

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值