Логотип

Если Вы хотите обучаться программированию микроконтроллеров, но попали, сюда не прочитав предыдущих уроков, то советую начать изучение материала с самого начала.

Программирование микроконтроллеров. Урок 20
АЦП.

Урок 20 (АЦП.)

На прошлом занятии мы с вами подключили к нашему МК часовой кварц и сделали электронные часы. А на этом уроке мы поработаем с Аналого-Цифровым Преобразователем и сделаем простенький вольтметр.

Программа часов, которую надо было сделать в качестве домашнего задания к прошлому уроку находится в приложенных файлах.


Тема преобразования аналоговых сигналов в цифру и обратно очень непростая и имеет массу неочевидных нюансов, которые могут сильно усложнить жизнь не только начинающему, но и опытному радиолюбителю. С другой стороны, у нас не стоит задача изучить ее досконально. Мы лишь посмотрим на вершину айсберга, а я постараюсь попроще изложить то, о чем знаю сам. А я, признаюсь, знаю не так уж много.

Микроконтроллер, являясь цифровым устройством, любое напряжение на своих выводах интерпретирует как 0 или 1, поэтому, для работы с аналоговым сигналом, ему требуется специальный блок, который мог бы переводить величину входного напряжения в цифровой код. Этим и занимается Аналого-Цифровой Преобразователь (АЦП). Обратную функцию, перевода цифрового кода в аналоговый сигнал выполняет Цифро-Аналоговый Преобразователь (ЦАП). Но это отдельная история.

Из прочитанного у Евстифеева вы уже должны знать о том, что почти все контроллеры имеют на своем борту многоканальный АЦП. Какие они вообще бывают и как работают можно почитать, например, здесь или поискать еще статьи в сети. Но если коротко и по-простому, то схема преобразования АЦП с помощью своего компаратора сравнивает величину напряжения на входе контроллера с данными ЦАП опорного напряжения. И делает это несколько раз приближаясь к измеряемой величине. После чего данные из схемы преобразования поступают в регистры данных АЦП и выставляется флаг «Преобразование АЦП завершено» (см. структурную схему модуля АЦП Евстифеев А.В. «Микроконтроллеры AVR семейства Tiny и Mega фирмы Atmel» 2008г. Страница 312). Отсюда вытекает первое ограничение. Величина исследуемого сигнала не должна превышать величину источника опорного напряжения (ИОН).

Очевидно, что чем стабильнее будет опорное напряжение, тем точней будет результат преобразования. У нашего контроллера есть три источника опорного напряжения:

  1. Внутренний ИОН - 2. 56V;
  2. Напряжение питания, подключенное к выводу AVcc;
  3. Наружный ИОН, подключенный к выходу AREF.

И вот мы уже имеем еще одно ограничение. Величина опорного напряжения не может быть выше напряжения питания МК. А из-за того, что вывод AREF подключен напрямую к ЦАПу схемы преобразования, то использовать в одном устройстве вместе с внешним ИОНом что-то еще не представляется возможным. Это третье ограничение.

При использовании в качестве ИОНа внутренний источник или напряжение питания на вывод AREF необходимо подключить небольшой конденсатор емкостью в 10…100 нф. для повышения надежности работы внутреннего ЦАПа. В качестве наружного ИОНа часто используют стабилизатор напряжения TL431 желательно с буквой (В). Он с очень хорошей точностью даст нам опорное напряжение в 2.5V с погрешностью - без буквы 2%; А – 1%; В – 0,5%. Схема подключения внешнего ИОНа представлена ниже.

Схема подключения внешнего ИОНа

Схема подключения внешнего ИОНа

Если в качестве ИОНа необходимо использовать напряжение питания, то подключение сильно упрощается. Может возникнуть резонный вопрос. Зачем нам внешний ИОН, когда есть внутренний? Ответ как всегда банален. Внутренний ИОН имеет большой разброс напряжения. Оно может гулять от контроллера к контроллеру от 2,4 до 2,7 вольт, а это в свою очередь, будет увеличивать погрешность измерений и требовать подстройки для каждого устройства, что в промышленных масштабах не гут.

За выбор источника опорного напряжения отвечают биты REFS1 и REFS0 регистра управления мультиплексором ADMUX. Давайте разберем этот регистр подробней. И так:
Биты 7 и 6 – REFS1 REFS0 – Определяют выбор ИОНа.
Бит 5 – ADLAR – выравнивает результат. (1) по левому краю, (0) по правому краю.
Бит 4 – в ATmega8 не используется.
Биты 3…0 – MUX3…MUX0 – Выбор входного канала.

