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

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

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

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

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

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

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

 

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

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

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

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

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

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

 

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

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

Библиотека сама создает экземпляр класса с именем 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 В.

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

Вот скетч резидентной программы вольтметра (sketch_65_3.zip).

// 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

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

Регистратор

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

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

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

 

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

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

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

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

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

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

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

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

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

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

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

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

    • Здравствуйте!
      Да, конечно. Все точно так же, только запуск и чтение происходит через внешний интерфейс.

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

        • Конечно. Можно в основном цикле периодически проверять готовность данных АЦП.

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

          • Здравствуйте!
            Время можно измерить если в начале прерывания установить выход микроконтроллера, например в 1, а при выходе из прерывания — в 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 мкс
    Я всегда усредняю несколько значений АЦП дабы избежать ошибки от помех.

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

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