Урок 65. Аналогово-цифровые преобразования Ардуино в фоновом режиме. Библиотека BackgroundADC.

Аналогово-цифровое преобразование Ардуино

Это внеплановый урок. Он посвящен работе АЦП Ардуино в фоновом режиме. Представлена моя библиотека, как альтернатива встроенной функции analogRead().

Предыдущий урок     Список уроков     Следующий урок

На мой взгляд, самой неэффективной операцией Ардуино является аналогово-цифровое преобразование под управлением встроенной функции analogRead(). Ничего так бесполезно не пожирает вычислительные ресурсы как она. Работает функция так:

  • задает АЦП нужный режим ;
  • запускает процесс преобразования;
  • ждет, пока преобразование закончится;
  • считывает результат.

АЦП это аппаратный узел. Он работает сам по себе. Программе надо только запустить процесс и можно выполнять другие задачи. Но функция analogRead() подвешивает программу на время преобразования. А это более 100 мкс. Бесконечное время!

 

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

Я давно понял, что придется разрабатывать свои функции работы с АЦП. Сейчас сделать это заставила чисто практическая задача.

Надеюсь, вы заметили на сайте новую рубрику ”Умный дом”. В ней я поэтапно разрабатываю систему управления “Умный дом”. Система сложная, разнообразная, интересная. Я представляю ее как дополнение к урокам Ардуино.

Так вот сейчас у меня этап разработки программы контроллера водоснабжения. В нем используется 7 каналов АЦП. Структура программы построена по принципу параллельной обработки задач. Реализовано прерывание от аппаратного таймера с периодом 500 мкс и синхронно с ним выполняются различные задачи.

И вот я добавляю в обработчик прерывания стандартную функцию чтения АЦП. Она выполняется за 106 – 114 мкс. Каждые 500 мкс программа висит более 100 мкс! Свыше20 % машинного времени коту под хвост! И в это время будут блокированы все остальные прерывания! А в обработчике прерывания еще много чего надо делать.

Проблематично использовать встроенную функцию analogRead() для серьезных задач. Поэтому я написал свою библиотеку, которая имеет отдельные функции для запуска преобразования АЦП и чтения результата. Мы запускаем АЦП и занимаемся другими задачами. Приходит время – считываем результат.

 

Библиотека BackgroundADC.h.

Это библиотека для работы с АЦП Ардуино в фоновом режиме. Разрабатывалась для плат с микроконтроллерами ATmega 168/328. Как будет работать с другими контроллерами – не знаю. Загрузить ее можно по ссылке:

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!

Библиотека сама создает экземпляр класса с именем BackgroundADC. Как для Serial мы не создаем экземпляр класса, а просто вызываем нужную функцию, например, Serial.begin(). Также происходит вызов функции для BackgroundADC, например, BackgroundADC.analogRead.

Теперь собственно о функциях.

 

void analogReference(byte mode)

Подобно одноименной встроенной функции, она задает опорное напряжение для АЦП. Параметр может иметь общепринятые для Ардуино значения:

  • DEFAULT – в качестве источника опорного напряжения используется напряжение питания микроконтроллера. Обычно это 5 или 3,3 В.
  • INTERNAL – используется внутреннее опорное напряжение микроконтроллера. Для контроллеров ATmega 168/328 это 1,1 В.
  • EXTERNAL – к АЦП подключен внешний источник опорного напряжения через вход AREF.

BackgroundADC.analogReference(INTERNAL);  // ИОН 1,1 В

 

void analogStart(byte pin)

Функция запускает преобразование АЦП для заданного входа.

BackgroundADC.analogStart(A0); // пуск АЦП  для канала 0

 

boolean analogCheck()

Функция позволяет узнать, закончилось ли преобразование. В случае, когда операция завершена, возвращает true.

if( BackgroundADC.analogCheck() == true ) {
  // результат готов
}

 

unsigned int analogRead()

Читает результат преобразования. Должна вызваться после завершения преобразования.

resADC = BackgroundADC.analogRead();  // чтение АЦП

 

Последовательность работы с АЦП следующая:

  • запустить преобразование функцией analogStart();
  • выждать заданное время или убедиться функцией analogCheck(), что преобразование завершилось;
  • считать результат функцией analogRead().

Простейшая программа чтения АЦП выглядит так:

// проверка библиотеки BackgroundADC
#include <BackgroundADC.h>

void setup() {
  Serial.begin(9600);
}

void loop() {
  BackgroundADC.analogStart(A0); // запуск АЦП
  while( BackgroundADC.analogCheck() == false ); // ожидание окончания преобразования
  Serial.println(BackgroundADC.analogRead()); // чтение АЦП и вывод в UART

  delay(500);
}