Предлагаю поближе рассмотреть бит 5 (ADLAR). Т.к. МК имеет 10-ти битный АЦП, то его данные не могут уместиться в один байт. Поэтому для вывода результата преобразования в контроллере имеются два восьмибитных регистра (ADCH, ADCL). Поместив в них 10-ти битный результат мы получим еще 6 пустых бит которые останутся нулевыми. Так вот. Если бит ADLAR будет установлен в (0), то результат вычисления смещается в право и будет занимать весь младший байт ADCL и два младших бита регистра ADCH, а если ADLAR будет установлен в (1), то результат смещается в лево и будет занимать весь старший байт ADCH и два старших бита регистра ADCL. Резонно задать вопрос, нафига это надо? Дело в том, что если нам не нужна особая точность измерений, или схема имеет высокий уровень «шума», то можно обрезать несколько младших бит результата измерения. А это проще сделать выровнив результат по левому краю и считать только старший регистр ADCH. При этом два младших бита находящиеся в регистре ADCL не будут использованы.

Таблицу выбора источника опорного напряжения можно посмотреть в даташите или у Евстифеева А.В. «Микроконтроллеры AVR семейства Tiny и Mega фирмы Atmel» 2008г. На странице 320.

Что же касается MUX3…MUX0, то вы можете посмотреть таблицу входов либо в даташите, либо у Евстифеева А.В. «Микроконтроллеры AVR семейства Tiny и Mega фирмы Atmel» 2008г. На странице 318. Здесь ещё надо сказать и о том, что для настройки АЦП есть возможность программно подключить любой вход как к «земле» (GND) для калибровки нуля, так и к дополнительному ИОНу 1.22V.

Помимо регистра управления мультиплексором есть еще регистр управления и состояния АЦП (ADCSR). Это основной регистр, который определяет работу самого АЦП:
Бит 7 – ADEN – разрешение работы АЦП. (0) выключено, (1) включено;
Бит 6 – ADSC – при установке (1) в этот бит, АЦП начинает процесс одиночного преобразования. После окончания преобразования бит автоматически сбрасывается в (0);
Бит 5 – ADFR – выбор режима запуска АЦП. Если бит установлен в (0), то АЦП работает в режиме одиночного преобразования. При котором программа должна производить запуск каждого преобразования самостоятельно с помощью бита ADSC. Если бит установлен в (1), то АЦП работает в автоматическом режиме и каждое последующее преобразование будут запускаться самостоятельно после окончания предыдущего. Кстати в других МК запуск преобразования АЦП может осуществляться не только по команде пользователя или автоматически, но и по команде от других периферийных устройств, например, от аналогового компаратора или таймера;
Бит 4 – ADIF – флаг прерывания;
Бит 3 – ADIE – разрешение прерывания по окончанию преобразования АЦП. (0) не разрешено, (1) разрешено;
Биты 2…0 – ADPS2…ADPS0 – выбор частоты преобразования.

В этом регистре особое внимание хочу обратить на биты 2…0 (ADPS2…ADPS0) которые управляют частотой преобразования. Дело в том, что АЦП не работает на частоте МК. Для формирования тактовых импульсов модуля, установлен делитель, который понижает частоту работы АЦП. Это процесс необходим для получения наибольшей точности преобразования. Она достигается при тактовой частоте модуля в пределах от 50 до 200 кГц. Поэтому коэффициент деления необходимо выбирать таким образом, чтобы частота работы АЦП была в этом диапазоне. Например, у нас частота работы МК 8МГц. Для простоты вычисления мы делим эту частоту на 100 кГц и получаем коэффициент 80. Смотрим по таблице коэффициентов ближайший по значению - 64. Частота работы АЦП с этим коэффициентом будет 125 кГц, что в пределах рабочего диапазона. Таблицу коэффициентов деления предделителя АЦП можно посмотреть в даташите, либо у Евстифеева А.В. «Микроконтроллеры AVR семейства Tiny и Mega фирмы Atmel» 2008г. На странице 317.

Вот вроде и вся теоретическая часть. Про разрядность, абсолютную погрешность и интегральную нелинейность на мой взгляд толково и просто написано здесь. Но также можно поискать и другие публикации в сети.

Теперь перейдем к практической части. Во-первых, нам необходимо собрать вот такую схему.

