Усреднение показаний цифрового датчика

Тема в разделе "Флудилка", создана пользователем donpavlensio, 19 янв 2015.

  1. donpavlensio

    donpavlensio Нерд

    Здравствуйте!
    Не знал, в каком разделе спрашивать, посему спрашиваю во флудилке, авось не побьют)))

    Есть цифровой датчик давления и температуры BMP180, а так же Arduino Uno с которой он взаимодействует.
    Идея состоит в том, чтобы использовать данный датчик в качестве высотомера - измерять разницу высот между двумя точками.
    Первый клик - фиксируется первый замер
    Второй клик - второй замер + расчет разности высот по барометрической формуле.
    Третий клик - обнуление.

    Все это получается, затык лишь в том, что мне хочется большей точности, а для этого надо усреднять данные с датчика. Единственное, на что хватило моих навыков - это по десять раз измерять давление, с присвоением его значения десяти разным переменным, а потом выводить из них среднюю величину.
    Код (Text):

    /*******************************/
           dps.getPressure(&z1);
          dps.getPressure(&z2);
          dps.getPressure(&z3);
          dps.getPressure(&z4);
          dps.getPressure(&z5);
          dps.getPressure(&z6);
          dps.getPressure(&z7);
          dps.getPressure(&z8);
          dps.getPressure(&z9);
          dps.getPressure(&z10);
         
          P1 = (z1+z2+z3+z4+z5+z6+z7+z8+z9+z10)/10/133.32;//Вычисление давления в мм.рт.ст.
    /*******************************/
     
    В принципе, работает, но мне совершенно не нравится, хочется чего-то более изящного, как вот в этой статье. И что-то подсказывает мне, что более изящное решение должно к тому же сэкономить на размере скомпилированной программы.
    Гуглем, увы, ничего не нашел.
     
  2. donpavlensio

    donpavlensio Нерд

    Эврика!
    Ну надо же, все выходные не мог найти, как это делается, а тут наконец-то допер
    Код (Text):
    long val = 0;
        for (int i=0; i<100; ++i) {
            dps.getPressure(&val);
            P1 = P1+val;
         
          }
          P1= P1/100/133.32;  
    Кстати, о погрешностях. Шум датчика приводит к тому, что "высота", определяемая покоящимся барометром заметно пляшет. Осреднение тут неплохо помогает.
    Вот график
    [​IMG]

    Для каждого числа осреднения делал по 30 замеров, чтобы была представительная выборка.
    Как видно, осреднение по 10 замерам наиболее выгодно с точки зрения точность/число замеров, дальнейший рост числа замеров не дает существенного выигрыша. Плюс к тому на 30 замерах, уже становится видимой пауза, а на 100 замерах между кликом и отображением цифры проходит более 1 секунды.
    Померил высоту потолков на стократном осреднении- четыре замера от 2,41 до 2,46, пятый отскочил на 2,86, в среднем получается 2,56 м. Весьма неплохо!
     
    Daniil нравится это.
  3. Unixon

    Unixon Оракул

    Еще лучше будет в цикле оценивать относительную погрешность, пока она не станет меньше порога, либо пока не выберется все время для измерения.
     
    donpavlensio нравится это.
  4. donpavlensio

    donpavlensio Нерд

    Спасибо за идею, поскриплю мозгами на досуге. Я так понимаю, тогда это реализуется через функцию while().
    Сразу вижу затык в том, что относительная погрешность при одном единственном измерении окажется равной нулю и гипотетический счетчик тогда на нем и остановится, вполне довольный собой.
     
  5. X-Dron

    X-Dron Гик

    В коде есть ошибки.
    Правильнее так
    Код (Text):

      long P1 = 0;
        for (int i=0; i<100; i++) {
            long val;
            dps.getPressure(&val);
            P1 = P1+val;
          }
          P1= P1/100/133.32;  
    Перед входом в цикл нужно обнулять не считываемое значение, а интегратор значений.
    int i=0; i<100; ++i - по-моему, выполняет цикл не 100 раз, а 99. Если ++ стоит до i операции, то при первом заходе в цикл она уже 1, а при проследнем 99. Результат 99 измерений делите на 100, получаете ошибку в 1 процент. Выведите значения i в Serial, посмотрите сколько раз крутится цикл. Можно на время теста поставить i<10.
     
    Последнее редактирование: 19 янв 2015
    ИгорьК и donpavlensio нравится это.
  6. Unixon

    Unixon Оракул

    Ну естественно, что нужно сначала сделать "нулевое" измерение и инициализировать переменные, а уже потом запускать цикл и считать погрешность. Еще лучше будет ввести некоторое минимальное количество измерений, а не только максимальное. У программы будет небольшая свобода действий в рамках этих ограничений.
     
  7. donpavlensio

    donpavlensio Нерд

    Да, точно. Просто в предыдущей версии интегратор значений рассчитывался каждый раз по новой. Поэтому его объявление у меня так и осталось в "шапке" (блин, как называется область программы, где вводятся макроопределения и т.п?). А так у меня первый замер осреднялся с предыдущим вычисленным значением интегратора. Спасибо.

    Нет, первый замер делается для i=0, последний - для i =99, в итоге - честные сто замеров.
     
  8. donpavlensio

    donpavlensio Нерд

    Подумал, подумал, да и решил, что такое решение в простом исполнении будет все равно ущербным и сильно зависящим от случая (первое же измерение цикла может вполне совпасть с нулевым), а правильное - весьма громоздким.
    И решил пойти своим путем. ИМХО гораздо практичнее и познавательнее вычислить величину случайной погрешности датчика при разной величине осреднения и выбрать то, которое тебя устраивает с полным знанием дела. Для сего наваял следующий скетч (ох уж и помучался я с ним, в экселе считать проще:))

    Код (Text):
    /*******************************************************
    Скетч для оценки точности (воспроизводимости результатов) цифрового датчика давления BMP085/BMP180
    Для оценки аналоговых датчиков надо закомментировать строки, относящиеся к датчику давления и
    раскомментировать дополнительные строки для аналогового сенсора
    Скетч так же быстро адаптируется для любого цифрового датчика. Но при использовании цифровых датчиков результаты оценки могут быть некорректными в случае использования в них математических  фильтров и склонности датчика к "заеданию"
    Данные выводятся через последовательный порт.

    Created 21/01/15
    by donPavlensio :)
    ********************************************************/

    #include <Wire.h>
    #include <BMP085.h> //библиотеки для работы с датчиком давления
    #include <math.h> // математическая библиотека, для барометра не нужна, пригодится только для расчета температуры с использованием термистора

    //#define SENSOR_PIN (A0) //раскомментировать для аналогового датчика

    BMP085 dps = BMP085();

    long val;

    void setup()
    {

      dps.init(); // Инициализация барометра
    //pinMode(SENSOR_PIN, INPUT);//Раскомментировать для использования аналогового датчика, хотя он и так по умолчанию работает на вход
    pinMode(13, OUTPUT); // Пин настроен на вывод, чтобы мигать светодиодом, когда закончится оценка

    Serial.begin(9600);
    delay(1000);

        Serial.print("av_rate"); //Шапка для будущих замеров
        Serial.print("\t");
        Serial.print("Mean");
        Serial.print("\t");
        Serial.print("StandDev");
        Serial.print("\t");
        Serial.print("RelDev");
        Serial.print("\t");
        Serial.print("MaxDev");
        Serial.print("\t");
        Serial.print("Millis/Meas");
        Serial.print("\t");
        Serial.println("TotMillis");

      Serial.println();

    for (int i = 1; i <= 256; i *= 2){    // Значение i задает число осреднений на замер (см. ниже), а количество значений, принимаемых i - число сессий.
      float Summ = 0;
      unsigned long MeanDelay = 0;
      float myArray[31];
      for(int j = 1; j <= 30; ++j){      // Этот счетчик задает число замеров для каждой сессии
        float z1=0;
        unsigned long startMillis = millis(); //считываем момент начала замера
        for (int n = 1; n <=i; ++n){  // n в каждой сессии может расти только до значения i, таким образом, для каждого замера давление будет определено i раз
          dps.getPressure(&val);  //Получаем давление с барометра
        // dps.getTemperature(&val); // раскомментировать для получения температуры с датчика BMP085
       
          z1 = z1+val;    //суммируем определенные значения с цифрового датчика, для аналогового датчика закомментировать
       
    //  z1 = z1 + analogRead(SENSOR_PIN); //Раскомментируйте строку для использования с аналоговым сенсором

        }
        z1 = z1/i;  //осредняем их делением на i;
     
        z1 = z1/133.32; // деление на 133.32 производится для перевода давления из паскалей в мм.рт.ст, для других сенсоров надо закомментировать строку
    // z1 = z1/10; // опция для термометра датчика BMP085
    // z1 = -14.46*log((10000.0 * z1*5.0/1024.0)/(5.0 - z1*5.0/1024.0)/27074.0); // формула для расчета температуры из данных с измерителя на основе термистора 10кОм
        myArray[j] = z1; //Записываем значение в массив, оно пригодится для статистической обработки
       
       
          unsigned long del = millis() - startMillis; // вычисляем время, прошедшее с начала измерения
          MeanDelay = MeanDelay + del; //Интегратор затраченного времени для вычисления среднего
          Summ = Summ+z1;
      }
      float Mean = Summ/30; //Находим среднее значение
      float SummDifSq = 0;

      for(int m=1; m <= 30; m++){
              SummDifSq = SummDifSq + sq(myArray[m]-Mean); //Находим сумму квадратов отклонений
            }
         
        float StandDev = sqrt(SummDifSq/30); // Находим стандартное отклонение
        float RelDev = StandDev*100/Mean; // Относительная ошибка, % от измеряемой величины
        float MaxDev = 4*StandDev;  //Теоретическое значение максимальной разницы между двумя замерами, с вероятностью 95% (то есть, максимальный отскок от среднего +/- 0.5MaxDev)
                                    // более существенные отскоки возможны, но маловероятны
        MeanDelay = MeanDelay/30; //Среднее время на один замер

        Serial.print(i);
        Serial.print("\t");
        Serial.print(Mean);  
        Serial.print("\t");  
        Serial.print(StandDev, 4);
        Serial.print("\t");
        Serial.print(RelDev,4);
        Serial.print("\t");
        Serial.print(MaxDev, 4);
        Serial.print("\t");
        Serial.print(MeanDelay);
        Serial.print("\t");
        Serial.println(MeanDelay*30);
     
    }
    }

    void loop()
    {
    digitalWrite(13, HIGH); //Мигаем светодиодом; loop исполняется после setup, поэтому мигание светодиода ознаменует конец испытания.
    delay(900);              // Светодиод подключать не обязательно, на самой плате UNO уже есть индицирующий светодиод для пина 13
    digitalWrite(13, LOW);
    delay(100);
    }
     
    Данный скетч при минимальном допиливании позволяет оценивать относительную погрешность практически любого датчика. В качестве бонуса узнаем также время измерения.
     
  9. donpavlensio

    donpavlensio Нерд

    Прогнал сразу все, что оказалось под рукой, кроме GPS (на окошке измерять погрешность - толку мало, к тому же градусы мало чего дают пониманию, надо это безобразие еще в UTM переводить), выкладываю результаты

    Первым пошел датчик давления BMP180
    [​IMG]
    Как видно, осреднение хорошо помогает вплоть до 16-32 испытаний на замер, дальнейшее увеличение даже до 1000 измерений не дает никакого прироста точности. При этом на 16 испытаний уходит 220 миллисекунд, а на 32 - 440 мс, то есть почти полсекунды. Не напрягает, но задержка уже заметная. Поэтому я выберу 16.

    Следующий - встроенный температурный датчик в том же BMP180
    [​IMG]
    Тут получилась полная каша. То ли в датчике реализовано фильтрование, то ли имеется эффект "залипания", в общем, с датчиком что-то не то. Но оказалось, что данные о температуре датчик выдает в 2,5 раза быстрее, чем о давлении. Кстати, когда ставил датчик в режиме логгера, то барометрическая кривая отрисовалась плавно, а вот температурная изменялась рывками с горизонтальными полочками.
    [​IMG]

    И напоследок, измерительная сборка из Амперкиного термистора и резистора на 10кОм
    [​IMG]

    Для термисторной сборки справедливо все тоже самое - максимальная точность достигается уже на 30 замерах, хотя тут можно не скупиться, скорость получения данных в этом случае ограничена только быстродействием самой ардуино.
     
  10. Unixon

    Unixon Оракул

    А если отложить по оси не количество измерений, а [еще и] время?
    Как будет выглядеть двумерный график Stand/Rel/Max~Deviation(measurement_delay, samples_count) ?
     
  11. donpavlensio

    donpavlensio Нерд

    Зависимость между размером выборки для осреднения и временем, затрачиваемым на замер линейная, как ей и положено быть. Поэтому добавление времени на график не даст чего-то нового, но т.к. порядки величин разные, то область построения растянется так, что погрешности "свалятся" к нулю и картина станет абсолютно ненаглядной. Разве что отдельный график строить время/погрешность, но принципиально он, очевидно, ничем не будет отличаться от приведенного.
     
  12. Unixon

    Unixon Оракул

    Вопрос в другом. Как зависит погрешность для выборки размером N от интервала времени между измерениями?
     
  13. donpavlensio

    donpavlensio Нерд

    Да, интересный вопрос. Зависимость точно будет.
    Я поначалу написал скетч под другой вариант расчета стандартного отклонения, который используется при проведении внутреннего контроля в лабораториях - там пробы выборочно анализируются повторно и считается все через разницу между основным (рядовым) и контрольным замерами. Только контроль обычно производят отдельно, а не тут же, на месте. А я сделал по схеме два замера - получение разности, переход к следующему. Вот, кусок старого кода, если интересно

    Код (Text):
    for (int i = 1; i <= 256; i *= 2){    // Значение i задает число осреднений на замер (см. ниже), а количество значений, принимаемых i - число сессий
      float difSqsumm = 0, Summ = 0;
      unsigned long MeanDelay = 0;
      for(int j = 1; j <= 30; ++j){      // Этот счетчик задает число замеров для каждой сессии
        float z1=0, z2=0;
        unsigned long startMillis = millis(); //считываем момент начала замера
        for (int n = 1; n <=i; n++){  // n в каждой сессии может расти только до значения i, таким образом, для каждого замера давление будет определено i раз
          dps.getPressure(&val);  //Получаем давление с барометра
          z1 = z1+val;    //суммируем определенные значения с датчика
       
       
          dps.getPressure(&val);
          z2 = z2+val;
       
       
        //  z1 = z1 + analogRead(SENSOR_PIN); //Раскомментируйте строку для использования с аналоговым сенсором

        }
        z1 = z1/i;  //осредняем их делением на i;
        z2 = z2/i;
     
        z1 = z1/133.32; // деление на 133.32 производится для перевода давления из паскалей в мм.рт.ст, для других сенсоров надо закомментировать строку
        z2 = z2/133.32;
        //float difSq = pow((z1-z2),2); //Находим квадратный корень разности
         
        unsigned long del = millis() - startMillis; // вычисляем время, прошедшее с начала измерения
        MeanDelay = MeanDelay + del;
     
        float diff = z1-z2;
        difSqsumm = difSqsumm + pow((z1-z2),2); //Находим сумму квадратов разностей
        Summ = Summ + (z1+z2); // Находим сумму всех членов

      }

    float StandDev = pow(difSqsumm/60, 0.5); // Находим стандартное отклонение (хе-хе, я не знал о функции sqrt(x,y) поэтому пользовался первым, что нашел - возведением в степень)
     

    То есть, у меня каждый раз сравнивались два рядом стоящих измерения, которые, очевидно, будут ближе друг к другу, нежели два измерения, разделенные значительным временным интервалом. Поэтому погрешность оказалась очень сильно заниженной. Зато какие красивые графики получались! Жалко лишь, что неправильные
    [​IMG]

    В общем, проверить такую зависимость вполне себе возможно - добавить delay() после каждого замера и завязать уже его величину на счетчик вместо числа замеров на осреднение. Только тут есть одна тонкость - измеряемая величина не должна изменяться за во время измерений, а давление имеет совершенно противоположную склонность. На секундных задержках между измерениями это не ощутимо, а вот на минутных может уже сказаться. Поэтому надо параллельно ставить барограф, чтобы он отслеживал тенденцию. Возможно, сделаю на днях.