Эта программа демонстрационная. Она зависает в ожидании результата преобразования и не использует преимущества фоновой работы АЦП. Результат ее работы считанные коды АЦП.

Считанные коды

Следующая программа запускает преобразование АЦП в прерывании по таймеру и тут же возвращается в основной цикл. Через 500 мкс она считывает результат и снова запускает АЦП. Таким образом, на обслуживание АЦП тратится минимум времени: только запуск и чтение.

// вольтметр, АЦП работает в фоновом режиме
#include <BackgroundADC.h>
#include <TimerOne.h>

unsigned int analogValue; // значение АЦП

void setup() {
  Serial.begin(9600);
  Timer1.initialize(500); // инициализация таймера 1, период 500 мкс
  Timer1.attachInterrupt(timerInterrupt, 500); // обработчик прерываний
}

void loop() {
  Serial.println(analogValue);
  delay(500);
}

//-------------------------------------- обработчик прерывания 500 мкс
void timerInterrupt() {
  analogValue = BackgroundADC.analogRead(); // чтение АЦП
  BackgroundADC.analogStart(A0); // пуск АЦП
}

Завершено ли преобразование АЦП в этой программе не проверяется, т.к. за 500 мкс операция гарантированно будет выполнена.

 

Вольтметр на 4 канала с усреднением значений.

В качестве примера давайте разработаем 4 канальный вольтметр, который будет усреднять измеренные значения. Несмотря на обилие аналогово-цифровых преобразований постараемся добиться минимальной временной нагрузки на микроконтроллер.

К четырем аналоговым входам платы A0 – A3 я подключил средние выводы подстроечных резисторов. Остальные выводы припаял к сигналам земля и 5 В. Таким образом на каждом из 4 входов я могу задавать любое напряжение в пределах 0…5 В.

Макет для проверки библиотеки

Вот скетч резидентной программы вольтметра:

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!

// 4 канальный вольтметр с усреднением 80 мс
#include <BackgroundADC.h>
#include <TimerOne.h>

#define AVERAGE_NUM 40 // число усреднений АЦП (* 2 мс)

unsigned int sumADC[4]; // переменные для усреднения АЦП
unsigned int averageADC[4]; // результат усреднения АЦП
byte chanelADC=0; // номер канала АЦП
byte averageCounter=0; // счетчик усреднения АЦП
boolean readyADC= false; // признак данные готовы
unsigned int x;
byte t=0;

void setup() {
  Serial.begin(9600);
  Timer1.initialize(500); // инициализация таймера 1, период 500 мкс
  Timer1.attachInterrupt(timerInterrupt, 500); // обработчик прерываний
}

void loop() {
  if( readyADC == true ) {
  readyADC= false;
  t++;
  if( (t & 0x3) == 0 ) {
    // вывод значений АЦП
    for( byte i=0; i<4; i++ ) {
      Serial.print(" ");
      noInterrupts();
      x= averageADC[i];
      interrupts();
      Serial.print( (float)x * 0.0001220703, 2);
      // 0.0001220703 = 5 / 1024 / 40
    }
    Serial.println("");
    }
  }
}

//-------------------------------------- обработчик прерывания 500 мкс
void timerInterrupt() {
  sumADC[chanelADC] += BackgroundADC.analogRead(); // суммирование кодов АЦП
  chanelADC++; // следующий канал
  if( chanelADC > 3 ) chanelADC = 0;
    BackgroundADC.analogStart(chanelADC); // пуск АЦП

    if( chanelADC == 0 ) {
      averageCounter++;
      if( averageCounter >= AVERAGE_NUM ) {
        averageCounter = 0;
        averageADC[0]= sumADC[0];
        averageADC[1]= sumADC[1];
        averageADC[2]= sumADC[2];
        averageADC[3]= sumADC[3];
        sumADC[0]= 0;
        sumADC[1]= 0;
        sumADC[2]= 0;
        sumADC[3]= 0;
        readyADC= true;
    }
  }
}

Я не знаю, что в нем пояснять.

  • В прерывании по таймеру 500 мкс считывается результат преобразования АЦП и запускается новая операция.
  • Результаты для каждого канала суммируются в sumADC[] и перегружаются в averageADC[].
  • По окончанию цикла усреднения (80 мс) становится активным признак readyADC.
  • По этому признаку в основном цикле loop() данные пересчитываются в вольты и выводятся в последовательный порт.

Результаты измерений

Чтобы превратить проект вольтметра в практичное устройство я написал программу верхнего уровня.