Схема подключчения резисторов для АЦП

Схема подключчения резисторов для АЦП

Я бы посоветовал не разбирать схему прошлого урока, а просто добавить в неё пару резисторов. Вот так.

Схема для тестирования АЦП Схема для тестирования АЦП

Схема для тестирования АЦП

Схема для тестирования АЦП

После того как схема собрана необходимо написать небольшую программу, которая будет запускать обработку измерения АЦП и в начале просто выводить на экран значения, полученные от преобразователя. Чуть позже мы ее усовершенствуем, добавив расчет напряжения. Тестовая программа прилагается к уроку и находятся в папке (Leson_20) (Ссылка). Но для начала разберем блок настроек программы в общем и настройку АЦП, в частности.

    #include                        		 //appnote
    #include "lib_macro_1.5.c"               	 //Общие макросы
//внутренние макросы
    //позиция отображения
    #define LED_HOUR_Y      1                	 //Y координата цифры
    #define LED_HOUR_FERST  18               	 //Х координата цифры
    #define PLATE_POINT     0                	 //место точки при отображении
    //работа АЦП
    #define RUN_ADC (ADCSRA |= (1<<ADSC))    	 //выключаем преобразование АЦП
    #define KOEF 300                         	 //коэффициент пересчета (показания АЦП на контрольном напряжении)
    #define CON_VOLTAG  50                   	 //Контрольное напряжение, умноженное на 10 (не более 6 вольт)
//глобальные переменные
    register unsigned char f_acp = UP;           //Флаг завершения преобразования АЦП
//Библиотеки, процедуры, функции и прерывания
    #include "Lib_Nokia_5110_ATmega.c"           //Библиотека работы с дисплеем
    #include "Lib_Nokia_number_24x15_1.04.c"     //Библиотека символов и подпрограмма их вывода
    interrupt [ADC_INT] void adc_int_interrapt (void);  //Прерывание по ADC_INT преобразование АЦП завершено
void main(void){
//переменные
    unsigned int d_adc_h = CLEAN;                 //данные из старшего регистра АЦП
    unsigned char d_adc_l = CLEAN;                //данные из младшего регистра АЦП
//Настройки МК
    OSCCAL = 0x9F;                                //Подстройка частоты внутреннего генератора
    SPL = 0x5F; SPH = 0x04;                       //Указатель программного стека в конце RAM
    ACSR_OFF;                                     //Компаратор выключен
//Настраиваем АЦП
    ADMUX   |= (1<<REFS1)|(1<<REFS0);               //Внутренний ИОН
    ADMUX   |= (0<<MUX3)|(0<<MUX2)|(1<<MUX1)|(0<<MUX0); //Выбрать канал ADC2
    ADCSRA  |= (1<<ADPS2)|(1<<ADPS1)|(0<<ADPS0);    //Делитель частоты 64
    ADCSRA  |= (1<<ADIE);                           //Разрешение прерывание АЦП
    ADCSRA  |= (1<<ADEN);                           //Разрешить работу АЦП 1 вкл
//другие настройки
    lcd_init();                                     //инициализация дисплея
    LED_ON;                                         //включение подсветки
    SEI;                                            //разрешить общие прерывания
//END Настройки

Как всегда, в начале настроек МК необходимо подстроить рабочую частоту и установить указатель программного стека в конец RAM. После чего мы выключим компаратор для того, чтобы не мешался. Настраиваем АЦП на работу с одним входом в режиме одиночных преобразований, а после переходим к инициализации дисплея и в конце разрешаем глобальные прерывания. Необходимо отметить то, что АЦП многоканальный и в случае получения данных с нескольких каналов, выбор каждого из них необходимо будет производить непосредственно перед запуском преобразования АЦП. Также перед началом преобразования можно устанавливать и другие настройки, например, менять источник опорного напряжения со внутреннего ИОНа на Vcc и управлять выравниванием результата вычисления.

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

while (1) {
    if (f_acp) {                                //если флаг АЦП поднят
        d_adc_l = ADCL;                         //данные из младшего регистра
        d_adc_h = ADCH;                         //данные из старшего регистра
        d_adc_h = (d_adc_h << 8);               //сдвигаем данные в лево
        d_adc_h += d_adc_l;                     //добавляем данные из младшего регистра
        f_acp = DOWN;                           //опустить флаг конца преобразования АЦП
        RUN_ADC;                                //запускаем новое преобразование
        ind_numer_24x15_3(d_adc_h, LED_HOUR_FERST, LED_HOUR_Y , PLATE_POINT); //индикация нового значения
    };
}} //END void main(void)

    //Прерывание по ADC_INT преобразование АЦП завершено
    interrupt [ADC_INT] void adc_int_interrapt (void) {
        f_acp = UP;                             //Поднять флаг окончания преобразования АЦП
    };
