Урок 13
|
Предыдущий урок | Ардуино - главная | Следуюший урок |
В уроке научимся работать с аналоговыми входами Ардуино.
Аналоговые входы платы Ардуино.
Плата Arduino UNO содержит 6 аналоговых входов предназначенных для измерения напряжения сигналов. Правильнее сказать, что 6 выводов платы могут работать в режиме, как дискретных выводов, так и аналоговых входов.
Эти выводы имеют номера от 14 до 19. Изначально они настроены как аналоговые входы, и обращение к ним можно производить через имена A0-A5. В любой момент их можно настроить на режим дискретных выходов.
pinMode(A3, OUTPUT); // установка режима дискретного вывода для A3
digitalWrite(A3, LOW); //- установка низкого состояния на выходе A3
Чтобы вернуть в режим аналогового входа:
pinMode(A3, INPUT); //- установка режима аналогового входа для A3
Аналоговые входы и подтягивающие резисторы.
К выводам аналоговых входов, так же как и к дискретным выводам, подключены подтягивающие резисторы. Включение этих резисторов производится командой
digitalWrite(A3, HIGH); //- включить подтягивающий резистор к входу A3
Команду необходимо применять к выводу настроенному в режиме входа.
Надо помнить, что резистор может оказать влияние на уровень входного аналогового сигнала. Ток от источника питания 5 В, через подтягивающий резистор, вызовет падение напряжения на внутреннем сопротивлении источника сигнала. Так что лучше резистор отключать.
Аналого-цифровой преобразователь платы Ардуино.
Собственно измерение напряжение на входах производится аналого-цифровым преобразователем (АЦП) с коммутатором на 6 каналов. АЦП имеет разрешение 10 бит, что соответствует коду на выходе преобразователя 0…1023. Погрешность измерения не более 2 единиц младшего разряда.
Для сохранения максимальной точности (10 разрядов) необходимо, чтобы внутреннее сопротивление источника сигнала не превышало 10 кОм. Это требование особенно важно при использовании резисторных делителей, подключенных к аналоговым входам платы. Сопротивление резисторов делителей не может быть слишком большим.
Программные функции аналогового ввода.
int analogRead(port)
Считывает значение напряжения на указанном аналоговом входе. Входное напряжение диапазона от 0 до уровня источника опорного напряжения (часто 5 В) преобразовывает в код от 0 до 1023.
При опорном напряжении равном 5 В разрешающая способность составляет 5 В / 1024 = 4,88 мВ.
Занимает на преобразование время примерно 100 мкс.
int inputCod; //- код входного напряжения
float inputVoltage; //- входное напряжение в В
inputCod= analogRead(A3); //- чтение напряжения на входе A3
inputVoltage= ( (float)inputCod * 5. / 1024. ); //- пересчет кода в напряжение (В)
void analogReference(type)
Задает опорное напряжение для АЦП. Оно определяет максимальное значение напряжения на аналоговом входе, которое АЦП может корректно преобразовать. Величина опорного напряжения также определяет коэффициент пересчета кода в напряжение:
Напряжение на входе = код АЦП * опорное напряжение / 1024.
Аргумент type может принимать следующие значения:
analogReference(INTERNAL); //- опорное напряжение равно 1,1 В
Рекомендуется внешний источник опорного напряжения подключать через токоограничительный резистор 5 кОм.
Двухканальный вольтметр на Ардуино.
В качестве примера использования функций аналогового ввода создадим проект простого цифрового вольтметра на Ардуино. Устройство должно измерять напряжения на двух аналоговых входах платы, и передавать измеренные значения на компьютер по последовательному порту. На примере этого проекта я покажу принципы создания простых систем измерения и сбора информации.
Решим, что вольтметр должен измерять напряжение в пределах не меньше 0…20 В и разработаем схему подключения входов вольтметра к плате Arduino UNO.
Если мы зададим опорное напряжение равным 5 В, то аналоговые входы платы будут измерять напряжение в пределах 0…5 В. А нам надо как минимум 0…20 В. Значит надо использовать делитель напряжения.
Напряжение на входе и выходе делителя связаны соотношением:
Uвыхода = ( Uвхода / (R1 + R2 )) * R2
Коэффициент передачи:
K = Uвыхода / Uвхода = R2 / ( R1 + R2 )
Нам необходим коэффициент передачи 1/4 ( 20 В * 1/4 = 5 В).
Для сохранения максимальной точности (10 разрядов) необходимо, чтобы внутреннее сопротивление источника сигнала не превышало 10 кОм. Поэтому выбираем резистор R2 равным 4,22 кОм. Рассчитываем сопротивление резистора R1.
0,25 = 4,22 / ( R1 + 4,22)
R1 = 4,22 / 0.25 – 4,22 = 12,66 кОм
У меня с ближайшим номиналом нашлись резисторы сопротивлением 15 кОм. С резисторами R1 = 15 кОм и R2 = 4,22 :
5 / (4,22 / (15 + 4,22)) = 22,77 В.
Схема вольтметра на базе Ардуино будет выглядит так.
Два делителя напряжения подключены к аналоговым входам A0 и A1. Конденсаторы C1 и C2 вместе с резисторами делителя образуют фильтры нижних частот, которые убирают из сигналов высокочастотные шумы.
Я собрал эту схему на макетной плате.
Первый вход вольтметра я подключил к регулируемому источнику питания, а второй к питанию 3,3 В платы Ардуино. Для контроля напряжения к первому входу я подключил вольтметр. Осталось написать программу.
Программа для измерения напряжения с помощью платы Ардуино.
Алгоритм простой. Надо:
Приведу скетч программы сразу полностью.
// программа измерения напряжения
//- на аналоговых входах A0 и A1
#include <MsTimer2.h>
#define MEASURE_PERIOD 500 //- время периода измерения
#define R1 15. //- сопротивление резистора R1
#define R2 4.22 //- сопротивление резистора R2
int timeCount; //- счетчик времени
float u1, u2; //- измеренные напряжения
void setup() {
Serial.begin(9600); //- инициализируем порт, скорость 9600
MsTimer2::set(1, timerInterupt); //- прерывания по таймеру, период 1 мс
MsTimer2::start(); //- разрешение прерывания
}
void loop() {
//- период 500 мс
if ( timeCount >= MEASURE_PERIOD ) {
timeCount= 0;
//- чтение кода канала 1 и пересчет в напряжение
u1= ((float)analogRead(A0)) * 5. / 1024. / R2 * (R1 + R2);
//- чтение кода канала 2 и пересчет в напряжение
u2= ((float)analogRead(A1)) * 5. / 1024. / R2 * (R1 + R2);
//- передача данных через последовательный порт
Serial.print("U1 = "); Serial.print(u1, 2);
Serial.print(" U2 = "); Serial.println(u2, 2);
}
}
// обработка прерывания 1 мс
void timerInterupt() {
timeCount++;
}
Поясню строчку, в которой пересчитывается код АЦП в напряжение:
// чтение кода канала 1 и пересчет в напряжение
u1= ((float)analogRead(A0)) * 5. / 1024. / R2 * (R1 + R2);
Загрузим программу в плату, запустим монитор последовательного порта.
Два бегущих столбика показывают значения измеренных напряжений. Все работает.
Измерение среднего значения сигнала.
Подключим первый канал нашего вольтметра к источнику напряжения с большим уровнем пульсаций. Увидим такую картину на мониторе.
Значения напряжения первого канала на экране монитора все время дергаются, скачут. А показания контрольного вольтметра вполне стабильны. Это объясняется тем, что контрольный вольтметр измеряет среднее значение сигнала, в то время как плата Ардуино считывает отдельные выборки каждые 500 мс. Естественно, момент чтения АЦП попадает в разные точки сигнала. А при высоком уровне пульсаций амплитуда в этих точках разная.
Кроме того, если считывать сигнал отдельными редкими выборками, то любая импульсная помеха может внести значительную ошибку в измерение.
Решение – сделать несколько частых выборок и усреднить измеренное значение. Для этого:
Задача из учебника математики 8 класса. Вот скетч программы, двух канального вольтметра среднего значения.
// программа измерения среднего напряжения
//- на аналоговых входах A0 и A1
#include <MsTimer2.h>
#define MEASURE_PERIOD 500 //- время периода измерения
#define R1 15. //- сопротивление резистора R1
#define R2 4.22 //- сопротивление резистора R2
int timeCount; //- счетчик времени
long sumU1, sumU2; //- переменные для суммирования кодов АЦП
long avarageU1, avarageU2; //- сумма кодов АЦП (среднее значение * 500)
boolean flagReady; //- признак готовности данных измерения
void setup() {
Serial.begin(9600); //- инициализируем порт, скорость 9600
MsTimer2::set(1, timerInterupt); //- прерывания по таймеру, период 1 мс
MsTimer2::start(); //- разрешение прерывания
}
void loop() {
if ( flagReady == true ) {
flagReady= false;
//- пересчет в напряжение и передача на компьютер
Serial.print("U1 = ");
Serial.print( (float)avarageU1 / 500. * 5. / 1024. / R2 * (R1 + R2), 2);
Serial.print(" U2 = ");
Serial.println( (float)avarageU2 / 500. * 5. / 1024. / R2 * (R1 + R2), 2);
}
}
// обработка прерывания 1 мс
void timerInterupt() {
timeCount++; //- +1 счетчик выборок усреднения
sumU1+= analogRead(A0); //- суммирование кодов АЦП
sumU2+= analogRead(A1); //- суммирование кодов АЦП
//- проверка числа выборок усреднения
if ( timeCount >= MEASURE_PERIOD ) {
timeCount= 0;
avarageU1= sumU1; //- перегрузка среднего значения
avarageU2= sumU2; //- перегрузка среднего значения
sumU1= 0;
sumU2= 0;
flagReady= true; //- признак результат измерений готов
}
}
В формулу пересчета кода АЦП в напряжение добавилось /500 – число выборок. Загружаем, запускаем монитор порта (Cntr+Shift+M).
Теперь, даже при значительном уровне пульсаций, показания меняются на сотые доли. Это только потому, что напряжение не стабилизировано.
Число выборок надо выбирать, учитывая:
Основным источником помех в аналоговых сигналах является сеть 50 Гц. Поэтому желательно выбирать время усреднения кратное 10 мс – времени полупериода сети частотой 50 Гц.
Оптимизация вычислений.
Вычисления с плавающей запятой просто пожирают ресурсы 8ми разрядного микроконтроллера. Любая операция с плавающей запятой требует денормализацию мантиссы, операцию с фиксированной запятой, нормализацию мантиссы, коррекцию порядка… И все операции с 32 разрядными числами. Поэтому необходимо свести к минимуму употребление вычислений с плавающей запятой. Как это сделать я расскажу в следующих уроках, но давайте хотя бы оптимизируем наши вычисления. Эффект будет значительный.
В нашей программе пересчет кода АЦП в напряжение записан так:
(float)avarageU1 / 500. * 5. / 1024. / R2 * (R1 + R2)
Сколько здесь вычислений, и все с плавающей запятой. А ведь большая часть вычислений – операции с константами. Часть строки:
/ 500. * 5. / 1024. / R2 * (R1 + R2)
мы можем расчитать на калькуляторе и заменить на одну константу. Тогда наши вычисления можно записать так:
(float)avarageU1 * 0.00004447756
Умные компиляторы сами распознают вычисления с константами и рассчитывать их на этапе компиляции. У меня возник вопрос, насколько умный компилятор Андруино. Решил проверить.
Я написал короткую программу. Она выполняет цикл из 10 000 проходов, а затем передает на компьютер время выполнения этих 10 000 циклов. Т.е. она позволяет увидеть время выполнения операций, размещенных в теле цикла.
// проверка оптимизации вычислений
int x= 876;
float y;
unsigned int count;
unsigned long timeCurrent, timePrev;
void setup() {
Serial.begin(9600);
}
void loop() {
count++;
//- y= (float)x / 500. * 5. / 1024. / 4.22 * (15. + 4.22);
//- y= (float)x * 0.00004447756;
if (count >= 10000) {
count= 0;
timeCurrent= millis();
Serial.println( timeCurrent - timePrev );
timePrev= timeCurrent;
}
}
В первом варианте, когда в цикле операции с плавающей запятой закомментированы и не выполняются, программа выдала результат 34 мс.
Т.е. 10 000 пустых циклов выполняются за 34 мс.
Затем я открыл строку:
y= (float)x / 500. * 5. / 1024. / 4.22 * (15. + 4.22);
повторяет наши вычисления. Результат 10 000 проходов за 922 мс или
( 922 – 34 ) / 10 000 = 88,8 мкс.
Т.е. эта строка вычислений с плавающей запятой требует на выполнение 89 мкс. Я думал будет больше.
Теперь я закрыл эту строку комментарием и открыл следующую, с умножением на заранее рассчитанную константу:
y= (float)x * 0.00004447756;
Результат 10 000 проходов за 166 мс или
( 166 – 34 ) / 10 000 = 13,2 мкс.
Потрясающий результат. Мы сэкономили 75,6 мкс на одной строке. Выполнили ее почти в 7 раз быстрее. У нас таких строк 2. Но ведь их в программе может быть и гораздо больше.
Вывод – вычисления с константами надо производить самим на калькуляторе и применять в программах как готовые коэффициенты. Компилятор Ардуино их на этапе компиляции не рассчитает. В нашем случае следует сделать так:
#define ADC_U_COEFF 0.00004447756 //- коэффициент перевода кода АЦП в напряжение
Serial.print( (float)avarageU1 * ADC_U_COEFF, 2);
Оптимальный по быстродействию вариант – это передать на компьютер код АЦП, а вместе с ним и все вычисления с плавающей запятой. При этом на компьютере принимать данные должна специализированная программа. Монитор порта из Arduino IDE не подойдет.
О других способах оптимизации программ Ардуино я буду рассказывать в будущих уроках по мере необходимости. Но без решения этого вопроса невозможно разрабатывать сложные программы на 8ми разрядном микроконтроллере.
В следующем уроке научимся работать с внутренним EEPROM, поговорим о контроле целостности данных.