Это внеплановый урок. Он посвящен работе АЦП Ардуино в фоновом режиме. Представлена моя библиотека, как альтернатива встроенной функции analogRead().
Предыдущий урок Список уроков Следующий урок
На мой взгляд, самой неэффективной операцией Ардуино является аналогово-цифровое преобразование под управлением встроенной функции analogRead(). Ничего так бесполезно не пожирает вычислительные ресурсы как она. Работает функция так:
- задает АЦП нужный режим ;
- запускает процесс преобразования;
- ждет, пока преобразование закончится;
- считывает результат.
АЦП это аппаратный узел. Он работает сам по себе. Программе надо только запустить процесс и можно выполнять другие задачи. Но функция analogRead() подвешивает программу на время преобразования. А это более 100 мкс. Бесконечное время!
Положение усугубляется тем, что обычно аналогово-цифровые преобразования происходят регулярно, с постоянной частотой. Для этого необходимо вызывать функцию analogRead() в обработчике прерывания от таймера. Т.е. приходится подвешивать программу в обработчике прерывания. А это удар по всему: снижается производительность микроконтроллера, блокируются другие прерывания, становятся непредсказуемыми длительности выходных сигналов и т.п.
Я давно понял, что придется разрабатывать свои функции работы с АЦП. Сейчас сделать это заставила чисто практическая задача.
Надеюсь, вы заметили на сайте новую рубрику ”Умный дом”. В ней я поэтапно разрабатываю систему управления “Умный дом”. Система сложная, разнообразная, интересная. Я представляю ее как дополнение к урокам Ардуино.
Так вот сейчас у меня этап разработки программы контроллера водоснабжения. В нем используется 7 каналов АЦП. Структура программы построена по принципу параллельной обработки задач. Реализовано прерывание от аппаратного таймера с периодом 500 мкс и синхронно с ним выполняются различные задачи.
И вот я добавляю в обработчик прерывания стандартную функцию чтения АЦП. Она выполняется за 106 – 114 мкс. Каждые 500 мкс программа висит более 100 мкс! Свыше20 % машинного времени коту под хвост! И в это время будут блокированы все остальные прерывания! А в обработчике прерывания еще много чего надо делать.
Проблематично использовать встроенную функцию analogRead() для серьезных задач. Поэтому я написал свою библиотеку, которая имеет отдельные функции для запуска преобразования АЦП и чтения результата. Мы запускаем АЦП и занимаемся другими задачами. Приходит время – считываем результат.
Библиотека BackgroundADC.h.
Это библиотека для работы с АЦП Ардуино в фоновом режиме. Разрабатывалась для плат с микроконтроллерами ATmega 168/328. Как будет работать с другими контроллерами – не знаю. Загрузить ее можно по ссылке:
Библиотека сама создает экземпляр класса с именем 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 В.
Вот скетч резидентной программы вольтметра:
// 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 измеренными значениями напряжений.
В программе есть регистратор, который отображает изменения значений каналов в реальном времени.
Это я крутил по очереди подстроечные резисторы от минимума до максимума.
Загрузить программу можно по ссылке:
Примеры практического применения библиотеки BackgroundADC есть в главе 5 рубрики ”Умный дом”.
Надеюсь, в следующем уроке вернуться к программному обеспечению сети Ethernet. Если не ошибаюсь, то на очереди UDP и HTTP серверы и клиенты.
Здраствуйте.
Мне кажется в програме нет начального BackgroundADC.analogStart(chanelADC). В первый раз срабатывания обработчик сразу начинает со считывания результата АЦП, или я не прав?
Здравствуйте!
Да. Первое данное будет не верное. Если это критично, то надо перед тем как использовать данные дать программе поработать определенное время. В этом случае 80 мс.
А задать его в разделе «void setup() {» нельзя?
Можно, но зачем. В любом случае подобные алгоритмы с фильтрацией, усреднением требуют определенное время для того, чтобы данные стали достоверными.
Здравствуйте, Эдуард.
А какой практический смысл Вы заложили в «t++» и дальнейший «if( (t & 0x3) == 0)»?
Ведь получается выдача в serial каждого четвертого пакета готовых данных, т.е. снятие показаний раз в ~320мс?
Здравствуйте!
Все правильно. Этот блок для того, чтобы данные выводились в последовательный порт не слишком часто. Чтобы визуально было легче воспринимать.
Привет,
Установлена IDE Arduino 1.0.4
Ошибка при компиляции
undefined reference to `BackgroundADC’
Как решить проблем?
Здравствуйте!
А вы библиотеку BackgroundADC подключили?
Да,как обычно, распаковал zip файл и поместил его в папку librares.
Потом запустил IDE и скопировал текст в скетч, при компиляции ругается
Не знаю. У меня все работает, у других тоже. У меня версия Arduino 1.8.1.
Здравствуйте, Эдуард
А с модулями на интерфейсе SPI подобная обработка возможна или нет?
Здравствуйте!
Да, конечно. Все точно так же, только запуск и чтение происходит через внешний интерфейс.
Уточню вопрос.
ADC работает автономно и позволяет работать программе во время формирования данных на своем выходе.
А SPI также?
Имею 4 датчика Pt100 на модулях MAX31865. Считывание с них данных «в лоб в основном цикле» и усреднения по 10 выборок с каждого (библиотека Adafruit) показывает 4.2 сек. Ни в какие ворота не лезет такая скорость. Хочу перекинуть в прерывание.
Конечно. Можно в основном цикле периодически проверять готовность данных АЦП.
Спасибо.
Вроде получилось сократить время одного измерения со 100 мсек до 150 мксек (использовал подход к решению проблемы из этого урока). Много, но уже легче.
А есть ли возможность измерить, сколько времени отъедают у программы все процессы, сидящие в прерывании (у меня там 10 кнопок, энкодер и 5 аналоговых входов, сделанные на основе Ваших уроков)?
Они хоть и разбросаны там по тактам, но всё же. Теперь еще и 4 термометра надо запихнуть.
На форуме скачал Вашу программу для измерений, но она блокирует прерывания.
Приходится копировать куски из прерывания, вставлять в loop и мерить. Не совсем удобно.
А за уроки Большое спасибо. Обучаюсь на живом устройстве. Хорошо ускоряют процесс работы.
Здравствуйте!
Время можно измерить если в начале прерывания установить выход микроконтроллера, например в 1, а при выходе из прерывания — в 0. И посмотреть осциллографом.
Добрый день!
Если воспользоваться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 мкс
Я всегда усредняю несколько значений АЦП дабы избежать ошибки от помех.
на версии 1.6.4 ругается, на версии 1.8.4 скомпилировалось.
Добрый день, Эдуард!
Хочу применить библиотеку BackgroundADC для плат с микроконтроллерами MassDuino (MD-328D). Подскажите или дайте ссылку, какие в них используются регистры для управления АЦП?
Здравствуйте!
Понятия не имею. У меня нет документации по регистрам микроконтроллера.
«Чтобы превратить проект вольтметра в практичное устройство я написал программу верхнего уровня.»
Вопрос: Скажите пожалуйста, «программа верхнего уровня» в какой ОС и на каком языке? Это Вы ловите из компорта и распоряжаетесь ими?
Здравствуйте!
Да. Данные циклически поступают в последовательный порт. Программа верхнего уровня принимает их и выводит их на экран компьютера.
Эдуард, здравствуйте. Подскажите, могу ли я с помощью Вашей библиотеки организовать 14-канальный вольтметр на Ардуино Мега2560?
Здравствуйте!
Думаю, что нет. Библиотека работает с регистрами ATmega328. У микроконтроллера Mega 2560 другие регистры.
Здравствуйте, Эдуард. Поясните, пожалуйста, для чего в скетче четырехканального вольтметра в loop() при считывании averageADC[i] запрещаются прерывания?
Здравствуйте!
Данные этого массива загружаются в прерывании по таймеру. Данные типа int, для 8ми разрядного микроконтроллера это 2 байта. И работает он с ними по байтам.
Представьте, что в основном цикле вы считываете элемент массива averageADC[i]. Микроконтроллер считал первый байт и тут вызвалось прерывание по таймеру. Работа программы прервалась, элемент массива, который вы считываете, загрузился новыми данными. После отработки прерывания программа вернулась на основной цикл и продолжила чтение элемента массива, т.е. считала 2й байт. В результате вы считали первый байт одного данного и второй байт следующего данного. Для предотвращения таких ситуаций я запрещаю прерывания на время чтения данного из averageADC[i].
Эдуард, получается, что нужно запрещать прерывания при считывании в основном цикле переменных и из других процедур, если разрядность переменных больше 8 разрядов?
Только тех, которые могут быть изменены в параллельных процессах.
Спасибо!
эдуард похоже эту программу можно использовать как 4 канальный осцилограф. спасибо за уроки
Здравствуйте!
Осциллограф для очень медленных процессов.
Насколько медленных? Точнее, какой получается период полного опроса всех каналов?
Здравствуйте! Скажите, а сможет ли эта программа обработать заодно и дребезг контактов тумблера, который через делитель напряжения пойдет на аналоговый вход А0? (Тумблер — это у меня переключатель режима дворников автомобиля; и представляет из себя 4-ех позиционный переключатель общего контакта на другие 4-е контакта, к которым припаяны резисторы; на выходе 2 провода, сопротивление между которыми меняется в зависимости от положения тумблера).
Здравствуйте!
Усреднение значения аналогового сигнала — это не устранение дребезга. Нужен еще гистерезис. Но, в вашем случае в устранении дребезга нет необходимости. У вас не тактовая кнопка. Вы переключаете режимы. Чем вам дребезг может помешать.
Спасибо! Я видно не уточнил, что этот переключатель режимов будет выполнять другую функцию (не управление дворниками). В зависимости от считанного программой напряжения «х» четыре условных оператора должны будут присваивать переменной «у» разные значения. Например: if (1.<х<=2. ) у=100; if (2.<х<=3. ) у=200; if (3.<х<=4. ) у=300; if (4.<х<=5. ) у=500; Переменная "у" потом будет участвовать в дальнейшей обработке программой. Дребезг может помешать условному оператору в определении условия? Дребезг, как я понимаю, в момент переключения многократно "теряет" сигнал до нуля.
Здравствуйте!
Попробуйте. У вас все равно будет еще аналоговый фильтр в виде конденсатора на аналоговом входе.
Здравствуйте, Эдуард. А в какой момент boolean analogCheck() становится false?
Здравствуйте!
Когда запускается преобразование АЦП. Эта функция анализирует аппаратный флаг АЦП.
boolean BackgroundADC_Class::analogCheck() {
if ( (ADCSRA & 0x40) == 0 ) return(true);
return(false);
}
Здравствуйте. Помогите пожалуйста переделать код для использования последних трех каналов с 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го по 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] и проверить, правильно ли происходит измерение в коде.
Большое спасибо, Эдуард! Всё получилось. Проблема была именно в правильном опросе входов. Оказалось все очень просто, нужно было прописать chanelADC + 5. Переменные sumADC[chanelADC], averageADC[0] у меня uint32_t, averageCounter uint16_t.
Рад, что помог.