//END programm

Если обработка данных АЦП может подождать и не критична ко времени, то лучше не нагружать прерывание, а просто выставить в нём флаг, который в момент прохождения общей программы «зацепится» за обработчик. Т.к. АЦП достаточно «медленный», то можно организовать программу таким образом, чтобы параллельно с преобразованием выполнялась еще и полезная работа. Но здесь есть нюанс. Т.к. в процессе работы контроллер сильно «шумит», то результаты работы АЦП могут быть не точными. Разработчики рекомендуют для выполнения особо ответственных преобразований погружать МК в «спящий» режим "ADC Noise Reduction" при котором остаются в работе только сторожевой таймер, внешние прерывания, модуль TWI, да и сам АЦП. О режимах пониженного энергопотребления можно почитать: Евстифеева А.В. «Микроконтроллеры AVR семейства Tiny и Mega фирмы Atmel» 2008г. Страницы 210…216.

После того как вы скомпилируете и зальете эту программу, у вас должно получиться вот так.

На видео хорошо видно, как у меня меняются показания АЦП, когда я поворачиваю регулировочный винт переменного резистора. Если все сделано правильно резисторы не имеют больших отклонений от номиналов, то выкрутив резистор на максимальное сопротивление и подключив вывод резистора R4 к шине питания +5V мы должны получить на экране цифру близкую к 999. Потому как наш резистивный делитель напряжения, подключенный к шине 5 вольт, будет давать на вход МК примерно 2.5V, а внутренний ИОН имеет значение 1023 при 2.56V.

Теперь необходимо доработать программу так, чтобы она нам показывала не абстрактные значения, вынутые из регистров АЦП, а понятное нам измеряемое напряжение.

Из принципа работы АЦП понятно, что полученные данные каким-то образом взаимосвязаны с измеряемым напряжением, а так как эта цифра, то мы можем подключить известный нам математический аппарат для вычислений и с его помощью рассчитать значения напряжения с «любой» точностью. По этому поводу написаны множество толстенных книг и защищено уйма диссертаций. Но так как я не ставлю целью защитить еще одну, то расскажу о самых простых способах переводить показания АЦП в нужные нам параметры. Безусловно это не самые лучшие и точные способы, но и они тоже находят своё применение.

Первый способ самый простой, относительно точный, но затратный как с точки зрения занимаемой памяти МК, так и с точки зрения потраченного времени. Решаем проблему перевода данных из АЦП в напряжение прямо в лоб. Составляем матрицу напряжений, например, от 0 до 10 вольт с заданной точностью в 0,1V и измеряя вольтметром каждое табличное напряжение смотрим какое число выдает нам АЦП на выходе и подставляем его в нашу матрицу. Она будет иметь 100 значений, а значит все 10 бит АЦП нам использовать не надо. Мы можем преспокойно обойтись только одним байтом отбросив два младших бита. При этом матрица будет занимать около 200 байт. Вроде и не так много памяти и точность неплохая и простота налицо. Но это всего 100 значений, а если их 1000, то матрица будет занимать уже 4 Кбайта. Необходимо также учитывать и потраченное время на составление такой таблицы. Короче такой способ имеет место быть только для устройств где не требуется много значений и цена их расчета (время и объем памяти, затраченные на расчет величины) не сопоставима велика по отношению к затраченным ячейкам памяти. Хороший пример - измерение заряда аккумулятора, состоящего из трех - четырех черточек на экранной батарейке. В таких случаях и матрицу не надо организовывать, достаточно просто нескольких ступенек if else if…

С другой стороны, если способ с матрицей не прокатывает, то можно применить аппроксимацию и вот тут-то начинаются математические дебри, в которые мне влезать не хочется потому как я и сам в этом деле не шибко разбираюсь. И все же.

Если подойти к вопросу расчетов без фантазии, то можно вполне обойтись пропорцией:

y = kx/c
Где: у – искомое напряжение; х – показания АЦП; k – контрольное напряжение; c – показания АЦП контрольного напряжения.

