Arduino用超声波测距模块HC-SR04获得精确测量值——误差数据的排除

    在Arduino板上最常用的测距模块就是超声波传感器HC-SR04,因为该模块使用方便,价格便宜(某宝上4元左右包邮)。它的主要性能指标为:采用40KHz超声波,测距:2cm—400cm,分辨率:3mm,有效角度:<15°,测量角度:30°。

    它的工作原理是:在trig端给一个至少10us的高电平信号,则该模块内部将循环发射8个40Khz脉冲,检测若有回波信号,则在echo端会产生高电平回响信号,其持续时间与所测距离成正比,所以只要测出持续时间就可以换算成距离。时序图如下:

    当我们得到了echo端的高电平长度(即超声波从模块发出后到接收到反射波所花的时间t,单位为us),就可以利用距离公式s=vt计算出距离(这里v为声速),不过这是包含了返回的时间,所以真正的距离还要再除以2。这里我稍微展开说下这里的声速v,通常我们常用的声速是340m/s,其实这只是一个大约的数,声速与气压、湿度、温度都有关系,不过在我们日常测量中,温度的影响还是比较大的,我们就来看下声速v和温度T的关系式:

    v=331.45+0.61T/°C

    其中的T为摄氏温度值,从这个公式我们可以看出,温度变化1C°,则速度随之变化0.61m/s,例如冬天0C°时的声速约为331m/s,而夏天40C°时的声速约则达到355m/s,也就是说,用两个温度下测出来1米的距离相差约7厘米多,所以在要求测量精度比较高的情况下,最好加一个测温模块。

    我们平时一般声速都用340m/s,其实从上面的公式里,就可以得出这是在摄氏约15C°时的声速,而网上大多数对HC-SR04进行编程的距离公式是用:s=t/58,有些人对除数58感到困惑,我这里解释下除数58的由来。首先我们在摄氏25C°时的声速约为346.7m/s,将距离s单位设为厘米(cm),时间t单位设为微秒(us),又因为时间是折返一次的时间,所以距离还要除以2。则有s=346.7*100cm/1000000/2*t=0.0173*t,而0.0173就约等于1/58,从而得出:s=t/58,不过我觉得还是用s=0.0173*t更值观些,而且乘法的运算速度也更快些。

    因此我们用HC-SR04进行测距时,如果想获得比较精确的结果,最好再加上一个测温模块。但在实际应用中,用HC-SR04进行测距时,最大的测量误差主要还是来自环境对超声波的干扰,例如被测物体表面不平整引起的折射,甚至空气中的水汽(大家可以测量下在潮湿气候中数据的不稳定情况)等诸多因素都会使得我们获得的采样数据出现较大的偏差。为解决这个问题,我们就必须将采样到的一些异常数据予以剔除,本程序采用了莱特、格拉布斯准则来剔除这些异常数据(关于莱特、格拉布斯准则来剔除这些异常数据的方法,可以参考本人在这之前发表的《Arduino测量误差数据的处理——莱特、格拉布斯准则剔除异常数据》一文),这样我们就能获得较为精确的距离测量值。

    本次实验,我们采用Arduino UNO板一块,超声波传感器HC-SR04一块,温湿度测量模块DHT11一块(也可以不用),杜邦线若干。其中HC-SR04的触发信号trig引脚接到Arduino的D8,反馈信号echo引脚接到Arduino的D9;DHT11的data引脚接到Arduino的D12。如下图所示:

    本次实验的测量数据处理方法为:每循环一次,采样20次,然后对这20个获得的数据进行异常数据的剔除,再由剩余的数据求得平均值,作为最后的距离值。本程序在Arduino UNO板上测试运行过,只需按上图连接,然后直接下载到Arduino UNO板即可,下面是完整的程序:

/* 
  超声波传感器HC-SR04测距:
 触发信号trig:触发高电平脉冲大于10us
 反馈信号echo:返回的高电平长度就是距离的us数字
 通过声波速度和采集到的时间计算出距离。
 测量数据处理方法为:每循环一次,采样20次,然后对这20个获得的数据进行异常数据的剔除,
 再由剩余的数据求得平均值,作为最后的距离值。 
 */
#include "DHT.h"
DHT dht;
const int DATAN=20; //每组处理测试数据的个数

// 引脚定义
const int trig = 8;    // 触发信号
const int echo = 9;    // 反馈信号
const int DHTPin =  12; // 定义DHTPin连接的引脚为D12
float thtmp; //存储温度值

double dt[DATAN];//存放一组从SR04读取的距离数据
double bdt[DATAN];//存放一组被剔除的数据
int dn;//每组处理测试数据个数,Detection返回时为有效数据个数
int bdn;//某组被剔除的数据个数

//初始化
void setup() {
  pinMode(echo, INPUT);//SR04的反馈端口echo设置为输入
  pinMode(trig, OUTPUT);//SR04的触发端口trig设置为输出
  pinMode(DHTPin, INPUT);  //设置DHT11的数据读入DHTPin    
  dht.setup(DHTPin); // 设置DHT11数据传输的引脚
  Serial.begin(9600);
}

