АЦП #
Некоторые микроконтроллеры имеют на борту АЦП - аналогово-цифровой преобразователь (ADC, Analog-to-Digital Converter). Это устройство, позволяющее измерять напряжение и передавать его в программу, по сути - вольтметр. Аналоговое напряжение оцифровывается и превращается в численную величину. АЦП имеет два важных параметра:
- Опорное напряжение - максимальное напряжение, которое можно подавать на АЦП. Минимальное - ноль, GND
- Разрешение (разрядность) - точность, количество значений, с которой АЦП измеряет напряжение от 0 до опорного, измеряется в битах. Например 10 бит = 2^10 = 1024 значения, при опорном 5V получится точность (цена деления)
5 / 1024 = 0.0049
V
Пины #
АЦП, как и остальная периферия МК, выведен на пины. На каких именно пинах есть выводы АЦП можно посмотреть на распиновке, например - Arduino Nano (прямоугольники с зелёным фоном):
Arduino Nano имеет один 8-канальный 10-битный АЦП, соответственно 8 выводов на плате. На выбор есть несколько опорных напряжений, по умолчанию опорное напряжение равно напряжению питания МК.
analogRead #
Для измерения напряжения используется функция analogRead(пин)
, где пин
- номер пина. В каком виде передаётся номер пина лучше уточнить в описании к конкретной плате, например на Arduino Nano вызов
analogRead(0)
- номер канала АЦПanalogRead(A0)
- подпись на платеanalogRead(14)
- номер GPIO Arduino
Измерит напряжение на одном и том же пине - подписанном как A0
на плате.
Функция возвращает результат в виде числа, соответствующего напряжению на пине: значениям от 0 до опорного Вольт соответствуют числа от 0
до разрядность - 1
, т.е. при разрядности 10 бит максимальное значение будет 1024 - 1 = 1023
.
Нельзя подавать на АЦП напряжение ниже нуля или выше опорного. Даже если это произойдёт - АЦП выдаст значение по границе своего диапазона, т.е. результат не опустится ниже 0
и не поднимется выше max-1
На Arduino Nano АЦП работает независимо от GPIO, то есть прочитать сигнал можно даже если пин настроен как выход. Не подключайте ничего к плате и загрузите следующий скетч:
void setup() {
Serial.begin(115200);
pinMode(A0, OUTPUT);
}
void loop() {
digitalWrite(A0, LOW);
delay(1);
Serial.println(analogRead(A0));
delay(500);
digitalWrite(A0, HIGH);
delay(1);
Serial.println(analogRead(A0));
delay(500);
}
Откройте монитор порта - в нём побегут цифры 0
и 1023
. Мы подаём напряжение на пин, на всякий случай ждём 1 мс и выводим значение analogRead
в порт. Затем выключаем пин и выводим новое значение, и так по кругу. При низком сигнале получаем 0
, а при высоком - 5V - получаем 1023
, т.к. оно равно опорному напряжению, это максимальное значение. Таким образом, МК сам выдаёт и сам измеряет это напряжение на пине.
Вспомните предыдущий урок - никуда не подключенный пин в режиме входа ловит всякие случайные наводки из воздуха. Если его цифровое значение меняется случайным образом между 0
и 1
, значит напряжение на нём скачет довольно сильно! Загрузите следующий скетч, к пину A0
ничего не подключено:
void setup() {
Serial.begin(115200);
}
void loop() {
Serial.println(analogRead(A0));
}
Откройте плоттер графиков в Arduino IDE:
Вот они, наводки из сетевых проводов! Если подключить к пину провод, второй конец которого оставить висеть в воздухе - амплитуда сигнала увеличится:
Была 100
, а стала около 900
- провод выполняет роль антенны и в нём сильнее наводятся помехи
Скорость работы #
Функция analogRead
выполняется не мгновенно - она переключает канал АЦП на указанный, запускает преобразование и ждёт окончания процесса оцифровки, после чего возвращает результат. По умолчанию на Arduino Nano/Uno и подобных АЦП настроен так, что analogRead
выполняется около 120 мкс (микросекунд). Работая с АЦП напрямую, можно увеличить скорость оцифровки, а также получать данные по прерыванию, не ожидая окончания преобразования. Подробнее об этом будет рассказано в отдельном цикле уроков по МК AVR.
Также например у ESP8266 analogRead
не только выполняется медленно, но и мешает работе WiFi: если вызывать чтение слишком часто - WiFi просто не будет работать.
Потенциометр #
Потенциометр является очень простым и удобным способом ввода данных в программу. Близкий друг потенциометра - джойстик, состоит из двух потенциометров, по одному на каждую ось. Вспомним урок про потенциометр и подключим его на аналоговый пин как делитель напряжения:
Загрузите следующий скетч - он измеряет сигнал на A0
и просто выводит его в порт с задержкой:
void setup() {
Serial.begin(115200);
}
void loop() {
Serial.println(analogRead(A0));
delay(50);
}
Повращайте рукоятку от минимума до максимума - напряжение на пине будет меняться от 0 до 5V, соответственно сигнал получится от 0
до 1023
:
Следующий интересный эксперимент: выведем график одновременно аналогового и цифрового сигналов через digitalRead
с того же пина. Его домножим на 500, чтобы графики были одного размера:
void setup() {
Serial.begin(115200);
}
void loop() {
Serial.print(digitalRead(A0) * 500);
Serial.print(',');
Serial.println(analogRead(A0));
delay(20);
}
На этом графике видно, при каком значении digitalRead
начинает считать сигнал за высокий и низкий: высокий - чуть больше 500, низкий - чуть меньше 500.
Опорное напряжение #
Информация в данной главе относится к Arduino Nano/UNO/Mega, варианты опорного для других плат и МК см. в документации на конкретную модель
Опорное напряжение играет главную роль в измерении аналогового сигнала, потому что именно от него зависит максимальное измеряемое напряжение и вообще возможность и точность перевода полученного значения 0.. 1023 в Вольты. Настроить его можно при помощи функции analogReference(режим)
, где режим
:
DEFAULT
- опорное напряжение равно напряжению питания МК. Активно по умолчаниюINTERNAL
- встроенный источник опорного на 1.1V (ATmega168 или ATmega328P) и 2.56V (ATmega8)EXTERNAL
- опорным будет считаться напряжение, поданное на пин AREFINTERNAL1V1
- встроенный источник опорного на 1.1V (Arduino Mega)INTERNAL2V56
- встроенный источник опорного на 2.56V (Arduino Mega)
После изменения источника опорного напряжения (вызова analogReference
) первые несколько измерений могут быть нестабильными
Что касается точности: при питании от 5V и режиме DEFAULT
получится точность измерения напряжения 5 / 1024 = 4.9
мВ. Поставив INTERNAL
, мы можем измерять напряжение от 0 до 1.1V с точностью 1.1 / 1024 = 0.98
мВ. Весьма неплохо!
Что касается внешнего источника опорного напряжения: нельзя подавать отрицательное напряжение или выше 5.5V в качестве внешнего опорного в пин AREF. Также при подключении внешнего опорного напряжения нужно вызвать analogReference(EXTERNAL)
до первого вызова analogRead
, считая с запуска программы, иначе можно повредить микроконтроллер!
Чтобы "на лету" переключаться между внутренними и внешним опорными, можно подключить его на AREF через резистор на ~5 кОм. Вход AREF имеет собственное сопротивление в ~32 кОм, поэтому реальное опорное будет вычисляться по формуле REF = V * 32 / (R + 32)
, где R
- сопротивление резистора (кОм), через которое подключено опорное напряжение V
. Например для 2.5V получим 2.5 * 32 / (32 + 5) = ~2.2V реальное опорное.
Измерение напряжения #
Ниже опорного #
Для измерения напряжения ниже опорного достаточно просто подключить его к выводу АЦП и вызвать analogRead
. Конвертировать полученное значение в Вольты можно по следующей формуле: V = ADC * Vref / N
, где:
ADC
- значение с АЦПVref
- опорное напряжение, ВольтN
- разрешение АЦП в количестве значений,2^разрядность
или2^разрядность - 1
С последним пунктом есть тонкий момент - у разных МК АЦП устроены и работают по-разному. Например у AVR (Arduino Nano/UNO/Mega) АЦП считает максимальным значением Vref минус 1 бит по разрешению. Это означает, что в формуле делить нужно на 2^разрядность
, то есть на 1024
при 10 битах.
Из даташита на ATMega328p
У других МК может быть более логично - если Vref соответствует максимальному значению с АЦП, то делить нужно на 2^разрядность - 1
. Эту информацию можно найти в документации на конкретный МК.
АЦП может "шуметь" в пределах пары бит, поэтому каким значением выбирать максимальное - на самом деле не так уж важно
Давайте измерим напряжение пальчиковой и литиевой батарейки:
Не перепутайте +
и -
!!!
#define VREF 4.6 // опорное (напряжение питания)
void setup() {
Serial.begin(115200);
}
void loop() {
float v = analogRead(0) * VREF / 1024;
Serial.println(v);
delay(100);
}
У меня получилось 1.51 на пальчиковую и 3.12 на литиевую батарейку.
При питании платы от USB напряжение питания составляет около 4.6V, Vref по умолчанию равно напряжению питания - его мы и указали в скетче
Точность преобразования значения с АЦП в Вольты зависит от точности Vref, поэтому в программе выше получится приблизительный результат. Чтобы измерить напряжение точнее - можно измерить вольтметром напряжение питания платы между 5V и GND и внести это число в программу - это и будет точное Vref. Некоторые АЦП имеют калиброванный источник Vref, при помощи которого можно сразу измерять внешнее напряжение с хорошей точностью. Также высокоточные источники опорного напряжения существуют в виде отдельных микросхем - можно подключить такую микросхему как EXTERNAL
в пин AREF и получить ещё более точные измерения.
Выше опорного #
Для измерения напряжения выше Vref, например 12V аккумулятора от шуруповёрта или батарейки Крона, нужно использовать делитель напряжения на резисторах:
Как выбрать/рассчитать делитель?
- Напряжение с делителя на АЦП будет равно
Vin * R2 / (R1 + R2)
- нужно подобрать такие R1 и R2, чтобы это напряжение было близко к Vref при максимальном входном напряжении - Согласно даташиту на ATmega, сумма
R1 + R2
не рекомендуется больше 10 кОм для достижения наибольшей точности измерения. В то же время, через делитель на 10 кОм будет течь ощутимый ток, что критично при питании от батареи. Если схема работает от сети или от аккумулятора, но МК не используется в режиме сна - спокойно ставим делитель 10 кОм
Давайте измерим напряжение на 9V батарейке Крона. В качестве делителя можно использовать 3 резистора по 10 кОм (два в параллель дадут 5 кОм), такого делителя хватит на напряжение до 15V:
Для вычисления напряжения выведем формулу из двух уже известных формул:
Vadc = Vin * R2 / (R1 + R2)
Vadc = ADC * Vref / N
Vin = ADC * Vref * (R1 + R2) / R2 / N
И получим финальный скетч для измерения высокого напряжения (до 15V):
#define VREF 4.6
#define DIV_R1 10000
#define DIV_R2 5000
void setup() {
Serial.begin(115200);
}
void loop() {
float v = analogRead(0) * VREF * ((DIV_R1 + DIV_R2) / DIV_R2) / 1024;
Serial.println(v);
delay(100);
}
Делитель и энергосбережение #
Если схема работает от аккумулятора и МК "спит": пусть аккумулятор 12V, тогда через 10 кОм делитель пойдёт ток 1.2 мА. Сам микроконтроллер в режиме сна потребляет ~1 мкА, что в тысячу раз меньше! На самом деле можно взять делитель с гораздо бОльшим суммарным сопротивлением (но не больше 20 МОм, внутреннего сопротивления самого АЦП), но обязательно поставить конденсатор на ~0.1 мкФ между АЦП и GND (вот здесь проводили эксперимент). Таким образом например при при R1 + R2 = 10
МОм ток через делитель будет 1.2 мкА, что уже гораздо лучше!