Как вы увидите дальше, эта формула дает совсем невысокую точность измерения. Для того чтобы настроить работу вольтметра по этой формуле необходимо перед индикацией вставить следующую строку:

d_adc_h = (d_adc_h * CON_VOLTAG) / KOEF;//формула для пересчета в напряжение

Установить CON_VOLTAG 50, KOEF 300, а PLATE_POINT 2. После чего подключаем наш вольтметр к шине 5 вольт и крутя регулировочный винт резистора добиваемся чтобы на экране у нас было отображено напряжение 5.0 (вольт). После чего можно пользоваться вольтметром. Я проверял им напряжения на шине 3.3V, батарейку 1.5V и 9V. Результаты были ожидаемы. О точности в 0,1V говорить не приходится от слова совсем. Но для оценки напряжения пользоваться можно.

Если же заморочиться более точными преобразованиями, то необходимо хотя бы полистать по этому поводу какую-нибудь серьезную литературу, чтобы оценить глубину проблемы и необходимость полного погружения.

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

Принципиальный график перевода одних величин в другие

Принципиальный график перевода одних величин в другие

Именно кривой, а не прямой т.к. прямую можно считать частным случаем кривой. Чтобы описать эту кривую используют разный математический аппарат, который делает это с известной точностью. Для серьезных преобразований используют полиномы 3-го ... 5-го порядка которые на заданном отрезке могут построить кривую с очень высокой точностью. Но для нас подойдут и более приземленные формулы, которыми также можно описывать процесс. На сайте planetcalc есть калькулятор аппроксимации функции одной переменной. С его помощью, применив предлагаемый мат. аппарат, можно выбрать варианты аппроксимаций и рассчитать необходимые коэффициенты.

Самая «простая» из аппроксимаций - линейная. Она имеет общий вид:

y=kx+c
где: у – искомое напряжение; х – показания АЦП; k и c – коэффициенты.

Всё вроде просто. Подставляй вместо (х) показания АЦП и получай на выходе искомое напряжение. Но для того чтобы подобрать необходимые коэффициенты надо определить ряд точек, провести измерение напряжений в этих точках и записать показания АЦП. Возьмем диапазон напряжений от 0 до 12 вольт. Для этого обязательно понадобится мультиметр (вольтметр). Без него не обойтись. Также необходимо достать/сделать лабораторный блок питания либо набрать ряд разных источников напряжения (1.5 V пальчиковая батарейка, 3.3 и 5 V шины питания макетки, 9 V «Крона» и 12 V аккумулятор из ИБП). Чем больше точек будет определено, тем точнее будут коэффициенты. Итак, корректируем сопротивление переменного резистора таким образом, чтобы АЦП мог охватить весь диапазон измеряемых напряжений и проводим измерения, записывая показания АЦП при разных значениях напряжения. Заводим данные в предлагаемые формы (не забываем про ноль) и рассчитываем коэффициенты. После чего необходимо разложить понравившуюся формулу на элементы и записать эти элементы в нашу программу. Не рекомендую записывать преобразование со степенями в одну строку и обязательно смотрите, особенно если используете регрессии со степенями, чтобы результат выполняемого действия не вылезал за пределы емкости типа переменной. Напомню, что переменная типа (int) может вместить в себя максимальное число 65 535.

Также у вас может возникнуть вопрос о том, как мы, пользуясь только целыми числами, можем отображать числа с точкой. Это не проблема. Вы, наверное, обратили внимание на то, что в своей простой формуле пересчета я использовал контрольное напряжение не 5 вольт, а 50. Это как раз для того, чтобы расчет производился с учетом десятых долей. А точку для отображения я установил также с учетом того, что число должно отображаться с дробной частью. Просто, когда будете вбивать данные в таблицы для пересчета умножайте показания напряжения на вольтметре на 10 и записывайте в таблицу только целую часть. Например, не 1.5V, а 15, не 3.3V, а 33 и т.д.


Провести такое исследование и сделать вольтметр более точным и будет вашим домашним заданием.


На следующим уроке мы поработаем с энергонезависимой памятью (ЕЕPROM). Для этого надо почитать: Евстифеев А.В. «Микроконтроллеры AVR семейства Tiny и Mega фирмы Atmel» 2008г. Страницы 189…193.


А на сегодня всё. Удачи.


25.02.20



Если вдруг найдете в статье неточности или заблуждения. Напишите мне об этом. Я подправлю.



Приложение:
Файлы к уроку 20