//主循环
void loop() {
  long IntervalTime=0; //定义一个时间变量
  int i=0;
  thtmp=(float) dht.getTemperature(); //从DHT11读取温度
  Serial.println(thtmp);//通过串口输出温度
  for(i=0;i<DATAN;i++) //进行一组数据,20(DATAN)次采样,并进行计算后送dt[0]—dt[19]
  {  
    digitalWrite(trig, 1);//置高电平
    delayMicroseconds(15);//延时15us
    digitalWrite(trig, 0);//设为低电平
    IntervalTime=pulseIn(echo, HIGH);//用自带的函数采样反馈的高电平的宽度,单位us
    if (isnan(thtmp)) dt[i]=0.0173*IntervalTime; //使用摄氏25C°时的声速约为346m/s计算出距离,单位cm(若不用测温模块)
    else dt[i]=(331.45+0.61*thtmp)/20000.00*IntervalTime; //使用v=331.45+0.61t计算出距离,单位cm(用DHT11温湿度测量模块)
  }
  dn=DATAN;//每组采用数据个数
  Serial.println(Detection(dt,bdt,dn,bdn,2));//输出经过剔除误差数据的距离均值,这里取自定义准则2
  Serial.println(bdn);//被剔除的数据个数
  for(i=0;i<bdn;i++)  Serial.println(bdt[i]);//被剔除的数据值
  delay(10000);//延时间隔决定采样的频率,根据实际需要变换参数,可以为0
}

//误差数据剔除程序,返回有效数据的平均值
//参数data输入为原始测量数据,返回时,前datanum个为有效数据
//参数baddata无输入数据,输出为被剔除的数据
//参数datanum输入为原始测量数据个数
//参数badnum无输入数据,输出为剔除的数据个数
//参数rule为莱特or格拉布斯准则选择,3为莱特准则,4为格拉布斯95%,5为格拉布斯99%,小于3为自定义准则
double Detection(double data[],double baddata[],int datanum,int &badnum,int rule)
{
  double data_b[datanum];//临时存放保留的数据
  double v[datanum]; //残差
  double g95[]={1.15,1.46,1.67,1.82,1.94,2.03,2.11,2.18,2.23,2.29,2.33,2.37,2.41,2.44,2.47,2.50,2.53,2.56,2.58,2.60,2.62,2.64,2.66,2.74,2.81,2.87,2.96,3.17};//格拉布斯95%
  double g99[]={1.16,1.49,1.75,1.94,2.10,2.22,2.32,2.41,2.48,2.55,2.61,2.66,2.71,2.75,2.79,2.82,2.85,2.88,2.91,2.94,2.96,2.99,3.01,3.10,3.18,3.24,3.34,3.58};//格拉布斯99%
  double bsl; //贝塞尔公式结果
  double maxdev; //有效的莱特 or 格拉布斯准则的最大偏差
  double sum; //累加临时存储
  double average; //平均值
  int badindex;//某次剔除数据数
  int validNum=0;//有效数据数
  int proindex=0;//循环的次数
  double lg;//莱特 or 格拉布斯准则的系数
  int i;
  if (rule<=3) //当rule小于等于3时,直接用莱特系数3或自定义的rule值
    lg=rule;
  else if(rule>5) //当rule大于5时,强制设为莱特准则
    lg=3;
  badnum=0;
  while(1)
  {
    if(rule==4) //格拉布斯95%
    {
      if(datanum>=100) lg=g95[27];//数据个数大于100个时
      else if(datanum>=50) lg=g95[26];
      else if(datanum>=40) lg=g95[25];
      else if(datanum>=35) lg=g95[24];
      else if(datanum>=30) lg=g95[23];
      else if(datanum>=25) lg=g95[22];
      else lg=g95[datanum-3];
    }
    else if(rule==5)//格拉布斯99%
    {
      if(datanum>=100) lg=g99[27];
      else if(datanum>=50) lg=g99[26];
      else if(datanum>=40) lg=g99[25];
      else if(datanum>=35) lg=g99[24];
      else if(datanum>=30) lg=g99[23];
      else if(datanum>=25) lg=g99[22];
      else lg=g99[datanum-3];
    }
    proindex++;
    sum=0;
    for(i=0;i<datanum;i++)
      sum+=data[i];
    average=sum/datanum; //计算平均值
    sum=0;
    for(i=0;i<datanum;i++)
    {
      v[i]=data[i]-average; //计算残差
      sum+=v[i]*v[i]; //计算残差平方和
    }
    bsl=sqrt(sum/(datanum-1)); //计算贝塞尔公式标准差
    maxdev=lg*bsl; //计算最大偏差
    //剔除坏值,即剔除粗差数据
    validNum=0;
    badindex=0;
    for(i=0;i<datanum;i++)
      if(fabs(v[i])>=maxdev && maxdev!=0) //当|Vi|>准则偏差值时
      {
        baddata[badnum++]=data[i];//将该Xi作为粗差数据,放入坏数据数组
        badindex++;
      }
      else data_b[validNum++]=data[i];//否则将效数数据暂存到data_b数组
        for(i=0;i<validNum;i++) //将暂存的效数数据送回数据数组data
          data[i]=data_b[i];
    datanum=validNum;//将当前有效数据个数作为数据个数
    if(datanum>5)//有效数据大于5个,则继续进行处理
    {
      if(badindex==0) //若没有可剔除的粗差数据
        break;//跳出循环,即粗差数据处理完毕
    }
    else break;//有效数据小于等于5个,直接跳出循环
  }
  return average;//子程序返回有效数据的均值
}

    本人网名为“不赦先生”,发表在CSDN上的文章均为本人原创,且仅在CSDN网站发布,其他网站的转载均未获得过本人的授权。

猜你喜欢

转载自blog.csdn.net/m0_61543203/article/details/127185686