В ней окно с 4 измеренными значениями напряжений.

Окно программы Voltmeter

В программе есть регистратор, который отображает изменения значений каналов в реальном времени.

Регистратор

Это я крутил по очереди подстроечные резисторы от минимума до максимума.

Загрузить программу можно по ссылке:

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!

Примеры практического применения библиотеки BackgroundADC есть в главе 5 рубрики ”Умный дом”.

 

Надеюсь, в следующем уроке вернуться к программному обеспечению сети Ethernet. Если не ошибаюсь, то на очереди UDP и HTTP серверы и клиенты.

 

Предыдущий урок     Список уроков     Следующий урок

0

Автор публикации

не в сети 2 месяца

Эдуард

282
Комментарии: 1940Публикации: 197Регистрация: 13-12-2015

42 комментария на «Урок 65. Аналогово-цифровые преобразования Ардуино в фоновом режиме. Библиотека BackgroundADC.»

  1. Здраствуйте.
    Мне кажется в програме нет начального BackgroundADC.analogStart(chanelADC). В первый раз срабатывания обработчик сразу начинает со считывания результата АЦП, или я не прав?

    0
    • Здравствуйте!
      Да. Первое данное будет не верное. Если это критично, то надо перед тем как использовать данные дать программе поработать определенное время. В этом случае 80 мс.

      0
    • Можно, но зачем. В любом случае подобные алгоритмы с фильтрацией, усреднением требуют определенное время для того, чтобы данные стали достоверными.

      0
  2. Здравствуйте, Эдуард.

    А какой практический смысл Вы заложили в «t++» и дальнейший «if( (t & 0x3) == 0)»?
    Ведь получается выдача в serial каждого четвертого пакета готовых данных, т.е. снятие показаний раз в ~320мс?

    0
    • Здравствуйте!
      Все правильно. Этот блок для того, чтобы данные выводились в последовательный порт не слишком часто. Чтобы визуально было легче воспринимать.

      0
  3. Привет,
    Установлена IDE Arduino 1.0.4
    Ошибка при компиляции
    undefined reference to `BackgroundADC’
    Как решить проблем?

    0
  4. Да,как обычно, распаковал zip файл и поместил его в папку librares.
    Потом запустил IDE и скопировал текст в скетч, при компиляции ругается

    0
  5. Здравствуйте, Эдуард
    А с модулями на интерфейсе SPI подобная обработка возможна или нет?

    0
      • Уточню вопрос.
        ADC работает автономно и позволяет работать программе во время формирования данных на своем выходе.
        А SPI также?
        Имею 4 датчика Pt100 на модулях MAX31865. Считывание с них данных «в лоб в основном цикле» и усреднения по 10 выборок с каждого (библиотека Adafruit) показывает 4.2 сек. Ни в какие ворота не лезет такая скорость. Хочу перекинуть в прерывание.

        0
          • Спасибо.
            Вроде получилось сократить время одного измерения со 100 мсек до 150 мксек (использовал подход к решению проблемы из этого урока). Много, но уже легче.
            А есть ли возможность измерить, сколько времени отъедают у программы все процессы, сидящие в прерывании (у меня там 10 кнопок, энкодер и 5 аналоговых входов, сделанные на основе Ваших уроков)?
            Они хоть и разбросаны там по тактам, но всё же. Теперь еще и 4 термометра надо запихнуть.
            На форуме скачал Вашу программу для измерений, но она блокирует прерывания.
            Приходится копировать куски из прерывания, вставлять в loop и мерить. Не совсем удобно.
            А за уроки Большое спасибо. Обучаюсь на живом устройстве. Хорошо ускоряют процесс работы.

            0
          • Здравствуйте!
            Время можно измерить если в начале прерывания установить выход микроконтроллера, например в 1, а при выходе из прерывания — в 0. И посмотреть осциллографом.

            0
  6. Добрый день!
    Если воспользоватьсяhttp://codius.ru/articles/Arduino_%D1%83%D1%81%D0%BA%D0%BE%D1%80%D1%8F%D0%B5%D0%BC_%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D1%83_%D0%BF%D0%BB%D0%B0%D1%82%D1%8B_%D0%A7%D0%B0%D1%81%D1%82%D1%8C_2_%D0%90%D0%BD%D0%B0%D0%BB%D0%BE%D0%B3%D0%BE_%D1%86%D0%B8%D1%84%D1%80%D0%BE%D0%B2%D0%BE%D0%B9_%D0%BF%D1%80%D0%B5%D0%BE%D0%B1%D1%80%D0%B0%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C_%D0%90%D0%A6%D0%9F_%D0%B8_analogRead
    то можно уменьшить время работы АЦП до 16 мкс
    Я всегда усредняю несколько значений АЦП дабы избежать ошибки от помех.

    0
  7. Добрый день, Эдуард!
    Хочу применить библиотеку BackgroundADC для плат с микроконтроллерами MassDuino (MD-328D). Подскажите или дайте ссылку, какие в них используются регистры для управления АЦП?

    0
  8. «Чтобы превратить проект вольтметра в практичное устройство я написал программу верхнего уровня.»

    Вопрос: Скажите пожалуйста, «программа верхнего уровня» в какой ОС и на каком языке? Это Вы ловите из компорта и распоряжаетесь ими?

    0
    • Здравствуйте!
      Да. Данные циклически поступают в последовательный порт. Программа верхнего уровня принимает их и выводит их на экран компьютера.

      0
  9. Эдуард, здравствуйте. Подскажите, могу ли я с помощью Вашей библиотеки организовать 14-канальный вольтметр на Ардуино Мега2560?

    0
    • Здравствуйте!
      Думаю, что нет. Библиотека работает с регистрами ATmega328. У микроконтроллера Mega 2560 другие регистры.

      0
  10. Здравствуйте, Эдуард. Поясните, пожалуйста, для чего в скетче четырехканального вольтметра в loop() при считывании averageADC[i] запрещаются прерывания?

    0
    • Здравствуйте!
      Данные этого массива загружаются в прерывании по таймеру. Данные типа int, для 8ми разрядного микроконтроллера это 2 байта. И работает он с ними по байтам.
      Представьте, что в основном цикле вы считываете элемент массива averageADC[i]. Микроконтроллер считал первый байт и тут вызвалось прерывание по таймеру. Работа программы прервалась, элемент массива, который вы считываете, загрузился новыми данными. После отработки прерывания программа вернулась на основной цикл и продолжила чтение элемента массива, т.е. считала 2й байт. В результате вы считали первый байт одного данного и второй байт следующего данного. Для предотвращения таких ситуаций я запрещаю прерывания на время чтения данного из averageADC[i].

      0
  11. эдуард похоже эту программу можно использовать как 4 канальный осцилограф. спасибо за уроки

    0
  12. Здравствуйте! Скажите, а сможет ли эта программа обработать заодно и дребезг контактов тумблера, который через делитель напряжения пойдет на аналоговый вход А0? (Тумблер — это у меня переключатель режима дворников автомобиля; и представляет из себя 4-ех позиционный переключатель общего контакта на другие 4-е контакта, к которым припаяны резисторы; на выходе 2 провода, сопротивление между которыми меняется в зависимости от положения тумблера).

    0
    • Здравствуйте!
      Усреднение значения аналогового сигнала — это не устранение дребезга. Нужен еще гистерезис. Но, в вашем случае в устранении дребезга нет необходимости. У вас не тактовая кнопка. Вы переключаете режимы. Чем вам дребезг может помешать.

      0
      • Спасибо! Я видно не уточнил, что этот переключатель режимов будет выполнять другую функцию (не управление дворниками). В зависимости от считанного программой напряжения «х» четыре условных оператора должны будут присваивать переменной «у» разные значения. Например: if (1.<х<=2. ) у=100; if (2.<х<=3. ) у=200; if (3.<х<=4. ) у=300; if (4.<х<=5. ) у=500; Переменная "у" потом будет участвовать в дальнейшей обработке программой. Дребезг может помешать условному оператору в определении условия? Дребезг, как я понимаю, в момент переключения многократно "теряет" сигнал до нуля.

        0
        • Здравствуйте!
          Попробуйте. У вас все равно будет еще аналоговый фильтр в виде конденсатора на аналоговом входе.

          0
    • Здравствуйте!
      Когда запускается преобразование АЦП. Эта функция анализирует аппаратный флаг АЦП.
      boolean BackgroundADC_Class::analogCheck() {
      if ( (ADCSRA & 0x40) == 0 ) return(true);
      return(false);
      }

      0
  13. Здравствуйте. Помогите пожалуйста переделать код для использования последних трех каналов с 5 по 7. С 0 по 2 все работает правильно, а на последних никак не могу разобраться. По одному все три канала работают, но так же оформить код, как в примере с 4-канальным вольтметром, не могу.
    Рабочий код в прерывании:
    sumADC[chanelADC] += BackgroundADC.analogRead(); // суммирование кодов АЦП
    chanelADC++; // следующий канал
    if ( chanelADC > 2 ) chanelADC = 0;
    BackgroundADC.analogStart(chanelADC); // пуск АЦП

    if ( chanelADC == 0 ) {
    averageCounter++; // +1 счетчик выборок усреднения
    if ( averageCounter >= SAMPLE_AVERAGE ) { // проверка числа выборок усреднения
    averageCounter = 0;
    averageADC[0] = (sumADC[0] >> 9); // перегрузка среднего значения (вместо деления используется сдвиг вправо на степень двойки 2^9 = 512)
    averageADC[1] = (sumADC[1] >> 9); // перегрузка среднего значения (вместо деления используется сдвиг вправо на степень двойки 2^9 = 512)
    averageADC[2] = (sumADC[2] >> 9); // перегрузка среднего значения (вместо деления используется сдвиг вправо на степень двойки 2^9 = 512)
    sumADC[0] = 0;
    sumADC[1] = 0;
    sumADC[2] = 0;
    readyADC = true;
    }
    }

    Часть цикла loop:
    if ( readyADC == true ) { // данные готовы
    readyADC = false;
    // вычисление температуры
    MsTimer2::stop();
    temperature1 = R_RESIST / ((float)1023 / averageADC[0] — 1);
    MsTimer2::start();
    temperature1 /= (float)T_RESIST; // (R/Ro)
    temperature1 = log(temperature1) / B; // 1/B * ln(R/Ro)
    temperature1 += 1.0 / (NOMINAL_T + 273.15); // 0.0033540164; // + (1/To)
    temperature1 = 1.0 / temperature1 — 273.15; // Invert
    temperature1 = temperature1 + (correct1 / 10);

    MsTimer2::stop();
    temperature2 = R_RESIST / ((float)1023 / averageADC[1] — 1);
    MsTimer2::start();
    temperature2 /= (float)T_RESIST; // (R/Ro)
    temperature2 = log(temperature2) / B; // 1/B * ln(R/Ro)
    temperature2 += 1.0 / (NOMINAL_T + 273.15); // 0.0033540164; // + (1/To)
    temperature2 = 1.0 / temperature2 — 273.15; // Invert
    temperature2 = temperature2 + (correct2 / 10);

    MsTimer2::stop();
    temperature3 = R_RESIST / ((float)1023 / averageADC[2] — 1);
    MsTimer2::start();
    temperature3 /= (float)T_RESIST; // (R/Ro)
    temperature3 = log(temperature3) / B; // 1/B * ln(R/Ro)
    temperature3 += 1.0 / (NOMINAL_T + 273.15); // 0.0033540164; // + (1/To)
    temperature3 = 1.0 / temperature3 — 273.15; // Invert
    temperature3 = temperature3 + (correct3 / 10);
    /*Serial.print(«averageADCA5 = «); Serial.print(averageADCA5);
    Serial.print(» averageADCA6 = «); Serial.print(averageADCA6);
    Serial.print(» averageADCA7 = «); Serial.println(averageADCA7);*/
    }

    На каналах с 5 по 7 у меня заработало только при использовании конструкции:
    BackgroundADC.analogStart(A0); // запуск АЦП
    while( BackgroundADC.analogCheck() == false ); // ожидание окончания преобразования
    Serial.println(BackgroundADC.analogRead()); // чтение АЦП и вывод в UART

    Но тогда размер прошивки больше чем при использовании штатного analogRead().

    0
    • Здравствуйте!
      Вы запускаете с 0го по 2й каналы. Если надо запускать с 5го по 7й, то:
      chanelADC++; // следующий канал
      if ( chanelADC > 2 ) chanelADC = 0;
      BackgroundADC.analogStart(chanelADC + 5); // пуск АЦП

      Вы не прислали объявление переменных. Возможно, у вас еще в этом ошибка. Если используете 512 выборок для усреднения, то переменные sumADC[chanelADC], averageADC[0] должны иметь минимальную разрядность 10 бит (АЦП) + 9 бит (усреднение) 19 разрядов. Если вы не используете тип long для этих переменных, то правильно работать не будет. У averageCounter должен быть тип unsigned int. Byte недостаточно.
      Дальше программу не смотрел, но вы можете вывести в порт averageADC[0] и проверить, правильно ли происходит измерение в коде.

      0
      • Большое спасибо, Эдуард! Всё получилось. Проблема была именно в правильном опросе входов. Оказалось все очень просто, нужно было прописать chanelADC + 5. Переменные sumADC[chanelADC], averageADC[0] у меня uint32_t, averageCounter uint16_t.

        0

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Нажимая кнопку "Отправить" Вы даёте свое согласие на обработку введенной персональной информации в соответствии с Федеральным Законом №152-ФЗ от 27.07.2006 "О персональных данных".